mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Finish drawer detail row convergence
This commit is contained in:
@@ -1239,10 +1239,11 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
drawer-width `justify-between` rows on desktop.
|
||||
This contract also covers secondary drawer facts in availability status and
|
||||
suggestion cards, resource change-history entries, Docker/PBS/PMG service
|
||||
support panels, and Docker container-update management cards. Headers,
|
||||
status summaries, actions, disk capacity summaries, and RAID state pairs may
|
||||
retain intentional endpoint alignment; ordinary label/value facts in those
|
||||
surfaces may not.
|
||||
support panels, Docker container-update management cards, action history,
|
||||
Discovery summaries, and the specialized Proxmox Mail Gateway drawer.
|
||||
Headers, status summaries, actions, disk capacity summaries, and RAID state
|
||||
pairs may retain intentional endpoint alignment; ordinary label/value facts
|
||||
in those surfaces may not.
|
||||
Read-only metadata chips belong to `MetadataBadge` and domain wrappers over
|
||||
it. Organization role and share-status chips must use
|
||||
`OrganizationRoleBadge` and `OrganizationShareStatusBadge`, so role/status
|
||||
|
||||
@@ -7616,6 +7616,7 @@
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxPageModel.ts",
|
||||
@@ -7815,6 +7816,7 @@
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxPageModel.ts",
|
||||
@@ -7889,6 +7891,7 @@
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts",
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx",
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx",
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts",
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx",
|
||||
"frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts",
|
||||
"frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx",
|
||||
|
||||
@@ -268,6 +268,7 @@ about the same disk cannot diverge.
|
||||
125. `frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx`
|
||||
126. `frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx`
|
||||
127. `frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx`
|
||||
127a. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx`
|
||||
128. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx`
|
||||
129. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx`
|
||||
130. `frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx`
|
||||
@@ -1331,9 +1332,11 @@ desktop rows use the shared fixed label track so labels and values remain
|
||||
visually adjacent instead of spanning the full drawer width.
|
||||
The same boundary applies to availability facts, resource change-history
|
||||
metadata, Docker/PBS/PMG service facts, nested PMG queue/mail breakdowns, and
|
||||
Docker container-update management facts. Their headers, actions, and compact
|
||||
status summaries remain feature-owned, while ordinary label/value rows compose
|
||||
`InfoCardKeyValueRow` rather than restoring a drawer-width flex split.
|
||||
Docker container-update management facts. It also covers action-history facts,
|
||||
Discovery facts, and the specialized `ProxmoxMailGatewayDrawer.tsx` inbound
|
||||
statistics. Their headers, actions, and compact status summaries remain
|
||||
feature-owned, while ordinary label/value rows compose `InfoCardKeyValueRow`
|
||||
rather than restoring a drawer-width flex split.
|
||||
Curated technical inventory follows the shared compact-row contract instead
|
||||
of the secondary-card contract. Docker-host drawers must project system,
|
||||
runtime, memory, storage, and telemetry facts through
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "478a39bdec4b3a20f309490a564bcaca8e423a34",
|
||||
"verified_at": "2026-08-27T20:06:10Z",
|
||||
"base_sha": "4b9abd4eca5a1399a2386814d98eeadded198e45",
|
||||
"verified_at": "2026-08-27T20:10:44Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx",
|
||||
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts",
|
||||
"frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx",
|
||||
"frontend-modern/src/features/alerts/thresholds/types.ts",
|
||||
"frontend-modern/src/features/alerts/useAlertsConfigurationSnapshotState.ts",
|
||||
"frontend-modern/src/types/alerts.ts"
|
||||
"frontend-modern/src/components/Discovery/DiscoveryTab.tsx",
|
||||
"frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx": "6e52a34562926185ae32483add8e303b0a8fae5c3d5315f5b87865f960f54929",
|
||||
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts": "adb7c745b8fdee148edf56beea8f1b7a02f30e6495d6fc6d7e2eff36f228d10c",
|
||||
"frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx": "00e8e50cd3892262c9195129b28fc834d4dbc87adb37aeb6f652957869367ad8",
|
||||
"frontend-modern/src/features/alerts/thresholds/types.ts": "d87f4509648798b55c01ac9e701faa2b9a96de7e25bf6453bfc10fbc5176e1f3",
|
||||
"frontend-modern/src/features/alerts/useAlertsConfigurationSnapshotState.ts": "34fff486ecf33457f42cefeb0b04bbd4b3e04dad24faca8bd99bfd86a75e70f7",
|
||||
"frontend-modern/src/types/alerts.ts": "ef4e8322a38e8443393f762ee4ba6c512ffd4ff18b3f32ff5fd64f5d3754585d"
|
||||
"frontend-modern/src/components/Discovery/DiscoveryTab.tsx": "50839ded7dc62cbc3a9fb11fc5fc575acd1997ec9a4b0d7331fba22f0afb0ab4",
|
||||
"frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx": "241e5f93f444b3838b80c05f6b4f78c4788931c760d01dc63f54ee5d4ee5270a",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "26faeb83e422b1d928385ce836e86a701ce544aa936b8de5bb38caee845bcb4d"
|
||||
},
|
||||
"routes": ["/alerts/thresholds/proxmox"],
|
||||
"routes": ["/proxmox/mail", "/standalone/machines", "/docker/overview", "/actions"],
|
||||
"viewports": [
|
||||
{ "width": 1440, "height": 900 },
|
||||
{ "width": 1920, "height": 800 },
|
||||
{ "width": 390, "height": 844 }
|
||||
],
|
||||
"states": [
|
||||
"CPU evaluation window loaded with the global five-minute rule and inherited platform overrides",
|
||||
"mobile platform overrides collapsed and expanded",
|
||||
"global rule changed to one minute with inherited labels updating immediately"
|
||||
"mail-gateway-eu expanded with inbound disposition statistics visible",
|
||||
"Apollo-114 and auth-service-01 management drawers inspected for Discovery and action-audit availability",
|
||||
"Actions open and history views inspected for seeded action records"
|
||||
],
|
||||
"interactions": [
|
||||
"confirmed the desktop three-column profile grid is readable without displacing threshold navigation",
|
||||
"confirmed the narrow layout keeps the global rule visible and collapses platform overrides",
|
||||
"expanded mobile platform overrides and confirmed every selector remains accessible",
|
||||
"changed the global CPU window to one minute and confirmed inherited labels and unsaved-change state update"
|
||||
"expanded mail-gateway-eu and confirmed Greylist, RBL, and average-process values remain adjacent to their labels on desktop",
|
||||
"confirmed the three inbound statistic rows span the mobile card cleanly at 390px without overlap",
|
||||
"repeated the PMG desktop check after the mobile span correction",
|
||||
"inspected machine and Docker host drawers plus Actions history; the demo does not seed resource-level Discovery facts or action-audit drawer rows, so those responsive states are covered by rendered component tests"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '@/utils/resourceAnalysisPresentation';
|
||||
import { ButtonLink, CopyValueButton } from '@/components/shared/Button';
|
||||
import { CopyableCodeRow } from '@/components/shared/CopyableCodeRow';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import { useDiscoveryTabState } from './useDiscoveryTabState';
|
||||
import { orderFactsByActionability } from './factOrdering';
|
||||
import { deriveCliCommand } from './cliCommand';
|
||||
@@ -1020,22 +1020,32 @@ export const DiscoveryTab: Component<DiscoveryTabProps> = (props) => {
|
||||
<div class="space-y-1.5">
|
||||
<For each={orderFactsByActionability(d().facts).slice(0, 8)}>
|
||||
{(fact) => (
|
||||
<div class="flex items-center justify-between gap-2 text-xs">
|
||||
<span class="min-w-0 text-muted truncate">{fact.key}</span>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="truncate font-medium text-base-content" title={fact.value}>
|
||||
{fact.value}
|
||||
</span>
|
||||
<CopyValueButton
|
||||
value={fact.value}
|
||||
copied={copiedDiscoveryValue() === fact.value.trim()}
|
||||
onCopyValue={handleCopyDiscoveryValue}
|
||||
label={`Copy ${fact.key}`}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
class="text-xs"
|
||||
label={fact.key}
|
||||
labelClass="truncate"
|
||||
labelTitle={fact.key}
|
||||
value={
|
||||
<>
|
||||
<span
|
||||
class="truncate font-medium text-base-content"
|
||||
title={fact.value}
|
||||
>
|
||||
{fact.value}
|
||||
</span>
|
||||
<CopyValueButton
|
||||
value={fact.value}
|
||||
copied={copiedDiscoveryValue() === fact.value.trim()}
|
||||
onCopyValue={handleCopyDiscoveryValue}
|
||||
label={`Copy ${fact.key}`}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
valueClass="flex items-center gap-1.5"
|
||||
valueTitle={fact.value}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
@@ -226,7 +226,16 @@ describe('DiscoveryTab', () => {
|
||||
service_version: '16.1',
|
||||
category: 'database',
|
||||
cli_access: 'psql',
|
||||
facts: [],
|
||||
facts: [
|
||||
{
|
||||
category: 'network',
|
||||
key: 'Listen address',
|
||||
value: '0.0.0.0:5432',
|
||||
source: 'ss',
|
||||
confidence: 0.95,
|
||||
discovered_at: '2026-04-15T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
@@ -255,6 +264,12 @@ describe('DiscoveryTab', () => {
|
||||
));
|
||||
|
||||
const runButton = await screen.findByRole('button', { name: 'Run Discovery' });
|
||||
const factRow = (await screen.findByText('Listen address')).parentElement;
|
||||
expect(factRow).toHaveClass('justify-between', 'lg:grid', 'lg:grid-cols-[7rem_minmax(0,1fr)]');
|
||||
expect(screen.getByText('0.0.0.0:5432').parentElement).toHaveClass(
|
||||
'text-right',
|
||||
'lg:text-left',
|
||||
);
|
||||
await waitFor(() => expect(runButton).not.toBeDisabled());
|
||||
fireEvent.click(runButton);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { For, Show } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
|
||||
import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import type { ActionAuditRecord } from '@/types/actionAudit';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import {
|
||||
@@ -55,27 +55,22 @@ const ActionHistoryRow: Component<{ audit: ActionAuditRecord }> = (props) => {
|
||||
<Show when={props.audit.request?.reason}>
|
||||
<div class="text-base-content">{props.audit.request.reason}</div>
|
||||
</Show>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Approval</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{formatActionApprovalPolicyLabel(props.audit.plan?.approvalPolicy)}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Approval"
|
||||
value={formatActionApprovalPolicyLabel(props.audit.plan?.approvalPolicy)}
|
||||
/>
|
||||
<Show when={preflight()}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-muted">Dry run</span>
|
||||
<span class="font-medium text-base-content">
|
||||
{preflight()?.dryRunAvailable ? 'Available' : 'Not available'}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Dry run"
|
||||
value={preflight()?.dryRunAvailable ? 'Available' : 'Not available'}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={preflight()?.intendedChange}>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="text-muted">Intent</span>
|
||||
<span class="max-w-[70%] text-right font-medium text-base-content">
|
||||
{preflight()?.intendedChange}
|
||||
</span>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label="Intent"
|
||||
value={preflight()?.intendedChange}
|
||||
valueClass="break-words"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={(preflight()?.safetyChecks || []).length > 0}>
|
||||
<div class="space-y-1">
|
||||
@@ -113,19 +108,20 @@ const ActionHistoryRow: Component<{ audit: ActionAuditRecord }> = (props) => {
|
||||
})()}
|
||||
</Show>
|
||||
<Show when={apt()?.facts.length}>
|
||||
<dl
|
||||
<div
|
||||
data-testid="resource-apt-action-facts"
|
||||
class="grid gap-1 rounded border border-border bg-surface px-2 py-1.5 sm:grid-cols-2"
|
||||
>
|
||||
<For each={apt()?.facts ?? []}>
|
||||
{(fact) => (
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<dt class="text-muted">{fact.label}</dt>
|
||||
<dd class="text-right font-medium text-base-content">{fact.value}</dd>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
label={fact.label}
|
||||
value={fact.value}
|
||||
valueClass="break-words"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</dl>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={verificationOutcome()}>
|
||||
{(() => {
|
||||
|
||||
+7
@@ -70,6 +70,13 @@ describe('ResourceActionHistory verification rendering', () => {
|
||||
);
|
||||
|
||||
const actionHistory = within(screen.getByTestId('resource-action-history-section'));
|
||||
const approvalRow = actionHistory.getAllByText('Approval')[0]?.parentElement;
|
||||
expect(approvalRow).toHaveClass(
|
||||
'justify-between',
|
||||
'lg:grid',
|
||||
'lg:grid-cols-[7rem_minmax(0,1fr)]',
|
||||
);
|
||||
expect(approvalRow?.lastElementChild).toHaveClass('text-right', 'lg:text-left');
|
||||
expect(actionHistory.getAllByText('Legacy check passed (source unclassified)')).toHaveLength(1);
|
||||
expect(actionHistory.getByText("systemctl is-active 'nginx'")).toBeInTheDocument();
|
||||
expect(actionHistory.queryByText('should not render')).toBeNull();
|
||||
|
||||
@@ -1247,6 +1247,18 @@ describe('shared primitive guardrails', () => {
|
||||
/class="flex items-(?:center|start) justify-between gap-2">\s*<span class="text-muted">/,
|
||||
);
|
||||
}
|
||||
|
||||
for (const consumerPath of [
|
||||
'src/components/Discovery/DiscoveryTab.tsx',
|
||||
'src/components/Infrastructure/ResourceActionHistory.tsx',
|
||||
'src/features/proxmox/ProxmoxMailGatewayDrawer.tsx',
|
||||
]) {
|
||||
const source = readFrontendSource(consumerPath);
|
||||
expect(source).toContain('InfoCardKeyValueRow');
|
||||
expect(source).not.toMatch(
|
||||
/class="flex items-(?:baseline|center|start) justify-between[^\"]*">\s*<(?:span|dt) class="[^\"]*text-muted/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps shared subtabs as one primitive and leaves shell styling to owning surfaces', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { For, Show, createMemo, createResource, type Component } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading';
|
||||
import { InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame';
|
||||
import { ObjectDrawerHeader } from '@/components/shared/ObjectDrawerHeader';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
|
||||
@@ -396,34 +397,28 @@ export const ProxmoxMailGatewayDrawer: Component<{
|
||||
outValue={stats()!.bytesOut ?? 0}
|
||||
format={(v) => (v > 0 ? formatBytes(v) : '—')}
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-baseline justify-between text-[11px]">
|
||||
<span class="text-muted">Greylist / Junk in</span>
|
||||
<span class="font-mono text-[10px] text-base-content font-semibold">
|
||||
{formatPlatformTableIntegerValue(stats()!.greylistCount)} /{' '}
|
||||
{formatPlatformTableIntegerValue(stats()!.junkIn)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-baseline justify-between text-[11px]">
|
||||
<span class="text-muted">RBL / Pregreet rejects</span>
|
||||
<span class="font-mono text-[10px] text-base-content font-semibold">
|
||||
{formatPlatformTableIntegerValue(stats()!.rblRejects)} /{' '}
|
||||
{formatPlatformTableIntegerValue(stats()!.pregreetRejects)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-baseline justify-between text-[11px]">
|
||||
<span class="text-muted">Avg process</span>
|
||||
<span class="font-mono text-[10px] text-base-content font-semibold">
|
||||
{stats()?.averageProcessTimeMs
|
||||
? `${Math.round(stats()!.averageProcessTimeMs)}ms`
|
||||
: '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<InfoCardKeyValueRow
|
||||
class="col-span-2 text-[11px] sm:col-span-1"
|
||||
label="Greylist / Junk in"
|
||||
value={`${formatPlatformTableIntegerValue(stats()!.greylistCount)} / ${formatPlatformTableIntegerValue(stats()!.junkIn)}`}
|
||||
valueClass="font-mono text-[10px]"
|
||||
/>
|
||||
<InfoCardKeyValueRow
|
||||
class="col-span-2 text-[11px] sm:col-span-1"
|
||||
label="RBL / Pregreet rejects"
|
||||
value={`${formatPlatformTableIntegerValue(stats()!.rblRejects)} / ${formatPlatformTableIntegerValue(stats()!.pregreetRejects)}`}
|
||||
valueClass="font-mono text-[10px]"
|
||||
/>
|
||||
<InfoCardKeyValueRow
|
||||
class="col-span-2 text-[11px] sm:col-span-1"
|
||||
label="Avg process"
|
||||
value={
|
||||
stats()?.averageProcessTimeMs
|
||||
? `${Math.round(stats()!.averageProcessTimeMs)}ms`
|
||||
: '—'
|
||||
}
|
||||
valueClass="font-mono text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
|
||||
@@ -6,8 +6,19 @@ import {
|
||||
MAIL_GATEWAY_PHONE_COLUMNS,
|
||||
MAIL_GATEWAY_PHONE_COLUMN_WIDTHS,
|
||||
} from '../ProxmoxMailGatewayTable';
|
||||
import mailGatewayDrawerSource from '../ProxmoxMailGatewayDrawer.tsx?raw';
|
||||
|
||||
describe('ProxmoxMailGatewayTable phone presentation', () => {
|
||||
it('keeps compact drawer statistics on the shared responsive row contract', () => {
|
||||
expect(mailGatewayDrawerSource).toContain('InfoCardKeyValueRow');
|
||||
expect(mailGatewayDrawerSource).not.toMatch(
|
||||
/class="flex items-baseline justify-between[^\"]*">\s*<span class="text-muted">/,
|
||||
);
|
||||
expect(
|
||||
mailGatewayDrawerSource.match(/class="col-span-2 text-\[11px\] sm:col-span-1"/g),
|
||||
).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('keeps fleet context and mail-flow counters visible without scrolling', () => {
|
||||
expect(MAIL_GATEWAY_PHONE_COLUMNS).toEqual([
|
||||
'instance',
|
||||
|
||||
Reference in New Issue
Block a user