Add self-serve Edge Agents management with navigation, routes, and tabs. Update legacy redirects and extend unit and E2E tests.
This commit is contained in:
@@ -1186,7 +1186,7 @@ const openDepartmentWorkspaceTab = (tabId) =>
|
||||
|
||||
const openPrimaryGatewayPage = async () => {
|
||||
if (!props.department_id) {
|
||||
SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true);
|
||||
SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1199,7 +1199,7 @@ const openPrimaryGatewayPage = async () => {
|
||||
|
||||
if (primaryGatewayId) {
|
||||
SessionUser.functions.redirectTo.superUser(
|
||||
`/configuration/edgegateway/${encodeURIComponent(String(primaryGatewayId))}/overview`,
|
||||
`/selfserve/edge-agents/${encodeURIComponent(String(primaryGatewayId))}/overview`,
|
||||
true
|
||||
);
|
||||
return;
|
||||
@@ -1208,7 +1208,7 @@ const openPrimaryGatewayPage = async () => {
|
||||
console.warn("Unable to resolve primary gateway from department workspace", error);
|
||||
}
|
||||
|
||||
SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true);
|
||||
SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true);
|
||||
};
|
||||
|
||||
const flatBuiltInMenuSections = computed(() => {
|
||||
@@ -1528,7 +1528,7 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
? buildMenuAction("department-gateways-open-fleet", {
|
||||
icon: "fas fa-sitemap",
|
||||
label: t("admin.pos.settings_wheel.open_fleet_landing"),
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true),
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true),
|
||||
})
|
||||
: null,
|
||||
SessionUser.canAccessSuperUser()
|
||||
|
||||
@@ -119,7 +119,7 @@ const gatewayCards = computed(() => {
|
||||
lastHeartbeatLabel: formatDate(gateway?.last_heartbeat_at),
|
||||
activeOperationLabel: gatewayOperationLabel(activeOperation),
|
||||
message: gatewayPrimaryMessage(gateway),
|
||||
link: `/superuser/configuration/edgegateway/${encodeURIComponent(String(gateway?.id ?? ""))}/overview`,
|
||||
link: `/superuser/selfserve/edge-agents/${encodeURIComponent(String(gateway?.id ?? ""))}/overview`,
|
||||
};
|
||||
})
|
||||
.filter((gateway) => gateway.id !== null);
|
||||
@@ -702,7 +702,7 @@ function modulePath(key) {
|
||||
<h3>{{ $t("system_status.sections.gateways") }}</h3>
|
||||
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
|
||||
</div>
|
||||
<RouterLink class="section-link" to="/superuser/configuration/edgegateway">
|
||||
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
|
||||
{{ $t("system_status.gateways.actions.open_fleet") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
@@ -324,6 +324,7 @@ const items = computed<NavigationItemProps[]>(() => [
|
||||
{ label: t('superuser.nav.scanners'), to: '/superuser/scanners' },
|
||||
{ label: t('superuser.nav.selfserve'), to: '/superuser/selfserve' },
|
||||
{ label: 'Self-serve sessions', to: '/superuser/selfserve/sessions' },
|
||||
{ label: 'Edge Agents', to: '/superuser/selfserve/edge-agents' },
|
||||
]},
|
||||
// Configurations
|
||||
{ label: t('superuser.nav.configuration'), type: 'category', children: [
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script>
|
||||
import { getEdgeGatewayModuleConfig, setEdgeGatewayModuleConfig } from "@/services/edgeGateways.js";
|
||||
import {
|
||||
getEdgeGatewayModuleConfig,
|
||||
setEdgeGatewayModuleConfig,
|
||||
testEdgeGatewayBrokerConfig,
|
||||
} from "@/services/edgeGateways.js";
|
||||
|
||||
const normalizeEntries = (entries) => {
|
||||
if (Array.isArray(entries)) {
|
||||
@@ -35,6 +39,7 @@ export const Config = {
|
||||
}),
|
||||
get_config: async () => Config.get_all().then((response) => response?.data?.config || {}),
|
||||
set_config: async (payload) => setEdgeGatewayModuleConfig(payload),
|
||||
test_broker: async (payload) => testEdgeGatewayBrokerConfig(payload),
|
||||
set: async (variable, value) => {
|
||||
const currentConfig = await Config.get_config();
|
||||
return Config.set_config({
|
||||
|
||||
@@ -506,17 +506,17 @@ const openGatewayPage = async (gatewayId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push(`/superuser/configuration/edgegateway/${encodeURIComponent(String(gatewayId))}/overview`);
|
||||
await router.push(`/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/overview`);
|
||||
};
|
||||
|
||||
const openPrimaryGatewayInventory = async () => {
|
||||
const gatewayId = summary.value?.primary_gateway?.id || null;
|
||||
if (!gatewayId) {
|
||||
await router.push("/superuser/configuration/edgegateway");
|
||||
await router.push("/superuser/selfserve/edge-agents");
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push(`/superuser/configuration/edgegateway/${encodeURIComponent(String(gatewayId))}/inventory`);
|
||||
await router.push(`/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/inventory`);
|
||||
};
|
||||
|
||||
const saveScannerLane = async (scanner) => {
|
||||
@@ -819,7 +819,7 @@ const getRelayConsumerContextLabel = (context) => {
|
||||
class="button is-dark"
|
||||
type="button"
|
||||
data-testid="department-hardware-open-fleet"
|
||||
@click="openPath('/superuser/configuration/edgegateway')"
|
||||
@click="openPath('/superuser/selfserve/edge-agents')"
|
||||
>
|
||||
Open Fleet Landing
|
||||
</button>
|
||||
|
||||
@@ -69,7 +69,7 @@ const openPrimaryGateway = async (gatewayId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push(`/superuser/configuration/edgegateway/${encodeURIComponent(String(gatewayId))}/overview`);
|
||||
await router.push(`/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/overview`);
|
||||
};
|
||||
|
||||
onMounted(loadSummaries);
|
||||
|
||||
@@ -26,12 +26,146 @@ const props = defineProps({
|
||||
|
||||
const levelFilter = ref("ALL");
|
||||
const typeFilter = ref("ALL");
|
||||
const expandedTimelineKeys = ref(new Set());
|
||||
const expandedRelayKeys = ref(new Set());
|
||||
const activeContextModal = ref(null);
|
||||
const copiedContextKey = ref("");
|
||||
|
||||
const entryContext = (entry = {}) => {
|
||||
const context = entry?.context || entry?.entry?.context || {};
|
||||
return context && typeof context === "object" && !Array.isArray(context) ? context : {};
|
||||
};
|
||||
|
||||
const formatDetailValue = (value) => {
|
||||
if (value === null || typeof value === "undefined" || value === "") {
|
||||
return "None";
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const detailRow = (label, value) => ({
|
||||
label,
|
||||
value: formatDetailValue(value),
|
||||
multiline: value !== null && typeof value === "object",
|
||||
});
|
||||
|
||||
const contextDetailRow = (value) => ({
|
||||
label: "Context",
|
||||
value: formatDetailValue(value),
|
||||
multiline: true,
|
||||
actionsOnly: true,
|
||||
});
|
||||
|
||||
const pushOptionalDetailRow = (rows, label, value) => {
|
||||
if (value !== null && typeof value !== "undefined" && value !== "") {
|
||||
rows.push(detailRow(label, value));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpandedKey = (expandedRef, key) => {
|
||||
const next = new Set(expandedRef.value);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
expandedRef.value = next;
|
||||
};
|
||||
|
||||
const timelineEntryKey = (entry = {}, index = 0) =>
|
||||
[
|
||||
"timeline",
|
||||
entry?.type || "log",
|
||||
entry?.created_at || "no-time",
|
||||
entry?.message || "no-message",
|
||||
entry?.entry?.id || entry?.id || index,
|
||||
].join(":");
|
||||
|
||||
const relayEntryKey = (entry = {}, index = 0) =>
|
||||
["relay", entry?.id || entry?.created_at || "no-time", entry?.message || index].join(":");
|
||||
|
||||
const isTimelineExpanded = (entry = {}, index = 0) => expandedTimelineKeys.value.has(timelineEntryKey(entry, index));
|
||||
|
||||
const isRelayExpanded = (entry = {}, index = 0) => expandedRelayKeys.value.has(relayEntryKey(entry, index));
|
||||
|
||||
const toggleTimelineEntry = (entry = {}, index = 0) => {
|
||||
toggleExpandedKey(expandedTimelineKeys, timelineEntryKey(entry, index));
|
||||
};
|
||||
|
||||
const toggleRelayEntry = (entry = {}, index = 0) => {
|
||||
toggleExpandedKey(expandedRelayKeys, relayEntryKey(entry, index));
|
||||
};
|
||||
|
||||
const closeContextModal = () => {
|
||||
activeContextModal.value = null;
|
||||
};
|
||||
|
||||
const openContextModal = (title, value) => {
|
||||
activeContextModal.value = {
|
||||
title,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const writeToClipboard = async (value) => {
|
||||
const text = String(value || "");
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof document === "undefined" || !document.body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const helper = document.createElement("textarea");
|
||||
helper.value = text;
|
||||
helper.setAttribute("readonly", "readonly");
|
||||
helper.style.position = "fixed";
|
||||
helper.style.opacity = "0";
|
||||
helper.style.pointerEvents = "none";
|
||||
document.body.appendChild(helper);
|
||||
helper.focus();
|
||||
helper.select();
|
||||
|
||||
let copied = false;
|
||||
try {
|
||||
copied = document.execCommand("copy");
|
||||
} finally {
|
||||
document.body.removeChild(helper);
|
||||
}
|
||||
|
||||
return copied;
|
||||
};
|
||||
|
||||
const copyContextValue = async (value, key) => {
|
||||
const copied = await writeToClipboard(value).catch(() => false);
|
||||
if (!copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
copiedContextKey.value = key;
|
||||
window.setTimeout(() => {
|
||||
if (copiedContextKey.value === key) {
|
||||
copiedContextKey.value = "";
|
||||
}
|
||||
}, 1600);
|
||||
};
|
||||
|
||||
const relayEntries = computed(() => {
|
||||
if (props.relayLogs.length) {
|
||||
return props.relayLogs;
|
||||
@@ -43,7 +177,8 @@ const relayEntries = computed(() => {
|
||||
.filter(Boolean);
|
||||
});
|
||||
|
||||
const relayModuleLabel = (entry = {}) => entryContext(entry).module_responsible || entryContext(entry).module || "edge_gateway";
|
||||
const relayModuleLabel = (entry = {}) =>
|
||||
entryContext(entry).module_responsible || entryContext(entry).module || "edge_gateway";
|
||||
|
||||
const relayHandlerLabel = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
@@ -72,14 +207,116 @@ const relayActorLabel = (entry = {}) => {
|
||||
return parts.join(" / ") || "No actor";
|
||||
};
|
||||
|
||||
const relaySignalLabel = (entry = {}) => {
|
||||
const normalizeRelayRole = (role = "") => {
|
||||
const normalized = String(role || "").trim().toUpperCase();
|
||||
const aliases = {
|
||||
ENTRANCE: "ENTRY",
|
||||
IN: "ENTRY",
|
||||
INLET: "ENTRY",
|
||||
ENTRY_GATE: "ENTRY",
|
||||
OUT: "EXIT",
|
||||
OUTLET: "EXIT",
|
||||
EXIT_GATE: "EXIT",
|
||||
MACHINE_PROGRAM_PICKER: "PROGRAM_PICKER",
|
||||
PROGRAM_SELECTOR: "PROGRAM_PICKER",
|
||||
PICKER: "PROGRAM_PICKER",
|
||||
MACHINE_CLEANER: "CLEANER",
|
||||
};
|
||||
return aliases[normalized] || normalized;
|
||||
};
|
||||
|
||||
const relayRequestPayload = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const signal = context.signal && typeof context.signal === "object" ? context.signal : {};
|
||||
return signal.request && typeof signal.request === "object" ? signal.request : {};
|
||||
};
|
||||
|
||||
const relayStateValue = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const request = relayRequestPayload(entry);
|
||||
const response = context.response && typeof context.response === "object" ? context.response : {};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(context, "target_on") && context.target_on !== null) {
|
||||
return Boolean(context.target_on);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(request, "on")) {
|
||||
return Boolean(request.on);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(response, "on")) {
|
||||
return Boolean(response.on);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const firstRelayText = (values = []) => {
|
||||
for (const value of values) {
|
||||
const text = String(value || "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const relayDisplayName = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const actionContext = context.action_context && typeof context.action_context === "object" ? context.action_context : {};
|
||||
const binding = context.binding && typeof context.binding === "object" ? context.binding : {};
|
||||
const request = relayRequestPayload(entry);
|
||||
return firstRelayText([
|
||||
context.relay_name,
|
||||
context.relay_label,
|
||||
actionContext.relay_name,
|
||||
actionContext.relay_label,
|
||||
binding.relay_name,
|
||||
binding.name,
|
||||
request.relay_name,
|
||||
request.relay_label,
|
||||
]);
|
||||
};
|
||||
|
||||
const relayIdentifier = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const signal = context.signal && typeof context.signal === "object" ? context.signal : {};
|
||||
const request = relayRequestPayload(entry);
|
||||
return context.relay_id || signal.relay_id || request.relayId || request.id || "unknown";
|
||||
};
|
||||
|
||||
const relaySignalLabel = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const actionContext = context.action_context && typeof context.action_context === "object" ? context.action_context : {};
|
||||
const signal = context.signal && typeof context.signal === "object" ? context.signal : {};
|
||||
const role = normalizeRelayRole(context.relay_role || actionContext.relay_role || actionContext.role);
|
||||
const action = String(context.action || signal.command_type || "RELAY").toUpperCase();
|
||||
const relayId = context.relay_id || signal.relay_id || signal.request?.relayId || signal.request?.id || "unknown";
|
||||
const targetOn = Object.prototype.hasOwnProperty.call(context, "target_on") ? context.target_on : signal.request?.on;
|
||||
const target = typeof targetOn === "boolean" ? (targetOn ? "ON" : "OFF") : "";
|
||||
return [action, relayId, target].filter(Boolean).join(" ");
|
||||
const relayTarget = relayDisplayName(entry) || relayIdentifier(entry);
|
||||
const state = relayStateValue(entry);
|
||||
|
||||
if (["ENTRY", "EXIT"].includes(role)) {
|
||||
return ["Open", role, relayTarget].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
if (["MACHINE", "PROGRAM_PICKER", "CLEANER"].includes(role) && state !== null) {
|
||||
return [role, state ? "ON" : "OFF", relayTarget].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const target = state !== null ? (state ? "ON" : "OFF") : "";
|
||||
return [action, relayTarget, target].filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
const relayDescription = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const existing = firstRelayText([context.description, entry.description]);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const subject = relaySignalLabel(entry);
|
||||
const handler = String(context.handler || context.execution_path || "local").toLowerCase();
|
||||
const channel = context.delivery_channel || context.execution?.channel || "";
|
||||
const failed = context.success === false || String(entry.level || "").toUpperCase() === "ERROR";
|
||||
return failed
|
||||
? `${subject} failed via ${handler}`
|
||||
: `${subject} handled by ${handler}${channel ? ` via ${channel}` : ""}`;
|
||||
};
|
||||
|
||||
const relayResponseLabel = (entry = {}) => {
|
||||
@@ -94,6 +331,59 @@ const relayResponseLabel = (entry = {}) => {
|
||||
return parts.join(" / ") || "No response payload";
|
||||
};
|
||||
|
||||
const timelineEntryMessage = (entry = {}) => {
|
||||
const rawEntry = entry?.entry && typeof entry.entry === "object" ? entry.entry : {};
|
||||
const type = String(entry.type || rawEntry.stream || "").toLowerCase();
|
||||
if (type === "relay") {
|
||||
return relayDescription(rawEntry);
|
||||
}
|
||||
return entry.message || rawEntry.message || rawEntry.action || rawEntry.code || "No message";
|
||||
};
|
||||
|
||||
const timelineDetailRows = (entry = {}) => {
|
||||
const rawEntry = entry?.entry && typeof entry.entry === "object" ? entry.entry : {};
|
||||
const context = entryContext(entry);
|
||||
const rows = [
|
||||
detailRow("Level", entry.level || rawEntry.level || rawEntry.severity || "INFO"),
|
||||
detailRow("Type", entry.type || rawEntry.stream || "log"),
|
||||
detailRow("Message", timelineEntryMessage(entry)),
|
||||
detailRow("Timestamp", entry.created_at || rawEntry.created_at || "No timestamp"),
|
||||
];
|
||||
|
||||
pushOptionalDetailRow(rows, "ID", rawEntry.id || entry.id);
|
||||
pushOptionalDetailRow(rows, "Source", rawEntry.source);
|
||||
pushOptionalDetailRow(rows, "Stream", rawEntry.stream);
|
||||
pushOptionalDetailRow(rows, "Operation", rawEntry.operation_type);
|
||||
pushOptionalDetailRow(rows, "Operation ID", rawEntry.operation_id);
|
||||
if (Object.keys(context).length) {
|
||||
rows.push(contextDetailRow(context));
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const relayDetailRows = (entry = {}) => {
|
||||
const context = entryContext(entry);
|
||||
const rows = [
|
||||
detailRow("Handler", relayHandlerLabel(entry)),
|
||||
detailRow("Module", relayModuleLabel(entry)),
|
||||
detailRow("Message", relayDescription(entry)),
|
||||
detailRow("Signal", relaySignalLabel(entry)),
|
||||
detailRow("Response", relayResponseLabel(entry)),
|
||||
detailRow("Actor", relayActorLabel(entry)),
|
||||
detailRow("Reason", relayReasonLabel(entry)),
|
||||
detailRow("Timestamp", entry.created_at || "No timestamp"),
|
||||
];
|
||||
|
||||
pushOptionalDetailRow(rows, "ID", entry.id);
|
||||
pushOptionalDetailRow(rows, "Source", entry.source);
|
||||
if (Object.keys(context).length) {
|
||||
rows.push(contextDetailRow(context));
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const filteredTimeline = computed(() =>
|
||||
props.timeline.filter((entry) => {
|
||||
if (levelFilter.value !== "ALL" && String(entry.level || "").toUpperCase() !== levelFilter.value) {
|
||||
@@ -143,15 +433,65 @@ const filteredTimeline = computed(() =>
|
||||
<p class="edge-gateway-logs__eyebrow">Timeline</p>
|
||||
<h3>Live and persisted entries</h3>
|
||||
<ul class="edge-gateway-logs__timeline" data-testid="gateway-logs-timeline">
|
||||
<li v-for="entry in filteredTimeline" :key="`${entry.type}-${entry.created_at}-${entry.message}`">
|
||||
<div>
|
||||
<strong>{{ entry.level || "INFO" }}</strong>
|
||||
<small>{{ entry.type || "log" }}</small>
|
||||
</div>
|
||||
<p>{{ entry.message || "No message" }}</p>
|
||||
<small>{{ entry.created_at || "No timestamp" }}</small>
|
||||
<li
|
||||
v-for="(entry, index) in filteredTimeline"
|
||||
:key="timelineEntryKey(entry, index)"
|
||||
class="edge-gateway-logs__entry"
|
||||
:class="{ 'edge-gateway-logs__entry--expanded': isTimelineExpanded(entry, index) }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="edge-gateway-logs__entry-toggle"
|
||||
:aria-expanded="isTimelineExpanded(entry, index)"
|
||||
:aria-controls="`gateway-log-entry-details-${index}`"
|
||||
:data-testid="`gateway-log-entry-${index}`"
|
||||
@click="toggleTimelineEntry(entry, index)"
|
||||
>
|
||||
<span class="edge-gateway-logs__entry-heading">
|
||||
<span class="edge-gateway-logs__entry-labels">
|
||||
<strong>{{ entry.level || "INFO" }}</strong>
|
||||
<small>{{ entry.type || "log" }}</small>
|
||||
</span>
|
||||
<span class="edge-gateway-logs__expand-indicator" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="edge-gateway-logs__summary">{{ timelineEntryMessage(entry) }}</span>
|
||||
</button>
|
||||
<dl
|
||||
v-if="isTimelineExpanded(entry, index)"
|
||||
:id="`gateway-log-entry-details-${index}`"
|
||||
class="edge-gateway-logs__entry-details"
|
||||
:data-testid="`gateway-log-entry-details-${index}`"
|
||||
>
|
||||
<div v-for="row in timelineDetailRows(entry)" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>
|
||||
<span v-if="row.actionsOnly" class="edge-gateway-logs__context-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-small is-light"
|
||||
:data-testid="`gateway-log-entry-context-view-${index}`"
|
||||
@click="openContextModal('Timeline context', row.value)"
|
||||
>
|
||||
View context
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-small is-light"
|
||||
:data-testid="`gateway-log-entry-context-copy-${index}`"
|
||||
@click="copyContextValue(row.value, `timeline-${index}`)"
|
||||
>
|
||||
{{ copiedContextKey === `timeline-${index}` ? "Copied" : "Copy" }}
|
||||
</button>
|
||||
</span>
|
||||
<pre v-else-if="row.multiline">{{ row.value }}</pre>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
<li v-if="!filteredTimeline.length" class="edge-gateway-logs__empty">
|
||||
No log entries match the current filter.
|
||||
</li>
|
||||
<li v-if="!filteredTimeline.length">No log entries match the current filter.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
@@ -159,33 +499,63 @@ const filteredTimeline = computed(() =>
|
||||
<p class="edge-gateway-logs__eyebrow">Relay dispatch</p>
|
||||
<h3>Signals and responses</h3>
|
||||
<ul class="edge-gateway-logs__timeline" data-testid="gateway-relay-logs">
|
||||
<li v-for="entry in relayEntries" :key="`relay-${entry.id || entry.created_at || entry.message}`">
|
||||
<div>
|
||||
<strong>{{ relayHandlerLabel(entry) }}</strong>
|
||||
<small>{{ relayModuleLabel(entry) }}</small>
|
||||
</div>
|
||||
<p>{{ entry.message || relaySignalLabel(entry) }}</p>
|
||||
<dl class="edge-gateway-logs__relay-details">
|
||||
<div>
|
||||
<dt>Signal</dt>
|
||||
<dd>{{ relaySignalLabel(entry) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Response</dt>
|
||||
<dd>{{ relayResponseLabel(entry) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Actor</dt>
|
||||
<dd>{{ relayActorLabel(entry) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Reason</dt>
|
||||
<dd>{{ relayReasonLabel(entry) }}</dd>
|
||||
<li
|
||||
v-for="(entry, index) in relayEntries"
|
||||
:key="relayEntryKey(entry, index)"
|
||||
class="edge-gateway-logs__entry"
|
||||
:class="{ 'edge-gateway-logs__entry--expanded': isRelayExpanded(entry, index) }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="edge-gateway-logs__entry-toggle"
|
||||
:aria-expanded="isRelayExpanded(entry, index)"
|
||||
:aria-controls="`gateway-relay-entry-details-${index}`"
|
||||
:data-testid="`gateway-relay-entry-${index}`"
|
||||
@click="toggleRelayEntry(entry, index)"
|
||||
>
|
||||
<span class="edge-gateway-logs__entry-heading">
|
||||
<span class="edge-gateway-logs__entry-labels">
|
||||
<strong>{{ relayHandlerLabel(entry) }}</strong>
|
||||
<small>{{ relayModuleLabel(entry) }}</small>
|
||||
</span>
|
||||
<span class="edge-gateway-logs__expand-indicator" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="edge-gateway-logs__summary">{{ relayDescription(entry) }}</span>
|
||||
</button>
|
||||
<dl
|
||||
v-if="isRelayExpanded(entry, index)"
|
||||
:id="`gateway-relay-entry-details-${index}`"
|
||||
class="edge-gateway-logs__entry-details"
|
||||
:data-testid="`gateway-relay-entry-details-${index}`"
|
||||
>
|
||||
<div v-for="row in relayDetailRows(entry)" :key="row.label">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>
|
||||
<span v-if="row.actionsOnly" class="edge-gateway-logs__context-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-small is-light"
|
||||
:data-testid="`gateway-relay-entry-context-view-${index}`"
|
||||
@click="openContextModal('Relay context', row.value)"
|
||||
>
|
||||
View context
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-small is-light"
|
||||
:data-testid="`gateway-relay-entry-context-copy-${index}`"
|
||||
@click="copyContextValue(row.value, `relay-${index}`)"
|
||||
>
|
||||
{{ copiedContextKey === `relay-${index}` ? "Copied" : "Copy" }}
|
||||
</button>
|
||||
</span>
|
||||
<pre v-else-if="row.multiline">{{ row.value }}</pre>
|
||||
<span v-else>{{ row.value }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<small>{{ entry.created_at || "No timestamp" }}</small>
|
||||
</li>
|
||||
<li v-if="!relayEntries.length">No relay dispatch entries recorded.</li>
|
||||
<li v-if="!relayEntries.length" class="edge-gateway-logs__empty">No relay dispatch entries recorded.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
@@ -205,6 +575,46 @@ const filteredTimeline = computed(() =>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeContextModal"
|
||||
class="modal is-active edge-gateway-logs__context-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gateway-context-modal-title"
|
||||
data-testid="gateway-context-modal"
|
||||
tabindex="-1"
|
||||
@keyup.esc="closeContextModal"
|
||||
>
|
||||
<div class="modal-background" @click="closeContextModal"></div>
|
||||
<div class="modal-card edge-gateway-logs__context-modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p id="gateway-context-modal-title" class="modal-card-title">{{ activeContextModal.title }}</p>
|
||||
<button class="delete" type="button" aria-label="close" @click="closeContextModal"></button>
|
||||
</header>
|
||||
<section class="modal-card-body edge-gateway-logs__context-modal-body">
|
||||
<pre data-testid="gateway-context-modal-body">{{ activeContextModal.value }}</pre>
|
||||
</section>
|
||||
<footer class="modal-card-foot edge-gateway-logs__context-modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light"
|
||||
data-testid="gateway-context-modal-copy"
|
||||
@click="copyContextValue(activeContextModal.value, 'modal')"
|
||||
>
|
||||
{{ copiedContextKey === "modal" ? "Copied" : "Copy" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-dark"
|
||||
data-testid="gateway-context-modal-close"
|
||||
@click="closeContextModal"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -250,7 +660,7 @@ const filteredTimeline = computed(() =>
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__timeline li {
|
||||
@@ -265,32 +675,200 @@ const filteredTimeline = computed(() =>
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__relay-details {
|
||||
margin: 0.75rem 0;
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
.edge-gateway-logs__timeline li.edge-gateway-logs__entry {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__relay-details div {
|
||||
.edge-gateway-logs__entry-toggle {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0.55rem 0.75rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-toggle:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-heading {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-labels {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-labels strong,
|
||||
.edge-gateway-logs__entry-labels small {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-labels strong {
|
||||
color: #1f2937;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-labels small {
|
||||
color: #64748b;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__expand-indicator {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__expand-indicator::before,
|
||||
.edge-gateway-logs__expand-indicator::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0.5rem;
|
||||
height: 1px;
|
||||
background: #475569;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.edge-gateway-logs__expand-indicator::after {
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry--expanded .edge-gateway-logs__expand-indicator::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry--expanded .edge-gateway-logs__summary {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-details {
|
||||
margin: 0;
|
||||
padding: 0.65rem 0.75rem 0.75rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-details div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(5rem, 0.35fr) 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__relay-details dt,
|
||||
.edge-gateway-logs__relay-details dd {
|
||||
.edge-gateway-logs__entry-details dt,
|
||||
.edge-gateway-logs__entry-details dd {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__relay-details dt {
|
||||
.edge-gateway-logs__entry-details dt {
|
||||
color: #64748b;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__relay-details dd {
|
||||
.edge-gateway-logs__entry-details dd {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__entry-details pre {
|
||||
max-height: 16rem;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-modal-card {
|
||||
width: calc(100vw - 2rem);
|
||||
max-width: none;
|
||||
height: calc(100vh - 2rem);
|
||||
max-height: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-modal-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-modal-body pre {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.edge-gateway-logs__context-modal-footer {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.edge-gateway-logs__entry-details div {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+80
-13
@@ -55,6 +55,7 @@ const NumberPlateScanners = lazyView('@/views/dashboards/superUserDashboard/Numb
|
||||
const Statistics = lazyView('@/views/dashboards/superUserDashboard/statistics/Statistics.vue');
|
||||
const DepartmentSelfServe = lazyView('@/views/dashboards/superUserDashboard/DepartmentSelfServe.vue');
|
||||
const SelfServeSessions = lazyView('@/views/dashboards/superUserDashboard/SelfServeSessions.vue');
|
||||
const EdgeAgents = lazyView('@/views/dashboards/superUserDashboard/selfserve/EdgeAgents.vue');
|
||||
const EdgeGatewaysWorkspacePage = lazyView('@/views/dashboards/superUserDashboard/EdgeGatewaysWorkspacePage.vue');
|
||||
const Invoicing = lazyView('@/views/dashboards/superUserDashboard/Invoicing.vue');
|
||||
|
||||
@@ -651,6 +652,72 @@ export const router = createRouter({
|
||||
component: SelfServeSessions,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagents',
|
||||
path: '/superuser/selfserve/edge-agents',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentdetail',
|
||||
path: '/superuser/selfserve/edge-agents/:id',
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentoverview',
|
||||
path: '/superuser/selfserve/edge-agents/:id/overview',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagenttasks',
|
||||
path: '/superuser/selfserve/edge-agents/:id/tasks',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentlogs',
|
||||
path: '/superuser/selfserve/edge-agents/:id/logs',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentstatistics',
|
||||
path: '/superuser/selfserve/edge-agents/:id/statistics',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentterminal',
|
||||
path: '/superuser/selfserve/edge-agents/:id/terminal',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentinventory',
|
||||
path: '/superuser/selfserve/edge-agents/:id/inventory',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentoperations',
|
||||
path: '/superuser/selfserve/edge-agents/:id/operations',
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/tasks`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentsettings',
|
||||
path: '/superuser/selfserve/edge-agents/:id/settings',
|
||||
component: EdgeAgents,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgeagentmanage',
|
||||
path: '/superuser/selfserve/edge-agents/:id/manage',
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/settings`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'departmentrelays',
|
||||
path: '/superuser/department/relays',
|
||||
@@ -666,73 +733,73 @@ export const router = createRouter({
|
||||
{
|
||||
name: 'edgegatewaydetail',
|
||||
path: '/superuser/configuration/edgegateway/:id',
|
||||
redirect: (to) => `/superuser/configuration/edgegateway/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewayoverview',
|
||||
path: '/superuser/configuration/edgegateway/:id/overview',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaytasks',
|
||||
path: '/superuser/configuration/edgegateway/:id/tasks',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/tasks`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaylogs',
|
||||
path: '/superuser/configuration/edgegateway/:id/logs',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/logs`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaystatistics',
|
||||
path: '/superuser/configuration/edgegateway/:id/statistics',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/statistics`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewayterminal',
|
||||
path: '/superuser/configuration/edgegateway/:id/terminal',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/terminal`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewayinventory',
|
||||
path: '/superuser/configuration/edgegateway/:id/inventory',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/inventory`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewayoperations',
|
||||
path: '/superuser/configuration/edgegateway/:id/operations',
|
||||
redirect: (to) => `/superuser/configuration/edgegateway/${encodeURIComponent(String(to.params.id))}/tasks`,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/tasks`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaysettings',
|
||||
path: '/superuser/configuration/edgegateway/:id/settings',
|
||||
component: EdgeGatewaysWorkspacePage,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/settings`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaymanage',
|
||||
path: '/superuser/configuration/edgegateway/:id/manage',
|
||||
redirect: (to) => `/superuser/configuration/edgegateway/${encodeURIComponent(String(to.params.id))}/settings`,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/settings`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewayslegacy',
|
||||
path: '/superuser/gateways',
|
||||
redirect: '/superuser/configuration/edgegateway',
|
||||
redirect: '/superuser/selfserve/edge-agents',
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
name: 'edgegatewaydetaillegacyroot',
|
||||
path: '/superuser/gateways/:id',
|
||||
redirect: (to) => `/superuser/configuration/edgegateway/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/overview`,
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
{
|
||||
@@ -754,7 +821,7 @@ export const router = createRouter({
|
||||
settings: 'settings',
|
||||
};
|
||||
const subPage = mapping[String(to.params.subPage || '').toLowerCase()] || 'overview';
|
||||
return `/superuser/configuration/edgegateway/${encodeURIComponent(String(to.params.id))}/${subPage}`;
|
||||
return `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/${subPage}`;
|
||||
},
|
||||
meta: { middleware: superUserMiddleware }
|
||||
},
|
||||
|
||||
@@ -652,6 +652,9 @@ export const getEdgeGatewayModuleConfig = async (variable = null) => {
|
||||
export const setEdgeGatewayModuleConfig = async (payload) =>
|
||||
authenticatedRequest(EDGE_GATEWAY_CONFIG_BASE, "POST", payload);
|
||||
|
||||
export const testEdgeGatewayBrokerConfig = async (payload = {}) =>
|
||||
authenticatedRequest(`${EDGE_GATEWAY_CONFIG_BASE}/broker-diagnostics`, "POST", payload);
|
||||
|
||||
export const EDGE_GATEWAY_OPERATIONS = {
|
||||
discovery(request = {}) {
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
const openFleetLanding = async () => {
|
||||
await router.push("/superuser/configuration/edgegateway");
|
||||
await router.push("/superuser/selfserve/edge-agents");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
const openFleetLanding = async () => {
|
||||
await router.push("/superuser/configuration/edgegateway");
|
||||
await router.push("/superuser/selfserve/edge-agents");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import SelfServeSuperUserTabs from "@/views/dashboards/superUserDashboard/selfse
|
||||
const router = useRouter();
|
||||
|
||||
const openFleetLanding = async () => {
|
||||
await router.push("/superuser/configuration/edgegateway");
|
||||
await router.push("/superuser/selfserve/edge-agents");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
import EdgeGatewayManager from "@/features/edgeGateways/EdgeGatewayManager.vue";
|
||||
import EdgeGatewayFleetLanding from "@/features/edgeGateways/EdgeGatewayFleetLanding.vue";
|
||||
import PageTitle from "@/components/global/PageTitle.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import ConfigurationSubPageWrapper from "@/views/dashboards/superUserDashboard/configuration/ConfigurationSubPageWrapper.vue";
|
||||
import { normalizeEdgeGatewayError } from "@/features/edgeGateways/edgeGatewayErrors.js";
|
||||
import router from "@/router";
|
||||
|
||||
const currentRoute = computed(() => router.currentRoute.value || {});
|
||||
const selectedGatewayId = computed(() => currentRoute.value.params?.id ?? null);
|
||||
const activeView = computed(() => {
|
||||
const routeName = currentRoute.value.name;
|
||||
if (routeName === "edgegatewaytasks") {
|
||||
return "tasks";
|
||||
}
|
||||
if (routeName === "edgegatewaylogs") {
|
||||
return "logs";
|
||||
}
|
||||
if (routeName === "edgegatewaystatistics") {
|
||||
return "statistics";
|
||||
}
|
||||
if (routeName === "edgegatewayterminal") {
|
||||
return "terminal";
|
||||
}
|
||||
if (routeName === "edgegatewayinventory") {
|
||||
return "inventory";
|
||||
}
|
||||
if (routeName === "edgegatewaysettings") {
|
||||
return "settings";
|
||||
}
|
||||
return "overview";
|
||||
});
|
||||
|
||||
const moduleConfigEntries = ref([]);
|
||||
const moduleConfigMap = ref({
|
||||
@@ -51,18 +23,16 @@ const moduleState = reactive({
|
||||
unavailable: false,
|
||||
error: null,
|
||||
});
|
||||
const brokerDiagnostics = reactive({
|
||||
testing: null,
|
||||
results: {},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const canonicalPath = (gatewayId, view = "overview") =>
|
||||
gatewayId
|
||||
? `/superuser/configuration/edgegateway/${encodeURIComponent(String(gatewayId))}/${view}`
|
||||
: "/superuser/configuration/edgegateway";
|
||||
|
||||
const handleNavigation = async ({ gatewayId, view }) => {
|
||||
const nextPath = canonicalPath(gatewayId, view || "overview");
|
||||
if (window.location.pathname === nextPath) {
|
||||
return;
|
||||
}
|
||||
await router.push(nextPath);
|
||||
const brokerDiagnosticKeys = {
|
||||
internal: "internal_broker_connection",
|
||||
public: "public_broker_url",
|
||||
secret: "broker_shared_secret",
|
||||
};
|
||||
|
||||
const loadModuleConfig = async () => {
|
||||
@@ -112,6 +82,64 @@ const saveModuleConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const brokerDiagnosticPayload = (target) => ({
|
||||
target,
|
||||
broker_url: moduleConfigMap.value.broker_url || "",
|
||||
public_broker_url: moduleConfigMap.value.public_broker_url || "",
|
||||
broker_auth_mode: moduleConfigMap.value.broker_auth_mode || "manager",
|
||||
broker_shared_secret: moduleConfigMap.value.broker_shared_secret || "",
|
||||
});
|
||||
|
||||
const testBrokerConfig = async (target) => {
|
||||
const resultKey = brokerDiagnosticKeys[target];
|
||||
if (!resultKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
brokerDiagnostics.testing = target;
|
||||
brokerDiagnostics.error = null;
|
||||
|
||||
try {
|
||||
const response = await SessionUser.superUser.modules.edgegateway.config.test_broker(
|
||||
brokerDiagnosticPayload(target)
|
||||
);
|
||||
const diagnostics = response?.data?.data || {};
|
||||
brokerDiagnostics.results = {
|
||||
...brokerDiagnostics.results,
|
||||
[resultKey]: diagnostics[resultKey] || null,
|
||||
};
|
||||
} catch (error) {
|
||||
const normalized = normalizeEdgeGatewayError(error);
|
||||
brokerDiagnostics.error = normalized;
|
||||
brokerDiagnostics.results = {
|
||||
...brokerDiagnostics.results,
|
||||
[resultKey]: {
|
||||
ok: false,
|
||||
status: "request_failed",
|
||||
message: normalized.message,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
brokerDiagnostics.testing = null;
|
||||
}
|
||||
};
|
||||
|
||||
const brokerDiagnosticResult = (target) =>
|
||||
brokerDiagnostics.results[brokerDiagnosticKeys[target]] || null;
|
||||
|
||||
const brokerDiagnosticState = (target) => {
|
||||
const result = brokerDiagnosticResult(target);
|
||||
if (!result) {
|
||||
return "idle";
|
||||
}
|
||||
return result.ok ? "success" : "danger";
|
||||
};
|
||||
|
||||
const brokerDiagnosticMessage = (target) => {
|
||||
const result = brokerDiagnosticResult(target);
|
||||
return result?.message || "";
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadModuleConfig();
|
||||
});
|
||||
@@ -123,7 +151,7 @@ onMounted(async () => {
|
||||
<template #title>
|
||||
<PageTitle
|
||||
title="Edge gateway module"
|
||||
subtitle="Module defaults, fleet state, rollout visibility, and gateway operations for the edge runtime"
|
||||
subtitle="Module defaults and broker settings for the edge runtime"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -144,7 +172,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div v-if="moduleState.unavailable" class="edge-gateway-module-page__disabled" data-testid="gateway-module-disabled-state">
|
||||
The edge gateway module routes are currently unavailable. Re-enable the module before managing fleet traffic here.
|
||||
The edge gateway module routes are currently unavailable. Re-enable the module before saving edge runtime defaults here.
|
||||
</div>
|
||||
|
||||
<div v-else class="edge-gateway-module-page__grid">
|
||||
@@ -182,25 +210,65 @@ onMounted(async () => {
|
||||
<p>Connection values used by the PHP manager when it prepares browser and gateway broker traffic.</p>
|
||||
</div>
|
||||
|
||||
<label class="edge-gateway-module-page__field">
|
||||
<div class="edge-gateway-module-page__field">
|
||||
<span>Internal broker URL</span>
|
||||
<input
|
||||
v-model="moduleConfigMap.broker_url"
|
||||
class="input"
|
||||
data-testid="gateway-module-broker-url"
|
||||
placeholder="http://edge-broker:4300"
|
||||
/>
|
||||
</label>
|
||||
<div class="edge-gateway-module-page__field-control">
|
||||
<input
|
||||
v-model="moduleConfigMap.broker_url"
|
||||
class="input"
|
||||
data-testid="gateway-module-broker-url"
|
||||
placeholder="http://edge-broker:4300"
|
||||
/>
|
||||
<button
|
||||
class="button is-light"
|
||||
type="button"
|
||||
data-testid="gateway-module-test-internal-broker"
|
||||
:class="{ 'is-loading': brokerDiagnostics.testing === 'internal' }"
|
||||
:disabled="moduleState.loading || moduleState.saving || Boolean(brokerDiagnostics.testing)"
|
||||
@click="testBrokerConfig('internal')"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="brokerDiagnosticResult('internal')"
|
||||
class="edge-gateway-module-page__diagnostic"
|
||||
data-testid="gateway-module-broker-diagnostic-internal"
|
||||
:data-state="brokerDiagnosticState('internal')"
|
||||
>
|
||||
{{ brokerDiagnosticMessage("internal") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="edge-gateway-module-page__field">
|
||||
<div class="edge-gateway-module-page__field">
|
||||
<span>Public broker URL</span>
|
||||
<input
|
||||
v-model="moduleConfigMap.public_broker_url"
|
||||
class="input"
|
||||
data-testid="gateway-module-public-broker-url"
|
||||
placeholder="https://api.truckwash.io:4433/edge-broker"
|
||||
/>
|
||||
</label>
|
||||
<div class="edge-gateway-module-page__field-control">
|
||||
<input
|
||||
v-model="moduleConfigMap.public_broker_url"
|
||||
class="input"
|
||||
data-testid="gateway-module-public-broker-url"
|
||||
placeholder="https://api.truckwash.io:4433/edge-broker"
|
||||
/>
|
||||
<button
|
||||
class="button is-light"
|
||||
type="button"
|
||||
data-testid="gateway-module-test-public-broker"
|
||||
:class="{ 'is-loading': brokerDiagnostics.testing === 'public' }"
|
||||
:disabled="moduleState.loading || moduleState.saving || Boolean(brokerDiagnostics.testing)"
|
||||
@click="testBrokerConfig('public')"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="brokerDiagnosticResult('public')"
|
||||
class="edge-gateway-module-page__diagnostic"
|
||||
data-testid="gateway-module-broker-diagnostic-public"
|
||||
:data-state="brokerDiagnosticState('public')"
|
||||
>
|
||||
{{ brokerDiagnosticMessage("public") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="edge-gateway-module-page__field">
|
||||
<span>Broker auth mode</span>
|
||||
@@ -212,17 +280,37 @@ onMounted(async () => {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="edge-gateway-module-page__field">
|
||||
<div class="edge-gateway-module-page__field">
|
||||
<span>Broker shared secret</span>
|
||||
<input
|
||||
v-model="moduleConfigMap.broker_shared_secret"
|
||||
autocomplete="off"
|
||||
class="input"
|
||||
data-testid="gateway-module-broker-shared-secret"
|
||||
placeholder="Shared secret"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
<div class="edge-gateway-module-page__field-control">
|
||||
<input
|
||||
v-model="moduleConfigMap.broker_shared_secret"
|
||||
autocomplete="off"
|
||||
class="input"
|
||||
data-testid="gateway-module-broker-shared-secret"
|
||||
placeholder="Shared secret"
|
||||
type="password"
|
||||
/>
|
||||
<button
|
||||
class="button is-light"
|
||||
type="button"
|
||||
data-testid="gateway-module-test-broker-secret"
|
||||
:class="{ 'is-loading': brokerDiagnostics.testing === 'secret' }"
|
||||
:disabled="moduleState.loading || moduleState.saving || Boolean(brokerDiagnostics.testing)"
|
||||
@click="testBrokerConfig('secret')"
|
||||
>
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="brokerDiagnosticResult('secret')"
|
||||
class="edge-gateway-module-page__diagnostic"
|
||||
data-testid="gateway-module-broker-diagnostic-secret"
|
||||
:data-state="brokerDiagnosticState('secret')"
|
||||
>
|
||||
{{ brokerDiagnosticMessage("secret") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edge-gateway-module-page__actions">
|
||||
@@ -237,17 +325,6 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<EdgeGatewayFleetLanding v-if="!moduleState.unavailable && !selectedGatewayId" />
|
||||
|
||||
<EdgeGatewayManager
|
||||
v-if="!moduleState.unavailable"
|
||||
:selected-gateway-id="selectedGatewayId"
|
||||
:active-view="activeView"
|
||||
:route-driven="true"
|
||||
:allow-destructive="true"
|
||||
@navigate="handleNavigation"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</ConfigurationSubPageWrapper>
|
||||
@@ -299,6 +376,41 @@ onMounted(async () => {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__field-control {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__field-control .input,
|
||||
.edge-gateway-module-page__field-control .select {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__field-control .button {
|
||||
flex: 0 0 auto;
|
||||
min-width: 5.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__diagnostic {
|
||||
margin: 0;
|
||||
min-height: 1.15rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.35;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__diagnostic[data-state="success"] {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__diagnostic[data-state="danger"] {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__section-title {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 0.5rem;
|
||||
@@ -329,4 +441,14 @@ onMounted(async () => {
|
||||
border: 1px solid #fdba74;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.edge-gateway-module-page__field-control {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edge-gateway-module-page__field-control .button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
const openFleetLanding = async () => {
|
||||
await router.push("/superuser/configuration/edgegateway");
|
||||
await router.push("/superuser/selfserve/edge-agents");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ const testConnection = async () => {
|
||||
>
|
||||
<p><strong>{{ gatewaySummary.total }}</strong> registered gateway(s), {{ gatewaySummary.online }} online, {{ gatewaySummary.degraded }} degraded, {{ gatewaySummary.offline }} offline.</p>
|
||||
<div class="buttons mt-3">
|
||||
<button class="button is-link" @click="router.push('/superuser/configuration/edgegateway')">Open fleet manager</button>
|
||||
<button class="button is-link" @click="router.push('/superuser/selfserve/edge-agents')">Open fleet manager</button>
|
||||
<button class="button is-light" @click="load">Refresh gateway health</button>
|
||||
</div>
|
||||
</ConfigurationCategory>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import EdgeGatewayFleetLanding from "@/features/edgeGateways/EdgeGatewayFleetLanding.vue";
|
||||
import EdgeGatewayManager from "@/features/edgeGateways/EdgeGatewayManager.vue";
|
||||
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import SuperUserDashboardNavigation from "@/views/dashboards/superUserDashboard/SuperUserDashboardNavigation.vue";
|
||||
import SelfServeSuperUserTabs from "@/views/dashboards/superUserDashboard/selfserve/SelfServeSuperUserTabs.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const selectedGatewayId = computed(() => route.params?.id ?? null);
|
||||
const activeView = computed(() => {
|
||||
const routeName = String(route.name || "");
|
||||
if (routeName === "edgeagenttasks") {
|
||||
return "tasks";
|
||||
}
|
||||
if (routeName === "edgeagentlogs") {
|
||||
return "logs";
|
||||
}
|
||||
if (routeName === "edgeagentstatistics") {
|
||||
return "statistics";
|
||||
}
|
||||
if (routeName === "edgeagentterminal") {
|
||||
return "terminal";
|
||||
}
|
||||
if (routeName === "edgeagentinventory") {
|
||||
return "inventory";
|
||||
}
|
||||
if (routeName === "edgeagentsettings") {
|
||||
return "settings";
|
||||
}
|
||||
return "overview";
|
||||
});
|
||||
|
||||
const canonicalPath = (gatewayId, view = "overview") =>
|
||||
gatewayId
|
||||
? `/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/${view}`
|
||||
: "/superuser/selfserve/edge-agents";
|
||||
|
||||
const handleNavigation = async ({ gatewayId, view }) => {
|
||||
const nextPath = canonicalPath(gatewayId, view || "overview");
|
||||
if (window.location.pathname === nextPath) {
|
||||
return;
|
||||
}
|
||||
await router.push(nextPath);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RestrictedPageWrapper :hasPermission="SessionUser.canAccessSuperUser()">
|
||||
<SuperUserDashboardNavigation />
|
||||
<SelfServeSuperUserTabs />
|
||||
|
||||
<main class="edge-agents-page" data-testid="edge-agents-page">
|
||||
<EdgeGatewayFleetLanding v-if="!selectedGatewayId" />
|
||||
|
||||
<EdgeGatewayManager
|
||||
:selected-gateway-id="selectedGatewayId"
|
||||
:active-view="activeView"
|
||||
:route-driven="true"
|
||||
:allow-destructive="true"
|
||||
@navigate="handleNavigation"
|
||||
/>
|
||||
</main>
|
||||
</RestrictedPageWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.edge-agents-page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ const route = useRoute();
|
||||
const tabs = [
|
||||
{ label: "Lanes", to: "/superuser/selfserve", exact: true },
|
||||
{ label: "Sessions", to: "/superuser/selfserve/sessions", exact: false },
|
||||
{ label: "Edge Agents", to: "/superuser/selfserve/edge-agents", exact: false },
|
||||
];
|
||||
|
||||
const isActive = (tab: { to: string; exact: boolean }): boolean => (
|
||||
|
||||
@@ -14,41 +14,42 @@ test.describe("Edge gateway routing and fleet navigation", () => {
|
||||
await primeSuperuserSession(page);
|
||||
});
|
||||
|
||||
test("redirects canonical detail routes onto overview", async ({ page }) => {
|
||||
test("redirects legacy configuration detail routes onto the Edge Agents overview", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway/701");
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/overview$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/overview$/);
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
await expect(page.getByTestId("gateway-tab-nav")).toBeVisible();
|
||||
});
|
||||
|
||||
test("redirects legacy sub-page URLs onto the mapped module route", async ({ page }) => {
|
||||
test("redirects legacy sub-page URLs onto the mapped Edge Agents route", async ({ page }) => {
|
||||
await page.goto("/superuser/gateways/701/configure");
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/inventory$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/inventory$/);
|
||||
await expect(page.getByTestId("gateway-inventory-page")).toBeVisible();
|
||||
});
|
||||
|
||||
test("redirects legacy operations and manage routes onto tasks and settings", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway/701/operations");
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/tasks$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/tasks$/);
|
||||
await expect(page.getByTestId("gateway-tasks-page")).toBeVisible();
|
||||
|
||||
await page.goto("/superuser/gateways/701/manage");
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/settings$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/settings$/);
|
||||
await expect(page.getByTestId("gateway-settings-page")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows the unavailable state for unknown detail routes", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway/999999/overview");
|
||||
await page.goto("/superuser/selfserve/edge-agents/999999/overview");
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/999999\/overview$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/999999\/overview$/);
|
||||
await expect(page.getByTestId("gateway-unavailable-state")).toContainText("999999");
|
||||
});
|
||||
|
||||
test("filters the fleet roster with search and summary chips", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByRole("link", { name: "Edge Agents" })).toHaveClass(/selfserve-tabs__link--active/);
|
||||
await expect(page.getByTestId("hardware-fleet-landing")).toBeVisible();
|
||||
await page.getByTestId("gateway-fleet-search").fill("ode");
|
||||
await expect(page.getByTestId("gateway-fleet-item-702")).toBeVisible();
|
||||
@@ -79,8 +80,38 @@ test.describe("Edge gateway routing and fleet navigation", () => {
|
||||
await expect(page.getByTestId("gateway-module-broker-shared-secret")).toHaveValue("updated-broker-secret");
|
||||
});
|
||||
|
||||
test("tests broker module configuration values", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByTestId("gateway-module-test-internal-broker").click();
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-internal")).toContainText(
|
||||
"Internal broker responded"
|
||||
);
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-internal")).toHaveAttribute(
|
||||
"data-state",
|
||||
"success"
|
||||
);
|
||||
|
||||
await page.getByTestId("gateway-module-test-public-broker").click();
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-public")).toContainText("Public broker responded");
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-public")).toHaveAttribute("data-state", "success");
|
||||
|
||||
await page.getByTestId("gateway-module-test-broker-secret").click();
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-secret")).toContainText(
|
||||
"Broker accepted the configured shared secret"
|
||||
);
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-secret")).toHaveAttribute("data-state", "success");
|
||||
|
||||
await page.getByTestId("gateway-module-broker-shared-secret").fill("wrong-secret");
|
||||
await page.getByTestId("gateway-module-test-broker-secret").click();
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-secret")).toContainText(
|
||||
"Broker rejected the configured shared secret"
|
||||
);
|
||||
await expect(page.getByTestId("gateway-module-broker-diagnostic-secret")).toHaveAttribute("data-state", "danger");
|
||||
});
|
||||
|
||||
test("opens a department workspace from the fleet landing", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("hardware-fleet-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("hardware-fleet-department-1")).toContainText("Copenhagen");
|
||||
@@ -109,7 +140,7 @@ test.describe("Edge gateway routing and fleet navigation", () => {
|
||||
|
||||
await page.getByTestId("gateway-open-full-page").click();
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/overview$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/overview$/);
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
});
|
||||
});
|
||||
@@ -121,7 +152,7 @@ test("shows colored issues with suggest-fix actions in the department workspace"
|
||||
});
|
||||
await seedAuthenticatedState(page, "superuser-edge-gateway-routes-token");
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
await page.getByTestId("hardware-fleet-open-workspace-1").click();
|
||||
await expect(page.getByTestId("department-hardware-workspace")).toBeVisible();
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("gateway-error-banner")).toContainText("Column not found");
|
||||
await page.getByTestId("gateway-error-details").click();
|
||||
@@ -114,7 +114,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Copy Test Pi");
|
||||
@@ -137,7 +137,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("gateway-fleet-usage")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("2 gateways");
|
||||
@@ -163,7 +163,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-onboarding")).toContainText("Install new gateway");
|
||||
@@ -178,7 +178,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await expect
|
||||
.poll(async () => (await page.getByTestId("gateway-installer-status-step").textContent())?.trim())
|
||||
.not.toBe("Waiting to start");
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/703\/overview$/, { timeout: 20_000 });
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/703\/overview$/, { timeout: 20_000 });
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("Canary Pi");
|
||||
await expect(page.locator("body")).toContainText("Gateway connected.");
|
||||
});
|
||||
@@ -194,7 +194,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
@@ -203,7 +203,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
||||
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/701\/overview$/, { timeout: 20_000 });
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/701\/overview$/, { timeout: 20_000 });
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
await expect(page.locator("body")).toContainText("Gateway reconnected.");
|
||||
});
|
||||
@@ -239,7 +239,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
await page.getByTestId("gateway-installer-label").fill("Broken Pi");
|
||||
@@ -258,7 +258,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await expect(page.getByTestId("gateway-installer-status-diagnostics")).toContainText(
|
||||
"Compose rollout failed during build/startup"
|
||||
);
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway(?:\?.*)?$/);
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents(?:\?.*)?$/);
|
||||
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-onboarding")).toContainText("Install new gateway");
|
||||
await expect(page.getByTestId("gateway-fleet-usage-coverage")).toContainText("0 gateways");
|
||||
@@ -271,7 +271,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/overview");
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
|
||||
await page.getByTestId("gateway-tab-settings").click();
|
||||
@@ -302,7 +302,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
await expect(page.getByTestId("gateway-fleet-landing")).toBeVisible();
|
||||
|
||||
await page.getByTestId("gateway-installer-department").selectOption("1");
|
||||
@@ -310,7 +310,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await page.getByTestId("gateway-installer-generate").click();
|
||||
|
||||
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway\/703\/overview$/, { timeout: 20_000 });
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/703\/overview$/, { timeout: 20_000 });
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("Canary Pi");
|
||||
await expect(page.locator("body")).toContainText("Gateway connected.");
|
||||
});
|
||||
@@ -337,7 +337,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/overview");
|
||||
await expect(page.getByTestId("gateway-detail-header")).toContainText("CPH Edge 01");
|
||||
|
||||
await page.getByTestId("gateway-tab-statistics").click();
|
||||
@@ -455,7 +455,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/overview");
|
||||
|
||||
await expect(page.getByTestId("gateway-overview-container-health")).toContainText("ONLINE");
|
||||
await expect(page.getByTestId("gateway-overview-container-health")).toContainText("6/6 containers healthy");
|
||||
@@ -466,13 +466,25 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
|
||||
test("@smoke manages discovery, bindings, tasks, logs, statistics, uninstall, and delete", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: {
|
||||
writeText: async (text) => {
|
||||
window.__copiedGatewayContext = text;
|
||||
},
|
||||
},
|
||||
});
|
||||
window.__copiedGatewayContext = "";
|
||||
});
|
||||
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/inventory");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/inventory");
|
||||
await expect(page.getByTestId("gateway-binding-card-0")).toContainText("Roskilde machine north", {
|
||||
timeout: 15_000,
|
||||
});
|
||||
@@ -501,11 +513,34 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await page.getByTestId("gateway-tab-logs").click();
|
||||
await expect(page.getByTestId("gateway-logs-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-logs-timeline")).toContainText("GATEWAY_OPERATION_QUEUED");
|
||||
await expect(page.getByTestId("gateway-relay-logs")).toContainText("Relay SWITCH M-7 handled by local");
|
||||
await expect(page.getByTestId("gateway-relay-logs")).toContainText("MACHINE ON Roskilde Maskine handled by local");
|
||||
await expect(page.getByTestId("gateway-relay-logs")).toContainText("Open ENTRY Roskilde indkoerselspc");
|
||||
await expect(page.getByTestId("gateway-relay-logs")).toContainText("selfserve");
|
||||
await expect(page.getByTestId("gateway-log-entry-details-0")).toHaveCount(0);
|
||||
await page.getByTestId("gateway-log-entry-0").click();
|
||||
await expect(page.getByTestId("gateway-log-entry-details-0")).toContainText("Timestamp");
|
||||
await expect(page.getByTestId("gateway-log-entry-details-0")).toContainText("Level");
|
||||
await expect(page.getByTestId("gateway-relay-entry-details-0")).toHaveCount(0);
|
||||
await expect(page.getByTestId("gateway-relay-logs")).not.toContainText("Customer 700123");
|
||||
await page.getByTestId("gateway-relay-entry-0").click();
|
||||
await expect(page.getByTestId("gateway-relay-entry-details-0")).toContainText("Customer 700123");
|
||||
await expect(page.getByTestId("gateway-relay-entry-details-0")).toContainText("Context");
|
||||
await expect(page.getByTestId("gateway-relay-entry-details-0")).not.toContainText("SET_RELAY_STATE");
|
||||
await expect(page.getByTestId("gateway-relay-entry-details-0")).not.toContainText("module_responsible");
|
||||
await page.getByTestId("gateway-relay-entry-context-copy-0").click();
|
||||
await expect(page.getByTestId("gateway-relay-entry-context-copy-0")).toContainText("Copied");
|
||||
expect(await page.evaluate(() => window.__copiedGatewayContext)).toContain('"module_responsible"');
|
||||
await page.getByTestId("gateway-relay-entry-context-view-0").click();
|
||||
await expect(page.getByTestId("gateway-context-modal")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-context-modal-body")).toContainText("SET_RELAY_STATE");
|
||||
await expect(page.getByTestId("gateway-context-modal-body")).toContainText("module_responsible");
|
||||
await page.getByTestId("gateway-context-modal-close").click();
|
||||
await expect(page.getByTestId("gateway-context-modal")).toHaveCount(0);
|
||||
await expect(page.getByTestId("gateway-relay-logs")).toContainText("Customer 700123");
|
||||
await page.getByTestId("gateway-logs-type-filter").selectOption("relay");
|
||||
await expect(page.getByTestId("gateway-logs-timeline")).toContainText("Relay SWITCH M-7 handled by local");
|
||||
await expect(page.getByTestId("gateway-logs-timeline")).toContainText(
|
||||
"MACHINE ON Roskilde Maskine handled by local"
|
||||
);
|
||||
|
||||
await page.getByTestId("gateway-tab-statistics").click();
|
||||
await expect(page.getByTestId("gateway-statistics-page")).toBeVisible();
|
||||
@@ -529,7 +564,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
await page.getByTestId("gateway-delete-confirmation").fill("CPH Edge 01");
|
||||
await page.getByTestId("gateway-delete").click();
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway$/, { timeout: 8_000 });
|
||||
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents$/, { timeout: 8_000 });
|
||||
await expect(page.locator("body")).toContainText(/(Gateway deleted\.|Gateway slettet\.)/);
|
||||
});
|
||||
|
||||
@@ -992,7 +1027,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/tasks");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/tasks");
|
||||
|
||||
await page.getByTestId("gateway-operation-discovery").click();
|
||||
await expect(page.getByTestId("gateway-operation-cancel")).toBeVisible();
|
||||
@@ -1035,7 +1070,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/tasks");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/tasks");
|
||||
|
||||
await expect(page.getByTestId("gateway-recent-commands")).toContainText("GET_RELAY_STATUS");
|
||||
await expect(page.getByTestId("gateway-recent-commands")).toContainText("Timed out");
|
||||
@@ -1069,7 +1104,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/not connected to the broker/i);
|
||||
@@ -1098,7 +1133,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/Broker did not complete/i, {
|
||||
@@ -1119,7 +1154,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/connecting|open/i);
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe("Edge gateway visuals", () => {
|
||||
test("desktop roster and module overview render together", async ({ page }, testInfo) => {
|
||||
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway/701/overview");
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/overview");
|
||||
|
||||
await expect(page.getByTestId("edge-gateway-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-fleet-roster")).toBeVisible();
|
||||
@@ -26,12 +26,12 @@ test.describe("Edge gateway visuals", () => {
|
||||
await expect(page.getByTestId("gateway-diagnostics-list")).toBeVisible();
|
||||
});
|
||||
|
||||
test("mobile module landing exposes onboarding and fleet controls", async ({ page }, testInfo) => {
|
||||
test("mobile Edge Agents landing exposes onboarding and fleet controls", async ({ page }, testInfo) => {
|
||||
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("edge-gateway-module-config")).toBeVisible();
|
||||
await expect(page.getByTestId("edge-agents-page")).toBeVisible();
|
||||
await expect(page.getByTestId("hardware-fleet-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("edge-gateway-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-fleet-roster")).toBeVisible();
|
||||
@@ -42,7 +42,7 @@ test.describe("Edge gateway visuals", () => {
|
||||
test("desktop hardware landing and navigation remain visually distinct", async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== "chromium-desktop", "Chromium desktop snapshot");
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
|
||||
await expect(page.getByTestId("hardware-fleet-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("desktop-buefy-navigation")).toBeVisible();
|
||||
@@ -51,7 +51,7 @@ test.describe("Edge gateway visuals", () => {
|
||||
"edge-gateways-hardware-fleet-landing-desktop-win32.png"
|
||||
);
|
||||
await expect(page.getByTestId("desktop-buefy-navigation")).toHaveScreenshot(
|
||||
"edge-gateways-configuration-navigation-desktop-win32.png"
|
||||
"edge-agents-navigation-desktop-win32.png"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe("Edge gateway live smoke gate", () => {
|
||||
|
||||
await loginAsOperator(page, superuserCredentials);
|
||||
|
||||
await page.goto("/superuser/configuration/edgegateway");
|
||||
await page.goto("/superuser/selfserve/edge-agents");
|
||||
await settlePage(page);
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/edgegateway(?:\?.*)?$/);
|
||||
await expectBodyHasContent(page);
|
||||
@@ -42,9 +42,9 @@ test.describe("Edge gateway live smoke gate", () => {
|
||||
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
|
||||
await expect(page.locator("body")).not.toContainText(/404/i);
|
||||
|
||||
await page.goto(`/superuser/configuration/edgegateway/${gatewayId}/overview`);
|
||||
await page.goto(`/superuser/selfserve/edge-agents/${gatewayId}/overview`);
|
||||
await settlePage(page);
|
||||
await expect(page).toHaveURL(new RegExp(`/superuser/configuration/edgegateway/${gatewayId}/overview(?:\\?.*)?$`));
|
||||
await expect(page).toHaveURL(new RegExp(`/superuser/selfserve/edge-agents/${gatewayId}/overview(?:\\?.*)?$`));
|
||||
await expect(page.getByTestId("gateway-detail-header")).toBeVisible();
|
||||
await expect(page.locator("body")).not.toContainText(/404/i);
|
||||
|
||||
|
||||
@@ -492,7 +492,7 @@ test.describe("Superuser system status smoke", () => {
|
||||
await expect(gatewayCards.nth(3)).toContainText("Udruller opdatering");
|
||||
await expect(gatewayCards.nth(4)).toContainText("Discovery har ikke rapporteret i 30 minutter.");
|
||||
await expect(gatewayCards.nth(6)).toContainText("gw-307.truckwash.test");
|
||||
await expect(page.locator('a[href="/superuser/configuration/edgegateway/301/overview"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="/superuser/selfserve/edge-agents/301/overview"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="gateway-card-309"]')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1896,7 +1896,7 @@ function createHttpEdgeGatewayFixture(options = {}) {
|
||||
level: "INFO",
|
||||
stream: "relay",
|
||||
source: "RELAY_DISPATCH",
|
||||
message: "Relay SWITCH M-7 handled by local via BROKER_FAST_PATH",
|
||||
message: "MACHINE ON Roskilde Maskine handled by local via BROKER_FAST_PATH",
|
||||
context: {
|
||||
module: "selfserve",
|
||||
module_responsible: "selfserve",
|
||||
@@ -1904,6 +1904,9 @@ function createHttpEdgeGatewayFixture(options = {}) {
|
||||
handler: "local",
|
||||
delivery_channel: "BROKER_FAST_PATH",
|
||||
relay_id: "M-7",
|
||||
relay_name: "Roskilde Maskine",
|
||||
relay_role: "MACHINE",
|
||||
description: "MACHINE ON Roskilde Maskine handled by local via BROKER_FAST_PATH",
|
||||
action: "SWITCH",
|
||||
target_on: true,
|
||||
associated: {
|
||||
@@ -1924,6 +1927,38 @@ function createHttpEdgeGatewayFixture(options = {}) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 804,
|
||||
created_at: "2026-04-08 08:14:56",
|
||||
level: "INFO",
|
||||
stream: "relay",
|
||||
source: "RELAY_DISPATCH",
|
||||
message: "Relay SWITCH IN-7 handled by local via BROKER_FAST_PATH",
|
||||
context: {
|
||||
module: "selfserve",
|
||||
module_responsible: "selfserve",
|
||||
reason: "Open self-serve ENTRY gate relay",
|
||||
handler: "local",
|
||||
delivery_channel: "BROKER_FAST_PATH",
|
||||
relay_id: "IN-7",
|
||||
relay_name: "Roskilde indkoerselspc",
|
||||
relay_role: "ENTRY",
|
||||
action: "SWITCH",
|
||||
target_on: true,
|
||||
signal: {
|
||||
command_type: "SET_RELAY_STATE",
|
||||
relay_id: "IN-7",
|
||||
request: {
|
||||
relayId: "IN-7",
|
||||
on: true,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
online: true,
|
||||
on: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
operations: [
|
||||
createFixtureOperation(
|
||||
@@ -4037,7 +4072,7 @@ function buildEdgeGatewayHardwareDepartmentWorkspace(edgeGatewayFixture, departm
|
||||
actions.push({
|
||||
code: "INSTALL_GATEWAY",
|
||||
label: "Install first edge gateway",
|
||||
path: "/superuser/configuration/edgegateway",
|
||||
path: "/superuser/selfserve/edge-agents",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4270,6 +4305,60 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/edgegateway/config/broker-diagnostics") && method === "POST") {
|
||||
const body = request.postDataJSON?.() || {};
|
||||
const target = String(body.target || "all");
|
||||
const payload = {
|
||||
target,
|
||||
checked_at: "2026-04-28 10:00:00",
|
||||
broker_auth_mode: String(body.broker_auth_mode || edgeGatewayFixture.config.broker_auth_mode || "manager"),
|
||||
broker_shared_secret_configured: Boolean(
|
||||
body.broker_shared_secret || edgeGatewayFixture.config.broker_shared_secret
|
||||
),
|
||||
};
|
||||
|
||||
if (target === "internal" || target === "all") {
|
||||
payload.internal_broker_connection = {
|
||||
ok: Boolean(body.broker_url),
|
||||
status: body.broker_url ? "connected" : "not_configured",
|
||||
status_code: body.broker_url ? 200 : null,
|
||||
url: body.broker_url || null,
|
||||
message: body.broker_url
|
||||
? "Internal broker responded to the health check."
|
||||
: "Internal broker URL is not configured.",
|
||||
};
|
||||
}
|
||||
|
||||
if (target === "public" || target === "all") {
|
||||
payload.public_broker_url = {
|
||||
ok: Boolean(body.public_broker_url),
|
||||
status: body.public_broker_url ? "connected" : "not_configured",
|
||||
status_code: body.public_broker_url ? 200 : null,
|
||||
url: body.public_broker_url || null,
|
||||
message: body.public_broker_url
|
||||
? "Public broker responded to the health check."
|
||||
: "Public broker URL is not configured.",
|
||||
};
|
||||
}
|
||||
|
||||
if (target === "secret" || target === "all") {
|
||||
const validSecret =
|
||||
String(body.broker_shared_secret || "") === String(edgeGatewayFixture.config.broker_shared_secret || "");
|
||||
payload.broker_shared_secret = {
|
||||
ok: validSecret,
|
||||
status: validSecret ? "validated" : "secret_rejected",
|
||||
status_code: validSecret ? 200 : 403,
|
||||
url: body.broker_url || edgeGatewayFixture.config.broker_url || null,
|
||||
message: validSecret
|
||||
? "Broker accepted the configured shared secret."
|
||||
: "Broker rejected the configured shared secret.",
|
||||
};
|
||||
}
|
||||
|
||||
await route.fulfill(json({ data: payload }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (edgeGatewayWorkspaceDepartmentsPattern.test(pathname) && method === "GET") {
|
||||
const hardware = ensureEdgeGatewayHardwareFixture(edgeGatewayFixture);
|
||||
const summaries = (hardware?.departments || [])
|
||||
|
||||
@@ -929,7 +929,7 @@ describe("ActionSettingsWheelButton", () => {
|
||||
|
||||
expect(SessionUser.request).toHaveBeenCalledWith("/modules/edge-gateways/workspace/departments/1", "GET");
|
||||
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
|
||||
"/configuration/edgegateway/701/overview",
|
||||
"/selfserve/edge-agents/701/overview",
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@ const edgeGatewaysPageSource = readFileSync(
|
||||
join(root, "src/views/dashboards/superUserDashboard/EdgeGatewaysWorkspacePage.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const edgeAgentsSource = readFileSync(
|
||||
join(root, "src/views/dashboards/superUserDashboard/selfserve/EdgeAgents.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const selfServeTabsSource = readFileSync(
|
||||
join(root, "src/views/dashboards/superUserDashboard/selfserve/SelfServeSuperUserTabs.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const departmentWorkspaceSource = readFileSync(
|
||||
join(root, "src/features/edgeGateways/EdgeGatewayDepartmentWorkspace.vue"),
|
||||
"utf8"
|
||||
@@ -71,6 +79,8 @@ describe("edge gateway workspace contract", () => {
|
||||
expect(logsSource).toContain('data-testid="gateway-logs-page"');
|
||||
expect(logsSource).toContain('data-testid="gateway-relay-logs"');
|
||||
expect(logsSource).toContain('<option value="relay">Relay</option>');
|
||||
expect(logsSource).toContain("const relayDescription = (entry = {}) =>");
|
||||
expect(logsSource).toContain('MACHINE_PROGRAM_PICKER: "PROGRAM_PICKER"');
|
||||
expect(managerSource).toContain(':relay-logs="logRelayLogs"');
|
||||
expect(statisticsSource).toContain('data-testid="gateway-statistics-page"');
|
||||
expect(terminalSource).toContain('data-testid="gateway-terminal-page"');
|
||||
@@ -118,13 +128,19 @@ describe("edge gateway workspace contract", () => {
|
||||
expect(managerSource).toContain(':stream-status="gatewayStreamStatus"');
|
||||
});
|
||||
|
||||
it("mounts a module workspace and a safe-subset department workspace", () => {
|
||||
it("keeps module configuration separate from the Edge Agents fleet workspace", () => {
|
||||
expect(edgeGatewaysPageSource).toContain('data-testid="edge-gateway-module-config"');
|
||||
expect(edgeGatewaysPageSource).toContain("gateway-module-disabled-state");
|
||||
expect(edgeGatewaysPageSource).toContain(':route-driven="true"');
|
||||
expect(edgeGatewaysPageSource).toContain(':allow-destructive="true"');
|
||||
expect(edgeGatewaysPageSource).toContain('import router from "@/router";');
|
||||
expect(edgeGatewaysPageSource).toContain("router.currentRoute.value");
|
||||
expect(edgeGatewaysPageSource).not.toContain("EdgeGatewayManager");
|
||||
expect(edgeGatewaysPageSource).not.toContain("EdgeGatewayFleetLanding");
|
||||
expect(edgeAgentsSource).toContain('data-testid="edge-agents-page"');
|
||||
expect(edgeAgentsSource).toContain("EdgeGatewayFleetLanding");
|
||||
expect(edgeAgentsSource).toContain("EdgeGatewayManager");
|
||||
expect(edgeAgentsSource).toContain(':route-driven="true"');
|
||||
expect(edgeAgentsSource).toContain(':allow-destructive="true"');
|
||||
expect(edgeAgentsSource).toContain("/superuser/selfserve/edge-agents");
|
||||
expect(selfServeTabsSource).toContain("Edge Agents");
|
||||
expect(selfServeTabsSource).toContain("/superuser/selfserve/edge-agents");
|
||||
expect(departmentGatewaysPageSource).toContain("EdgeGatewayDepartmentWorkspace");
|
||||
expect(departmentWorkspaceSource).toContain(':route-driven="false"');
|
||||
expect(departmentWorkspaceSource).toContain(':allow-destructive="false"');
|
||||
@@ -133,32 +149,40 @@ describe("edge gateway workspace contract", () => {
|
||||
|
||||
it("registers canonical module routes and keeps legacy redirects", () => {
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway'");
|
||||
expect(routerSource).toContain("name: 'edgegatewayoverview'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/overview'");
|
||||
expect(routerSource).toContain("name: 'edgegatewaytasks'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/tasks'");
|
||||
expect(routerSource).toContain("name: 'edgegatewaylogs'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/logs'");
|
||||
expect(routerSource).toContain("name: 'edgegatewaystatistics'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/statistics'");
|
||||
expect(routerSource).toContain("name: 'edgegatewayterminal'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/terminal'");
|
||||
expect(routerSource).toContain("name: 'edgegatewayinventory'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/inventory'");
|
||||
expect(routerSource).toContain("name: 'edgegatewayoperations'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/operations'");
|
||||
expect(routerSource).toContain("component: EdgeGatewaysWorkspacePage");
|
||||
expect(routerSource).toContain("name: 'edgeagents'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents'");
|
||||
expect(routerSource).toContain("name: 'edgeagentoverview'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/overview'");
|
||||
expect(routerSource).toContain("name: 'edgeagenttasks'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/tasks'");
|
||||
expect(routerSource).toContain("name: 'edgeagentlogs'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/logs'");
|
||||
expect(routerSource).toContain("name: 'edgeagentstatistics'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/statistics'");
|
||||
expect(routerSource).toContain("name: 'edgeagentterminal'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/terminal'");
|
||||
expect(routerSource).toContain("name: 'edgeagentinventory'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/inventory'");
|
||||
expect(routerSource).toContain("name: 'edgeagentoperations'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/operations'");
|
||||
expect(routerSource).toContain("/tasks`");
|
||||
expect(routerSource).toContain("name: 'edgegatewaysettings'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/settings'");
|
||||
expect(routerSource).toContain("name: 'edgegatewaymanage'");
|
||||
expect(routerSource).toContain("path: '/superuser/configuration/edgegateway/:id/manage'");
|
||||
expect(routerSource).toContain("name: 'edgeagentsettings'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/settings'");
|
||||
expect(routerSource).toContain("name: 'edgeagentmanage'");
|
||||
expect(routerSource).toContain("path: '/superuser/selfserve/edge-agents/:id/manage'");
|
||||
expect(routerSource).toContain("/settings`");
|
||||
expect(routerSource).toContain("name: 'edgegatewayoverview'");
|
||||
expect(routerSource).toContain(
|
||||
"redirect: (to) => `/superuser/selfserve/edge-agents/${encodeURIComponent(String(to.params.id))}/overview`"
|
||||
);
|
||||
expect(routerSource).toContain("path: '/superuser/gateways'");
|
||||
expect(routerSource).toContain("configure: 'inventory'");
|
||||
expect(routerSource).toContain("assess: 'overview'");
|
||||
});
|
||||
|
||||
it("surfaces the module in superuser navigation and configuration tabs", () => {
|
||||
expect(navigationSource).toContain("/superuser/selfserve/edge-agents");
|
||||
expect(navigationSource).toContain("/superuser/configuration/edgegateway");
|
||||
expect(configurationNavigationSource).toContain("Edge Gateway");
|
||||
expect(configurationNavigationSource).toContain("/superuser/configuration/edgegateway");
|
||||
|
||||
@@ -410,7 +410,7 @@ describe("superuser system status dashboard", () => {
|
||||
expect(wrapper.get('[data-testid="gateway-summary-card-online"]').text()).toContain("4");
|
||||
expect(wrapper.get('[data-testid="gateway-summary-card-degraded"]').text()).toContain("3");
|
||||
expect(wrapper.get('[data-testid="gateway-summary-card-offline"]').text()).toContain("3");
|
||||
expect(wrapper.get('a[href="/superuser/configuration/edgegateway"]').text()).toContain("Open fleet");
|
||||
expect(wrapper.get('a[href="/superuser/selfserve/edge-agents"]').text()).toContain("Open fleet");
|
||||
|
||||
const gatewayCards = wrapper.findAll('[data-testid^="gateway-card-"]');
|
||||
expect(gatewayCards).toHaveLength(8);
|
||||
@@ -433,7 +433,7 @@ describe("superuser system status dashboard", () => {
|
||||
"Discovery has not reported in 30 minutes."
|
||||
);
|
||||
expect(wrapper.get('[data-testid="gateway-card-207"]').text()).toContain("gw-207.truckwash.test");
|
||||
expect(wrapper.find('a[href="/superuser/configuration/edgegateway/201/overview"]').exists()).toBe(true);
|
||||
expect(wrapper.find('a[href="/superuser/selfserve/edge-agents/201/overview"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="gateway-card-209"]').exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
|
||||
Reference in New Issue
Block a user