mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
fix alert history filter reset
This commit is contained in:
@@ -1223,6 +1223,14 @@ Future alert-history control-flow work should extend the feature hook, new
|
||||
grouping or trend semantics should extend the history model, and render-heavy
|
||||
history surfaces should extend those section owners instead of putting fetch,
|
||||
resource-incident state, or table rendering back into the shell.
|
||||
Alert History toolbar state is one route-owned unit. Search, period, and
|
||||
severity are read and written by `useAlertHistoryState.ts`; its active-filter
|
||||
count includes search, and `AlertHistoryFiltersCard.tsx` delegates contextual
|
||||
Clear filters to one composite hook mutation instead of letting `FilterBar`
|
||||
issue sequential route writes. That reset removes all three query parameters
|
||||
in one navigation and clears any transient chart-bucket selection, so a
|
||||
search-only result set remains visibly resettable and an older URL write
|
||||
cannot resurrect another filter.
|
||||
That same history surface now also owns the canonical resource-incident
|
||||
handoff. `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`
|
||||
must treat the selected incident resource as a unified-resource consumer,
|
||||
|
||||
@@ -3926,6 +3926,12 @@ Its feature owner may clear incompatible hidden facets when the workspace
|
||||
changes and may preserve that workspace during a composite reset, but the
|
||||
shared `FilterBar` remains the sole owner of Saved, Clear, filter-menu, and
|
||||
popover chrome across desktop and narrow layouts.
|
||||
Alert History follows the same rule for its route-owned search, period, and
|
||||
severity state. Its feature hook owns one composite reset and exposes a
|
||||
search-aware active-state accessor to `FilterBar`; the shared shell owns where
|
||||
the contextual Clear filters action appears, while the alerts feature must not
|
||||
fall back to sequential per-control URL writes or hide that action for a
|
||||
search-only result set.
|
||||
Because that popover combines view application, default selection, removal,
|
||||
and an inline naming form, it is a labelled non-modal dialog rather than an
|
||||
ARIA menu. Its trigger exposes the dialog relationship, Escape returns focus
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "59eb6dd68f3f1f7ac58639c2b82099eb076c14bc",
|
||||
"verified_at": "2026-08-03T10:53:05Z",
|
||||
"base_sha": "2402858e3c80b29d898e551c82969566c6a649b8",
|
||||
"verified_at": "2026-08-03T11:15:05Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx",
|
||||
"frontend-modern/src/routing/resourceLinks.ts"
|
||||
"frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx",
|
||||
"frontend-modern/src/features/alerts/useAlertHistoryState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx": "6c838d09098ea243c8368753ef0f00c80cf11b59c7cbcd5f62750bba75fbafb2",
|
||||
"frontend-modern/src/routing/resourceLinks.ts": "c57c920355766f401923340b746614e02b6e6a40fbd871bfd3be47611d952e93"
|
||||
"frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx": "768d706dad29c92c8a205059e48f16bd4fc8f652ad093501759f7f50c72d849c",
|
||||
"frontend-modern/src/features/alerts/useAlertHistoryState.ts": "e190111f1b89a806b7bd07cf2b10a5ba4f8e5d204c0cebea871964252c5ccebd"
|
||||
},
|
||||
"routes": ["/proxmox/backups"],
|
||||
"routes": ["/alerts/history"],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
@@ -23,21 +23,20 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Desktop Proxmox Backups default Coverage workspace with Posture filtering",
|
||||
"Desktop By date workspace with URL-backed search and PBS snapshots source filtering",
|
||||
"Desktop Saved views empty, named-view, restored-view, and removed-view states",
|
||||
"Phone-width filter shell collapsed and expanded in Coverage and By date workspaces",
|
||||
"Desktop default, search-only, combined period/severity/search, and chart-bucket states",
|
||||
"Desktop Saved views empty, named-view, restored-view, removed-view, and deep-linked states",
|
||||
"Phone-width filter shell collapsed and expanded with search-only and combined filter state",
|
||||
"Phone-width Saved views dialog bounded within the viewport with active filter state"
|
||||
],
|
||||
"interactions": [
|
||||
"Switched from Coverage to By date and confirmed the incompatible posture parameter was removed in the same navigation",
|
||||
"Entered backup-vault search text, selected PBS snapshots, and confirmed the route became ?view=date&q=backup-vault&source=pbs",
|
||||
"Saved the filtered slice as Backup URL audit, cleared it, reapplied it, and confirmed the exact query string and visible controls were restored",
|
||||
"Reloaded the explicit deep link and confirmed search and source state remained selected",
|
||||
"Cleared the combined filter state and confirmed one navigation retained ?view=date while removing every narrowing parameter",
|
||||
"Removed the temporary saved view and confirmed the Saved count returned to zero",
|
||||
"Expanded the phone-width filter shell, selected Unknown posture, switched to By date, and confirmed hidden posture state did not survive",
|
||||
"Opened and dismissed the phone-width Saved dialog with Escape and confirmed it stayed viewport-bounded without document overflow",
|
||||
"Read both browser consoles after the final responsive pass and confirmed no warnings or errors"
|
||||
"Entered search text alone and confirmed Clear filters appeared, then cleared the URL and visible search state in one action",
|
||||
"Selected Last 30d and Critical with search text, confirmed all three query parameters, and cleared them atomically back to defaults",
|
||||
"Saved the combined slice as Alert reset audit, cleared it, reapplied it, and confirmed the exact query string and visible controls were restored",
|
||||
"Reloaded the explicit deep link and confirmed search, period, and severity remained selected",
|
||||
"Removed the temporary saved view and confirmed it no longer appeared",
|
||||
"Selected an alert-frequency bucket, added a toolbar filter, and confirmed Clear filters removed both the route-owned filters and transient chart selection",
|
||||
"Expanded the phone-width filter shell and confirmed Saved and Clear filters remained aligned without overflow",
|
||||
"Opened and dismissed the phone-width Saved dialog with Escape and confirmed it stayed viewport-bounded",
|
||||
"Read the browser console after the final responsive pass and confirmed no warnings or errors"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ export function AlertHistoryFiltersCard(props: AlertHistoryFiltersCardProps) {
|
||||
clearOnEscape: true,
|
||||
}}
|
||||
filters={buildFilters()}
|
||||
onClearAll={props.state.clearFilters}
|
||||
showClearAll={() => props.state.activeFilterCount() > 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,4 +146,34 @@ describe('useAlertHistoryState', () => {
|
||||
expect(AlertsAPI.clearHistory).toHaveBeenCalledTimes(1);
|
||||
expect(result.alertHistory()).toEqual([]);
|
||||
});
|
||||
|
||||
it('clears search, period, and severity in one route write', async () => {
|
||||
const [activeAlerts] = createSignal({});
|
||||
vi.mocked(AlertsAPI.getHistory).mockResolvedValue([] as any);
|
||||
setMockLocation('?q=backup+failed&period=30d&severity=critical');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlertHistoryState({
|
||||
activeAlerts,
|
||||
getResource: () => undefined,
|
||||
allResources: () => [],
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(AlertsAPI.getHistory).toHaveBeenCalledTimes(1));
|
||||
expect(result.activeFilterCount()).toBe(3);
|
||||
expect(result.searchTerm()).toBe('backup failed');
|
||||
expect(result.timeFilter()).toBe('30d');
|
||||
expect(result.severityFilter()).toBe('critical');
|
||||
|
||||
navigateSpy.mockClear();
|
||||
result.clearFilters();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/alerts/history', { replace: true });
|
||||
expect(result.activeFilterCount()).toBe(0);
|
||||
expect(result.searchTerm()).toBe('');
|
||||
expect(result.timeFilter()).toBe('7d');
|
||||
expect(result.severityFilter()).toBe('all');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,11 @@ export interface UseAlertHistoryStateProps {
|
||||
|
||||
const DEFAULT_TIME_FILTER: AlertHistoryRange = '7d';
|
||||
const DEFAULT_SEVERITY_FILTER: AlertSeverityFilter = 'all';
|
||||
const ALERT_HISTORY_QUERY_PARAMS = {
|
||||
query: 'q',
|
||||
period: 'period',
|
||||
severity: 'severity',
|
||||
} as const;
|
||||
|
||||
const parsePeriod = (raw: string | null | undefined): AlertHistoryRange =>
|
||||
raw === '24h' || raw === '7d' || raw === '30d' || raw === 'all' ? raw : DEFAULT_TIME_FILTER;
|
||||
@@ -54,10 +59,11 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const timeFilter: Accessor<AlertHistoryRange> = () =>
|
||||
parsePeriod(new URLSearchParams(location.search).get('period'));
|
||||
parsePeriod(new URLSearchParams(location.search).get(ALERT_HISTORY_QUERY_PARAMS.period));
|
||||
const severityFilter: Accessor<AlertSeverityFilter> = () =>
|
||||
parseSeverity(new URLSearchParams(location.search).get('severity'));
|
||||
const searchTerm: Accessor<string> = () => new URLSearchParams(location.search).get('q') ?? '';
|
||||
parseSeverity(new URLSearchParams(location.search).get(ALERT_HISTORY_QUERY_PARAMS.severity));
|
||||
const searchTerm: Accessor<string> = () =>
|
||||
new URLSearchParams(location.search).get(ALERT_HISTORY_QUERY_PARAMS.query) ?? '';
|
||||
|
||||
const updateSearchParam = (mutate: (params: URLSearchParams) => void): void => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
@@ -69,9 +75,9 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const setTimeFilter = (value: AlertHistoryRange): void => {
|
||||
updateSearchParam((params) => {
|
||||
if (value === DEFAULT_TIME_FILTER) {
|
||||
params.delete('period');
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.period);
|
||||
} else {
|
||||
params.set('period', value);
|
||||
params.set(ALERT_HISTORY_QUERY_PARAMS.period, value);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -79,9 +85,9 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const setSeverityFilter = (value: AlertSeverityFilter): void => {
|
||||
updateSearchParam((params) => {
|
||||
if (value === DEFAULT_SEVERITY_FILTER) {
|
||||
params.delete('severity');
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.severity);
|
||||
} else {
|
||||
params.set('severity', value);
|
||||
params.set(ALERT_HISTORY_QUERY_PARAMS.severity, value);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -89,9 +95,9 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const setSearchTerm = (value: string): void => {
|
||||
updateSearchParam((params) => {
|
||||
if (value === '') {
|
||||
params.delete('q');
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.query);
|
||||
} else {
|
||||
params.set('q', value);
|
||||
params.set(ALERT_HISTORY_QUERY_PARAMS.query, value);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -119,11 +125,21 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
|
||||
const activeFilterCount = createMemo(() => {
|
||||
let count = 0;
|
||||
if (searchTerm().trim()) count++;
|
||||
if (timeFilter() !== '7d') count++;
|
||||
if (severityFilter() !== 'all') count++;
|
||||
return count;
|
||||
});
|
||||
|
||||
const clearFilters = (): void => {
|
||||
updateSearchParam((params) => {
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.query);
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.period);
|
||||
params.delete(ALERT_HISTORY_QUERY_PARAMS.severity);
|
||||
});
|
||||
setSelectedBarIndex(null);
|
||||
};
|
||||
|
||||
const userLocale =
|
||||
Intl.DateTimeFormat().resolvedOptions().locale ||
|
||||
(typeof navigator !== 'undefined' ? navigator.language : undefined) ||
|
||||
@@ -176,20 +192,20 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let mutated = false;
|
||||
|
||||
if (!params.has('period')) {
|
||||
if (!params.has(ALERT_HISTORY_QUERY_PARAMS.period)) {
|
||||
const legacy = window.localStorage.getItem('alertHistoryTimeFilter');
|
||||
const parsed = parsePeriod(legacy);
|
||||
if (parsed !== DEFAULT_TIME_FILTER && legacy === parsed) {
|
||||
params.set('period', parsed);
|
||||
params.set(ALERT_HISTORY_QUERY_PARAMS.period, parsed);
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.has('severity')) {
|
||||
if (!params.has(ALERT_HISTORY_QUERY_PARAMS.severity)) {
|
||||
const legacy = window.localStorage.getItem('alertHistorySeverityFilter');
|
||||
const parsed = parseSeverity(legacy);
|
||||
if (parsed !== DEFAULT_SEVERITY_FILTER && legacy === parsed) {
|
||||
params.set('severity', parsed);
|
||||
params.set(ALERT_HISTORY_QUERY_PARAMS.severity, parsed);
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
@@ -303,6 +319,7 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
|
||||
resourceIncidentEventFilters: resourceIncidentsState.resourceIncidentEventFilters,
|
||||
setResourceIncidentEventFilters: resourceIncidentsState.setResourceIncidentEventFilters,
|
||||
activeFilterCount,
|
||||
clearFilters,
|
||||
incidentTimelines,
|
||||
incidentLoading,
|
||||
incidentErrors,
|
||||
|
||||
@@ -466,6 +466,10 @@ describe('tab path helpers', () => {
|
||||
expect(alertHistoryFiltersCardSource).toContain('export function AlertHistoryFiltersCard');
|
||||
expect(alertHistoryFiltersCardSource).toContain('getAlertHistorySearchPlaceholder');
|
||||
expect(alertHistoryFiltersCardSource).toContain('filterChipStatusDot');
|
||||
expect(alertHistoryFiltersCardSource).toContain('onClearAll={props.state.clearFilters}');
|
||||
expect(alertHistoryFiltersCardSource).toContain(
|
||||
'showClearAll={() => props.state.activeFilterCount() > 0}',
|
||||
);
|
||||
expect(alertHistoryFiltersCardSource).not.toContain('severityDot');
|
||||
expect(alertHistoryFiltersCardSource).not.toContain('h-2 w-2 rounded-full ${className}');
|
||||
expect(alertResourceIncidentsPanelSource).toContain(
|
||||
@@ -526,6 +530,7 @@ describe('tab path helpers', () => {
|
||||
);
|
||||
expect(alertHistoryStateSource).toContain('export function useAlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('export type AlertHistoryState');
|
||||
expect(alertHistoryStateSource).toContain('const clearFilters = (): void =>');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.getHistory');
|
||||
expect(alertHistoryStateSource).toContain('AlertsAPI.clearHistory');
|
||||
expect(alertHistoryStateSource).toContain('useAlertResourceIncidentsState');
|
||||
|
||||
Reference in New Issue
Block a user