diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index d541c2939..ae7c036ed 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -31,18 +31,20 @@ querying, and the operator-facing storage health presentation layer. 6. `frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx` 7. `frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx` 8. `frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx` -9. `frontend-modern/src/components/Storage/Storage.tsx` -10. `frontend-modern/src/features/storageBackups/storageModelCore.ts` -11. `frontend-modern/src/hooks/useRecoveryPoints.ts` -12. `frontend-modern/src/hooks/useRecoveryRollups.ts` -13. `frontend-modern/src/pages/RecoveryRoute.tsx` -14. `frontend-modern/src/pages/Dashboard.tsx` -15. `frontend-modern/src/pages/DashboardPanels/dashboardWidgets.ts` -16. `frontend-modern/src/pages/DashboardPanels/RecoveryStatusPanel.tsx` -17. `frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx` -18. `frontend-modern/src/types/recovery.ts` -19. `frontend-modern/src/utils/recoveryTablePresentation.ts` -20. `frontend-modern/src/utils/textPresentation.ts` +9. `frontend-modern/src/components/Recovery/RecoveryHistoryTable.tsx` +10. `frontend-modern/src/components/Recovery/useRecoveryHistorySectionState.ts` +11. `frontend-modern/src/components/Storage/Storage.tsx` +12. `frontend-modern/src/features/storageBackups/storageModelCore.ts` +13. `frontend-modern/src/hooks/useRecoveryPoints.ts` +14. `frontend-modern/src/hooks/useRecoveryRollups.ts` +15. `frontend-modern/src/pages/RecoveryRoute.tsx` +16. `frontend-modern/src/pages/Dashboard.tsx` +17. `frontend-modern/src/pages/DashboardPanels/dashboardWidgets.ts` +18. `frontend-modern/src/pages/DashboardPanels/RecoveryStatusPanel.tsx` +19. `frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx` +20. `frontend-modern/src/types/recovery.ts` +21. `frontend-modern/src/utils/recoveryTablePresentation.ts` +22. `frontend-modern/src/utils/textPresentation.ts` ## Shared Boundaries @@ -113,7 +115,10 @@ canonical route parsing, filter/query state, transport hook inputs, and URL synchronization, while `frontend-modern/src/components/Recovery/Recovery.tsx` is the composition root for the operator-facing recovery surface and the split section owners under `frontend-modern/src/components/Recovery/` hold the -protected inventory, activity, and history presentation layers. +protected inventory, activity, and history presentation layers. The history +surface is further split so `RecoveryHistorySection.tsx` owns the toolbar and +controller boundary, `useRecoveryHistorySectionState.ts` owns local section UI +state, and `RecoveryHistoryTable.tsx` owns the row/detail renderer. That same shared `internal/api/` dependency now also assumes tenant-scoped resource handlers seed registries from canonical unified resources only: recovery- and storage-adjacent API helpers may not fall back to raw tenant diff --git a/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx index 9a33f49b7..8d5db6b21 100644 --- a/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx @@ -1,14 +1,7 @@ -import { - For, - Show, - createEffect, - createSignal, - onCleanup, -} from 'solid-js'; +import { For, Show } from 'solid-js'; import type { Accessor, Component } from 'solid-js'; import { Card } from '@/components/shared/Card'; -import { EmptyState } from '@/components/shared/EmptyState'; import { FilterActionButton, FilterToolbarPanel, @@ -19,77 +12,31 @@ import { } from '@/components/shared/FilterToolbar'; import { PageControls } from '@/components/shared/PageControls'; import { SearchInput } from '@/components/shared/SearchInput'; -import { getSourcePlatformBadge } from '@/components/shared/sourcePlatformBadges'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/shared/Table'; import type { ColumnDef } from '@/hooks/useColumnVisibility'; import { STORAGE_KEYS } from '@/utils/localStorage'; -import type { RecoveryOutcome, RecoveryPoint } from '@/types/recovery'; +import type { RecoveryOutcome } from '@/types/recovery'; import type { Resource } from '@/types/resource'; -import { getRecoveryEmptyStateActionClass, getRecoveryFilterPanelClearClass, getRecoveryDrawerCloseButtonClass } from '@/utils/recoveryActionPresentation'; +import { getRecoveryFilterPanelClearClass } from '@/utils/recoveryActionPresentation'; import { getRecoveryArtifactModePresentation, type RecoveryArtifactMode } from '@/utils/recoveryArtifactModePresentation'; +import { normalizeRecoveryModeQueryValue } from '@/utils/recoveryRecordPresentation'; import { - getRecoveryHistoryEmptyState, - getRecoveryPointsLoadingState, -} from '@/utils/recoveryEmptyStatePresentation'; -import { - getRecoveryPointDetailsSummary, - getRecoveryPointRepositoryLabel, - getRecoveryPointSubjectLabel, - getRecoveryPointTimestampMs, - normalizeRecoveryModeQueryValue, -} from '@/utils/recoveryRecordPresentation'; -import { - getRecoveryArtifactColumnHeaderClass, - getRecoveryArtifactRowClass, - getRecoveryEventTimeTextClass, getRecoveryHistorySearchPlaceholder, getRecoverySearchHistoryEmptyMessage, - getRecoverySubjectTypeBadgeClass, - getRecoverySubjectTypeLabel, RECOVERY_ADVANCED_FILTER_FIELD_CLASS, RECOVERY_ADVANCED_FILTER_LABEL_CLASS, - RECOVERY_GROUP_HEADER_ROW_CLASS, - RECOVERY_GROUP_HEADER_TEXT_CLASS, } from '@/utils/recoveryTablePresentation'; -import { getRecoveryOutcomeBadgeClass } from '@/utils/recoveryOutcomePresentation'; import { titleCaseDelimitedLabel } from '@/utils/textPresentation'; -import { formatBytes } from '@/utils/format'; -import { formatRecoveryTimeOnly } from '@/utils/recoveryDatePresentation'; import { normalizeSourcePlatformQueryValue, getSourcePlatformLabel } from '@/utils/sourcePlatforms'; -import { RecoveryPointDetails } from '@/components/Recovery/RecoveryPointDetails'; +import { + RecoveryHistoryTable, + type RecoveryPointGroup, + type RecoveryPointsModel, +} from '@/components/Recovery/RecoveryHistoryTable'; +import { useRecoveryHistorySectionState } from '@/components/Recovery/useRecoveryHistorySectionState'; type ArtifactMode = RecoveryArtifactMode; type VerificationFilter = 'all' | 'verified' | 'unverified' | 'unknown'; -interface RecoveryPointGroup { - key: string; - label: string; - tone: 'recent' | 'default'; - items: RecoveryPoint[]; -} - -interface RecoveryPointsMeta { - page: number; - limit: number; - total: number; - totalPages: number; -} - -interface RecoveryPointsModel { - meta: Accessor; - response: { - loading: boolean; - error: unknown; - }; -} - interface PageControlsColumnVisibility { availableToggles: () => ColumnDef[]; isHiddenByUser: (id: string) => boolean; @@ -144,58 +91,28 @@ interface RecoveryHistorySectionProps { } export const RecoveryHistorySection: Component = (props) => { - const [selectedPoint, setSelectedPoint] = createSignal(null); - const [moreFiltersOpen, setMoreFiltersOpen] = createSignal(false); - const [historyFiltersOpen, setHistoryFiltersOpen] = createSignal(false); - let advancedFiltersPanelRef: HTMLDivElement | undefined; - let advancedFiltersButtonRef: HTMLButtonElement | undefined; - - const historyActiveFilterCount = () => { - let count = 0; - if (props.queryFilter().trim() !== '') count += 1; - if (props.providerFilter() !== 'all') count += 1; - if (props.historyOutcomeFilter() !== 'all') count += 1; - if (props.scopeFilter() !== 'all') count += 1; - if (props.modeFilter() !== 'all') count += 1; - if (props.verificationFilter() !== 'all') count += 1; - if (props.clusterFilter() !== 'all') count += 1; - if (props.nodeFilter() !== 'all') count += 1; - if (props.namespaceFilter() !== 'all') count += 1; - return count; - }; - - createEffect(() => { - props.currentPage(); - props.providerFilter(); - props.historyOutcomeFilter(); - props.scopeFilter(); - props.modeFilter(); - props.verificationFilter(); - props.clusterFilter(); - props.nodeFilter(); - props.namespaceFilter(); - props.queryFilter(); - setSelectedPoint(null); - }); - - const handleAdvancedFiltersClickOutside = (event: MouseEvent) => { - const target = event.target as Node; - if (advancedFiltersPanelRef?.contains(target) || advancedFiltersButtonRef?.contains(target)) { - return; - } - setMoreFiltersOpen(false); - }; - - createEffect(() => { - if (moreFiltersOpen()) { - document.addEventListener('mousedown', handleAdvancedFiltersClickOutside); - } else { - document.removeEventListener('mousedown', handleAdvancedFiltersClickOutside); - } - }); - - onCleanup(() => { - document.removeEventListener('mousedown', handleAdvancedFiltersClickOutside); + const { + advancedFiltersButtonRef, + advancedFiltersPanelRef, + clearSelectedPoint, + historyActiveFilterCount, + historyFiltersOpen, + moreFiltersOpen, + selectedPoint, + setHistoryFiltersOpen, + setMoreFiltersOpen, + toggleSelectedPoint, + } = useRecoveryHistorySectionState({ + clusterFilter: props.clusterFilter, + currentPage: props.currentPage, + historyOutcomeFilter: props.historyOutcomeFilter, + modeFilter: props.modeFilter, + namespaceFilter: props.namespaceFilter, + nodeFilter: props.nodeFilter, + providerFilter: props.providerFilter, + queryFilter: props.queryFilter, + scopeFilter: props.scopeFilter, + verificationFilter: props.verificationFilter, }); return ( @@ -449,414 +366,22 @@ export const RecoveryHistorySection: Component = (p - - 0} - fallback={ -
- - - - } - /> - } - > -
{getRecoveryPointsLoadingState().text}
- -
- } - > -
- - - - - {(column) => ( - - {column.label} - - )} - - - - - - {(group) => ( - <> - - -
-
- - {group.label} - - - - recent - - -
- - {group.items.length} - -
-
-
- - - {(point) => { - const resourceIndex = props.resourcesById(); - const subject = getRecoveryPointSubjectLabel(point, resourceIndex); - const tsMs = getRecoveryPointTimestampMs(point); - const timeOnly = - point.completedAt && Number.isFinite(tsMs) - ? formatRecoveryTimeOnly(tsMs) - : '—'; - const subjectType = getRecoverySubjectTypeLabel(point); - const provider = String(point.provider || '').trim(); - const mode = - (normalizeRecoveryModeQueryValue(String(point.mode || '').toLowerCase()) as ArtifactMode) || - 'local'; - const outcome = (String(point.outcome || 'unknown').toLowerCase() as RecoveryOutcome) || 'unknown'; - const repoLabel = getRecoveryPointRepositoryLabel(point); - const detailsSummary = getRecoveryPointDetailsSummary(point); - const entityId = String(point.entityId || '').trim(); - const cluster = String(point.cluster || '').trim(); - const nodeAgent = String(point.node || '').trim(); - const namespace = String(point.namespace || '').trim(); - - return ( - <> - setSelectedPoint(selectedPoint()?.id === point.id ? null : point)} - > - - {(column) => { - switch (column.id) { - case 'time': - return ( - - {timeOnly} - - ); - case 'type': - return ( - - —}> - - {subjectType} - - - - ); - case 'subject': - return ( - -
- - {subject} - - - - - - - - - -
-
- ); - case 'entityId': - return ( - - {entityId || '—'} - - ); - case 'cluster': - return ( - - {cluster || '—'} - - ); - case 'nodeAgent': - return ( - - {nodeAgent || '—'} - - ); - case 'namespace': - return ( - - {namespace || '—'} - - ); - case 'source': { - const badge = getSourcePlatformBadge(provider); - return ( - - - {badge?.label || getSourcePlatformLabel(provider)} - - - ); - } - case 'verified': - return ( - - {typeof point.verified === 'boolean' ? ( - point.verified ? ( - - - - - - ) : ( - - - - - - ) - ) : ( - — - )} - - ); - case 'size': - return ( - - {point.sizeBytes && point.sizeBytes > 0 - ? formatBytes(point.sizeBytes) - : '—'} - - ); - case 'method': - return ( - - - {getRecoveryArtifactModePresentation(mode).label} - - - ); - case 'repository': - return ( - - {repoLabel || '—'} - - ); - case 'details': - return ( - - {detailsSummary || '—'} - - ); - case 'outcome': - return ( - - - {titleCaseDelimitedLabel(outcome)} - - - ); - default: - return ( - - - - - ); - } - }} -
-
- - - - -
-

- Recovery Point Details -

- -
-
- -
-
-
-
- - ); - }} -
- - )} -
-
-
-
- -
-
- 0} - fallback={Showing 0 of 0 recovery points} - > - - Showing {(props.recoveryPoints.meta().page - 1) * props.recoveryPoints.meta().limit + 1} -{' '} - {Math.min( - props.recoveryPoints.meta().page * props.recoveryPoints.meta().limit, - props.recoveryPoints.meta().total, - )}{' '} - of {props.recoveryPoints.meta().total} recovery points - - -
-
- - - Page {props.currentPage()} / {props.totalPages()} - - -
-
-
+ ); }; diff --git a/frontend-modern/src/components/Recovery/RecoveryHistoryTable.tsx b/frontend-modern/src/components/Recovery/RecoveryHistoryTable.tsx new file mode 100644 index 000000000..ec141f269 --- /dev/null +++ b/frontend-modern/src/components/Recovery/RecoveryHistoryTable.tsx @@ -0,0 +1,501 @@ +import { For, Show } from 'solid-js'; +import type { Accessor, Component } from 'solid-js'; + +import { RecoveryPointDetails } from '@/components/Recovery/RecoveryPointDetails'; +import { EmptyState } from '@/components/shared/EmptyState'; +import { getSourcePlatformBadge } from '@/components/shared/sourcePlatformBadges'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/shared/Table'; +import type { ColumnDef } from '@/hooks/useColumnVisibility'; +import { formatBytes } from '@/utils/format'; +import { getRecoveryDrawerCloseButtonClass, getRecoveryEmptyStateActionClass } from '@/utils/recoveryActionPresentation'; +import { getRecoveryArtifactModePresentation, type RecoveryArtifactMode } from '@/utils/recoveryArtifactModePresentation'; +import { + getRecoveryHistoryEmptyState, + getRecoveryPointsLoadingState, +} from '@/utils/recoveryEmptyStatePresentation'; +import { getRecoveryOutcomeBadgeClass } from '@/utils/recoveryOutcomePresentation'; +import { + getRecoveryPointDetailsSummary, + getRecoveryPointRepositoryLabel, + getRecoveryPointSubjectLabel, + getRecoveryPointTimestampMs, + normalizeRecoveryModeQueryValue, +} from '@/utils/recoveryRecordPresentation'; +import { + getRecoveryArtifactColumnHeaderClass, + getRecoveryArtifactRowClass, + getRecoveryEventTimeTextClass, + getRecoverySubjectTypeBadgeClass, + getRecoverySubjectTypeLabel, + RECOVERY_GROUP_HEADER_ROW_CLASS, + RECOVERY_GROUP_HEADER_TEXT_CLASS, +} from '@/utils/recoveryTablePresentation'; +import { normalizeSourcePlatformQueryValue, getSourcePlatformLabel } from '@/utils/sourcePlatforms'; +import { titleCaseDelimitedLabel } from '@/utils/textPresentation'; +import type { RecoveryOutcome, RecoveryPoint } from '@/types/recovery'; +import type { Resource } from '@/types/resource'; +import { formatRecoveryTimeOnly } from '@/utils/recoveryDatePresentation'; + +type ArtifactMode = RecoveryArtifactMode; + +export interface RecoveryPointGroup { + key: string; + label: string; + tone: 'recent' | 'default'; + items: RecoveryPoint[]; +} + +interface RecoveryPointsMeta { + page: number; + limit: number; + total: number; + totalPages: number; +} + +export interface RecoveryPointsModel { + meta: Accessor; + response: { + loading: boolean; + error: unknown; + }; +} + +interface RecoveryHistoryTableProps { + currentPage: Accessor; + groupedByDay: Accessor; + hasActiveArtifactFilters: Accessor; + mobileVisibleArtifactColumns: Accessor; + recoveryPoints: RecoveryPointsModel; + resetAllArtifactFilters: () => void; + resourcesById: Accessor>; + selectedPoint: Accessor; + setCurrentPage: (value: number) => void; + tableColumnCount: Accessor; + tableMinWidth: Accessor; + toggleSelectedPoint: (point: RecoveryPoint) => void; + totalPages: Accessor; + clearSelectedPoint: () => void; +} + +export const RecoveryHistoryTable: Component = (props) => ( + 0} + fallback={ +
+ + + + } + /> + } + > +
{getRecoveryPointsLoadingState().text}
+ +
+ } + > +
+ + + + + {(column) => ( + + {column.label} + + )} + + + + + + {(group) => ( + <> + + +
+
+ + {group.label} + + + + recent + + +
+ + {group.items.length} + +
+
+
+ + + {(point) => { + const resourceIndex = props.resourcesById(); + const subject = getRecoveryPointSubjectLabel(point, resourceIndex); + const tsMs = getRecoveryPointTimestampMs(point); + const timeOnly = + point.completedAt && Number.isFinite(tsMs) + ? formatRecoveryTimeOnly(tsMs) + : '—'; + const subjectType = getRecoverySubjectTypeLabel(point); + const provider = normalizeSourcePlatformQueryValue( + String(point.provider || '').trim(), + ); + const mode = + normalizeRecoveryModeQueryValue(String(point.mode || '').toLowerCase()) || + 'local'; + const outcome = + (String(point.outcome || 'unknown').toLowerCase() as RecoveryOutcome) || + 'unknown'; + const repoLabel = getRecoveryPointRepositoryLabel(point); + const detailsSummary = getRecoveryPointDetailsSummary(point); + const entityId = String(point.entityId || '').trim(); + const cluster = String(point.cluster || '').trim(); + const nodeAgent = String(point.node || '').trim(); + const namespace = String(point.namespace || '').trim(); + + return ( + <> + props.toggleSelectedPoint(point)} + > + + {(column) => { + switch (column.id) { + case 'time': + return ( + + {timeOnly} + + ); + case 'type': + return ( + + —}> + + {subjectType} + + + + ); + case 'subject': + return ( + +
+ + {subject} + + + + + + + + + +
+
+ ); + case 'entityId': + return ( + + {entityId || '—'} + + ); + case 'cluster': + return ( + + {cluster || '—'} + + ); + case 'nodeAgent': + return ( + + {nodeAgent || '—'} + + ); + case 'namespace': + return ( + + {namespace || '—'} + + ); + case 'source': { + const badge = getSourcePlatformBadge(provider); + return ( + + + {badge?.label || getSourcePlatformLabel(provider)} + + + ); + } + case 'verified': + return ( + + {typeof point.verified === 'boolean' ? ( + point.verified ? ( + + + + + + ) : ( + + + + + + ) + ) : ( + — + )} + + ); + case 'size': + return ( + + {point.sizeBytes && point.sizeBytes > 0 + ? formatBytes(point.sizeBytes) + : '—'} + + ); + case 'method': + return ( + + + {getRecoveryArtifactModePresentation(mode as ArtifactMode).label} + + + ); + case 'repository': + return ( + + {repoLabel || '—'} + + ); + case 'details': + return ( + + {detailsSummary || '—'} + + ); + case 'outcome': + return ( + + + {titleCaseDelimitedLabel(outcome)} + + + ); + default: + return ( + + - + + ); + } + }} +
+
+ + + + +
+

+ Recovery Point Details +

+ +
+
+ +
+
+
+
+ + ); + }} +
+ + )} +
+
+
+
+ +
+
+ 0} + fallback={Showing 0 of 0 recovery points} + > + + Showing {(props.recoveryPoints.meta().page - 1) * props.recoveryPoints.meta().limit + 1} -{' '} + {Math.min( + props.recoveryPoints.meta().page * props.recoveryPoints.meta().limit, + props.recoveryPoints.meta().total, + )}{' '} + of {props.recoveryPoints.meta().total} recovery points + + +
+
+ + + Page {props.currentPage()} / {props.totalPages()} + + +
+
+
+); diff --git a/frontend-modern/src/components/Recovery/useRecoveryHistorySectionState.ts b/frontend-modern/src/components/Recovery/useRecoveryHistorySectionState.ts new file mode 100644 index 000000000..37b253db9 --- /dev/null +++ b/frontend-modern/src/components/Recovery/useRecoveryHistorySectionState.ts @@ -0,0 +1,97 @@ +import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'; +import type { Accessor } from 'solid-js'; + +import type { RecoveryOutcome, RecoveryPoint } from '@/types/recovery'; +import type { RecoveryArtifactMode } from '@/utils/recoveryArtifactModePresentation'; + +type ArtifactMode = RecoveryArtifactMode; +type VerificationFilter = 'all' | 'verified' | 'unverified' | 'unknown'; + +interface UseRecoveryHistorySectionStateParams { + clusterFilter: Accessor; + currentPage: Accessor; + historyOutcomeFilter: Accessor<'all' | RecoveryOutcome>; + modeFilter: Accessor<'all' | ArtifactMode>; + namespaceFilter: Accessor; + nodeFilter: Accessor; + providerFilter: Accessor; + queryFilter: Accessor; + scopeFilter: Accessor<'all' | 'workload'>; + verificationFilter: Accessor; +} + +export function useRecoveryHistorySectionState( + params: UseRecoveryHistorySectionStateParams, +) { + const [selectedPoint, setSelectedPoint] = createSignal(null); + const [moreFiltersOpen, setMoreFiltersOpen] = createSignal(false); + const [historyFiltersOpen, setHistoryFiltersOpen] = createSignal(false); + let advancedFiltersPanelRef: HTMLDivElement | undefined; + let advancedFiltersButtonRef: HTMLButtonElement | undefined; + + const historyActiveFilterCount = createMemo(() => { + let count = 0; + if (params.queryFilter().trim() !== '') count += 1; + if (params.providerFilter() !== 'all') count += 1; + if (params.historyOutcomeFilter() !== 'all') count += 1; + if (params.scopeFilter() !== 'all') count += 1; + if (params.modeFilter() !== 'all') count += 1; + if (params.verificationFilter() !== 'all') count += 1; + if (params.clusterFilter() !== 'all') count += 1; + if (params.nodeFilter() !== 'all') count += 1; + if (params.namespaceFilter() !== 'all') count += 1; + return count; + }); + + createEffect(() => { + params.currentPage(); + params.providerFilter(); + params.historyOutcomeFilter(); + params.scopeFilter(); + params.modeFilter(); + params.verificationFilter(); + params.clusterFilter(); + params.nodeFilter(); + params.namespaceFilter(); + params.queryFilter(); + setSelectedPoint(null); + }); + + const handleAdvancedFiltersClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if (advancedFiltersPanelRef?.contains(target) || advancedFiltersButtonRef?.contains(target)) { + return; + } + setMoreFiltersOpen(false); + }; + + createEffect(() => { + if (moreFiltersOpen()) { + document.addEventListener('mousedown', handleAdvancedFiltersClickOutside); + } else { + document.removeEventListener('mousedown', handleAdvancedFiltersClickOutside); + } + }); + + onCleanup(() => { + document.removeEventListener('mousedown', handleAdvancedFiltersClickOutside); + }); + + return { + advancedFiltersButtonRef: (element: HTMLButtonElement) => { + advancedFiltersButtonRef = element; + }, + advancedFiltersPanelRef: (element: HTMLDivElement) => { + advancedFiltersPanelRef = element; + }, + clearSelectedPoint: () => setSelectedPoint(null), + historyActiveFilterCount, + historyFiltersOpen, + moreFiltersOpen, + selectedPoint, + setHistoryFiltersOpen, + setMoreFiltersOpen, + toggleSelectedPoint: (point: RecoveryPoint) => + setSelectedPoint((current) => (current?.id === point.id ? null : point)), + }; +} diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index 15350e96f..1d256cd47 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -71,7 +71,9 @@ import resourceBadgePresentationSource from '@/utils/resourceBadgePresentation.t import recoveryComponentSource from '@/components/Recovery/Recovery.tsx?raw'; import recoveryActivitySectionSource from '@/components/Recovery/RecoveryActivitySection.tsx?raw'; import recoveryHistorySectionSource from '@/components/Recovery/RecoveryHistorySection.tsx?raw'; +import recoveryHistoryTableSource from '@/components/Recovery/RecoveryHistoryTable.tsx?raw'; import recoveryProtectedInventorySectionSource from '@/components/Recovery/RecoveryProtectedInventorySection.tsx?raw'; +import recoveryHistorySectionStateSource from '@/components/Recovery/useRecoveryHistorySectionState.ts?raw'; import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.ts?raw'; import problemResourcesTableSource from '@/pages/DashboardPanels/ProblemResourcesTable.tsx?raw'; import workloadTypeBadgesSource from '@/components/shared/workloadTypeBadges.ts?raw'; @@ -110,6 +112,8 @@ const recoverySource = [ recoveryProtectedInventorySectionSource, recoveryActivitySectionSource, recoveryHistorySectionSource, + recoveryHistoryTableSource, + recoveryHistorySectionStateSource, ].join('\n'); describe('monitored-system model guardrails', () => { diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 46718bfcd..aa7a21214 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -30,7 +30,9 @@ import recoverySummarySource from '@/components/Recovery/RecoverySummary.tsx?raw import recoveryComponentSource from '@/components/Recovery/Recovery.tsx?raw'; import recoveryActivitySectionSource from '@/components/Recovery/RecoveryActivitySection.tsx?raw'; import recoveryHistorySectionSource from '@/components/Recovery/RecoveryHistorySection.tsx?raw'; +import recoveryHistoryTableSource from '@/components/Recovery/RecoveryHistoryTable.tsx?raw'; import recoveryProtectedInventorySectionSource from '@/components/Recovery/RecoveryProtectedInventorySection.tsx?raw'; +import recoveryHistorySectionStateSource from '@/components/Recovery/useRecoveryHistorySectionState.ts?raw'; import recoverySurfaceStateSource from '@/features/recovery/useRecoverySurfaceState.ts?raw'; import dashboardRecoverySource from '@/hooks/useDashboardRecovery.ts?raw'; import recoveryOutcomePresentationSource from '@/utils/recoveryOutcomePresentation.ts?raw'; @@ -267,6 +269,8 @@ const recoverySource = [ recoveryProtectedInventorySectionSource, recoveryActivitySectionSource, recoveryHistorySectionSource, + recoveryHistoryTableSource, + recoveryHistorySectionStateSource, ].join('\n'); import rolesPanelSource from '@/components/Settings/RolesPanel.tsx?raw'; import auditWebhookPanelSource from '@/components/Settings/AuditWebhookPanel.tsx?raw'; @@ -429,6 +433,11 @@ describe('frontend resource type boundaries', () => { "import { useRecoverySurfaceState } from '@/features/recovery/useRecoverySurfaceState';", ); expect(recoveryComponentSource).toContain('useRecoverySurfaceState'); + expect(recoveryHistorySectionSource).toContain('useRecoveryHistorySectionState'); + expect(recoveryHistorySectionSource).toContain('RecoveryHistoryTable'); + expect(recoveryHistorySectionStateSource).toContain( + 'export function useRecoveryHistorySectionState', + ); expect(recoverySource).not.toContain('parseRecoveryLinkSearch'); expect(recoverySource).not.toContain('buildRecoveryPath'); expect(recoverySource).not.toContain('useRecoveryRollups');