Files
Jungfraujoch/frontend/src/components/SettingsPanel.tsx
T
leonarski_fandClaude Opus 4.8 59da040e30 frontend: shared SettingsPanel + typed upload with error display
Generalize the expert-panel boilerplate (container + title + widgets +
upload button) and surface real upload errors via hey-api.

- SettingsPanel: consistent padded Paper + Typography title (one place to
  size it) + optional Upload button + result snackbar; PanelTitle and
  UploadSnackbarView exported for panels with bespoke layouts (DetectorSettings
  grid, ROI)
- useUpload(mutation): wraps a generated hey-api mutation, shows the server's
  error message on failure (errorMessage handles 400 string / 500 error_message)
- "unsaved changes" dot on panel titles, derived from the existing
  last-downloaded snapshot (dirty = !isEqual(value, lastDownloaded))
- fix PixelMask: TIFF upload was swallowing errors; now shows them
- config panels (FileWriter, ImageFormat, Detector, AzInt, DarkMask, Indexing,
  Instrument, ZeroMQ, ROI, DetectorSelection) upload through the typed SDK
  instead of ButtonWithSnackbar's raw fetch; status/display panels share the
  same shell for consistent spacing
- ButtonWithSnackbar now only powers bodyless action buttons (pedestal/cancel/
  initialize, start/trigger, deactivate)

Build (tsc + vite) passes; dev server transforms all modules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:04:55 +02:00

82 lines
2.8 KiB
TypeScript

import { ReactNode } from "react";
import { Alert, Box, Button, Paper, Snackbar, Stack, Tooltip, Typography } from "@mui/material";
import { SxProps, Theme } from "@mui/material/styles";
import { TypographyProps } from "@mui/material/Typography";
import { UploadSnackbar } from "./useUpload";
/** The upload-result snackbar, usable on its own by panels that don't use SettingsPanel. */
export function UploadSnackbarView({ snackbar }: { snackbar?: UploadSnackbar }) {
if (!snackbar) return null;
return (
<Snackbar open={snackbar.open} autoHideDuration={5000} onClose={snackbar.onClose}>
<Alert onClose={snackbar.onClose} variant="filled"
severity={snackbar.success ? "success" : "error"} sx={{ width: "100%" }}>
{snackbar.success ? "Ok!" : snackbar.message}
</Alert>
</Snackbar>
);
}
/** A title with an optional "unsaved changes" marker. */
export function PanelTitle({ title, variant = "h6", dirty }: { title: string; variant?: TypographyProps["variant"]; dirty?: boolean }) {
return (
<Typography variant={variant} component="div">
{title}
{dirty && (
<Tooltip title="Unsaved changes — upload to apply">
<Box component="span" sx={{ color: "warning.main", ml: 0.5 }}></Box>
</Tooltip>
)}
</Typography>
);
}
type SettingsPanelProps = {
title?: string;
titleVariant?: TypographyProps["variant"];
dirty?: boolean;
onUpload?: () => void;
uploadText?: string;
uploadDisabled?: boolean;
snackbar?: UploadSnackbar;
sx?: SxProps<Theme>;
spacing?: number;
children: ReactNode;
};
/**
* Consistent shell for the expert configuration panels: a padded Paper, a title
* (with an optional "unsaved changes" marker), the panel widgets, an optional
* Upload button, and the upload result snackbar.
*/
function SettingsPanel({
title,
titleVariant = "h6",
dirty,
onUpload,
uploadText = "Upload",
uploadDisabled,
snackbar,
sx,
spacing = 3,
children,
}: SettingsPanelProps) {
return (
<Paper sx={[{ width: "100%", p: 3, textAlign: "center" }, ...(Array.isArray(sx) ? sx : [sx])]}>
<Stack spacing={spacing} alignItems="center">
{title !== undefined && <PanelTitle title={title} variant={titleVariant} dirty={dirty} />}
{children}
{onUpload && (
<Button variant="contained" color="primary" disableElevation
onClick={onUpload} disabled={uploadDisabled}>
{uploadText}
</Button>
)}
</Stack>
<UploadSnackbarView snackbar={snackbar} />
</Paper>
);
}
export default SettingsPanel;