mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
refactor(alerts): canonicalize thresholds surface routing
This commit is contained in:
parent
0f736ef5eb
commit
b2344cdbbd
16 changed files with 390 additions and 94 deletions
|
|
@ -234,7 +234,14 @@ tab render owners live in
|
|||
threshold row grouping, override-ID compatibility, resource normalization,
|
||||
thresholds-table controller logic, or per-tab runtime should land in those
|
||||
feature hooks and tab owners rather than being rebuilt inside the shell.
|
||||
Within the Proxmox tab, render-heavy ownership now further routes through
|
||||
The shell-owned thresholds sub-routes are now the neutral user-facing paths
|
||||
`/alerts/thresholds/infrastructure`, `/alerts/thresholds/systems`,
|
||||
`/alerts/thresholds/mail-gateway`, and `/alerts/thresholds/containers`.
|
||||
Legacy `/alerts/thresholds/proxmox` and `/alerts/thresholds/agents` links
|
||||
must redirect to the neutral infrastructure and systems routes so API-backed
|
||||
platforms like TrueNAS do not remain stranded behind provider-specific deep
|
||||
links.
|
||||
Within the infrastructure tab, render-heavy ownership now further routes through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx`,
|
||||
|
|
@ -244,7 +251,7 @@ Within the Proxmox tab, render-heavy ownership now further routes through
|
|||
and `frontend-modern/src/components/Alerts/ThresholdsTableProxmoxStorageSection.tsx`
|
||||
with the shared section contract in
|
||||
`frontend-modern/src/features/alerts/thresholds/thresholdsTableSectionProps.ts`.
|
||||
Future Proxmox thresholds presentation work should extend those section owners
|
||||
Future infrastructure-thresholds presentation work should extend those section owners
|
||||
instead of expanding `frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx`
|
||||
back into a mixed render surface.
|
||||
The Docker tab now follows that same section-owner shape through
|
||||
|
|
@ -255,10 +262,10 @@ and `frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSectio
|
|||
Future Docker thresholds presentation work should extend those section owners
|
||||
instead of expanding `frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`
|
||||
back into a mixed render surface.
|
||||
The agents tab now follows that same shell-versus-section pattern through
|
||||
The systems tab now follows that same shell-versus-section pattern through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx`
|
||||
and `frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx`.
|
||||
Future agent thresholds presentation work should extend those section owners
|
||||
Future systems-thresholds presentation work should extend those section owners
|
||||
instead of expanding `frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`
|
||||
back into a mixed render surface.
|
||||
The alert resource thresholds editor now follows the same shape: shared metric
|
||||
|
|
|
|||
|
|
@ -1230,7 +1230,15 @@ and the tab render owners live in
|
|||
`frontend-modern/src/components/Alerts/ThresholdsTablePMGTab.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`, and
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`.
|
||||
The Proxmox tab is itself now a shell that composes
|
||||
`frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts`
|
||||
owns the neutral thresholds sub-route contract:
|
||||
`/alerts/thresholds/infrastructure`, `/alerts/thresholds/systems`,
|
||||
`/alerts/thresholds/mail-gateway`, and `/alerts/thresholds/containers`.
|
||||
Legacy `/alerts/thresholds/proxmox` and `/alerts/thresholds/agents` links
|
||||
must redirect to the neutral infrastructure and systems routes so API-backed
|
||||
platforms such as TrueNAS stay on canonical page language rather than
|
||||
provider-specific aliases.
|
||||
The infrastructure tab is itself now a shell that composes
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx`,
|
||||
|
|
@ -1240,7 +1248,7 @@ The Proxmox tab is itself now a shell that composes
|
|||
and `frontend-modern/src/components/Alerts/ThresholdsTableProxmoxStorageSection.tsx`
|
||||
using the shared contract in
|
||||
`frontend-modern/src/features/alerts/thresholds/thresholdsTableSectionProps.ts`.
|
||||
Future Proxmox thresholds presentation changes should extend those section
|
||||
Future infrastructure-thresholds presentation changes should extend those section
|
||||
surfaces rather than restoring mixed JSX ownership to
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxTab.tsx`.
|
||||
The Docker tab now follows that same composition pattern through
|
||||
|
|
@ -1251,10 +1259,10 @@ and `frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSectio
|
|||
Future Docker thresholds presentation changes should extend those section
|
||||
surfaces rather than restoring mixed JSX ownership to
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx`.
|
||||
The agents tab now follows that same composition pattern through
|
||||
The systems tab now follows that same composition pattern through
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx`
|
||||
and `frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx`.
|
||||
Future agent thresholds presentation changes should extend those section
|
||||
Future systems-thresholds presentation changes should extend those section
|
||||
surfaces rather than restoring mixed JSX ownership to
|
||||
`frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx`.
|
||||
The thresholds tab adapter contract now lives in
|
||||
|
|
|
|||
|
|
@ -87,12 +87,11 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
<nav class="-mb-px flex gap-4 sm:gap-6" aria-label="Tabs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('proxmox')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'proxmox' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
onClick={() => state.handleTabClick('infrastructure')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'infrastructure' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Server class="w-4 h-4" />
|
||||
<span class="hidden sm:inline">Proxmox / PBS</span>
|
||||
<span class="sm:hidden">Proxmox</span>
|
||||
<span>Infrastructure</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -105,11 +104,11 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleTabClick('agents')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
onClick={() => state.handleTabClick('systems')}
|
||||
class={`py-3 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5 ${state.activeTab() === 'systems' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-muted hover:text-base-content hover:border-slate-300'}`}
|
||||
>
|
||||
<Users class="w-4 h-4" />
|
||||
<span>Agents</span>
|
||||
<span>Systems</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -122,7 +121,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
</nav>
|
||||
</div>
|
||||
|
||||
<Show when={state.activeTab() === 'proxmox'}>
|
||||
<Show when={state.activeTab() === 'infrastructure'}>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -143,7 +142,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
</Show>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Show when={state.activeTab() === 'proxmox'}>
|
||||
<Show when={state.activeTab() === 'infrastructure'}>
|
||||
<ThresholdsTableProxmoxTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
|
|
@ -151,7 +150,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
|
|||
<ThresholdsTablePMGTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
<Show when={state.activeTab() === 'agents'}>
|
||||
<Show when={state.activeTab() === 'systems'}>
|
||||
<ThresholdsTableAgentsTab state={state} tableProps={props} />
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -258,14 +258,31 @@ describe('ThresholdsTable basics', () => {
|
|||
});
|
||||
|
||||
describe('ThresholdsTable navigation and redirection', () => {
|
||||
it('redirects from base path to proxmox', () => {
|
||||
it('redirects from base path to infrastructure', () => {
|
||||
setPathname('/alerts/thresholds');
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/proxmox', { replace: true });
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure', {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('loads agents tab from canonical route', async () => {
|
||||
it('redirects legacy thresholds sub-routes onto canonical infrastructure and systems paths', () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure', {
|
||||
replace: true,
|
||||
});
|
||||
|
||||
cleanup();
|
||||
mockNavigate.mockReset();
|
||||
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/systems', { replace: true });
|
||||
});
|
||||
|
||||
it('loads systems tab from canonical route', async () => {
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host: Agent = {
|
||||
id: 'legacy-h1',
|
||||
hostname: 'legacy-host',
|
||||
|
|
@ -278,16 +295,24 @@ describe('ThresholdsTable navigation and redirection', () => {
|
|||
render(() => <ThresholdsTable {...(baseProps() as any)} agents={[host]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('resource-table-Agents')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-table-Systems')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to correct route when tabs are clicked', () => {
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} />);
|
||||
|
||||
const hostsTab = screen.getAllByRole('button').find((el) => el.textContent?.includes('Agents'));
|
||||
if (hostsTab) fireEvent.click(hostsTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/agents');
|
||||
const infrastructureTab = screen
|
||||
.getAllByRole('button')
|
||||
.find((el) => el.textContent?.includes('Infrastructure'));
|
||||
if (infrastructureTab) fireEvent.click(infrastructureTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/infrastructure');
|
||||
|
||||
const systemsTab = screen
|
||||
.getAllByRole('button')
|
||||
.find((el) => el.textContent?.includes('Systems'));
|
||||
if (systemsTab) fireEvent.click(systemsTab);
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/systems');
|
||||
|
||||
const mailTab = screen
|
||||
.getAllByRole('button')
|
||||
|
|
@ -298,8 +323,8 @@ describe('ThresholdsTable navigation and redirection', () => {
|
|||
});
|
||||
|
||||
describe('ThresholdsTable Resource Rendering', () => {
|
||||
it('renders agents correctly', async () => {
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
it('renders systems correctly', async () => {
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host: Agent = {
|
||||
id: 'h1',
|
||||
hostname: 'host1',
|
||||
|
|
@ -312,15 +337,15 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
render(() => <ThresholdsTable {...(baseProps() as any)} agents={[host]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('resource-table-Agents')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-table-Systems')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('resource-count-Agents')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('resource-count-Systems')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('resource-name-h1')).toHaveTextContent('Host 1');
|
||||
});
|
||||
|
||||
it('renders governed agents with the policy-aware display label', async () => {
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
it('renders governed systems with the policy-aware display label', async () => {
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host = {
|
||||
id: 'h2',
|
||||
hostname: 'secret-host',
|
||||
|
|
@ -338,15 +363,43 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
render(() => <ThresholdsTable {...(baseProps() as any)} agents={[host]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('resource-table-Agents')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-table-Systems')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('resource-name-h2')).toHaveTextContent('redacted by policy');
|
||||
expect(screen.getByTestId('resource-name-h2')).not.toHaveTextContent('secret-host');
|
||||
});
|
||||
|
||||
it('renders proxmox nodes and guests correctly', async () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
it('renders TrueNAS appliances on the canonical systems tab with their disk surface', async () => {
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const truenasSystem = {
|
||||
id: 'truenas-resource',
|
||||
type: 'truenas',
|
||||
name: 'truenas-main',
|
||||
displayName: 'TrueNAS Main',
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
platformData: {
|
||||
agent: {
|
||||
agentId: 'truenas-main',
|
||||
disks: [{ mountpoint: '/mnt/tank', type: 'zfs', used: 50, total: 100 }],
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
render(() => <ThresholdsTable {...(baseProps() as any)} agents={[truenasSystem]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('resource-name-truenas-main')).toHaveTextContent('TrueNAS Main');
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByTestId('resource-row-agent:truenas-main/disk:mnt-tank'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders infrastructure hosts and guests correctly', async () => {
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
const node = {
|
||||
id: 'node1',
|
||||
name: 'pve1',
|
||||
|
|
@ -366,7 +419,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('section-Proxmox Nodes')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('section-Virtualization Hosts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('resource-name-node1')).toHaveTextContent('PVE');
|
||||
|
|
@ -376,7 +429,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
it('renders governed guests with the policy-aware display label', async () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
const guest = {
|
||||
id: 'guest2',
|
||||
name: 'secret-vm-2',
|
||||
|
|
@ -402,7 +455,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
it('renders governed guest groups with the policy-aware node header label', async () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
const node = {
|
||||
id: 'node-governed',
|
||||
name: 'secret-node',
|
||||
|
|
@ -438,7 +491,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
it('renders governed storage with the policy-aware display label', async () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
const storage = {
|
||||
id: 'storage1',
|
||||
name: 'secret-datastore',
|
||||
|
|
@ -514,7 +567,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
});
|
||||
|
||||
it('renders governed agent disk node labels with the policy-aware display label', async () => {
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host = {
|
||||
id: 'agent-governed',
|
||||
type: 'agent',
|
||||
|
|
@ -552,7 +605,7 @@ describe('ThresholdsTable Resource Rendering', () => {
|
|||
|
||||
describe('ThresholdsTable Metric Formatting', () => {
|
||||
it('formats metrics correctly', async () => {
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host: Agent = {
|
||||
id: 'h1',
|
||||
hostname: 'host1',
|
||||
|
|
@ -583,7 +636,7 @@ describe('ThresholdsTable Metric Formatting', () => {
|
|||
|
||||
describe('ThresholdsTable V6 ID compatibility', () => {
|
||||
it('matches agent overrides keyed by actionable agent ID', async () => {
|
||||
setPathname('/alerts/thresholds/agents');
|
||||
setPathname('/alerts/thresholds/systems');
|
||||
const host = {
|
||||
id: 'resource:host:abc123',
|
||||
type: 'agent',
|
||||
|
|
@ -749,7 +802,7 @@ describe('ThresholdsTable V6 ID compatibility', () => {
|
|||
});
|
||||
|
||||
it('removes PBS offline alerts using legacy compatibility IDs when disabled', async () => {
|
||||
setPathname('/alerts/thresholds/proxmox');
|
||||
setPathname('/alerts/thresholds/infrastructure');
|
||||
const removeAlerts = vi.fn();
|
||||
const pbs = {
|
||||
id: 'pbs-main',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cleanup, render } from '@solidjs/testing-library';
|
|||
import type { ThresholdsTableProps } from '@/features/alerts/thresholds/types';
|
||||
import { useThresholdsTableState } from '../useThresholdsTableState';
|
||||
|
||||
let mockPathname = '/alerts/thresholds/agents';
|
||||
let mockPathname = '/alerts/thresholds/systems';
|
||||
const navigateSpy = vi.fn();
|
||||
|
||||
vi.mock('@solidjs/router', () => ({
|
||||
|
|
@ -161,7 +161,7 @@ const buildProps = (): ThresholdsTableProps =>
|
|||
}) as unknown as ThresholdsTableProps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPathname = '/alerts/thresholds/agents';
|
||||
mockPathname = '/alerts/thresholds/systems';
|
||||
navigateSpy.mockReset();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
|
@ -182,7 +182,7 @@ describe('useThresholdsTableState', () => {
|
|||
render(() => <Harness />);
|
||||
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured!.activeTab()).toBe('agents');
|
||||
expect(captured!.activeTab()).toBe('systems');
|
||||
|
||||
captured!.dismissHelpBanner();
|
||||
expect(captured!.helpBannerDismissed()).toBe(true);
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
|||
rawName: originalDisplayName,
|
||||
host: normalizedHost,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
resourceType: 'Virtualization Host',
|
||||
status: node.status,
|
||||
uptime: node.uptime,
|
||||
cpu: (node.cpu?.current ?? 0) / 100,
|
||||
|
|
@ -112,7 +112,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
|||
displayName,
|
||||
rawName: agentResource.identity?.hostname ?? agentResource.name,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
resourceType: 'System',
|
||||
node: displayName,
|
||||
instance:
|
||||
readString(agentData?.platform) ||
|
||||
|
|
@ -140,7 +140,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
|||
displayName: name,
|
||||
rawName: name,
|
||||
type: 'agent' as const,
|
||||
resourceType: 'Agent',
|
||||
resourceType: 'System',
|
||||
node: '',
|
||||
instance: '',
|
||||
status: 'unknown',
|
||||
|
|
@ -208,7 +208,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
|||
displayName: diskLabel,
|
||||
rawName: disk.device || diskLabel,
|
||||
type: 'agentDisk' as const,
|
||||
resourceType: 'Agent Disk',
|
||||
resourceType: 'System Disk',
|
||||
host: agentIdForActions,
|
||||
node: agentDisplayName,
|
||||
instance: disk.type || '',
|
||||
|
|
@ -232,7 +232,7 @@ export function useThresholdsHostData(inputs: ThresholdsDataInputs) {
|
|||
displayName: name,
|
||||
rawName: name,
|
||||
type: 'agentDisk' as const,
|
||||
resourceType: 'Agent Disk',
|
||||
resourceType: 'System Disk',
|
||||
host: '',
|
||||
node: 'Unknown Agent',
|
||||
instance: '',
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
const [bulkEditIds, setBulkEditIds] = createSignal<string[]>([]);
|
||||
const [bulkEditColumns, setBulkEditColumns] = createSignal<string[]>([]);
|
||||
const [isBulkEditDialogOpen, setIsBulkEditDialogOpen] = createSignal(false);
|
||||
const [activeTab, setActiveTab] = createSignal<ThresholdsActiveTab>('proxmox');
|
||||
const [activeTab, setActiveTab] = createSignal<ThresholdsActiveTab>('infrastructure');
|
||||
const [dockerIgnoredInput, setDockerIgnoredInput] = createSignal(
|
||||
props.dockerIgnoredPrefixes().join('\n'),
|
||||
);
|
||||
|
|
@ -131,9 +131,9 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
const getActiveTabFromRoute = (): ThresholdsActiveTab => {
|
||||
const path = location.pathname;
|
||||
if (path.includes('/thresholds/containers')) return 'docker';
|
||||
if (path.includes('/thresholds/agents')) return 'agents';
|
||||
if (path.includes('/thresholds/systems') || path.includes('/thresholds/agents')) return 'systems';
|
||||
if (path.includes('/thresholds/mail-gateway')) return 'pmg';
|
||||
return 'proxmox';
|
||||
return 'infrastructure';
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -145,16 +145,26 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
|
||||
createEffect(() => {
|
||||
if (location.pathname === '/alerts/thresholds') {
|
||||
navigate('/alerts/thresholds/proxmox', { replace: true });
|
||||
navigate('/alerts/thresholds/infrastructure', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === '/alerts/thresholds/proxmox') {
|
||||
navigate('/alerts/thresholds/infrastructure', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === '/alerts/thresholds/agents') {
|
||||
navigate('/alerts/thresholds/systems', { replace: true });
|
||||
}
|
||||
});
|
||||
|
||||
const handleTabClick = (tab: ThresholdsActiveTab) => {
|
||||
const tabRoutes: Record<ThresholdsActiveTab, string> = {
|
||||
agents: '/alerts/thresholds/agents',
|
||||
infrastructure: '/alerts/thresholds/infrastructure',
|
||||
docker: '/alerts/thresholds/containers',
|
||||
pmg: '/alerts/thresholds/mail-gateway',
|
||||
proxmox: '/alerts/thresholds/proxmox',
|
||||
systems: '/alerts/thresholds/systems',
|
||||
};
|
||||
navigate(tabRoutes[tab]);
|
||||
};
|
||||
|
|
@ -284,9 +294,9 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
const items: ThresholdsSummaryItem[] = [
|
||||
{
|
||||
key: 'nodes',
|
||||
label: 'Nodes',
|
||||
label: 'Virtualization Hosts',
|
||||
overrides: countOverrides(nodesWithOverrides()),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: props.nodes?.length ?? 0,
|
||||
},
|
||||
{
|
||||
|
|
@ -298,44 +308,44 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
},
|
||||
{
|
||||
key: 'agents',
|
||||
label: 'Agents',
|
||||
label: 'Systems',
|
||||
overrides: countOverrides(agentsWithOverrides()),
|
||||
tab: 'agents',
|
||||
tab: 'systems',
|
||||
total: props.agents?.length ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'agentDisks',
|
||||
label: 'Agent Disks',
|
||||
label: 'System Disks',
|
||||
overrides: countOverrides(agentDisksWithOverrides()),
|
||||
tab: 'agents',
|
||||
tab: 'systems',
|
||||
total: agentDisksWithOverrides().length,
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
label: 'Storage',
|
||||
overrides: countOverrides(storageWithOverrides()),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: props.storage?.length ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'backups',
|
||||
label: 'Recovery',
|
||||
overrides: backupOverridesCount(),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
key: 'snapshots',
|
||||
label: 'Snapshot Age',
|
||||
overrides: snapshotOverridesCount(),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
key: 'pbs',
|
||||
label: 'PBS Servers',
|
||||
overrides: countOverrides(pbsServersWithOverrides()),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: props.pbsInstances?.length ?? 0,
|
||||
},
|
||||
{
|
||||
|
|
@ -356,7 +366,7 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
|
|||
key: 'guests',
|
||||
label: 'VMs & Containers',
|
||||
overrides: countOverrides(guestsFlat()),
|
||||
tab: 'proxmox',
|
||||
tab: 'infrastructure',
|
||||
total: props.allGuests?.()?.length ?? 0,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ResourcePolicy } from '@/types/resource';
|
|||
import type { BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts';
|
||||
import type { AlertResourceThresholdMap } from '@/components/Alerts/alertResourceTableModel';
|
||||
|
||||
export type ThresholdsActiveTab = 'proxmox' | 'pmg' | 'agents' | 'docker';
|
||||
export type ThresholdsActiveTab = 'infrastructure' | 'pmg' | 'systems' | 'docker';
|
||||
|
||||
export interface Resource {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export function Alerts() {
|
|||
|
||||
const expectedPath = pathForTab(tab);
|
||||
|
||||
// Allow sub-paths for thresholds tab (e.g., /alerts/thresholds/proxmox)
|
||||
// Allow sub-paths for thresholds tab (e.g., /alerts/thresholds/infrastructure)
|
||||
const isThresholdsSubPath =
|
||||
tab === 'thresholds' && currentPath.startsWith('/alerts/thresholds/');
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ describe('tab path helpers', () => {
|
|||
it('resolves tab from path', () => {
|
||||
expect(tabFromPath('/alerts')).toBe('overview');
|
||||
expect(tabFromPath('/alerts/thresholds')).toBe('thresholds');
|
||||
expect(tabFromPath('/alerts/thresholds/infrastructure')).toBe('thresholds');
|
||||
expect(tabFromPath('/alerts/thresholds/systems')).toBe('thresholds');
|
||||
expect(tabFromPath('/alerts/thresholds/proxmox')).toBe('thresholds');
|
||||
expect(tabFromPath('/alerts/custom-rules')).toBe('thresholds');
|
||||
expect(tabFromPath('/foo/bar')).toBe('overview');
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ describe('alertOverviewPresentation', () => {
|
|||
thresholds: {
|
||||
title: 'Alert Thresholds',
|
||||
description:
|
||||
'Tune resource thresholds and override rules for nodes, guests, and containers.',
|
||||
'Tune threshold and override rules for infrastructure, systems, storage, and containers.',
|
||||
},
|
||||
destinations: {
|
||||
title: 'Notification Destinations',
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ describe('alertThresholdsPresentation', () => {
|
|||
it('exports canonical thresholds empty-state copy', () => {
|
||||
expect(PBS_THRESHOLDS_EMPTY_STATE).toBe('No PBS servers configured.');
|
||||
expect(GUEST_THRESHOLDS_EMPTY_STATE).toBe('No VMs or containers found.');
|
||||
expect(NODE_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No nodes match the current filters.');
|
||||
expect(NODE_THRESHOLDS_FILTER_EMPTY_STATE).toBe(
|
||||
'No virtualization hosts match the current filters.',
|
||||
);
|
||||
expect(PBS_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No PBS servers match the current filters.');
|
||||
expect(GUEST_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No VMs or containers match the current filters.');
|
||||
expect(GUEST_FILTERING_EMPTY_STATE).toBe('Configure guest filtering rules.');
|
||||
|
|
@ -69,9 +71,9 @@ describe('alertThresholdsPresentation', () => {
|
|||
expect(STORAGE_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No storage devices match the current filters.');
|
||||
expect(PMG_THRESHOLDS_EMPTY_STATE).toContain('No mail gateways configured yet.');
|
||||
expect(PMG_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No mail gateways match the current filters.');
|
||||
expect(AGENT_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No agents match the current filters.');
|
||||
expect(AGENT_DISKS_EMPTY_STATE).toContain('Agents with mounted filesystems will appear here.');
|
||||
expect(AGENT_DISKS_FILTER_EMPTY_STATE).toBe('No agent disks match the current filters.');
|
||||
expect(AGENT_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No systems match the current filters.');
|
||||
expect(AGENT_DISKS_EMPTY_STATE).toContain('Systems with mounted filesystems will appear here.');
|
||||
expect(AGENT_DISKS_FILTER_EMPTY_STATE).toBe('No system disks match the current filters.');
|
||||
expect(CONTAINER_RUNTIMES_FILTER_EMPTY_STATE).toBe('No container runtimes match the current filters.');
|
||||
expect(CONTAINERS_FILTER_EMPTY_STATE).toBe('No containers match the current filters.');
|
||||
});
|
||||
|
|
@ -167,7 +169,7 @@ describe('alertThresholdsPresentation', () => {
|
|||
});
|
||||
|
||||
it('exports canonical thresholds section titles', () => {
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_NODES).toBe('Proxmox Nodes');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_NODES).toBe('Virtualization Hosts');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_PBS).toBe('PBS Servers');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_GUESTS).toBe('VMs & Containers');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING).toBe('Guest Filtering');
|
||||
|
|
@ -175,12 +177,12 @@ describe('alertThresholdsPresentation', () => {
|
|||
expect(ALERT_THRESHOLDS_SECTION_TITLE_SNAPSHOTS).toBe('Snapshot Age');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_STORAGE).toBe('Storage Devices');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_PMG).toBe('Mail Gateway Thresholds');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_AGENTS).toBe('Agents');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_AGENT_DISKS).toBe('Agent Disks');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_AGENTS).toBe('Systems');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_AGENT_DISKS).toBe('System Disks');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_DOCKER_HOSTS).toBe('Container Runtimes');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_DOCKER_CONTAINERS).toBe('Containers');
|
||||
expect(getAlertThresholdsSectionTitles()).toEqual({
|
||||
nodes: 'Proxmox Nodes',
|
||||
nodes: 'Virtualization Hosts',
|
||||
pbs: 'PBS Servers',
|
||||
guests: 'VMs & Containers',
|
||||
guestFiltering: 'Guest Filtering',
|
||||
|
|
@ -188,8 +190,8 @@ describe('alertThresholdsPresentation', () => {
|
|||
snapshots: 'Snapshot Age',
|
||||
storage: 'Storage Devices',
|
||||
pmg: 'Mail Gateway Thresholds',
|
||||
agents: 'Agents',
|
||||
agentDisks: 'Agent Disks',
|
||||
agents: 'Systems',
|
||||
agentDisks: 'System Disks',
|
||||
dockerHosts: 'Container Runtimes',
|
||||
dockerContainers: 'Containers',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const ALERTS_PAGE_OVERVIEW_DESCRIPTION =
|
|||
'Monitor active alerts, acknowledgements, and recent status changes across platforms.';
|
||||
export const ALERTS_PAGE_THRESHOLDS_TITLE = 'Alert Thresholds';
|
||||
export const ALERTS_PAGE_THRESHOLDS_DESCRIPTION =
|
||||
'Tune resource thresholds and override rules for nodes, guests, and containers.';
|
||||
'Tune threshold and override rules for infrastructure, systems, storage, and containers.';
|
||||
export const ALERTS_PAGE_DESTINATIONS_TITLE = 'Notification Destinations';
|
||||
export const ALERTS_PAGE_DESTINATIONS_DESCRIPTION =
|
||||
'Configure email, webhooks, and escalation paths for alert delivery.';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const PBS_THRESHOLDS_EMPTY_STATE = 'No PBS servers configured.';
|
||||
export const GUEST_THRESHOLDS_EMPTY_STATE = 'No VMs or containers found.';
|
||||
export const NODE_THRESHOLDS_FILTER_EMPTY_STATE = 'No nodes match the current filters.';
|
||||
export const NODE_THRESHOLDS_FILTER_EMPTY_STATE = 'No virtualization hosts match the current filters.';
|
||||
export const PBS_THRESHOLDS_FILTER_EMPTY_STATE = 'No PBS servers match the current filters.';
|
||||
export const GUEST_THRESHOLDS_FILTER_EMPTY_STATE = 'No VMs or containers match the current filters.';
|
||||
export const GUEST_FILTERING_EMPTY_STATE = 'Configure guest filtering rules.';
|
||||
|
|
@ -11,10 +11,10 @@ export const STORAGE_THRESHOLDS_FILTER_EMPTY_STATE = 'No storage devices match t
|
|||
export const PMG_THRESHOLDS_EMPTY_STATE =
|
||||
'No mail gateways configured yet. Add a PMG instance in Settings to manage thresholds.';
|
||||
export const PMG_THRESHOLDS_FILTER_EMPTY_STATE = 'No mail gateways match the current filters.';
|
||||
export const AGENT_THRESHOLDS_FILTER_EMPTY_STATE = 'No agents match the current filters.';
|
||||
export const AGENT_THRESHOLDS_FILTER_EMPTY_STATE = 'No systems match the current filters.';
|
||||
export const AGENT_DISKS_EMPTY_STATE =
|
||||
'No agent disks found. Agents with mounted filesystems will appear here.';
|
||||
export const AGENT_DISKS_FILTER_EMPTY_STATE = 'No agent disks match the current filters.';
|
||||
'No system disks found. Systems with mounted filesystems will appear here.';
|
||||
export const AGENT_DISKS_FILTER_EMPTY_STATE = 'No system disks match the current filters.';
|
||||
export const CONTAINER_RUNTIMES_FILTER_EMPTY_STATE =
|
||||
'No container runtimes match the current filters.';
|
||||
export const CONTAINERS_FILTER_EMPTY_STATE = 'No containers match the current filters.';
|
||||
|
|
@ -61,7 +61,7 @@ export const ALERT_THRESHOLDS_DOCKER_SERVICES_CRITICAL_GAP_DESCRIPTION =
|
|||
'Raise a critical alert when the missing replica gap meets or exceeds this value.';
|
||||
export const ALERT_THRESHOLDS_DOCKER_SERVICES_GAP_VALIDATION_MESSAGE =
|
||||
'Critical gap must be greater than or equal to the warning gap when enabled.';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_NODES = 'Proxmox Nodes';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_NODES = 'Virtualization Hosts';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_PBS = 'PBS Servers';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_GUESTS = 'VMs & Containers';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING = 'Guest Filtering';
|
||||
|
|
@ -69,8 +69,8 @@ export const ALERT_THRESHOLDS_SECTION_TITLE_BACKUPS = 'Recovery';
|
|||
export const ALERT_THRESHOLDS_SECTION_TITLE_SNAPSHOTS = 'Snapshot Age';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_STORAGE = 'Storage Devices';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_PMG = 'Mail Gateway Thresholds';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_AGENTS = 'Agents';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_AGENT_DISKS = 'Agent Disks';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_AGENTS = 'Systems';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_AGENT_DISKS = 'System Disks';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_DOCKER_HOSTS = 'Container Runtimes';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_DOCKER_CONTAINERS = 'Containers';
|
||||
|
||||
|
|
|
|||
|
|
@ -49,16 +49,16 @@ test.describe.serial('Core E2E flows', () => {
|
|||
await page.getByRole('button', { name: 'Thresholds' }).click();
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds/);
|
||||
await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible();
|
||||
// Proxmox Nodes section only appears when PVE nodes exist in unified resources.
|
||||
// Virtualization Hosts only appears when PVE nodes exist in unified resources.
|
||||
// In v6 the unified registry may not include PVE nodes — skip gracefully in that case.
|
||||
const proxmoxNodesHeading = page.getByRole('heading', { name: 'Proxmox Nodes' });
|
||||
const proxmoxNodesHeading = page.getByRole('heading', { name: 'Virtualization Hosts' });
|
||||
const hasProxmoxNodes = await proxmoxNodesHeading.isVisible({ timeout: 5000 }).catch(() => false);
|
||||
if (!hasProxmoxNodes) {
|
||||
test.skip(true, 'Proxmox Nodes section not present (nodes not in unified resources)');
|
||||
test.skip(true, 'Virtualization Hosts section not present (nodes not in unified resources)');
|
||||
}
|
||||
|
||||
const proxmoxNodesSection = page
|
||||
.getByRole('heading', { name: 'Proxmox Nodes' })
|
||||
.getByRole('heading', { name: 'Virtualization Hosts' })
|
||||
.locator('xpath=ancestor::*[.//table][1]');
|
||||
|
||||
const globalDefaultsRow = proxmoxNodesSection.locator('table tbody tr').filter({
|
||||
|
|
@ -86,7 +86,7 @@ test.describe.serial('Core E2E flows', () => {
|
|||
break;
|
||||
}
|
||||
if (targetRowIndex < 0) {
|
||||
test.skip(true, 'No Proxmox node row without an existing override was found');
|
||||
test.skip(true, 'No virtualization host row without an existing override was found');
|
||||
}
|
||||
|
||||
const targetRow = nodeRows.nth(targetRowIndex);
|
||||
|
|
@ -160,7 +160,7 @@ test.describe.serial('Core E2E flows', () => {
|
|||
|
||||
await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible();
|
||||
const rowAfterReload = page
|
||||
.getByRole('heading', { name: 'Proxmox Nodes' })
|
||||
.getByRole('heading', { name: 'Virtualization Hosts' })
|
||||
.locator('xpath=ancestor::*[.//table][1]')
|
||||
.locator('table tbody tr')
|
||||
.filter({ hasText: resourceName })
|
||||
|
|
|
|||
215
tests/integration/tests/26-truenas-alert-thresholds.spec.ts
Normal file
215
tests/integration/tests/26-truenas-alert-thresholds.spec.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/truenas-alert-thresholds.png';
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`truenas-alert-thresholds-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
});
|
||||
|
||||
test.describe('TrueNAS alert thresholds', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('routes TrueNAS through the neutral infrastructure and systems thresholds surfaces', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.pathname !== '/api/resources') {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'truenas-resource',
|
||||
type: 'truenas',
|
||||
name: 'truenas-main',
|
||||
displayName: 'TrueNAS Main',
|
||||
platformId: 'truenas-main',
|
||||
platformType: 'truenas',
|
||||
sourceType: 'hybrid',
|
||||
sources: ['agent', 'truenas'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'TrueNAS Main',
|
||||
hostname: 'truenas-main',
|
||||
platformId: 'truenas-main',
|
||||
},
|
||||
identity: {
|
||||
hostname: 'truenas-main',
|
||||
},
|
||||
agent: {
|
||||
agentId: 'truenas-main',
|
||||
hostname: 'truenas-main',
|
||||
platform: 'TrueNAS SCALE',
|
||||
disks: [
|
||||
{
|
||||
mountpoint: '/mnt/tank',
|
||||
type: 'zfs',
|
||||
used: 50 * 1024 * 1024 * 1024,
|
||||
total: 100 * 1024 * 1024 * 1024,
|
||||
},
|
||||
],
|
||||
},
|
||||
platformData: {
|
||||
sources: ['agent', 'truenas'],
|
||||
agent: {
|
||||
agentId: 'truenas-main',
|
||||
disks: [
|
||||
{
|
||||
mountpoint: '/mnt/tank',
|
||||
type: 'zfs',
|
||||
used: 50 * 1024 * 1024 * 1024,
|
||||
total: 100 * 1024 * 1024 * 1024,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'storage-truenas-tank',
|
||||
type: 'storage',
|
||||
name: 'tank',
|
||||
displayName: 'tank',
|
||||
parentId: 'truenas-resource',
|
||||
parentName: 'TrueNAS Main',
|
||||
platformId: 'truenas-storage-1',
|
||||
platformType: 'truenas',
|
||||
sourceType: 'api',
|
||||
sources: ['truenas'],
|
||||
status: 'online',
|
||||
lastSeen: '2026-03-29T22:00:00Z',
|
||||
canonicalIdentity: {
|
||||
displayName: 'tank',
|
||||
platformId: 'truenas-storage-1',
|
||||
},
|
||||
storage: {
|
||||
platform: 'truenas',
|
||||
type: 'zfs-pool',
|
||||
topology: 'pool',
|
||||
isZfs: true,
|
||||
},
|
||||
platformData: {
|
||||
node: 'truenas-main',
|
||||
instance: 'tank',
|
||||
sources: ['truenas'],
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/config', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
activationState: 'active',
|
||||
overrides: {},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/alerts/active', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/notifications/email', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
provider: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
to: [],
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/notifications/apprise', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/alerts/thresholds/proxmox', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds\/infrastructure/);
|
||||
await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Infrastructure' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Storage Devices' })).toBeVisible();
|
||||
await expect(page.getByText('tank', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Systems' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds\/systems/);
|
||||
const systemsTable = page.locator('table').first();
|
||||
const systemDisksSection = page.getByTestId('section-agentDisks');
|
||||
await expect(page.getByRole('heading', { name: 'Systems' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'System Disks' })).toBeVisible();
|
||||
await expect(systemsTable.getByText('TrueNAS Main', { exact: true })).toBeVisible();
|
||||
await expect(systemDisksSection.getByText('TrueNAS Main', { exact: true })).toBeVisible();
|
||||
await expect(systemDisksSection.getByText('/mnt/tank', { exact: true })).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue