mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-24 08:23:31 +00:00
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:
parent
a959d50dce
commit
a0113b9648
13 changed files with 205 additions and 61 deletions
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export function AlertHistoryItemActions(props: AlertHistoryItemActionsProps) {
|
|||
void props.state.openResourceIncidentPanel(
|
||||
props.alert.resourceId as string,
|
||||
props.alert.resourceName,
|
||||
rowKey(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue