mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
refactor(recovery): align operator recovery vocabulary
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -113,7 +113,7 @@ export const RecoveryActivitySection: Component<RecoveryActivitySectionProps> =
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Show when={props.selectedHistorySubjectLabel()}>
|
||||
<div class="inline-flex items-center gap-2 rounded-md border border-border bg-surface-alt px-2.5 py-1.5 text-xs">
|
||||
<span class="font-semibold uppercase tracking-wide text-muted">Focused</span>
|
||||
<span class="font-semibold uppercase tracking-wide text-muted">Focused Item</span>
|
||||
<span class="max-w-[18rem] truncate font-medium text-base-content">
|
||||
{props.selectedHistorySubjectLabel()}
|
||||
</span>
|
||||
|
||||
@@ -165,9 +165,9 @@ export const RecoveryPointDetails: Component<RecoveryPointDetailsProps> = (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 = [
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getRecoveryProtectedItemsLoadingState,
|
||||
} from '@/utils/recoveryEmptyStatePresentation';
|
||||
import {
|
||||
getRecoveryArtifactColumnLabel,
|
||||
getRecoveryRollupAgeTextClass,
|
||||
getRecoveryRollupIssueTone,
|
||||
getRecoveryProtectedSearchPlaceholder,
|
||||
@@ -151,7 +152,7 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
Protected Items
|
||||
</div>
|
||||
<div class="text-xs text-muted">
|
||||
Unified subject rollups across backups, snapshots, and replication providers.
|
||||
Unified protection inventory across workloads, datasets, and other protected items from every connected platform.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
@@ -283,8 +284,8 @@ export const RecoveryProtectedInventorySection: Component<
|
||||
<TableRow class="bg-surface-alt text-muted border-b border-border">
|
||||
{(
|
||||
[
|
||||
['subject', 'Subject'],
|
||||
['source', 'Source'],
|
||||
['subject', getRecoveryArtifactColumnLabel('subject', 'Subject')],
|
||||
['source', getRecoveryArtifactColumnLabel('source', 'Source')],
|
||||
['lastBackup', 'Latest Point'],
|
||||
['outcome', 'Outcome'],
|
||||
] as const
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -142,13 +142,13 @@ export const RecoverySummary: Component<RecoverySummaryProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted">Multi-source</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted">Multi-platform</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-base-content">
|
||||
{platformCoverage().mixedCount}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted">Primary</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted">Primary Platform</div>
|
||||
<div class="mt-1 text-xl font-semibold text-base-content">
|
||||
{platformCoverage().primaryLabel ?? 'n/a'}
|
||||
</div>
|
||||
|
||||
@@ -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(() => <Recovery />);
|
||||
|
||||
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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<string, string> = {
|
||||
subject: 'Item',
|
||||
source: 'Platform',
|
||||
};
|
||||
|
||||
const RECOVERY_ARTIFACT_COLUMN_SPECS: Record<string, { headerClass: string; minWidthPx: number }> = {
|
||||
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-/, '')
|
||||
|
||||
Reference in New Issue
Block a user