mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-09 10:57:04 +00:00
refactor(recovery): neutralize placement vocabulary
This commit is contained in:
parent
2e49680bbc
commit
a4e2f6f448
12 changed files with 186 additions and 16 deletions
|
|
@ -360,6 +360,14 @@ when a recovery point includes canonical item-class metadata,
|
|||
`RecoveryPointDetails.tsx` must surface it as `Item Type` in the summary grid
|
||||
instead of jumping directly from item identity to platform and point-method
|
||||
metadata.
|
||||
That same shared presentation layer also owns recovery placement vocabulary.
|
||||
Cluster, node, and namespace facets remain valid supporting filters for
|
||||
Proxmox-heavy and Kubernetes-heavy operators, but the governed recovery
|
||||
surface must present them through platform-neutral labels such as
|
||||
`Cluster / Site`, `Host / Agent`, and `Namespace / Group` across advanced
|
||||
filters, active chips, table headers, and point details so the page treats
|
||||
placement as optional context inside a multi-platform recovery model rather
|
||||
than a Proxmox-native spine.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ import {
|
|||
getRecoveryItemTypePresentation,
|
||||
normalizeRecoveryItemTypeQueryValue,
|
||||
} from '@/utils/recoveryItemTypePresentation';
|
||||
import {
|
||||
getRecoveryLocationFacetAllLabel,
|
||||
getRecoveryLocationFacetLabel,
|
||||
} from '@/utils/recoveryLocationPresentation';
|
||||
import { normalizeRecoveryModeQueryValue } from '@/utils/recoveryRecordPresentation';
|
||||
import {
|
||||
getRecoveryHistorySearchPlaceholder,
|
||||
|
|
@ -178,7 +182,7 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
<div>
|
||||
<div class={filterPanelTitleClass}>Filter results</div>
|
||||
<div class={filterPanelDescriptionClass}>
|
||||
Narrow by scope, method, verification, or location.
|
||||
Narrow by scope, method, verification, or placement.
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.activeAdvancedFilterCount() > 0}>
|
||||
|
|
@ -261,7 +265,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
|
||||
<Show when={props.showClusterFilter()}>
|
||||
<label class="flex min-w-0 flex-col gap-1">
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>Cluster</span>
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>
|
||||
{getRecoveryLocationFacetLabel('cluster')}
|
||||
</span>
|
||||
<select
|
||||
value={props.clusterFilter()}
|
||||
onChange={(event) => {
|
||||
|
|
@ -270,7 +276,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
}}
|
||||
class={RECOVERY_ADVANCED_FILTER_FIELD_CLASS}
|
||||
>
|
||||
<option value="all">Any cluster</option>
|
||||
<option value="all">
|
||||
{getRecoveryLocationFacetAllLabel('cluster')}
|
||||
</option>
|
||||
<For each={props.clusterOptions().filter((value) => value !== 'all')}>
|
||||
{(cluster) => <option value={cluster}>{cluster}</option>}
|
||||
</For>
|
||||
|
|
@ -280,7 +288,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
|
||||
<Show when={props.showNodeFilter()}>
|
||||
<label class="flex min-w-0 flex-col gap-1">
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>Node or agent</span>
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>
|
||||
{getRecoveryLocationFacetLabel('node')}
|
||||
</span>
|
||||
<select
|
||||
value={props.nodeFilter()}
|
||||
onChange={(event) => {
|
||||
|
|
@ -289,7 +299,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
}}
|
||||
class={RECOVERY_ADVANCED_FILTER_FIELD_CLASS}
|
||||
>
|
||||
<option value="all">Any node or agent</option>
|
||||
<option value="all">
|
||||
{getRecoveryLocationFacetAllLabel('node')}
|
||||
</option>
|
||||
<For each={props.nodeOptions().filter((value) => value !== 'all')}>
|
||||
{(node) => <option value={node}>{node}</option>}
|
||||
</For>
|
||||
|
|
@ -299,7 +311,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
|
||||
<Show when={props.showNamespaceFilter()}>
|
||||
<label class="flex min-w-0 flex-col gap-1">
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>Namespace</span>
|
||||
<span class={RECOVERY_ADVANCED_FILTER_LABEL_CLASS}>
|
||||
{getRecoveryLocationFacetLabel('namespace')}
|
||||
</span>
|
||||
<select
|
||||
value={props.namespaceFilter()}
|
||||
onChange={(event) => {
|
||||
|
|
@ -308,7 +322,9 @@ export const RecoveryHistorySection: Component<RecoveryHistorySectionProps> = (p
|
|||
}}
|
||||
class={RECOVERY_ADVANCED_FILTER_FIELD_CLASS}
|
||||
>
|
||||
<option value="all">Any namespace</option>
|
||||
<option value="all">
|
||||
{getRecoveryLocationFacetAllLabel('namespace')}
|
||||
</option>
|
||||
<For each={props.namespaceOptions().filter((value) => value !== 'all')}>
|
||||
{(namespace) => <option value={namespace}>{namespace}</option>}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ describe('RecoveryPointDetails', () => {
|
|||
completedAt: '2026-03-10T10:00:00Z',
|
||||
verified: true,
|
||||
immutable: true,
|
||||
display: {
|
||||
clusterLabel: 'Lab Cluster',
|
||||
nodeHostLabel: 'pve-01',
|
||||
namespaceLabel: 'Finance',
|
||||
},
|
||||
subjectRef: {
|
||||
type: 'proxmox-vm',
|
||||
name: '100',
|
||||
|
|
@ -84,10 +89,16 @@ describe('RecoveryPointDetails', () => {
|
|||
expect(screen.getByText('Repository Health')).toBeInTheDocument();
|
||||
expect(screen.getByText('Verification')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cluster / Site')).toBeInTheDocument();
|
||||
expect(screen.getByText('Host / Agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Namespace / Group')).toBeInTheDocument();
|
||||
expect(screen.getByText('Point Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Method')).toBeInTheDocument();
|
||||
expect(screen.getByText('Outcome')).toBeInTheDocument();
|
||||
expect(screen.getByText('VM')).toBeInTheDocument();
|
||||
expect(screen.getByText('Lab Cluster')).toBeInTheDocument();
|
||||
expect(screen.getByText('pve-01')).toBeInTheDocument();
|
||||
expect(screen.getByText('Finance')).toBeInTheDocument();
|
||||
expect(screen.getByText('Backup')).toBeInTheDocument();
|
||||
expect(screen.getByText('Remote Copy')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Success').length).toBeGreaterThan(0);
|
||||
|
|
@ -104,6 +115,9 @@ describe('RecoveryPointDetails', () => {
|
|||
point={{
|
||||
id: 'point-2',
|
||||
provider: 'truenas',
|
||||
display: {
|
||||
nodeHostLabel: 'tn-scale-01',
|
||||
},
|
||||
subjectRef: {
|
||||
type: 'truenas-dataset',
|
||||
name: 'tank/apps',
|
||||
|
|
@ -119,7 +133,9 @@ describe('RecoveryPointDetails', () => {
|
|||
expect(screen.queryByText('Platform Details')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('PBS Details')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Item Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Host / Agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dataset')).toBeInTheDocument();
|
||||
expect(screen.getByText('tn-scale-01')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Snapshot').length).toBeGreaterThan(0);
|
||||
|
||||
const platformCard = screen.getByText('Platform').parentElement?.parentElement;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { PBSDatastore } from '@/types/api';
|
|||
import type { RecoveryExternalRef, RecoveryPoint } from '@/types/recovery';
|
||||
import { formatAbsoluteTime, formatBytes, formatUptime } from '@/utils/format';
|
||||
import { getRecoveryItemTypeLabel } from '@/utils/recoveryItemTypePresentation';
|
||||
import { getRecoveryPointLocationEntries } from '@/utils/recoveryLocationPresentation';
|
||||
import {
|
||||
getRecoveryPointKindLabel,
|
||||
getRecoveryPointModeLabel,
|
||||
|
|
@ -159,6 +160,9 @@ export const RecoveryPointDetails: Component<RecoveryPointDetailsProps> = (props
|
|||
pairs.push({ k: 'ID', v: p.id });
|
||||
if (itemType && itemType !== 'Unknown') pairs.push({ k: 'Item Type', v: itemType });
|
||||
pairs.push({ k: 'Platform', v: providerLabel() || 'n/a' });
|
||||
for (const entry of getRecoveryPointLocationEntries(p)) {
|
||||
pairs.push({ k: entry.label, v: entry.value });
|
||||
}
|
||||
pairs.push({ k: 'Point Type', v: getRecoveryPointKindLabel(p.kind) });
|
||||
pairs.push({ k: 'Method', v: getRecoveryPointModeLabel(p.mode) });
|
||||
pairs.push({ k: 'Outcome', v: getRecoveryPointOutcomeLabel(p.outcome) });
|
||||
|
|
|
|||
|
|
@ -50,7 +50,16 @@ const pointsByRollupId: Record<string, any[]> = {
|
|||
outcome: 'success',
|
||||
completedAt: '2026-02-14T10:00:00.000Z',
|
||||
sizeBytes: 1234,
|
||||
display: { itemType: 'vm', subjectType: 'proxmox-vm' },
|
||||
cluster: 'lab-cluster',
|
||||
node: 'pve-01',
|
||||
namespace: 'finance',
|
||||
display: {
|
||||
itemType: 'vm',
|
||||
subjectType: 'proxmox-vm',
|
||||
clusterLabel: 'Lab Cluster',
|
||||
nodeHostLabel: 'pve-01',
|
||||
namespaceLabel: 'Finance',
|
||||
},
|
||||
},
|
||||
],
|
||||
'ext:truenas-1': [
|
||||
|
|
@ -337,6 +346,18 @@ describe('Recovery', () => {
|
|||
expect(detailsPanel).not.toBeNull();
|
||||
expect(within(detailsPanel as HTMLTableCellElement).getByText('Item Type')).toBeInTheDocument();
|
||||
expect(within(detailsPanel as HTMLTableCellElement).getByText('VM')).toBeInTheDocument();
|
||||
expect(
|
||||
within(detailsPanel as HTMLTableCellElement).getByText('Cluster / Site'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(detailsPanel as HTMLTableCellElement).getByText('Host / Agent'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(detailsPanel as HTMLTableCellElement).getByText('Namespace / Group'),
|
||||
).toBeInTheDocument();
|
||||
expect(within(detailsPanel as HTMLTableCellElement).getByText('Lab Cluster')).toBeInTheDocument();
|
||||
expect(within(detailsPanel as HTMLTableCellElement).getAllByText('pve-01').length).toBeGreaterThan(0);
|
||||
expect(within(detailsPanel as HTMLTableCellElement).getByText('Finance')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters protected rollups by provider', async () => {
|
||||
|
|
@ -523,7 +544,7 @@ describe('Recovery', () => {
|
|||
fireEvent.click(await screen.findByRole('tab', { name: /recovery events/i }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /^filter$/i }));
|
||||
|
||||
const clusterSelect = await screen.findByLabelText('Cluster');
|
||||
const clusterSelect = await screen.findByLabelText('Cluster / Site');
|
||||
fireEvent.change(clusterSelect, { target: { value: 'dev-cluster' } });
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -602,10 +623,10 @@ describe('Recovery', () => {
|
|||
fireEvent.click(await screen.findByRole('tab', { name: /recovery events/i }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /^filter$/i }));
|
||||
|
||||
fireEvent.change(await screen.findByLabelText('Node or agent'), {
|
||||
fireEvent.change(await screen.findByLabelText('Host / Agent'), {
|
||||
target: { value: 'node-agent-1' },
|
||||
});
|
||||
fireEvent.change(await screen.findByLabelText('Namespace'), {
|
||||
fireEvent.change(await screen.findByLabelText('Namespace / Group'), {
|
||||
target: { value: 'tenant-a' },
|
||||
});
|
||||
|
||||
|
|
@ -696,7 +717,7 @@ describe('Recovery', () => {
|
|||
await screen.findByText(/Showing 1 - 1 of 1 recovery points/i);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^filter$/i }));
|
||||
fireEvent.change(await screen.findByLabelText('Cluster'), {
|
||||
fireEvent.change(await screen.findByLabelText('Cluster / Site'), {
|
||||
target: { value: 'dev-cluster' },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface RecoveryPointDisplay {
|
|||
itemType?: string;
|
||||
isWorkload?: boolean;
|
||||
clusterLabel?: string;
|
||||
nodeHostLabel?: string;
|
||||
nodeAgentLabel?: string;
|
||||
namespaceLabel?: string;
|
||||
entityIdLabel?: string;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ describe('getRecoveryFilterChipPresentation', () => {
|
|||
expect(getRecoveryFilterChipPresentation('namespace')).toMatchObject({
|
||||
clearButtonClass:
|
||||
'rounded px-1 py-0.5 text-[10px] hover:bg-violet-100 dark:hover:bg-violet-900',
|
||||
label: 'Namespace',
|
||||
label: 'Namespace / Group',
|
||||
});
|
||||
expect(getRecoveryFilterChipPresentation('namespace').className).toContain('border-violet-200');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getRecoveryLocationFacetAllLabel,
|
||||
getRecoveryLocationFacetLabel,
|
||||
getRecoveryPointLocationEntries,
|
||||
} from '@/utils/recoveryLocationPresentation';
|
||||
|
||||
describe('recoveryLocationPresentation', () => {
|
||||
it('returns platform-neutral placement labels for recovery facets', () => {
|
||||
expect(getRecoveryLocationFacetLabel('cluster')).toBe('Cluster / Site');
|
||||
expect(getRecoveryLocationFacetLabel('node')).toBe('Host / Agent');
|
||||
expect(getRecoveryLocationFacetLabel('namespace')).toBe('Namespace / Group');
|
||||
expect(getRecoveryLocationFacetAllLabel('cluster')).toBe('Any cluster or site');
|
||||
expect(getRecoveryLocationFacetAllLabel('node')).toBe('Any host or agent');
|
||||
expect(getRecoveryLocationFacetAllLabel('namespace')).toBe('Any namespace or group');
|
||||
});
|
||||
|
||||
it('builds recovery point placement entries from the canonical display contract', () => {
|
||||
expect(
|
||||
getRecoveryPointLocationEntries({
|
||||
id: 'p1',
|
||||
provider: 'proxmox-pve',
|
||||
kind: 'backup',
|
||||
mode: 'local',
|
||||
outcome: 'success',
|
||||
cluster: 'cluster-a',
|
||||
node: 'node-a',
|
||||
namespace: 'tenant-a',
|
||||
display: {
|
||||
clusterLabel: 'Lab Cluster',
|
||||
nodeHostLabel: 'pve-01',
|
||||
namespaceLabel: 'Tenant A',
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
{ key: 'cluster', label: 'Cluster / Site', value: 'Lab Cluster' },
|
||||
{ key: 'node', label: 'Host / Agent', value: 'pve-01' },
|
||||
{ key: 'namespace', label: 'Namespace / Group', value: 'Tenant A' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -43,9 +43,15 @@ describe('recoveryTablePresentation', () => {
|
|||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.type).toBe('Item Type');
|
||||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.subject).toBe('Item');
|
||||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.source).toBe('Platform');
|
||||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.cluster).toBe('Cluster / Site');
|
||||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.nodeAgent).toBe('Host / Agent');
|
||||
expect(RECOVERY_ARTIFACT_COLUMN_LABELS.namespace).toBe('Namespace / Group');
|
||||
expect(getRecoveryArtifactColumnLabel('type', 'Type')).toBe('Item Type');
|
||||
expect(getRecoveryArtifactColumnLabel('subject', 'Subject')).toBe('Item');
|
||||
expect(getRecoveryArtifactColumnLabel('source', 'Source')).toBe('Platform');
|
||||
expect(getRecoveryArtifactColumnLabel('cluster', 'Cluster')).toBe('Cluster / Site');
|
||||
expect(getRecoveryArtifactColumnLabel('nodeAgent', 'Node')).toBe('Host / Agent');
|
||||
expect(getRecoveryArtifactColumnLabel('namespace', 'Namespace')).toBe('Namespace / Group');
|
||||
expect(getRecoveryArtifactColumnLabel('outcome', 'Outcome')).toBe('Outcome');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { getRecoveryLocationFacetLabel } from '@/utils/recoveryLocationPresentation';
|
||||
|
||||
export type RecoveryFilterChipKind = 'day' | 'cluster' | 'item-type' | 'node' | 'namespace';
|
||||
|
||||
type RecoveryFilterChipPresentation = {
|
||||
|
|
@ -13,7 +15,7 @@ const CHIP_PRESENTATION: Record<RecoveryFilterChipKind, RecoveryFilterChipPresen
|
|||
cluster: {
|
||||
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-cyan-100 dark:hover:bg-cyan-900`,
|
||||
className: `${CHIP_BASE_CLASS} border-cyan-200 bg-cyan-50 text-cyan-700 dark:border-cyan-700 dark:bg-cyan-900 dark:text-cyan-200`,
|
||||
label: 'Cluster',
|
||||
label: getRecoveryLocationFacetLabel('cluster'),
|
||||
},
|
||||
day: {
|
||||
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-blue-100 dark:hover:bg-blue-900`,
|
||||
|
|
@ -28,12 +30,12 @@ const CHIP_PRESENTATION: Record<RecoveryFilterChipKind, RecoveryFilterChipPresen
|
|||
namespace: {
|
||||
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-violet-100 dark:hover:bg-violet-900`,
|
||||
className: `${CHIP_BASE_CLASS} border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-700 dark:bg-violet-900 dark:text-violet-200`,
|
||||
label: 'Namespace',
|
||||
label: getRecoveryLocationFacetLabel('namespace'),
|
||||
},
|
||||
node: {
|
||||
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-emerald-100 dark:hover:bg-emerald-900`,
|
||||
className: `${CHIP_BASE_CLASS} border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-700 dark:bg-emerald-900 dark:text-emerald-200`,
|
||||
label: 'Node/Agent',
|
||||
label: getRecoveryLocationFacetLabel('node'),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
50
frontend-modern/src/utils/recoveryLocationPresentation.ts
Normal file
50
frontend-modern/src/utils/recoveryLocationPresentation.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { RecoveryPoint } from '@/types/recovery';
|
||||
|
||||
export type RecoveryLocationFacetKind = 'cluster' | 'node' | 'namespace';
|
||||
|
||||
interface RecoveryLocationFacetPresentation {
|
||||
allLabel: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const RECOVERY_LOCATION_FACET_PRESENTATION: Record<
|
||||
RecoveryLocationFacetKind,
|
||||
RecoveryLocationFacetPresentation
|
||||
> = {
|
||||
cluster: {
|
||||
allLabel: 'Any cluster or site',
|
||||
label: 'Cluster / Site',
|
||||
},
|
||||
node: {
|
||||
allLabel: 'Any host or agent',
|
||||
label: 'Host / Agent',
|
||||
},
|
||||
namespace: {
|
||||
allLabel: 'Any namespace or group',
|
||||
label: 'Namespace / Group',
|
||||
},
|
||||
};
|
||||
|
||||
export function getRecoveryLocationFacetLabel(kind: RecoveryLocationFacetKind): string {
|
||||
return RECOVERY_LOCATION_FACET_PRESENTATION[kind].label;
|
||||
}
|
||||
|
||||
export function getRecoveryLocationFacetAllLabel(kind: RecoveryLocationFacetKind): string {
|
||||
return RECOVERY_LOCATION_FACET_PRESENTATION[kind].allLabel;
|
||||
}
|
||||
|
||||
export function getRecoveryPointLocationEntries(
|
||||
point: RecoveryPoint,
|
||||
): Array<{ key: RecoveryLocationFacetKind; label: string; value: string }> {
|
||||
const cluster = String(point.display?.clusterLabel || point.cluster || '').trim();
|
||||
const node = String(
|
||||
point.display?.nodeHostLabel || point.display?.nodeAgentLabel || point.node || '',
|
||||
).trim();
|
||||
const namespace = String(point.display?.namespaceLabel || point.namespace || '').trim();
|
||||
|
||||
return [
|
||||
{ key: 'cluster', label: getRecoveryLocationFacetLabel('cluster'), value: cluster },
|
||||
{ key: 'node', label: getRecoveryLocationFacetLabel('node'), value: node },
|
||||
{ key: 'namespace', label: getRecoveryLocationFacetLabel('namespace'), value: namespace },
|
||||
].filter((entry) => entry.value !== '');
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import {
|
|||
getRecoveryItemTypeBadgeClass,
|
||||
getRecoveryItemTypeLabel,
|
||||
} from '@/utils/recoveryItemTypePresentation';
|
||||
import { getRecoveryLocationFacetLabel } from '@/utils/recoveryLocationPresentation';
|
||||
import { normalizeRecoveryOutcome } from '@/utils/recoveryOutcomePresentation';
|
||||
import type { RecoveryIssueTone } from '@/utils/recoveryIssuePresentation';
|
||||
|
||||
|
|
@ -20,6 +21,9 @@ 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> = {
|
||||
cluster: getRecoveryLocationFacetLabel('cluster'),
|
||||
nodeAgent: getRecoveryLocationFacetLabel('node'),
|
||||
namespace: getRecoveryLocationFacetLabel('namespace'),
|
||||
type: 'Item Type',
|
||||
subject: 'Item',
|
||||
source: 'Platform',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue