mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Normalize frontend product labels
This commit is contained in:
parent
ff6016daab
commit
47d898cd50
30 changed files with 123 additions and 76 deletions
|
|
@ -209,7 +209,11 @@ canonical owners for alert enablement copy, history administration wording,
|
|||
bulk-edit labels, schedule/configuration text, email-destination field labels,
|
||||
frequency chips, grouping card styling, history source and resource badges,
|
||||
severity badges, tab labels, thresholds empty states, and thresholds section
|
||||
status labels. Future alert configuration or history presentation work should
|
||||
status labels. Overview stat-card labels must also route through the alert
|
||||
overview presentation helper, and user-facing configuration or thresholds copy
|
||||
must use workload, VM, and container vocabulary instead of exposing internal
|
||||
guest override/filter names unless the UI is naming a backend field directly.
|
||||
Future alert configuration or history presentation work should
|
||||
extend those helpers instead of rebuilding alert-specific semantics in pages,
|
||||
dashboard surfaces, feature hooks, or thresholds shells.
|
||||
|
||||
|
|
|
|||
|
|
@ -264,7 +264,16 @@ work extends shared components instead of creating new local variants.
|
|||
inherits that same boundary: shared data-grid shells must route scrollbar
|
||||
hiding and table-width sizing through shared classes plus HTML attributes,
|
||||
not inline overflow or min-width styles.
|
||||
3. Add feature-specific presentation only when no shared primitive should own it
|
||||
3. Add feature-specific presentation only when no shared primitive should own it.
|
||||
Feature surfaces under `frontend-modern/src/features/` that display
|
||||
product labels must consume the owning subsystem's presentation utilities
|
||||
rather than hard-coding divergent page-local copy. Shared primitives and
|
||||
feature shells may compose those labels, but they must not become a second
|
||||
source of truth for alert, storage, recovery, infrastructure, workload, or
|
||||
adjacent product vocabulary. Table-mode segmented controls that expose a
|
||||
grouping accessible label must use the shared `Group by` casing, while
|
||||
visible options describe the mode itself (`Grouped`, `List`, or the
|
||||
owning surface's equivalent) instead of resource-specific concepts.
|
||||
4. Add guardrail tests when a new shared pattern is introduced.
|
||||
Shared monitored-system warning primitives must prove their admission-freeze
|
||||
posture through the canonical `frontend-modern/src/utils/monitoredSystemPresentation.ts`
|
||||
|
|
|
|||
|
|
@ -779,6 +779,10 @@ stays in `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`, while
|
|||
toolbar defaults, active-filter counting, and reset semantics live in
|
||||
`frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and
|
||||
`frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`.
|
||||
Dashboard table-mode controls must also keep their accessible group name aligned
|
||||
with the shared table presentation contract by using `Group by` for the
|
||||
grouped/list selector instead of reintroducing local `Group By` casing or
|
||||
platform-specific cluster wording.
|
||||
The dashboard-owned filter-config assembly now lives in
|
||||
`frontend-modern/src/components/Dashboard/useDashboardState.ts`, so future
|
||||
filter runtime changes must extend through those owners instead of
|
||||
|
|
|
|||
|
|
@ -847,6 +847,11 @@ contract in `frontend-modern/src/utils/storageSources.ts`: storage pages and
|
|||
cross-surface storage links must reuse one canonical ordering, label, tone, and
|
||||
default-option model for sources like PVE, PBS, Ceph, and TrueNAS instead of
|
||||
re-sorting or re-presenting those source options locally.
|
||||
Storage filter option labels for grouped views, node/host filters, sort
|
||||
controls, and source selectors are also canonical presentation contracts:
|
||||
storage surfaces must consume `frontend-modern/src/components/Storage/storagePageState.ts`
|
||||
and `frontend-modern/src/utils/storageSources.ts` rather than re-declaring
|
||||
page-local title casing or alternate all-option labels.
|
||||
That same storage ownership also includes the physical-disk detail identity
|
||||
contract in `frontend-modern/src/components/Storage/` and
|
||||
`frontend-modern/src/features/storageBackups/`: historical disk charts must
|
||||
|
|
@ -1441,7 +1446,10 @@ The recovery table presentation helper now owns the canonical subject-type
|
|||
label fallback for recovery rows and delegates its title-casing to the shared
|
||||
`frontend-modern/src/utils/textPresentation.ts` helper rather than keeping a
|
||||
local recovery-only formatter, so subject and outcome labels stay aligned with
|
||||
the shared frontend label contract.
|
||||
the shared frontend label contract. Protected-inventory and recovery-event
|
||||
filters, table headers, and column-picker labels must use that helper for
|
||||
artifact fields such as `Item Type`, so the recovery tabs do not drift into
|
||||
near-identical page-local casing.
|
||||
That same recovery drill-in surface now also keeps provider-specific metadata
|
||||
inside a provider-neutral detail shell through
|
||||
`frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx`, so PBS
|
||||
|
|
|
|||
|
|
@ -139,9 +139,10 @@ cross-source deduplication.
|
|||
is collection-method detail when a provider/API platform is also present.
|
||||
Infrastructure table presentation controls must describe the table mode
|
||||
rather than a platform-specific resource concept: the grouped/flat toggle
|
||||
uses operator-facing `Grouped` and `List` wording, while Proxmox,
|
||||
Kubernetes, and other platform clusters stay reserved for actual resource
|
||||
identity, filters, and detail surfaces.
|
||||
uses operator-facing `Grouped` and `List` wording with a `Group by`
|
||||
accessible group label, while Proxmox, Kubernetes, and other platform
|
||||
clusters stay reserved for actual resource identity, filters, and detail
|
||||
surfaces.
|
||||
|
||||
Resource detail mappers now reuse the shared
|
||||
`frontend-modern/src/utils/textPresentation.ts` title-case helper for sensor
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
<FilterSegmentedControl
|
||||
value={props.groupingMode()}
|
||||
onChange={(value) => props.setGroupingMode(value as 'grouped' | 'flat')}
|
||||
aria-label="Group By"
|
||||
aria-label="Group by"
|
||||
options={[
|
||||
{
|
||||
value: 'grouped',
|
||||
|
|
|
|||
|
|
@ -744,7 +744,7 @@ describe('Dashboard performance contract', () => {
|
|||
expect(dashboardWorkloadViewportSyncSource).toContain('groupedWindowing.onScroll');
|
||||
expect(dashboardWorkloadRouteStateSource).not.toContain("from './workloadTopology'");
|
||||
expect(dashboardWorkloadRouteModelSource).toContain("from './workloadTopology'");
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('workloadNodeScopeId');
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('workloadHostScopeId');
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('getKubernetesContextKey');
|
||||
expect(dashboardSelectionStateSource).toContain(
|
||||
'const [selectedGuestId, setSelectedGuestIdRaw]',
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ describe('DashboardFilter', () => {
|
|||
it('renders Grouped and List buttons', () => {
|
||||
const props = makeProps();
|
||||
render(() => <DashboardFilter {...props} />);
|
||||
expect(screen.getByLabelText('Group by')).toBeInTheDocument();
|
||||
expect(screen.getByText('Grouped')).toBeInTheDocument();
|
||||
expect(screen.getByText('List')).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
} from '@/utils/recoveryLocationPresentation';
|
||||
import { normalizeRecoveryModeQueryValue } from '@/utils/recoveryRecordPresentation';
|
||||
import {
|
||||
getRecoveryArtifactColumnLabel,
|
||||
getRecoveryHistorySearchPlaceholder,
|
||||
getRecoverySearchHistoryEmptyMessage,
|
||||
RECOVERY_ADVANCED_FILTER_FIELD_CLASS,
|
||||
|
|
@ -403,7 +404,7 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
|
||||
<LabeledFilterSelect
|
||||
id="recovery-item-type-filter-history"
|
||||
label="Item type"
|
||||
label={getRecoveryArtifactColumnLabel('type', 'Item Type')}
|
||||
value={props.itemTypeFilter()}
|
||||
onChange={(event) => {
|
||||
props.setItemTypeFilter(
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
>
|
||||
<LabeledFilterSelect
|
||||
id="recovery-item-type-filter"
|
||||
label="Item Type"
|
||||
label={getRecoveryArtifactColumnLabel('type', 'Item Type')}
|
||||
value={props.itemTypeFilter()}
|
||||
onChange={(event) =>
|
||||
props.setItemTypeFilter(
|
||||
|
|
@ -419,7 +419,7 @@ export const RecoveryProtectedInventorySection: Component<
|
|||
{(
|
||||
[
|
||||
['item', getRecoveryArtifactColumnLabel('item', 'Item')],
|
||||
['type', 'Item Type'],
|
||||
['type', getRecoveryArtifactColumnLabel('type', 'Item Type')],
|
||||
['platform', getRecoveryArtifactColumnLabel('platform', 'Platform')],
|
||||
['lastBackup', 'Latest Point'],
|
||||
['outcome', 'Protection State'],
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ describe('Recovery', () => {
|
|||
);
|
||||
expect(screen.getAllByText(/^1 event$/i)).toHaveLength(1);
|
||||
expect(within(historyControls).queryByText(/day group/i)).not.toBeInTheDocument();
|
||||
expect(within(historyControls).getByLabelText('Item type').className).not.toContain('min-w-[');
|
||||
expect(within(historyControls).getByLabelText('Item Type').className).not.toContain('min-w-[');
|
||||
expect(within(historyControls).getByLabelText('Platform').className).not.toContain('min-w-[');
|
||||
expect(within(historyControls).getByLabelText('Status').className).not.toContain('min-w-[');
|
||||
const recoverySummaryPanel = screen.getByTestId('recovery-summary');
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
<FilterSegmentedControl
|
||||
value={props.groupBy!()}
|
||||
onChange={(value) => props.setGroupBy!(value as StorageGroupByFilter)}
|
||||
aria-label="Group By"
|
||||
aria-label="Group by"
|
||||
options={STORAGE_GROUP_BY_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -267,7 +267,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
value={props.sortKey()}
|
||||
onChange={(e) => props.setSortKey(e.currentTarget.value)}
|
||||
disabled={props.sortDisabled}
|
||||
aria-label="Sort By"
|
||||
aria-label="Sort by"
|
||||
class={STORAGE_FILTER_SORT_SELECT_CLASS}
|
||||
>
|
||||
{sortOptions().map((option) => (
|
||||
|
|
@ -279,7 +279,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
title={sortDirectionTitle()}
|
||||
onClick={toggleSortDirection}
|
||||
disabled={props.sortDisabled}
|
||||
aria-label="Sort Direction"
|
||||
aria-label="Sort direction"
|
||||
class={STORAGE_FILTER_SORT_DIRECTION_BUTTON_CLASS}
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -656,10 +656,10 @@ describe('Storage', () => {
|
|||
.some((element) => element.getAttribute('title') === 'Pool redundancy is reduced.'),
|
||||
).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Sort By'), {
|
||||
fireEvent.change(screen.getByLabelText('Sort by'), {
|
||||
target: { value: 'usage' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sort Direction' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sort direction' }));
|
||||
|
||||
await waitFor(() => {
|
||||
const orderedRowIds = Array.from(document.querySelectorAll('tr[data-row-id]')).map((row) =>
|
||||
|
|
@ -668,7 +668,7 @@ describe('Storage', () => {
|
|||
expect(orderedRowIds.slice(0, 2)).toEqual(['storage-1', 'storage-2']);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'By Status' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'By status' }));
|
||||
|
||||
// Group headers now show just the key name (without prefix)
|
||||
expect(screen.getAllByText('degraded').length).toBeGreaterThan(0);
|
||||
|
|
@ -710,18 +710,18 @@ describe('Storage', () => {
|
|||
'true',
|
||||
);
|
||||
expect((screen.getByLabelText('Node') as HTMLSelectElement).value).toBe('node-2');
|
||||
expect((screen.getByLabelText('Sort By') as HTMLSelectElement).value).toBe('usage');
|
||||
expect((screen.getByLabelText('Sort by') as HTMLSelectElement).value).toBe('usage');
|
||||
expect(getStorageSourceSelect().value).toBe('proxmox-pve');
|
||||
expect((screen.getByLabelText('Status') as HTMLSelectElement).value).toBe('warning');
|
||||
|
||||
// Grouping controls are only shown on the Pools view.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Pools' }));
|
||||
expect(screen.getByRole('button', { name: 'By Status' })).toHaveAttribute(
|
||||
expect(screen.getByRole('button', { name: 'By status' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'By Type' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'By type' }));
|
||||
|
||||
await waitFor(() => {
|
||||
const [nextPath] = navigateSpy.mock.calls.at(-1) as [string];
|
||||
|
|
@ -791,7 +791,7 @@ describe('Storage', () => {
|
|||
expect((screen.getByLabelText('Node') as HTMLSelectElement).value).toBe('node-2');
|
||||
expect(getStorageSourceSelect().value).toBe('proxmox-pve');
|
||||
expect((screen.getByLabelText('Status') as HTMLSelectElement).value).toBe('available');
|
||||
expect((screen.getByLabelText('Sort By') as HTMLSelectElement).value).toBe('usage');
|
||||
expect((screen.getByLabelText('Sort by') as HTMLSelectElement).value).toBe('usage');
|
||||
});
|
||||
|
||||
it('canonicalizes source aliases in storage URL params back to owned option values', async () => {
|
||||
|
|
@ -1550,7 +1550,7 @@ describe('Storage', () => {
|
|||
label: option.textContent,
|
||||
})),
|
||||
).toEqual([
|
||||
{ value: 'all', label: 'All Sources' },
|
||||
{ value: 'all', label: 'All sources' },
|
||||
{ value: 'proxmox-pve', label: 'PVE' },
|
||||
{ value: 'truenas', label: 'TrueNAS' },
|
||||
]);
|
||||
|
|
@ -1728,7 +1728,7 @@ describe('Storage', () => {
|
|||
.getAllByRole('option')
|
||||
.map((option) => option.textContent)
|
||||
.filter(Boolean);
|
||||
expect(options).toContain('All Disk Hosts');
|
||||
expect(options).toContain('All disk hosts');
|
||||
expect(options).toContain('pve1');
|
||||
expect(options).not.toContain('mini');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ describe('StorageControls', () => {
|
|||
createSignal<'all' | 'warning' | 'critical'>('all');
|
||||
const [sourceFilter, setSourceFilter] = createSignal('all');
|
||||
const [sourceOptions] = createSignal<StorageSourceOption[]>([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' as const },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' as const },
|
||||
{ key: 'proxmox-pve', label: 'PVE', tone: 'orange' as const },
|
||||
]);
|
||||
const [selectedNodeId, setSelectedNodeId] = createSignal('all');
|
||||
|
|
@ -38,7 +38,7 @@ describe('StorageControls', () => {
|
|||
setSourceFilter={setSourceFilter}
|
||||
sourceOptions={sourceOptions}
|
||||
nodeFilterOptions={[
|
||||
{ value: 'all', label: 'All Nodes' },
|
||||
{ value: 'all', label: 'All nodes' },
|
||||
{ value: 'node-1', label: 'pve1' },
|
||||
]}
|
||||
selectedNodeId={selectedNodeId}
|
||||
|
|
@ -65,7 +65,7 @@ describe('StorageControls', () => {
|
|||
createSignal<'all' | 'warning' | 'critical'>('all');
|
||||
const [sourceFilter, setSourceFilter] = createSignal('truenas');
|
||||
const [sourceOptions, setSourceOptions] = createSignal<StorageSourceOption[]>([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' as const },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' as const },
|
||||
]);
|
||||
const [selectedNodeId, setSelectedNodeId] = createSignal('all');
|
||||
|
||||
|
|
@ -86,14 +86,14 @@ describe('StorageControls', () => {
|
|||
sourceFilter={sourceFilter}
|
||||
setSourceFilter={setSourceFilter}
|
||||
sourceOptions={sourceOptions}
|
||||
nodeFilterOptions={[{ value: 'all', label: 'All Nodes' }]}
|
||||
nodeFilterOptions={[{ value: 'all', label: 'All nodes' }]}
|
||||
selectedNodeId={selectedNodeId}
|
||||
setSelectedNodeId={setSelectedNodeId}
|
||||
/>
|
||||
));
|
||||
|
||||
setSourceOptions([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' as const },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' as const },
|
||||
{ key: 'proxmox-pve', label: 'PVE', tone: 'orange' as const },
|
||||
{ key: 'truenas', label: 'TrueNAS', tone: 'blue' as const },
|
||||
]);
|
||||
|
|
@ -104,7 +104,7 @@ describe('StorageControls', () => {
|
|||
label: option.textContent,
|
||||
})),
|
||||
).toEqual([
|
||||
{ value: 'all', label: 'All Sources' },
|
||||
{ value: 'all', label: 'All sources' },
|
||||
{ value: 'proxmox-pve', label: 'PVE' },
|
||||
{ value: 'truenas', label: 'TrueNAS' },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -117,8 +117,8 @@ describe('storagePageState', () => {
|
|||
expect(getStorageStatusFilterValue('attention')).toBe('attention');
|
||||
expect(toStorageHealthFilterValue('available')).toBe('healthy');
|
||||
expect(toStorageHealthFilterValue('attention')).toBe('attention');
|
||||
expect(getStorageNodeFilterLabel('pools')).toBe('All Nodes');
|
||||
expect(getStorageNodeFilterLabel('disks')).toBe('All Disk Hosts');
|
||||
expect(getStorageNodeFilterLabel('pools')).toBe('All nodes');
|
||||
expect(getStorageNodeFilterLabel('disks')).toBe('All disk hosts');
|
||||
expect(DEFAULT_STORAGE_VIEW).toBe('pools');
|
||||
expect(DEFAULT_STORAGE_SOURCE_FILTER).toBe('all');
|
||||
expect(DEFAULT_STORAGE_DISK_ROLE_FILTER).toBe('all');
|
||||
|
|
@ -171,7 +171,7 @@ describe('storagePageState', () => {
|
|||
expect(coerceSelectedStorageNodeId('node-1', nodeOptions)).toBe('node-1');
|
||||
expect(coerceSelectedStorageNodeId('missing', nodeOptions)).toBe('all');
|
||||
expect(buildStorageNodeFilterOptions('disks', nodeOptions)).toEqual([
|
||||
{ value: 'all', label: 'All Disk Hosts' },
|
||||
{ value: 'all', label: 'All disk hosts' },
|
||||
{ value: 'node-1', label: 'pve1' },
|
||||
{ value: 'node-2', label: 'pve2' },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe('storageSourceOptions', () => {
|
|||
makeStorage({ id: '4', type: 'custom-backend' }),
|
||||
]);
|
||||
|
||||
expect(options[0]).toMatchObject({ key: 'all', label: 'All Sources' });
|
||||
expect(options[0]).toMatchObject({ key: 'all', label: 'All sources' });
|
||||
expect(options.map((option) => option.key)).toEqual([
|
||||
'all',
|
||||
'proxmox-pve',
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ describe('useStorageFilterState', () => {
|
|||
);
|
||||
|
||||
expect(result.sourceFilterOptions()).toEqual([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' },
|
||||
{ key: 'proxmox-pve', label: 'PVE', tone: 'orange' },
|
||||
{ key: 'truenas', label: 'TrueNAS', tone: 'blue' },
|
||||
{ key: 'agent', label: 'Agent', tone: 'slate' },
|
||||
]);
|
||||
expect(result.nodeFilterOptions()).toEqual([
|
||||
{ value: 'all', label: 'All Nodes' },
|
||||
{ value: 'all', label: 'All nodes' },
|
||||
{ value: 'node-1', label: 'pve1' },
|
||||
]);
|
||||
expect(result.storageFilterGroupBy()).toBe('node');
|
||||
|
|
@ -56,9 +56,9 @@ describe('useStorageFilterState', () => {
|
|||
|
||||
it('coerces stale selected nodes and disk facets, and maps status setters', () => {
|
||||
const [view] = createSignal<'pools' | 'disks'>('disks');
|
||||
const [nodeOptions] = createSignal<StorageNodeOption[]>([{ id: 'all', label: 'All Nodes' }]);
|
||||
const [nodeOptions] = createSignal<StorageNodeOption[]>([{ id: 'all', label: 'All nodes' }]);
|
||||
const [diskNodeOptions] = createSignal<StorageNodeOption[]>([
|
||||
{ id: 'all', label: 'All Nodes' },
|
||||
{ id: 'all', label: 'All nodes' },
|
||||
]);
|
||||
const [selectedNodeId, setSelectedNodeId] = createSignal('missing');
|
||||
const [sourceOptions] = createSignal(['all']);
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('useStorageFilterToolbarModel', () => {
|
|||
const [sortKey, setSortKey] = createSignal('priority');
|
||||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('desc');
|
||||
const [sourceOptions, setSourceOptions] = createSignal<StorageSourceOption[]>([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' as const },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' as const },
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
|
|
@ -82,16 +82,16 @@ describe('useStorageFilterToolbarModel', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(result.sourceOptions()).toEqual([{ key: 'all', label: 'All Sources', tone: 'slate' }]);
|
||||
expect(result.sourceOptions()).toEqual([{ key: 'all', label: 'All sources', tone: 'slate' }]);
|
||||
|
||||
setSourceOptions([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' },
|
||||
{ key: 'proxmox-pve', label: 'PVE', tone: 'orange' },
|
||||
{ key: 'truenas', label: 'TrueNAS', tone: 'blue' },
|
||||
]);
|
||||
|
||||
expect(result.sourceOptions()).toEqual([
|
||||
{ key: 'all', label: 'All Sources', tone: 'slate' },
|
||||
{ key: 'all', label: 'All sources', tone: 'slate' },
|
||||
{ key: 'proxmox-pve', label: 'PVE', tone: 'orange' },
|
||||
{ key: 'truenas', label: 'TrueNAS', tone: 'blue' },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export const STORAGE_STATUS_FILTER_OPTIONS: StorageOption[] = [
|
|||
|
||||
export const STORAGE_GROUP_BY_OPTIONS: StorageOption[] = [
|
||||
{ value: 'none', label: 'Flat' },
|
||||
{ value: 'node', label: 'By Node' },
|
||||
{ value: 'type', label: 'By Type' },
|
||||
{ value: 'status', label: 'By Status' },
|
||||
{ value: 'node', label: 'By node' },
|
||||
{ value: 'type', label: 'By type' },
|
||||
{ value: 'status', label: 'By status' },
|
||||
];
|
||||
|
||||
export const normalizeStorageHealthFilter = (value: string): StorageHealthFilter => {
|
||||
|
|
@ -196,7 +196,7 @@ export const hasActiveStorageFilters = (state: StorageFilterActivityState): bool
|
|||
DEFAULT_STORAGE_DISK_GROUP_FILTER;
|
||||
|
||||
export const getStorageNodeFilterLabel = (view: StorageView): string =>
|
||||
view === 'disks' ? 'All Disk Hosts' : 'All Nodes';
|
||||
view === 'disks' ? 'All disk hosts' : 'All nodes';
|
||||
|
||||
export const readStorageRouteValue = (value: string | undefined, defaultValue: string): string => {
|
||||
const normalized = (value || '').trim();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { Card } from '@/components/shared/Card';
|
||||
import {
|
||||
ALERT_OVERVIEW_ACKNOWLEDGED_LABEL,
|
||||
ALERT_OVERVIEW_LAST_24_HOURS_LABEL,
|
||||
ALERT_OVERVIEW_WORKLOAD_OVERRIDES_LABEL,
|
||||
} from '@/utils/alertOverviewPresentation';
|
||||
|
||||
import type { AlertOverviewState } from './useAlertOverviewState';
|
||||
|
||||
|
|
@ -13,7 +18,7 @@ export function AlertOverviewStatsCards(props: AlertOverviewStatsCardsProps) {
|
|||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[10px] sm:text-sm text-muted uppercase tracking-wider sm:normal-case">
|
||||
Acknowledged
|
||||
{ALERT_OVERVIEW_ACKNOWLEDGED_LABEL}
|
||||
</p>
|
||||
<p class="text-lg sm:text-2xl font-semibold text-yellow-600 dark:text-yellow-400">
|
||||
{props.state.alertStats().acknowledged}
|
||||
|
|
@ -40,7 +45,7 @@ export function AlertOverviewStatsCards(props: AlertOverviewStatsCardsProps) {
|
|||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[10px] sm:text-sm text-muted uppercase tracking-wider sm:normal-case">
|
||||
Last 24 Hours
|
||||
{ALERT_OVERVIEW_LAST_24_HOURS_LABEL}
|
||||
</p>
|
||||
<p class="text-lg sm:text-2xl font-semibold text-base-content">
|
||||
{props.state.alertStats().total24h}
|
||||
|
|
@ -67,7 +72,7 @@ export function AlertOverviewStatsCards(props: AlertOverviewStatsCardsProps) {
|
|||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[10px] sm:text-sm text-muted uppercase tracking-wider sm:normal-case">
|
||||
Guest Overrides
|
||||
{ALERT_OVERVIEW_WORKLOAD_OVERRIDES_LABEL}
|
||||
</p>
|
||||
<p class="text-lg sm:text-2xl font-semibold text-blue-600 dark:text-blue-400">
|
||||
{props.state.alertStats().overrides}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export function InfrastructurePageSurface() {
|
|||
<FilterSegmentedControl
|
||||
value={groupingMode()}
|
||||
onChange={(value) => setGroupingMode(value as GroupingMode)}
|
||||
aria-label="Group By"
|
||||
aria-label="Group by"
|
||||
options={[
|
||||
{
|
||||
value: 'grouped',
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ describe('InfrastructurePageSurface guardrails', () => {
|
|||
'data-testid="infrastructure-interaction-surface"',
|
||||
);
|
||||
expect(infrastructurePageSurfaceSource).toContain('data-summary-clear-ignore');
|
||||
expect(infrastructurePageSurfaceSource).toContain('aria-label="Group by"');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Group By"');
|
||||
expect(infrastructurePageSurfaceSource).toContain("title: 'Grouped table view'");
|
||||
expect(infrastructurePageSurfaceSource).toContain('Grouped');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain("title: 'Group by cluster'");
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ describe('alertConfigPresentation', () => {
|
|||
);
|
||||
expect(ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL).toBe('Max alerts / hour');
|
||||
expect(ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX).toBe('alerts');
|
||||
expect(ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP).toBe('Per guest/metric combination');
|
||||
expect(ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP).toBe('Per workload/metric combination');
|
||||
expect(ALERT_CONFIG_GROUPING_TITLE).toBe('Smart grouping');
|
||||
expect(ALERT_CONFIG_GROUPING_DESCRIPTION).toBe('Bundle similar alerts together.');
|
||||
expect(ALERT_CONFIG_GROUPING_WINDOW_LABEL).toBe('Grouping window');
|
||||
|
|
@ -108,8 +108,8 @@ describe('alertConfigPresentation', () => {
|
|||
'Alerts within this window are grouped together. Set to 0 to send immediately.',
|
||||
);
|
||||
expect(ALERT_CONFIG_GROUPING_STRATEGY_LABEL).toBe('Grouping strategy');
|
||||
expect(ALERT_CONFIG_GROUPING_BY_NODE).toBe('By Node');
|
||||
expect(ALERT_CONFIG_GROUPING_BY_GUEST).toBe('By Guest');
|
||||
expect(ALERT_CONFIG_GROUPING_BY_NODE).toBe('By node');
|
||||
expect(ALERT_CONFIG_GROUPING_BY_GUEST).toBe('By workload');
|
||||
expect(getAlertConfigQuietHourSuppressOptions()).toEqual([
|
||||
{
|
||||
key: 'performance',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import {
|
|||
ALERT_HISTORY_EMPTY_STATE,
|
||||
ALERT_HISTORY_LOADING_STATE,
|
||||
ALERT_HISTORY_SEARCH_PLACEHOLDER,
|
||||
ALERT_OVERVIEW_ACKNOWLEDGED_LABEL,
|
||||
ALERT_OVERVIEW_LAST_24_HOURS_LABEL,
|
||||
ALERT_OVERVIEW_WORKLOAD_OVERRIDES_LABEL,
|
||||
ALERTS_EMPTY_STATE,
|
||||
ALERTS_PAGE_DEFAULT_TITLE,
|
||||
ALERTS_PAGE_DEFAULT_DESCRIPTION,
|
||||
|
|
@ -62,6 +65,12 @@ describe('alertOverviewPresentation', () => {
|
|||
expect(getAlertListEmptyState(false)).toBe('No unacknowledged alerts');
|
||||
});
|
||||
|
||||
it('returns canonical alert overview stat labels', () => {
|
||||
expect(ALERT_OVERVIEW_ACKNOWLEDGED_LABEL).toBe('Acknowledged');
|
||||
expect(ALERT_OVERVIEW_LAST_24_HOURS_LABEL).toBe('Last 24 Hours');
|
||||
expect(ALERT_OVERVIEW_WORKLOAD_OVERRIDES_LABEL).toBe('Workload Overrides');
|
||||
});
|
||||
|
||||
it('returns canonical alert history search and empty-state copy', () => {
|
||||
expect(ALERT_HISTORY_SEARCH_PLACEHOLDER).toBe('Search alerts...');
|
||||
expect(getAlertHistorySearchPlaceholder()).toBe('Search alerts...');
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ describe('alertThresholdsPresentation', () => {
|
|||
);
|
||||
expect(PBS_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No PBS servers match the current filters.');
|
||||
expect(GUEST_THRESHOLDS_FILTER_EMPTY_STATE).toBe('No VMs or containers match the current filters.');
|
||||
expect(GUEST_FILTERING_EMPTY_STATE).toBe('Configure guest filtering rules.');
|
||||
expect(GUEST_FILTERING_EMPTY_STATE).toBe('Configure VM and container filtering rules.');
|
||||
expect(BACKUP_THRESHOLDS_EMPTY_STATE).toBe('Configure recovery alert thresholds.');
|
||||
expect(SNAPSHOT_THRESHOLDS_EMPTY_STATE).toBe('Configure snapshot age thresholds.');
|
||||
expect(STORAGE_THRESHOLDS_EMPTY_STATE).toBe('No storage devices found.');
|
||||
|
|
@ -100,19 +100,19 @@ describe('alertThresholdsPresentation', () => {
|
|||
expect(ALERT_THRESHOLDS_DOCKER_IGNORED_PREFIXES_PLACEHOLDER).toBe('runner-');
|
||||
expect(getAlertThresholdsGuestFilterPresentation()).toEqual({
|
||||
ignoredPrefixes: {
|
||||
title: 'Ignored Prefixes',
|
||||
description: 'Skip metrics for guests starting with:',
|
||||
title: 'Ignored prefixes',
|
||||
description: 'Skip metrics for VMs and containers starting with:',
|
||||
placeholder: 'dev-',
|
||||
},
|
||||
tagWhitelist: {
|
||||
title: 'Tag Whitelist',
|
||||
title: 'Required tags',
|
||||
description:
|
||||
'Only monitor guests with at least one of these tags (leave empty to disable whitelist):',
|
||||
'Only monitor VMs and containers with at least one of these tags (leave empty to disable this filter):',
|
||||
placeholder: 'production',
|
||||
},
|
||||
tagBlacklist: {
|
||||
title: 'Tag Blacklist',
|
||||
description: 'Ignore guests with any of these tags:',
|
||||
title: 'Ignored tags',
|
||||
description: 'Ignore VMs and containers with any of these tags:',
|
||||
placeholder: 'maintenance',
|
||||
},
|
||||
});
|
||||
|
|
@ -172,7 +172,7 @@ describe('alertThresholdsPresentation', () => {
|
|||
expect(ALERT_THRESHOLDS_SECTION_TITLE_NODES).toBe('Virtualization Hosts');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_PBS).toBe('PBS Servers');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_GUESTS).toBe('VMs & Containers');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING).toBe('Guest Filtering');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING).toBe('VM and Container Filtering');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_BACKUPS).toBe('Recovery');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_SNAPSHOTS).toBe('Snapshot Age');
|
||||
expect(ALERT_THRESHOLDS_SECTION_TITLE_STORAGE).toBe('Storage Devices');
|
||||
|
|
@ -185,7 +185,7 @@ describe('alertThresholdsPresentation', () => {
|
|||
nodes: 'Virtualization Hosts',
|
||||
pbs: 'PBS Servers',
|
||||
guests: 'VMs & Containers',
|
||||
guestFiltering: 'Guest Filtering',
|
||||
guestFiltering: 'VM and Container Filtering',
|
||||
backups: 'Recovery',
|
||||
snapshots: 'Snapshot Age',
|
||||
storage: 'Storage Devices',
|
||||
|
|
|
|||
|
|
@ -839,7 +839,7 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(dashboardWorkloadDerivedStateSource).toContain('buildGuestParentNodeMap(');
|
||||
expect(dashboardWorkloadRouteStateSource).not.toContain("from './workloadTopology'");
|
||||
expect(dashboardWorkloadRouteModelSource).toContain("from './workloadTopology'");
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('workloadNodeScopeId');
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('workloadHostScopeId');
|
||||
expect(dashboardWorkloadRouteModelSource).toContain('getKubernetesContextKey');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('isWorkloadsRoute,');
|
||||
expect(dashboardSelectionStateSource).toContain(
|
||||
|
|
|
|||
|
|
@ -22,15 +22,15 @@ export const ALERT_CONFIG_COOLDOWN_PERIOD_HELP =
|
|||
'Minimum time between alerts for the same issue';
|
||||
export const ALERT_CONFIG_COOLDOWN_MAX_ALERTS_LABEL = 'Max alerts / hour';
|
||||
export const ALERT_CONFIG_COOLDOWN_MAX_ALERTS_SUFFIX = 'alerts';
|
||||
export const ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP = 'Per guest/metric combination';
|
||||
export const ALERT_CONFIG_COOLDOWN_MAX_ALERTS_HELP = 'Per workload/metric combination';
|
||||
export const ALERT_CONFIG_GROUPING_TITLE = 'Smart grouping';
|
||||
export const ALERT_CONFIG_GROUPING_DESCRIPTION = 'Bundle similar alerts together.';
|
||||
export const ALERT_CONFIG_GROUPING_WINDOW_LABEL = 'Grouping window';
|
||||
export const ALERT_CONFIG_GROUPING_WINDOW_HELP =
|
||||
'Alerts within this window are grouped together. Set to 0 to send immediately.';
|
||||
export const ALERT_CONFIG_GROUPING_STRATEGY_LABEL = 'Grouping strategy';
|
||||
export const ALERT_CONFIG_GROUPING_BY_NODE = 'By Node';
|
||||
export const ALERT_CONFIG_GROUPING_BY_GUEST = 'By Guest';
|
||||
export const ALERT_CONFIG_GROUPING_BY_NODE = 'By node';
|
||||
export const ALERT_CONFIG_GROUPING_BY_GUEST = 'By workload';
|
||||
export const ALERT_CONFIG_QUIET_HOUR_SUPPRESS_OPTIONS = [
|
||||
{
|
||||
key: 'performance',
|
||||
|
|
@ -129,7 +129,7 @@ export function getAlertConfigSummaryGrouping(
|
|||
byNode: boolean,
|
||||
byGuest: boolean,
|
||||
) {
|
||||
const groupingTargets = [byNode && 'node', byGuest && 'guest'].filter(Boolean).join(' and ');
|
||||
const groupingTargets = [byNode && 'node', byGuest && 'workload'].filter(Boolean).join(' and ');
|
||||
return groupingTargets
|
||||
? `${ALERT_CONFIG_SUMMARY_GROUPING_PREFIX} ${windowMinutes} minute windows by ${groupingTargets}`
|
||||
: `${ALERT_CONFIG_SUMMARY_GROUPING_PREFIX} ${windowMinutes} minute windows`;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ export const ALERT_HISTORY_EMPTY_STATE = 'No alerts found';
|
|||
export const ALERT_HISTORY_EMPTY_DESCRIPTION = 'Try adjusting your filters or check back later';
|
||||
export const ALERT_BUCKET_EMPTY_LABEL = 'No alerts';
|
||||
export const ALERT_HISTORY_LOADING_STATE = 'Loading alert history...';
|
||||
export const ALERT_OVERVIEW_ACKNOWLEDGED_LABEL = 'Acknowledged';
|
||||
export const ALERT_OVERVIEW_LAST_24_HOURS_LABEL = 'Last 24 Hours';
|
||||
export const ALERT_OVERVIEW_WORKLOAD_OVERRIDES_LABEL = 'Workload Overrides';
|
||||
export const ALERTS_PAGE_DEFAULT_TITLE = 'Alerts';
|
||||
export const ALERTS_PAGE_OVERVIEW_TITLE = 'Alerts Overview';
|
||||
export const ALERTS_PAGE_THRESHOLDS_TITLE = 'Alert Thresholds';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export const GUEST_THRESHOLDS_EMPTY_STATE = 'No VMs or containers found.';
|
|||
export const NODE_THRESHOLDS_FILTER_EMPTY_STATE = 'No virtualization hosts match the current filters.';
|
||||
export const PBS_THRESHOLDS_FILTER_EMPTY_STATE = 'No PBS servers match the current filters.';
|
||||
export const GUEST_THRESHOLDS_FILTER_EMPTY_STATE = 'No VMs or containers match the current filters.';
|
||||
export const GUEST_FILTERING_EMPTY_STATE = 'Configure guest filtering rules.';
|
||||
export const GUEST_FILTERING_EMPTY_STATE = 'Configure VM and container filtering rules.';
|
||||
export const BACKUP_THRESHOLDS_EMPTY_STATE = 'Configure recovery alert thresholds.';
|
||||
export const SNAPSHOT_THRESHOLDS_EMPTY_STATE = 'Configure snapshot age thresholds.';
|
||||
export const STORAGE_THRESHOLDS_EMPTY_STATE = 'No storage devices found.';
|
||||
|
|
@ -20,17 +20,17 @@ export const CONTAINER_RUNTIMES_FILTER_EMPTY_STATE =
|
|||
export const CONTAINERS_FILTER_EMPTY_STATE = 'No containers match the current filters.';
|
||||
export const ALERT_THRESHOLDS_SEARCH_PLACEHOLDER = 'Search resources...';
|
||||
export const ALERT_THRESHOLDS_HELP_DISMISS_LABEL = 'Dismiss tips';
|
||||
export const ALERT_THRESHOLDS_GUEST_IGNORED_PREFIXES_TITLE = 'Ignored Prefixes';
|
||||
export const ALERT_THRESHOLDS_GUEST_IGNORED_PREFIXES_TITLE = 'Ignored prefixes';
|
||||
export const ALERT_THRESHOLDS_GUEST_IGNORED_PREFIXES_DESCRIPTION =
|
||||
'Skip metrics for guests starting with:';
|
||||
'Skip metrics for VMs and containers starting with:';
|
||||
export const ALERT_THRESHOLDS_GUEST_IGNORED_PREFIXES_PLACEHOLDER = 'dev-';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_WHITELIST_TITLE = 'Tag Whitelist';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_WHITELIST_TITLE = 'Required tags';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_WHITELIST_DESCRIPTION =
|
||||
'Only monitor guests with at least one of these tags (leave empty to disable whitelist):';
|
||||
'Only monitor VMs and containers with at least one of these tags (leave empty to disable this filter):';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_WHITELIST_PLACEHOLDER = 'production';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_BLACKLIST_TITLE = 'Tag Blacklist';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_BLACKLIST_TITLE = 'Ignored tags';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_BLACKLIST_DESCRIPTION =
|
||||
'Ignore guests with any of these tags:';
|
||||
'Ignore VMs and containers with any of these tags:';
|
||||
export const ALERT_THRESHOLDS_GUEST_TAG_BLACKLIST_PLACEHOLDER = 'maintenance';
|
||||
export const ALERT_THRESHOLDS_BACKUP_ORPHANED_TITLE = 'Orphaned backups';
|
||||
export const ALERT_THRESHOLDS_BACKUP_ORPHANED_DESCRIPTION =
|
||||
|
|
@ -64,7 +64,7 @@ export const ALERT_THRESHOLDS_DOCKER_SERVICES_GAP_VALIDATION_MESSAGE =
|
|||
export const ALERT_THRESHOLDS_SECTION_TITLE_NODES = 'Virtualization Hosts';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_PBS = 'PBS Servers';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_GUESTS = 'VMs & Containers';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING = 'Guest Filtering';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_GUEST_FILTERING = 'VM and Container Filtering';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_BACKUPS = 'Recovery';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_SNAPSHOTS = 'Snapshot Age';
|
||||
export const ALERT_THRESHOLDS_SECTION_TITLE_STORAGE = 'Storage Devices';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface StorageSourceOption {
|
|||
|
||||
const ALL_STORAGE_SOURCE_OPTION: StorageSourceOption = {
|
||||
key: 'all',
|
||||
label: 'All Sources',
|
||||
label: 'All sources',
|
||||
tone: 'slate',
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue