mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 12:52:25 +00:00
Extract alert resource incidents runtime owner
This commit is contained in:
@@ -288,10 +288,12 @@ primitive.
|
||||
The history tab itself now follows the same shell-versus-runtime rule. The
|
||||
canonical history runtime owner is
|
||||
`frontend-modern/src/features/alerts/useAlertHistoryState.ts`, which now owns
|
||||
alert-history fetch, persistent filter state, resource-incident panel loading,
|
||||
and history-clear flow, while the pure analytics model for history-item
|
||||
projection, trend buckets, group labels, axis ticks, and selected bucket
|
||||
detail now lives in
|
||||
alert-history fetch, persistent filter state, history-clear flow, and
|
||||
composition of the derived history owners. Resource-incident panel loading,
|
||||
refresh, and expansion state now live in
|
||||
`frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts`, while
|
||||
the pure analytics model for history-item projection, trend buckets, group
|
||||
labels, axis ticks, and selected bucket detail now lives in
|
||||
`frontend-modern/src/features/alerts/alertHistoryModel.ts`. The tab shell in
|
||||
`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` now composes
|
||||
`frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx`,
|
||||
|
||||
@@ -551,10 +551,12 @@ back into a mixed 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
|
||||
fetch, persistent filters, resource-incident panel state, and history-clear
|
||||
behavior, while `frontend-modern/src/features/alerts/alertHistoryModel.ts`
|
||||
owns grouped/trend derivation and the bucket/range analytics contract. The
|
||||
render-heavy surfaces now route through
|
||||
fetch, persistent filters, history-clear behavior, and composition of the
|
||||
derived history owners. Resource-incident panel runtime now lives in
|
||||
`frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts`, while
|
||||
`frontend-modern/src/features/alerts/alertHistoryModel.ts` owns grouped/trend
|
||||
derivation and the bucket/range analytics contract. The render-heavy surfaces
|
||||
now route through
|
||||
`frontend-modern/src/features/alerts/AlertHistoryFrequencyCard.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx`,
|
||||
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AlertsAPI } from '@/api/alerts';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import { eventBus } from '@/stores/events';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { Alert, Incident } from '@/types/api';
|
||||
import type { Alert } from '@/types/api';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
getAlertAdministrationClearHistoryError,
|
||||
getAlertAdministrationClearHistoryConfirmation,
|
||||
} from '@/utils/alertAdministrationPresentation';
|
||||
import { getAlertResourceIncidentLoadFailure } from '@/utils/alertIncidentPresentation';
|
||||
|
||||
import {
|
||||
applyAlertHistoryWindow,
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
type HistoryItem,
|
||||
} from './alertHistoryModel';
|
||||
import { useAlertIncidentTimelineState } from './useAlertIncidentTimelineState';
|
||||
import { INCIDENT_EVENT_TYPES } from './types';
|
||||
import { useAlertResourceIncidentsState } from './useAlertResourceIncidentsState';
|
||||
|
||||
export interface UseAlertHistoryStateProps {
|
||||
activeAlerts: Accessor<Record<string, Alert>>;
|
||||
@@ -61,21 +60,8 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const [alertHistory, setAlertHistory] = createSignal<Alert[]>([]);
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
const [selectedBarIndex, setSelectedBarIndex] = createSignal<number | null>(null);
|
||||
const [resourceIncidentPanel, setResourceIncidentPanel] = createSignal<{
|
||||
resourceId: string;
|
||||
resourceName: string;
|
||||
} | null>(null);
|
||||
const [resourceIncidents, setResourceIncidents] = createSignal<Record<string, Incident[]>>({});
|
||||
const [resourceIncidentLoading, setResourceIncidentLoading] = createSignal<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [expandedResourceIncidentIds, setExpandedResourceIncidentIds] = createSignal<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [resourceIncidentEventFilters, setResourceIncidentEventFilters] = createSignal<Set<string>>(
|
||||
new Set(INCIDENT_EVENT_TYPES),
|
||||
);
|
||||
const [filtersOpen, setFiltersOpen] = createSignal(false);
|
||||
const resourceIncidentsState = useAlertResourceIncidentsState();
|
||||
|
||||
const {
|
||||
incidentTimelines,
|
||||
@@ -150,10 +136,7 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const unsubscribeOrgSwitched = eventBus.on('org_switched', () => {
|
||||
setAlertHistory([]);
|
||||
setSelectedBarIndex(null);
|
||||
setResourceIncidentPanel(null);
|
||||
setResourceIncidents({});
|
||||
setResourceIncidentLoading({});
|
||||
setExpandedResourceIncidentIds(new Set<string>());
|
||||
resourceIncidentsState.resetResourceIncidentsState();
|
||||
resetState();
|
||||
void fetchHistory(timeFilter());
|
||||
});
|
||||
@@ -174,49 +157,6 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
void fetchHistory(range);
|
||||
});
|
||||
|
||||
const loadResourceIncidents = async (resourceId: string, limit = 10) => {
|
||||
if (!resourceId) return;
|
||||
|
||||
setResourceIncidentLoading((prev) => ({ ...prev, [resourceId]: true }));
|
||||
try {
|
||||
const incidents = await AlertsAPI.getIncidentsForResource(resourceId, limit);
|
||||
setResourceIncidents((prev) => ({ ...prev, [resourceId]: incidents }));
|
||||
} catch (error) {
|
||||
logger.error(getAlertResourceIncidentLoadFailure(), error);
|
||||
notificationStore.error(getAlertResourceIncidentLoadFailure());
|
||||
} finally {
|
||||
setResourceIncidentLoading((prev) => ({ ...prev, [resourceId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const openResourceIncidentPanel = async (resourceId: string, resourceName: string) => {
|
||||
if (!resourceId) return;
|
||||
|
||||
setResourceIncidentPanel({ resourceId, resourceName });
|
||||
setExpandedResourceIncidentIds(new Set<string>());
|
||||
if (!(resourceId in resourceIncidents())) {
|
||||
await loadResourceIncidents(resourceId);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshResourceIncidentPanel = async () => {
|
||||
const selection = resourceIncidentPanel();
|
||||
if (!selection) return;
|
||||
await loadResourceIncidents(selection.resourceId);
|
||||
};
|
||||
|
||||
const toggleResourceIncidentDetails = (incidentId: string) => {
|
||||
setExpandedResourceIncidentIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(incidentId)) {
|
||||
next.delete(incidentId);
|
||||
} else {
|
||||
next.add(incidentId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const allHistoryData = createMemo<HistoryItem[]>(() => {
|
||||
return buildAlertHistoryItems({
|
||||
activeAlerts: props.activeAlerts() || {},
|
||||
@@ -285,13 +225,13 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
loading,
|
||||
selectedBarIndex,
|
||||
setSelectedBarIndex,
|
||||
resourceIncidentPanel,
|
||||
setResourceIncidentPanel,
|
||||
resourceIncidents,
|
||||
resourceIncidentLoading,
|
||||
expandedResourceIncidentIds,
|
||||
resourceIncidentEventFilters,
|
||||
setResourceIncidentEventFilters,
|
||||
resourceIncidentPanel: resourceIncidentsState.resourceIncidentPanel,
|
||||
setResourceIncidentPanel: resourceIncidentsState.setResourceIncidentPanel,
|
||||
resourceIncidents: resourceIncidentsState.resourceIncidents,
|
||||
resourceIncidentLoading: resourceIncidentsState.resourceIncidentLoading,
|
||||
expandedResourceIncidentIds: resourceIncidentsState.expandedResourceIncidentIds,
|
||||
resourceIncidentEventFilters: resourceIncidentsState.resourceIncidentEventFilters,
|
||||
setResourceIncidentEventFilters: resourceIncidentsState.setResourceIncidentEventFilters,
|
||||
filtersOpen,
|
||||
setFiltersOpen,
|
||||
activeFilterCount,
|
||||
@@ -307,9 +247,9 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
toggleIncidentTimeline,
|
||||
setIncidentNoteDraft,
|
||||
saveIncidentNote,
|
||||
openResourceIncidentPanel,
|
||||
refreshResourceIncidentPanel,
|
||||
toggleResourceIncidentDetails,
|
||||
openResourceIncidentPanel: resourceIncidentsState.openResourceIncidentPanel,
|
||||
refreshResourceIncidentPanel: resourceIncidentsState.refreshResourceIncidentPanel,
|
||||
toggleResourceIncidentDetails: resourceIncidentsState.toggleResourceIncidentDetails,
|
||||
alertData,
|
||||
groupedAlerts,
|
||||
alertTrends,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
|
||||
import { AlertsAPI } from '@/api/alerts';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { Incident } from '@/types/api';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { getAlertResourceIncidentLoadFailure } from '@/utils/alertIncidentPresentation';
|
||||
|
||||
import { INCIDENT_EVENT_TYPES } from './types';
|
||||
|
||||
export function useAlertResourceIncidentsState() {
|
||||
const [resourceIncidentPanel, setResourceIncidentPanel] = createSignal<{
|
||||
resourceId: string;
|
||||
resourceName: string;
|
||||
} | null>(null);
|
||||
const [resourceIncidents, setResourceIncidents] = createSignal<Record<string, Incident[]>>({});
|
||||
const [resourceIncidentLoading, setResourceIncidentLoading] = createSignal<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [expandedResourceIncidentIds, setExpandedResourceIncidentIds] = createSignal<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [resourceIncidentEventFilters, setResourceIncidentEventFilters] = createSignal<Set<string>>(
|
||||
new Set(INCIDENT_EVENT_TYPES),
|
||||
);
|
||||
|
||||
const loadResourceIncidents = async (resourceId: string, limit = 10) => {
|
||||
if (!resourceId) return;
|
||||
|
||||
setResourceIncidentLoading((prev) => ({ ...prev, [resourceId]: true }));
|
||||
try {
|
||||
const incidents = await AlertsAPI.getIncidentsForResource(resourceId, limit);
|
||||
setResourceIncidents((prev) => ({ ...prev, [resourceId]: incidents }));
|
||||
} catch (error) {
|
||||
logger.error(getAlertResourceIncidentLoadFailure(), error);
|
||||
notificationStore.error(getAlertResourceIncidentLoadFailure());
|
||||
} finally {
|
||||
setResourceIncidentLoading((prev) => ({ ...prev, [resourceId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const openResourceIncidentPanel = async (resourceId: string, resourceName: string) => {
|
||||
if (!resourceId) return;
|
||||
|
||||
setResourceIncidentPanel({ resourceId, resourceName });
|
||||
setExpandedResourceIncidentIds(new Set<string>());
|
||||
if (!(resourceId in resourceIncidents())) {
|
||||
await loadResourceIncidents(resourceId);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshResourceIncidentPanel = async () => {
|
||||
const selection = resourceIncidentPanel();
|
||||
if (!selection) return;
|
||||
await loadResourceIncidents(selection.resourceId);
|
||||
};
|
||||
|
||||
const toggleResourceIncidentDetails = (incidentId: string) => {
|
||||
setExpandedResourceIncidentIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(incidentId)) {
|
||||
next.delete(incidentId);
|
||||
} else {
|
||||
next.add(incidentId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const resetResourceIncidentsState = () => {
|
||||
setResourceIncidentPanel(null);
|
||||
setResourceIncidents({});
|
||||
setResourceIncidentLoading({});
|
||||
setExpandedResourceIncidentIds(new Set<string>());
|
||||
setResourceIncidentEventFilters(new Set(INCIDENT_EVENT_TYPES));
|
||||
};
|
||||
|
||||
return {
|
||||
resourceIncidentPanel,
|
||||
setResourceIncidentPanel,
|
||||
resourceIncidents,
|
||||
resourceIncidentLoading,
|
||||
expandedResourceIncidentIds,
|
||||
resourceIncidentEventFilters,
|
||||
setResourceIncidentEventFilters,
|
||||
openResourceIncidentPanel,
|
||||
refreshResourceIncidentPanel,
|
||||
toggleResourceIncidentDetails,
|
||||
resetResourceIncidentsState,
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import alertHistoryTableGroupRowSource from '@/features/alerts/AlertHistoryTable
|
||||
import alertHistoryTableSectionSource from '@/features/alerts/AlertHistoryTableSection.tsx?raw';
|
||||
import alertResourceIncidentsPanelSource from '@/features/alerts/AlertResourceIncidentsPanel.tsx?raw';
|
||||
import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw';
|
||||
import alertResourceIncidentsStateSource from '@/features/alerts/useAlertResourceIncidentsState.ts?raw';
|
||||
import alertHistoryModelSource from '@/features/alerts/alertHistoryModel.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertOverviewActiveAlertsSectionSource from '@/features/alerts/AlertOverviewActiveAlertsSection.tsx?raw';
|
||||
@@ -336,9 +337,10 @@ describe('tab path helpers', () => {
|
||||
expect(alertHistoryStateSource).toContain('export function useAlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('export type AlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.getHistory');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.clearHistory');
|
||||
expect(alertHistoryStateSource).toContain('useAlertResourceIncidentsState');
|
||||
expect(alertHistoryStateSource).toContain('useAlertIncidentTimelineState');
|
||||
expect(alertHistoryStateSource).not.toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertHistoryStateSource).toContain('buildAlertHistoryItems');
|
||||
expect(alertHistoryStateSource).toContain('buildAlertTrends');
|
||||
expect(alertHistoryStateSource).toContain('groupAlertHistoryItems');
|
||||
@@ -350,6 +352,10 @@ describe('tab path helpers', () => {
|
||||
expect(alertHistoryModelSource).toContain('export function buildAlertTrends');
|
||||
expect(alertHistoryModelSource).toContain('export function groupAlertHistoryItems');
|
||||
expect(alertHistoryModelSource).toContain('export const MS_PER_HOUR');
|
||||
expect(alertResourceIncidentsStateSource).toContain(
|
||||
'export function useAlertResourceIncidentsState',
|
||||
);
|
||||
expect(alertResourceIncidentsStateSource).toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertIncidentTimelineStateSource).toContain(
|
||||
'export function useAlertIncidentTimelineState',
|
||||
);
|
||||
|
||||
@@ -340,6 +340,7 @@ import alertHistoryTableGroupRowSource from '@/features/alerts/AlertHistoryTable
|
||||
import alertHistoryTableSectionSource from '@/features/alerts/AlertHistoryTableSection.tsx?raw';
|
||||
import alertResourceIncidentsPanelSource from '@/features/alerts/AlertResourceIncidentsPanel.tsx?raw';
|
||||
import alertHistoryStateSource from '@/features/alerts/useAlertHistoryState.ts?raw';
|
||||
import alertResourceIncidentsStateSource from '@/features/alerts/useAlertResourceIncidentsState.ts?raw';
|
||||
import alertHistoryModelSource from '@/features/alerts/alertHistoryModel.ts?raw';
|
||||
import alertIncidentTimelineStateSource from '@/features/alerts/useAlertIncidentTimelineState.ts?raw';
|
||||
import alertOverviewActiveAlertsSectionSource from '@/features/alerts/AlertOverviewActiveAlertsSection.tsx?raw';
|
||||
@@ -2937,9 +2938,10 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(alertHistoryStateSource).toContain('export function useAlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('export type AlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.getHistory');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.clearHistory');
|
||||
expect(alertHistoryStateSource).toContain('useAlertResourceIncidentsState');
|
||||
expect(alertHistoryStateSource).toContain('useAlertIncidentTimelineState');
|
||||
expect(alertHistoryStateSource).not.toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertHistoryStateSource).toContain('buildAlertHistoryItems');
|
||||
expect(alertHistoryStateSource).toContain('buildAlertTrends');
|
||||
expect(alertHistoryStateSource).toContain('groupAlertHistoryItems');
|
||||
@@ -2951,6 +2953,10 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(alertHistoryModelSource).toContain('export function buildAlertTrends');
|
||||
expect(alertHistoryModelSource).toContain('export function groupAlertHistoryItems');
|
||||
expect(alertHistoryModelSource).toContain('export const MS_PER_HOUR');
|
||||
expect(alertResourceIncidentsStateSource).toContain(
|
||||
'export function useAlertResourceIncidentsState',
|
||||
);
|
||||
expect(alertResourceIncidentsStateSource).toContain('AlertsAPI.getIncidentsForResource');
|
||||
expect(alertsPageSource).toContain('getAlertsSidebarTabClass');
|
||||
expect(alertsPageSource).toContain('getAlertsMobileTabClass');
|
||||
expect(alertsPageSource).toContain('getAlertsTabTitle');
|
||||
|
||||
Reference in New Issue
Block a user