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>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import {ChangeEvent, memo, useEffect, useState} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {FormControlLabel, Checkbox, Stack, Radio, RadioGroup, Typography} from "@mui/material";
|
||||
import NumberTextField from "./NumberTextField";
|
||||
import {azim_int_settings} from "../client";
|
||||
import {putConfigAzimIntMutation} from "../client/@tanstack/react-query.gen";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: azim_int_settings
|
||||
@@ -29,6 +30,7 @@ function AzIntSettings({s: serverS}: MyProps) {
|
||||
const [lowQError, setLowQError] = useState(false);
|
||||
const [highQError, setHighQError] = useState(false);
|
||||
const [qSpacingError, setQSpacingError] = useState(false);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigAzimIntMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -39,15 +41,13 @@ function AzIntSettings({s: serverS}: MyProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serverS]);
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: '8%',
|
||||
marginRight: '8%',
|
||||
}}>
|
||||
<div><strong>Azimuthal integration settings </strong></div>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Azimuthal integration" dirty={dirty}
|
||||
onUpload={() => submit(s)}
|
||||
uploadDisabled={pending || (s.high_q_recipA <= s.low_q_recipA) || highQError || lowQError || qSpacingError}
|
||||
snackbar={snackbar}>
|
||||
<Stack spacing={2} direction="row">
|
||||
<NumberTextField
|
||||
default={1.0}
|
||||
@@ -139,22 +139,8 @@ function AzIntSettings({s: serverS}: MyProps) {
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/azim_int"}
|
||||
input={JSON.stringify(s)}
|
||||
method={"PUT"}
|
||||
text={"Upload"}
|
||||
disabled={
|
||||
(s.high_q_recipA <= s.low_q_recipA)
|
||||
|| highQError
|
||||
|| lowQError
|
||||
|| qSpacingError
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(AzIntSettings);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Checkbox, FormControlLabel, Stack} from '@mui/material';
|
||||
import _ from 'lodash';
|
||||
import NumberTextField from './NumberTextField';
|
||||
import ButtonWithSnackbar from './ButtonWithSnackbar';
|
||||
import { dark_mask_settings } from '../client';
|
||||
import { putConfigDarkMaskMutation } from '../client/@tanstack/react-query.gen';
|
||||
import SettingsPanel from './SettingsPanel';
|
||||
import { useUpload } from './useUpload';
|
||||
|
||||
type MyProps = {
|
||||
s?: dark_mask_settings;
|
||||
@@ -28,6 +29,7 @@ function DarkMaskSettings({s: serverS}: MyProps) {
|
||||
const [numberOfFramesOld, setNumberOfFramesOld] = useState(1000);
|
||||
const [maxPixelCountErr, setMaxPixelCountErr] = useState(false);
|
||||
const [maxFramesWithSignalErr, setMaxFramesWithSignalErr] = useState(false);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigDarkMaskMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -51,17 +53,11 @@ function DarkMaskSettings({s: serverS}: MyProps) {
|
||||
maxPixelCountErr ||
|
||||
maxFramesWithSignalErr;
|
||||
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<Paper style={{ textAlign: 'center' }} sx={{ width: '100%' }}>
|
||||
<br />
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<strong>Dark data collection settings for mask</strong>
|
||||
<SettingsPanel title="Dark mask" dirty={dirty}
|
||||
onUpload={() => submit(s)} uploadDisabled={pending || hasError()} snackbar={snackbar}>
|
||||
<div>(DECTRIS detectors only)</div>
|
||||
|
||||
<FormControlLabel
|
||||
@@ -161,17 +157,7 @@ function DarkMaskSettings({s: serverS}: MyProps) {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ButtonWithSnackbar
|
||||
path={'/config/dark_mask'}
|
||||
color={'primary'}
|
||||
method={'PUT'}
|
||||
disabled={hasError()}
|
||||
text={'Upload'}
|
||||
input={JSON.stringify(s)}
|
||||
/>
|
||||
</Stack>
|
||||
<br />
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {ChangeEvent, memo, useEffect, useState} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Grid, Slider, Switch, Typography} from "@mui/material";
|
||||
import {putConfigSpotFinding, spot_finding_settings} from "../client";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import _ from "lodash";
|
||||
|
||||
type MyProps = {
|
||||
@@ -91,12 +91,12 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
|
||||
apply({...s, high_res_gap_Q_recipA: v});
|
||||
};
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ height: 800, width: '100%' }}>
|
||||
return (
|
||||
<SettingsPanel title="Spot finding parameters" sx={{ height: 800 }}>
|
||||
<Grid container spacing={0}>
|
||||
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<br/><strong>Spot finding parameters</strong><br/><br/>
|
||||
<Switch onChange={enableSpotFindingToggle} checked={s.enable}/>
|
||||
Spot finding
|
||||
<br/><br/>
|
||||
@@ -183,7 +183,8 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DataProcessingSettings);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import {Stack, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material";
|
||||
import {Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import {detector_list, detector_list_element} from "../client";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import {putConfigSelectDetectorMutation} from "../client/@tanstack/react-query.gen";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: detector_list
|
||||
@@ -39,6 +41,7 @@ const default_detector_element: detector_list_element = {
|
||||
|
||||
function DetectorSelection({s}: MyProps) {
|
||||
const [choice, setChoice] = useState("0");
|
||||
const { submit, pending, snackbar } = useUpload(putConfigSelectDetectorMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if (s !== undefined)
|
||||
@@ -105,13 +108,14 @@ function DetectorSelection({s}: MyProps) {
|
||||
return v;
|
||||
};
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<strong>Detector selection </strong>
|
||||
const dirty = s !== undefined && Number(choice) !== s.current_id;
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Detector selection" dirty={dirty}
|
||||
uploadText="Select detector"
|
||||
onUpload={() => submit({ id: Number(choice) })}
|
||||
uploadDisabled={pending || s === undefined}
|
||||
snackbar={snackbar}>
|
||||
|
||||
{detector_info()}
|
||||
|
||||
@@ -128,17 +132,8 @@ function DetectorSelection({s}: MyProps) {
|
||||
{detector_options().map(d => (<MenuItem value={d.id}> {d.name} </MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/select_detector"}
|
||||
input={JSON.stringify({id: Number(choice)})}
|
||||
method={"PUT"}
|
||||
text={"Select detector"}
|
||||
disabled={s === undefined}
|
||||
/>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DetectorSelection);
|
||||
|
||||
@@ -7,9 +7,13 @@ import InputLabel from "@mui/material/InputLabel";
|
||||
import Select, {SelectChangeEvent} from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {detector_settings, detector_timing} from "../client";
|
||||
import {putConfigDetectorMutation} from "../client/@tanstack/react-query.gen";
|
||||
import NumberTextField from "./NumberTextField";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import Button from "@mui/material/Button";
|
||||
import {PanelTitle, UploadSnackbarView} from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: detector_settings
|
||||
@@ -46,6 +50,7 @@ function DetectorSettings({s: serverS}: MyProps) {
|
||||
const [eigerThresholdErr, setEigerThresholdErr] = useState(false);
|
||||
const [eigerThresholdKeVOld, setEigerThresholdKeVOld] = useState(6.0);
|
||||
const [downloadCounter, setDownloadCounter] = useState(0);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigDetectorMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -139,13 +144,15 @@ function DetectorSettings({s: serverS}: MyProps) {
|
||||
setS(prev => ({...prev, jungfrau_storage_cell_count: Number(event.target.value)}));
|
||||
};
|
||||
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ height: 1150, width: '100%' }}>
|
||||
|
||||
<Grid container>
|
||||
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<br/><strong>Detector settings </strong>
|
||||
<br/><PanelTitle title="Detector settings" dirty={dirty}/>
|
||||
<br/><br/><br/>
|
||||
<NumberTextField start_val={s.frame_time_us}
|
||||
label={"Internal frame time"}
|
||||
@@ -410,22 +417,18 @@ function DetectorSettings({s: serverS}: MyProps) {
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/detector"}
|
||||
disabled={countTimeErr
|
||||
|| frameTimeErr
|
||||
|| internalFrameGeneratorImagesErr
|
||||
|| detectorTriggerDelayErr
|
||||
|| storageCellDelayErr
|
||||
|| pedestalG0FramesErr
|
||||
|| pedestalG1FramesErr
|
||||
|| pedestalG2FramesErr
|
||||
|| pedestalMinImageCountErr}
|
||||
text={"Upload"}
|
||||
input={JSON.stringify(s)}
|
||||
method={"PUT"}
|
||||
/>
|
||||
<Button color="primary" variant="contained" disableElevation
|
||||
onClick={() => submit(s)}
|
||||
disabled={pending
|
||||
|| countTimeErr
|
||||
|| frameTimeErr
|
||||
|| internalFrameGeneratorImagesErr
|
||||
|| detectorTriggerDelayErr
|
||||
|| storageCellDelayErr
|
||||
|| pedestalG0FramesErr
|
||||
|| pedestalG1FramesErr
|
||||
|| pedestalG2FramesErr
|
||||
|| pedestalMinImageCountErr}>Upload</Button>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/deactivate"}
|
||||
@@ -435,6 +438,7 @@ function DetectorSettings({s: serverS}: MyProps) {
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
<UploadSnackbarView snackbar={snackbar}/>
|
||||
</Paper>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {memo} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Stack, Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material";
|
||||
import {Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material";
|
||||
import {detector_status, detector_state, detector_power_state} from "../client";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
|
||||
type MyProps = {
|
||||
s?: detector_status
|
||||
@@ -42,14 +43,8 @@ function DetectorStatus({s}: MyProps) {
|
||||
high_voltage_V: []
|
||||
};
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
|
||||
<strong>Detector status</strong>
|
||||
return (
|
||||
<SettingsPanel title="Detector status">
|
||||
|
||||
<TableContainer component={Paper}
|
||||
style={{marginLeft: "auto", marginRight: "auto"}}
|
||||
@@ -85,9 +80,8 @@ function DetectorStatus({s}: MyProps) {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DetectorStatus);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Checkbox, FormControlLabel, FormGroup, Select, Stack} from "@mui/material";
|
||||
import {Checkbox, FormControlLabel, FormGroup, Select} from "@mui/material";
|
||||
import {file_writer_format, file_writer_settings} from "../client";
|
||||
import {putConfigFileWriterMutation} from "../client/@tanstack/react-query.gen";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: file_writer_settings
|
||||
@@ -31,6 +32,7 @@ function stringToEnum(value: string): file_writer_format {
|
||||
function FileWriterSettings({s: serverS}: MyProps) {
|
||||
const [s, setS] = useState<file_writer_settings>(default_file_writer_settings);
|
||||
const [lastDownloadedS, setLastDownloadedS] = useState<file_writer_settings>(default_file_writer_settings);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigFileWriterMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -40,13 +42,11 @@ function FileWriterSettings({s: serverS}: MyProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serverS]);
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div><strong>File Writer settings </strong></div>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<SettingsPanel title="File writer" dirty={dirty}
|
||||
onUpload={() => submit(s)} uploadDisabled={pending} snackbar={snackbar}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -80,16 +80,8 @@ function FileWriterSettings({s: serverS}: MyProps) {
|
||||
<MenuItem value={file_writer_format.NO_FILE_WRITTEN}>No files saved</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/file_writer"}
|
||||
input={JSON.stringify(s)}
|
||||
method={"PUT"}
|
||||
text={"Upload"}
|
||||
/>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(FileWriterSettings);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {ChangeEvent, memo, useEffect, useState} from 'react';
|
||||
|
||||
import {Checkbox, FormControlLabel, FormGroup, List, ListItem, Stack, Tooltip} from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import Select, {SelectChangeEvent} from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {image_format_settings} from "../client";
|
||||
import {putConfigImageFormatMutation} from "../client/@tanstack/react-query.gen";
|
||||
import NumberTextField from "./NumberTextField";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: image_format_settings
|
||||
@@ -59,6 +60,7 @@ function ImageFormatSettings({s: serverS}: MyProps) {
|
||||
const [jungfrauConversionFactorErr, setJungfrauConversionFactorErr] = useState(false);
|
||||
const [jungfrauConversionFactorOld, setJungfrauConversionFactorOld] = useState(12.4);
|
||||
const [downloadCounter, setDownloadCounter] = useState(0);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigImageFormatMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -103,13 +105,11 @@ function ImageFormatSettings({s: serverS}: MyProps) {
|
||||
setS(prev => ({...prev, signed_output: val}));
|
||||
};
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<strong>Output image format settings </strong>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Output image format" dirty={dirty}
|
||||
onUpload={() => submit(s)} uploadDisabled={pending || jungfrauConversionFactorErr} snackbar={snackbar}>
|
||||
<FormGroup>
|
||||
<FormControlLabel control={
|
||||
<Checkbox checked={s.geometry_transform}
|
||||
@@ -254,18 +254,8 @@ function ImageFormatSettings({s: serverS}: MyProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonWithSnackbar
|
||||
path={"/config/image_format"}
|
||||
color={"primary"}
|
||||
method={"PUT"}
|
||||
disabled={jungfrauConversionFactorErr}
|
||||
text={"Upload"}
|
||||
input={JSON.stringify(s)}
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ImageFormatSettings);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import {memo} from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Stack, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material";
|
||||
import {Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material";
|
||||
import {image_pusher_status, image_pusher_type} from "../client";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
|
||||
type MyProps = {
|
||||
s?: image_pusher_status
|
||||
@@ -34,10 +35,7 @@ function renderPusherType(type?: image_pusher_type): string {
|
||||
|
||||
function ImagePusherStatus({s}: MyProps) {
|
||||
return (
|
||||
<Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={2} sx={{justifyContent: "center", alignItems: "center"}}>
|
||||
<strong>Image pusher status</strong>
|
||||
<SettingsPanel title="Image pusher status" spacing={2}>
|
||||
<TableContainer component={Paper} sx={{width: '90%'}} style={{marginLeft: "auto", marginRight: "auto"}}>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
@@ -60,9 +58,7 @@ function ImagePusherStatus({s}: MyProps) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import {Stack, Typography, FormControlLabel, Checkbox} from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import Select, {SelectChangeEvent} from "@mui/material/Select";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {broker_status, geom_refinement_algorithm, indexing_algorithm, indexing_settings} from "../client";
|
||||
import {putConfigIndexingMutation} from "../client/@tanstack/react-query.gen";
|
||||
import NumberTextField from "./NumberTextField";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: indexing_settings
|
||||
@@ -48,6 +49,7 @@ function IndexingSettings({s: serverS, status}: MyProps) {
|
||||
const [unitCellDistToleranceError, setUnitCellDistToleranceError] = useState(false);
|
||||
const [rotationIndexingMinAngularRangeDegError, setRotationIndexingMinAngularRangeDegError] = useState(false);
|
||||
const [rotationIndexingAngularStrideDegError, setRotationIndexingAngularStrideDegError] = useState(false);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigIndexingMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -86,13 +88,11 @@ function IndexingSettings({s: serverS, status}: MyProps) {
|
||||
|| rotationIndexingAngularStrideDegError
|
||||
|| rotationIndexingMinAngularRangeDegError;
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<strong>Indexing settings </strong>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Indexing" dirty={dirty}
|
||||
onUpload={() => submit(s)} uploadDisabled={pending || isError()} snackbar={snackbar}>
|
||||
|
||||
<b>Algorithm</b>
|
||||
<FormControl sx = {{width: "80%"}}>
|
||||
@@ -290,18 +290,8 @@ function IndexingSettings({s: serverS, status}: MyProps) {
|
||||
}}
|
||||
sx={{width: '80%'}}/>
|
||||
|
||||
<ButtonWithSnackbar
|
||||
path={"/config/indexing"}
|
||||
color={"primary"}
|
||||
method={"PUT"}
|
||||
disabled={isError()}
|
||||
text={"Upload"}
|
||||
input={JSON.stringify(s)}
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(IndexingSettings);
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
Autocomplete,
|
||||
Checkbox,
|
||||
FormControlLabel, FormGroup,
|
||||
Stack,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import {instrument_metadata} from "../client";
|
||||
import {putConfigInstrumentMutation} from "../client/@tanstack/react-query.gen";
|
||||
import _ from "lodash";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: instrument_metadata
|
||||
@@ -37,6 +37,7 @@ const source_types : string[] = [
|
||||
function InstrumentMetadata({s: serverS}: MyProps) {
|
||||
const [s, setS] = useState<instrument_metadata>(default_instrument_metadata);
|
||||
const [lastDownloadedS, setLastDownloadedS] = useState<instrument_metadata>(default_instrument_metadata);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigInstrumentMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -46,16 +47,11 @@ function InstrumentMetadata({s: serverS}: MyProps) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serverS]);
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ width: '100%' }}>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: '8%',
|
||||
marginRight: '8%',
|
||||
}}>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
|
||||
<div><strong>Source and instrument metadata</strong></div>
|
||||
return (
|
||||
<SettingsPanel title="Source and instrument metadata" dirty={dirty}
|
||||
onUpload={() => submit(s)} uploadDisabled={pending} snackbar={snackbar}>
|
||||
<TextField id="source_name" label="Source name" variant="outlined"
|
||||
fullWidth
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -104,16 +100,8 @@ function InstrumentMetadata({s: serverS}: MyProps) {
|
||||
}}/>
|
||||
} label="Electron source"/>
|
||||
</FormGroup>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/instrument"}
|
||||
input={JSON.stringify(s)}
|
||||
method={"PUT"}
|
||||
text={"Upload"}
|
||||
/>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(InstrumentMetadata);
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import {memo} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {Grid, Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material";
|
||||
import {Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material";
|
||||
import {measurement_statistics} from "../client";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
|
||||
type MyProps = {
|
||||
s: measurement_statistics,
|
||||
};
|
||||
|
||||
function MeasurementStatistics({s}: MyProps) {
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ height: 550, width: '100%' }}>
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<br/><strong>Measurement statistics</strong><br/><br/>
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Measurement statistics" sx={{ height: 550 }}>
|
||||
<TableContainer component={Paper} style={{marginLeft: "auto", marginRight: "auto"}}>
|
||||
<Table size="small" aria-label="simple table">
|
||||
<TableBody>
|
||||
@@ -75,11 +72,8 @@ function MeasurementStatistics({s}: MyProps) {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<br/>
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MeasurementStatistics);
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
TableContainer,
|
||||
TableRow, TextField
|
||||
} from "@mui/material";
|
||||
import {putConfigUserMaskTiff, pixel_mask_statistics} from "../client";
|
||||
import Button from "@mui/material/Button";
|
||||
import {pixel_mask_statistics} from "../client";
|
||||
import {putConfigUserMaskTiffMutation} from "../client/@tanstack/react-query.gen";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: pixel_mask_statistics
|
||||
@@ -18,11 +20,7 @@ type MyProps = {
|
||||
|
||||
function PixelMask({s}: MyProps) {
|
||||
const [file, setFile] = useState<File>();
|
||||
|
||||
const handleUpload = () => {
|
||||
if (file)
|
||||
putConfigUserMaskTiff({ body: file });
|
||||
};
|
||||
const { submit, pending, snackbar } = useUpload(putConfigUserMaskTiffMutation());
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
@@ -30,47 +28,46 @@ function PixelMask({s}: MyProps) {
|
||||
}
|
||||
};
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{width: '100%'}}>
|
||||
<br/><strong>Pixel mask</strong><br/><br/>
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<TableContainer component={Paper} style={{marginLeft: "auto", marginRight: "auto"}}>
|
||||
<Table size="small" aria-label="simple table">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> User mask: </TableCell>
|
||||
<TableCell align="right">{s?.user_mask ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> Error pixels: </TableCell>
|
||||
<TableCell align="right">{s?.wrong_gain ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> Noisy pixels: </TableCell>
|
||||
<TableCell align="right">{s?.too_high_pedestal_rms ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
return (
|
||||
<SettingsPanel title="Pixel mask"
|
||||
onUpload={() => file && submit(file)}
|
||||
uploadDisabled={!file || pending}
|
||||
snackbar={snackbar}>
|
||||
<Grid container spacing={0}>
|
||||
<Grid item xs={1}/>
|
||||
<Grid item xs={10}>
|
||||
<TableContainer component={Paper} style={{marginLeft: "auto", marginRight: "auto"}}>
|
||||
<Table size="small" aria-label="simple table">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> User mask: </TableCell>
|
||||
<TableCell align="right">{s?.user_mask ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> Error pixels: </TableCell>
|
||||
<TableCell align="right">{s?.wrong_gain ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> Noisy pixels: </TableCell>
|
||||
<TableCell align="right">{s?.too_high_pedestal_rms ?? "N/A"}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
<br/>
|
||||
<a href="/config/mask.tiff">Download pixel mask</a><br/>
|
||||
<a href="/config/user_mask.tiff">Download user pixel mask</a><br/><br/>
|
||||
<TextField
|
||||
type="file"
|
||||
inputProps={{accept: '.tiff'}}
|
||||
onChange={handleFileChange}
|
||||
/><br/><br/>
|
||||
<Button onClick={handleUpload}
|
||||
variant="contained"
|
||||
disableElevation
|
||||
disabled={!file}
|
||||
color={"primary"}>Upload</Button>
|
||||
<br/><br/>
|
||||
</Paper>
|
||||
<div>
|
||||
<a href="/config/mask.tiff">Download pixel mask</a><br/>
|
||||
<a href="/config/user_mask.tiff">Download user pixel mask</a>
|
||||
</div>
|
||||
<TextField
|
||||
type="file"
|
||||
inputProps={{accept: '.tiff'}}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PixelMask);
|
||||
|
||||
@@ -12,7 +12,9 @@ import Button from "@mui/material/Button";
|
||||
import {DataGrid, GridActionsCellItem, GridColDef, GridRowModel} from "@mui/x-data-grid";
|
||||
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import {putConfigRoiMutation} from "../client/@tanstack/react-query.gen";
|
||||
import {UploadSnackbarView} from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
import _ from "lodash";
|
||||
|
||||
type MyProps = {
|
||||
@@ -49,6 +51,7 @@ function ROI({s: serverS}: MyProps) {
|
||||
const [boxErr, setBoxErr] = useState<string>();
|
||||
const [circleErr, setCircleErr] = useState<string>();
|
||||
const [azimErr, setAzimErr] = useState<string | ReactNode>();
|
||||
const { submit, pending, snackbar } = useUpload(putConfigRoiMutation());
|
||||
|
||||
useEffect(() => {
|
||||
if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) {
|
||||
@@ -386,19 +389,15 @@ function ROI({s: serverS}: MyProps) {
|
||||
disableElevation>Add Circle ROI</Button>
|
||||
<Button color="secondary" onClick={addAzimButton} variant="contained"
|
||||
disableElevation>Add Azimuthal ROI</Button>
|
||||
<ButtonWithSnackbar
|
||||
path={"/config/roi"}
|
||||
color={"primary"}
|
||||
method={"PUT"}
|
||||
disabled={(boxErr !== undefined) || (circleErr !== undefined)}
|
||||
text={"Upload"}
|
||||
input={JSON.stringify(roiStruct())}
|
||||
/>
|
||||
<Button color="primary" variant="contained" disableElevation
|
||||
onClick={() => submit(roiStruct())}
|
||||
disabled={pending || (boxErr !== undefined) || (circleErr !== undefined)}>Upload</Button>
|
||||
<br/><br/>
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={1}/>
|
||||
</Grid>
|
||||
<UploadSnackbarView snackbar={snackbar}/>
|
||||
</Paper>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
@@ -1,12 +1,13 @@
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {FormControlLabel, Radio, RadioGroup, Stack} from "@mui/material";
|
||||
import {FormControlLabel, Radio, RadioGroup} from "@mui/material";
|
||||
import NumberTextField from "./NumberTextField";
|
||||
import {zeromq_preview_settings} from "../client";
|
||||
import {putConfigZeromqPreviewMutation} from "../client/@tanstack/react-query.gen";
|
||||
import _ from "lodash";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import ButtonWithSnackbar from "./ButtonWithSnackbar";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {useUpload} from "./useUpload";
|
||||
|
||||
type MyProps = {
|
||||
s?: zeromq_preview_settings
|
||||
@@ -25,6 +26,7 @@ function ZeroMQPreview({s: serverS}: MyProps) {
|
||||
const [downloadCounter, setDownloadCounter] = useState(0);
|
||||
const [freqChoice, setFreqChoice] = useState("custom");
|
||||
const [periodMs, setPeriodMs] = useState(default_zeromq_preview_settings.period_ms);
|
||||
const { submit, pending, snackbar } = useUpload(putConfigZeromqPreviewMutation());
|
||||
|
||||
const update_freq = (choice: string) => {
|
||||
switch (choice) {
|
||||
@@ -69,13 +71,13 @@ function ZeroMQPreview({s: serverS}: MyProps) {
|
||||
const empty = () : boolean =>
|
||||
(s.socket_address === undefined) || (s.socket_address === "");
|
||||
|
||||
return <Paper style={{textAlign: 'center'}} sx={{ width: '100%'}}>
|
||||
<br/>
|
||||
<Stack spacing={3} sx={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<div><strong>ZeroMQ preview settings </strong></div>
|
||||
const dirty = !_.isEqual(s, lastDownloadedS);
|
||||
|
||||
return (
|
||||
<SettingsPanel title="ZeroMQ preview" dirty={dirty}
|
||||
onUpload={() => submit(s)}
|
||||
uploadDisabled={pending || ((freqChoice == "custom") && periodErr)}
|
||||
snackbar={snackbar}>
|
||||
<div>
|
||||
{empty() ? "No preview available" : `ZeroMQ socket: ${s.socket_address}`}
|
||||
</div>
|
||||
@@ -103,17 +105,8 @@ function ZeroMQPreview({s: serverS}: MyProps) {
|
||||
}}
|
||||
disabled={freqChoice != "custom"}
|
||||
fullWidth/>
|
||||
<ButtonWithSnackbar
|
||||
color={"primary"}
|
||||
path={"/config/zeromq_preview"}
|
||||
input={JSON.stringify(s)}
|
||||
method={"PUT"}
|
||||
text={"Upload"}
|
||||
disabled={((freqChoice == "custom") && periodErr)}
|
||||
/>
|
||||
</Stack>
|
||||
<br/>
|
||||
</Paper>
|
||||
</SettingsPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ZeroMQPreview);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
||||
|
||||
/** Pull a human-readable message out of a hey-api error (400 = string, 500 = error_message). */
|
||||
export function errorMessage(error: unknown): string {
|
||||
if (typeof error === "string") return error;
|
||||
if (error && typeof error === "object" && "msg" in error)
|
||||
return String((error as { msg?: unknown }).msg ?? "Unknown error");
|
||||
return "Upload failed";
|
||||
}
|
||||
|
||||
export type UploadSnackbar = {
|
||||
open: boolean;
|
||||
success: boolean;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps a generated hey-api mutation (e.g. putConfigFileWriterMutation()) and
|
||||
* surfaces the result as a snackbar. submit(body) sends the typed body; on
|
||||
* failure the server's error message is shown.
|
||||
*/
|
||||
export function useUpload<TError, TVars extends { body?: unknown }>(
|
||||
mutationOptions: UseMutationOptions<unknown, TError, TVars>,
|
||||
) {
|
||||
const mutation = useMutation(mutationOptions);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const submit = (body: TVars["body"]) =>
|
||||
mutation.mutate({ body } as TVars, { onSettled: () => setOpen(true) });
|
||||
|
||||
const snackbar: UploadSnackbar = {
|
||||
open,
|
||||
success: mutation.isSuccess,
|
||||
message: mutation.error ? errorMessage(mutation.error) : "",
|
||||
onClose: () => setOpen(false),
|
||||
};
|
||||
|
||||
return { submit, pending: mutation.isPending, snackbar };
|
||||
}
|
||||
Reference in New Issue
Block a user