Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def to_dict(self) -> dict:
class BuildInfo:
def __init__(self,
vehicle_id: str,
version_id: str,
remote_info: RemoteInfo,
git_hash: str,
board: str,
Expand All @@ -56,13 +57,15 @@ def __init__(self,

Parameters:
vehicle_id (str): The vehicle ID associated with the build.
version_id (str): The version ID associated with the build.
remote_info (RemoteInfo): The remote repository containing the
source commit to build on.
git_hash (str): The git commit hash to build on.
board (str): Board to build for.
selected_features (set): Set of features selected for the build.
"""
self.vehicle_id = vehicle_id
self.version_id = version_id
self.remote_info = remote_info
self.git_hash = git_hash
self.board = board
Expand All @@ -77,6 +80,7 @@ def __init__(self,
def to_dict(self) -> dict:
return {
'vehicle_id': self.vehicle_id,
'version_id': self.version_id,
'remote_info': self.remote_info.to_dict(),
'git_hash': self.git_hash,
'board': self.board,
Expand Down
2 changes: 2 additions & 0 deletions web/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Build schemas
from .builds import (
BuildVersionInfo,
RemoteInfo,
BuildProgress,
BuildRequest,
Expand All @@ -36,6 +37,7 @@
# Admin
"RefreshRemotesResponse",
# Builds
"BuildVersionInfo",
"RemoteInfo",
"BuildProgress",
"BuildRequest",
Expand Down
15 changes: 12 additions & 3 deletions web/schemas/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions web/services/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BuildOut,
BuildProgress,
RemoteInfo,
BuildVersionInfo,
)
from schemas.vehicles import VehicleBase, BoardBase

Expand Down Expand Up @@ -139,6 +140,7 @@ def create_build(
# 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,
Expand Down Expand Up @@ -370,8 +372,11 @@ def _build_info_to_output(
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,
Expand Down
135 changes: 121 additions & 14 deletions web/static/js/add_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -262,7 +266,7 @@ const Features = (() => {
});
}

function checkUncheckOptionById(id, check) {
function checkUncheckOptionById(id, check, updateDependencies = true) {
let feature = getOptionById(id);
if (!feature) return;

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions web/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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;
Expand All @@ -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>`;
Expand Down
6 changes: 6 additions & 0 deletions web/templates/add_build.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ <h5 class="modal-title" id="dependencyCheckModalLabel">Attention!</h5>
</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>
5 changes: 3 additions & 2 deletions web/ui/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)