mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Restore mobile alert history validation
This commit is contained in:
@@ -31,7 +31,7 @@ On your TrueNAS system:
|
||||
| Apps | TrueNAS Overview | Native app state, image/version, ports, volumes, networks, and runtime container details |
|
||||
| ZFS Pools | Storage | Total/used/free capacity, pool status (ONLINE/DEGRADED/FAULTED) |
|
||||
| ZFS Datasets | Storage | Used/available space, mount status, read-only flag |
|
||||
| Physical Disks | Storage | Model, serial, size, transport type, rotational flag |
|
||||
| Physical Disks | Storage | Model, serial, size, transport type, rotational flag, temperature, and native SMART failure evidence |
|
||||
| ZFS Snapshots | Recovery | Dataset, creation time, size, referenced data |
|
||||
| Replication Tasks | Recovery | Source/target datasets, direction, last run status |
|
||||
| TrueNAS Alerts | Alerts | Native TrueNAS alert messages and severity levels |
|
||||
@@ -47,6 +47,12 @@ TrueNAS resources are mapped into the unified resource model:
|
||||
- **ZFS snapshots and replication** → appear on the **Recovery** page as recovery points.
|
||||
- **TrueNAS alerts** → surfaced on the **Alerts** page alongside Proxmox and other platform alerts.
|
||||
|
||||
TrueNAS drive-health alerts that identify a specific disk remain disk-health
|
||||
evidence after they are dismissed in TrueNAS. Dismissal acknowledges the
|
||||
notification; Pulse continues to show the affected disk risk while TrueNAS
|
||||
continues to report the underlying SMART condition. Other dismissed TrueNAS
|
||||
alerts remain suppressed.
|
||||
|
||||
Resources from TrueNAS can be filtered using the **source** filter on any page.
|
||||
|
||||
## Multiple TrueNAS Systems
|
||||
|
||||
@@ -14,6 +14,7 @@ const shippedDocs = [
|
||||
{ source: path.join(repoRoot, 'docs', 'PRIVACY.md'), target: 'PRIVACY.md' },
|
||||
{ source: path.join(repoRoot, 'docs', 'CONFIGURATION.md'), target: 'CONFIGURATION.md' },
|
||||
{ source: path.join(repoRoot, 'docs', 'PROXY_AUTH.md'), target: 'PROXY_AUTH.md' },
|
||||
{ source: path.join(repoRoot, 'docs', 'TRUENAS.md'), target: 'TRUENAS.md' },
|
||||
{ source: path.join(repoRoot, 'SECURITY.md'), target: 'SECURITY.md' },
|
||||
{ source: path.join(repoRoot, 'TERMS.md'), target: 'TERMS.md' },
|
||||
];
|
||||
|
||||
@@ -36,6 +36,7 @@ interface AlertResourceIncidentsPanelProps {
|
||||
getResource?: (resourceId: string) => Resource | undefined;
|
||||
onClose?: () => void;
|
||||
showCloseAction?: boolean;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelProps) {
|
||||
@@ -59,9 +60,11 @@ export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelPr
|
||||
<Card padding="md">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-base-content">
|
||||
{getAlertResourceIncidentPanelTitle()}
|
||||
</h3>
|
||||
<Show when={props.showTitle ?? true}>
|
||||
<h3 class="text-sm font-semibold text-base-content">
|
||||
{getAlertResourceIncidentPanelTitle()}
|
||||
</h3>
|
||||
</Show>
|
||||
<p class="text-xs text-muted">
|
||||
{resourceDisplayName()}
|
||||
<Show when={incidents().length > 0}>
|
||||
|
||||
@@ -4,6 +4,10 @@ import { Match, Switch } from 'solid-js';
|
||||
import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel';
|
||||
import { ActionIconButton } from '@/components/shared/Button';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import {
|
||||
getAlertIncidentTimelineDialogTitle,
|
||||
getAlertResourceIncidentPanelTitle,
|
||||
} from '@/utils/alertIncidentPresentation';
|
||||
|
||||
import { AlertResourceIncidentsPanel } from './AlertResourceIncidentsPanel';
|
||||
import type { AlertHistoryState } from './useAlertHistoryState';
|
||||
@@ -26,7 +30,9 @@ export function MobileAlertHistoryInvestigationDialog(
|
||||
props: MobileAlertHistoryInvestigationDialogProps,
|
||||
) {
|
||||
const dialogTitle = () =>
|
||||
props.investigation.kind === 'timeline' ? 'Incident timeline' : 'Resource incidents';
|
||||
props.investigation.kind === 'timeline'
|
||||
? getAlertIncidentTimelineDialogTitle()
|
||||
: getAlertResourceIncidentPanelTitle();
|
||||
const dialogLabel = () => `${dialogTitle()} for ${props.investigation.alert.resourceName}`;
|
||||
const rowKey = () => props.investigation.rowKey;
|
||||
const alert = () => props.investigation.alert;
|
||||
@@ -89,6 +95,7 @@ export function MobileAlertHistoryInvestigationDialog(
|
||||
state={props.state}
|
||||
onClose={props.onClose}
|
||||
showCloseAction={false}
|
||||
showTitle={false}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -130,6 +130,7 @@ describe('AlertHistoryMobileList', () => {
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Resource incidents for pve-production-01' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('heading', { name: 'Resource incidents' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps resource investigation scrolling outside the virtualized history row', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ALERT_INCIDENT_TIMELINE_DIALOG_TITLE,
|
||||
ALERT_RESOURCE_INCIDENT_EMPTY_STATE,
|
||||
ALERT_RESOURCE_INCIDENT_LOADING_STATE,
|
||||
ALERT_RESOURCE_INCIDENT_NOTE_PLACEHOLDER,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
ALERT_RESOURCE_INCIDENT_SAVING_NOTE_LABEL,
|
||||
getAlertHistoryStatusPresentation,
|
||||
getAlertIncidentLevelBadgeClass,
|
||||
getAlertIncidentTimelineDialogTitle,
|
||||
getAlertResourceIncidentAcknowledgedByLabel,
|
||||
getAlertResourceIncidentCountLabel,
|
||||
getAlertResourceIncidentEmptyState,
|
||||
@@ -102,6 +104,7 @@ describe('alertIncidentPresentation', () => {
|
||||
});
|
||||
|
||||
it('returns canonical resource incident loading and empty-state copy', () => {
|
||||
expect(ALERT_INCIDENT_TIMELINE_DIALOG_TITLE).toBe('Incident timeline');
|
||||
expect(ALERT_RESOURCE_INCIDENT_PANEL_TITLE).toBe('Resource incidents');
|
||||
expect(ALERT_RESOURCE_INCIDENT_LOADING_STATE).toBe('Loading incidents...');
|
||||
expect(ALERT_RESOURCE_INCIDENT_EMPTY_STATE).toBe(
|
||||
@@ -116,6 +119,7 @@ describe('alertIncidentPresentation', () => {
|
||||
expect(ALERT_RESOURCE_INCIDENT_SAVE_NOTE_LABEL).toBe('Save Note');
|
||||
expect(ALERT_RESOURCE_INCIDENT_SAVING_NOTE_LABEL).toBe('Saving...');
|
||||
expect(getAlertResourceIncidentPanelTitle()).toBe('Resource incidents');
|
||||
expect(getAlertIncidentTimelineDialogTitle()).toBe('Incident timeline');
|
||||
expect(getAlertResourceIncidentLoadingState()).toEqual({
|
||||
text: 'Loading incidents...',
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AlertHistoryStatusPresentation {
|
||||
}
|
||||
|
||||
export const ALERT_RESOURCE_INCIDENT_PANEL_TITLE = 'Resource incidents';
|
||||
export const ALERT_INCIDENT_TIMELINE_DIALOG_TITLE = 'Incident timeline';
|
||||
export const ALERT_RESOURCE_INCIDENT_LOADING_STATE = 'Loading incidents...';
|
||||
export const ALERT_RESOURCE_INCIDENT_EMPTY_STATE = 'No incidents recorded for this resource yet.';
|
||||
export const ALERT_RESOURCE_INCIDENT_REFRESH_LABEL = 'Refresh';
|
||||
@@ -121,6 +122,10 @@ export function getAlertResourceIncidentPanelTitle() {
|
||||
return ALERT_RESOURCE_INCIDENT_PANEL_TITLE;
|
||||
}
|
||||
|
||||
export function getAlertIncidentTimelineDialogTitle() {
|
||||
return ALERT_INCIDENT_TIMELINE_DIALOG_TITLE;
|
||||
}
|
||||
|
||||
export function getAlertResourceIncidentCountLabel(count: number) {
|
||||
return `${count} incident${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user