diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 0d2f7009a..e352ad9c5 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -153,6 +153,12 @@ Operator-facing filter and detail labels should likewise prefer `platform` wording over implementation-facing `provider` wording, so the recovery surface describes the monitored platform families Pulse covers rather than exposing backend transport vocabulary as the primary UI model. +That same operator-facing vocabulary should also prefer `item` over backend +`subject` wording, and `platform` over generic `source` wording, across the +primary recovery headers, tables, focus chips, and detail metadata labels. +The data model can keep its internal subject/provider fields, but the page +frame that operators read should present one consistent protected-item and +platform model from summary through drill-in. That same shared presentation layer also owns the distinction between aggregate recovery-method language and single-record recovery-method language. Timeline legends and daily breakdowns must use aggregate labels such as diff --git a/frontend-modern/src/components/Recovery/Recovery.tsx b/frontend-modern/src/components/Recovery/Recovery.tsx index 3b76dac65..4624ee3cf 100644 --- a/frontend-modern/src/components/Recovery/Recovery.tsx +++ b/frontend-modern/src/components/Recovery/Recovery.tsx @@ -29,6 +29,7 @@ import { getRecoveryRollupSubjectLabel, } from '@/utils/recoveryRecordPresentation'; import { + getRecoveryArtifactColumnLabel, getRecoveryGroupNoTimestampLabel, getRecoveryArtifactTableMinWidth, STALE_ISSUE_THRESHOLD_MS, @@ -279,12 +280,12 @@ const Recovery: Component = () => { const artifactColumns: ColumnDef[] = [ { id: 'time', label: 'Time' }, createHiddenCanonicalTypeColumn(), - { id: 'subject', label: 'Subject' }, + { id: 'subject', label: getRecoveryArtifactColumnLabel('subject', 'Subject') }, { id: 'entityId', label: 'ID', toggleable: true }, { id: 'cluster', label: 'Cluster', toggleable: true }, { id: 'nodeAgent', label: 'Node/Agent', toggleable: true }, { id: 'namespace', label: 'Namespace', toggleable: true }, - { id: 'source', label: 'Source' }, + { id: 'source', label: getRecoveryArtifactColumnLabel('source', 'Source') }, { id: 'verified', label: 'Verified', toggleable: true }, { id: 'size', label: 'Size', toggleable: true }, { id: 'method', label: 'Method' }, @@ -537,7 +538,7 @@ const Recovery: Component = () => { when={workspaceView() === 'inventory'} fallback="Cross-platform recovery artifacts grouped by day and filtered through one shared recovery model." > - Platform-neutral protection rollups across every connected recovery platform in the selected window. + Platform-neutral protection inventory across every connected recovery platform in the selected window. diff --git a/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx b/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx index 468d60edd..2d4c8a3ee 100644 --- a/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx @@ -113,7 +113,7 @@ export const RecoveryActivitySection: Component =
- Focused + Focused Item {props.selectedHistorySubjectLabel()} diff --git a/frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx b/frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx index cd7c66420..205258e2c 100644 --- a/frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryPointDetails.tsx @@ -165,9 +165,9 @@ export const RecoveryPointDetails: Component = (props if (p.verified != null) pairs.push({ k: 'Verified', v: p.verified ? 'Verified' : 'Not Verified' }); if (p.encrypted != null) pairs.push({ k: 'Encrypted', v: p.encrypted ? 'Encrypted' : 'Not Encrypted' }); - if (p.subjectResourceId) pairs.push({ k: 'Subject Resource', v: p.subjectResourceId }); + if (p.subjectResourceId) pairs.push({ k: 'Item Resource', v: p.subjectResourceId }); if (p.repositoryResourceId) pairs.push({ k: 'Repository Resource', v: p.repositoryResourceId }); - if (p.subjectRef) pairs.push({ k: 'Subject Ref', v: labelForRef(p.subjectRef) }); + if (p.subjectRef) pairs.push({ k: 'Item Ref', v: labelForRef(p.subjectRef) }); if (p.repositoryRef) pairs.push({ k: 'Repository Ref', v: labelForRef(p.repositoryRef) }); const commonDetailKeys = [ diff --git a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx index 4dac8f1ce..03ba3290f 100644 --- a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx @@ -24,6 +24,7 @@ import { getRecoveryProtectedItemsLoadingState, } from '@/utils/recoveryEmptyStatePresentation'; import { + getRecoveryArtifactColumnLabel, getRecoveryRollupAgeTextClass, getRecoveryRollupIssueTone, getRecoveryProtectedSearchPlaceholder, @@ -151,7 +152,7 @@ export const RecoveryProtectedInventorySection: Component< Protected Items
- Unified subject rollups across backups, snapshots, and replication providers. + Unified protection inventory across workloads, datasets, and other protected items from every connected platform.
@@ -283,8 +284,8 @@ export const RecoveryProtectedInventorySection: Component< {( [ - ['subject', 'Subject'], - ['source', 'Source'], + ['subject', getRecoveryArtifactColumnLabel('subject', 'Subject')], + ['source', getRecoveryArtifactColumnLabel('source', 'Source')], ['lastBackup', 'Latest Point'], ['outcome', 'Outcome'], ] as const diff --git a/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx b/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx index 33e645b23..02da7653b 100644 --- a/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx +++ b/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx @@ -57,6 +57,8 @@ describe('RecoverySummary', () => { expect(screen.getByText('Recent History')).toBeInTheDocument(); expect(screen.getByText('Attention Queue')).toBeInTheDocument(); expect(screen.getByText('Recovery Points')).toBeInTheDocument(); + expect(screen.getByText('Multi-platform')).toBeInTheDocument(); + expect(screen.getByText('Primary Platform')).toBeInTheDocument(); expect(screen.getByText('2 protected')).toBeInTheDocument(); expect(screen.getByText('2 platforms')).toBeInTheDocument(); expect(screen.getByText('1 healthy')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Recovery/RecoverySummary.tsx b/frontend-modern/src/components/Recovery/RecoverySummary.tsx index cf13d92eb..289318e5c 100644 --- a/frontend-modern/src/components/Recovery/RecoverySummary.tsx +++ b/frontend-modern/src/components/Recovery/RecoverySummary.tsx @@ -142,13 +142,13 @@ export const RecoverySummary: Component = (props) => {
-
Multi-source
+
Multi-platform
{platformCoverage().mixedCount}
-
Primary
+
Primary Platform
{platformCoverage().primaryLabel ?? 'n/a'}
diff --git a/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx b/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx index 4b78e0786..c4d3898d2 100644 --- a/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx +++ b/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx @@ -190,6 +190,9 @@ describe('Recovery', () => { await waitFor(() => { expect(screen.getAllByRole('table')).toHaveLength(1); }); + const inventoryTable = screen.getAllByRole('table')[0]; + expect(within(inventoryTable).getByText('Item')).toBeInTheDocument(); + expect(within(inventoryTable).getByText('Platform')).toBeInTheDocument(); expect(screen.queryByText('Backups By Date')).not.toBeInTheDocument(); fireEvent.click(await screen.findByText('VM 123')); @@ -199,6 +202,9 @@ describe('Recovery', () => { await waitFor(() => { expect(screen.getAllByRole('table')).toHaveLength(1); }); + const historyTable = screen.getAllByRole('table')[0]; + expect(within(historyTable).getByText('Item')).toBeInTheDocument(); + expect(within(historyTable).getByText('Platform')).toBeInTheDocument(); }); it('persists the selected recovery workspace view in the route when explicitly changed', async () => { @@ -277,6 +283,7 @@ describe('Recovery', () => { render(() => ); expect(await screen.findByText('2 platforms')).toBeInTheDocument(); + expect(screen.getByText('Multi-platform')).toBeInTheDocument(); }); it('keeps recovery history width aligned with canonical column specs', async () => { @@ -304,7 +311,7 @@ describe('Recovery', () => { }); }); - expect(await screen.findByText('Focused')).toBeInTheDocument(); + expect(await screen.findByText('Focused Item')).toBeInTheDocument(); expect(screen.getAllByText('VM 123').length).toBeGreaterThan(0); await screen.findByText(/Showing 1 - 1 of 1 recovery points/i); const tables = await screen.findAllByRole('table'); diff --git a/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts b/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts index d31835fbf..69201df96 100644 --- a/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { ProtectionRollup, RecoveryPoint } from '@/types/recovery'; import { + getRecoveryArtifactColumnLabel, getRecoveryArtifactColumnHeaderClass, getRecoveryArtifactRowClass, getRecoveryArtifactTableMinWidth, @@ -16,6 +17,7 @@ import { isRecoveryRollupStale, RECOVERY_ADVANCED_FILTER_FIELD_CLASS, RECOVERY_ADVANCED_FILTER_LABEL_CLASS, + RECOVERY_ARTIFACT_COLUMN_LABELS, RECOVERY_GROUP_HEADER_ROW_CLASS, RECOVERY_GROUP_HEADER_TEXT_CLASS, RECOVERY_GROUP_NO_TIMESTAMP_LABEL, @@ -38,6 +40,11 @@ describe('recoveryTablePresentation', () => { expect(getRecoveryProtectedSearchPlaceholder()).toBe('Search protected items...'); expect(getRecoveryHistorySearchPlaceholder()).toBe('Search recovery history...'); expect(getRecoverySearchHistoryEmptyMessage()).toBe('Recent searches appear here.'); + expect(RECOVERY_ARTIFACT_COLUMN_LABELS.subject).toBe('Item'); + expect(RECOVERY_ARTIFACT_COLUMN_LABELS.source).toBe('Platform'); + expect(getRecoveryArtifactColumnLabel('subject', 'Subject')).toBe('Item'); + expect(getRecoveryArtifactColumnLabel('source', 'Source')).toBe('Platform'); + expect(getRecoveryArtifactColumnLabel('outcome', 'Outcome')).toBe('Outcome'); }); it('derives time text classes from event recency', () => { diff --git a/frontend-modern/src/utils/recoveryTablePresentation.ts b/frontend-modern/src/utils/recoveryTablePresentation.ts index 97d6a3d3c..1579ed8ae 100644 --- a/frontend-modern/src/utils/recoveryTablePresentation.ts +++ b/frontend-modern/src/utils/recoveryTablePresentation.ts @@ -21,6 +21,10 @@ export const RECOVERY_GROUP_NO_TIMESTAMP_LABEL = 'No Timestamp'; export const RECOVERY_PROTECTED_SEARCH_PLACEHOLDER = 'Search protected items...'; export const RECOVERY_HISTORY_SEARCH_PLACEHOLDER = 'Search recovery history...'; export const RECOVERY_SEARCH_HISTORY_EMPTY_MESSAGE = 'Recent searches appear here.'; +export const RECOVERY_ARTIFACT_COLUMN_LABELS: Record = { + subject: 'Item', + source: 'Platform', +}; const RECOVERY_ARTIFACT_COLUMN_SPECS: Record = { time: { headerClass: 'w-[76px] text-right', minWidthPx: 76 }, @@ -67,6 +71,10 @@ export function getRecoverySearchHistoryEmptyMessage(): string { return RECOVERY_SEARCH_HISTORY_EMPTY_MESSAGE; } +export function getRecoveryArtifactColumnLabel(id: string, fallback?: string): string { + return RECOVERY_ARTIFACT_COLUMN_LABELS[id] || fallback || id; +} + const normalizeRecoverySubjectTypeKey = (value: string): string => value .replace(/^k8s-/, '')