mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-10 03:51:54 +00:00
Extract alert overview state owner
This commit is contained in:
parent
6dbb87071f
commit
24c724f0dd
7 changed files with 342 additions and 161 deletions
|
|
@ -177,6 +177,12 @@ Active alert card state, acknowledged badge, and primary/secondary action
|
|||
button presentation now route through
|
||||
`frontend-modern/src/utils/alertOverviewPresentation.ts` instead of remaining
|
||||
inline in `frontend-modern/src/features/alerts/OverviewTab.tsx`.
|
||||
The canonical overview runtime owner is now
|
||||
`frontend-modern/src/features/alerts/useAlertOverviewState.ts`, which owns the
|
||||
derived alert read-model, Last 24 Hours stat refresh, and single/bulk
|
||||
acknowledge control flow for `frontend-modern/src/features/alerts/OverviewTab.tsx`.
|
||||
Future overview action or stat behavior should extend that hook instead of
|
||||
putting acknowledge mutations and timer state back into the tab shell.
|
||||
Dashboard recent-alert rendering and dashboard alert summary/tone copy now
|
||||
route through that same alert overview presentation owner and the alert-owned
|
||||
`frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx` surface instead
|
||||
|
|
|
|||
|
|
@ -429,6 +429,12 @@ while `frontend-modern/src/features/alerts/OverviewTab.tsx` and
|
|||
surface composition. Future incident timeline fetch, note-save, or expansion
|
||||
control flow should extend that feature hook rather than forking back into
|
||||
either tab surface.
|
||||
Overview alert runtime now follows that same shell-versus-runtime split. The
|
||||
shell stays in `frontend-modern/src/features/alerts/OverviewTab.tsx`, while
|
||||
`frontend-modern/src/features/alerts/useAlertOverviewState.ts` owns derived
|
||||
alert stats, filtered ordering, and single/bulk acknowledge runtime behavior.
|
||||
Future overview control flow should extend that hook rather than restoring
|
||||
action timers or acknowledge mutations to the tab shell.
|
||||
Alert history runtime now follows that same pattern. The shell stays in
|
||||
`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx`, while
|
||||
`frontend-modern/src/features/alerts/useAlertHistoryState.ts` owns history
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { InvestigateAlertButton } from '@/components/Alerts/InvestigateAlertButton';
|
||||
|
||||
import { createSignal, createMemo, onCleanup, For, Show, createEffect } from 'solid-js';
|
||||
import { createSignal, onCleanup, For, Show, createEffect } from 'solid-js';
|
||||
import { useLocation } from '@solidjs/router';
|
||||
|
||||
import type { Alert } from '@/types/api';
|
||||
import type { Override } from './types';
|
||||
import { alertTypeDisplayLabel } from './helpers';
|
||||
import { getCanonicalAlertId } from './identity';
|
||||
import { useAlertOverviewState } from './useAlertOverviewState';
|
||||
import { useAlertIncidentTimelineState } from './useAlertIncidentTimelineState';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
|
|
@ -21,8 +22,7 @@ import {
|
|||
getAlertOverviewSecondaryActionClass,
|
||||
getAlertOverviewStartedAtClass,
|
||||
} from '@/utils/alertOverviewPresentation';
|
||||
|
||||
// Overview Tab - Shows current alert status
|
||||
|
||||
export function OverviewTab(props: {
|
||||
overrides: Override[];
|
||||
activeAlerts: Record<string, Alert>;
|
||||
|
|
@ -37,9 +37,20 @@ export function OverviewTab(props: {
|
|||
}) {
|
||||
const location = useLocation();
|
||||
let hashScrollRafId: number | undefined;
|
||||
const pendingProcessingResetTimeouts = new Set<number>();
|
||||
// Loading states for buttons
|
||||
const [processingAlerts, setProcessingAlerts] = createSignal<Set<string>>(new Set());
|
||||
const [lastHashScrolled, setLastHashScrolled] = createSignal<string | null>(null);
|
||||
const {
|
||||
alertStats,
|
||||
filteredAlerts,
|
||||
processingAlerts,
|
||||
bulkAckProcessing,
|
||||
handleAlertAcknowledgement,
|
||||
handleBulkAcknowledge,
|
||||
} = useAlertOverviewState({
|
||||
activeAlerts: () => props.activeAlerts,
|
||||
overrides: () => props.overrides,
|
||||
showAcknowledged: props.showAcknowledged,
|
||||
updateAlert: props.updateAlert,
|
||||
});
|
||||
const {
|
||||
incidentTimelines,
|
||||
incidentLoading,
|
||||
|
|
@ -54,63 +65,6 @@ export function OverviewTab(props: {
|
|||
setIncidentNoteDraft,
|
||||
saveIncidentNote,
|
||||
} = useAlertIncidentTimelineState();
|
||||
const [lastHashScrolled, setLastHashScrolled] = createSignal<string | null>(null);
|
||||
// Tick every 60s so the "Last 24 Hours" count stays fresh as alerts age out
|
||||
const [tick, setTick] = createSignal(Date.now());
|
||||
const tickInterval = setInterval(() => setTick(Date.now()), 60_000);
|
||||
onCleanup(() => clearInterval(tickInterval));
|
||||
const processingReleaseTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const clearProcessingReleaseTimer = (alertIdentifier: string) => {
|
||||
const timer = processingReleaseTimers.get(alertIdentifier);
|
||||
if (timer === undefined) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
processingReleaseTimers.delete(alertIdentifier);
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
processingReleaseTimers.forEach((timer) => clearTimeout(timer));
|
||||
processingReleaseTimers.clear();
|
||||
});
|
||||
|
||||
// Get alert stats from actual active alerts
|
||||
const alertStats = createMemo(() => {
|
||||
// Access the store properly for reactivity
|
||||
const alertIds = Object.keys(props.activeAlerts);
|
||||
const alerts = alertIds.map((id) => props.activeAlerts[id]);
|
||||
return {
|
||||
active: alerts.filter((a) => !a.acknowledged).length,
|
||||
acknowledged: alerts.filter((a) => a.acknowledged).length,
|
||||
total24h: alerts.filter((a) => {
|
||||
const age = tick() - new Date(a.startTime).getTime();
|
||||
return age >= 0 && age < 86_400_000;
|
||||
}).length,
|
||||
overrides: props.overrides.length,
|
||||
};
|
||||
});
|
||||
|
||||
const filteredAlerts = createMemo(() => {
|
||||
const alerts = Object.values(props.activeAlerts);
|
||||
// Sort: unacknowledged first, then by start time (newest first)
|
||||
return alerts
|
||||
.filter((alert) => props.showAcknowledged() || !alert.acknowledged)
|
||||
.sort((a, b) => {
|
||||
// Acknowledged status comparison first
|
||||
if (a.acknowledged !== b.acknowledged) {
|
||||
return a.acknowledged ? 1 : -1; // Unacknowledged first
|
||||
}
|
||||
// Then by time
|
||||
return new Date(b.startTime).getTime() - new Date(a.startTime).getTime();
|
||||
});
|
||||
});
|
||||
|
||||
const unacknowledgedAlerts = createMemo(() =>
|
||||
Object.values(props.activeAlerts).filter((alert) => !alert.acknowledged),
|
||||
);
|
||||
|
||||
const [bulkAckProcessing, setBulkAckProcessing] = createSignal(false);
|
||||
|
||||
const scrollToAlertHash = () => {
|
||||
const hash = location.hash;
|
||||
|
|
@ -147,10 +101,6 @@ export function OverviewTab(props: {
|
|||
cancelAnimationFrame(hashScrollRafId);
|
||||
hashScrollRafId = undefined;
|
||||
}
|
||||
pendingProcessingResetTimeouts.forEach((timeoutId) => {
|
||||
window.clearTimeout(timeoutId);
|
||||
});
|
||||
pendingProcessingResetTimeouts.clear();
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -319,44 +269,8 @@ export function OverviewTab(props: {
|
|||
type="button"
|
||||
class="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md border border-blue-200 dark:border-blue-700 bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-200 transition-colors hover:bg-blue-100 dark:hover:bg-blue-900 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
disabled={bulkAckProcessing()}
|
||||
onClick={async () => {
|
||||
if (bulkAckProcessing()) return;
|
||||
const pending = unacknowledgedAlerts();
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
setBulkAckProcessing(true);
|
||||
try {
|
||||
const result = await AlertsAPI.bulkAcknowledge(
|
||||
pending.map((alert) => getCanonicalAlertId(alert)),
|
||||
);
|
||||
const successes = result.results.filter((r) => r.success);
|
||||
const failures = result.results.filter((r) => !r.success);
|
||||
|
||||
successes.forEach((res) => {
|
||||
props.updateAlert(res.alertIdentifier, {
|
||||
acknowledged: true,
|
||||
ackTime: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
if (successes.length > 0) {
|
||||
notificationStore.success(
|
||||
`Acknowledged ${successes.length} ${successes.length === 1 ? 'alert' : 'alerts'}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
notificationStore.error(
|
||||
`Failed to acknowledge ${failures.length} ${failures.length === 1 ? 'alert' : 'alerts'}.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Bulk acknowledge failed', error);
|
||||
notificationStore.error('Failed to acknowledge alerts');
|
||||
} finally {
|
||||
setBulkAckProcessing(false);
|
||||
}
|
||||
onClick={() => {
|
||||
void handleBulkAcknowledge();
|
||||
}}
|
||||
>
|
||||
{bulkAckProcessing()
|
||||
|
|
@ -454,62 +368,7 @@ export function OverviewTab(props: {
|
|||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const alertIdentifier = getCanonicalAlertId(alert);
|
||||
|
||||
// Prevent double-clicks
|
||||
if (processingAlerts().has(alertIdentifier)) return;
|
||||
|
||||
setProcessingAlerts(
|
||||
(prev) => new Set(prev).add(alertIdentifier),
|
||||
);
|
||||
|
||||
// Store current state to avoid race conditions
|
||||
const wasAcknowledged = alert.acknowledged;
|
||||
|
||||
try {
|
||||
if (wasAcknowledged) {
|
||||
// Call API first, only update local state if successful
|
||||
await AlertsAPI.unacknowledge(alertIdentifier);
|
||||
// Only update local state after successful API call
|
||||
props.updateAlert(alertIdentifier, {
|
||||
acknowledged: false,
|
||||
ackTime: undefined,
|
||||
ackUser: undefined,
|
||||
});
|
||||
notificationStore.success('Alert restored');
|
||||
} else {
|
||||
// Call API first, only update local state if successful
|
||||
await AlertsAPI.acknowledge(alertIdentifier);
|
||||
// Only update local state after successful API call
|
||||
props.updateAlert(alertIdentifier, {
|
||||
acknowledged: true,
|
||||
ackTime: new Date().toISOString(),
|
||||
});
|
||||
notificationStore.success('Alert acknowledged');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Failed to ${wasAcknowledged ? 'unacknowledge' : 'acknowledge'} alert:`,
|
||||
err,
|
||||
);
|
||||
notificationStore.error(
|
||||
`Failed to ${wasAcknowledged ? 'restore' : 'acknowledge'} alert`,
|
||||
);
|
||||
// Don't update local state on error - let WebSocket keep the correct state
|
||||
} finally {
|
||||
// Keep button disabled for longer to prevent race conditions with WebSocket updates
|
||||
clearProcessingReleaseTimer(alertIdentifier);
|
||||
const timer = setTimeout(() => {
|
||||
processingReleaseTimers.delete(alertIdentifier);
|
||||
setProcessingAlerts((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(alertIdentifier);
|
||||
return next;
|
||||
});
|
||||
}, 1500); // 1.5 seconds to allow server to process and WebSocket to sync
|
||||
processingReleaseTimers.set(alertIdentifier, timer);
|
||||
}
|
||||
await handleAlertAcknowledgement(alert);
|
||||
}}
|
||||
>
|
||||
{processingAlerts().has(getCanonicalAlertId(alert))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import { renderHook } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AlertsAPI } from '@/api/alerts';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { Alert } from '@/types/api';
|
||||
|
||||
import { useAlertOverviewState } from '../useAlertOverviewState';
|
||||
|
||||
vi.mock('@/api/alerts', () => ({
|
||||
AlertsAPI: {
|
||||
acknowledge: vi.fn(),
|
||||
bulkAcknowledge: vi.fn(),
|
||||
unacknowledge: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/notifications', () => ({
|
||||
notificationStore: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function makeAlert(id: string, startTime: string, acknowledged = false): Alert {
|
||||
return {
|
||||
id,
|
||||
type: 'cpu',
|
||||
level: 'warning',
|
||||
resourceId: `vm-${id}`,
|
||||
resourceName: `VM ${id}`,
|
||||
node: 'node-1',
|
||||
message: `CPU high on ${id}`,
|
||||
startTime,
|
||||
acknowledged,
|
||||
} as Alert;
|
||||
}
|
||||
|
||||
describe('useAlertOverviewState', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-22T12:00:00Z'));
|
||||
vi.mocked(AlertsAPI.acknowledge).mockReset();
|
||||
vi.mocked(AlertsAPI.unacknowledge).mockReset();
|
||||
vi.mocked(AlertsAPI.bulkAcknowledge).mockReset();
|
||||
vi.mocked(notificationStore.success).mockReset();
|
||||
vi.mocked(notificationStore.error).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('owns overview stats, filtering, and acknowledge flows outside the tab shell', async () => {
|
||||
const now = Date.now();
|
||||
const [activeAlerts] = createSignal<Record<string, Alert>>({
|
||||
warning: makeAlert('warning', new Date(now - 60_000).toISOString(), false),
|
||||
acknowledged: makeAlert('acknowledged', new Date(now - 2 * 60_000).toISOString(), true),
|
||||
old: makeAlert('old', new Date(now - 3 * 86_400_000).toISOString(), false),
|
||||
});
|
||||
const [showAcknowledged] = createSignal(false);
|
||||
const updateAlert = vi.fn();
|
||||
|
||||
vi.mocked(AlertsAPI.acknowledge).mockResolvedValue(undefined as any);
|
||||
vi.mocked(AlertsAPI.unacknowledge).mockResolvedValue(undefined as any);
|
||||
vi.mocked(AlertsAPI.bulkAcknowledge).mockResolvedValue({
|
||||
results: [
|
||||
{ alertIdentifier: 'warning', success: true },
|
||||
{ alertIdentifier: 'old', success: false },
|
||||
],
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlertOverviewState({
|
||||
activeAlerts,
|
||||
overrides: () => [],
|
||||
showAcknowledged,
|
||||
updateAlert,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.alertStats()).toMatchObject({
|
||||
active: 2,
|
||||
acknowledged: 1,
|
||||
total24h: 2,
|
||||
overrides: 0,
|
||||
});
|
||||
expect(result.filteredAlerts().map((alert) => alert.id)).toEqual(['warning', 'old']);
|
||||
|
||||
await result.handleAlertAcknowledgement(activeAlerts().warning);
|
||||
|
||||
expect(AlertsAPI.acknowledge).toHaveBeenCalledWith('warning');
|
||||
expect(updateAlert).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.objectContaining({ acknowledged: true }),
|
||||
);
|
||||
expect(notificationStore.success).toHaveBeenCalledWith('Alert acknowledged');
|
||||
expect(result.processingAlerts().has('warning')).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(result.processingAlerts().has('warning')).toBe(false);
|
||||
|
||||
await result.handleBulkAcknowledge();
|
||||
|
||||
expect(AlertsAPI.bulkAcknowledge).toHaveBeenCalledWith(['warning', 'old']);
|
||||
expect(updateAlert).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.objectContaining({ acknowledged: true }),
|
||||
);
|
||||
expect(notificationStore.error).toHaveBeenCalledWith('Failed to acknowledge 1 alert.');
|
||||
});
|
||||
});
|
||||
172
frontend-modern/src/features/alerts/useAlertOverviewState.ts
Normal file
172
frontend-modern/src/features/alerts/useAlertOverviewState.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { createMemo, createSignal, onCleanup } from 'solid-js';
|
||||
import type { Accessor } from 'solid-js';
|
||||
|
||||
import { AlertsAPI } from '@/api/alerts';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { Alert } from '@/types/api';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
import { getCanonicalAlertId } from './identity';
|
||||
import type { Override } from './types';
|
||||
|
||||
export interface UseAlertOverviewStateProps {
|
||||
activeAlerts: Accessor<Record<string, Alert>>;
|
||||
overrides: Accessor<Override[]>;
|
||||
showAcknowledged: Accessor<boolean>;
|
||||
updateAlert: (alertIdentifier: string, updates: Partial<Alert>) => void;
|
||||
}
|
||||
|
||||
export function useAlertOverviewState(props: UseAlertOverviewStateProps) {
|
||||
const [processingAlerts, setProcessingAlerts] = createSignal<Set<string>>(new Set());
|
||||
const [bulkAckProcessing, setBulkAckProcessing] = createSignal(false);
|
||||
const [tick, setTick] = createSignal(Date.now());
|
||||
const tickInterval = setInterval(() => setTick(Date.now()), 60_000);
|
||||
const processingReleaseTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const clearProcessingReleaseTimer = (alertIdentifier: string) => {
|
||||
const timer = processingReleaseTimers.get(alertIdentifier);
|
||||
if (timer === undefined) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
processingReleaseTimers.delete(alertIdentifier);
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(tickInterval);
|
||||
processingReleaseTimers.forEach((timer) => clearTimeout(timer));
|
||||
processingReleaseTimers.clear();
|
||||
});
|
||||
|
||||
const alertStats = createMemo(() => {
|
||||
const alerts = Object.values(props.activeAlerts());
|
||||
return {
|
||||
active: alerts.filter((alert) => !alert.acknowledged).length,
|
||||
acknowledged: alerts.filter((alert) => alert.acknowledged).length,
|
||||
total24h: alerts.filter((alert) => {
|
||||
const age = tick() - new Date(alert.startTime).getTime();
|
||||
return age >= 0 && age < 86_400_000;
|
||||
}).length,
|
||||
overrides: props.overrides().length,
|
||||
};
|
||||
});
|
||||
|
||||
const filteredAlerts = createMemo(() =>
|
||||
Object.values(props.activeAlerts())
|
||||
.filter((alert) => props.showAcknowledged() || !alert.acknowledged)
|
||||
.sort((a, b) => {
|
||||
if (a.acknowledged !== b.acknowledged) {
|
||||
return a.acknowledged ? 1 : -1;
|
||||
}
|
||||
return new Date(b.startTime).getTime() - new Date(a.startTime).getTime();
|
||||
}),
|
||||
);
|
||||
|
||||
const unacknowledgedAlerts = createMemo(() =>
|
||||
Object.values(props.activeAlerts()).filter((alert) => !alert.acknowledged),
|
||||
);
|
||||
|
||||
const releaseAlertProcessing = (alertIdentifier: string) => {
|
||||
clearProcessingReleaseTimer(alertIdentifier);
|
||||
const timer = setTimeout(() => {
|
||||
processingReleaseTimers.delete(alertIdentifier);
|
||||
setProcessingAlerts((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(alertIdentifier);
|
||||
return next;
|
||||
});
|
||||
}, 1500);
|
||||
processingReleaseTimers.set(alertIdentifier, timer);
|
||||
};
|
||||
|
||||
const handleAlertAcknowledgement = async (alert: Alert) => {
|
||||
const alertIdentifier = getCanonicalAlertId(alert);
|
||||
if (processingAlerts().has(alertIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingAlerts((prev) => new Set(prev).add(alertIdentifier));
|
||||
const wasAcknowledged = alert.acknowledged;
|
||||
|
||||
try {
|
||||
if (wasAcknowledged) {
|
||||
await AlertsAPI.unacknowledge(alertIdentifier);
|
||||
props.updateAlert(alertIdentifier, {
|
||||
acknowledged: false,
|
||||
ackTime: undefined,
|
||||
ackUser: undefined,
|
||||
});
|
||||
notificationStore.success('Alert restored');
|
||||
} else {
|
||||
await AlertsAPI.acknowledge(alertIdentifier);
|
||||
props.updateAlert(alertIdentifier, {
|
||||
acknowledged: true,
|
||||
ackTime: new Date().toISOString(),
|
||||
});
|
||||
notificationStore.success('Alert acknowledged');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to ${wasAcknowledged ? 'unacknowledge' : 'acknowledge'} alert:`,
|
||||
error,
|
||||
);
|
||||
notificationStore.error(`Failed to ${wasAcknowledged ? 'restore' : 'acknowledge'} alert`);
|
||||
} finally {
|
||||
releaseAlertProcessing(alertIdentifier);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkAcknowledge = async () => {
|
||||
if (bulkAckProcessing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = unacknowledgedAlerts();
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBulkAckProcessing(true);
|
||||
try {
|
||||
const result = await AlertsAPI.bulkAcknowledge(
|
||||
pending.map((alert) => getCanonicalAlertId(alert)),
|
||||
);
|
||||
const successes = result.results.filter((entry) => entry.success);
|
||||
const failures = result.results.filter((entry) => !entry.success);
|
||||
|
||||
successes.forEach((entry) => {
|
||||
props.updateAlert(entry.alertIdentifier, {
|
||||
acknowledged: true,
|
||||
ackTime: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
if (successes.length > 0) {
|
||||
notificationStore.success(
|
||||
`Acknowledged ${successes.length} ${successes.length === 1 ? 'alert' : 'alerts'}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
notificationStore.error(
|
||||
`Failed to acknowledge ${failures.length} ${failures.length === 1 ? 'alert' : 'alerts'}.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Bulk acknowledge failed', error);
|
||||
notificationStore.error('Failed to acknowledge alerts');
|
||||
} finally {
|
||||
setBulkAckProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
alertStats,
|
||||
filteredAlerts,
|
||||
unacknowledgedAlerts,
|
||||
processingAlerts,
|
||||
bulkAckProcessing,
|
||||
handleAlertAcknowledgement,
|
||||
handleBulkAcknowledge,
|
||||
};
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigura
|
|||
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
|
||||
import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertOverviewStateSource from '@/features/alerts/useAlertOverviewState.ts?raw';
|
||||
import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw';
|
||||
import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw';
|
||||
import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw';
|
||||
import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw';
|
||||
import alertThresholdsTabSource from '@/features/alerts/tabs/ThresholdsTab.tsx?raw';
|
||||
import thresholdsTableSource from '@/components/Alerts/ThresholdsTable.tsx?raw';
|
||||
|
|
@ -221,6 +223,14 @@ describe('tab path helpers', () => {
|
|||
);
|
||||
expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.getIncidentTimeline');
|
||||
expect(alertIncidentTimelineStateSource).toContain('AlertsAPI.addIncidentNote');
|
||||
expect(alertOverviewTabSource).toContain('useAlertOverviewState');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.bulkAcknowledge');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.acknowledge');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.unacknowledge');
|
||||
expect(alertOverviewStateSource).toContain('export function useAlertOverviewState');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.bulkAcknowledge');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.acknowledge');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.unacknowledge');
|
||||
expect(alertScheduleTabSource).toContain('getAlertConfigQuietHourSuppressOptions');
|
||||
expect(alertThresholdsTabSource).toContain('ThresholdsTable');
|
||||
expect(thresholdsTableSource).toContain(
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigura
|
|||
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
|
||||
import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertOverviewStateSource from '@/features/alerts/useAlertOverviewState.ts?raw';
|
||||
import alertDestinationsTabSource from '@/features/alerts/tabs/DestinationsTab.tsx?raw';
|
||||
import alertHistoryTabSource from '@/features/alerts/tabs/HistoryTab.tsx?raw';
|
||||
import alertScheduleTabSource from '@/features/alerts/tabs/ScheduleTab.tsx?raw';
|
||||
|
|
@ -3612,6 +3613,7 @@ describe('frontend resource type boundaries', () => {
|
|||
it('keeps alert incident timeline state copy in a shared presentation utility', () => {
|
||||
expect(alertOverviewTabSource).toContain('IncidentTimelinePanel');
|
||||
expect(alertOverviewTabSource).toContain('useAlertIncidentTimelineState');
|
||||
expect(alertOverviewTabSource).toContain('useAlertOverviewState');
|
||||
expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterContainerClass');
|
||||
expect(alertIncidentEventFiltersSource).toContain('getAlertIncidentEventFilterChipClass');
|
||||
expect(alertIncidentEventFiltersSource).toContain(
|
||||
|
|
@ -3644,6 +3646,9 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(alertOverviewTabSource).toContain('getAlertOverviewStartedAtClass');
|
||||
expect(alertOverviewTabSource).toContain('getAlertOverviewPrimaryActionClass');
|
||||
expect(alertOverviewTabSource).toContain('getAlertOverviewSecondaryActionClass');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.bulkAcknowledge');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.acknowledge');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.unacknowledge');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.getIncidentTimeline');
|
||||
expect(alertOverviewTabSource).not.toContain('AlertsAPI.addIncidentNote');
|
||||
expect(alertOverviewTabSource).not.toContain('Loading timeline...');
|
||||
|
|
@ -3651,6 +3656,10 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(alertOverviewTabSource).not.toContain('No timeline events yet.');
|
||||
expect(alertOverviewTabSource).not.toContain('No incident timeline available.');
|
||||
expect(alertOverviewTabSource).not.toContain('Failed to load timeline.');
|
||||
expect(alertOverviewStateSource).toContain('export function useAlertOverviewState');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.bulkAcknowledge');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.acknowledge');
|
||||
expect(alertOverviewStateSource).toContain('AlertsAPI.unacknowledge');
|
||||
expect(alertOverviewPresentationSource).toContain(
|
||||
'export function getAlertTimelineLoadingState',
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue