fix(alerts): open resource incidents at the row that asked for them

Fixes #1687. Also addresses the locale half of #1685.

The per-row Resource button in Alerts > History rendered its panel as a
page-level sibling in HistoryTab, between the filters card and the table.
Reproduced at 1280px: scrolled 3200px into the history, clicking the button
opened the panel 2595px above the top of the viewport. The only visible
effect was the row list shifting down as the panel was inserted above it, so
the button read as dead and the reporter could not reach the incident detail
or the absolute timestamps it carries.

Render the panel inline under the row instead, matching the neighbouring
Timeline button, in both the desktop table and the phone card list. Because
several alerts can share one resource, the panel state now carries the
originating rowKey and each row renders it only on a match, which also keeps
exactly one panel open at a time. Re-triggering the same row closes it.

The resource resolver moves from a HistoryTab prop onto the history state.
useAlertHistoryState already receives getResource; re-exposing it avoids
threading the lookup through the table section, group row, alert row and
mobile list now that the panel mounts in four places rather than one.

Row timestamps were built with a hardcoded 'en-US' locale while the rest of
this feature already formats through the viewer's locale, so a European
reader saw "05:19 AM" for 05:19. They also showed clock time only, with the
date available just in the day group header, which scrolls out of sight in a
long history. Both surfaces now format through the viewer's locale and carry
the full absolute date and time as a title, with the formatters owned by the
history state so table and mobile cannot drift.

Verified against mock data at 1280px and 390px: the panel opens in view
under the clicked row and card, a non-owning row renders nothing, timestamps
render 23:50 with a "Thursday, 6 August 2026 at 23:50:18" title across all
98 rendered rows, and there is no horizontal overflow at phone width.

Contract-Neutral: Contract deltas staged in alerts.md (inline resource-incident panel placement, rowKey targeting, locale-aware row timestamps) and frontend-primitives.md rule 35 plus its prose (resource resolver moves from the tab prop chain to the history state). Residual demand is an 'alerts frontend surface proof' from a fixed policy list that does not name the three test files this change actually exercises: __tests__/useAlertHistoryState.test.tsx, __tests__/AlertHistoryMobileList.test.tsx and __tests__/HistoryTab.test.tsx, all staged with new assertions covering the panel placement, the toggle, and the timestamp title. Editing an unrelated listed proof file would be fabrication.
This commit is contained in:
rcourtman 2026-08-06 18:07:15 +01:00
parent a959d50dce
commit a0113b9648
13 changed files with 205 additions and 61 deletions

View file

@ -382,6 +382,25 @@ links to the retired top-level routes, and the supporting
`buildResolvedResourceSurfaceLinks` helper was deleted from
`frontend-modern/src/routing/resourceLinks.ts` as part of the same pass.
That panel renders inline under the history row that opened it, in both the
desktop table (`AlertHistoryTableAlertRow.tsx`) and the phone card list
(`AlertHistoryMobileList.tsx`). It must not go back to being a page-level
sibling in `tabs/HistoryTab.tsx`: the alert history is a long scroller, so a
page-level panel opened thousands of pixels above a reader scrolled into the
list and the row-level button read as inert (#1687). Because several alerts
can share one resource, `resourceIncidentPanel` carries the originating
`rowKey` and each row renders the panel only when that key matches, which also
keeps exactly one panel open at a time. Re-triggering the same row closes it,
matching the neighbouring Timeline toggle.
Alert history row timestamps render clock time in the viewer's own locale and
must carry the absolute date and time as a title. The date otherwise lives
only in the day group header, which scrolls out of sight, and a hardcoded
`en-US` format misreported the time of day to everyone outside the US
(#1687, #1685). `useAlertHistoryState` owns both formatters
(`formatAlertRowTime`, `formatAlertRowTimestamp`) so the desktop table and the
mobile list cannot drift apart.
Alert browser surfaces no longer manage their own runtime-capabilities fetch or
`hasAIAlertsFeature` prop chain. `frontend-modern/src/pages/Alerts.tsx` and the
shared alert overview surfaces (`OverviewTab.tsx`, `HistoryTab.tsx`,

View file

@ -2394,11 +2394,17 @@ default` instead of fusing provider and badge text such as
fetch path.
35. Keep alert-history feature composition on the current owned state contract.
`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` must react to the
shared `alertData()` history state instead of reviving deleted aliases, and
it must pass unified-resource resolution through to
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx` so
the panel can render shared route chips without creating another page-local
resource lookup or provider-specific handoff layer.
shared `alertData()` history state instead of reviving deleted aliases.
Unified-resource resolution reaches
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`
through the history state rather than a prop chain: `useAlertHistoryState`
already receives `getResource` and re-exposes it, so the panel resolves a
display name wherever it is mounted. That replaced the previous
tab-passes-the-resolver rule, which only worked while the panel was a
page-level sibling of the tab; it now mounts inside the history row that
opened it (see the alerts contract, #1687) and the tab no longer renders it
at all. Neither route may create another page-local resource lookup or a
provider-specific handoff layer.
36. Keep the alert-thresholds containers surface on the canonical shared owner.
`alertOverridesModel.ts`, `useAlertOverridesState.ts`, and
`useAlertsConfigurationState.ts` must surface API-backed `app-container`
@ -5049,12 +5055,18 @@ now route through
Future alert-history control flow should extend the hook, pure history analytics
should extend the model, and section rendering should extend those owners
rather than rebuilding any of those concerns in the tab shell.
That same feature shell now owns the resource-resolution handoff into the
resource-incident panel. `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx`
must pass the unified-resource resolver through to
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx`, and the
tab shell itself should only react to the current `alertData()` contract rather
than reviving deleted history-state aliases such as `filteredAlerts()`. The
The resource-resolution handoff into the resource-incident panel now belongs to
the history state rather than the tab shell.
`frontend-modern/src/features/alerts/useAlertHistoryState.ts` re-exposes the
`getResource` resolver it is already given, and
`frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx` reads it
from there. The panel mounts inside the history row that opened it rather than
beside the tab's other cards, so a prop chain from
`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` would have to thread
through the table section, the group row, the alert row, and the mobile list to
reach it. The tab shell itself should still only react to the current
`alertData()` contract rather than reviving deleted history-state aliases such
as `filteredAlerts()`. The
panel may render compact route chips, but it must stay on shared route helpers
and feature-owned composition instead of growing provider-local routing logic
or another page-local resource lookup path.

View file

@ -1,40 +1,28 @@
{
"version": 1,
"base_sha": "4bb0786fbe797a00c8327cfc77a12f04c605bffd",
"verified_at": "2026-08-06T16:42:57Z",
"base_sha": "a959d50dcec61c90128b90516b209bfe2a21b0e7",
"verified_at": "2026-08-06T16:57:33Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/AppLayout.tsx",
"frontend-modern/src/components/shared/Subtabs.tsx",
"frontend-modern/src/components/shared/mobileNavBarModel.ts",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx"
"frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx",
"frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx",
"frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx",
"frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx",
"frontend-modern/src/features/alerts/tabs/HistoryTab.tsx",
"frontend-modern/src/features/alerts/useAlertHistoryState.ts",
"frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts"
],
"content_sha256": {
"frontend-modern/src/AppLayout.tsx": "2052252bcd26738b2d129619c003bb385152e25d2da37a4bbe3872d340766c31",
"frontend-modern/src/components/shared/Subtabs.tsx": "2bff774d03e632a71de233c8bcee138d76cdc88d2fcad8cd17bcdb956cf21ebe",
"frontend-modern/src/components/shared/mobileNavBarModel.ts": "2c1b0a5fc7c20e04ef8c8d8e42dc07a1c188fff2252d999b0686bbe0862f7e84",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "6c60a03cef2df987ee44c56a2512912285c4ee869f7d63e92e56836f657c06c2"
"frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx": "4855ab08d2455ef16a5e41ad722646f7e2a53a1a93e3da13feb405bc1f98898f",
"frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx": "ad2d9c41b11e414f4c54177ba4fb6e4f3bb0ce8c0831e0eaf95a09904ce95e51",
"frontend-modern/src/features/alerts/AlertHistoryTableAlertRow.tsx": "a400738ec0054729c25506edc23b4aff4b92fa6de80b4ea94c82f673ae786939",
"frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx": "1f24d6d8523b3976906d01f47167b0389334df629473bfc98e3d26cbee76b905",
"frontend-modern/src/features/alerts/tabs/HistoryTab.tsx": "bf14cbca7e0f79aeb503a8062c890474e39722503b15f13febf97cc889efd2a1",
"frontend-modern/src/features/alerts/useAlertHistoryState.ts": "89f038b0d5335774dda21312b9e04a9b02ae37c661e640a58c9edbb1b45c9b2c",
"frontend-modern/src/features/alerts/useAlertResourceIncidentsState.ts": "6a39b8d2cdec5f751992077bb2129ad3310d648a18a8e891209de12adc7d528b"
},
"routes": [
"/",
"/docker",
"/docker/images",
"/docker/networks",
"/actions"
],
"states": [
"logged-in workspace via login form (admin)",
"Docker overview with hosts and containers tables rendered from live :7655 backend",
"Docker platform subtab rail (Overview/Images/Storage/Networks/Swarm)",
"Actions page shared Subtabs (Open/History)",
"mobile viewport 375x812 with mobile nav bar tablist (12 tabs)",
"desktop nav with platform and utility tabs"
],
"interactions": [
"double-click desktop nav tab Docker: navigated, window.getSelection() empty",
"double-click platform subtab rail Images then Networks: navigated to /docker/networks, selection empty",
"computed user-select probed 'none' on all 10 desktop role=tab elements, platform rail anchors, Actions Open/History subtab buttons, and 12 mobile nav tabs",
"double-click Add filter button: no selection (native button control baseline)"
"/alerts/history"
],
"viewports": [
{
@ -42,8 +30,22 @@
"height": 800
},
{
"width": 375,
"height": 812
"width": 390,
"height": 844
}
],
"states": [
"Baseline reproduction of #1687 at 1280px: scrolled 3200px into the history and clicked a row's Resource button; the panel opened 2595px above the viewport top, so the only visible effect was the row list shifting down under the cursor",
"After the fix at 1280px: the same click renders the panel in the row immediately below the button, showing 'Resource incidents, pve1, 2 incidents' with the event filter chips and two incident cards carrying full absolute timestamps",
"Desktop row timestamps render in the browser locale (23:50 rather than the previously hardcoded en-US 11:50 PM) and every timestamp cell carries a full absolute date-time title, verified across all 98 rendered rows",
"Mobile card list at 390px: tapping Resource on a card scrolled 2600px down opens the panel inside that same card at y=550, fully in view, with no horizontal document overflow",
"A row that does not own the panel renders no incidents block, so only one panel is open at a time even when several alerts share one resource",
"Resource button re-clicked on the same row closes the panel, matching the neighbouring Timeline toggle"
],
"interactions": [
"Scrolled the app-scroll-shell container to 3200px and clicked a row-level Resource button before and after the change, measuring the panel's viewport offset each time",
"Read the rendered timestamp cells' text and title attributes directly from the DOM to confirm locale formatting and the absolute date-time tooltip",
"Resized to 390px, reloaded, scrolled to 2600px and tapped a card's Resource button, then measured the panel position relative to its own card",
"Checked document scrollWidth against the viewport at 390px for horizontal overflow"
]
}

View file

@ -47,6 +47,7 @@ export function AlertHistoryItemActions(props: AlertHistoryItemActionsProps) {
void props.state.openResourceIncidentPanel(
props.alert.resourceId as string,
props.alert.resourceName,
rowKey(),
);
}}
>

View file

@ -8,6 +8,7 @@ import {
} from '@/utils/alertIncidentPresentation';
import { AlertHistoryItemActions } from './AlertHistoryItemActions';
import { AlertResourceIncidentsPanel } from './AlertResourceIncidentsPanel';
import { getGroupSummaryLabel } from './AlertHistoryTableGroupRow';
import type { AlertHistoryState } from './useAlertHistoryState';
@ -69,11 +70,11 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) {
<dl class="mt-3 grid grid-cols-3 gap-2 text-[10px] text-muted">
<div>
<dt class="uppercase tracking-wide">Time</dt>
<dd class="mt-0.5 font-mono text-base-content">
{new Date(alert.startTime).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})}
<dd
class="mt-0.5 font-mono text-base-content"
title={props.state.formatAlertRowTimestamp(alert.startTime)}
>
{props.state.formatAlertRowTime(alert.startTime)}
</dd>
</div>
<div>
@ -134,6 +135,14 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) {
/>
</div>
</Show>
{/* Same reasoning as the desktop row: the panel belongs
under the card that asked for it, not at page level. */}
<Show when={props.state.resourceIncidentPanel()?.rowKey === rowKey()}>
<div class="mt-3 border-t border-border pt-3">
<AlertResourceIncidentsPanel state={props.state} />
</div>
</Show>
</article>
);
}}

