Cleanup superuser dashboard tabs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
const { t, te, locale } = useI18n();
|
||||
|
||||
const MAX_GATEWAY_CARDS = 8;
|
||||
const DEFAULT_DASHBOARD_TAB = "infrastructure";
|
||||
const gatewayStatusPriority = Object.freeze({
|
||||
OFFLINE: 0,
|
||||
DEGRADED: 1,
|
||||
@@ -77,7 +78,14 @@ const gatewayFleetUsage = ref(createEmptyGatewayFleetUsage());
|
||||
const gatewaySectionSuppressed = ref(false);
|
||||
const gatewayDepartments = ref({});
|
||||
const gatewayDepartmentsLoaded = ref(false);
|
||||
const activeDashboardTab = ref(DEFAULT_DASHBOARD_TAB);
|
||||
const showGatewaySection = computed(() => canViewGateways.value && !gatewaySectionSuppressed.value);
|
||||
const dashboardTabKeys = computed(() => [
|
||||
DEFAULT_DASHBOARD_TAB,
|
||||
...(showGatewaySection.value ? ["gateways"] : []),
|
||||
"modules",
|
||||
"sessions",
|
||||
]);
|
||||
const gatewaySummaryCards = computed(() => [
|
||||
{
|
||||
id: "total",
|
||||
@@ -187,6 +195,16 @@ const isStale = computed(() => {
|
||||
return nowTick.value - lastLoadedAt.value.getTime() > (refreshAfterSeconds.value * 2000);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [activeDashboardTab.value, dashboardTabKeys.value.join("|")],
|
||||
() => {
|
||||
if (!dashboardTabKeys.value.includes(activeDashboardTab.value)) {
|
||||
activeDashboardTab.value = DEFAULT_DASHBOARD_TAB;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const loadStatus = async ({ force = false } = {}) => {
|
||||
const [snapshotValue] = await Promise.all([
|
||||
getSuperuserSystemStatus({ force }),
|
||||
@@ -707,203 +725,250 @@ function modulePath(key) {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<section class="system-status-section">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
|
||||
<RouterLink class="section-link" to="/superuser/system/replication">
|
||||
{{ $t("system_status.actions.open_replication") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="status-card-grid">
|
||||
<article
|
||||
v-for="card in statusCards"
|
||||
:key="card.id"
|
||||
class="status-card"
|
||||
:class="statusClass(card.status)"
|
||||
:data-testid="`status-card-${card.id}`"
|
||||
>
|
||||
<div class="status-card__top">
|
||||
<p class="status-card__title">{{ card.title }}</p>
|
||||
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
|
||||
</div>
|
||||
<strong class="status-card__primary">{{ card.primary }}</strong>
|
||||
<p class="status-card__secondary">{{ card.secondary }}</p>
|
||||
<small class="status-card__detail">{{ card.detail }}</small>
|
||||
<small
|
||||
v-if="card.replicationLabel"
|
||||
class="status-card__detail status-card__replication"
|
||||
:data-testid="`status-card-${card.id}-replication`"
|
||||
>
|
||||
{{ card.replicationLabel }}
|
||||
</small>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="showGatewaySection"
|
||||
class="system-status-section"
|
||||
data-testid="system-status-gateways"
|
||||
<b-tabs
|
||||
v-model="activeDashboardTab"
|
||||
expanded
|
||||
type="is-boxed"
|
||||
class="system-status-tabs"
|
||||
data-testid="system-status-dashboard-tabs"
|
||||
>
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3>{{ $t("system_status.sections.gateways") }}</h3>
|
||||
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
|
||||
</div>
|
||||
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
|
||||
{{ $t("system_status.gateways.actions.open_fleet") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<b-tab-item value="infrastructure">
|
||||
<template #header>
|
||||
<b-icon pack="fas" icon="server" />
|
||||
<span data-testid="system-status-tab-infrastructure">
|
||||
{{ $t("system_status.sections.infrastructure") }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="gateway-summary-grid">
|
||||
<article
|
||||
v-for="card in gatewaySummaryCards"
|
||||
:key="card.id"
|
||||
class="summary-card gateway-summary-card"
|
||||
:class="card.toneClass"
|
||||
:data-testid="`gateway-summary-card-${card.id}`"
|
||||
<section class="system-status-section" data-testid="system-status-panel-infrastructure">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
|
||||
<RouterLink class="section-link" to="/superuser/system/replication">
|
||||
{{ $t("system_status.actions.open_replication") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="status-card-grid">
|
||||
<article
|
||||
v-for="card in statusCards"
|
||||
:key="card.id"
|
||||
class="status-card"
|
||||
:class="statusClass(card.status)"
|
||||
:data-testid="`status-card-${card.id}`"
|
||||
>
|
||||
<div class="status-card__top">
|
||||
<p class="status-card__title">{{ card.title }}</p>
|
||||
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
|
||||
</div>
|
||||
<strong class="status-card__primary">{{ card.primary }}</strong>
|
||||
<p class="status-card__secondary">{{ card.secondary }}</p>
|
||||
<small class="status-card__detail">{{ card.detail }}</small>
|
||||
<small
|
||||
v-if="card.replicationLabel"
|
||||
class="status-card__detail status-card__replication"
|
||||
:data-testid="`status-card-${card.id}-replication`"
|
||||
>
|
||||
{{ card.replicationLabel }}
|
||||
</small>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</b-tab-item>
|
||||
|
||||
<b-tab-item v-if="showGatewaySection" value="gateways">
|
||||
<template #header>
|
||||
<b-icon pack="fas" icon="network-wired" />
|
||||
<span data-testid="system-status-tab-gateways">
|
||||
{{ $t("system_status.sections.gateways") }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<section
|
||||
class="system-status-section"
|
||||
data-testid="system-status-gateways"
|
||||
>
|
||||
<p class="summary-label">{{ card.title }}</p>
|
||||
<strong>{{ card.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="gatewayError"
|
||||
class="notification is-warning is-light gateway-notification"
|
||||
data-testid="gateway-section-error"
|
||||
>
|
||||
{{ $t("system_status.gateways.error") }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="gatewayLoading && !gatewayCards.length"
|
||||
class="notification is-light gateway-notification"
|
||||
>
|
||||
{{ $t("system_status.gateways.loading") }}
|
||||
</div>
|
||||
|
||||
<div v-if="gatewayCards.length" class="gateway-grid">
|
||||
<article
|
||||
v-for="gateway in gatewayCards"
|
||||
:key="gateway.id"
|
||||
class="gateway-card"
|
||||
:class="gateway.toneClass"
|
||||
:data-testid="`gateway-card-${gateway.id}`"
|
||||
>
|
||||
<div class="gateway-card__top">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
|
||||
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
|
||||
<h3>{{ $t("system_status.sections.gateways") }}</h3>
|
||||
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
|
||||
</div>
|
||||
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
|
||||
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
|
||||
{{ $t("system_status.gateways.actions.open_fleet") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="gateway-card__meta">
|
||||
<span>
|
||||
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
|
||||
</span>
|
||||
<span v-if="gateway.activeOperationLabel">
|
||||
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
|
||||
</span>
|
||||
<div class="gateway-summary-grid">
|
||||
<article
|
||||
v-for="card in gatewaySummaryCards"
|
||||
:key="card.id"
|
||||
class="summary-card gateway-summary-card"
|
||||
:class="card.toneClass"
|
||||
:data-testid="`gateway-summary-card-${card.id}`"
|
||||
>
|
||||
<p class="summary-label">{{ card.title }}</p>
|
||||
<strong>{{ card.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-if="gateway.message" class="gateway-card__message">
|
||||
{{ gateway.message }}
|
||||
</p>
|
||||
|
||||
<RouterLink :to="gateway.link" class="module-card__link">
|
||||
{{ $t("system_status.gateways.actions.open_gateway") }}
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!gatewayLoading && !gatewayError"
|
||||
class="gateway-empty"
|
||||
data-testid="gateway-empty-state"
|
||||
>
|
||||
{{ $t("system_status.gateways.empty") }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="system-status-section">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.modules") }}</h3>
|
||||
</div>
|
||||
<div class="module-grid">
|
||||
<article
|
||||
v-for="module in modules"
|
||||
:key="module.key"
|
||||
class="module-card"
|
||||
:class="statusClass(module.status)"
|
||||
:data-testid="`module-card-${module.key}`"
|
||||
>
|
||||
<div class="module-card__top">
|
||||
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
|
||||
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
|
||||
<div
|
||||
v-if="gatewayError"
|
||||
class="notification is-warning is-light gateway-notification"
|
||||
data-testid="gateway-section-error"
|
||||
>
|
||||
{{ $t("system_status.gateways.error") }}
|
||||
</div>
|
||||
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
|
||||
{{ moduleReasonText(module) }}
|
||||
</p>
|
||||
<div class="module-card__meta">
|
||||
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
|
||||
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
|
||||
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
|
||||
|
||||
<div
|
||||
v-if="gatewayLoading && !gatewayCards.length"
|
||||
class="notification is-light gateway-notification"
|
||||
>
|
||||
{{ $t("system_status.gateways.loading") }}
|
||||
</div>
|
||||
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
|
||||
{{ $t("system_status.actions.open_config") }}
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="system-status-section">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.sessions") }}</h3>
|
||||
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
|
||||
</div>
|
||||
<div v-if="gatewayCards.length" class="gateway-grid">
|
||||
<article
|
||||
v-for="gateway in gatewayCards"
|
||||
:key="gateway.id"
|
||||
class="gateway-card"
|
||||
:class="gateway.toneClass"
|
||||
:data-testid="`gateway-card-${gateway.id}`"
|
||||
>
|
||||
<div class="gateway-card__top">
|
||||
<div>
|
||||
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
|
||||
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
|
||||
</div>
|
||||
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="table-container sessions-table">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("system_status.table.active") }}</th>
|
||||
<th>{{ $t("system_status.table.name") }}</th>
|
||||
<th>{{ $t("system_status.table.type") }}</th>
|
||||
<th>{{ $t("system_status.table.device") }}</th>
|
||||
<th>{{ $t("system_status.table.route") }}</th>
|
||||
<th>{{ $t("system_status.table.first_seen") }}</th>
|
||||
<th>{{ $t("system_status.table.last_seen") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
|
||||
<td>
|
||||
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
|
||||
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
|
||||
<div class="gateway-card__meta">
|
||||
<span>
|
||||
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ sessionDisplayName(session) }}</strong>
|
||||
<div class="session-context">{{ sessionContextLabel(session) }}</div>
|
||||
</td>
|
||||
<td>{{ formatSessionKind(session.session_kind) }}</td>
|
||||
<td>{{ formatDeviceType(session.device_type) }}</td>
|
||||
<td class="session-route">{{ session.last_route || "--" }}</td>
|
||||
<td>{{ formatDate(session.first_seen_at) }}</td>
|
||||
<td>{{ formatDate(session.last_seen_at) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!sessions.recent_sessions?.length">
|
||||
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<span>
|
||||
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
|
||||
</span>
|
||||
<span v-if="gateway.activeOperationLabel">
|
||||
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="gateway.message" class="gateway-card__message">
|
||||
{{ gateway.message }}
|
||||
</p>
|
||||
|
||||
<RouterLink :to="gateway.link" class="module-card__link">
|
||||
{{ $t("system_status.gateways.actions.open_gateway") }}
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!gatewayLoading && !gatewayError"
|
||||
class="gateway-empty"
|
||||
data-testid="gateway-empty-state"
|
||||
>
|
||||
{{ $t("system_status.gateways.empty") }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</b-tab-item>
|
||||
|
||||
<b-tab-item value="modules">
|
||||
<template #header>
|
||||
<b-icon pack="fas" icon="puzzle-piece" />
|
||||
<span data-testid="system-status-tab-modules">
|
||||
{{ $t("system_status.sections.modules") }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<section class="system-status-section" data-testid="system-status-panel-modules">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.modules") }}</h3>
|
||||
</div>
|
||||
<div class="module-grid">
|
||||
<article
|
||||
v-for="module in modules"
|
||||
:key="module.key"
|
||||
class="module-card"
|
||||
:class="statusClass(module.status)"
|
||||
:data-testid="`module-card-${module.key}`"
|
||||
>
|
||||
<div class="module-card__top">
|
||||
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
|
||||
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
|
||||
</div>
|
||||
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
|
||||
{{ moduleReasonText(module) }}
|
||||
</p>
|
||||
<div class="module-card__meta">
|
||||
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
|
||||
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
|
||||
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
|
||||
</div>
|
||||
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
|
||||
{{ $t("system_status.actions.open_config") }}
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</b-tab-item>
|
||||
|
||||
<b-tab-item value="sessions">
|
||||
<template #header>
|
||||
<b-icon pack="fas" icon="users" />
|
||||
<span data-testid="system-status-tab-sessions">
|
||||
{{ $t("system_status.sections.sessions") }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<section class="system-status-section" data-testid="system-status-panel-sessions">
|
||||
<div class="section-heading">
|
||||
<h3>{{ $t("system_status.sections.sessions") }}</h3>
|
||||
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="table-container sessions-table">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("system_status.table.active") }}</th>
|
||||
<th>{{ $t("system_status.table.name") }}</th>
|
||||
<th>{{ $t("system_status.table.type") }}</th>
|
||||
<th>{{ $t("system_status.table.device") }}</th>
|
||||
<th>{{ $t("system_status.table.route") }}</th>
|
||||
<th>{{ $t("system_status.table.first_seen") }}</th>
|
||||
<th>{{ $t("system_status.table.last_seen") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
|
||||
<td>
|
||||
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
|
||||
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ sessionDisplayName(session) }}</strong>
|
||||
<div class="session-context">{{ sessionContextLabel(session) }}</div>
|
||||
</td>
|
||||
<td>{{ formatSessionKind(session.session_kind) }}</td>
|
||||
<td>{{ formatDeviceType(session.device_type) }}</td>
|
||||
<td class="session-route">{{ session.last_route || "--" }}</td>
|
||||
<td>{{ formatDate(session.first_seen_at) }}</td>
|
||||
<td>{{ formatDate(session.last_seen_at) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!sessions.recent_sessions?.length">
|
||||
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</b-tab-item>
|
||||
</b-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -924,10 +989,10 @@ function modulePath(key) {
|
||||
.module-card,
|
||||
.gateway-card {
|
||||
border: 1px solid #d7dde7;
|
||||
border-radius: 18px;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.1rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.06);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.summary-card--overall {
|
||||
@@ -991,6 +1056,23 @@ function modulePath(key) {
|
||||
color: #0f4c81;
|
||||
}
|
||||
|
||||
.system-status-tabs {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.system-status-tabs :deep(.tab-content) {
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
.system-status-tabs :deep(.tabs ul) {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.system-status-tabs :deep(.tabs a) {
|
||||
min-height: 2.75rem;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.status-card__top,
|
||||
.module-card__top {
|
||||
display: flex;
|
||||
@@ -1107,7 +1189,7 @@ function modulePath(key) {
|
||||
|
||||
.gateway-empty {
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 18px;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.1rem;
|
||||
color: #475569;
|
||||
background: rgba(248, 250, 252, 0.8);
|
||||
|
||||
@@ -1,61 +1,12 @@
|
||||
<script setup>
|
||||
import { useRouter} from "vue-router";
|
||||
import { ref } from 'vue';
|
||||
import { showAuthSignOutForm } from '@/components/forms/auth/authSignOutForm.vue';
|
||||
import UserNavigation from "@/components/global/UserNavigation.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import NavigationMenuSuperUser from "@/components/models/navigation/menus/NavigationMenuSuperUser.vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const tabs = [
|
||||
{ name: t('superuser_dashboard.nav.dashboard'), path: '/superuser' },
|
||||
{ name: t('superuser_dashboard.nav.statistics'), path: '/superuser/statistics' },
|
||||
{ name: t('common.departments'), path: '/superuser/departments' },
|
||||
{ name: t('superuser_dashboard.nav.employees'), path: '/superuser/users' },
|
||||
{ name: t('superuser_dashboard.nav.customers'), path: '/superuser/customers' },
|
||||
{ name: t('superuser_dashboard.nav.complaints'), path: '/superuser/complaints' },
|
||||
{ name: t('common.products'), path: '/superuser/products' },
|
||||
{ name: t('common.invoices'), path: '/superuser/invoices' },
|
||||
{ name: t('superuser_dashboard.nav.transaction_history'), path: '/superuser/orders' },
|
||||
{ name: t('superuser_dashboard.nav.scanners'), path: '/superuser/scanners' },
|
||||
{ name: t('superuser_dashboard.nav.roles'), path: '/superuser/roles' },
|
||||
{ name: t('common.vehicles'), path: '/superuser/vehicles' },
|
||||
{ name: SessionUser.objects.categories.meta.title, path: '/superuser/categories' },
|
||||
{ name: t('superuser_dashboard.nav.xlvask'), path: '/superuser/xlvask' },
|
||||
{ name: t('superuser_dashboard.nav.configuration'), path: '/superuser/configuration' }
|
||||
];
|
||||
|
||||
// Get the current path
|
||||
const currentPath = ref(router.currentRoute.value.path);
|
||||
|
||||
// Get the index of the active tab
|
||||
const activeTab = tabs.findIndex(tab => tab.path === currentPath.value);
|
||||
|
||||
// Change the tab
|
||||
const changeTab = (index) => {
|
||||
router.push(tabs[index].path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UserNavigation />
|
||||
<NavigationMenuSuperUser />
|
||||
<!--
|
||||
<div class="tabs is-right">
|
||||
<ul>
|
||||
<li v-for="(tab, index) in tabs" :key="index" :class="{'is-active': activeTab === index}" @click="changeTab(index)">
|
||||
<a>{{ tab.name }}</a>
|
||||
</li>
|
||||
Sign out button
|
||||
<li>
|
||||
<a @click="showAuthSignOutForm">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -784,14 +784,17 @@ test.describe("Superuser system status smoke", () => {
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser$/);
|
||||
await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
|
||||
await expect(page.getByTestId("system-status-dashboard-tabs")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
|
||||
await expect(page.getByTestId("status-card-database")).toContainText("truckwash", { timeout: PAGE_READY_TIMEOUT });
|
||||
await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering");
|
||||
await expect(page.getByTestId("status-card-minio-replication")).toContainText("67.4% replikering");
|
||||
await page.getByTestId("system-status-tab-sessions").click();
|
||||
await expect(page.locator("body")).toContainText("Acme Logistics");
|
||||
await expect(page.locator("body")).toContainText("Kunde");
|
||||
await expect(page.locator("body")).toContainText("Bruger");
|
||||
await expect(page.locator("body")).toContainText("Computer");
|
||||
await expect(page.locator("body")).toContainText("/superuser/vehicles");
|
||||
await page.getByTestId("system-status-tab-modules").click();
|
||||
await expect(page.locator("body")).toContainText("Forbindelse til OpenAI API bekræftet.");
|
||||
await expect(page.locator("body")).toContainText("Redis er utilgængelig; cache til modulprober omgås.");
|
||||
await expect(page.locator("body")).not.toContainText("OpenAI API connectivity confirmed.");
|
||||
@@ -975,6 +978,7 @@ test.describe("Superuser system status smoke", () => {
|
||||
await installSystemStatusMock(page, longReasonSnapshot);
|
||||
|
||||
await page.goto("/superuser");
|
||||
await page.getByTestId("system-status-tab-modules").click();
|
||||
|
||||
const card = page.getByTestId("module-card-shelly");
|
||||
const title = card.locator(".module-card__title");
|
||||
@@ -1004,6 +1008,7 @@ test.describe("Superuser system status smoke", () => {
|
||||
await installSystemStatusMock(page, longTitleSnapshot);
|
||||
|
||||
await page.goto("/superuser");
|
||||
await page.getByTestId("system-status-tab-modules").click();
|
||||
|
||||
const card = page.getByTestId("module-card-licenseplaterecognizer");
|
||||
const title = card.locator(".module-card__title");
|
||||
@@ -1133,6 +1138,7 @@ test.describe("Superuser system status smoke", () => {
|
||||
});
|
||||
|
||||
await page.goto("/superuser");
|
||||
await page.getByTestId("system-status-tab-gateways").click();
|
||||
|
||||
await expect(page.getByTestId("system-status-gateways")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
|
||||
await expect(page.getByTestId("gateway-summary-card-total")).toContainText("10");
|
||||
|
||||
@@ -226,6 +226,11 @@ const flushDashboardLoad = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openDashboardTab = async (wrapper, tab) => {
|
||||
await wrapper.get(`[data-testid="system-status-tab-${tab}"]`).trigger("click");
|
||||
await flushRendering();
|
||||
};
|
||||
|
||||
const resetStatusStore = () => {
|
||||
SuperUserSystemStatusObject.snapshot.value = null;
|
||||
SuperUserSystemStatusObject.loading.value = false;
|
||||
@@ -277,7 +282,7 @@ describe("superuser system status dashboard", () => {
|
||||
resetSessionUser();
|
||||
});
|
||||
|
||||
it("renders infrastructure, modules, warnings, and recent sessions from the shared snapshot", async () => {
|
||||
it("renders infrastructure, modules, warnings, and recent sessions from the shared snapshot tabs", async () => {
|
||||
installDashboardMocks();
|
||||
|
||||
const wrapper = mountWithApp(SystemStatusDashboard, {
|
||||
@@ -291,10 +296,16 @@ describe("superuser system status dashboard", () => {
|
||||
expect(wrapper.get('[data-testid="status-card-database"]').text()).toContain("truckwash");
|
||||
expect(wrapper.get('[data-testid="status-card-database-replication"]').text()).toContain("100.0% replication");
|
||||
expect(wrapper.get('a[href="/superuser/system/replication"]').exists()).toBe(true);
|
||||
expect(wrapper.get('[data-testid="system-status-dashboard-tabs"]').exists()).toBe(true);
|
||||
expect(wrapper.get('[data-testid="system-status-panel-infrastructure"]').isVisible()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Redis is unavailable; module probe caching is bypassed.");
|
||||
|
||||
await openDashboardTab(wrapper, "modules");
|
||||
expect(wrapper.get('a[href="/superuser/configuration/openai"]').exists()).toBe(true);
|
||||
|
||||
await openDashboardTab(wrapper, "sessions");
|
||||
expect(wrapper.text()).toContain("Acme Logistics");
|
||||
expect(wrapper.text()).toContain("/superuser/vehicles");
|
||||
expect(wrapper.text()).toContain("Redis is unavailable; module probe caching is bypassed.");
|
||||
expect(wrapper.get('a[href="/superuser/configuration/openai"]').exists()).toBe(true);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
@@ -333,6 +344,30 @@ describe("superuser system status dashboard", () => {
|
||||
expect(authenticatedRequestMock).toHaveBeenCalledWith("/superuser/system/status", "GET", {});
|
||||
expect(authenticatedRequestMock.mock.calls.some(([path]) => path === "/edge-gateways")).toBe(false);
|
||||
expect(wrapper.find('[data-testid="system-status-gateways"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("returns to infrastructure when the active gateway tab becomes unavailable", async () => {
|
||||
installDashboardMocks({
|
||||
gateways: [createGateway(201, { label: "Gateway Atlas" })],
|
||||
});
|
||||
|
||||
const wrapper = mountWithApp(SystemStatusDashboard, {
|
||||
messages: { en: enMessages },
|
||||
});
|
||||
|
||||
await flushDashboardLoad();
|
||||
await openDashboardTab(wrapper, "gateways");
|
||||
|
||||
expect(wrapper.get('[data-testid="system-status-gateways"]').isVisible()).toBe(true);
|
||||
|
||||
SessionUser.permissions.value = ["user"];
|
||||
await flushRendering();
|
||||
|
||||
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
|
||||
expect(wrapper.get('[data-testid="system-status-panel-infrastructure"]').isVisible()).toBe(true);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
@@ -425,6 +460,7 @@ describe("superuser system status dashboard", () => {
|
||||
});
|
||||
|
||||
await flushDashboardLoad();
|
||||
await openDashboardTab(wrapper, "gateways");
|
||||
|
||||
expect(wrapper.get('[data-testid="gateway-summary-card-total"]').text()).toContain("10");
|
||||
expect(wrapper.get('[data-testid="gateway-summary-card-online"]').text()).toContain("4");
|
||||
@@ -472,6 +508,7 @@ describe("superuser system status dashboard", () => {
|
||||
|
||||
expect(authenticatedRequestMock.mock.calls.some(([path]) => path === "/edge-gateways")).toBe(true);
|
||||
expect(wrapper.find('[data-testid="system-status-gateways"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
|
||||
expect(wrapper.get('[data-testid="status-card-database"]').text()).toContain("truckwash");
|
||||
|
||||
wrapper.unmount();
|
||||
@@ -487,6 +524,7 @@ describe("superuser system status dashboard", () => {
|
||||
});
|
||||
|
||||
await flushDashboardLoad();
|
||||
await openDashboardTab(wrapper, "gateways");
|
||||
|
||||
expect(wrapper.get('[data-testid="system-status-gateways"]').exists()).toBe(true);
|
||||
expect(wrapper.get('[data-testid="gateway-section-error"]').text()).toContain(
|
||||
|
||||
Reference in New Issue
Block a user