Преглед на файлове

web: add functionality to copy existing build configuration in add-build page

Shiv Tyagi преди 1 месец
родител
ревизия
463f7ec7b4
променени са 7 файла, в които са добавени 156 реда и са изтрити 23 реда
  1. 2 0
      web/schemas/__init__.py
  2. 12 3
      web/schemas/builds.py
  3. 7 2
      web/services/builds.py
  4. 121 14
      web/static/js/add_build.js
  5. 5 2
      web/static/js/index.js
  6. 6 0
      web/templates/add_build.html
  7. 3 2
      web/ui/router.py

+ 2 - 0
web/schemas/__init__.py

@@ -12,6 +12,7 @@ from .admin import (
 
 # Build schemas
 from .builds import (
+    BuildVersionInfo,
     RemoteInfo,
     BuildProgress,
     BuildRequest,
@@ -36,6 +37,7 @@ __all__ = [
     # Admin
     "RefreshRemotesResponse",
     # Builds
+    "BuildVersionInfo",
     "RemoteInfo",
     "BuildProgress",
     "BuildRequest",

+ 12 - 3
web/schemas/builds.py

@@ -43,15 +43,24 @@ class BuildSubmitResponse(BaseModel):
     )
 
 
+# --- Build Version Info ---
+class BuildVersionInfo(BaseModel):
+    """Version information for a build."""
+    id: str = Field(..., description="Version ID used for this build")
+    remote_info: RemoteInfo = Field(
+        ..., description="Source repository information"
+    )
+    git_hash: str = Field(..., description="Git commit hash used for build")
+
+
 # --- Build Output ---
 class BuildOut(BaseModel):
     """Complete build information output schema."""
     build_id: str = Field(..., description="Unique build identifier")
     vehicle: VehicleBase = Field(..., description="Target vehicle information")
     board: BoardBase = Field(..., description="Target board information")
-    git_hash: str = Field(..., description="Git commit hash used for build")
-    remote_info: RemoteInfo = Field(
-        ..., description="Source repository information"
+    version: BuildVersionInfo = Field(
+        ..., description="Version information for this build"
     )
     selected_features: List[str] = Field(
         default_factory=list,

+ 7 - 2
web/services/builds.py

@@ -12,6 +12,7 @@ from schemas import (
     BuildOut,
     BuildProgress,
     RemoteInfo,
+    BuildVersionInfo,
 )
 from schemas.vehicles import VehicleBase, BoardBase
 
@@ -139,6 +140,7 @@ class BuildsService:
         # Create build info
         build_info = build_manager.BuildInfo(
             vehicle_id=vehicle_id,
+            version_id=build_request.version_id,
             remote_info=remote_info,
             git_hash=git_hash,
             board=board_name,
@@ -370,8 +372,11 @@ class BuildsService:
                 id=build_info.board,
                 name=build_info.board  # Board name is same as board ID for now
             ),
-            git_hash=build_info.git_hash,
-            remote_info=remote_info,
+            version=BuildVersionInfo(
+                id=build_info.version_id,
+                remote_info=remote_info,
+                git_hash=build_info.git_hash
+            ),
             selected_features=selected_feature_labels,
             progress=progress,
             time_created=build_info.time_created,

+ 121 - 14
web/static/js/add_build.js

@@ -87,7 +87,7 @@ const Features = (() => {
         });
     }
 
-    function handleOptionStateChange(feature_id, triggered_by_ui) {
+    function handleOptionStateChange(feature_id, triggered_by_ui, updateDependencies = true) {
         // feature_id is the feature ID from the API
         let element = document.getElementById(feature_id);
         if (!element) return;
@@ -97,13 +97,17 @@ const Features = (() => {
         
         if (element.checked) {
             selected_options += 1;
-            enableDependenciesForFeature(feature.id);
+            if (updateDependencies) {
+                enableDependenciesForFeature(feature.id);
+            }
         } else {
             selected_options -= 1;
-            if (triggered_by_ui) {
-                askToDisableDependentsForFeature(feature.id);
-            } else {
-                disabledDependentsForFeature(feature.id);
+            if (updateDependencies) {
+                if (triggered_by_ui) {
+                    askToDisableDependentsForFeature(feature.id);
+                } else {
+                    disabledDependentsForFeature(feature.id);
+                }
             }
         }
 
@@ -262,7 +266,7 @@ const Features = (() => {
         });
     }
 
-    function checkUncheckOptionById(id, check) {
+    function checkUncheckOptionById(id, check, updateDependencies = true) {
         let feature = getOptionById(id);
         if (!feature) return;
         
@@ -273,7 +277,7 @@ const Features = (() => {
         }
         element.checked = check;
         const triggered_by_ui = false;
-        handleOptionStateChange(feature.id, triggered_by_ui);
+        handleOptionStateChange(feature.id, triggered_by_ui, updateDependencies);
     }
 
     function checkUncheckAll(check) {
@@ -288,15 +292,77 @@ const Features = (() => {
         });
     }
 
-    return {reset, handleOptionStateChange, getCategoryIdByName, applyDefaults, checkUncheckAll, checkUncheckCategory, getOptionById};
+    return {reset, handleOptionStateChange, getCategoryIdByName, applyDefaults, checkUncheckAll, checkUncheckCategory, getOptionById, checkUncheckOptionById};
 })();
 
 var init_categories_expanded = false;
 
-function init() {
+var rebuildConfig = {
+    vehicleId: null,
+    versionId: null,
+    boardId: null,
+    selectedFeatures: [],
+    isRebuildMode: false
+};
+
+async function init() {
+    if (typeof rebuildFromBuildId !== 'undefined') {
+        await initRebuild(rebuildFromBuildId);
+    }
+    
     fetchVehicles();
 }
 
+async function initRebuild(buildId) {
+    try {
+        const buildResponse = await fetch(`/api/v1/builds/${buildId}`);
+        if (!buildResponse.ok) {
+            throw new Error('Failed to fetch build details');
+        }
+        const buildData = await buildResponse.json();
+        
+        if (!buildData.vehicle || !buildData.vehicle.id) {
+            throw new Error('Vehicle information is missing from the build');
+        }
+        if (!buildData.version || !buildData.version.id) {
+            throw new Error('Version information is missing from the build');
+        }
+        if (!buildData.board || !buildData.board.id) {
+            throw new Error('Board information is missing from the build');
+        }
+        
+        rebuildConfig.vehicleId = buildData.vehicle.id;
+        rebuildConfig.versionId = buildData.version.id;
+        rebuildConfig.boardId = buildData.board.id;
+        rebuildConfig.selectedFeatures = buildData.selected_features || [];
+        rebuildConfig.isRebuildMode = true;
+        
+    } catch (error) {
+        console.error('Error loading rebuild configuration:', error);
+        alert('Failed to load build configuration: ' + error.message + '\n\nRedirecting to new build page...');
+        window.location.href = '/add_build';
+        throw error;
+    }
+}
+
+function applyRebuildFeatures(featuresList) {
+    Features.checkUncheckAll(false);
+    
+    if (featuresList && featuresList.length > 0) {
+        featuresList.forEach(featureId => {
+            Features.checkUncheckOptionById(featureId, true, false);
+        });
+    }
+}
+
+function clearRebuildConfig() {
+    rebuildConfig.vehicleId = null;
+    rebuildConfig.versionId = null;
+    rebuildConfig.boardId = null;
+    rebuildConfig.selectedFeatures = [];
+    rebuildConfig.isRebuildMode = false;
+}
+
 // enables or disables the elements with ids passed as an array
 // if enable is true, the elements are enabled and vice-versa
 function enableDisableElementsById(ids, enable) {
@@ -330,7 +396,19 @@ function fetchVehicles() {
     sendAjaxRequestForJsonResponse(request_url)
         .then((json_response) => {
             let all_vehicles = json_response;
-            let new_vehicle = all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter": all_vehicles[0].id;
+            
+            if (rebuildConfig.vehicleId) {
+                const vehicleExists = all_vehicles.some(v => v.id === rebuildConfig.vehicleId);
+                if (!vehicleExists) {
+                    console.warn(`Rebuild vehicle '${rebuildConfig.vehicleId}' not found in available vehicles`);
+                    alert(`Warning: The vehicle from the original build is no longer available.\n\nRedirecting to new build page...`);
+                    window.location.href = '/add_build';
+                    return;
+                }
+            }
+            
+            let new_vehicle = rebuildConfig.vehicleId || 
+                             (all_vehicles.find(vehicle => vehicle.name === "Copter") ? "copter" : all_vehicles[0].id);
             updateVehicles(all_vehicles, new_vehicle);
         })
         .catch((message) => {
@@ -360,7 +438,18 @@ function onVehicleChange(new_vehicle_id) {
         .then((json_response) => {
             let all_versions = json_response;
             all_versions = sortVersions(all_versions);
-            const new_version = all_versions[0].id;
+            
+            if (rebuildConfig.versionId) {
+                const versionExists = all_versions.some(v => v.id === rebuildConfig.versionId);
+                if (!versionExists) {
+                    console.warn(`Rebuild version '${rebuildConfig.versionId}' not found for vehicle '${new_vehicle_id}'`);
+                    alert(`Warning: The version from the original build is no longer available.\n\nRedirecting to new build page...`);
+                    window.location.href = '/add_build';
+                    return;
+                }
+            }
+            
+            const new_version = rebuildConfig.versionId || all_versions[0].id;
             updateVersions(all_versions, new_version);
         })
         .catch((message) => {
@@ -405,7 +494,18 @@ function onVersionChange(new_version) {
         .then((boards_response) => {
             // Keep full board objects with id and name
             let boards = boards_response;
-            let new_board = boards.length > 0 ? boards[0].id : null;
+            
+            if (rebuildConfig.boardId) {
+                const boardExists = boards.some(b => b.id === rebuildConfig.boardId);
+                if (!boardExists) {
+                    console.warn(`Rebuild board '${rebuildConfig.boardId}' not found for version '${version_id}'`);
+                    alert(`Warning: The board from the original build is no longer available.\n\nRedirecting to new build page...`);
+                    window.location.href = '/add_build';
+                    return;
+                }
+            }
+            
+            let new_board = rebuildConfig.boardId || (boards.length > 0 ? boards[0].id : null);
             updateBoards(boards, new_board);
         })
         .catch((message) => {
@@ -443,7 +543,14 @@ function onBoardChange(new_board) {
         .then((features_response) => {
             Features.reset(features_response);
             fillBuildOptions(features_response);
-            Features.applyDefaults();
+            
+            // TODO: Refactor to use a single method to apply both rebuild and default features
+            if (rebuildConfig.isRebuildMode) {
+                applyRebuildFeatures(rebuildConfig.selectedFeatures);
+                clearRebuildConfig();
+            } else {
+                Features.applyDefaults();
+            }
         })
         .catch((message) => {
             console.log("Features update failed. "+message);

+ 5 - 2
web/static/js/index.js

@@ -81,7 +81,7 @@ function updateBuildsTable(builds) {
         table_body_html +=  `<tr>
                                 <td class="align-middle"><span class="badge text-bg-${status_color}">${build_info['progress']['state']}</span></td>
                                 <td class="align-middle">${build_age}</td>
-                                <td class="align-middle"><a href="https://github.com/ArduPilot/ardupilot/commit/${build_info['git_hash']}">${build_info['git_hash'].substring(0,8)}</a></td>
+                                <td class="align-middle"><a href="https://github.com/ArduPilot/ardupilot/commit/${build_info['version']['git_hash']}">${build_info['version']['git_hash'].substring(0,8)}</a></td>
                                 <td class="align-middle">${build_info['board']['name']}</td>
                                 <td class="align-middle">${build_info['vehicle']['name']}</td>
                                 <td class="align-middle" id="${row_num}_features">
@@ -101,6 +101,9 @@ function updateBuildsTable(builds) {
                                     <button class="btn btn-md btn-outline-${download_button_color} m-1 tooltip-button" data-bs-toggle="tooltip" data-bs-animation="false" data-bs-title="Download build artifacts" id="${build_info['build_id']}-download-btn" onclick="window.location.href='/api/v1/builds/${build_info['build_id']}/artifact';" ${downloadDisabled}>
                                         <i class="bi bi-download"></i>
                                     </button>
+                                    <button class="btn btn-md btn-outline-primary m-1 tooltip-button" data-bs-toggle="tooltip" data-bs-animation="false" data-bs-title="Copy and re-build" onclick="window.location.href='/add_build?rebuild_from=${build_info['build_id']}';">
+                                        <i class="bi bi-arrow-clockwise"></i>
+                                    </button>
                                 </td>
                             </tr>`;
         row_num += 1;
@@ -115,7 +118,7 @@ function updateBuildsTable(builds) {
                                 <th scope="col" style="width: 5%">Vehicle</th>
                                 <th scope="col">Features</th>
                                 <th scope="col" style="width: 15%">Progress</th>
-                                <th scope="col" style="width: 15%">Actions</th>
+                                <th scope="col" style="width: 18%">Actions</th>
                             </thead>
                             <tbody>${table_body_html}</tbody>
                         </table>`;

+ 6 - 0
web/templates/add_build.html

@@ -123,6 +123,12 @@
         </div>
     </div>
   
+    {% if rebuild_from != None %}
+    <script>
+        var rebuildFromBuildId = '{{rebuild_from}}';
+    </script>
+    {% endif %}
+    
     <script type="text/javascript" src="/static/js/add_build.js"></script>
 </body>
 </html>

+ 3 - 2
web/ui/router.py

@@ -33,17 +33,18 @@ async def index(request: Request, build_id: str = None):
 
 
 @router.get("/add_build", response_class=HTMLResponse)
-async def add_build(request: Request):
+async def add_build(request: Request, rebuild_from: str = None):
     """
     Render the add build page for creating new firmware builds.
 
     Args:
         request: FastAPI Request object
+        rebuild_from: Optional build ID to copy configuration from
 
     Returns:
         Rendered HTML template
     """
     return templates.TemplateResponse(
         "add_build.html",
-        {"request": request}
+        {"request": request, "rebuild_from": rebuild_from}
     )