mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Show Mail Gateway alerts on the gateway they belong to (#2490)
A gateway Proxmox reported online could be failing its job: mail held in the queue past the configured age. The Mail Gateway table ignored alerts, so that gateway rendered green and counted as Healthy while the problem sat only on the Alerts page. Alerts are keyed by the PMG instance id and rows by the unified id, so nothing joined them. Rows now match unacknowledged alerts across the gateway's identity aliases. An alerted, reachable gateway reads and filters as needing attention (red when an alert is critical) and names the alert type under its name. Its expansion leads with the exact alert message. On phones, six tracks truncated "mail-gateway-eu" and "mail-gateway-us" to the same text; the node count moves into the expansion so names stay whole.
This commit is contained in:
@@ -4825,7 +4825,10 @@ acknowledged severity, and reveals overflow through an in-place accessible
|
||||
disclosure rather than an inert hidden-count row. Aggregate drawers that include
|
||||
alerts from child resources must also preserve the affected resource identity
|
||||
and alert metric beside that text rather than collapsing them into generic VM
|
||||
or host labels. The remaining Overview rows are additive
|
||||
or host labels. The Mail Gateway drawer receives its row's open alerts, matched
|
||||
across the gateway's identity aliases, and its header status follows them, so
|
||||
it never shows "Healthy" beside an active warning; its inline detail row sets
|
||||
`whitespace-normal` because platform table cells do not wrap. The remaining Overview rows are additive
|
||||
operator context that the parent table row cannot carry, such as OS/runtime,
|
||||
Pulse observation or action coverage, primary reachability, protection gaps,
|
||||
pending updates, or an identified service. Routine health, placement, and
|
||||
|
||||
@@ -1146,6 +1146,16 @@ the daemon tally as width shrinks, keeps one five-value phone projection
|
||||
(cluster, health, OSDs, pools, capacity) whose values each fit whole, and
|
||||
leaves quorum membership and the FSID to the cluster disclosure, where the
|
||||
FSID stays searchable and is shown in full.
|
||||
The Mail Gateway row reflects its open alerts, not only the provider status:
|
||||
a reachable gateway with an unacknowledged alert (mail queued past its age
|
||||
threshold, a backlog) renders and filters as needing attention, names the alert
|
||||
type under the gateway name, and its expansion leads with the shared drawer
|
||||
attention section carrying the exact alert message. Alerts are matched across
|
||||
the row's identity aliases, because they are keyed by the PMG instance id while
|
||||
the row carries the unified id. Its phone projection is the five-track
|
||||
instance, uptime, mail, queue and deferred set with a 40 percent identity, so
|
||||
gateways that share a name prefix stay distinguishable; node count lives in the
|
||||
expansion.
|
||||
The Proxmox host table drops temperature from its phone projection
|
||||
(container widths under 440px) so the node name, expansion toggle, and
|
||||
status dot share the same five-column shape as the guest table beneath it,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "27f020f033119d589150ae90f4570a7d8a579132",
|
||||
"verified_at": "2026-10-05T03:34:23Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "0b57910805f48b86ab1823e61e7fee52e5e514632cf53fcdb1490f39c17eed83",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "8032d6deec8859124834cf66bec7e12dbbeb430b9f25710ba64f5ca71811feb9"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/mail"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"mail-gateway-eu online with an open warning alert (messages queued 41 minutes, threshold 30): amber dot, row tint, Message Age under its name, filter counts Healthy 0 / Degraded 2",
|
||||
"Before: the same gateway rendered green and counted Healthy",
|
||||
"Phone 390 and 340: Instance, Age, In, Q, Def with whole, distinguishable gateway names; before both read mail-gatew...",
|
||||
"Expanded drawer at 1440 and 390: amber header dot, Needs attention section first with the full alert message wrapping, statistics and cluster nodes below",
|
||||
"No console errors"
|
||||
],
|
||||
"interactions": [
|
||||
"Expanded mail-gateway-eu at desktop and phone widths",
|
||||
"Compared rendered state against the active alert list from /api/alerts/active"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { For, Show, createMemo, createResource, type Component } from 'solid-js';
|
||||
import { DrawerAttentionSection } from '@/components/shared/DrawerAttentionSection';
|
||||
import { alertTypeDisplayLabel } from '@/features/alerts/helpers';
|
||||
import type { Alert } from '@/types/api';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading';
|
||||
import { InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
@@ -217,6 +220,8 @@ function InOutBar(props: {
|
||||
export const ProxmoxMailGatewayDrawer: Component<{
|
||||
instanceRow: Resource;
|
||||
onClose?: () => void;
|
||||
/** Open alerts for this gateway, matched across its identity aliases by the table. */
|
||||
alerts?: Alert[];
|
||||
}> = (props) => {
|
||||
const id = () => {
|
||||
const meta = props.instanceRow.pmg;
|
||||
@@ -236,7 +241,19 @@ export const ProxmoxMailGatewayDrawer: Component<{
|
||||
);
|
||||
const relayDomains = createMemo(() => instance()?.relayDomains ?? []);
|
||||
|
||||
const health = () => classifyHealth(instance()?.status ?? props.instanceRow.status);
|
||||
// A reachable gateway with open alerts reads as needing attention, matching
|
||||
// its table row, rather than a green "Healthy" beside the alert below.
|
||||
const health = () => {
|
||||
const base = classifyHealth(instance()?.status ?? props.instanceRow.status);
|
||||
const open = (props.alerts ?? []).filter((alert) => !alert.acknowledged);
|
||||
if (open.length === 0 || base.variant === 'danger') return base;
|
||||
return {
|
||||
variant: open.some((alert) => alert.level === 'critical')
|
||||
? ('danger' as const)
|
||||
: ('warning' as const),
|
||||
label: 'Needs attention',
|
||||
};
|
||||
};
|
||||
const name = () =>
|
||||
asTrimmedString(instance()?.name) ||
|
||||
asTrimmedString(props.instanceRow.name) ||
|
||||
@@ -330,6 +347,18 @@ export const ProxmoxMailGatewayDrawer: Component<{
|
||||
)}
|
||||
</Show>
|
||||
|
||||
{/* The exact problem first: a phone cannot hover the row's short reason. */}
|
||||
<DrawerAttentionSection
|
||||
items={(props.alerts ?? []).map((alert) => ({
|
||||
id: alert.id,
|
||||
message: alert.message,
|
||||
subject: name(),
|
||||
metric: alertTypeDisplayLabel(alert.type),
|
||||
severity: alert.level,
|
||||
acknowledged: alert.acknowledged,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Show
|
||||
when={!instance.error}
|
||||
fallback={
|
||||
|
||||
@@ -3,6 +3,12 @@ import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { getAlertsForResource } from '@/utils/alerts';
|
||||
import { getResourceIdentityAliases } from '@/utils/resourceIdentity';
|
||||
import { alertTypeDisplayLabel } from '@/features/alerts/helpers';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import type { Alert } from '@/types/api';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
@@ -38,9 +44,11 @@ export type MailGatewayPhoneColumn =
|
||||
export type MailGatewayColumn =
|
||||
MailGatewayPhoneColumn | 'version' | 'spam' | 'virus' | 'quarantine';
|
||||
|
||||
// Phones carry five tracks. With node count as a sixth, gateway names that
|
||||
// share a prefix ("mail-gateway-eu", "mail-gateway-us") truncated to the same
|
||||
// text; the count stays in the row expansion.
|
||||
export const MAIL_GATEWAY_PHONE_COLUMNS: readonly MailGatewayPhoneColumn[] = [
|
||||
'instance',
|
||||
'nodes',
|
||||
'uptime',
|
||||
'mail',
|
||||
'queue',
|
||||
@@ -56,12 +64,12 @@ export const MAIL_GATEWAY_NARROW_PHONE_COLUMNS: readonly MailGatewayPhoneColumn[
|
||||
];
|
||||
|
||||
export const MAIL_GATEWAY_PHONE_COLUMN_WIDTHS: Readonly<Record<MailGatewayPhoneColumn, number>> = {
|
||||
instance: 30,
|
||||
nodes: 12,
|
||||
uptime: 16,
|
||||
mail: 14,
|
||||
queue: 14,
|
||||
deferred: 14,
|
||||
instance: 40,
|
||||
nodes: 0,
|
||||
uptime: 15,
|
||||
mail: 15,
|
||||
queue: 15,
|
||||
deferred: 15,
|
||||
};
|
||||
|
||||
export const MAIL_GATEWAY_NARROW_PHONE_COLUMN_WIDTHS: Readonly<
|
||||
@@ -104,10 +112,59 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
}> = (props) => {
|
||||
// A gateway the provider reports online can still be failing its job (mail
|
||||
// held in the queue past the configured age, a backlog). Its open alerts
|
||||
// decide the row's state, the status filter counts and the reason shown
|
||||
// under its name, so "is mail flowing" is answered here, not only on Alerts.
|
||||
const { activeAlerts } = useWebSocket();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
// Alerts are keyed by the PMG instance id while the row carries the unified
|
||||
// id, so match across the resource's identity aliases (alerts contract).
|
||||
// There is deliberately no node-name fallback: PMG node alerts carry node
|
||||
// names, and matching on them could pin one gateway's alert on another.
|
||||
const alertIdsFor = (resource: Resource): string[] => [
|
||||
resource.id,
|
||||
...getResourceIdentityAliases(resource),
|
||||
];
|
||||
// One scan of the active-alert map per gateway, shared by the filter, the
|
||||
// status counts, the row and its drawer.
|
||||
const openAlertsById = createMemo(() => {
|
||||
const byId = new Map<string, Alert[]>();
|
||||
for (const resource of props.resources) {
|
||||
byId.set(
|
||||
resource.id,
|
||||
getAlertsForResource(
|
||||
alertIdsFor(resource),
|
||||
activeAlerts,
|
||||
alertsActivation.detectionEnabled(),
|
||||
)
|
||||
.filter((alert) => !alert.acknowledged)
|
||||
.sort((a, b) => Number(b.level === 'critical') - Number(a.level === 'critical')),
|
||||
);
|
||||
}
|
||||
return byId;
|
||||
});
|
||||
const openAlertsFor = (resource: Resource): Alert[] => openAlertsById().get(resource.id) ?? [];
|
||||
// Filtering keeps the provider's buckets: an alerted, reachable gateway
|
||||
// counts under attention (degraded), an unreachable one stays offline.
|
||||
const effectiveStatus = (resource: Resource): string | undefined => {
|
||||
if (openAlertsFor(resource).length === 0) return resource.status;
|
||||
return getSimpleStatusIndicator(resource.status).variant === 'danger'
|
||||
? resource.status
|
||||
: 'warning';
|
||||
};
|
||||
// The dot follows the most severe open alert, so a critical one reads red.
|
||||
const indicatorFor = (resource: Resource) => {
|
||||
const base = getSimpleStatusIndicator(effectiveStatus(resource));
|
||||
return openAlertsFor(resource)[0]?.level === 'critical'
|
||||
? { ...base, variant: 'danger' as const }
|
||||
: base;
|
||||
};
|
||||
const tableState = createPlatformTableFilterState({
|
||||
resources: () => props.resources,
|
||||
initialStatus: 'all' as PlatformResourceStatusFilter,
|
||||
filter: filterPlatformResources,
|
||||
filter: (resources, search, status) =>
|
||||
filterPlatformResources(resources, search, status, effectiveStatus),
|
||||
});
|
||||
const detail = createPlatformResourceDetailState({ idPrefix: 'proxmox-mail-gateway-detail' });
|
||||
const observedWidth = useObservedElementWidth();
|
||||
@@ -118,9 +175,9 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
const width = observedWidth.width();
|
||||
return typeof width === 'number' && width > 0 && width < 360;
|
||||
});
|
||||
// Uptime and mail-flow counters remain in the narrow projection; node count
|
||||
// moves into the existing instance detail expansion below 360px.
|
||||
const showNodes = createMemo(() => !isNarrowPhone());
|
||||
// Uptime and mail-flow counters remain in the phone projection; node count
|
||||
// moves into the existing instance detail expansion on phones.
|
||||
const showNodes = createMemo(() => layout() !== 'compact');
|
||||
const showUptime = createMemo(() => true);
|
||||
const showOperational = createMemo(() => ['operational', 'expanded', 'full'].includes(layout()));
|
||||
const showVirus = createMemo(() => ['expanded', 'full'].includes(layout()));
|
||||
@@ -262,7 +319,16 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
const pmg = () => instance.pmg;
|
||||
const name = () => asTrimmedString(instance.name) || instance.id;
|
||||
const version = () => asTrimmedString(pmg()?.version) || '—';
|
||||
const indicator = () => getSimpleStatusIndicator(instance.status);
|
||||
const rowAlerts = createMemo(() => openAlertsFor(instance));
|
||||
const indicator = () => indicatorFor(instance);
|
||||
// Same tint as the other platform tables, from the cached alerts.
|
||||
const rowAlertBg = () => {
|
||||
const top = rowAlerts()[0];
|
||||
if (!top) return '';
|
||||
return top.level === 'critical'
|
||||
? 'bg-red-50 dark:bg-red-950/25'
|
||||
: 'bg-yellow-50 dark:bg-yellow-950/25';
|
||||
};
|
||||
const isOpen = () => detail.isExpanded(instance);
|
||||
const detailRowId = () => detail.detailRowId(instance);
|
||||
return (
|
||||
@@ -271,6 +337,7 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
{...getPlatformResourceDetailRowInteractionProps({
|
||||
expanded: isOpen(),
|
||||
onToggle: () => detail.toggle(instance),
|
||||
class: rowAlertBg(),
|
||||
})}
|
||||
>
|
||||
<TableCell class={getPlatformTableCellClassForKind('name')}>
|
||||
@@ -284,12 +351,35 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={indicator().variant}
|
||||
title={instance.status || 'unknown'}
|
||||
title={rowAlerts()[0]?.message || instance.status || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="font-semibold text-base-content truncate" title={name()}>
|
||||
{name()}
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<span
|
||||
class="block font-semibold text-base-content truncate"
|
||||
title={name()}
|
||||
>
|
||||
{name()}
|
||||
</span>
|
||||
<Show when={rowAlerts()[0]}>
|
||||
{(alert) => (
|
||||
<span
|
||||
class={`block truncate text-[11px] font-medium ${
|
||||
alert().level === 'critical'
|
||||
? 'text-red-600 dark:text-red-300'
|
||||
: 'text-amber-700 dark:text-amber-300'
|
||||
}`}
|
||||
title={rowAlerts()
|
||||
.map((open) => open.message)
|
||||
.join('\n')}
|
||||
data-mail-gateway-alert-reason
|
||||
>
|
||||
{alertTypeDisplayLabel(alert().type)}
|
||||
{rowAlerts().length > 1 ? ` +${rowAlerts().length - 1}` : ''}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<Show when={showVersion()}>
|
||||
@@ -377,12 +467,14 @@ export const ProxmoxMailGatewayTable: Component<{
|
||||
<InlineDetailTableRow
|
||||
cellId={detailRowId()}
|
||||
colspan={visibleColumnCount()}
|
||||
contentClass="px-4 py-4"
|
||||
cellClass="whitespace-normal"
|
||||
contentClass="min-w-0 whitespace-normal px-4 py-4"
|
||||
data-inline-detail-for={instance.id}
|
||||
>
|
||||
<ProxmoxMailGatewayDrawer
|
||||
instanceRow={instance}
|
||||
onClose={() => detail.close(instance)}
|
||||
alerts={rowAlerts()}
|
||||
/>
|
||||
</InlineDetailTableRow>
|
||||
</Show>
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Alert } from '@/types/api';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { ProxmoxMailGatewayTable } from '../ProxmoxMailGatewayTable';
|
||||
|
||||
const activeAlerts: Record<string, Alert> = {};
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => ({ activeAlerts }),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/alertsActivation', () => ({
|
||||
useAlertsActivation: () => ({ detectionEnabled: () => true }),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/apiClient', () => ({
|
||||
apiFetch: vi.fn(async () => new Response(JSON.stringify(null), { status: 200 })),
|
||||
}));
|
||||
|
||||
const gateway = (id: string, name: string, instanceId: string): Resource =>
|
||||
({
|
||||
id,
|
||||
type: 'pmg',
|
||||
name,
|
||||
displayName: name,
|
||||
platformType: 'proxmox-pmg',
|
||||
sourceType: 'api',
|
||||
status: 'online',
|
||||
lastSeen: Date.now(),
|
||||
canonicalIdentity: { primaryId: `agent:${instanceId}`, aliases: [instanceId, name] },
|
||||
pmg: { instanceId, nodeCount: 1, mailCountTotal: 10, queueTotal: 3, queueDeferred: 1 },
|
||||
}) as unknown as Resource;
|
||||
|
||||
describe('ProxmoxMailGatewayTable open alerts', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
for (const key of Object.keys(activeAlerts)) delete activeAlerts[key];
|
||||
});
|
||||
|
||||
it('marks an online gateway with an open alert and says why, keyed by its instance id', async () => {
|
||||
activeAlerts['pmg-main::oldest'] = {
|
||||
id: 'pmg-main::oldest',
|
||||
type: 'message-age',
|
||||
level: 'warning',
|
||||
resourceId: 'pmg-main',
|
||||
resourceName: 'mail-gateway-eu',
|
||||
node: 'https://pmg.example:8006',
|
||||
message: 'PMG mail-gateway-eu has messages queued for 41 minutes (threshold: 30 minutes)',
|
||||
acknowledged: false,
|
||||
} as unknown as Alert;
|
||||
|
||||
render(() => (
|
||||
<ProxmoxMailGatewayTable
|
||||
resources={[
|
||||
gateway('pmg-unified-eu', 'mail-gateway-eu', 'pmg-main'),
|
||||
gateway('pmg-unified-us', 'mail-gateway-us', 'pmg-edge'),
|
||||
]}
|
||||
emptyTitle="No gateways"
|
||||
emptyDescription="No gateways"
|
||||
/>
|
||||
));
|
||||
|
||||
const reason = document.querySelector('[data-mail-gateway-alert-reason]');
|
||||
expect(reason?.textContent).toBe('Message Age');
|
||||
expect(reason?.getAttribute('title')).toContain('queued for 41 minutes');
|
||||
expect(document.querySelectorAll('[data-mail-gateway-alert-reason]')).toHaveLength(1);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /mail-gateway-eu/ }));
|
||||
expect(
|
||||
await screen.findByText(/queued for 41 minutes \(threshold: 30 minutes\)/),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText('Needs attention')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('ignores acknowledged alerts', () => {
|
||||
activeAlerts['pmg-main::oldest'] = {
|
||||
id: 'pmg-main::oldest',
|
||||
type: 'message-age',
|
||||
level: 'warning',
|
||||
resourceId: 'pmg-main',
|
||||
resourceName: 'mail-gateway-eu',
|
||||
node: 'https://pmg.example:8006',
|
||||
message: 'queued',
|
||||
acknowledged: true,
|
||||
} as unknown as Alert;
|
||||
|
||||
render(() => (
|
||||
<ProxmoxMailGatewayTable
|
||||
resources={[gateway('pmg-unified-eu', 'mail-gateway-eu', 'pmg-main')]}
|
||||
emptyTitle="No gateways"
|
||||
emptyDescription="No gateways"
|
||||
/>
|
||||
));
|
||||
|
||||
expect(document.querySelector('[data-mail-gateway-alert-reason]')).toBeNull();
|
||||
});
|
||||
|
||||
it("does not pin another gateway's node alert on a row that shares its node name", () => {
|
||||
activeAlerts['pmg-edge::node'] = {
|
||||
id: 'pmg-edge::node',
|
||||
type: 'message-age',
|
||||
level: 'warning',
|
||||
resourceId: 'pmg-edge',
|
||||
resourceName: 'mail-gateway-us',
|
||||
node: 'mail-gateway-eu',
|
||||
message: 'queued on edge',
|
||||
acknowledged: false,
|
||||
} as unknown as Alert;
|
||||
|
||||
render(() => (
|
||||
<ProxmoxMailGatewayTable
|
||||
resources={[gateway('pmg-unified-eu', 'mail-gateway-eu', 'pmg-main')]}
|
||||
emptyTitle="No gateways"
|
||||
emptyDescription="No gateways"
|
||||
/>
|
||||
));
|
||||
|
||||
expect(document.querySelector('[data-mail-gateway-alert-reason]')).toBeNull();
|
||||
});
|
||||
|
||||
it('reads red for a critical open alert', () => {
|
||||
activeAlerts['pmg-main::queue'] = {
|
||||
id: 'pmg-main::queue',
|
||||
type: 'queue-total',
|
||||
level: 'critical',
|
||||
resourceId: 'pmg-main',
|
||||
resourceName: 'mail-gateway-eu',
|
||||
node: 'https://pmg.example:8006',
|
||||
message: 'Queue above critical',
|
||||
acknowledged: false,
|
||||
} as unknown as Alert;
|
||||
|
||||
render(() => (
|
||||
<ProxmoxMailGatewayTable
|
||||
resources={[gateway('pmg-unified-eu', 'mail-gateway-eu', 'pmg-main')]}
|
||||
emptyTitle="No gateways"
|
||||
emptyDescription="No gateways"
|
||||
/>
|
||||
));
|
||||
|
||||
const reason = document.querySelector('[data-mail-gateway-alert-reason]');
|
||||
expect(reason?.className).toContain('text-red-600');
|
||||
const row = reason?.closest('tr');
|
||||
expect(row?.querySelector('[class*="bg-red"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MAIL_GATEWAY_PHONE_COLUMN_WIDTHS,
|
||||
} from '../ProxmoxMailGatewayTable';
|
||||
import mailGatewayDrawerSource from '../ProxmoxMailGatewayDrawer.tsx?raw';
|
||||
import mailGatewayTableSource from '../ProxmoxMailGatewayTable.tsx?raw';
|
||||
|
||||
describe('ProxmoxMailGatewayTable phone presentation', () => {
|
||||
it('keeps compact drawer statistics on the shared responsive row contract', () => {
|
||||
@@ -22,23 +23,17 @@ describe('ProxmoxMailGatewayTable phone presentation', () => {
|
||||
).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('keeps fleet context and mail-flow counters visible without scrolling', () => {
|
||||
expect(MAIL_GATEWAY_PHONE_COLUMNS).toEqual([
|
||||
'instance',
|
||||
'nodes',
|
||||
'uptime',
|
||||
'mail',
|
||||
'queue',
|
||||
'deferred',
|
||||
]);
|
||||
expect(MAIL_GATEWAY_PHONE_COLUMN_WIDTHS).toEqual({
|
||||
instance: 30,
|
||||
nodes: 12,
|
||||
uptime: 16,
|
||||
mail: 14,
|
||||
queue: 14,
|
||||
deferred: 14,
|
||||
});
|
||||
it('keeps gateway names distinguishable with five phone tracks', () => {
|
||||
// A sixth track (node count) truncated "mail-gateway-eu" and
|
||||
// "mail-gateway-us" to the same text; the count lives in the expansion.
|
||||
expect(MAIL_GATEWAY_PHONE_COLUMNS).toEqual(['instance', 'uptime', 'mail', 'queue', 'deferred']);
|
||||
expect(MAIL_GATEWAY_PHONE_COLUMN_WIDTHS.instance).toBe(40);
|
||||
expect(
|
||||
MAIL_GATEWAY_PHONE_COLUMNS.reduce(
|
||||
(total, column) => total + MAIL_GATEWAY_PHONE_COLUMN_WIDTHS[column],
|
||||
0,
|
||||
),
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it('demotes node count below 360px while keeping five mail-flow fields', () => {
|
||||
@@ -73,4 +68,13 @@ describe('ProxmoxMailGatewayTable phone presentation', () => {
|
||||
expect(Math.min(...widths)).toBeGreaterThan(7);
|
||||
expect(widths.reduce((total, width) => total + width, 0)).toBeCloseTo(100, 1);
|
||||
});
|
||||
|
||||
it('matches gateway alerts across identity aliases and shows the problem in the drawer', () => {
|
||||
// Alerts are keyed by the PMG instance id, rows by the unified id.
|
||||
expect(mailGatewayTableSource).toContain('getResourceIdentityAliases(resource)');
|
||||
expect(mailGatewayTableSource).toContain(
|
||||
'filterPlatformResources(resources, search, status, effectiveStatus)',
|
||||
);
|
||||
expect(mailGatewayDrawerSource).toContain('<DrawerAttentionSection');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user