|
|
|
@@ -6,6 +6,9 @@ import {
|
|
|
|
|
createEdgeGatewayOperation,
|
|
|
|
|
deleteEdgeGateway,
|
|
|
|
|
getEdgeGateway,
|
|
|
|
|
getEdgeGatewayInstallTokenStatus,
|
|
|
|
|
getEdgeGatewayLogs,
|
|
|
|
|
getEdgeGatewayStatistics,
|
|
|
|
|
listEdgeGatewayDepartments,
|
|
|
|
|
listEdgeGateways,
|
|
|
|
|
peekCachedEdgeGateway,
|
|
|
|
@@ -16,11 +19,15 @@ import {
|
|
|
|
|
setDepartmentGatewayCutover,
|
|
|
|
|
updateEdgeGateway,
|
|
|
|
|
} from "@/services/edgeGateways.js";
|
|
|
|
|
import { createGatewayShellClient } from "@/features/edgeGateways/edgeGatewayLiveSessions.js";
|
|
|
|
|
import { normalizeEdgeGatewayError } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
|
|
|
|
import EdgeGatewayOverviewPage from "@/features/edgeGateways/EdgeGatewayOverviewPage.vue";
|
|
|
|
|
import EdgeGatewayInventoryPage from "@/features/edgeGateways/EdgeGatewayInventoryPage.vue";
|
|
|
|
|
import EdgeGatewayOperationsPage from "@/features/edgeGateways/EdgeGatewayOperationsPage.vue";
|
|
|
|
|
import EdgeGatewayManagePage from "@/features/edgeGateways/EdgeGatewayManagePage.vue";
|
|
|
|
|
import EdgeGatewayTasksPage from "@/features/edgeGateways/EdgeGatewayTasksPage.vue";
|
|
|
|
|
import EdgeGatewayLogsPage from "@/features/edgeGateways/EdgeGatewayLogsPage.vue";
|
|
|
|
|
import EdgeGatewayStatisticsPage from "@/features/edgeGateways/EdgeGatewayStatisticsPage.vue";
|
|
|
|
|
import EdgeGatewayTerminalPage from "@/features/edgeGateways/EdgeGatewayTerminalPage.vue";
|
|
|
|
|
import EdgeGatewaySettingsPage from "@/features/edgeGateways/EdgeGatewaySettingsPage.vue";
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
departmentId: { type: Number, default: null },
|
|
|
|
@@ -87,44 +94,88 @@ const fleetFilter = ref("ALL");
|
|
|
|
|
const fleetUsage = ref(createEmptyFleetUsage());
|
|
|
|
|
const rotateBundle = ref(null);
|
|
|
|
|
const editableBindings = ref([]);
|
|
|
|
|
const metadataForm = ref({ label: "", is_primary: false });
|
|
|
|
|
const transportModeDraft = ref("gateway");
|
|
|
|
|
const rotateConfirmation = ref("");
|
|
|
|
|
const deleteConfirmation = ref("");
|
|
|
|
|
const installerStatus = ref(null);
|
|
|
|
|
const installerDiagnosticsCopyState = ref("idle");
|
|
|
|
|
const selectedOperationId = ref(null);
|
|
|
|
|
const updateTargetVersion = ref("");
|
|
|
|
|
const logsTimeline = ref([]);
|
|
|
|
|
const gatewayShellSessions = ref([]);
|
|
|
|
|
const statisticsSnapshot = ref({});
|
|
|
|
|
const streamStatus = ref("idle");
|
|
|
|
|
const streamError = ref("");
|
|
|
|
|
const terminalStatus = ref("idle");
|
|
|
|
|
const terminalError = ref("");
|
|
|
|
|
const terminalTranscript = ref("");
|
|
|
|
|
const terminalCommand = ref("");
|
|
|
|
|
const terminalSession = ref(null);
|
|
|
|
|
const loading = ref({
|
|
|
|
|
init: false,
|
|
|
|
|
detail: false,
|
|
|
|
|
installer: false,
|
|
|
|
|
discovery: false,
|
|
|
|
|
bindings: false,
|
|
|
|
|
logs: false,
|
|
|
|
|
metadata: false,
|
|
|
|
|
statistics: false,
|
|
|
|
|
cutover: false,
|
|
|
|
|
rotate: false,
|
|
|
|
|
delete: false,
|
|
|
|
|
operation: false,
|
|
|
|
|
operationCancel: false,
|
|
|
|
|
terminal: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let autoRefreshHandle = null;
|
|
|
|
|
let lastFleetRefreshAt = 0;
|
|
|
|
|
let shellClient = null;
|
|
|
|
|
let shellGatewayId = null;
|
|
|
|
|
|
|
|
|
|
const tabs = computed(() =>
|
|
|
|
|
props.allowDestructive
|
|
|
|
|
? [
|
|
|
|
|
{ id: "overview", label: "Overview" },
|
|
|
|
|
{ id: "tasks", label: "Tasks" },
|
|
|
|
|
{ id: "logs", label: "Logs" },
|
|
|
|
|
{ id: "statistics", label: "Statistics" },
|
|
|
|
|
{ id: "terminal", label: "Terminal" },
|
|
|
|
|
{ id: "inventory", label: "Inventory" },
|
|
|
|
|
{ id: "operations", label: "Operations" },
|
|
|
|
|
{ id: "manage", label: "Manage" },
|
|
|
|
|
{ id: "settings", label: "Settings" },
|
|
|
|
|
]
|
|
|
|
|
: [
|
|
|
|
|
{ id: "overview", label: "Overview" },
|
|
|
|
|
{ id: "tasks", label: "Tasks" },
|
|
|
|
|
{ id: "logs", label: "Logs" },
|
|
|
|
|
{ id: "statistics", label: "Statistics" },
|
|
|
|
|
{ id: "inventory", label: "Inventory" },
|
|
|
|
|
{ id: "operations", label: "Operations" },
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const currentView = computed(() => (props.routeDriven ? props.activeView : localView.value));
|
|
|
|
|
const currentView = computed(() => {
|
|
|
|
|
const nextView = props.routeDriven ? props.activeView : localView.value;
|
|
|
|
|
if (nextView === "operations") {
|
|
|
|
|
return "tasks";
|
|
|
|
|
}
|
|
|
|
|
if (nextView === "manage") {
|
|
|
|
|
return "settings";
|
|
|
|
|
}
|
|
|
|
|
return nextView;
|
|
|
|
|
});
|
|
|
|
|
const hasGateway = computed(() => Boolean(selectedGateway.value));
|
|
|
|
|
const showFleetLanding = computed(() => !hasGateway.value && !unavailableGatewayId.value);
|
|
|
|
|
const installerCopyLabel = computed(() =>
|
|
|
|
|
installerCopyState.value === "copied" ? "Copied" : installerCopyState.value === "error" ? "Retry" : "Copy command"
|
|
|
|
|
);
|
|
|
|
|
const installerDiagnosticsCopyLabel = computed(() =>
|
|
|
|
|
installerDiagnosticsCopyState.value === "copied"
|
|
|
|
|
? "Copied"
|
|
|
|
|
: installerDiagnosticsCopyState.value === "error"
|
|
|
|
|
? "Retry"
|
|
|
|
|
: "Copy diagnostics"
|
|
|
|
|
);
|
|
|
|
|
const selectedDepartmentId = computed(() => props.departmentId ?? selectedGatewayView.value?.department_id ?? null);
|
|
|
|
|
const errorDetailsText = computed(() => (errorState.value?.details || []).join("\n"));
|
|
|
|
|
|
|
|
|
@@ -136,6 +187,19 @@ const tone = (status) => ({ ONLINE: "success", DEGRADED: "warning", OFFLINE: "da
|
|
|
|
|
const statusLabel = (status) => ({ ONLINE: "Online", DEGRADED: "Needs attention", OFFLINE: "Offline" }[status] || "Unknown");
|
|
|
|
|
const discoveryLabel = (status) => ({ READY: "Ready", STALE: "Stale", PENDING: "Pending", FAILED: "Failed" }[status] || "Unknown");
|
|
|
|
|
const transportLabel = (mode) => ({ gateway: "Gateway", cloud: "Cloud" }[mode] || mode || "Unknown");
|
|
|
|
|
const installerStateLabel = (status) =>
|
|
|
|
|
({ PENDING: "Pending", RUNNING: "Running", FAILED: "Failed", COMPLETED: "Completed" }[
|
|
|
|
|
String(status || "").toUpperCase()
|
|
|
|
|
] || "Pending");
|
|
|
|
|
const installerStepLabel = (step) =>
|
|
|
|
|
({
|
|
|
|
|
VERIFY_TOKEN: "Verifying token",
|
|
|
|
|
INSTALL_PACKAGES: "Installing packages",
|
|
|
|
|
DOWNLOAD_ARTIFACTS: "Downloading artifacts",
|
|
|
|
|
WRITE_CONFIG: "Writing config",
|
|
|
|
|
START_STACK: "Starting stack",
|
|
|
|
|
WAIT_FOR_CLAIM: "Waiting for claim",
|
|
|
|
|
}[String(step || "").toUpperCase()] || "Waiting to start");
|
|
|
|
|
const deptName = (id) => departments.value.find((item) => Number(item.id) === Number(id))?.name || `Department ${id}`;
|
|
|
|
|
const metricText = (value, suffix = "") => (Number.isFinite(Number(value)) ? `${Number(value)}${suffix}` : "No data");
|
|
|
|
|
const normalizeFleetUsage = (value) => {
|
|
|
|
@@ -302,6 +366,30 @@ const view = (gateway) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectedGatewayView = computed(() => view(selectedGateway.value));
|
|
|
|
|
const gatewayOperations = computed(() =>
|
|
|
|
|
Array.isArray(selectedGatewayView.value?.operations) ? selectedGatewayView.value.operations : []
|
|
|
|
|
);
|
|
|
|
|
const recentCommands = computed(() =>
|
|
|
|
|
Array.isArray(selectedGatewayView.value?.recent_commands) ? selectedGatewayView.value.recent_commands : []
|
|
|
|
|
);
|
|
|
|
|
const operationSummary = computed(() => selectedGatewayView.value?.recent_operations_summary || {});
|
|
|
|
|
const selectedOperation = computed(
|
|
|
|
|
() => gatewayOperations.value.find((operation) => String(operation.id) === String(selectedOperationId.value)) || null
|
|
|
|
|
);
|
|
|
|
|
const selectedOperationEvents = computed(() =>
|
|
|
|
|
Array.isArray(selectedOperation.value?.events) ? selectedOperation.value.events : []
|
|
|
|
|
);
|
|
|
|
|
const installerStatusView = computed(() => {
|
|
|
|
|
if (!installerStatus.value) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...installerStatus.value,
|
|
|
|
|
stateLabel: installerStateLabel(installerStatus.value.status),
|
|
|
|
|
stepLabel: installerStepLabel(installerStatus.value.step),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const summaryCards = computed(() => {
|
|
|
|
|
const rows = gateways.value.map(view).filter(Boolean);
|
|
|
|
@@ -385,6 +473,116 @@ const syncDrafts = (gateway) => {
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildLogsTimelineFromGateway = (gateway) => {
|
|
|
|
|
const auditLogs = Array.isArray(gateway?.audit_logs) ? gateway.audit_logs : [];
|
|
|
|
|
const logEntries = Array.isArray(gateway?.log_entries) ? gateway.log_entries : [];
|
|
|
|
|
const operations = Array.isArray(gateway?.operations) ? gateway.operations : [];
|
|
|
|
|
const operationEvents = operations.flatMap((operation) =>
|
|
|
|
|
(operation?.events || []).map((entry) => ({
|
|
|
|
|
type: "operation_event",
|
|
|
|
|
level: entry.level || "INFO",
|
|
|
|
|
message: entry.message || entry.code || "",
|
|
|
|
|
created_at: entry.created_at || null,
|
|
|
|
|
entry: {
|
|
|
|
|
...entry,
|
|
|
|
|
operation_id: operation.id,
|
|
|
|
|
operation_type: operation.type,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...auditLogs.map((entry) => ({
|
|
|
|
|
type: "audit",
|
|
|
|
|
level: entry.severity || "INFO",
|
|
|
|
|
message: entry.action || "AUDIT_EVENT",
|
|
|
|
|
created_at: entry.created_at || null,
|
|
|
|
|
entry,
|
|
|
|
|
})),
|
|
|
|
|
...logEntries.map((entry) => ({
|
|
|
|
|
type: "log",
|
|
|
|
|
level: entry.level || "INFO",
|
|
|
|
|
message: entry.message || "",
|
|
|
|
|
created_at: entry.created_at || null,
|
|
|
|
|
entry,
|
|
|
|
|
})),
|
|
|
|
|
...operationEvents,
|
|
|
|
|
].sort((left, right) => String(right.created_at || "").localeCompare(String(left.created_at || "")));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildStatisticsSnapshotFromGateway = (gateway) => ({
|
|
|
|
|
system_metrics: gateway?.metadata?.system_metrics || gateway?.agent_runtime?.system_metrics || {},
|
|
|
|
|
backlog_depth: gateway?.backlog_depth || {},
|
|
|
|
|
channel_status: gateway?.channel_status || {},
|
|
|
|
|
transport_health: gateway?.transport_health || {},
|
|
|
|
|
version_drift:
|
|
|
|
|
gateway?.version_drift || {
|
|
|
|
|
installed_version: gateway?.installed_version || null,
|
|
|
|
|
target_version: gateway?.target_version || null,
|
|
|
|
|
is_drifted:
|
|
|
|
|
Boolean(gateway?.installed_version) &&
|
|
|
|
|
Boolean(gateway?.target_version) &&
|
|
|
|
|
String(gateway.installed_version) !== String(gateway.target_version),
|
|
|
|
|
},
|
|
|
|
|
fleet_usage: buildFleetUsageFromRows(gateway ? [gateway] : []),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const resetGatewayViewState = () => {
|
|
|
|
|
rotateBundle.value = null;
|
|
|
|
|
rotateConfirmation.value = "";
|
|
|
|
|
deleteConfirmation.value = "";
|
|
|
|
|
selectedOperationId.value = null;
|
|
|
|
|
updateTargetVersion.value = "";
|
|
|
|
|
logsTimeline.value = [];
|
|
|
|
|
gatewayShellSessions.value = [];
|
|
|
|
|
statisticsSnapshot.value = {};
|
|
|
|
|
streamStatus.value = "idle";
|
|
|
|
|
streamError.value = "";
|
|
|
|
|
terminalStatus.value = "idle";
|
|
|
|
|
terminalError.value = "";
|
|
|
|
|
terminalTranscript.value = "";
|
|
|
|
|
terminalCommand.value = "";
|
|
|
|
|
terminalSession.value = null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const syncGatewayState = (gateway, { resetViewState = false } = {}) => {
|
|
|
|
|
if (!gateway) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (resetViewState) {
|
|
|
|
|
resetGatewayViewState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
syncDrafts(gateway);
|
|
|
|
|
metadataForm.value = {
|
|
|
|
|
label: String(gateway.label || ""),
|
|
|
|
|
is_primary: Boolean(gateway.is_primary),
|
|
|
|
|
};
|
|
|
|
|
transportModeDraft.value = gateway.department_transport_mode || "gateway";
|
|
|
|
|
if (!updateTargetVersion.value) {
|
|
|
|
|
updateTargetVersion.value = String(gateway.target_version || "");
|
|
|
|
|
}
|
|
|
|
|
if (!logsTimeline.value.length) {
|
|
|
|
|
logsTimeline.value = buildLogsTimelineFromGateway(gateway);
|
|
|
|
|
}
|
|
|
|
|
if (!Object.keys(statisticsSnapshot.value || {}).length) {
|
|
|
|
|
statisticsSnapshot.value = buildStatisticsSnapshotFromGateway(gateway);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!selectedOperationId.value ||
|
|
|
|
|
!gatewayOperations.value.some((operation) => String(operation.id) === String(selectedOperationId.value))
|
|
|
|
|
) {
|
|
|
|
|
selectedOperationId.value = String(gateway.active_operation?.id || gatewayOperations.value[0]?.id || "");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatInstallerDiagnostics = () =>
|
|
|
|
|
(installerStatusView.value?.diagnostics || [])
|
|
|
|
|
.map((entry) => `${entry.name || "Diagnostic"}\n${entry.output || "No output"}`)
|
|
|
|
|
.join("\n\n");
|
|
|
|
|
|
|
|
|
|
const clearError = () => {
|
|
|
|
|
errorState.value = null;
|
|
|
|
|
};
|
|
|
|
@@ -392,6 +590,7 @@ const clearError = () => {
|
|
|
|
|
const scheduleInstallerCopyReset = () => {
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
installerCopyState.value = "idle";
|
|
|
|
|
installerDiagnosticsCopyState.value = "idle";
|
|
|
|
|
}, 1800);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -504,9 +703,19 @@ const mergeGateway = (gateway) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clearSelection = () => {
|
|
|
|
|
if (shellClient) {
|
|
|
|
|
try {
|
|
|
|
|
shellClient.close();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
// Best effort.
|
|
|
|
|
}
|
|
|
|
|
shellClient = null;
|
|
|
|
|
shellGatewayId = null;
|
|
|
|
|
}
|
|
|
|
|
selectedGateway.value = null;
|
|
|
|
|
activeGatewayId.value = null;
|
|
|
|
|
unavailableGatewayId.value = null;
|
|
|
|
|
resetGatewayViewState();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setSelectedGatewaySnapshot = (gateway) => {
|
|
|
|
@@ -514,11 +723,21 @@ const setSelectedGatewaySnapshot = (gateway) => {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousGatewayId = normId(selectedGateway.value?.id);
|
|
|
|
|
const mergedGateway = mergeGateway(gateway) || gateway;
|
|
|
|
|
if (previousGatewayId && previousGatewayId !== String(mergedGateway.id) && shellClient) {
|
|
|
|
|
try {
|
|
|
|
|
shellClient.close();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
// Best effort.
|
|
|
|
|
}
|
|
|
|
|
shellClient = null;
|
|
|
|
|
shellGatewayId = null;
|
|
|
|
|
}
|
|
|
|
|
selectedGateway.value = mergedGateway;
|
|
|
|
|
activeGatewayId.value = String(mergedGateway.id);
|
|
|
|
|
unavailableGatewayId.value = null;
|
|
|
|
|
syncDrafts(mergedGateway);
|
|
|
|
|
syncGatewayState(mergedGateway, { resetViewState: previousGatewayId !== String(mergedGateway.id) });
|
|
|
|
|
return mergedGateway;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -776,6 +995,31 @@ const pollForClaimedGateway = async ({ existingIds, baseline, departmentId, labe
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pollInstallerStatus = async (claimTokenId, pollToken) => {
|
|
|
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
|
|
|
if (installerClaimPollToken.value !== pollToken) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await getEdgeGatewayInstallTokenStatus(claimTokenId);
|
|
|
|
|
installerStatus.value = unwrap(response, null);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
["FAILED", "COMPLETED"].includes(String(installerStatus.value?.status || "").toUpperCase()) ||
|
|
|
|
|
installerStatus.value?.gateway_id
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
fail(error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await sleep(1500);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generateInstaller = async () => {
|
|
|
|
|
const departmentId = Number(installerDepartmentId.value || selectedDepartmentId.value || 0);
|
|
|
|
|
if (!departmentId) {
|
|
|
|
@@ -785,16 +1029,29 @@ const generateInstaller = async () => {
|
|
|
|
|
|
|
|
|
|
loading.value.installer = true;
|
|
|
|
|
installerCopyState.value = "idle";
|
|
|
|
|
installerDiagnosticsCopyState.value = "idle";
|
|
|
|
|
try {
|
|
|
|
|
const label = installerLabel.value.trim();
|
|
|
|
|
const pollToken = installerClaimPollToken.value + 1;
|
|
|
|
|
const existingIds = new Set(gateways.value.map((item) => Number(item.id)));
|
|
|
|
|
const baseline = buildGatewayClaimBaseline(gateways.value);
|
|
|
|
|
const response = await createEdgeGatewayInstallToken({ department_id: departmentId, label: label || undefined });
|
|
|
|
|
const installToken = unwrap(response, null);
|
|
|
|
|
installerClaimPollToken.value = pollToken;
|
|
|
|
|
installerCommand.value = unwrap(response, null)?.install_command || "";
|
|
|
|
|
installerCommand.value = installToken?.install_command || "";
|
|
|
|
|
installerStatus.value = {
|
|
|
|
|
claim_token_id: installToken?.claim_token_id ?? null,
|
|
|
|
|
status: "PENDING",
|
|
|
|
|
step: null,
|
|
|
|
|
message: "Waiting for the installer to start.",
|
|
|
|
|
diagnostics: [],
|
|
|
|
|
last_error: null,
|
|
|
|
|
};
|
|
|
|
|
flashMessage.value = "Installer token generated.";
|
|
|
|
|
clearError();
|
|
|
|
|
if (installToken?.claim_token_id) {
|
|
|
|
|
pollInstallerStatus(installToken.claim_token_id, pollToken).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
pollForClaimedGateway({ existingIds, baseline, departmentId, label, pollToken }).catch(() => {});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
fail(error);
|
|
|
|
@@ -824,11 +1081,38 @@ const copyInstallerCommand = async () => {
|
|
|
|
|
scheduleInstallerCopyReset();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveMetadata = async (payload) => {
|
|
|
|
|
const copyInstallerDiagnostics = async () => {
|
|
|
|
|
if (!formatInstallerDiagnostics()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const copied = await copyPlainText(formatInstallerDiagnostics());
|
|
|
|
|
installerDiagnosticsCopyState.value = copied ? "copied" : "error";
|
|
|
|
|
flashMessage.value = copied ? "Installer diagnostics copied." : "Could not copy installer diagnostics.";
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
installerDiagnosticsCopyState.value = "error";
|
|
|
|
|
flashMessage.value = "Could not copy installer diagnostics.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scheduleInstallerCopyReset();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateMetadataField = (field, value) => {
|
|
|
|
|
metadataForm.value = {
|
|
|
|
|
...metadataForm.value,
|
|
|
|
|
[field]: field === "is_primary" ? Boolean(value) : value,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveMetadata = async () => {
|
|
|
|
|
if (!selectedGateway.value?.id) return;
|
|
|
|
|
loading.value.metadata = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await updateEdgeGateway(selectedGateway.value.id, payload);
|
|
|
|
|
const response = await updateEdgeGateway(selectedGateway.value.id, {
|
|
|
|
|
label: String(metadataForm.value.label || "").trim(),
|
|
|
|
|
is_primary: Boolean(metadataForm.value.is_primary),
|
|
|
|
|
});
|
|
|
|
|
const gateway = unwrap(response, null);
|
|
|
|
|
if (gateway) {
|
|
|
|
|
setSelectedGatewaySnapshot(gateway);
|
|
|
|
@@ -842,12 +1126,16 @@ const saveMetadata = async (payload) => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const applyCutover = async (transportMode) => {
|
|
|
|
|
const updateTransportMode = (value) => {
|
|
|
|
|
transportModeDraft.value = value || "gateway";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveTransportMode = async () => {
|
|
|
|
|
if (!selectedDepartmentId.value) return;
|
|
|
|
|
loading.value.cutover = true;
|
|
|
|
|
try {
|
|
|
|
|
await setDepartmentGatewayCutover(selectedDepartmentId.value, transportMode);
|
|
|
|
|
applyDepartmentTransportModeLocally(selectedDepartmentId.value, transportMode);
|
|
|
|
|
await setDepartmentGatewayCutover(selectedDepartmentId.value, transportModeDraft.value);
|
|
|
|
|
applyDepartmentTransportModeLocally(selectedDepartmentId.value, transportModeDraft.value);
|
|
|
|
|
flashMessage.value = "Department cutover updated.";
|
|
|
|
|
clearError();
|
|
|
|
|
} catch (error) {
|
|
|
|
@@ -857,6 +1145,14 @@ const applyCutover = async (transportMode) => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateRotateConfirmation = (value) => {
|
|
|
|
|
rotateConfirmation.value = String(value || "");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateDeleteConfirmation = (value) => {
|
|
|
|
|
deleteConfirmation.value = String(value || "");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const queueOperation = async ({ type, request }) => {
|
|
|
|
|
if (!selectedGateway.value?.id) return;
|
|
|
|
|
loading.value.operation = true;
|
|
|
|
@@ -865,6 +1161,7 @@ const queueOperation = async ({ type, request }) => {
|
|
|
|
|
const gateway = unwrap(response, null)?.gateway || response?.data?.data?.gateway || null;
|
|
|
|
|
if (gateway) {
|
|
|
|
|
setSelectedGatewaySnapshot(gateway);
|
|
|
|
|
selectedOperationId.value = String(gateway.active_operation?.id || gateway.operations?.[0]?.id || "");
|
|
|
|
|
}
|
|
|
|
|
flashMessage.value = `Gateway operation ${type} queued.`;
|
|
|
|
|
clearError();
|
|
|
|
@@ -875,6 +1172,23 @@ const queueOperation = async ({ type, request }) => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const queueUpdate = async () => {
|
|
|
|
|
await queueOperation({
|
|
|
|
|
type: "UPDATE",
|
|
|
|
|
request: {
|
|
|
|
|
target_version: String(updateTargetVersion.value || selectedGateway.value?.target_version || "").trim(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateTargetVersionDraft = (value) => {
|
|
|
|
|
updateTargetVersion.value = String(value || "");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const queueUninstall = async () => {
|
|
|
|
|
await queueOperation({ type: "UNINSTALL", request: {} });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cancelOperation = async (operation) => {
|
|
|
|
|
if (!selectedGateway.value?.id || !operation?.id) return;
|
|
|
|
|
loading.value.operationCancel = true;
|
|
|
|
@@ -896,6 +1210,25 @@ const cancelOperation = async (operation) => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectOperation = (operation) => {
|
|
|
|
|
if (!operation?.id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedOperationId.value = String(operation.id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const retryOperation = async (operation) => {
|
|
|
|
|
if (!operation?.type) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await queueOperation({
|
|
|
|
|
type: operation.type,
|
|
|
|
|
request: operation.request || operation.result || {},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const queueDiscovery = async () => {
|
|
|
|
|
const previousInventoryCount = selectedGateway.value?.inventory?.length || 0;
|
|
|
|
|
await queueOperation({ type: "DISCOVERY", request: {} });
|
|
|
|
@@ -944,6 +1277,163 @@ const saveBindingsAction = async () => {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadLogs = async (gatewayId = selectedGateway.value?.id) => {
|
|
|
|
|
if (!gatewayId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value.logs = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await getEdgeGatewayLogs(gatewayId);
|
|
|
|
|
const payload = unwrap(response, {});
|
|
|
|
|
if (payload?.gateway) {
|
|
|
|
|
setSelectedGatewaySnapshot(payload.gateway);
|
|
|
|
|
}
|
|
|
|
|
logsTimeline.value = Array.isArray(payload?.timeline) ? payload.timeline : buildLogsTimelineFromGateway(payload?.gateway);
|
|
|
|
|
gatewayShellSessions.value = Array.isArray(payload?.shell_sessions) ? payload.shell_sessions : [];
|
|
|
|
|
clearError();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
fail(error);
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value.logs = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadStatistics = async (gatewayId = selectedGateway.value?.id) => {
|
|
|
|
|
if (!gatewayId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value.statistics = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await getEdgeGatewayStatistics(gatewayId);
|
|
|
|
|
const payload = unwrap(response, {});
|
|
|
|
|
if (payload?.gateway) {
|
|
|
|
|
setSelectedGatewaySnapshot(payload.gateway);
|
|
|
|
|
}
|
|
|
|
|
statisticsSnapshot.value = payload || {};
|
|
|
|
|
clearError();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
fail(error);
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value.statistics = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const connectTerminal = async () => {
|
|
|
|
|
const gatewayId = normId(selectedGateway.value?.id);
|
|
|
|
|
if (!gatewayId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shellClient && shellGatewayId === gatewayId && ["connecting", "open"].includes(terminalStatus.value)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shellClient) {
|
|
|
|
|
try {
|
|
|
|
|
shellClient.close();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
// Best effort.
|
|
|
|
|
}
|
|
|
|
|
shellClient = null;
|
|
|
|
|
shellGatewayId = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loading.value.terminal = true;
|
|
|
|
|
terminalStatus.value = "connecting";
|
|
|
|
|
terminalError.value = "";
|
|
|
|
|
terminalTranscript.value = "";
|
|
|
|
|
terminalCommand.value = "";
|
|
|
|
|
terminalSession.value = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const client = await createGatewayShellClient(
|
|
|
|
|
gatewayId,
|
|
|
|
|
{
|
|
|
|
|
reason: "Interactive diagnostic terminal",
|
|
|
|
|
cols: 120,
|
|
|
|
|
rows: 28,
|
|
|
|
|
cwd: "/opt/truckwash-edge-agent",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
onOpen() {
|
|
|
|
|
terminalStatus.value = "open";
|
|
|
|
|
},
|
|
|
|
|
onMessage(message) {
|
|
|
|
|
if (message?.type === "output") {
|
|
|
|
|
terminalTranscript.value = `${terminalTranscript.value}${String(message.data || "")}`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message?.type === "closed") {
|
|
|
|
|
terminalStatus.value = "closed";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError(error) {
|
|
|
|
|
terminalStatus.value = "error";
|
|
|
|
|
terminalError.value = normalizeEdgeGatewayError(error).message || "Terminal session failed.";
|
|
|
|
|
},
|
|
|
|
|
onClose() {
|
|
|
|
|
if (shellClient === client) {
|
|
|
|
|
shellClient = null;
|
|
|
|
|
shellGatewayId = null;
|
|
|
|
|
if (terminalStatus.value !== "error") {
|
|
|
|
|
terminalStatus.value = "closed";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
shellClient = client;
|
|
|
|
|
shellGatewayId = gatewayId;
|
|
|
|
|
terminalSession.value = client.session;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
terminalStatus.value = "error";
|
|
|
|
|
terminalError.value = normalizeEdgeGatewayError(error).message || "Terminal session failed.";
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value.terminal = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disconnectTerminal = () => {
|
|
|
|
|
if (shellClient) {
|
|
|
|
|
try {
|
|
|
|
|
shellClient.close();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
// Best effort.
|
|
|
|
|
}
|
|
|
|
|
shellClient = null;
|
|
|
|
|
shellGatewayId = null;
|
|
|
|
|
}
|
|
|
|
|
terminalStatus.value = "closed";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateTerminalInput = (value) => {
|
|
|
|
|
terminalCommand.value = value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sendTerminalCommand = () => {
|
|
|
|
|
if (!shellClient || terminalStatus.value !== "open" || !String(terminalCommand.value || "").trim()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shellClient.sendInput(terminalCommand.value);
|
|
|
|
|
terminalCommand.value = "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const runTerminalShortcut = (command) => {
|
|
|
|
|
if (!shellClient || terminalStatus.value !== "open") {
|
|
|
|
|
terminalCommand.value = String(command || "");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shellClient.sendInput(command);
|
|
|
|
|
terminalCommand.value = "";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rotateCredentials = async () => {
|
|
|
|
|
if (!selectedGateway.value?.id) return;
|
|
|
|
|
loading.value.rotate = true;
|
|
|
|
@@ -1030,7 +1520,7 @@ const autoRefreshTick = async () => {
|
|
|
|
|
if (selectedGateway.value?.id) {
|
|
|
|
|
const shouldRefreshDetail =
|
|
|
|
|
currentView.value === "overview" ||
|
|
|
|
|
currentView.value === "operations" ||
|
|
|
|
|
currentView.value === "tasks" ||
|
|
|
|
|
Boolean(selectedGateway.value.active_operation);
|
|
|
|
|
const refreshes = [];
|
|
|
|
|
if (shouldRefreshDetail) {
|
|
|
|
@@ -1087,6 +1577,39 @@ watch(installerCommand, () => {
|
|
|
|
|
installerCopyState.value = "idle";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
[() => currentView.value, () => selectedGateway.value?.id],
|
|
|
|
|
([viewId, gatewayId], previous = []) => {
|
|
|
|
|
const [previousViewId, previousGatewayId] = previous;
|
|
|
|
|
const normalizedGatewayId = normId(gatewayId);
|
|
|
|
|
if (
|
|
|
|
|
previousViewId === "terminal" &&
|
|
|
|
|
(viewId !== "terminal" || String(previousGatewayId || "") !== String(normalizedGatewayId || ""))
|
|
|
|
|
) {
|
|
|
|
|
disconnectTerminal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!normalizedGatewayId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewId === "logs") {
|
|
|
|
|
loadLogs(normalizedGatewayId).catch(() => {});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewId === "statistics") {
|
|
|
|
|
loadStatistics(normalizedGatewayId).catch(() => {});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewId === "terminal") {
|
|
|
|
|
connectTerminal().catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (props.departmentId) {
|
|
|
|
|
installerDepartmentId.value = String(props.departmentId);
|
|
|
|
@@ -1096,6 +1619,13 @@ onMounted(() => {
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
stopAutoRefresh();
|
|
|
|
|
if (shellClient) {
|
|
|
|
|
try {
|
|
|
|
|
shellClient.close();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
// Best effort.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
@@ -1243,6 +1773,47 @@ onUnmounted(() => {
|
|
|
|
|
rows="4"
|
|
|
|
|
data-testid="gateway-install-command"
|
|
|
|
|
></textarea>
|
|
|
|
|
|
|
|
|
|
<article v-if="installerStatusView" class="edge-installer-status" data-testid="gateway-installer-status">
|
|
|
|
|
<div class="edge-panel-head">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="edge-card-label">Installer session</p>
|
|
|
|
|
<strong data-testid="gateway-installer-status-state">{{ installerStatusView.stateLabel }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<small>{{ installerStatusView.updated_at || installerStatusView.started_at || "Awaiting update" }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
<p data-testid="gateway-installer-status-step">{{ installerStatusView.stepLabel }}</p>
|
|
|
|
|
<p>{{ installerStatusView.message || "Waiting for installer progress." }}</p>
|
|
|
|
|
<p v-if="installerStatusView.last_error" data-testid="gateway-installer-status-error">
|
|
|
|
|
{{ installerStatusView.last_error }}
|
|
|
|
|
</p>
|
|
|
|
|
<details
|
|
|
|
|
v-if="installerStatusView.diagnostics?.length"
|
|
|
|
|
class="edge-installer-status__diagnostics"
|
|
|
|
|
data-testid="gateway-installer-status-diagnostics"
|
|
|
|
|
>
|
|
|
|
|
<summary>Diagnostics</summary>
|
|
|
|
|
<div class="edge-diagnostics">
|
|
|
|
|
<article
|
|
|
|
|
v-for="entry in installerStatusView.diagnostics"
|
|
|
|
|
:key="`${entry.name}-${entry.output}`"
|
|
|
|
|
class="edge-diagnostic-item"
|
|
|
|
|
>
|
|
|
|
|
<strong>{{ entry.name || "Diagnostic" }}</strong>
|
|
|
|
|
<pre class="edge-json-block">{{ entry.output || "No output" }}</pre>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
<button
|
|
|
|
|
v-if="installerStatusView.diagnostics?.length"
|
|
|
|
|
type="button"
|
|
|
|
|
class="button is-light"
|
|
|
|
|
data-testid="gateway-installer-copy-diagnostics"
|
|
|
|
|
@click="copyInstallerDiagnostics"
|
|
|
|
|
>
|
|
|
|
|
{{ installerDiagnosticsCopyLabel }}
|
|
|
|
|
</button>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
@@ -1284,38 +1855,86 @@ onUnmounted(() => {
|
|
|
|
|
<EdgeGatewayInventoryPage
|
|
|
|
|
v-else-if="currentView === 'inventory'"
|
|
|
|
|
:gateway="selectedGatewayView"
|
|
|
|
|
:editable-bindings="editableBindings"
|
|
|
|
|
:loading-discovery="loading.discovery || loading.operation"
|
|
|
|
|
:loading-bindings="loading.bindings"
|
|
|
|
|
:can-edit="true"
|
|
|
|
|
@queue-discovery="queueDiscovery"
|
|
|
|
|
:bindings="editableBindings"
|
|
|
|
|
:busy="loading.discovery || loading.bindings || loading.operation"
|
|
|
|
|
@trigger-discovery="queueDiscovery"
|
|
|
|
|
@add-binding="addBindingRow"
|
|
|
|
|
@remove-binding="removeBindingRow"
|
|
|
|
|
@update-binding="updateBinding"
|
|
|
|
|
@save-bindings="saveBindingsAction"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EdgeGatewayOperationsPage
|
|
|
|
|
v-else-if="currentView === 'operations'"
|
|
|
|
|
:gateway="selectedGatewayView"
|
|
|
|
|
<EdgeGatewayTasksPage
|
|
|
|
|
v-else-if="currentView === 'tasks'"
|
|
|
|
|
:operations="gatewayOperations"
|
|
|
|
|
:recent-commands="recentCommands"
|
|
|
|
|
:operation-summary="operationSummary"
|
|
|
|
|
:selected-operation-id="selectedOperationId"
|
|
|
|
|
:operation-events="selectedOperationEvents"
|
|
|
|
|
:allow-destructive="allowDestructive"
|
|
|
|
|
:loading-create="loading.operation"
|
|
|
|
|
:loading-cancel="loading.operationCancel"
|
|
|
|
|
@run-operation="queueOperation"
|
|
|
|
|
:busy="loading.operation || loading.operationCancel"
|
|
|
|
|
:update-target-version="updateTargetVersion"
|
|
|
|
|
:stream-status="streamStatus"
|
|
|
|
|
:stream-error="streamError"
|
|
|
|
|
@queue-discovery="queueDiscovery"
|
|
|
|
|
@queue-update="queueUpdate"
|
|
|
|
|
@queue-uninstall="queueUninstall"
|
|
|
|
|
@cancel-operation="cancelOperation"
|
|
|
|
|
@retry-operation="retryOperation"
|
|
|
|
|
@select-operation="selectOperation"
|
|
|
|
|
@update-target-version="updateTargetVersionDraft"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EdgeGatewayManagePage
|
|
|
|
|
v-else-if="currentView === 'manage' && allowDestructive"
|
|
|
|
|
<EdgeGatewayLogsPage
|
|
|
|
|
v-else-if="currentView === 'logs'"
|
|
|
|
|
:timeline="logsTimeline"
|
|
|
|
|
:shell-sessions="gatewayShellSessions"
|
|
|
|
|
:stream-status="streamStatus"
|
|
|
|
|
:stream-error="streamError"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EdgeGatewayStatisticsPage
|
|
|
|
|
v-else-if="currentView === 'statistics'"
|
|
|
|
|
:statistics="statisticsSnapshot"
|
|
|
|
|
:stream-status="streamStatus"
|
|
|
|
|
:stream-error="streamError"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EdgeGatewayTerminalPage
|
|
|
|
|
v-else-if="currentView === 'terminal' && allowDestructive"
|
|
|
|
|
:status="terminalStatus"
|
|
|
|
|
:error="terminalError"
|
|
|
|
|
:transcript="terminalTranscript"
|
|
|
|
|
:command="terminalCommand"
|
|
|
|
|
:session="terminalSession"
|
|
|
|
|
:busy="loading.terminal"
|
|
|
|
|
@connect="connectTerminal"
|
|
|
|
|
@disconnect="disconnectTerminal"
|
|
|
|
|
@send-command="sendTerminalCommand"
|
|
|
|
|
@update-command="updateTerminalInput"
|
|
|
|
|
@shortcut="runTerminalShortcut"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<EdgeGatewaySettingsPage
|
|
|
|
|
v-else-if="currentView === 'settings' && allowDestructive"
|
|
|
|
|
:gateway="selectedGatewayView"
|
|
|
|
|
:metadata-form="metadataForm"
|
|
|
|
|
:transport-mode="transportModeDraft"
|
|
|
|
|
:rotate-confirmation="rotateConfirmation"
|
|
|
|
|
:delete-confirmation="deleteConfirmation"
|
|
|
|
|
:credential-bundle="rotateBundle"
|
|
|
|
|
:busy="loading.metadata || loading.cutover || loading.rotate || loading.delete"
|
|
|
|
|
:loading-metadata="loading.metadata"
|
|
|
|
|
:loading-cutover="loading.cutover"
|
|
|
|
|
:loading-rotate="loading.rotate"
|
|
|
|
|
:loading-delete="loading.delete"
|
|
|
|
|
:rotate-bundle="rotateBundle"
|
|
|
|
|
@update-metadata="updateMetadataField"
|
|
|
|
|
@save-metadata="saveMetadata"
|
|
|
|
|
@apply-cutover="applyCutover"
|
|
|
|
|
@update-transport-mode="updateTransportMode"
|
|
|
|
|
@save-transport-mode="saveTransportMode"
|
|
|
|
|
@update-rotate-confirmation="updateRotateConfirmation"
|
|
|
|
|
@rotate-credentials="rotateCredentials"
|
|
|
|
|
@update-delete-confirmation="updateDeleteConfirmation"
|
|
|
|
|
@delete-gateway="removeGateway"
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
@@ -1498,6 +2117,20 @@ onUnmounted(() => {
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-installer-status {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border: 1px solid rgba(32, 41, 58, 0.08);
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
background: rgba(248, 250, 252, 0.9);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-installer-status__diagnostics {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 960px) {
|
|
|
|
|
.edge-v2-layout {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|