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>
82 lines
2.8 KiB
TypeScript
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;
|