Normalize frontend product labels

This commit is contained in:
rcourtman 2026-04-28 21:51:50 +01:00
parent ff6016daab
commit 47d898cd50
30 changed files with 123 additions and 76 deletions

View file

@ -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.

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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]',

View file

@ -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();
});

View file

@ -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(

View file

@ -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'],

View file

@ -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');

View file

@ -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

View file

@ -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');
});

View file

@ -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' },
]);

View file

@ -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' },
]);

View file

@ -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',

View file

@ -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']);

View file

@ -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' },
]);

View file

@ -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();

View file

@ -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}

View file

@ -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',

View file

@ -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'");

View file

@ -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',

View file

@ -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...');

View file

@ -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',

View file

@ -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(

View file

@ -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`;

View file

@ -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';

View file

@ -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';

View file

@ -19,7 +19,7 @@ export interface StorageSourceOption {
const ALL_STORAGE_SOURCE_OPTION: StorageSourceOption = {
key: 'all',
label: 'All Sources',
label: 'All sources',
tone: 'slate',
};