From 5d31ce45abc62bf8453c17e9669e2e2f159b3d29 Mon Sep 17 00:00:00 2001 From: jbellister-slac Date: Mon, 2 Mar 2026 23:34:15 -0800 Subject: [PATCH 1/4] ENH: Connect frontend snapshot restore functionality to the backend api --- src/pages/SnapshotDetailsPage.tsx | 6 +++--- src/routes/snapshot-details.tsx | 31 +++++++++++++++++++++++++++---- src/services/snapshotService.ts | 25 ++++++++++++++++++++++++- src/types/api.ts | 17 +++++++++++++++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/pages/SnapshotDetailsPage.tsx b/src/pages/SnapshotDetailsPage.tsx index b05d088..ebe4bef 100644 --- a/src/pages/SnapshotDetailsPage.tsx +++ b/src/pages/SnapshotDetailsPage.tsx @@ -25,7 +25,7 @@ import { SnapshotSummaryDTO } from '../types/api'; interface SnapshotDetailsPageProps { snapshot: Snapshot | null; onBack: () => void; - onRestore?: (pvs: PV[]) => void; + onRestore?: (pvs: PV[]) => Promise; onCompare?: (comparisonSnapshotId: string) => void; } @@ -127,9 +127,9 @@ export function SnapshotDetailsPage({ setShowRestoreDialog(true); }; - const confirmRestore = () => { + const confirmRestore = async () => { if (onRestore) { - onRestore(pvsToRestore); + await onRestore(pvsToRestore); } setShowRestoreDialog(false); }; diff --git a/src/routes/snapshot-details.tsx b/src/routes/snapshot-details.tsx index 51543c0..ab21949 100644 --- a/src/routes/snapshot-details.tsx +++ b/src/routes/snapshot-details.tsx @@ -3,6 +3,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { useQueryClient } from '@tanstack/react-query'; import { Box, CircularProgress, Typography, Button } from '@mui/material'; import { SnapshotDetailsPage } from '../pages'; +import { snapshotService } from '../services/snapshotService'; import { Snapshot, PV, Severity, Status } from '../types'; import { useSnapshot, snapshotKeys } from '../hooks'; import { SnapshotDTO, PVValueDTO } from '../types/api'; @@ -108,10 +109,32 @@ function SnapshotDetails() { navigate({ to: '/snapshots' }); }, [navigate, queryClient]); - const handleRestore = useCallback((pvs: PV[]) => { - // eslint-disable-next-line no-alert - alert(`Restoring ${pvs.length} PV(s) - This feature is not yet implemented`); - }, []); + const handleRestore = useCallback( + async (pvs: PV[]) => { + if (!id) return; + + try { + // If user selected PVs, restore only those. If not, restore all + const request = pvs.length > 0 ? { pvIds: pvs.map((pv) => pv.uuid) } : undefined; + + const result = await snapshotService.restoreSnapshot(id, request); + + console.log('Restore successful:', result); + + // Optional: temporary visual feedback + alert( + `Restore completed:\n` + + `${result.successCount}/${result.totalPVs} succeeded\n` + + `${result.failureCount} failed` + ); + } catch (err) { + console.error('Failed to restore PVs:', err); + + alert(err instanceof Error ? `Restore failed: ${err.message}` : 'Restore failed'); + } + }, + [id] + ); const handleCompare = useCallback( (comparisonSnapshotId: string) => { diff --git a/src/services/snapshotService.ts b/src/services/snapshotService.ts index 044d99c..4646f4a 100644 --- a/src/services/snapshotService.ts +++ b/src/services/snapshotService.ts @@ -4,7 +4,14 @@ import { API_CONFIG } from '../config/api'; import { apiClient } from './apiClient'; -import { SnapshotDTO, SnapshotSummaryDTO, NewSnapshotDTO, JobCreatedDTO } from '../types'; +import { + SnapshotDTO, + SnapshotSummaryDTO, + NewSnapshotDTO, + JobCreatedDTO, + RestoreResultDTO, + RestoreRequestDTO, +} from '../types'; export const snapshotService = { /** @@ -74,6 +81,22 @@ export const snapshotService = { ); }, + /** + * Restore PV values from a previously saved snapshot + * + * @param snapshotId - The snapshot ID to restore from + * @param request - Specific PVs to restore (undefined = all) + */ + async restoreSnapshot( + snapshotId: string, + request?: RestoreRequestDTO + ): Promise { + return apiClient.post( + `${API_CONFIG.endpoints.snapshots}/${snapshotId}/restore`, + request + ); + }, + /** * Delete a snapshot by ID */ diff --git a/src/types/api.ts b/src/types/api.ts index a748d64..90706a0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -61,6 +61,23 @@ export interface SnapshotDTO extends SnapshotSummaryDTO { pvValues: PVValueDTO[]; } +export interface RestoreRequestDTO { + pvIds?: string[]; +} + +export interface RestoreFailureDTO { + pvId: string; + pvName: string; + error: string; +} + +export interface RestoreResultDTO { + totalPVs: number; + successCount: number; + failureCount: number; + failures: RestoreFailureDTO[]; +} + export interface EpicsValueDTO { // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; From 5f42abd4e02dd17b78fec231f8b1fda7dfc47356 Mon Sep 17 00:00:00 2001 From: jbellister-slac Date: Sun, 29 Mar 2026 11:36:20 -0700 Subject: [PATCH 2/4] WIP: UI updates for restore snapshot progress --- src/components/Layout.tsx | 2 + src/components/RestoreStatusPanel.tsx | 381 ++++++++++++++++++++++++++ src/components/Sidebar.tsx | 134 +++++++++ src/contexts/RestoreContext.tsx | 237 ++++++++++++++++ src/main.tsx | 5 +- src/pages/SnapshotDetailsPage.tsx | 5 +- src/routes/snapshot-details.tsx | 47 ++-- src/services/snapshotService.ts | 10 + 8 files changed, 789 insertions(+), 32 deletions(-) create mode 100644 src/components/RestoreStatusPanel.tsx create mode 100644 src/contexts/RestoreContext.tsx diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 7e18348..55e6a86 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -9,6 +9,7 @@ import { Sidebar } from './Sidebar'; // import { UserAvatar } from './UserAvatar'; import { CreateSnapshotDialog } from './CreateSnapshotDialog'; import { LiveDataWarningBanner } from './LiveDataWarningBanner'; +import { RestoreStatusPanel } from './RestoreStatusPanel'; import { useAdminMode } from '../contexts/AdminModeContext'; import { ApiKeyErrorBanner } from './ApiKeyErrorBanner'; @@ -116,6 +117,7 @@ export function Layout({ children }: LayoutProps) { }} > {children} + diff --git a/src/components/RestoreStatusPanel.tsx b/src/components/RestoreStatusPanel.tsx new file mode 100644 index 0000000..f57a56e --- /dev/null +++ b/src/components/RestoreStatusPanel.tsx @@ -0,0 +1,381 @@ +import { + Box, + Button, + Collapse, + IconButton, + LinearProgress, + Paper, + Stack, + Typography, +} from '@mui/material'; +import { + CheckCircleOutline, + ErrorOutline, + ExpandMore, + ExpandLess, + Close, + Restore as RestoreIcon, +} from '@mui/icons-material'; +import { useRestore } from '../contexts/RestoreContext'; + +export function RestoreStatusPanel() { + const { restoreProgress, setPanelState, clearRestore } = useRestore(); + + const { status, snapshotTitle, requestedPvCount, progress, message, error, panelState } = + restoreProgress; + + if (status === 'idle' || panelState === 'closed') { + return null; + } + + const isRunning = status === 'running'; + const isExpanded = panelState === 'expanded'; + const isWarning = status === 'warning'; + const isError = status === 'error'; + const isSuccess = status === 'success'; + + let headerTitle: string; + if (isRunning) { + headerTitle = 'Restoring Snapshot'; + } else if (isError) { + headerTitle = 'Restore Failed'; + } else { + headerTitle = 'Restore Complete'; + } + + let summaryText: string; + if (isRunning) { + summaryText = + message || `Restoring ${requestedPvCount ?? 0} PV${requestedPvCount === 1 ? '' : 's'}`; + } else if (isError) { + summaryText = error || 'Restore failed'; + } else { + summaryText = message || 'Restore complete'; + } + + const progressValue = typeof progress === 'number' ? Math.max(0, Math.min(100, progress)) : null; + + let detailText: string; + if (isRunning) { + detailText = progressValue !== null ? `${progressValue}% complete` : 'In progress'; + } else if (isError) { + detailText = 'Unable to complete restore'; + } else { + detailText = '100% complete'; + } + + const handleToggleCollapse = () => { + setPanelState(isExpanded ? 'collapsed' : 'expanded'); + }; + + const renderHeaderIcon = () => { + if (isRunning) { + return ; + } + if (isError || isWarning) { + return ; + } + return ; + }; + + const renderProgressSection = () => { + let completedBarColor: string; + if (isError) { + completedBarColor = '#c62828'; + } else if (isWarning) { + completedBarColor = '#b26a00'; + } else { + completedBarColor = '#07072d'; + } + if (isRunning) { + return ( + <> + + {progressValue !== null ? ( + + ) : ( + + )} + + + {detailText} + + + ); + } + + return ( + <> + + + + + {detailText} + + + ); + }; + + const renderStatusCard = () => { + if (isError) { + return ( + + + + + + Restore failed + + + {error || 'An error occurred while restoring the snapshot.'} + + + + + ); + } + + if (isWarning) { + return ( + + + + + + Restore completed with issues + + + {message || 'Some PVs may have failed during restore.'} + + + + + ); + } + + if (isSuccess) { + return ( + + + + + + Restore successful + + + {message || 'All requested PVs were restored successfully.'} + + + + + ); + } + + return null; + }; + + return ( + theme.zIndex.drawer + 2, + borderRadius: 2, + overflow: 'hidden', + boxShadow: '0 10px 30px rgba(0, 0, 0, 0.18)', + backgroundColor: 'white', + }} + > + + + {renderHeaderIcon()} + + {headerTitle} + + + + + + {isExpanded ? : } + + setPanelState('closed')} + sx={{ + color: 'rgba(255,255,255,0.92)', + p: 0.5, + }} + > + + + + + + + + + {snapshotTitle || 'Snapshot'} + + + + {requestedPvCount ?? 0} PVs + + + + {renderProgressSection()} + + + + {isRunning && ( + + + You can navigate away + + + The restore will continue in the background. Progress is shown here as it updates. + + + )} + + {!isRunning && renderStatusCard()} + + + + {summaryText} + + + + {!isRunning && ( + + + + + + )} + + + + + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index aed001b..dbf304b 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -26,6 +26,7 @@ import { import { Link, useLocation, useNavigate } from '@tanstack/react-router'; import { useSnapshot } from '../contexts'; import { useAdminMode } from '../contexts/AdminModeContext'; +import { useRestore } from '../contexts/RestoreContext'; const SIDEBAR_WIDTH_EXPANDED = 240; const SIDEBAR_WIDTH_COLLAPSED = 60; @@ -67,6 +68,7 @@ export function Sidebar({ open, onToggle, onSaveSnapshot }: SidebarProps) { const location = useLocation(); const navigate = useNavigate(); const { snapshotProgress, clearSnapshot } = useSnapshot(); + const { restoreProgress, reopenPanel } = useRestore(); const { isAdminMode } = useAdminMode(); const menuItems = [ @@ -95,6 +97,54 @@ export function Sidebar({ open, onToggle, onSaveSnapshot }: SidebarProps) { const showStatus = snapshotProgress.isCreating || snapshotProgress.snapshotId || snapshotProgress.error; + const showRestoreStatus = restoreProgress.status !== 'idle'; + + const handleRestoreClick = () => { + // Always reopen panel when clicked + reopenPanel(); + }; + + const renderRestoreIcon = (): ReactElement => { + if (restoreProgress.status === 'running') { + if (restoreProgress.progress !== null) { + return ( + + + + ); + } + return ; + } + + if (restoreProgress.status === 'error') { + return ; + } + + if (restoreProgress.status === 'warning') { + return ; + } + + return ; + }; + + const getRestoreMessage = () => { + if (restoreProgress.status === 'running') { + return restoreProgress.message || 'Restoring snapshot...'; + } + if (restoreProgress.status === 'error') { + return 'Restore failed - Click to view'; + } + if (restoreProgress.status === 'warning') { + return 'Restore complete with issues - Click to view'; + } + return 'Restore complete'; + }; + const renderStatusIcon = (): ReactElement => { if (snapshotProgress.isCreating) { if (snapshotProgress.progress !== null) { @@ -302,6 +352,90 @@ export function Sidebar({ open, onToggle, onSaveSnapshot }: SidebarProps) { )} + {/* Restore Snapshot Status Indicator */} + {showRestoreStatus && ( + + + {renderRestoreIcon()} + {open && ( + + + {getRestoreMessage()} + + + {/* Snapshot title */} + {restoreProgress.snapshotTitle && ( + + {restoreProgress.snapshotTitle} + + )} + + {restoreProgress.status === 'running' && restoreProgress.progress !== null && ( + + {restoreProgress.progress}% complete + + )} + + {restoreProgress.snapshotTitle && ( + + {restoreProgress.snapshotTitle} + + )} + + )} + + + )} + {/* Save Snapshot Button */} void; + clearRestore: () => void; + setPanelState: (state: RestorePanelState) => void; + reopenPanel: () => void; +} + +const initialRestoreProgress: RestoreProgress = { + status: 'idle', + isRestoring: false, + jobId: null, + snapshotId: null, + snapshotTitle: null, + requestedPvCount: null, + progress: null, + message: null, + error: null, + panelState: 'expanded', +}; + +const POLL_INTERVAL_MS = 1000; + +const RestoreContext = createContext(undefined); + +export function RestoreProvider({ children }: { children: ReactNode }) { + const [restoreProgress, setRestoreProgress] = useState(initialRestoreProgress); + + const pollIntervalRef = useRef(null); + + const stopPolling = useCallback(() => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + }, []); + + const pollJobStatus = useCallback( + (jobId: string, snapshotId: string, snapshotTitle: string, requestedPvCount: number) => { + const poll = async () => { + try { + const job = await jobService.getJobStatus(jobId); + + if (job.status === 'completed') { + stopPolling(); + + const failureMatch = job.message?.match(/\((\d+)\s+failed\)/i); + const failureCount = failureMatch ? Number(failureMatch[1]) : 0; + + setRestoreProgress((prev) => ({ + ...prev, + status: failureCount > 0 ? 'warning' : 'success', + isRestoring: false, + jobId, + snapshotId, + snapshotTitle, + requestedPvCount, + progress: 100, + error: null, + message: job.message || 'Restore completed', + })); + } else if (job.status === 'failed') { + stopPolling(); + + setRestoreProgress((prev) => ({ + ...prev, + status: 'error', + isRestoring: false, + jobId, + snapshotId, + snapshotTitle, + requestedPvCount, + progress: null, + error: job.error || 'Restore failed', + message: null, + })); + } else { + setRestoreProgress((prev) => ({ + ...prev, + status: 'running', + isRestoring: true, + jobId, + snapshotId, + snapshotTitle, + requestedPvCount, + progress: job.progress ?? null, + error: null, + message: job.message || 'Restoring snapshot...', + })); + } + } catch (err) { + stopPolling(); + + setRestoreProgress((prev) => ({ + ...prev, + status: 'error', + isRestoring: false, + jobId, + snapshotId, + snapshotTitle, + requestedPvCount, + progress: null, + error: err instanceof Error ? err.message : 'Failed to check restore job status', + message: null, + })); + } + }; + + poll(); + pollIntervalRef.current = window.setInterval(poll, POLL_INTERVAL_MS); + }, + [stopPolling] + ); + + const startRestore = useCallback( + (params: StartRestoreParams) => { + stopPolling(); + + setRestoreProgress({ + status: 'running', + isRestoring: true, + jobId: null, + snapshotId: params.snapshotId, + snapshotTitle: params.snapshotTitle, + requestedPvCount: params.requestedPvCount, + progress: 0, + error: null, + message: 'Starting restore...', + panelState: 'expanded', + }); + + const request = params.pvIds && params.pvIds.length > 0 ? { pvIds: params.pvIds } : undefined; + + snapshotService + .restoreSnapshotAsync(params.snapshotId, request) + .then((result) => { + setRestoreProgress((prev) => ({ + ...prev, + jobId: result.jobId, + message: result.message || 'Restore queued', + })); + + pollJobStatus( + result.jobId, + params.snapshotId, + params.snapshotTitle, + params.requestedPvCount + ); + }) + .catch((err) => { + setRestoreProgress((prev) => ({ + ...prev, + status: 'error', + isRestoring: false, + progress: null, + error: err instanceof Error ? err.message : 'Failed to start restore', + message: null, + })); + }); + }, + [pollJobStatus, stopPolling] + ); + + const clearRestore = useCallback(() => { + stopPolling(); + setRestoreProgress(initialRestoreProgress); + }, [stopPolling]); + + const setPanelState = useCallback((state: RestorePanelState) => { + setRestoreProgress((prev) => ({ + ...prev, + panelState: state, + })); + }, []); + + const reopenPanel = useCallback(() => { + setRestoreProgress((prev) => ({ + ...prev, + panelState: 'expanded', + })); + }, []); + + const value = useMemo( + () => ({ + restoreProgress, + startRestore, + clearRestore, + setPanelState, + reopenPanel, + }), + [restoreProgress, startRestore, clearRestore, setPanelState, reopenPanel] + ); + + return {children}; +} + +export function useRestore(): RestoreContextType { + const context = useContext(RestoreContext); + if (!context) { + throw new Error('useRestore must be used within a RestoreProvider'); + } + return context; +} diff --git a/src/main.tsx b/src/main.tsx index f379195..2f3c648 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider, QueryCache, MutationCache } from '@ta import { ApiKeyError } from './config/api'; import { HeartbeatProvider } from './contexts/HeartbeatContext'; import { LivePVProvider } from './contexts/LivePVContext'; +import { RestoreProvider } from './contexts/RestoreContext'; import { SnapshotProvider } from './contexts/SnapshotContext'; import { AdminModeProvider } from './contexts/AdminModeContext'; import { ApiKeyErrorProvider, useApiKeyError } from './contexts/ApiKeyErrorContext'; @@ -55,7 +56,9 @@ function App() { - + + + diff --git a/src/pages/SnapshotDetailsPage.tsx b/src/pages/SnapshotDetailsPage.tsx index ebe4bef..a13f401 100644 --- a/src/pages/SnapshotDetailsPage.tsx +++ b/src/pages/SnapshotDetailsPage.tsx @@ -128,9 +128,8 @@ export function SnapshotDetailsPage({ }; const confirmRestore = async () => { - if (onRestore) { - await onRestore(pvsToRestore); - } + if (!onRestore) return; + await onRestore(pvsToRestore); setShowRestoreDialog(false); }; diff --git a/src/routes/snapshot-details.tsx b/src/routes/snapshot-details.tsx index ab21949..8707407 100644 --- a/src/routes/snapshot-details.tsx +++ b/src/routes/snapshot-details.tsx @@ -3,10 +3,10 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { useQueryClient } from '@tanstack/react-query'; import { Box, CircularProgress, Typography, Button } from '@mui/material'; import { SnapshotDetailsPage } from '../pages'; -import { snapshotService } from '../services/snapshotService'; import { Snapshot, PV, Severity, Status } from '../types'; import { useSnapshot, snapshotKeys } from '../hooks'; import { SnapshotDTO, PVValueDTO } from '../types/api'; +import { useRestore } from '../contexts/RestoreContext'; // Map severity number from backend to Severity enum const mapSeverity = (severity?: number): Severity => { @@ -97,6 +97,7 @@ interface SearchParams { function SnapshotDetails() { const navigate = useNavigate(); const queryClient = useQueryClient(); + const { startRestore } = useRestore(); // eslint-disable-next-line @typescript-eslint/no-use-before-define const { id } = Route.useSearch(); @@ -109,33 +110,6 @@ function SnapshotDetails() { navigate({ to: '/snapshots' }); }, [navigate, queryClient]); - const handleRestore = useCallback( - async (pvs: PV[]) => { - if (!id) return; - - try { - // If user selected PVs, restore only those. If not, restore all - const request = pvs.length > 0 ? { pvIds: pvs.map((pv) => pv.uuid) } : undefined; - - const result = await snapshotService.restoreSnapshot(id, request); - - console.log('Restore successful:', result); - - // Optional: temporary visual feedback - alert( - `Restore completed:\n` + - `${result.successCount}/${result.totalPVs} succeeded\n` + - `${result.failureCount} failed` - ); - } catch (err) { - console.error('Failed to restore PVs:', err); - - alert(err instanceof Error ? `Restore failed: ${err.message}` : 'Restore failed'); - } - }, - [id] - ); - const handleCompare = useCallback( (comparisonSnapshotId: string) => { navigate({ @@ -152,6 +126,23 @@ function SnapshotDetails() { return mapSnapshotDTOtoSnapshot(snapshotDTO); }, [snapshotDTO]); + const handleRestore = useCallback( + async (pvs: PV[]): Promise => { + if (!id || !snapshot) return; + + const pvIds = pvs.length > 0 ? pvs.map((pv) => pv.uuid) : undefined; + const requestedPvCount = pvIds?.length ?? snapshot.pvs.length; + + startRestore({ + snapshotId: snapshot.uuid, + snapshotTitle: snapshot.title, + requestedPvCount, + pvIds, + }); + }, + [id, snapshot, startRestore] + ); + if (!id) { return ( { + return apiClient.post( + `${API_CONFIG.endpoints.snapshots}/${snapshotId}/restore?async=true`, + request + ); + }, + /** * Delete a snapshot by ID */ From 4bba8d26d98601b383576760e26338faceb0bcce Mon Sep 17 00:00:00 2001 From: jbellister-slac Date: Mon, 30 Mar 2026 17:24:19 -0700 Subject: [PATCH 3/4] ENH: Add information about PVs that failed to restore. A bit of cleanup --- src/components/RestoreStatusPanel.tsx | 104 ++++++++++++++++++-------- src/components/Sidebar.tsx | 16 ---- src/contexts/RestoreContext.tsx | 6 ++ src/types/api.ts | 17 +++++ 4 files changed, 97 insertions(+), 46 deletions(-) diff --git a/src/components/RestoreStatusPanel.tsx b/src/components/RestoreStatusPanel.tsx index f57a56e..b748cba 100644 --- a/src/components/RestoreStatusPanel.tsx +++ b/src/components/RestoreStatusPanel.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Box, Button, @@ -16,13 +17,25 @@ import { Close, Restore as RestoreIcon, } from '@mui/icons-material'; +import { useNavigate } from '@tanstack/react-router'; import { useRestore } from '../contexts/RestoreContext'; export function RestoreStatusPanel() { + const [showFailures, setShowFailures] = useState(false); const { restoreProgress, setPanelState, clearRestore } = useRestore(); + const navigate = useNavigate(); - const { status, snapshotTitle, requestedPvCount, progress, message, error, panelState } = - restoreProgress; + const { + status, + snapshotId, + snapshotTitle, + requestedPvCount, + progress, + message, + error, + panelState, + } = restoreProgress; + const failures = restoreProgress.failures ?? []; if (status === 'idle' || panelState === 'closed') { return null; @@ -43,16 +56,6 @@ export function RestoreStatusPanel() { headerTitle = 'Restore Complete'; } - let summaryText: string; - if (isRunning) { - summaryText = - message || `Restoring ${requestedPvCount ?? 0} PV${requestedPvCount === 1 ? '' : 's'}`; - } else if (isError) { - summaryText = error || 'Restore failed'; - } else { - summaryText = message || 'Restore complete'; - } - const progressValue = typeof progress === 'number' ? Math.max(0, Math.min(100, progress)) : null; let detailText: string; @@ -64,6 +67,13 @@ export function RestoreStatusPanel() { detailText = '100% complete'; } + let detailButtonLabel: string; + if (failures.length > 0) { + detailButtonLabel = showFailures ? 'Hide Failures' : `View ${failures.length} Failed PVs`; + } else { + detailButtonLabel = 'View Details'; + } + const handleToggleCollapse = () => { setPanelState(isExpanded ? 'collapsed' : 'expanded'); }; @@ -279,7 +289,7 @@ export function RestoreStatusPanel() { setPanelState('closed')} + onClick={clearRestore} sx={{ color: 'rgba(255,255,255,0.92)', p: 0.5, @@ -297,7 +307,7 @@ export function RestoreStatusPanel() { - {requestedPvCount ?? 0} PVs + {isRunning && message ? message : `${requestedPvCount ?? 0} PVs`} @@ -326,12 +336,6 @@ export function RestoreStatusPanel() { {!isRunning && renderStatusCard()} - - - {summaryText} - - - {!isRunning && ( )} + + {/* Failure details list */} + + + {failures.map((f, i) => ( + + + {f.pvName} + + + {f.error} + + + ))} + {failures.length >= 50 && ( + + Showing first 50 failures. Check server logs for full details. + + )} + + diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index dbf304b..bfb14c3 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -414,22 +414,6 @@ export function Sidebar({ open, onToggle, onSaveSnapshot }: SidebarProps) { {restoreProgress.progress}% complete )} - - {restoreProgress.snapshotTitle && ( - - {restoreProgress.snapshotTitle} - - )} )} diff --git a/src/contexts/RestoreContext.tsx b/src/contexts/RestoreContext.tsx index b255316..fe9e37f 100644 --- a/src/contexts/RestoreContext.tsx +++ b/src/contexts/RestoreContext.tsx @@ -22,6 +22,7 @@ export interface RestoreProgress { progress: number | null; message: string | null; error: string | null; + failures: Array<{ pvId: string; pvName: string; error: string }>; panelState: RestorePanelState; } @@ -50,6 +51,7 @@ const initialRestoreProgress: RestoreProgress = { progress: null, message: null, error: null, + failures: [], panelState: 'expanded', }; @@ -80,6 +82,8 @@ export function RestoreProvider({ children }: { children: ReactNode }) { const failureMatch = job.message?.match(/\((\d+)\s+failed\)/i); const failureCount = failureMatch ? Number(failureMatch[1]) : 0; + const resultData = job.jobData?.result; + const failures = resultData?.failures ?? []; setRestoreProgress((prev) => ({ ...prev, @@ -91,6 +95,7 @@ export function RestoreProvider({ children }: { children: ReactNode }) { requestedPvCount, progress: 100, error: null, + failures, message: job.message || 'Restore completed', })); } else if (job.status === 'failed') { @@ -159,6 +164,7 @@ export function RestoreProvider({ children }: { children: ReactNode }) { requestedPvCount: params.requestedPvCount, progress: 0, error: null, + failures: [], message: 'Starting restore...', panelState: 'expanded', }); diff --git a/src/types/api.ts b/src/types/api.ts index 90706a0..a6fed33 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -194,6 +194,19 @@ export interface ApiKeyCreateResultDTO extends ApiKeyDTO { token: string; } +export interface RestoreFailureDetail { + pvId: string; + pvName: string; + error: string; +} + +export interface RestoreResultData { + total_pvs: number; + success_count: number; + failure_count: number; + failures: RestoreFailureDetail[]; +} + /** * Job DTOs for async task tracking */ @@ -205,6 +218,10 @@ export interface JobDTO { message?: string; resultId?: string; error?: string; + jobData?: { + result?: RestoreResultData; + [key: string]: unknown; + }; createdAt: string; startedAt?: string; completedAt?: string; From 63698964adfa195fa7bcb2fae13fa3792f5279d4 Mon Sep 17 00:00:00 2001 From: jbellister-slac Date: Mon, 30 Mar 2026 17:58:30 -0700 Subject: [PATCH 4/4] More cleanup --- src/components/RestoreStatusPanel.tsx | 47 ++++++++++++++------------- src/services/snapshotService.ts | 11 ------- src/types/api.ts | 7 ---- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/components/RestoreStatusPanel.tsx b/src/components/RestoreStatusPanel.tsx index b748cba..ef88a68 100644 --- a/src/components/RestoreStatusPanel.tsx +++ b/src/components/RestoreStatusPanel.tsx @@ -95,8 +95,9 @@ export function RestoreStatusPanel() { } else if (isWarning) { completedBarColor = '#b26a00'; } else { - completedBarColor = '#07072d'; + completedBarColor = '#2c3e50'; } + if (isRunning) { return ( <> @@ -110,7 +111,7 @@ export function RestoreStatusPanel() { backgroundColor: '#d7d9e0', }, '& .MuiLinearProgress-bar': { - backgroundColor: '#07072d', + backgroundColor: '#2c3e50', }, }} > @@ -136,19 +137,17 @@ export function RestoreStatusPanel() { sx={{ mt: 1, mb: 0.75, - height: 6, - borderRadius: 999, - backgroundColor: '#d7d9e0', - overflow: 'hidden', + '& .MuiLinearProgress-root': { + height: 6, + borderRadius: 999, + backgroundColor: '#d7d9e0', + }, + '& .MuiLinearProgress-bar': { + backgroundColor: completedBarColor, + }, }} > - + - - You can navigate away - - - The restore will continue in the background. Progress is shown here as it updates. + + + You can navigate away + + { + ' - The restore will continue in the background. Progress is shown here as it updates.' + } )} @@ -342,12 +343,12 @@ export function RestoreStatusPanel() { variant="contained" fullWidth sx={{ - backgroundColor: '#05052f', + backgroundColor: '#2c3e50', borderRadius: 1.5, textTransform: 'none', fontWeight: 600, boxShadow: 'none', - '&:hover': { backgroundColor: '#0b0b45', boxShadow: 'none' }, + '&:hover': { backgroundColor: '#34495e', boxShadow: 'none' }, }} onClick={() => { if (failures.length > 0) { @@ -412,7 +413,7 @@ export function RestoreStatusPanel() { ))} {failures.length >= 50 && ( - Showing first 50 failures. Check server logs for full details. + Showing first 50 failures only. )} diff --git a/src/services/snapshotService.ts b/src/services/snapshotService.ts index bc9e599..e34802d 100644 --- a/src/services/snapshotService.ts +++ b/src/services/snapshotService.ts @@ -9,7 +9,6 @@ import { SnapshotSummaryDTO, NewSnapshotDTO, JobCreatedDTO, - RestoreResultDTO, RestoreRequestDTO, } from '../types'; @@ -87,16 +86,6 @@ export const snapshotService = { * @param snapshotId - The snapshot ID to restore from * @param request - Specific PVs to restore (undefined = all) */ - async restoreSnapshot( - snapshotId: string, - request?: RestoreRequestDTO - ): Promise { - return apiClient.post( - `${API_CONFIG.endpoints.snapshots}/${snapshotId}/restore`, - request - ); - }, - async restoreSnapshotAsync( snapshotId: string, request?: RestoreRequestDTO diff --git a/src/types/api.ts b/src/types/api.ts index a6fed33..7df6224 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -71,13 +71,6 @@ export interface RestoreFailureDTO { error: string; } -export interface RestoreResultDTO { - totalPVs: number; - successCount: number; - failureCount: number; - failures: RestoreFailureDTO[]; -} - export interface EpicsValueDTO { // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any;