Add "Cancel Operation" functionality for Edge Gateways with UI, API, and state management integration.
This commit is contained in:
@@ -724,15 +724,25 @@ function mergeEdgeGatewayFixtureRecord(baseGateway, overrides = {}) {
|
||||
function createFixtureOperation(type, request = {}, overrides = {}) {
|
||||
const startedAt = overrides.started_at || null;
|
||||
const completedAt = overrides.completed_at || null;
|
||||
const status = overrides.status || "PENDING";
|
||||
return {
|
||||
id: overrides.id || Date.now(),
|
||||
type,
|
||||
status: overrides.status || "PENDING",
|
||||
status,
|
||||
request,
|
||||
summary: overrides.summary || {
|
||||
label: overrides.status === "COMPLETED" ? "Completed" : overrides.status === "FAILED" ? "Failed" : "Queued",
|
||||
progress: overrides.status === "COMPLETED" ? 100 : overrides.status === "FAILED" ? 100 : 0,
|
||||
retryable: overrides.status !== "FAILED",
|
||||
label:
|
||||
status === "COMPLETED"
|
||||
? "Completed"
|
||||
: status === "FAILED"
|
||||
? "Failed"
|
||||
: status === "CANCELLED"
|
||||
? "Cancelled"
|
||||
: status === "CANCEL_REQUESTED"
|
||||
? "Cancellation requested"
|
||||
: "Queued",
|
||||
progress: ["COMPLETED", "FAILED"].includes(status) ? 100 : 0,
|
||||
retryable: status !== "FAILED",
|
||||
},
|
||||
result: overrides.result || {},
|
||||
error_code: overrides.error_code || null,
|
||||
@@ -752,6 +762,11 @@ function buildEdgeGatewayOperationSummary(operations = []) {
|
||||
const status = String(operation.status || "PENDING");
|
||||
if (status === "PENDING") summary.pending += 1;
|
||||
if (status === "IN_PROGRESS") summary.in_progress += 1;
|
||||
if (status === "CANCEL_REQUESTED") summary.cancel_requested += 1;
|
||||
if (status === "CANCELLED") {
|
||||
summary.cancelled += 1;
|
||||
summary.latest_cancelled_at = summary.latest_cancelled_at || operation.completed_at || null;
|
||||
}
|
||||
if (status === "COMPLETED") {
|
||||
summary.completed += 1;
|
||||
summary.latest_completed_at = summary.latest_completed_at || operation.completed_at || null;
|
||||
@@ -770,8 +785,11 @@ function buildEdgeGatewayOperationSummary(operations = []) {
|
||||
total: operations.length,
|
||||
pending: 0,
|
||||
in_progress: 0,
|
||||
cancel_requested: 0,
|
||||
cancelled: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
latest_cancelled_at: null,
|
||||
latest_completed_at: null,
|
||||
latest_failed_at: null,
|
||||
latest_type: null,
|
||||
@@ -806,7 +824,7 @@ function buildEdgeGatewayRuntimeFixture(gateway) {
|
||||
const cloudRelays = relayHealth.filter((relay) => relay.execution_path === "cloud");
|
||||
const operations = Array.isArray(gateway.operations) ? gateway.operations : [];
|
||||
const activeOperation =
|
||||
operations.find((operation) => ["PENDING", "IN_PROGRESS"].includes(String(operation.status))) || null;
|
||||
operations.find((operation) => ["PENDING", "IN_PROGRESS", "CANCEL_REQUESTED"].includes(String(operation.status))) || null;
|
||||
const versionDrift =
|
||||
gateway.installed_version && gateway.target_version && gateway.installed_version !== gateway.target_version;
|
||||
const credentialFreshnessState = gateway.metadata?.credentials_rotated_at ? "FRESH" : "UNKNOWN";
|
||||
@@ -829,10 +847,14 @@ function buildEdgeGatewayRuntimeFixture(gateway) {
|
||||
const containerHealth = gateway.container_health ||
|
||||
gateway.metadata?.container_health || {
|
||||
state: gateway.status === "OFFLINE" ? "OFFLINE" : "ONLINE",
|
||||
summary: gateway.status === "OFFLINE" ? "0/2 containers healthy" : "2/2 containers healthy",
|
||||
summary: gateway.status === "OFFLINE" ? "0/6 containers healthy" : "6/6 containers healthy",
|
||||
services: [
|
||||
{ name: "edge-agent", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
{ name: "lan-worker", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
{ name: "redis", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
{ name: "mariadb", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
{ name: "minio", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
{ name: "auto-updater", status: gateway.status === "OFFLINE" ? "offline" : "healthy" },
|
||||
],
|
||||
};
|
||||
const outboxStatus = gateway.outbox_status ||
|
||||
@@ -931,7 +953,7 @@ function buildEdgeGatewayRuntimeFixture(gateway) {
|
||||
gateway.error_state ||
|
||||
(diagnostics[0]
|
||||
? { ...diagnostics[0] }
|
||||
: activeOperation?.error_code
|
||||
: activeOperation?.error_code && !["CANCEL_REQUESTED", "CANCELLED"].includes(String(activeOperation?.status || ""))
|
||||
? { code: activeOperation.error_code, message: activeOperation.error_message }
|
||||
: null),
|
||||
};
|
||||
@@ -2926,6 +2948,74 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/\/edge-gateways\/\d+\/operations\/\d+\/cancel$/.test(pathname) && method === "POST") {
|
||||
const gatewayId = extractMatchId(/\/edge-gateways\/(\d+)\/operations\/\d+\/cancel$/);
|
||||
const operationId = extractMatchId(/\/operations\/(\d+)\/cancel$/);
|
||||
const gateway = findGateway(gatewayId);
|
||||
const now = toSqlDateTime();
|
||||
|
||||
if (!gateway) {
|
||||
await route.fulfill(json({ message: "Gateway not found" }, 404));
|
||||
return true;
|
||||
}
|
||||
|
||||
const operation = (gateway.operations || []).find((entry) => Number(entry.id) === Number(operationId)) || null;
|
||||
if (!operation) {
|
||||
await route.fulfill(json({ message: "Operation not found" }, 404));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation.status === "IN_PROGRESS" || operation.status === "CANCEL_REQUESTED") {
|
||||
operation.status = "CANCELLED";
|
||||
operation.completed_at = now;
|
||||
operation.updated_at = now;
|
||||
operation.error_code = "EDGE_GATEWAY_CANCELLED";
|
||||
operation.error_message = "Operation cancelled by operator";
|
||||
operation.summary = {
|
||||
...(operation.summary || {}),
|
||||
label: "Cancelled",
|
||||
retryable: true,
|
||||
};
|
||||
operation.events = [
|
||||
...(operation.events || []),
|
||||
{
|
||||
id: edgeGatewayFixture.nextOperationEventId++,
|
||||
level: "WARNING",
|
||||
code: "OPERATION_CANCELLED",
|
||||
message: "Operation cancelled by operator",
|
||||
created_at: now,
|
||||
},
|
||||
];
|
||||
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
|
||||
} else if (operation.status === "PENDING") {
|
||||
operation.status = "CANCELLED";
|
||||
operation.completed_at = now;
|
||||
operation.updated_at = now;
|
||||
operation.error_code = "EDGE_GATEWAY_CANCELLED";
|
||||
operation.error_message = "Operation cancelled by operator";
|
||||
operation.summary = {
|
||||
...(operation.summary || {}),
|
||||
label: "Cancelled",
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
|
||||
gateway.audit_logs = [
|
||||
{ id: Date.now(), created_at: now, action: "GATEWAY_OPERATION_CANCELLED", actor_type: "USER" },
|
||||
...(gateway.audit_logs || []),
|
||||
];
|
||||
Object.assign(gateway, buildEdgeGatewayRuntimeFixture(gateway));
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
operation: cloneJson(operation),
|
||||
gateway: buildHttpEdgeGatewayGateway(edgeGatewayFixture, gateway, true),
|
||||
},
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/\/edge-gateways\/\d+\/operations$/.test(pathname) && method === "POST") {
|
||||
const gatewayId = extractMatchId(/\/edge-gateways\/(\d+)\/operations$/);
|
||||
const gateway = findGateway(gatewayId);
|
||||
@@ -2935,6 +3025,24 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeOperation = (gateway.operations || []).find((entry) =>
|
||||
["PENDING", "IN_PROGRESS", "CANCEL_REQUESTED"].includes(String(entry.status || ""))
|
||||
);
|
||||
if (activeOperation) {
|
||||
await route.fulfill(
|
||||
json(
|
||||
{
|
||||
data: {
|
||||
message: "Another gateway operation is already active",
|
||||
error_code: "EDGE_GATEWAY_CONFLICT",
|
||||
},
|
||||
},
|
||||
409
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const operationType = String(body.type || "").toUpperCase();
|
||||
const operationRequest = body.request || {};
|
||||
const now = toSqlDateTime();
|
||||
|
||||
Reference in New Issue
Block a user