View file

@ -10,6 +10,7 @@ import {
} from '@/utils/alertIncidentPresentation';
import { AlertHistoryItemActions } from './AlertHistoryItemActions';
import { AlertResourceIncidentsPanel } from './AlertResourceIncidentsPanel';
import type { AlertHistoryState } from './useAlertHistoryState';
type AlertHistoryAlert = ReturnType<AlertHistoryState['groupedAlerts']>[number]['alerts'][number];
@ -30,11 +31,9 @@ export function AlertHistoryTableAlertRow(props: AlertHistoryTableAlertRowProps)
>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} alert-history-timestamp-column font-mono whitespace-nowrap text-muted`}
title={props.state.formatAlertRowTimestamp(props.alert.startTime)}
>
{new Date(props.alert.startTime).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})}
{props.state.formatAlertRowTime(props.alert.startTime)}
</TableCell>
<TableCell
@ -121,6 +120,17 @@ export function AlertHistoryTableAlertRow(props: AlertHistoryTableAlertRowProps)
</TableCell>
</TableRow>
</Show>
{/* The incidents panel opens under the row that asked for it. Rendering
it at page level put the panel thousands of pixels above a reader
scrolled into the history, so the row button read as inert (#1687). */}
<Show when={props.state.resourceIncidentPanel()?.rowKey === rowKey()}>
<TableRow class="border-b border-border bg-surface-alt">
<TableCell colspan={9} class="p-3">
<AlertResourceIncidentsPanel state={props.state} />
</TableCell>
</TableRow>
</Show>
</>
);
}

View file

@ -33,7 +33,7 @@ import type { AlertHistoryState } from './useAlertHistoryState';
interface AlertResourceIncidentsPanelProps {
state: AlertHistoryState;
getResource: (resourceId: string) => Resource | undefined;
getResource?: (resourceId: string) => Resource | undefined;
}
export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelProps) {
@ -43,7 +43,8 @@ export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelPr
const resourceId = selection().resourceId;
const incidents = () => props.state.resourceIncidents()[resourceId] || [];
const isLoading = () => props.state.resourceIncidentLoading()[resourceId];
const resource = () => props.getResource(resourceId);
const lookupResource = () => props.getResource ?? props.state.getResource;
const resource = () => lookupResource()?.(resourceId);
const resourceDisplayName = () => {
const current = resource();
if (current) {

View file

@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest';
import { AlertHistoryMobileList } from '../AlertHistoryMobileList';
import type { AlertHistoryState } from '../useAlertHistoryState';
function createState() {
function createState(openRowKey: string | null = null) {
const toggleIncidentTimeline = vi.fn();
const openResourceIncidentPanel = vi.fn();
const alert = {
@ -46,6 +46,19 @@ function createState() {
loadIncidentTimeline: vi.fn(),
toggleIncidentTimeline,
openResourceIncidentPanel,
resourceIncidentPanel: () =>
openRowKey
? { resourceId: 'node-1', resourceName: 'pve-production-01', rowKey: openRowKey }
: null,
resourceIncidents: () => ({ 'node-1': [] }),
resourceIncidentLoading: () => ({}),
resourceIncidentEventFilters: () => new Set<string>(),
setResourceIncidentEventFilters: vi.fn(),
refreshResourceIncidentPanel: vi.fn(),
setResourceIncidentPanel: vi.fn(),
getResource: () => undefined,
formatAlertRowTime: () => '14:30',
formatAlertRowTimestamp: () => 'Tuesday, 4 August 2026 at 14:30:00',
} as unknown as AlertHistoryState;
return { openResourceIncidentPanel, state, toggleIncidentTimeline };
@ -63,6 +76,12 @@ describe('AlertHistoryMobileList', () => {
expect(screen.getByRole('heading', { name: 'pve-production-01' })).toBeInTheDocument();
expect(screen.getByText('Backup failed after the target became unavailable.')).toBeVisible();
expect(screen.getByText('14m')).toBeVisible();
// #1687: the row shows clock time, but the absolute date has to be
// reachable without scrolling back to the day header.
expect(screen.getByText('14:30')).toHaveAttribute(
'title',
'Tuesday, 4 August 2026 at 14:30:00',
);
expect(screen.getByText('Production cluster')).toBeVisible();
expect(screen.getByText('warning')).toBeVisible();
expect(screen.getByText('resolved')).toBeVisible();
@ -78,6 +97,30 @@ describe('AlertHistoryMobileList', () => {
);
await fireEvent.click(screen.getByRole('button', { name: 'Resource' }));
expect(openResourceIncidentPanel).toHaveBeenCalledWith('node-1', 'pve-production-01');
expect(openResourceIncidentPanel).toHaveBeenCalledWith(
'node-1',
'pve-production-01',
'alert-1-row',
);
});
// #1687: the panel used to render at page level, so on a phone it opened far
// above whatever card the reader had tapped and the button looked inert.
it('renders the resource incidents panel inside the card that opened it', () => {
const { state } = createState('alert-1-row');
const { container } = render(() => <AlertHistoryMobileList state={state} />);
const card = container.querySelector('article');
expect(card).not.toBeNull();
expect(card?.textContent).toContain('No incidents recorded for this resource yet.');
});
it('leaves the card alone while a different row owns the panel', () => {
const { state } = createState('some-other-row');
const { container } = render(() => <AlertHistoryMobileList state={state} />);
expect(container.textContent).not.toContain('No incidents recorded for this resource yet.');
});
});

View file

@ -53,8 +53,12 @@ describe('HistoryTab', () => {
expect(screen.getByText('frequency-card')).toBeInTheDocument();
expect(screen.getByText('filters-card')).toBeInTheDocument();
expect(screen.getByText('resource-incidents-panel')).toBeInTheDocument();
expect(screen.getByText('history-table')).toBeInTheDocument();
expect(screen.getByText('administration-card')).toBeInTheDocument();
// #1687: the resource incidents panel is deliberately NOT a page-level
// sibling any more. It renders inside the row that opened it, so that a
// reader scrolled deep into the history sees it appear where they clicked
// instead of thousands of pixels above the viewport.
expect(screen.queryByText('resource-incidents-panel')).not.toBeInTheDocument();
});
});

View file

@ -130,15 +130,24 @@ describe('useAlertHistoryState', () => {
expect(result.alertData()).toHaveLength(1);
expect(eventBus.on).toHaveBeenCalledWith('org_switched', expect.any(Function));
await result.openResourceIncidentPanel('resource-1', 'db-01');
await result.openResourceIncidentPanel('resource-1', 'db-01', 'row-1');
expect(AlertsAPI.getIncidentsForResource).toHaveBeenCalledWith('resource-1', 10);
expect(result.resourceIncidentPanel()).toEqual({
resourceId: 'resource-1',
resourceName: 'db-01',
rowKey: 'row-1',
});
expect(result.resourceIncidents()['resource-1']).toHaveLength(1);
// Re-opening from the same row closes the panel, the way the neighbouring
// Timeline button toggles. A different row re-targets it instead.
await result.openResourceIncidentPanel('resource-1', 'db-01', 'row-1');
expect(result.resourceIncidentPanel()).toBeNull();
await result.openResourceIncidentPanel('resource-1', 'db-01', 'row-2');
expect(result.resourceIncidentPanel()?.rowKey).toBe('row-2');
result.setTimeFilter('24h');
await waitFor(() => expect(AlertsAPI.getHistory).toHaveBeenCalledTimes(2));

View file

@ -9,7 +9,6 @@ import { AlertHistoryAdministrationCard } from '../AlertHistoryAdministrationCar
import { AlertHistoryFiltersCard } from '../AlertHistoryFiltersCard';
import { AlertHistoryFrequencyCard } from '../AlertHistoryFrequencyCard';
import { AlertHistoryTableSection } from '../AlertHistoryTableSection';
import { AlertResourceIncidentsPanel } from '../AlertResourceIncidentsPanel';
import { useAlertHistoryState } from '../useAlertHistoryState';
export interface HistoryTabProps {
@ -70,7 +69,6 @@ export function HistoryTab(props: HistoryTabProps) {
<div class="space-y-4">
<AlertHistoryFrequencyCard state={historyState} />
<AlertHistoryFiltersCard state={historyState} isMobile={isMobile()} />
<AlertResourceIncidentsPanel state={historyState} getResource={props.getResource} />
<AlertHistoryTableSection state={historyState} />
<AlertHistoryAdministrationCard state={historyState} />
</div>

View file

@ -145,6 +145,21 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
(typeof navigator !== 'undefined' ? navigator.language : undefined) ||
'en-US';
// Row timestamps render clock time only; the date lives in the day group
// header, which scrolls out of sight in a long history. Hovering a row gives
// the absolute date and time so a reader never has to scroll back up to work
// out which day they are looking at (#1687).
const formatAlertRowTime = (startTime: string) =>
new Date(startTime).toLocaleTimeString(userLocale, {
hour: '2-digit',
minute: '2-digit',
});
const formatAlertRowTimestamp = (startTime: string) =>
new Date(startTime).toLocaleString(userLocale, {
dateStyle: 'full',
timeStyle: 'medium',
});
let fetchRequestId = 0;
const fetchHistory = async (range: AlertHistoryRange) => {
const requestId = ++fetchRequestId;
@ -332,6 +347,11 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) {
toggleIncidentTimeline,
setIncidentNoteDraft,
saveIncidentNote,
// Exposed so the inline resource panel can resolve a display name without
// threading the lookup down through the table and mobile list.
getResource: props.getResource,
formatAlertRowTime,
formatAlertRowTimestamp,
openResourceIncidentPanel: resourceIncidentsState.openResourceIncidentPanel,
refreshResourceIncidentPanel: resourceIncidentsState.refreshResourceIncidentPanel,
toggleResourceIncidentDetails: resourceIncidentsState.toggleResourceIncidentDetails,

View file

@ -12,6 +12,10 @@ export function useAlertResourceIncidentsState() {
const [resourceIncidentPanel, setResourceIncidentPanel] = createSignal<{
resourceId: string;
resourceName: string;
// The row that opened the panel. The panel renders inline underneath that
// row, and several alerts in the history can share one resource, so keying
// on resourceId alone would open a copy under every one of them.
rowKey: string;
} | null>(null);
const [resourceIncidents, setResourceIncidents] = createSignal<Record<string, Incident[]>>({});
const [resourceIncidentLoading, setResourceIncidentLoading] = createSignal<
@ -39,10 +43,22 @@ export function useAlertResourceIncidentsState() {
}
};
const openResourceIncidentPanel = async (resourceId: string, resourceName: string) => {
const openResourceIncidentPanel = async (
resourceId: string,
resourceName: string,
rowKey: string,
) => {
if (!resourceId) return;
setResourceIncidentPanel({ resourceId, resourceName });
// Clicking the same row's button again closes the panel, matching how the
// neighbouring Timeline button toggles its own expansion.
const current = resourceIncidentPanel();
if (current && current.rowKey === rowKey) {
setResourceIncidentPanel(null);
return;
}
setResourceIncidentPanel({ resourceId, resourceName, rowKey });
setExpandedResourceIncidentIds(new Set<string>());
if (!(resourceId in resourceIncidents())) {
await loadResourceIncidents(resourceId);