diff --git a/frontend/openapi-ts.config.ts b/frontend/openapi-ts.config.ts index a35193eb..ed71a8a9 100644 --- a/frontend/openapi-ts.config.ts +++ b/frontend/openapi-ts.config.ts @@ -13,5 +13,6 @@ export default defineConfig({ enums: { mode: 'javascript' }, }, '@hey-api/sdk', + '@tanstack/react-query', ], }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 619f1e11..4e746d46 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@mui/icons-material": "^6.1.2", "@mui/material": "^6.1.2", "@mui/x-data-grid": "^7.19.0", + "@tanstack/react-query": "^5.101.0", "@types/node": "^25.2.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -3492,6 +3493,32 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@turf/area": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a28a1bfa..da894318 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "@mui/icons-material": "^6.1.2", "@mui/material": "^6.1.2", "@mui/x-data-grid": "^7.19.0", + "@tanstack/react-query": "^5.101.0", "@types/node": "^25.2.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83974b4d..97194c70 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,8 @@ -import React, {Component} from 'react'; +import React, {useState} from 'react'; import {createTheme, CssBaseline, Grid, Stack, Switch, ThemeProvider, Typography} from "@mui/material"; import {indigo, lime} from "@mui/material/colors"; +import {useQuery} from "@tanstack/react-query"; import DataProcessingSettings from "./components/DataProcessingSettings"; import DetectorSettings from "./components/DetectorSettings"; @@ -18,9 +19,8 @@ import ImageFormatSettings from "./components/ImageFormatSettings"; import InstrumentMetadata from "./components/InstrumentMetadata"; import {JFJOCH_VERSION} from "./version"; import FpgaStatus from "./components/FpgaStatus"; -import {getStatistics, plot_type} from "./client"; -import {client} from "./client/client.gen"; -import {jfjoch_statistics} from "./client"; +import {plot_type} from "./client"; +import {getStatisticsOptions} from "./client/@tanstack/react-query.gen"; import DataCollection from "./components/DataCollection"; import ZeroMQPreview from "./components/ZeroMQPreview"; import PixelMask from "./components/PixelMask"; @@ -38,218 +38,162 @@ const jfjoch_theme = createTheme({ }, }); -type MyState = { - show_detector_setup: boolean, - show_module_calibration: boolean, - show_preview: boolean, - show_roi_setup: boolean, - show_fpga_status: boolean, - connection_error: boolean, - show_data_collection: boolean, - s: jfjoch_statistics +function renderTitleWithSwitch(name: string, checked: boolean, func: ((event: React.ChangeEvent) => void)) { + return + + + + + {name} + + + + + + + } -type MyProps = {} +function App() { + const [showDetectorSetup, setShowDetectorSetup] = useState(false); + const [showModuleCalibration, setShowModuleCalibration] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const [showRoiSetup, setShowRoiSetup] = useState(false); + const [showFpgaStatus, setShowFpgaStatus] = useState(false); + const [showDataCollection, setShowDataCollection] = useState(false); -class App extends Component { - interval: ReturnType | undefined; + // Poll general statistics once per second; structural sharing keeps unchanged + // slices referentially stable so memoized children below only re-render when + // their own slice changes. + const {data, isError, refetch} = useQuery({ + ...getStatisticsOptions(), + refetchInterval: 1000, + }); + const s = isError || data === undefined ? {} : data; - state : MyState = { - show_detector_setup: false, - show_module_calibration: false, - show_preview: false, - show_roi_setup: false, - show_fpga_status: false, - connection_error: true, - show_data_collection: false, - s: {} - } + const isMeasuring = s.broker?.state === 'Measuring'; - getValues = () => { - getStatistics({ throwOnError: true }) - .then(({data}) => this.setState({s: data, connection_error: false})) - .catch(_ => { - this.setState({s: {}, connection_error: true}); - }); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - componentDidMount() { - client.setConfig({ baseUrl: '' }); - this.getValues(); - this.interval = setInterval(() => this.getValues(), 1000); - } - - showPreviewToggle = (event: React.ChangeEvent) => { - this.setState({show_preview: event.target.checked}); - } - - showDetectorSetupToggle = (event: React.ChangeEvent) => { - this.setState({show_detector_setup: event.target.checked}); - } - - showModuleCalibrationToggle = (event: React.ChangeEvent) => { - this.setState({show_module_calibration: event.target.checked}); - } - - showROISetupToggle = (event: React.ChangeEvent) => { - this.setState({show_roi_setup: event.target.checked}); - } - - showFPGAStatusToggle = (event: React.ChangeEvent) => { - this.setState({show_fpga_status: event.target.checked}); - } - - showDataCollectionToggle = (event: React.ChangeEvent) => { - this.setState({show_data_collection: event.target.checked}); - } - - renderTitleWithSwitch = (name: string, checked: boolean, func: ((event: React.ChangeEvent) => void)) => { - return - - - - - {name} - - - - + return + +



+
+ + + + - - - }; - - isMeasuring() : boolean { - return (this.state.s.broker !== undefined) - && (this.state.s.broker.state == 'Measuring'); - } - - render() { - return - -



-
- - - - - - - - - - - - - - - - - - - { - this.renderTitleWithSwitch("Live image preview", this.state.show_preview, this.showPreviewToggle) - } - - {this.state.show_preview ? : ""} - -

- { - this.renderTitleWithSwitch("Data collection", - this.state.show_data_collection, this.showDataCollectionToggle) - } - - - {this.state.show_data_collection ? : ""} - - -

- { - this.renderTitleWithSwitch("Jungfraujoch expert configuration", - this.state.show_detector_setup, this.showDetectorSetupToggle) - } - - {this.state.show_detector_setup ? - - - - - : ""} - - - {this.state.show_detector_setup ? - - - - - - : ""} - - - {this.state.show_detector_setup ? - - - - - - : ""} - -

- { - this.renderTitleWithSwitch("JUNGFRAU module calibration", - this.state.show_module_calibration, this.showModuleCalibrationToggle) - } - - {this.state.show_module_calibration ? : ""} - -

- { - this.renderTitleWithSwitch("Region of interest (ROI)", - this.state.show_roi_setup, this.showROISetupToggle) - } - - {this.state.show_roi_setup ? : ""} - -

- { - this.renderTitleWithSwitch("FPGA status", - this.state.show_fpga_status, this.showFPGAStatusToggle) - } - - {this.state.show_fpga_status ? : ""} - + + + -
-
-
Developed at Paul Scherrer Institute (2019-2024). Main author: Filip Leonarski
- For more information see J. Synchrotron - Rad. (2023). 30, 227–234
- Version: {JFJOCH_VERSION}    - API reference    - Documentation
-
-
- } + + + + + + + + + + { + renderTitleWithSwitch("Live image preview", showPreview, e => setShowPreview(e.target.checked)) + } + + {showPreview ? : ""} + +

+ { + renderTitleWithSwitch("Data collection", + showDataCollection, e => setShowDataCollection(e.target.checked)) + } + + + {showDataCollection ? : ""} + + +

+ { + renderTitleWithSwitch("Jungfraujoch expert configuration", + showDetectorSetup, e => setShowDetectorSetup(e.target.checked)) + } + + {showDetectorSetup ? + + + + + : ""} + + + {showDetectorSetup ? + + + + + + : ""} + + + {showDetectorSetup ? + + + + + + : ""} + +

+ { + renderTitleWithSwitch("JUNGFRAU module calibration", + showModuleCalibration, e => setShowModuleCalibration(e.target.checked)) + } + + {showModuleCalibration ? : ""} + +

+ { + renderTitleWithSwitch("Region of interest (ROI)", + showRoiSetup, e => setShowRoiSetup(e.target.checked)) + } + + {showRoiSetup ? : ""} + +

+ { + renderTitleWithSwitch("FPGA status", + showFpgaStatus, e => setShowFpgaStatus(e.target.checked)) + } + + {showFpgaStatus ? : ""} + +
+
+
+
Developed at Paul Scherrer Institute (2019-2024). Main author: Filip Leonarski
+ For more information see J. Synchrotron + Rad. (2023). 30, 227–234
+ Version: {JFJOCH_VERSION}    + API reference    + Documentation
+
+
} export default App; diff --git a/frontend/src/client/@tanstack/react-query.gen.ts b/frontend/src/client/@tanstack/react-query.gen.ts new file mode 100644 index 00000000..548c3c9d --- /dev/null +++ b/frontend/src/client/@tanstack/react-query.gen.ts @@ -0,0 +1,1317 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; + +import { client } from '../client.gen'; +import { getConfigAzimInt, getConfigDarkMask, getConfigDetector, getConfigFileWriter, getConfigImageFormat, getConfigIndexing, getConfigInstrument, getConfigMask, getConfigMaskTiff, getConfigRoi, getConfigSelectDetector, getConfigSpotFinding, getConfigUserMask, getConfigUserMaskTiff, getConfigZeromqMetadata, getConfigZeromqPreview, getDetectorStatus, getFpgaStatus, getImageBufferImageCbor, getImageBufferImageJpeg, getImageBufferImageTiff, getImageBufferStartCbor, getImageBufferStatus, getImagePusherStatus, getPreviewPedestalTiff, getPreviewPlot, getPreviewPlotBin, getResultScan, getStatistics, getStatisticsCalibration, getStatisticsDataCollection, getStatus, getVersion, getXfelEventCode, getXfelPulseId, type Options, postCancel, postConfigImageFormatConversion, postConfigImageFormatRaw, postDeactivate, postImageBufferClear, postInitialize, postPedestal, postStart, postTrigger, postWaitTillDone, postWaitUntilRunning, putConfigAzimInt, putConfigDarkMask, putConfigDetector, putConfigFileWriter, putConfigImageFormat, putConfigIndexing, putConfigInstrument, putConfigInternalGeneratorImage, putConfigInternalGeneratorImageTiff, putConfigRoi, putConfigSelectDetector, putConfigSpotFinding, putConfigUserMask, putConfigUserMaskTiff, putConfigZeromqMetadata, putConfigZeromqPreview } from '../sdk.gen'; +import type { getConfigAzimIntData, getConfigAzimIntResponse, getConfigDarkMaskData, getConfigDarkMaskResponse, getConfigDetectorData, getConfigDetectorResponse, getConfigFileWriterData, getConfigFileWriterResponse, getConfigImageFormatData, getConfigImageFormatResponse, getConfigIndexingData, getConfigIndexingResponse, getConfigInstrumentData, getConfigInstrumentResponse, getConfigMaskData, getConfigMaskResponse, getConfigMaskTiffData, getConfigMaskTiffResponse, getConfigRoiData, getConfigRoiResponse, getConfigSelectDetectorData, getConfigSelectDetectorResponse, getConfigSpotFindingData, getConfigSpotFindingResponse, getConfigUserMaskData, getConfigUserMaskResponse, getConfigUserMaskTiffData, getConfigUserMaskTiffResponse, getConfigZeromqMetadataData, getConfigZeromqMetadataResponse, getConfigZeromqPreviewData, getConfigZeromqPreviewResponse, getDetectorStatusData, getDetectorStatusError, getDetectorStatusResponse, getFpgaStatusData, getFpgaStatusResponse, getImageBufferImageCborData, getImageBufferImageCborError, getImageBufferImageCborResponse, getImageBufferImageJpegData, getImageBufferImageJpegError, getImageBufferImageJpegResponse, getImageBufferImageTiffData, getImageBufferImageTiffResponse, getImageBufferStartCborData, getImageBufferStartCborError, getImageBufferStartCborResponse, getImageBufferStatusData, getImageBufferStatusError, getImageBufferStatusResponse, getImagePusherStatusData, getImagePusherStatusError, getImagePusherStatusResponse, getPreviewPedestalTiffData, getPreviewPedestalTiffResponse, getPreviewPlotBinData, getPreviewPlotBinError, getPreviewPlotBinResponse, getPreviewPlotData, getPreviewPlotError, getPreviewPlotResponse, getResultScanData, getResultScanError, getResultScanResponse, getStatisticsCalibrationData, getStatisticsCalibrationResponse, getStatisticsData, getStatisticsDataCollectionData, getStatisticsDataCollectionResponse, getStatisticsResponse, getStatusData, getStatusResponse, getVersionData, getVersionResponse, getXfelEventCodeData, getXfelEventCodeResponse, getXfelPulseIdData, getXfelPulseIdResponse, postCancelData, postConfigImageFormatConversionData, postConfigImageFormatConversionError, postConfigImageFormatRawData, postConfigImageFormatRawError, postDeactivateData, postDeactivateError, postImageBufferClearData, postImageBufferClearError, postInitializeData, postInitializeError, postPedestalData, postPedestalError, postStartData, postStartError, postTriggerData, postWaitTillDoneData, postWaitTillDoneError, postWaitUntilRunningData, postWaitUntilRunningError, putConfigAzimIntData, putConfigAzimIntError, putConfigDarkMaskData, putConfigDarkMaskError, putConfigDetectorData, putConfigDetectorError, putConfigFileWriterData, putConfigFileWriterError, putConfigImageFormatData, putConfigImageFormatError, putConfigIndexingData, putConfigIndexingError, putConfigInstrumentData, putConfigInstrumentError, putConfigInternalGeneratorImageData, putConfigInternalGeneratorImageError, putConfigInternalGeneratorImageTiffData, putConfigInternalGeneratorImageTiffError, putConfigRoiData, putConfigRoiError, putConfigSelectDetectorData, putConfigSelectDetectorError, putConfigSpotFindingData, putConfigSpotFindingError, putConfigUserMaskData, putConfigUserMaskError, putConfigUserMaskTiffData, putConfigUserMaskTiffError, putConfigZeromqMetadataData, putConfigZeromqMetadataError, putConfigZeromqPreviewData, putConfigZeromqPreviewError } from '../types.gen'; + +/** + * Initialize detector and data acquisition + * + * Should be used in two cases: + * - Detector is in `Inactive` state + * - Detector is in `Error` state + * X-ray shutter must be closed. + * This operation will reconfigure network interface of the detector. + * During operation of the detector it is recommended to use the `POST /pedestal` operation instead. + * If storage cells are used, the execution time might be few minutes. + * + * This is async function - one needs to use `POST /wait_till_done` to ensure operation is done. + * + */ +export const postInitializeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postInitialize({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Collect dark current for the detector + * + * Updates calibration of the JUNGFRAU detector. Must be in `Idle` state. + * + * X-ray shutter must be closed. Recommended to run once per hour for long integration times (> 100 us). + * + * This is async function - one needs to use `POST /wait_till_done` to ensure operation is done. + * + */ +export const postPedestalMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postPedestal({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Start detector + * + * Start data acquisition. + * Detector must be in `Idle` state. + * Default behavior is for the call to block until detector is ready to accept soft/TTL triggers. + * However, this behavior can be changed by settings `async_start` to true in the request body, + * in which case the call will return immediately and one needs to use `/wait_until_running` to ensure detector is ready to run. + * + */ +export const postStartMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postStart({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Wait for acquisition running + * + * Block execution of external script till detector and Jungfraujoch are ready to collect data. + * To not block web server for a indefinite period of time, the procedure is provided with a timeout. + * Extending timeout is possible, but requires to ensure safety that client will not close the connection and retry the connection. + * + */ +export const postWaitUntilRunningMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postWaitUntilRunning({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Wait for acquisition done + * + * Block execution of external script till initialization, data collection or pedestal is finished. + * Running this command does not affect (cancel) running data collection, it is only to ensure synchronous execution of other software. + * + * To not block web server for a indefinite period of time, the procedure is provided with a timeout. + * Extending timeout is possible, but requires to ensure safety that client will not close the connection and retry the connection. + * + */ +export const postWaitTillDoneMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postWaitTillDone({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Send soft trigger to the detector + * + * Generate soft trigger + */ +export const postTriggerMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postTrigger({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Cancel running data collection + * + * Command will inform FPGA network card to stop pedestal or data collection at the current stage. + * Any frame that is currently being processed by CPU will be finished and sent to writer. + * Given the command is making sure to gracefully stop data acquisition and detector, it might take some time to switch back after command finished to `Idle` state. + * + * If data collection is not running, the command has no effect. + * + */ +export const postCancelMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postCancel({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Prepare detector to turn off + * + * Should be in `Idle` or `Error` state. + * Command deactivates data acquisition and turns off detector high voltage and ASIC. + * Should be used always before turning off power from the detector. + * + */ +export const postDeactivateMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postDeactivate({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + } +]; + +const createQueryKey = (id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray): [ + QueryKey[0] +] => { + const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getConfigDetectorQueryKey = (options?: Options) => createQueryKey('getConfigDetector', options); + +/** + * Get detector configuration + * + * Can be done anytime + */ +export const getConfigDetectorOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigDetector({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigDetectorQueryKey(options) +}); + +/** + * Change detector configuration + * + * Detector settings are ones that have effect on calibration, i.e., pedestal has to be collected again after changing these settings. + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * If detector is in `Idle` state , pedestal procedure will be executed automatically - there must be no X-rays on the detector during the operation. + * If detector is in `Inactive` or `Error` states, new settings will be saved, but no calibration will be executed. + * + */ +export const putConfigDetectorMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigDetector({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigIndexingQueryKey = (options?: Options) => createQueryKey('getConfigIndexing', options); + +/** + * Get indexing configuration + * + * Can be done anytime + */ +export const getConfigIndexingOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigIndexing({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigIndexingQueryKey(options) +}); + +/** + * Change indexing algorithm settings + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const putConfigIndexingMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigIndexing({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigFileWriterQueryKey = (options?: Options) => createQueryKey('getConfigFileWriter', options); + +/** + * Get file writer settings + * + * Can be done anytime + */ +export const getConfigFileWriterOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigFileWriter({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigFileWriterQueryKey(options) +}); + +/** + * Change file writer settings + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const putConfigFileWriterMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigFileWriter({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigInstrumentQueryKey = (options?: Options) => createQueryKey('getConfigInstrument', options); + +/** + * Get instrument metadata + * + * Can be done anytime + */ +export const getConfigInstrumentOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigInstrument({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigInstrumentQueryKey(options) +}); + +/** + * Change instrument metadata + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const putConfigInstrumentMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigInstrument({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigImageFormatQueryKey = (options?: Options) => createQueryKey('getConfigImageFormat', options); + +/** + * Get image output format + * + * Can be done anytime + */ +export const getConfigImageFormatOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigImageFormat({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigImageFormatQueryKey(options) +}); + +/** + * Change image output format + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const putConfigImageFormatMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigImageFormat({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Configure format for raw data collection + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const postConfigImageFormatRawMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postConfigImageFormatRaw({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Configure format for data collection with full conversion + * + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const postConfigImageFormatConversionMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postConfigImageFormatConversion({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigSpotFindingQueryKey = (options?: Options) => createQueryKey('getConfigSpotFinding', options); + +/** + * Get data processing configuration + * + * Can be done anytime + */ +export const getConfigSpotFindingOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigSpotFinding({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigSpotFindingQueryKey(options) +}); + +/** + * Configure spot finding + * + * Can be done anytime, also while data collection is running + */ +export const putConfigSpotFindingMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigSpotFinding({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigAzimIntQueryKey = (options?: Options) => createQueryKey('getConfigAzimInt', options); + +/** + * Get azimuthal integration configuration + * + * Can be done anytime + */ +export const getConfigAzimIntOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigAzimInt({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigAzimIntQueryKey(options) +}); + +/** + * Configure azimuthal integration + * + * Can be done when detector is Inactive or Idle + */ +export const putConfigAzimIntMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigAzimInt({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Load binary image for internal FPGA generator + * + * Load image for internal FPGA generator. This can only happen in Idle state of the detector. + * Requires binary blob with 16-bit integer numbers of size of detector in raw/converted coordinates + * (depending on detector settings). + * + */ +export const putConfigInternalGeneratorImageMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigInternalGeneratorImage({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Load TIFF image for internal FPGA generator + * + * Load image for internal FPGA generator. This can only happen in Idle state of the detector. + * Requires TIFF with 16-bit integer numbers of size of detector in raw/converted coordinates + * (depending on detector settings). + * + */ +export const putConfigInternalGeneratorImageTiffMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigInternalGeneratorImageTiff({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigSelectDetectorQueryKey = (options?: Options) => createQueryKey('getConfigSelectDetector', options); + +/** + * List available detectors + * + * Configured detectors that can be selected by used + */ +export const getConfigSelectDetectorOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigSelectDetector({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigSelectDetectorQueryKey(options) +}); + +/** + * Select detector + * + * Jungfraujoch allows to control multiple detectors and/or region-of-interests. + * The command allows to choose one detector from the list (ID has to be consistent with one provided by GET response). + * Changing detector will set detector to `Inactive` state and will require reinitialization. + * + */ +export const putConfigSelectDetectorMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigSelectDetector({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigZeromqPreviewQueryKey = (options?: Options) => createQueryKey('getConfigZeromqPreview', options); + +/** + * Get ZeroMQ preview settings + */ +export const getConfigZeromqPreviewOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigZeromqPreview({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigZeromqPreviewQueryKey(options) +}); + +/** + * Set ZeroMQ preview settings + * + * Jungfraujoch can generate preview message stream on ZeroMQ SUB socket. + * Here settings of the socket can be adjusted. + * While the data structure contains also socket_address, this cannot be changed via HTTP and is ignore in PUT request. + * Options set with this PUT request have no effect on HTTP based preview. + * + */ +export const putConfigZeromqPreviewMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigZeromqPreview({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigZeromqMetadataQueryKey = (options?: Options) => createQueryKey('getConfigZeromqMetadata', options); + +/** + * Get ZeroMQ metadata socket settings + */ +export const getConfigZeromqMetadataOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigZeromqMetadata({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigZeromqMetadataQueryKey(options) +}); + +/** + * Set ZeroMQ metadata settings + * + * Jungfraujoch can generate metadata message stream on ZeroMQ PUB socket. This stream covers all images. + * Here settings of the socket can be adjusted. + * While the data structure contains also socket_address, this cannot be changed via HTTP and is ignore in PUT request. + * + */ +export const putConfigZeromqMetadataMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigZeromqMetadata({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigDarkMaskQueryKey = (options?: Options) => createQueryKey('getConfigDarkMask', options); + +/** + * Get settings for dark data collection to calculate mask + */ +export const getConfigDarkMaskOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigDarkMask({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigDarkMaskQueryKey(options) +}); + +/** + * Set configuration for dark data collection to calculate mask + * + * This is only possible when operating DECTRIS detectors at the moment; it will be also available for PSI EIGER at some point. + * This can only be done when detector is `Idle`, `Error` or `Inactive` states. + * + */ +export const putConfigDarkMaskMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigDarkMask({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getStatusQueryKey = (options?: Options) => createQueryKey('getStatus', options); + +/** + * Get Jungfraujoch status + * + * Status of the data acquisition + */ +export const getStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getStatusQueryKey(options) +}); + +export const getFpgaStatusQueryKey = (options?: Options) => createQueryKey('getFpgaStatus', options); + +/** + * Get status of FPGA devices + */ +export const getFpgaStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getFpgaStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getFpgaStatusQueryKey(options) +}); + +export const getXfelPulseIdQueryKey = (options?: Options) => createQueryKey('getXfelPulseId', options); + +/** + * Return XFEL pulse IDs for the current data acquisition + * + * Return array of XFEL pulse IDs - (-1) if image not recorded + */ +export const getXfelPulseIdOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getXfelPulseId({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getXfelPulseIdQueryKey(options) +}); + +export const getXfelEventCodeQueryKey = (options?: Options) => createQueryKey('getXfelEventCode', options); + +/** + * Return XFEL event codes for the current data acquisition + * + * Return array of XFEL event codes + */ +export const getXfelEventCodeOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getXfelEventCode({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getXfelEventCodeQueryKey(options) +}); + +export const getImagePusherStatusQueryKey = (options?: Options) => createQueryKey('getImagePusherStatus', options); + +/** + * Get status of image pusher + */ +export const getImagePusherStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImagePusherStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImagePusherStatusQueryKey(options) +}); + +export const getDetectorStatusQueryKey = (options?: Options) => createQueryKey('getDetectorStatus', options); + +/** + * Get detector status + * + * Status of the JUNGFRAU detector + */ +export const getDetectorStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getDetectorStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getDetectorStatusQueryKey(options) +}); + +export const getConfigRoiQueryKey = (options?: Options) => createQueryKey('getConfigRoi', options); + +/** + * Get ROI definitions + */ +export const getConfigRoiOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigRoi({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigRoiQueryKey(options) +}); + +/** + * Upload ROI definitions + */ +export const putConfigRoiMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigRoi({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getStatisticsQueryKey = (options?: Options) => createQueryKey('getStatistics', options); + +/** + * Get general statistics + */ +export const getStatisticsOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStatistics({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getStatisticsQueryKey(options) +}); + +export const getStatisticsDataCollectionQueryKey = (options?: Options) => createQueryKey('getStatisticsDataCollection', options); + +/** + * Get data collection statistics + * + * Results of the last data collection + */ +export const getStatisticsDataCollectionOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStatisticsDataCollection({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getStatisticsDataCollectionQueryKey(options) +}); + +export const getStatisticsCalibrationQueryKey = (options?: Options) => createQueryKey('getStatisticsCalibration', options); + +/** + * Get calibration statistics + * + * Statistics are provided for each module/storage cell separately + */ +export const getStatisticsCalibrationOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStatisticsCalibration({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getStatisticsCalibrationQueryKey(options) +}); + +export const getConfigMaskQueryKey = (options?: Options) => createQueryKey('getConfigMask', options); + +/** + * Get mask of the detector (binary) + * + * Detector must be Initialized. + * Get full pixel mask of the detector. + * See NXmx standard for meaning of pixel values. + * + */ +export const getConfigMaskOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigMask({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigMaskQueryKey(options) +}); + +export const getConfigUserMaskQueryKey = (options?: Options) => createQueryKey('getConfigUserMask', options); + +/** + * Detector must be Initialized. + * Get user mask of the detector (binary) + * + * + * Get user pixel mask of the detector in the actual detector coordinates: 0 - good pixel, 1 - masked + */ +export const getConfigUserMaskOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigUserMask({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigUserMaskQueryKey(options) +}); + +/** + * Upload user mask of the detector (binary) + * + * Should be in `Idle` state. + * Upload user mask of the detector - this is for example to account for beam stop shadow or misbehaving regions. + * If detector is conversion mode the mask can be both in raw (1024x512; stacked modules) or converted coordinates. + * In the latter case - module gaps are ignored and don't need to be assigned value. + * Mask is expected as binary array (4-byte; unsigned). + * 0 - good pixel, other value - masked + * User mask is stored in NXmx pixel mask (bit 8), as well as used in spot finding and azimuthal integration. + * + */ +export const putConfigUserMaskMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigUserMask({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getConfigMaskTiffQueryKey = (options?: Options) => createQueryKey('getConfigMaskTiff', options); + +/** + * Get mask of the detector (TIFF) + * + * Should be in `Idle` state. + * Get full pixel mask of the detector + * See NXmx standard for meaning of pixel values + * + */ +export const getConfigMaskTiffOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigMaskTiff({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigMaskTiffQueryKey(options) +}); + +export const getConfigUserMaskTiffQueryKey = (options?: Options) => createQueryKey('getConfigUserMaskTiff', options); + +/** + * Detector must be Initialized. + * Get user mask of the detector (TIFF) + * + * + * Get user pixel mask of the detector in the actual detector coordinates: 0 - good pixel, 1 - masked + */ +export const getConfigUserMaskTiffOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getConfigUserMaskTiff({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getConfigUserMaskTiffQueryKey(options) +}); + +/** + * Upload user mask of the detector + * + * Should be in `Idle` state. + * Upload user mask of the detector - this is for example to account for beam stop shadow or misbehaving regions. + * If detector is conversion mode the mask can be both in raw (1024x512; stacked modules) or converted coordinates. + * In the latter case - module gaps are ignored and don't need to be assigned value. + * Mask is expected as a single-channel TIFF (8-, 16- or 32-bit integer, signed or unsigned). + * 0 - good pixel, other value - masked + * User mask is stored in NXmx pixel mask (bit 8), as well as used in spot finding and azimuthal integration. + * User mask is not automatically applied - i.e. pixels with user mask will have a valid pixel value in the images. + * + */ +export const putConfigUserMaskTiffMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await putConfigUserMaskTiff({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getPreviewPedestalTiffQueryKey = (options: Options) => createQueryKey('getPreviewPedestalTiff', options); + +/** + * Get pedestal in TIFF format + */ +export const getPreviewPedestalTiffOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPreviewPedestalTiff({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPreviewPedestalTiffQueryKey(options) +}); + +export const getPreviewPlotQueryKey = (options: Options) => createQueryKey('getPreviewPlot', options); + +/** + * Generate 1D plot from Jungfraujoch + */ +export const getPreviewPlotOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPreviewPlot({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPreviewPlotQueryKey(options) +}); + +export const getPreviewPlotBinQueryKey = (options: Options) => createQueryKey('getPreviewPlotBin', options); + +/** + * Generate 1D plot from Jungfraujoch and send in raw binary format. + * Data are provided as (32-bit) float binary array. + * This format doesn't transmit information about X-axis, only values, so it is of limited use for azimuthal integration. + * + */ +export const getPreviewPlotBinOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPreviewPlotBin({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPreviewPlotBinQueryKey(options) +}); + +export const getResultScanQueryKey = (options?: Options) => createQueryKey('getResultScan', options); + +/** + * Get full scan result + */ +export const getResultScanOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getResultScan({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getResultScanQueryKey(options) +}); + +export const getImageBufferStartCborQueryKey = (options?: Options) => createQueryKey('getImageBufferStartCbor', options); + +/** + * Get Start message in CBOR format + * + * Contains metadata for a dataset (e.g., experimental geometry) + */ +export const getImageBufferStartCborOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImageBufferStartCbor({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImageBufferStartCborQueryKey(options) +}); + +export const getImageBufferImageCborQueryKey = (options?: Options) => createQueryKey('getImageBufferImageCbor', options); + +/** + * Get image message in CBOR format + * + * Contains full image data and metadata. The image must come from the latest data collection. + */ +export const getImageBufferImageCborOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImageBufferImageCbor({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImageBufferImageCborQueryKey(options) +}); + +export const getImageBufferImageJpegQueryKey = (options?: Options) => createQueryKey('getImageBufferImageJpeg', options); + +/** + * Get preview image in JPEG format using custom settings + */ +export const getImageBufferImageJpegOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImageBufferImageJpeg({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImageBufferImageJpegQueryKey(options) +}); + +export const getImageBufferImageTiffQueryKey = (options?: Options) => createQueryKey('getImageBufferImageTiff', options); + +/** + * Get preview image in TIFF format + */ +export const getImageBufferImageTiffOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImageBufferImageTiff({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImageBufferImageTiffQueryKey(options) +}); + +/** + * Clear image buffer + * + * Turns off image buffer for the last data collection. Can be only run when Jungfraujoch is not collecting data. + */ +export const postImageBufferClearMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postImageBufferClear({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getImageBufferStatusQueryKey = (options?: Options) => createQueryKey('getImageBufferStatus', options); + +/** + * Get status of the image buffers + * + * Can be run at any stage of Jungfraujoch operation, including during data collection. + * The status of the image buffer is volatile during data collection - if data collection goes for more images than available buffer slots, + * then image might be replaced in the buffer between calling /images and /image.cbor. + * + */ +export const getImageBufferStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getImageBufferStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getImageBufferStatusQueryKey(options) +}); + +export const getVersionQueryKey = (options?: Options) => createQueryKey('getVersion', options); + +/** + * Get Jungfraujoch version of jfjoch_broker + */ +export const getVersionOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getVersion({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getVersionQueryKey(options) +}); diff --git a/frontend/src/components/AzIntSettings.tsx b/frontend/src/components/AzIntSettings.tsx index 4594813b..af00ab69 100644 --- a/frontend/src/components/AzIntSettings.tsx +++ b/frontend/src/components/AzIntSettings.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import {ChangeEvent, memo, useEffect, useState} from 'react'; import Paper from '@mui/material/Paper'; -import {FormControlLabel, Checkbox, Stack, Tooltip, ListItem, List, Radio, RadioGroup, Typography} from "@mui/material"; +import {FormControlLabel, Checkbox, Stack, Radio, RadioGroup, Typography} from "@mui/material"; import NumberTextField from "./NumberTextField"; import {azim_int_settings} from "../client"; import _ from "lodash"; @@ -12,15 +12,6 @@ type MyProps = { s?: azim_int_settings }; -type MyState = { - s: azim_int_settings - last_downloaded_s: azim_int_settings - download_counter: number - low_q_error: boolean - high_q_error: boolean - q_spacing_error: boolean -}; - const default_azim_int_settings : azim_int_settings = { solid_angle_corr: true, polarization_corr: true, @@ -31,163 +22,139 @@ const default_azim_int_settings : azim_int_settings = { force_cpu: false } -class AzIntSettings extends React.Component { - state : MyState = { - s: default_azim_int_settings, - last_downloaded_s: default_azim_int_settings, - download_counter: 0, - low_q_error: false, - high_q_error: false, - q_spacing_error: false, - } +function AzIntSettings({s: serverS}: MyProps) { + const [s, setS] = useState(default_azim_int_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_azim_int_settings); + const [downloadCounter, setDownloadCounter] = useState(0); + const [lowQError, setLowQError] = useState(false); + const [highQError, setHighQError] = useState(false); + const [qSpacingError, setQSpacingError] = useState(false); - getValues = () => { - if (this.props.s !== undefined) { - let format_set: azim_int_settings = this.props.s; - if (!_.isEqual(format_set, this.state.last_downloaded_s)) { - this.setState(prevState => ({ - s: format_set, - last_downloaded_s: format_set, - download_counter: prevState.download_counter + 1 - })); - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); + setDownloadCounter(c => c + 1); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } + return +
+ +
Azimuthal integration settings
+ + { + setS(prev => ({...prev, q_spacing: val})); + setQSpacingError(err); + }} + fullWidth/> - componentDidUpdate() { - this.getValues(); - } + { + setS(prev => ({...prev, low_q_recipA: val})); + setLowQError(err); + }} + fullWidth/> - render() { - return -
- -
Azimuthal integration settings
- - { - this.setState(prevState => ({ - s: {...prevState.s, q_spacing: val}, - q_spacing_error: err - })); - }} - fullWidth/> - - { - this.setState(prevState => ({ - s: {...prevState.s, low_q_recipA: val}, - low_q_error: err - })); - }} - fullWidth/> - - { - this.setState(prevState => ({ - s: {...prevState.s, high_q_recipA: val}, - high_q_error: err - })); - }} - fullWidth/> - - - ) => { - this.state.s.solid_angle_corr = event.target.checked; - }} - />} label="Solid angle correction"/> - ) => { - this.state.s.polarization_corr = event.target.checked; - }} - />} label="Polarization correction"/> - ) => { - this.state.s.force_cpu = event.target.checked; - }} - />} label="Force CPU calculation in FPGA workflow"/> - - Azimuthal bins - - ) => { - this.setState(prevState => ({ - s: {...prevState.s, azimuthal_bins: Number(event.target.value)} - })); - }} - > - {[1, 2, 4, 8, 16, 32, 64, 128].map((value) => ( - } - label={value.toString()} - /> - ))} - - - - + { + setS(prev => ({...prev, high_q_recipA: val})); + setHighQError(err); + }} + fullWidth/>
-
-
- } + + ) => { + setS(prev => ({...prev, solid_angle_corr: event.target.checked})); + }} + />} label="Solid angle correction"/> + ) => { + setS(prev => ({...prev, polarization_corr: event.target.checked})); + }} + />} label="Polarization correction"/> + ) => { + setS(prev => ({...prev, force_cpu: event.target.checked})); + }} + />} label="Force CPU calculation in FPGA workflow"/> + + Azimuthal bins + + ) => { + setS(prev => ({...prev, azimuthal_bins: Number(event.target.value)})); + }} + > + {[1, 2, 4, 8, 16, 32, 64, 128].map((value) => ( + } + label={value.toString()} + /> + ))} + + + + +
+
+
} -export default AzIntSettings; +export default memo(AzIntSettings); diff --git a/frontend/src/components/ButtonWithSnackbar.tsx b/frontend/src/components/ButtonWithSnackbar.tsx index 1a91afc8..7e000430 100644 --- a/frontend/src/components/ButtonWithSnackbar.tsx +++ b/frontend/src/components/ButtonWithSnackbar.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {memo, SyntheticEvent, useState} from 'react'; import { Alert, SnackbarCloseReason @@ -15,86 +15,75 @@ type MyProps = { method?: "GET" | "POST" | "PUT" } -type MyState = { - snackbar_open: boolean, - start_success: boolean, - start_error?: string -} +function ButtonWithSnackbar({input, disabled, path, text, color, method}: MyProps) { + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [startSuccess, setStartSuccess] = useState(true); + const [startError, setStartError] = useState(); -class ButtonWithSnackbar extends Component { - state : MyState = { - snackbar_open: false, - start_success: true - } + const printError = (msg: string) => { + setSnackbarOpen(true); + setStartSuccess(false); + setStartError(msg); + }; - - printError = (msg: string) => { - this.setState({ - snackbar_open: true, - start_success: false, - start_error: msg - }); - } - - handleResponse = (response : Response) => { + const handleResponse = (response : Response) => { if (response.ok) { - this.setState({snackbar_open: true, start_success: true}); + setSnackbarOpen(true); + setStartSuccess(true); } else { if (response.status == 404) - this.printError("404: Service not found"); + printError("404: Service not found"); else if (response.status == 400) - this.printError("400: Input parsing or validation error"); + printError("400: Input parsing or validation error"); else if (response.status == 500) { response.text().then((val: string) => { try { - this.printError(JSON.parse(val).msg); + printError(JSON.parse(val).msg); } catch (e) { - this.printError("500: Unknown error"); + printError("500: Unknown error"); } }); } } - } + }; - startButton = () => { - fetch(this.props.path, { - method: (this.props.method === undefined) ? "POST" : this.props.method, - body: this.props.input - }).then(this.handleResponse); - } + const startButton = () => { + fetch(path, { + method: (method === undefined) ? "POST" : method, + body: input + }).then(handleResponse); + }; - handleClose = (event?: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => { + const handleClose = (event?: SyntheticEvent | Event, reason?: SnackbarCloseReason) => { if (reason === 'clickaway') { return; } - this.setState({snackbar_open: false}); + setSnackbarOpen(false); }; - render() { - return <> - + + - {this.props.text} - - - - {this.state.start_success ? "Ok!" : this.state.start_error} - - - - } + {startSuccess ? "Ok!" : startError} + + + } -export default ButtonWithSnackbar; \ No newline at end of file +export default memo(ButtonWithSnackbar); diff --git a/frontend/src/components/Calibration.tsx b/frontend/src/components/Calibration.tsx index 4b9e9550..9f569922 100644 --- a/frontend/src/components/Calibration.tsx +++ b/frontend/src/components/Calibration.tsx @@ -1,5 +1,4 @@ - -import React from 'react'; +import {memo} from 'react'; import Paper from '@mui/material/Paper'; import {DataGrid, GridColDef} from "@mui/x-data-grid"; @@ -9,37 +8,32 @@ type MyProps = { s?: calibration_statistics }; -type MyState = {}; +const columns : GridColDef[] = [ + { field: 'module_number', type: 'number', headerName: 'Module' }, + { field: 'storage_cell_number', type: 'number', headerName: 'SC'}, + { field: 'masked_pixels', headerName: 'Masked pxls', type: 'number'}, + { field: 'pedestal_g0_mean', headerName: 'Pedestal G0', type: 'number'}, + { field: 'pedestal_g1_mean', headerName: 'Pedestal G1', type: 'number'}, + { field: 'pedestal_g2_mean', headerName: 'Pedestal G2', type: 'number'}, + { field: 'gain_g0_mean', headerName: 'Gain G0', type: 'number'}, + { field: 'gain_g1_mean', headerName: 'Gain G1', type: 'number'}, + { field: 'gain_g2_mean', headerName: 'Gain G2', type: 'number'} +]; -class Calibration extends React.Component { - columns : GridColDef[] = [ - { field: 'module_number', type: 'number', headerName: 'Module' }, - { field: 'storage_cell_number', type: 'number', headerName: 'SC'}, - { field: 'masked_pixels', headerName: 'Masked pxls', type: 'number'}, - { field: 'pedestal_g0_mean', headerName: 'Pedestal G0', type: 'number'}, - { field: 'pedestal_g1_mean', headerName: 'Pedestal G1', type: 'number'}, - { field: 'pedestal_g2_mean', headerName: 'Pedestal G2', type: 'number'}, - { field: 'gain_g0_mean', headerName: 'Gain G0', type: 'number'}, - { field: 'gain_g1_mean', headerName: 'Gain G1', type: 'number'}, - { field: 'gain_g2_mean', headerName: 'Gain G2', type: 'number'} - ]; - - render() { - return - {((this.props.s !== undefined) && (this.props.s.length > 0)) ? - Number(row.module_number) * 64 + Number(row.storage_cell_number)} - rows={this.props.s} - columns={this.columns} - initialState={{ - pagination: { paginationModel: { pageSize: 8 } }, - }} - pageSizeOptions={[4,8,16]} - /> :
- } - - - } +function Calibration({s}: MyProps) { + return + {((s !== undefined) && (s.length > 0)) ? + Number(row.module_number) * 64 + Number(row.storage_cell_number)} + rows={s} + columns={columns} + initialState={{ + pagination: { paginationModel: { pageSize: 8 } }, + }} + pageSizeOptions={[4,8,16]} + /> :
+ } + } -export default Calibration; +export default memo(Calibration); diff --git a/frontend/src/components/DarkMaskSettings.tsx b/frontend/src/components/DarkMaskSettings.tsx index a4ff4ec5..9dac096c 100644 --- a/frontend/src/components/DarkMaskSettings.tsx +++ b/frontend/src/components/DarkMaskSettings.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import {memo, useEffect, useState} from 'react'; import Paper from '@mui/material/Paper'; import {Checkbox, FormControlLabel, Stack} from '@mui/material'; import _ from 'lodash'; @@ -10,18 +10,6 @@ type MyProps = { s?: dark_mask_settings; }; -type MyState = { - s: dark_mask_settings; - last_downloaded_s: dark_mask_settings; - download_counter: number; - threshold_err: boolean; - frame_time_err: boolean; - number_of_frames_err: boolean; - number_of_frames_old: number; - max_pixel_count_err: boolean; - max_frames_with_signal_err: boolean; -}; - const default_dark_mask_settings: dark_mask_settings = { detector_threshold_keV: 4.0, frame_time_us: 1000, @@ -30,203 +18,161 @@ const default_dark_mask_settings: dark_mask_settings = { max_frames_with_signal: 10, }; -class DarkMaskSettings extends Component { - state: MyState = { - s: default_dark_mask_settings, - last_downloaded_s: default_dark_mask_settings, - download_counter: 0, - threshold_err: false, - frame_time_err: false, - number_of_frames_err: false, - max_pixel_count_err: false, - max_frames_with_signal_err: false, - number_of_frames_old: 1000 - }; +function DarkMaskSettings({s: serverS}: MyProps) { + const [s, setS] = useState(default_dark_mask_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_dark_mask_settings); + const [downloadCounter, setDownloadCounter] = useState(0); + const [thresholdErr, setThresholdErr] = useState(false); + const [frameTimeErr, setFrameTimeErr] = useState(false); + const [numberOfFramesErr, setNumberOfFramesErr] = useState(false); + const [numberOfFramesOld, setNumberOfFramesOld] = useState(1000); + const [maxPixelCountErr, setMaxPixelCountErr] = useState(false); + const [maxFramesWithSignalErr, setMaxFramesWithSignalErr] = useState(false); - getValues = () => { - if (this.props.s !== undefined) { - let incoming: dark_mask_settings = this.props.s; - if (!_.isEqual(incoming, this.state.last_downloaded_s)) { - this.setState((prev) => ({ - s: incoming, - last_downloaded_s: incoming, - download_counter: prev.download_counter + 1, - threshold_err: false, - frame_time_err: false, - number_of_frames_err: false, - number_of_frames_old: - incoming.number_of_frames === 0 ? prev.number_of_frames_old : incoming.number_of_frames, - max_pixel_count_err: false, - max_frames_with_signal_err: false, - enable: (this.props.s?.number_of_frames !== 0) - })); - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); + setDownloadCounter(c => c + 1); + setThresholdErr(false); + setFrameTimeErr(false); + setNumberOfFramesErr(false); + setNumberOfFramesOld(serverS.number_of_frames === 0 ? numberOfFramesOld : serverS.number_of_frames); + setMaxPixelCountErr(false); + setMaxFramesWithSignalErr(false); } - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } + const hasError = () => + thresholdErr || + frameTimeErr || + numberOfFramesErr || + maxPixelCountErr || + maxFramesWithSignalErr; - componentDidUpdate() { - this.getValues(); - } + return ( + +
+ + Dark data collection settings for mask +
(DECTRIS detectors only)
- hasError = () => - this.state.threshold_err || - this.state.frame_time_err || - this.state.number_of_frames_err || - this.state.max_pixel_count_err || - this.state.max_frames_with_signal_err; + { + setDownloadCounter(c => c + 1); + setNumberOfFramesErr(false); + if (!event.target.checked) + setS(prev => ({...prev, number_of_frames: 0})); + else + setS(prev => ({...prev, number_of_frames: numberOfFramesOld})); + }} + /> + } + label="Enable dark data collection" + /> - render() { - return ( - -
- - Dark data collection settings for mask -
(DECTRIS detectors only)
+ - { - if (!event.target.checked) - this.setState(prevState => ({ - download_counter: this.state.download_counter + 1, - number_of_frames_err: false, - s: { - ...prevState.s, - number_of_frames: 0 - } - })); - else - this.setState(prevState => ({ - download_counter: this.state.download_counter + 1, - number_of_frames_err: false, - s: { - ...prevState.s, - number_of_frames: this.state.number_of_frames_old - } - })); - }} - /> - } - label="Enable dark data collection" + { + setThresholdErr(err); + setS(prev => ({ ...prev, detector_threshold_keV: val })); + }} /> - + { + setFrameTimeErr(err); + setS(prev => ({ ...prev, frame_time_us: val * 1000 })); + }} + /> + { + if (!err) + setNumberOfFramesOld(val); + setNumberOfFramesErr(err); + setS(prev => ({...prev, number_of_frames: val})); + }} - { - this.setState((prev) => ({ - threshold_err: err, - s: { ...prev.s, detector_threshold_keV: val }, - })); - }} - /> - - { - this.setState((prev) => ({ - frame_time_err: err, - s: { ...prev.s, frame_time_us: val * 1000 }, - })); - }} - /> - { - let old: number = this.state.number_of_frames_old; - if (!err) - old = val; - this.setState(prevState => ({ - number_of_frames_err: err, - number_of_frames_old: old, - s: {...prevState.s, number_of_frames: val} - })); - }} - - /> - - - { - this.setState((prev) => ({ - max_pixel_count_err: err, - s: { ...prev.s, max_allowed_pixel_count: val }, - })); - }} - /> - - { - this.setState((prev) => ({ - max_frames_with_signal_err: err, - s: { ...prev.s, max_frames_with_signal: val }, - })); - }} - /> - - -
-
- ); - } + + { + setMaxPixelCountErr(err); + setS(prev => ({ ...prev, max_allowed_pixel_count: val })); + }} + /> + + { + setMaxFramesWithSignalErr(err); + setS(prev => ({ ...prev, max_frames_with_signal: val })); + }} + /> + + +
+
+
+ ); } -export default DarkMaskSettings; +export default memo(DarkMaskSettings); diff --git a/frontend/src/components/DataCollection.tsx b/frontend/src/components/DataCollection.tsx index f2d5cacb..68a73e2e 100644 --- a/frontend/src/components/DataCollection.tsx +++ b/frontend/src/components/DataCollection.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import {ChangeEvent, memo, useState} from 'react'; import Paper from '@mui/material/Paper'; -import {Stack, TextField, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Checkbox} from "@mui/material"; +import {Stack, TextField, Radio, RadioGroup, FormControlLabel, FormControl, Checkbox} from "@mui/material"; import NumberTextField from "./NumberTextField"; import {dataset_settings, grid_scan, rotation_axis} from "../client"; import ButtonWithSnackbar from "./ButtonWithSnackbar"; @@ -10,325 +10,261 @@ type MyProps = { frame_time_us: number }; -type MyState = { - s: dataset_settings, - beam_x_pxl_err: boolean, - beam_y_pxl_err: boolean, - detector_distance_mm_err: boolean, - incident_energy_kev_err: boolean, - images_per_trigger_err: boolean, - images_per_file_err: boolean, - image_time_us_err: boolean, - ntrigger_err: boolean, - mode: 'still' | 'rotation' | 'grid', - goniometer: rotation_axis, - grid: grid_scan, -}; +function DataCollection({frame_time_us}: MyProps) { + const [s, setS] = useState(() => ({ + beam_x_pxl: 0, + beam_y_pxl: 0, + detector_distance_mm: 100, + incident_energy_keV: 12.398, + images_per_trigger: 1, + image_time_us: frame_time_us, + ntrigger: 1, + images_per_file: 1000 + })); + const [goniometer, setGoniometer] = useState({ + start: 0, + step: 0.1, + name: "omega", + vector: [0,1,0] + }); + const [grid, setGrid] = useState({ + n_fast: 10, + step_x_um: 10.0, + step_y_um: 10.0, + vertical: false, + snake: false + }); + const [beamXPxlErr, setBeamXPxlErr] = useState(false); + const [beamYPxlErr, setBeamYPxlErr] = useState(false); + const [detectorDistanceMmErr, setDetectorDistanceMmErr] = useState(false); + const [incidentEnergyKevErr, setIncidentEnergyKevErr] = useState(false); + const [imagesPerTriggerErr, setImagesPerTriggerErr] = useState(false); + const [imageTimeUsErr, setImageTimeUsErr] = useState(false); + const [ntriggerErr, setNtriggerErr] = useState(false); + const [imagesPerFileErr, setImagesPerFileErr] = useState(false); + const [mode, setMode] = useState<'still' | 'rotation' | 'grid'>('still'); -class DataCollection extends React.Component { - state : MyState = { - s : { - beam_x_pxl: 0, - beam_y_pxl: 0, - detector_distance_mm: 100, - incident_energy_keV: 12.398, - images_per_trigger: 1, - image_time_us: this.props.frame_time_us, - ntrigger: 1, - images_per_file: 1000 - }, - goniometer : { - start: 0, - step: 0.1, - name: "omega", - vector: [0,1,0] - }, - grid: { - n_fast: 10, - step_x_um: 10.0, - step_y_um: 10.0, - vertical: false, - snake: false - }, - beam_x_pxl_err: false, - beam_y_pxl_err: false, - detector_distance_mm_err: false, - incident_energy_kev_err: false, - images_per_trigger_err: false, - image_time_us_err: false, - ntrigger_err: false, - images_per_file_err: false, - mode: 'still', - } + const error = () : boolean => + beamXPxlErr + || beamYPxlErr + || detectorDistanceMmErr + || incidentEnergyKevErr + || imageTimeUsErr + || imagesPerTriggerErr + || imagesPerFileErr + || ntriggerErr; - error() : boolean { - return this.state.beam_x_pxl_err - || this.state.beam_y_pxl_err - || this.state.detector_distance_mm_err - || this.state.incident_energy_kev_err - || this.state.image_time_us_err - || this.state.images_per_trigger_err - || this.state.images_per_file_err - || this.state.ntrigger_err; - } - - getDatasetSettings = () => { - let d = this.state.s; - if (this.state.mode === 'rotation') - d.goniometer = this.state.goniometer; - else if (this.state.mode === 'grid') - d.grid_scan = this.state.grid; + const getDatasetSettings = () : dataset_settings => { + let d = {...s}; + if (mode === 'rotation') + d.goniometer = goniometer; + else if (mode === 'grid') + d.grid_scan = grid; return d; - } + }; - render() { - return -
- - ) => { - this.setState(prevState => ({ - s: {...prevState.s, file_prefix: event.target.value} - })); - } + return +
+ + ) => { + setS(prev => ({...prev, file_prefix: event.target.value})); } - value={this.state.s.file_prefix} - sx={{width:"90%"}} + } + value={s.file_prefix} + sx={{width:"90%"}} + /> + + { + setS(prev => ({...prev, images_per_trigger: val})); + setImagesPerTriggerErr(err); + }} + min={1} + default={s.images_per_trigger} + /> + { + setS(prev => ({...prev, ntrigger: val})); + setNtriggerErr(err); + }} + min={1} + default={s.ntrigger} + /> + { + let image_time_us = Math.round(val * 1000.0); + setS(prev => ({...prev, image_time_us: image_time_us})); + setImageTimeUsErr(err); + }} + units={"ms"} + float={true} + min={0.01} + default={(s.image_time_us ?? 500) / 1000.0} /> - - { - this.setState(prevState => ({ - s: {...prevState.s, images_per_trigger: val}, - images_per_trigger_err: err - } - )); - }} - min={1} - default={this.state.s.images_per_trigger} - /> - { - this.setState(prevState => ({ - s: {...prevState.s, ntrigger: val}, - ntrigger_err: err - } - )); - }} - min={1} - default={this.state.s.ntrigger} - /> - { - let image_time_us = Math.round(val * 1000.0); - this.setState(prevState => ({ - s: {...prevState.s, - image_time_us: image_time_us - }, - image_time_us_err: err - } - )); - }} - units={"ms"} - float={true} - min={0.01} - default={(this.state.s.image_time_us ?? 500) / 1000.0} - /> - - { - this.setState(prevState => ({ - s: {...prevState.s, - images_per_file: val - }, - images_per_file_err: err - } - )); - }} - float={false} - min={1} - default={this.state.s.images_per_file} - /> - - - { - this.setState(prevState => ({ - s: {...prevState.s, beam_x_pxl: val}, - beam_x_pxl_err: err - } - )); - }} - units={"pxl"} - float={true} - default={this.state.s.beam_x_pxl} - /> - { - this.setState(prevState => ({ - s: {...prevState.s, beam_y_pxl: val}, - beam_y_pxl_err: err - } - )); - }} - units={"pxl"} - float={true} - default={this.state.s.beam_y_pxl} - /> - { - this.setState(prevState => ({ - s: {...prevState.s, detector_distance_mm: val}, - detector_distance_mm_err: err - } - )); - }} - units={"mm"} - min={0.1} - float={true} - default={this.state.s.detector_distance_mm} - /> - { - this.setState(prevState => ({ - s: {...prevState.s, incident_energy_keV: val}, - incident_energy_kev_err: err - } - )); - }} - min={0.1} - max={500.0} - units={"keV"} - float={true} - default={this.state.s.incident_energy_keV} - /> - - - - - - this.setState({mode: event.target.value as 'still' | 'rotation' | 'grid'})}> - } label="Still"/> - } label="Rotation"/> - } label="Grid scan"/> - - - {this.state.mode === 'rotation' && ( - - this.setState(prevState => ( - {goniometer : {...prevState.goniometer, start: val}} - ))} - units="°" - float={true} - default={this.state.goniometer.start} - /> - this.setState(prevState => ( - {goniometer : {...prevState.goniometer, step: val}} - ))} - units="°" - float={true} - min={0.001} - default={this.state.goniometer.step} - /> - this.setState(prevState => ( - {goniometer : {...prevState.goniometer, name: e.target.value}} - ))} - /> - - )} - {this.state.mode === 'grid' && ( - - this.setState( - prevState => ({grid : {...prevState.grid, n_fast: val}}))} - min={1} - default={this.state.grid.n_fast} - /> - this.setState( - prevState => ({grid : {...prevState.grid, step_x_um: val}}))} - units="μm" - float={true} - default={this.state.grid.step_x_um} - /> - this.setState( - prevState => ({grid : {...prevState.grid, step_y_um: val}}))} - units="μm" - float={true} - default={this.state.grid.step_y_um} - /> - this.setState(prevState => ( - {grid : {...prevState.grid, snake: e.target.checked}} - ))} - /> - } - label="Snake scan" - /> - this.setState(prevState => ( - {grid : {...prevState.grid, vertical: e.target.checked}} - ))} - /> - } - label="Vertical scan" - /> - - )} - - - - - + { + setS(prev => ({...prev, images_per_file: val})); + setImagesPerFileErr(err); + }} + float={false} + min={1} + default={s.images_per_file} + /> -
-
- } + + { + setS(prev => ({...prev, beam_x_pxl: val})); + setBeamXPxlErr(err); + }} + units={"pxl"} + float={true} + default={s.beam_x_pxl} + /> + { + setS(prev => ({...prev, beam_y_pxl: val})); + setBeamYPxlErr(err); + }} + units={"pxl"} + float={true} + default={s.beam_y_pxl} + /> + { + setS(prev => ({...prev, detector_distance_mm: val})); + setDetectorDistanceMmErr(err); + }} + units={"mm"} + min={0.1} + float={true} + default={s.detector_distance_mm} + /> + { + setS(prev => ({...prev, incident_energy_keV: val})); + setIncidentEnergyKevErr(err); + }} + min={0.1} + max={500.0} + units={"keV"} + float={true} + default={s.incident_energy_keV} + /> + + + + + + setMode(event.target.value as 'still' | 'rotation' | 'grid')}> + } label="Still"/> + } label="Rotation"/> + } label="Grid scan"/> + + + {mode === 'rotation' && ( + + setGoniometer(prev => ({...prev, start: val}))} + units="°" + float={true} + default={goniometer.start} + /> + setGoniometer(prev => ({...prev, step: val}))} + units="°" + float={true} + min={0.001} + default={goniometer.step} + /> + setGoniometer(prev => ({...prev, name: e.target.value}))} + /> + + )} + {mode === 'grid' && ( + + setGrid(prev => ({...prev, n_fast: val}))} + min={1} + default={grid.n_fast} + /> + setGrid(prev => ({...prev, step_x_um: val}))} + units="μm" + float={true} + default={grid.step_x_um} + /> + setGrid(prev => ({...prev, step_y_um: val}))} + units="μm" + float={true} + default={grid.step_y_um} + /> + setGrid(prev => ({...prev, snake: e.target.checked}))} + /> + } + label="Snake scan" + /> + setGrid(prev => ({...prev, vertical: e.target.checked}))} + /> + } + label="Vertical scan" + /> + + )} + + + + + +
+
+
} -export default DataCollection; +export default memo(DataCollection); diff --git a/frontend/src/components/DataProcessingPlot.tsx b/frontend/src/components/DataProcessingPlot.tsx index c47eb9bf..b693e797 100644 --- a/frontend/src/components/DataProcessingPlot.tsx +++ b/frontend/src/components/DataProcessingPlot.tsx @@ -1,6 +1,8 @@ -import React, {Component, ReactNode} from 'react'; +import {ReactNode} from 'react'; -import {azint_unit, getPreviewPlot, plot_type, plot_unit_x, plots} from "../client"; +import {useQuery} from "@tanstack/react-query"; +import {azint_unit, plot_type, plot_unit_x} from "../client"; +import {getPreviewPlotOptions} from "../client/@tanstack/react-query.gen"; import MultiLinePlotWrapper from "./MultiLinePlotWrapper"; type MyProps = { @@ -10,11 +12,6 @@ type MyProps = { azint: azint_unit; }; -type MyState = { - plots : plots, - connection_error: boolean -} - type PlotlyPlot = { x: number[], y: (number | null)[], @@ -97,79 +94,49 @@ function AxisTypeY(plot: plot_type) : string | ReactNode { } } -class DataProcessingPlot extends Component { - interval: ReturnType | undefined; +function DataProcessingPlot({type, binning, angle, azint}: MyProps) { + const {data: plots, isError} = useQuery({ + ...getPreviewPlotOptions({ query: { type, binning, experimental_coord: angle, azint_unit: azint } }), + refetchInterval: 1000, + }); - state: MyState = { - plots: { - plot : [ - { - x: [0, 100, 200, 300, 400, 500], - y: [0.1, 0.3, 0.5, 0.2, 0.0, 0.1], - title: "Example" - } - ], - unit_x: plot_unit_x.IMAGE_NUMBER - }, - connection_error: true - } + if (isError + || (plots === undefined) + || (plots.plot === null) + || (plots.plot.length === 0)) + return
No plots available
; - getValues() { - getPreviewPlot({ throwOnError: true, query: { type: this.props.type, binning: this.props.binning, experimental_coord: this.props.angle, azint_unit: this.props.azint } }) - .then(({data}) => this.setState({plots: data, connection_error: false})) - .catch(error => { - this.setState({connection_error: true}); - }); - } + let data: PlotlyData = []; + if ((plots.plot[0].z !== undefined) + && (plots.plot[0].z.length > 0)) { - componentDidMount() { - this.getValues(); - this.interval = setInterval(() => this.getValues(), 1000); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - - render() { - if (this.state.connection_error - || (this.state.plots === undefined) - || (this.state.plots.plot === null) - || (this.state.plots.plot.length === 0)) - return
No plots available
; - - let data: PlotlyData = []; - if ((this.state.plots.plot[0].z !== undefined) - && (this.state.plots.plot[0].z.length > 0)) { + data.push({ + x: plots.plot[0].x, + y: plots.plot[0].y, + z: plots.plot[0].z, + type: "heatmap", + colorscale: "Viridis" + }) + return + } else { + plots.plot.map(d => data.push({ - x: this.state.plots.plot[0].x, - y: this.state.plots.plot[0].y, - z: this.state.plots.plot[0].z, - type: "heatmap", - colorscale: "Viridis" - }) - return - - } else { - this.state.plots.plot.map(d => - data.push({ - x: d.x, - y: d.y, - type: "scatter", - mode: "line", - name: d.title - })); - return - } + x: d.x, + y: d.y, + type: "scatter", + mode: "line", + name: d.title + })); + return } } diff --git a/frontend/src/components/DataProcessingPlots.tsx b/frontend/src/components/DataProcessingPlots.tsx index 303d5f99..1e8206c2 100644 --- a/frontend/src/components/DataProcessingPlots.tsx +++ b/frontend/src/components/DataProcessingPlots.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {memo, useState} from 'react'; import Paper from '@mui/material/Paper'; import {Box, Checkbox, FormControlLabel, Grid} from "@mui/material"; @@ -14,131 +14,119 @@ type MyProps = { height: number } -type MyState = { - type: plot_type, - binning: string, - tab : String, - angle_units: boolean, - azint_unit: azint_unit -} +function DataProcessingPlots({type: initialType, height}: MyProps) { + const [type, setType] = useState(initialType); + const [binning, setBinning] = useState("0"); + const [tab, setTab] = useState(initialType); + const [angleUnits, setAngleUnits] = useState(true); + const [azintUnit, setAzintUnit] = useState(azint_unit.Q_RECIP_A); -class DataProcessingPlots extends Component { - state: MyState = { - type: this.props.type, - binning: "0", - tab: this.props.type, - angle_units: true, - azint_unit: azint_unit.Q_RECIP_A - } - - plotTypeChange = (event : SelectChangeEvent) => { + const plotTypeChange = (event : SelectChangeEvent) => { let val = String(event.target.value).toString(); - this.setState({tab: val, type: val as plot_type}); + setTab(val); + setType(val as plot_type); }; - handleChange = (event : SelectChangeEvent) => { - this.setState({ binning: String(event.target.value).toString() }); + const handleChange = (event : SelectChangeEvent) => { + setBinning(String(event.target.value).toString()); }; - render() { - return - + return + - - + + - + - - - - - this.setState( - {angle_units: e.target.checked})} - /> - } - label="X-axis exp. units" - /> - - this.setState({ - azint_unit: e.target.checked ? azint_unit.TWO_THETA_DEG : azint_unit.Q_RECIP_A - })} - /> - } - label="2θ (az. int.)" - /> - - - - - - - - - - - } + + + + + setAngleUnits(e.target.checked)} + /> + } + label="X-axis exp. units" + /> + + setAzintUnit( + e.target.checked ? azint_unit.TWO_THETA_DEG : azint_unit.Q_RECIP_A + )} + /> + } + label="2θ (az. int.)" + /> + + + + + + + + + + } -export default DataProcessingPlots; \ No newline at end of file +export default memo(DataProcessingPlots); diff --git a/frontend/src/components/DataProcessingSettings.tsx b/frontend/src/components/DataProcessingSettings.tsx index 8019a15c..9a73519f 100644 --- a/frontend/src/components/DataProcessingSettings.tsx +++ b/frontend/src/components/DataProcessingSettings.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, useEffect, useState} from 'react'; import Paper from '@mui/material/Paper'; import {Grid, Slider, Switch, Typography} from "@mui/material"; @@ -10,12 +10,6 @@ type MyProps = { update: () => void }; -type MyState = { - s: spot_finding_settings, - last_downloaded_s: spot_finding_settings, - high_res_gap_Q_recipA: number -} - const default_spot_finding_settings: spot_finding_settings = { enable: true, indexing: true, @@ -31,221 +25,165 @@ const default_spot_finding_settings: spot_finding_settings = { high_res_gap_Q_recipA: 1.5 }; -class DataProcessingSettings extends Component { - state : MyState = { - s: default_spot_finding_settings, - last_downloaded_s: default_spot_finding_settings, - high_res_gap_Q_recipA: 1.5 - } - - putValues(x: spot_finding_settings) { - putConfigSpotFinding({ body: x, throwOnError: true }) - .catch(error => console.log(error) ); - } - - getValues() { - const incoming = this.props.s; - - // Only adopt the server copy when it actually changed, otherwise the - // 1 s statistics poll would overwrite edits the user is making. - if ((incoming !== undefined) && !_.isEqual(incoming, this.state.last_downloaded_s)) - this.setState(prevState => ({ - s: incoming, - last_downloaded_s: incoming, - high_res_gap_Q_recipA: incoming.high_res_gap_Q_recipA ?? prevState.high_res_gap_Q_recipA - })); - } - - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - setPhotonCountThreshold = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: {...prevState.s, photon_count_threshold: newValue as number}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setSignalToNoiseThreshold = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: {...prevState.s, signal_to_noise_threshold: newValue as number}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setMinPixPerSpot = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: {...prevState.s, min_pix_per_spot: newValue as number}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setLowResolutionLimit = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: {...prevState.s, low_resolution_limit: newValue as number}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setHighResolutionLimit = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: {...prevState.s, high_resolution_limit: newValue as number}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setHighResolutionLimitForCountingLowResSpots = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: { - ...prevState.s, - high_resolution_limit_for_spot_count_low_res: newValue as number - }}), () => {this.putValues(this.state.s);}); - this.props.update(); - } - - setIceRingWidth = (event: Event, newValue: number | number[]) => { - this.setState(prevState => ({s: { - ...prevState.s, - ice_ring_width_q_recipA: newValue as number - }}), () => {this.putValues(this.state.s);}); - this.props.update(); - } - enableSpotFindingToggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s: {...prevState.s, enable: event.target.checked}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - enableIndexingToggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s: {...prevState.s, indexing: event.target.checked}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - enableQuickIntegrationToggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s: {...prevState.s, quick_integration: event.target.checked}}), - () => {this.putValues(this.state.s);}); - this.props.update(); - } - - enableHighResGapToggle = (event: React.ChangeEvent) => { - const checked = event.target.checked; - this.setState(prevState => { - return { - s: { - ...prevState.s, - high_res_gap_Q_recipA: checked ? prevState.high_res_gap_Q_recipA : undefined - } - }; - }, () => { this.putValues(this.state.s); }); - this.props.update(); - } - - setHighResGap = (event: Event, newValue: number | number[]) => { - const v = newValue as number; - this.setState(prevState => ({ - s: { - ...prevState.s, - high_res_gap_Q_recipA: v - }, - high_res_gap_Q_recipA: v - }), () => { this.putValues(this.state.s); }); - this.props.update(); - } - - render() { - return - - - - -
Spot finding parameters

- - Spot finding -

- - Count threshold - - -
Signal-to-noise threshold - value.toFixed(1)} - /> - -
Minimum pixel / spot - - Low resolution limit [Å] - value.toFixed(1)} - /> - - High resolution limit [Å] - value.toFixed(1)} - /> - High resolution limit for counting low resolution spots [Å] - value.toFixed(1)} - /> - Ice ring width in Q-space [Å-1] - value.toFixed(3)} - /> - - - High‑res gap Q-space filter [Å-1] - - value.toFixed(2)} - /> - -

- - Indexing

- - Quick MX integration -
- - -
- } +function putValues(x: spot_finding_settings) { + putConfigSpotFinding({ body: x, throwOnError: true }) + .catch(error => console.log(error) ); } -export default DataProcessingSettings; \ No newline at end of file +function DataProcessingSettings({s: serverS, update}: MyProps) { + const [s, setS] = useState(default_spot_finding_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_spot_finding_settings); + const [highResGap, setHighResGap] = useState(1.5); + + // Only adopt the server copy when it actually changed, otherwise the + // 1 s statistics poll would overwrite edits the user is making. + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); + setHighResGap(serverS.high_res_gap_Q_recipA ?? highResGap); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); + + const apply = (next: spot_finding_settings) => { + setS(next); + putValues(next); + update(); + }; + + const setPhotonCountThreshold = (event: Event, newValue: number | number[]) => + apply({...s, photon_count_threshold: newValue as number}); + + const setSignalToNoiseThreshold = (event: Event, newValue: number | number[]) => + apply({...s, signal_to_noise_threshold: newValue as number}); + + const setMinPixPerSpot = (event: Event, newValue: number | number[]) => + apply({...s, min_pix_per_spot: newValue as number}); + + const setLowResolutionLimit = (event: Event, newValue: number | number[]) => + apply({...s, low_resolution_limit: newValue as number}); + + const setHighResolutionLimit = (event: Event, newValue: number | number[]) => + apply({...s, high_resolution_limit: newValue as number}); + + const setHighResolutionLimitForCountingLowResSpots = (event: Event, newValue: number | number[]) => + apply({...s, high_resolution_limit_for_spot_count_low_res: newValue as number}); + + const setIceRingWidth = (event: Event, newValue: number | number[]) => + apply({...s, ice_ring_width_q_recipA: newValue as number}); + + const enableSpotFindingToggle = (event: ChangeEvent) => + apply({...s, enable: event.target.checked}); + + const enableIndexingToggle = (event: ChangeEvent) => + apply({...s, indexing: event.target.checked}); + + const enableQuickIntegrationToggle = (event: ChangeEvent) => + apply({...s, quick_integration: event.target.checked}); + + const enableHighResGapToggle = (event: ChangeEvent) => + apply({...s, high_res_gap_Q_recipA: event.target.checked ? highResGap : undefined}); + + const handleHighResGap = (event: Event, newValue: number | number[]) => { + const v = newValue as number; + setHighResGap(v); + apply({...s, high_res_gap_Q_recipA: v}); + }; + + return + + + + +
Spot finding parameters

+ + Spot finding +

+ + Count threshold + + +
Signal-to-noise threshold + value.toFixed(1)} + /> + +
Minimum pixel / spot + + Low resolution limit [Å] + value.toFixed(1)} + /> + + High resolution limit [Å] + value.toFixed(1)} + /> + High resolution limit for counting low resolution spots [Å] + value.toFixed(1)} + /> + Ice ring width in Q-space [Å-1] + value.toFixed(3)} + /> + + + High‑res gap Q-space filter [Å-1] + + value.toFixed(2)} + /> + +

+ + Indexing

+ + Quick MX integration +
+ + +
+} + +export default memo(DataProcessingSettings); diff --git a/frontend/src/components/DetectorSelection.tsx b/frontend/src/components/DetectorSelection.tsx index 67192be0..9d7426fd 100644 --- a/frontend/src/components/DetectorSelection.tsx +++ b/frontend/src/components/DetectorSelection.tsx @@ -1,23 +1,18 @@ -import React, {Component} from 'react'; +import {memo, ReactNode, useEffect, useState} from 'react'; import Select, { SelectChangeEvent } from '@mui/material/Select'; -import {Grid,Stack, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material"; +import {Stack, 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 { ReactNode } from "react"; type MyProps = { s?: detector_list } -type MyState = { - choice: string -}; - type MapElement = { id: number name: string @@ -42,30 +37,29 @@ const default_detector_element: detector_list_element = { min_count_time_ns: 0 } -class DetectorSelection extends Component { - state : MyState = { - choice: "0" - } +function DetectorSelection({s}: MyProps) { + const [choice, setChoice] = useState("0"); - componentDidMount() { - if (this.props.s !== undefined) - this.setState({choice: this.props.s.current_id.toString()}); - } + useEffect(() => { + if (s !== undefined) + setChoice(s.current_id.toString()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - handleChange = (event : SelectChangeEvent) => { - this.setState({ choice: String(event.target.value).toString() }); + const handleChange = (event : SelectChangeEvent) => { + setChoice(String(event.target.value).toString()); }; - current_detector_info = () : detector_list_element => { - if (this.props.s === undefined) + const current_detector_info = () : detector_list_element => { + if (s === undefined) return default_detector_element; - if (this.props.s.detectors.length <= this.props.s.current_id) + if (s.detectors.length <= s.current_id) return default_detector_element; - return this.props.s.detectors[this.props.s.current_id]; - } + return s.detectors[s.current_id]; + }; - detector_info = () : ReactNode => { - let x : detector_list_element = this.current_detector_info(); + const detector_info = () : ReactNode => { + let x : detector_list_element = current_detector_info(); let arr: DetectorTableElement[] = [ {title: "Current Detector", val: x.description}, @@ -95,58 +89,56 @@ class DetectorSelection extends Component { - } + }; - detector_list() : MapElement[] { + const detector_options = () : MapElement[] => { let v: MapElement[] = []; - if (this.props.s !== undefined) { - let id: number = this.props.s.current_id; - v = this.props.s.detectors.map(d => ({ + if (s !== undefined) { + let id: number = s.current_id; + v = s.detectors.map(d => ({ id: d.id, name: `${d.description} (${d.width}x${d.height} pxl) ` + ((d.id === id) ? "*** CURRENT ***" : "") })); } return v; - } + }; - render() { - return -
- - Detector selection + return +
+ + Detector selection - {this.detector_info()} + {detector_info()} - - Detector - - - - -
-
- } + + Detector + + + +
+
+
} -export default DetectorSelection; \ No newline at end of file +export default memo(DetectorSelection); diff --git a/frontend/src/components/DetectorSettings.tsx b/frontend/src/components/DetectorSettings.tsx index a12edc84..8685d1b0 100644 --- a/frontend/src/components/DetectorSettings.tsx +++ b/frontend/src/components/DetectorSettings.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, useEffect, useState} from 'react'; import {Checkbox, FormControlLabel, FormGroup, Grid, List, ListItem, Switch, Tooltip} from "@mui/material"; import Paper from "@mui/material/Paper"; @@ -6,7 +6,7 @@ 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 {detector_settings, detector_timing, postDeactivate} from "../client"; +import {detector_settings, detector_timing} from "../client"; import NumberTextField from "./NumberTextField"; import _ from "lodash"; import ButtonWithSnackbar from "./ButtonWithSnackbar"; @@ -15,26 +15,6 @@ type MyProps = { s?: detector_settings } -type MyState = { - s: detector_settings, - last_downloaded_s: detector_settings, - bit_width_selection: string, - storage_cell_list_value: string, - timing_mode_list_value: detector_timing, - frame_time_err: boolean, - count_time_err: boolean, - storage_cell_delay_err: boolean, - detector_trigger_delay_err: boolean, - internal_frame_generator_images_err: boolean, - pedestal_g0_frames_err: boolean, - pedestal_g1_frames_err: boolean, - pedestal_g2_frames_err: boolean, - pedestal_min_image_count_err: boolean, - eiger_threshold_err: boolean, - eiger_threshold_keV_old: number, - download_counter: number -} - function extractDepthForSelection(input: detector_settings) : string { if (input.eiger_bit_depth === undefined) return 'a'; @@ -48,64 +28,75 @@ function extractDepthForSelection(input: detector_settings) : string { return ""; } -class DetectorSettings extends Component { - state : MyState = { - s: { - frame_time_us: 1000 - }, - last_downloaded_s: { - frame_time_us: 1000 - }, - bit_width_selection: "a", - timing_mode_list_value: detector_timing.TRIGGER, - storage_cell_list_value: "1", - pedestal_g0_frames_err: false, - pedestal_g1_frames_err: false, - pedestal_g2_frames_err: false, - pedestal_min_image_count_err: false, - storage_cell_delay_err: false, - detector_trigger_delay_err: false, - frame_time_err: false, - count_time_err: false, - internal_frame_generator_images_err: false, - eiger_threshold_err: false, - eiger_threshold_keV_old: 6.0, - download_counter: 0 - } +function DetectorSettings({s: serverS}: MyProps) { + const [s, setS] = useState({ frame_time_us: 1000 }); + const [lastDownloadedS, setLastDownloadedS] = useState({ frame_time_us: 1000 }); + const [bitWidthSelection, setBitWidthSelection] = useState("a"); + const [storageCellListValue, setStorageCellListValue] = useState("1"); + const [timingModeListValue, setTimingModeListValue] = useState(detector_timing.TRIGGER); + const [frameTimeErr, setFrameTimeErr] = useState(false); + const [countTimeErr, setCountTimeErr] = useState(false); + const [storageCellDelayErr, setStorageCellDelayErr] = useState(false); + const [detectorTriggerDelayErr, setDetectorTriggerDelayErr] = useState(false); + const [internalFrameGeneratorImagesErr, setInternalFrameGeneratorImagesErr] = useState(false); + const [pedestalG0FramesErr, setPedestalG0FramesErr] = useState(false); + const [pedestalG1FramesErr, setPedestalG1FramesErr] = useState(false); + const [pedestalG2FramesErr, setPedestalG2FramesErr] = useState(false); + const [pedestalMinImageCountErr, setPedestalMinImageCountErr] = useState(false); + const [eigerThresholdErr, setEigerThresholdErr] = useState(false); + const [eigerThresholdKeVOld, setEigerThresholdKeVOld] = useState(6.0); + const [downloadCounter, setDownloadCounter] = useState(0); - eigerThresholdToggle = (event: React.ChangeEvent) => { + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setDownloadCounter(c => c + 1); + setS(serverS); + setBitWidthSelection(extractDepthForSelection(serverS)); + setLastDownloadedS(serverS); + setStorageCellListValue(String(serverS.jungfrau_storage_cell_count ?? "1")); + setTimingModeListValue(serverS.timing ?? detector_timing.TRIGGER); + setPedestalG0FramesErr(false); + setPedestalG1FramesErr(false); + setPedestalG2FramesErr(false); + setPedestalMinImageCountErr(false); + setStorageCellDelayErr(false); + setDetectorTriggerDelayErr(false); + setFrameTimeErr(false); + setCountTimeErr(false); + setInternalFrameGeneratorImagesErr(false); + setEigerThresholdErr(false); + setEigerThresholdKeVOld(6.0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); + + const eigerThresholdToggle = (event: ChangeEvent) => { if (event.target.checked) - this.setState(prevState => ({s : {...prevState.s, - eiger_threshold_keV: prevState.eiger_threshold_keV_old}})); + setS(prev => ({...prev, eiger_threshold_keV: eigerThresholdKeVOld})); else - this.setState(prevState => ({s: {...prevState.s, eiger_threshold_keV: undefined}})); - } + setS(prev => ({...prev, eiger_threshold_keV: undefined})); + }; - countTimeToggle = (event: React.ChangeEvent) => { + const countTimeToggle = (event: ChangeEvent) => { if (event.target.checked) - this.setState(prevState => ({s : {...prevState.s, count_time_us: prevState.s.frame_time_us - 20}})); + setS(prev => ({...prev, count_time_us: prev.frame_time_us - 20})); else - this.setState(prevState => ({s : {...prevState.s, count_time_us: undefined}})); - } + setS(prev => ({...prev, count_time_us: undefined})); + }; - internalFrameGeneratorToggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s : {...prevState.s, internal_frame_generator: event.target.checked}})); - } + const internalFrameGeneratorToggle = (event: ChangeEvent) => { + setS(prev => ({...prev, internal_frame_generator: event.target.checked})); + }; - useGainHG0Toggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s : {...prevState.s, jungfrau_use_gain_hg0: event.target.checked}})); - } + const useGainHG0Toggle = (event: ChangeEvent) => { + setS(prev => ({...prev, jungfrau_use_gain_hg0: event.target.checked})); + }; - fixedGainG1Toggle = (event: React.ChangeEvent) => { - this.setState(prevState => ({s : {...prevState.s, jungfrau_fixed_gain_g1: event.target.checked}})); - } + const fixedGainG1Toggle = (event: ChangeEvent) => { + setS(prev => ({...prev, jungfrau_fixed_gain_g1: event.target.checked})); + }; - deactivate = () => { - postDeactivate({ throwOnError: true }) - .catch(error => {} ); - } - - handleBitWidthChange = (event: SelectChangeEvent) => { + const handleBitWidthChange = (event: SelectChangeEvent) => { let val: detector_settings['eiger_bit_depth'] = undefined; if (event.target.value === "a") @@ -117,16 +108,11 @@ class DetectorSettings extends Component { else if (event.target.value === "32") val = 32; - this.setState(prevState => ({ - bit_width_selection: event.target.value, - s: { - ...prevState.s, - eiger_bit_depth: val - } - })); + setBitWidthSelection(event.target.value); + setS(prev => ({...prev, eiger_bit_depth: val})); }; - handleTimingChange = (event : SelectChangeEvent) => { + const handleTimingChange = (event : SelectChangeEvent) => { let d : detector_timing = detector_timing.TRIGGER; switch (event.target.value) { @@ -144,381 +130,312 @@ class DetectorSettings extends Component { break; } - this.setState(prevState => ({ - timing_mode_list_value: d, - s : {...prevState.s, - timing: d - } - }) - ); + setTimingModeListValue(d); + setS(prev => ({...prev, timing: d})); }; - handleChange = (event : SelectChangeEvent) => { - this.setState(prevState => ({ - storage_cell_list_value: String(event.target.value).toString(), - s : {...prevState.s, - jungfrau_storage_cell_count: Number(event.target.value) - } - }) - ); + const handleChange = (event : SelectChangeEvent) => { + setStorageCellListValue(String(event.target.value).toString()); + setS(prev => ({...prev, jungfrau_storage_cell_count: Number(event.target.value)})); }; - getValues() { - if (this.props.s !== undefined) { - let det_set: detector_settings = this.props.s; - if (!_.isEqual(det_set, this.state.last_downloaded_s)) { - this.setState(prevState => ({ - download_counter: prevState.download_counter + 1, - s: det_set, - bit_width_selection: extractDepthForSelection(det_set), - last_downloaded_s: det_set, - storage_cell_list_value: String(det_set.jungfrau_storage_cell_count ?? "1"), - timing_mode_list_value: det_set.timing ?? detector_timing.TRIGGER, - pedestal_g0_frames_err: false, - pedestal_g1_frames_err: false, - pedestal_g2_frames_err: false, - pedestal_min_image_count_err: false, - storage_cell_delay_err: false, - detector_trigger_delay_err: false, - frame_time_err: false, - count_time_err: false, - internal_frame_generator_images_err: false, - eiger_threshold_err: false, - eiger_threshold_keV_old: 6.0 - })); - } - } - } + return - componentDidMount() { - this.getValues(); - } + - componentDidUpdate() { - this.getValues(); - } - - render() { - return - - - - - -
Detector settings -


- { - this.setState(prevState => ({ - frame_time_err: err, - s: {...prevState.s, frame_time_us: val} - })); - }}/> -
- - - - + + +
Detector settings +


+ { + setFrameTimeErr(err); + setS(prev => ({...prev, frame_time_us: val})); + }}/> +
+ + + + + + + { + setCountTimeErr(err); + setS(prev => ({...prev, count_time_us: val})); + }}/> + + + + + + + { + setDetectorTriggerDelayErr(err); + setS(prev => ({...prev, detector_trigger_delay_ns: val})); + }} + sx={{width: 120}} + />   + + Timing mode + + +

+
+ + + + Internal image generator (FPGA)
+
+ + + +
+ + - - { - this.setState(prevState => ({ - count_time_err: err, - s: {...prevState.s, count_time_us: val} - })); - }}/> + + + { + setInternalFrameGeneratorImagesErr(err); + setS(prev => ({...prev, internal_frame_generator_images: val})); + }} + disabled={!s.internal_frame_generator} + fullWidth/> + - -
- - - - { - this.setState(prevState => ({ - detector_trigger_delay_err: err, - s: {...prevState.s, detector_trigger_delay_ns: val} - })); - }} - sx={{width: 120}} - />   + +
+
+ + + + JUNGFRAU settings
+
+ + + + + + } label="HG0"/> + + } label="Fixed G1"/> +
+
+ + + + JUNGFRAU storage cells

+
+ + + + + Count + +    + { + setStorageCellDelayErr(err); + setS(prev => ({...prev, jungfrau_storage_cell_delay_ns: val})); + }} + disabled={storageCellListValue === "1"} + fullWidth/> +

+
+ + + + JUNGFRAU Pedestal

+
+ + + + { + setPedestalG0FramesErr(err); + setS(prev => ({...prev, jungfrau_pedestal_g0_frames: val})); + }} + sx={{width: "80px"}}/>  + { + setPedestalG1FramesErr(err); + setS(prev => ({...prev, jungfrau_pedestal_g1_frames: val})); + }} + sx={{width: "80px"}}/>  + { + setPedestalG2FramesErr(err); + setS(prev => ({...prev, jungfrau_pedestal_g2_frames: val})); + }} + sx={{width: "80px"}}/> +  

+
+ + + + + { + setPedestalMinImageCountErr(err); + setS(prev => ({...prev, jungfrau_pedestal_min_image_count: val})); + }} + sx={{width: "120px"}}/>

+
+ + + + EIGER settings

+
+ + + + + + + { + if (!err) + setEigerThresholdKeVOld(val); + setEigerThresholdErr(err); + setS(prev => ({...prev, eiger_threshold_keV: val})); + }} + disabled={s.eiger_threshold_keV === undefined} + /> +   - Timing mode + Read-out - -

-
- - - - Internal image generator (FPGA)
-
- - - -
- - - - - - { - this.setState(prevState => ({ - internal_frame_generator_images_err: err, - s: {...prevState.s, internal_frame_generator_images: val} - })); - }} - disabled={!this.state.s.internal_frame_generator} - fullWidth/> - - - - -
- - - - JUNGFRAU settings
-
- - - - - - } label="HG0"/> - - } label="Fixed G1"/> -
-
- - - - JUNGFRAU storage cells

-
- - - - - Count - -    - { - this.setState(prevState => ({ - storage_cell_delay_err: err, - s: {...prevState.s, jungfrau_storage_cell_delay_ns: val} - })); - }} - disabled={this.state.storage_cell_list_value === "1"} - fullWidth/> -

-
- - - - JUNGFRAU Pedestal

-
- - - - { - this.setState(prevState => ({ - pedestal_g0_frames_err: err, - s: {...prevState.s, jungfrau_pedestal_g0_frames: val} - })); - }} - sx={{width: "80px"}}/>  - { - this.setState(prevState => ({ - pedestal_g1_frames_err: err, - s: {...prevState.s, jungfrau_pedestal_g1_frames: val} - })); - }} - sx={{width: "80px"}}/>  - { - this.setState(prevState => ({ - pedestal_g2_frames_err: err, - s: {...prevState.s, jungfrau_pedestal_g2_frames: val} - })); - }} - sx={{width: "80px"}}/> -  

-
- - - - - { - this.setState(prevState => ({ - pedestal_min_image_count_err: err, - s: {...prevState.s, jungfrau_pedestal_min_image_count: val} - })); - }} - sx={{width: "120px"}}/>

-
- - - - EIGER settings

-
- - - - - - - { - let old: number = this.state.eiger_threshold_keV_old; - if (!err) - old = val; - this.setState(prevState => ({ - eiger_threshold_err: err, - eiger_threshold_keV_old: old, - s: {...prevState.s, eiger_threshold_keV: val} - })); - }} - disabled={this.state.s.eiger_threshold_keV === undefined} - /> -   - - Read-out - -

-

-
- - - -    - -

-
- +

+
-
- } + + + +    + +

+
+ + +
} -export default DetectorSettings; \ No newline at end of file +export default memo(DetectorSettings); diff --git a/frontend/src/components/DetectorStatus.tsx b/frontend/src/components/DetectorStatus.tsx index e6dc0055..1c4e84e7 100644 --- a/frontend/src/components/DetectorStatus.tsx +++ b/frontend/src/components/DetectorStatus.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {memo} from 'react'; import Paper from '@mui/material/Paper'; import {Stack, Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material"; @@ -8,8 +8,6 @@ type MyProps = { s?: detector_status } -type MyState = {} - function powerchipToString(s : detector_status) : string { switch (s.powerchip) { case detector_power_state.POWER_ON: @@ -24,7 +22,7 @@ function powerchipToString(s : detector_status) : string { function reduce_array(arr: number[], unit: string) : string { if (arr.length == 0) return "N/A"; - + if (arr.every(val => val === arr[0])) return arr[0].toString() + " " + unit; else { @@ -34,69 +32,62 @@ function reduce_array(arr: number[], unit: string) : string { } } -class DetectorStatus extends Component { - det() : detector_status { - if (this.props.s === undefined) - return { - state: detector_state.NOT_CONNECTED, - powerchip: detector_power_state.POWER_OFF, - server_version: "N/A", - number_of_triggers_left: 0, - fpga_temp_degC: [], - high_voltage_V: [] - } - else - return this.props.s; - } +function DetectorStatus({s}: MyProps) { + const det : detector_status = s ?? { + state: detector_state.NOT_CONNECTED, + powerchip: detector_power_state.POWER_OFF, + server_version: "N/A", + number_of_triggers_left: 0, + fpga_temp_degC: [], + high_voltage_V: [] + }; - render() { - return -
- + return +
+ - Detector status + Detector status - - - - - Detector state: - {(this.det().state.toString())} - - - Detector ASIC power: - {powerchipToString(this.det())} - - - Triggers remaining: - {this.det().number_of_triggers_left} - - - FPGA temperature: - {reduce_array(this.det().fpga_temp_degC, "degC")} - - - High voltage: - {reduce_array(this.det().high_voltage_V, "V")} - - - Detector server version: - {this.det().server_version} - - -
-
+ + + + + Detector state: + {(det.state.toString())} + + + Detector ASIC power: + {powerchipToString(det)} + + + Triggers remaining: + {det.number_of_triggers_left} + + + FPGA temperature: + {reduce_array(det.fpga_temp_degC, "degC")} + + + High voltage: + {reduce_array(det.high_voltage_V, "V")} + + + Detector server version: + {det.server_version} + + +
+
-
-
-
- } +
+
+
} -export default DetectorStatus; \ No newline at end of file +export default memo(DetectorStatus); diff --git a/frontend/src/components/ErrorMessage.tsx b/frontend/src/components/ErrorMessage.tsx index 0eef8ad5..64fb2d92 100644 --- a/frontend/src/components/ErrorMessage.tsx +++ b/frontend/src/components/ErrorMessage.tsx @@ -1,45 +1,40 @@ -import React, {Component} from 'react'; +import {memo, ReactNode} from 'react'; import {broker_status} from "../client"; import {Alert, AlertColor} from "@mui/material"; -import { ReactNode } from "react"; type MyProps = { s?: broker_status } -type MyState = {} +function severity(input?: broker_status['message_severity']) : AlertColor { + if (input === undefined) + return "error"; -class ErrorMessage extends Component { - severity(input?: broker_status['message_severity']) : AlertColor { - if (input === undefined) + switch (input) { + case 'info': + return "info"; + case 'warning': + return "warning"; + case 'error': return "error"; - - switch (input) { - case 'info': - return "info"; - case 'warning': - return "warning"; - case 'error': - return "error"; - case 'success': - return "success"; - } - } - - alert(input: AlertColor, message: string) : ReactNode { - return {message} - } - - render() { - if (this.props.s === undefined) - return this.alert("error", "Not connected to Jungfraujoch instance; check if jfjoch_broker is running"); - - if (this.props.s.message === undefined) - return this.alert("info", ""); - - return this.alert(this.severity(this.props.s.message_severity),this.props.s.message); + case 'success': + return "success"; } } -export default ErrorMessage; +function alert(input: AlertColor, message: string) : ReactNode { + return {message} +} + +function ErrorMessage({s}: MyProps) { + if (s === undefined) + return alert("error", "Not connected to Jungfraujoch instance; check if jfjoch_broker is running"); + + if (s.message === undefined) + return alert("info", ""); + + return alert(severity(s.message_severity), s.message); +} + +export default memo(ErrorMessage); diff --git a/frontend/src/components/FileWriterSettings.tsx b/frontend/src/components/FileWriterSettings.tsx index 90e0601b..de07c66a 100644 --- a/frontend/src/components/FileWriterSettings.tsx +++ b/frontend/src/components/FileWriterSettings.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import {memo, useEffect, useState} from 'react'; import Paper from '@mui/material/Paper'; import {Checkbox, FormControlLabel, FormGroup, Select, Stack} from "@mui/material"; @@ -13,12 +13,6 @@ type MyProps = { s?: file_writer_settings }; -type MyState = { - s: file_writer_settings, - last_downloaded_s: file_writer_settings, - download_counter: number -}; - const default_file_writer_settings : file_writer_settings = { overwrite: false, format: file_writer_format.N_XMX_LEGACY @@ -34,99 +28,68 @@ function stringToEnum(value: string): file_writer_format { return enumValue || file_writer_format.N_XMX_ONLY_DATA; } +function FileWriterSettings({s: serverS}: MyProps) { + const [s, setS] = useState(default_file_writer_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_file_writer_settings); - -class FileWriterSettings extends React.Component { - state : MyState = { - s: default_file_writer_settings, - last_downloaded_s: default_file_writer_settings, - download_counter: 0 - } - - - getValues = () => { - if (this.props.s !== undefined) { - let format_set : file_writer_settings = this.props.s; - if (!_.isEqual(format_set, this.state.last_downloaded_s)) { - this.setState(prevState => ({ - s: format_set, - last_downloaded_s: format_set, - download_counter: prevState.download_counter + 1, - })); - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - render() { - return -
- -
File Writer settings
- - { - this.setState(prevState => ({ - s: { - ...prevState.s, - overwrite: event.target.checked - } - })); - }} - /> - } - label="Overwrite existing files" - /> - - - File format - - - +
+ +
File Writer settings
+ + { + setS(prev => ({...prev, overwrite: event.target.checked})); + }} + /> + } + label="Overwrite existing files" /> -
-
-
- } + + + File format + + + + +
+ } -export default FileWriterSettings; +export default memo(FileWriterSettings); diff --git a/frontend/src/components/FpgaStatus.tsx b/frontend/src/components/FpgaStatus.tsx index e96b02be..c61b63d8 100644 --- a/frontend/src/components/FpgaStatus.tsx +++ b/frontend/src/components/FpgaStatus.tsx @@ -1,10 +1,8 @@ - -import React from 'react'; +import {memo, ReactNode} from 'react'; import Paper from '@mui/material/Paper'; import {DataGrid, GridCellParams, GridColDef} from "@mui/x-data-grid"; import { fpga_status} from "../client"; -import { ReactNode } from "react"; function Circles(val: boolean) : ReactNode { if (val) @@ -63,46 +61,40 @@ type MyProps = { s?: fpga_status; }; -type MyState = {}; +const columns : GridColDef[] = [ + { field: 'pci_dev_id', type: 'string', headerName: 'PCIe ID' }, + { field: 'base_mac_addr', type: 'string', headerName: 'MAC address', width: 200 }, + { field: 'eth_link_status', type: 'string', headerName: 'Link status', align: `center`, + renderCell: (params : GridCellParams) => EthLinkStatus(params.row.eth_link_count, + params.row.eth_link_status) + }, + { field: 'idle', type: 'string', headerName: 'Idle', align: `center`, + renderCell: (params : GridCellParams) => CardIdle(params.row.idle) + }, + { field: 'power_usage_W', type: 'number', headerName: 'Power usage [W]', width: 150}, + { field: 'fpga_temp_C', type: 'number', headerName: 'FPGA temperature [C]', width: 200 }, + { field: 'hbm_temp_C', type: 'number', headerName: 'HBM temperature [C]', width: 200 }, + { field: 'packets_sls', type: 'number', headerName: 'Data (current data collection)', width: 150, + valueGetter: (params, row) => DataVolume(row.packets_sls)}, + { field: "fw_version", type: 'string', headerName: 'Firmware version', width: 150 }, + { field: "pcie_link", type: 'string', headerName: 'PCIe link', width: 100, align: `center`, + renderCell: (params : GridCellParams) => LinkSpeed(params.row.pcie_link_speed, params.row.pcie_link_width)} +]; -class FpgaStatus extends React.Component { - - columns : GridColDef[] = [ - { field: 'pci_dev_id', type: 'string', headerName: 'PCIe ID' }, - { field: 'base_mac_addr', type: 'string', headerName: 'MAC address', width: 200 }, - { field: 'eth_link_status', type: 'string', headerName: 'Link status', align: `center`, - renderCell: (params : GridCellParams) => EthLinkStatus(params.row.eth_link_count, - params.row.eth_link_status) - }, - { field: 'idle', type: 'string', headerName: 'Idle', align: `center`, - renderCell: (params : GridCellParams) => CardIdle(params.row.idle) - }, - { field: 'power_usage_W', type: 'number', headerName: 'Power usage [W]', width: 150}, - { field: 'fpga_temp_C', type: 'number', headerName: 'FPGA temperature [C]', width: 200 }, - { field: 'hbm_temp_C', type: 'number', headerName: 'HBM temperature [C]', width: 200 }, - { field: 'packets_sls', type: 'number', headerName: 'Data (current data collection)', width: 150, - valueGetter: (params, row) => DataVolume(row.packets_sls)}, - { field: "fw_version", type: 'string', headerName: 'Firmware version', width: 150 }, - { field: "pcie_link", type: 'string', headerName: 'PCIe link', width: 100, align: `center`, - renderCell: (params : GridCellParams) => LinkSpeed(params.row.pcie_link_speed, params.row.pcie_link_width)} - ]; - - render() { - return - {((this.props.s !== undefined) && (this.props.s.length > 0)) ? - row.pci_dev_id} - autosizeOnMount={true} - initialState={{ - pagination: { paginationModel: { pageSize: 8 } }, - }} - /> :
No FPGA available
- } -
- - } +function FpgaStatus({s}: MyProps) { + return + {((s !== undefined) && (s.length > 0)) ? + row.pci_dev_id} + autosizeOnMount={true} + initialState={{ + pagination: { paginationModel: { pageSize: 8 } }, + }} + /> :
No FPGA available
+ } +
} -export default FpgaStatus; +export default memo(FpgaStatus); diff --git a/frontend/src/components/ImageFormatSettings.tsx b/frontend/src/components/ImageFormatSettings.tsx index 6cc41002..1c4eebef 100644 --- a/frontend/src/components/ImageFormatSettings.tsx +++ b/frontend/src/components/ImageFormatSettings.tsx @@ -1,13 +1,12 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, useEffect, useState} from 'react'; -import {Checkbox, FormControlLabel, FormGroup, Grid, List, ListItem, Stack, Switch, Tooltip} from "@mui/material"; -import Button from "@mui/material/Button"; +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, postConfigImageFormatConversion, postConfigImageFormatRaw, putConfigImageFormat} from "../client"; +import {image_format_settings} from "../client"; import NumberTextField from "./NumberTextField"; import _ from "lodash"; import ButtonWithSnackbar from "./ButtonWithSnackbar"; @@ -16,17 +15,6 @@ type MyProps = { s?: image_format_settings } -type MyState = { - s: image_format_settings, - last_downloaded_s: image_format_settings, - bit_width_selection: string, - sign_selection: string, - jungfrau_conversion_factor_err: boolean, - jungfrau_conversion_factor_old: number, - download_counter: number, - pedestal_g0_rms_limit_err: boolean -} - const default_image_format_settings: image_format_settings = { summation: true, geometry_transform: true, @@ -63,65 +51,29 @@ function extractDepthForSelection(input: image_format_settings) : string { return ""; } -class ImageFormatSettings extends Component { - state : MyState = { - s: default_image_format_settings, - last_downloaded_s: default_image_format_settings, - jungfrau_conversion_factor_err: false, - jungfrau_conversion_factor_old: 12.4, - bit_width_selection: "a", - sign_selection: "a", - download_counter: 0, - pedestal_g0_rms_limit_err: false - } +function ImageFormatSettings({s: serverS}: MyProps) { + const [s, setS] = useState(default_image_format_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_image_format_settings); + const [bitWidthSelection, setBitWidthSelection] = useState("a"); + const [signSelection, setSignSelection] = useState("a"); + const [jungfrauConversionFactorErr, setJungfrauConversionFactorErr] = useState(false); + const [jungfrauConversionFactorOld, setJungfrauConversionFactorOld] = useState(12.4); + const [downloadCounter, setDownloadCounter] = useState(0); - raw = () => { - postConfigImageFormatRaw({ throwOnError: true }) - .catch(error => console.log(error) ); - this.getValues(); - } - - conv = () => { - postConfigImageFormatConversion({ throwOnError: true }) - .catch(error => console.log(error) ); - this.getValues(); - } - - uploadButton = () => { this.putValues(); } - - putValues = () => { - putConfigImageFormat({ body: this.state.s, throwOnError: true }) - .catch(error => console.log(error) ); - } - - getValues = () => { - if (this.props.s !== undefined) { - let format_set: image_format_settings = this.props.s; - if (!_.isEqual(format_set, this.state.last_downloaded_s)) { - - this.setState(prevState => ({ - s: format_set, - last_downloaded_s: format_set, - jungfrau_conversion_factor_err: false, - download_counter: prevState.download_counter + 1, - jungfrau_conversion_factor_old: format_set.jungfrau_conversion_factor_keV - ?? prevState.jungfrau_conversion_factor_old, - bit_width_selection: extractDepthForSelection(format_set), - sign_selection: extractSignForSelection(format_set) - })); - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); + setJungfrauConversionFactorErr(false); + setDownloadCounter(c => c + 1); + setJungfrauConversionFactorOld(serverS.jungfrau_conversion_factor_keV ?? jungfrauConversionFactorOld); + setBitWidthSelection(extractDepthForSelection(serverS)); + setSignSelection(extractSignForSelection(serverS)); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - handleBitWidthChange = (event: SelectChangeEvent) => { + const handleBitWidthChange = (event: SelectChangeEvent) => { let val: image_format_settings['bit_depth_image'] = undefined; if (event.target.value === "a") @@ -133,16 +85,11 @@ class ImageFormatSettings extends Component { else if (event.target.value === "32") val = 32; - this.setState(prevState => ({ - bit_width_selection: event.target.value, - s: { - ...prevState.s, - bit_depth_image: val - } - })); + setBitWidthSelection(event.target.value); + setS(prev => ({...prev, bit_depth_image: val})); }; - handleSignChange = (event: SelectChangeEvent) => { + const handleSignChange = (event: SelectChangeEvent) => { let val: boolean|undefined = undefined; if (event.target.value === "a") @@ -152,250 +99,173 @@ class ImageFormatSettings extends Component { else if (event.target.value === "s") val = true; - this.setState(prevState => ({ - sign_selection: event.target.value, - s: { - ...prevState.s, - signed_output: val - } - })); + setSignSelection(event.target.value); + setS(prev => ({...prev, signed_output: val})); }; - render() { - return -
- - Output image format settings - - ) => { - this.setState( - prevState => ({ - s: { - ...prevState.s, - geometry_transform: event.target.checked - } - }) - ); - }}/> - } label="Geometry transformation"/> - ) => { - this.setState( - prevState => ({s: {...prevState.s, summation: event.target.checked}}) - ); - }}/> - } label="Image summation"/> - ) => { - this.setState( - prevState => ({ - s: { - ...prevState.s, - mask_chip_edges: event.target.checked - } - }) - ); - }}/> - } label="Mask chip edges"/> + return +
+ + Output image format settings + + ) => { + setS(prev => ({...prev, geometry_transform: event.target.checked})); + }}/> + } label="Geometry transformation"/> + ) => { + setS(prev => ({...prev, summation: event.target.checked})); + }}/> + } label="Image summation"/> + ) => { + setS(prev => ({...prev, mask_chip_edges: event.target.checked})); + }}/> + } label="Mask chip edges"/> - ) => { - this.setState( - prevState => ({ - s: { - ...prevState.s, - mask_module_edges: event.target.checked - } - }) - ); - }}/> - } label="Mask module edges"/> - ) => { - this.setState( - prevState => ({ - s: { - ...prevState.s, - apply_mask: event.target.checked - } - }) - ); - }}/> - } label="Apply pixel masks on images"/> - + ) => { + setS(prev => ({...prev, mask_module_edges: event.target.checked})); + }}/> + } label="Mask module edges"/> + ) => { + setS(prev => ({...prev, apply_mask: event.target.checked})); + }}/> + } label="Apply pixel masks on images"/> +
- Output pixel value - - - Bit-width - - - - Sign - - - - JUNGFRAU specific - - ) => { - this.setState( - prevState => ({ - s: { - ...prevState.s, - jungfrau_mask_pixels_without_g0: event.target.checked - } - })); - }}/> - } label="Mask pixels not switching to G0"/> - ) => { - if (event.target.checked) { - this.setState( - prevState => ({ - jungfrau_conversion_factor_err: false, - s: { - ...prevState.s, - jungfrau_conversion: true, - jungfrau_conversion_factor_keV: undefined - } - })); - } else { - this.setState( - prevState => ({ - jungfrau_conversion_factor_err: false, - s: { - ...prevState.s, - jungfrau_conversion: false, - jungfrau_conversion_factor_keV: undefined - } - })); - } - }}/> - } label="Conversion to photons"/> + Output pixel value + + + Bit-width + + + + Sign + + + + JUNGFRAU specific + + ) => { + setS(prev => ({...prev, jungfrau_mask_pixels_without_g0: event.target.checked})); + }}/> + } label="Mask pixels not switching to G0"/> + ) => { + setJungfrauConversionFactorErr(false); + setS(prev => ({ + ...prev, + jungfrau_conversion: event.target.checked, + jungfrau_conversion_factor_keV: undefined + })); + }}/> + } label="Conversion to photons"/> - - - - - ) => { - if (!event.target.checked) - this.setState(prevState => ({ - download_counter: this.state.download_counter + 1, - jungfrau_conversion_factor_err: false, - s: { - ...prevState.s, - jungfrau_conversion_factor_keV: undefined - } - })); - else - this.setState(prevState => ({ - download_counter: this.state.download_counter + 1, - jungfrau_conversion_factor_err: false, - s: { - ...prevState.s, - jungfrau_conversion_factor_keV: this.state.jungfrau_conversion_factor_old - } - })); - }} - /> - - { - let old: number = this.state.jungfrau_conversion_factor_old; - if (!err) - old = val; - this.setState(prevState => ({ - jungfrau_conversion_factor_err: err, - jungfrau_conversion_factor_old: old, - s: {...prevState.s, jungfrau_conversion_factor_keV: val} - })); - }}/> - + + + + + ) => { + setDownloadCounter(c => c + 1); + setJungfrauConversionFactorErr(false); + if (!event.target.checked) + setS(prev => ({...prev, jungfrau_conversion_factor_keV: undefined})); + else + setS(prev => ({...prev, jungfrau_conversion_factor_keV: jungfrauConversionFactorOld})); + }} + /> + + { + if (!err) + setJungfrauConversionFactorOld(val); + setJungfrauConversionFactorErr(err); + setS(prev => ({...prev, jungfrau_conversion_factor_keV: val})); + }}/> + - - - - { - this.setState(prevState => ({ - pedestal_g0_rms_limit_err: err, - s: {...prevState.s, jungfrau_pedestal_g0_rms_limit: val} - })); - }} - /> - - + + + { + setS(prev => ({...prev, jungfrau_pedestal_g0_rms_limit: val})); + }} /> -
-
-
- } + + + +
+ } -export default ImageFormatSettings; \ No newline at end of file +export default memo(ImageFormatSettings); diff --git a/frontend/src/components/ImagePusherStatus.tsx b/frontend/src/components/ImagePusherStatus.tsx index d28633e5..0a21e00a 100644 --- a/frontend/src/components/ImagePusherStatus.tsx +++ b/frontend/src/components/ImagePusherStatus.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-only */ -import React, {Component} from 'react'; +import {memo} from 'react'; import Paper from '@mui/material/Paper'; import {Stack, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material"; import {image_pusher_status, image_pusher_type} from "../client"; @@ -32,41 +32,38 @@ function renderPusherType(type?: image_pusher_type): string { } } -class ImagePusherStatus extends Component { - render() { - const { s } = this.props; - return ( - -
- - Image pusher status - - - - - Pusher type: - {renderPusherType(s?.pusher_type)} - - - Images written: - {s?.images_written ?? "N/A"} - - - Images not written due to errors: - {s?.images_write_error ?? "N/A"} - - - FIFO utilization: - {renderFifo(s?.writer_fifo_utilization)} - - -
-
-
-
-
- ); - } +function ImagePusherStatus({s}: MyProps) { + return ( + +
+ + Image pusher status + + + + + Pusher type: + {renderPusherType(s?.pusher_type)} + + + Images written: + {s?.images_written ?? "N/A"} + + + Images not written due to errors: + {s?.images_write_error ?? "N/A"} + + + FIFO utilization: + {renderFifo(s?.writer_fifo_utilization)} + + +
+
+
+
+
+ ); } -export default ImagePusherStatus; \ No newline at end of file +export default memo(ImagePusherStatus); diff --git a/frontend/src/components/IndexingSettings.tsx b/frontend/src/components/IndexingSettings.tsx index 5859146a..3e0c9b3b 100644 --- a/frontend/src/components/IndexingSettings.tsx +++ b/frontend/src/components/IndexingSettings.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {memo, useEffect, useState} from 'react'; import {Stack, Typography, FormControlLabel, Checkbox} from "@mui/material"; import Paper from "@mui/material/Paper"; @@ -16,22 +16,6 @@ type MyProps = { status?: broker_status } -type MyState = { - s: indexing_settings, - last_downloaded_s: indexing_settings, - download_counter: number, - viable_cell_min_spots_error: boolean, - fft_high_resolution_A_error: boolean, - fft_max_unit_cell_A_error: boolean, - fft_min_unit_cell_A_error: boolean, - fft_num_vectors_error: boolean, - tolerance_error: boolean, - thread_count_error: boolean, - unit_cell_dist_tolerance_error: boolean, - rotation_indexing_min_angular_range_deg_error: boolean, - rotation_indexing_angular_stride_deg_error: boolean -} - const default_indexing_settings: indexing_settings = { algorithm: indexing_algorithm.FFBIDX, geom_refinement_algorithm: geom_refinement_algorithm.NONE, @@ -50,331 +34,274 @@ const default_indexing_settings: indexing_settings = { blocking: false }; -class IndexingSettings extends Component { - state : MyState = { - s: default_indexing_settings, - last_downloaded_s: default_indexing_settings, - download_counter: 0, - fft_high_resolution_A_error: false, - fft_max_unit_cell_A_error: false, - fft_min_unit_cell_A_error: false, - fft_num_vectors_error: false, - tolerance_error: false, - thread_count_error: false, - unit_cell_dist_tolerance_error: false, - viable_cell_min_spots_error: false, - rotation_indexing_min_angular_range_deg_error: false, - rotation_indexing_angular_stride_deg_error: false - } +function IndexingSettings({s: serverS, status}: MyProps) { + const [s, setS] = useState(default_indexing_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_indexing_settings); + const [downloadCounter, setDownloadCounter] = useState(0); + const [viableCellMinSpotsError, setViableCellMinSpotsError] = useState(false); + const [fftHighResolutionAError, setFftHighResolutionAError] = useState(false); + const [fftMaxUnitCellAError, setFftMaxUnitCellAError] = useState(false); + const [fftMinUnitCellAError, setFftMinUnitCellAError] = useState(false); + const [fftNumVectorsError, setFftNumVectorsError] = useState(false); + const [toleranceError, setToleranceError] = useState(false); + const [threadCountError, setThreadCountError] = useState(false); + const [unitCellDistToleranceError, setUnitCellDistToleranceError] = useState(false); + const [rotationIndexingMinAngularRangeDegError, setRotationIndexingMinAngularRangeDegError] = useState(false); + const [rotationIndexingAngularStrideDegError, setRotationIndexingAngularStrideDegError] = useState(false); - getValues = () => { - if (this.props.s !== undefined) { - let format_set: indexing_settings = this.props.s; - if (!_.isEqual(format_set, this.state.last_downloaded_s)) { - - this.setState(prevState => ({ - s: format_set, - last_downloaded_s: format_set, - fft_high_resolution_A_error: false, - fft_max_unit_cell_A_error: false, - fft_min_unit_cell_A_error: false, - fft_num_vectors_error: false, - tolerance_error: false, - thread_count_error: false, - unit_cell_dist_tolerance_error: false, - viable_cell_min_spots_error: false, - download_counter: prevState.download_counter + 1, - })); - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); + setFftHighResolutionAError(false); + setFftMaxUnitCellAError(false); + setFftMinUnitCellAError(false); + setFftNumVectorsError(false); + setToleranceError(false); + setThreadCountError(false); + setUnitCellDistToleranceError(false); + setViableCellMinSpotsError(false); + setDownloadCounter(c => c + 1); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - handleAlgorithmChange = (event: SelectChangeEvent) => { - this.setState(prevState => ({ - s: { - ...prevState.s, - algorithm: event.target.value as indexing_algorithm - } - })); + const handleAlgorithmChange = (event: SelectChangeEvent) => { + setS(prev => ({...prev, algorithm: event.target.value as indexing_algorithm})); }; - handleRefinementAlgorithmChange = (event: SelectChangeEvent) => { - this.setState(prevState => ({ - s: { - ...prevState.s, - geom_refinement_algorithm: event.target.value as geom_refinement_algorithm - } - })); + const handleRefinementAlgorithmChange = (event: SelectChangeEvent) => { + setS(prev => ({...prev, geom_refinement_algorithm: event.target.value as geom_refinement_algorithm})); }; - isError() { - return this.state.tolerance_error - || this.state.fft_high_resolution_A_error - || this.state.fft_min_unit_cell_A_error - || this.state.fft_max_unit_cell_A_error - || this.state.fft_num_vectors_error - || this.state.thread_count_error - || this.state.unit_cell_dist_tolerance_error - || this.state.viable_cell_min_spots_error - || this.state.rotation_indexing_angular_stride_deg_error - || this.state.rotation_indexing_min_angular_range_deg_error; - } + const isError = () => + toleranceError + || fftHighResolutionAError + || fftMinUnitCellAError + || fftMaxUnitCellAError + || fftNumVectorsError + || threadCountError + || unitCellDistToleranceError + || viableCellMinSpotsError + || rotationIndexingAngularStrideDegError + || rotationIndexingMinAngularRangeDegError; - render() { - return -
- - Indexing settings + return +
+ + Indexing settings - Algorithm - - Algorithm - - - - { - this.setState(prevState => ({ - tolerance_error: err, - s: {...prevState.s, tolerance: val} - })); - }}/> - { - this.setState(prevState => ({ - tolerance_error: err, - s: {...prevState.s, - unit_cell_dist_tolerance: val / 100.0} - })); - }}/> - { - this.setState(prevState => ({ - viable_cell_min_spots_error: err, - s: {...prevState.s, - viable_cell_min_spots: val} - })); - }}/>  + Algorithm - Geometry refinement + Algorithm - { - this.setState(prevState => ({ - s: {...prevState.s, index_ice_rings: event.target.checked} - })); - }} - /> - } - label="Index ice rings" - /> - { - this.setState(prevState => ({ - s: {...prevState.s, blocking: event.target.checked} - })); - }} - /> - } - label="Blocking thread pool" - /> - Rotation (3D) indexing settings - - - { - this.setState(prevState => ({ - s: {...prevState.s, rotation_indexing: event.target.checked} - })); - }} - /> - } - label="Enable" - /> - - { - this.setState(prevState => ({ - rotation_indexing_min_angular_range_deg_error: err, - s: {...prevState.s, rotation_indexing_min_angular_range_deg: val} - })); - }}/>  - { - this.setState(prevState => ({ - rotation_indexing_angular_stride_deg_error: err, - s: {...prevState.s, rotation_indexing_angular_stride_deg: val} - })); - }}/>  - - - - FFT indexing settings - - { - this.setState(prevState => ({ - fft_min_unit_cell_A_error: err, - s: {...prevState.s, fft_min_unit_cell_A: val} - })); - }}/>  - { - this.setState(prevState => ({ - fft_max_unit_cell_A_error: err, - s: {...prevState.s, fft_max_unit_cell_A: val} - })); - }}/>  - - { - this.setState(prevState => ({ - fft_high_resolution_A_error: err, - s: {...prevState.s, fft_high_resolution_A: val} - })); - }}/>  - - + { + setToleranceError(err); + setS(prev => ({...prev, tolerance: val})); + }}/> + { + setToleranceError(err); + setS(prev => ({...prev, unit_cell_dist_tolerance: val / 100.0})); + }}/> + { - this.setState(prevState => ({ - fft_num_vectors_error: err, - s: {...prevState.s, fft_num_vectors: val} - })); - }} - sx={{width: '80%'}}/>  - Computing resources - GPU count: {this.props.status !== undefined ? this.props.status.gpu_count : "N/A"} + setViableCellMinSpotsError(err); + setS(prev => ({...prev, viable_cell_min_spots: val})); + }}/>  + + Geometry refinement + + + { + setS(prev => ({...prev, index_ice_rings: event.target.checked})); + }} + /> + } + label="Index ice rings" + /> + { + setS(prev => ({...prev, blocking: event.target.checked})); + }} + /> + } + label="Blocking thread pool" + /> + Rotation (3D) indexing settings + - { + setS(prev => ({...prev, rotation_indexing: event.target.checked})); + }} + /> + } + label="Enable" + /> + + { - this.setState(prevState => ({ - thread_count_error: err, - s: {...prevState.s, thread_count: val} - })); - }} - sx={{width: '80%'}}/> - - + setRotationIndexingMinAngularRangeDegError(err); + setS(prev => ({...prev, rotation_indexing_min_angular_range_deg: val})); + }}/>  + { + setRotationIndexingAngularStrideDegError(err); + setS(prev => ({...prev, rotation_indexing_angular_stride_deg: val})); + }}/>  -
-
- } + + FFT indexing settings + + { + setFftMinUnitCellAError(err); + setS(prev => ({...prev, fft_min_unit_cell_A: val})); + }}/>  + { + setFftMaxUnitCellAError(err); + setS(prev => ({...prev, fft_max_unit_cell_A: val})); + }}/>  + + { + setFftHighResolutionAError(err); + setS(prev => ({...prev, fft_high_resolution_A: val})); + }}/>  + + { + setFftNumVectorsError(err); + setS(prev => ({...prev, fft_num_vectors: val})); + }} + sx={{width: '80%'}}/>  + Computing resources + GPU count: {status !== undefined ? status.gpu_count : "N/A"} + + { + setThreadCountError(err); + setS(prev => ({...prev, thread_count: val})); + }} + sx={{width: '80%'}}/> + + + +
+
+
} -export default IndexingSettings; - +export default memo(IndexingSettings); diff --git a/frontend/src/components/InstrumentMetadata.tsx b/frontend/src/components/InstrumentMetadata.tsx index 661e1786..0d91a1fd 100644 --- a/frontend/src/components/InstrumentMetadata.tsx +++ b/frontend/src/components/InstrumentMetadata.tsx @@ -1,10 +1,10 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, useEffect, useState} from 'react'; import { Autocomplete, Checkbox, FormControlLabel, FormGroup, - Grid, Stack, + Stack, TextField } from "@mui/material"; import Paper from "@mui/material/Paper"; @@ -16,11 +16,6 @@ type MyProps = { s?: instrument_metadata } -type MyState = { - s: instrument_metadata - last_downloaded_s: instrument_metadata -}; - const default_instrument_metadata: instrument_metadata = { source_name: "Swiss Light Source", instrument_name: "X06SA", @@ -39,113 +34,86 @@ const source_types : string[] = [ "Metal Jet X-ray" ]; -class ImageFormatSettings extends Component { - state : MyState = { - s: default_instrument_metadata, - last_downloaded_s: default_instrument_metadata - } +function InstrumentMetadata({s: serverS}: MyProps) { + const [s, setS] = useState(default_instrument_metadata); + const [lastDownloadedS, setLastDownloadedS] = useState(default_instrument_metadata); - getValues () { - if (this.props.s !== undefined) { - let instr_metadata: instrument_metadata = this.props.s; - if (!_.isEqual(instr_metadata, this.state.last_downloaded_s)) - this.setState({ - s: instr_metadata, - last_downloaded_s: instr_metadata - }); + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setS(serverS); + setLastDownloadedS(serverS); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - render() { - return - + return + -
Source and instrument metadata
- ) => { - this.setState(prevState => ({ - s: {...prevState.s, source_name: event.target.value} - })) - }} - value={this.state.s.source_name}/> - ) => { - this.setState(prevState => ({ - s: {...prevState.s, instrument_name: event.target.value} - })) - }} - value={this.state.s.instrument_name}/> - { - if (typeof newValue === "string") - this.setState(prevState => ({s: {...prevState.s, source_type: newValue}})); - }} +
Source and instrument metadata
+ ) => { + setS(prev => ({...prev, source_name: event.target.value})); + }} + value={s.source_name}/> + ) => { + setS(prev => ({...prev, instrument_name: event.target.value})); + }} + value={s.instrument_name}/> + { + if (typeof newValue === "string") + setS(prev => ({...prev, source_type: newValue})); + }} - inputValue={this.state.s.source_type} + inputValue={s.source_type} - onInputChange={(_, newInputValue: string) => { - this.setState(prevState => ({ - s: {...prevState.s, source_type: newInputValue} - })); - }} - selectOnFocus - clearOnBlur - value={this.state.s.source_type} - options={source_types} - renderInput={(params) => } - /> - - ) => { - this.setState( - prevState => ({ - s: {...prevState.s, pulsed_source: event.target.checked} - })); - }}/> - } label="Pulsed source (XFEL)"/> - ) => { - this.setState( - prevState => ({ - s: {...prevState.s, electron_source: event.target.checked} - })); - }}/> - } label="Electron source"/> - - { + setS(prev => ({...prev, source_type: newInputValue})); + }} + selectOnFocus + clearOnBlur + value={s.source_type} + options={source_types} + renderInput={(params) => } /> -
-
-
- } + + ) => { + setS(prev => ({...prev, pulsed_source: event.target.checked})); + }}/> + } label="Pulsed source (XFEL)"/> + ) => { + setS(prev => ({...prev, electron_source: event.target.checked})); + }}/> + } label="Electron source"/> + + +
+
+
} -export default ImageFormatSettings; \ No newline at end of file +export default memo(InstrumentMetadata); diff --git a/frontend/src/components/MeasurementStatistics.tsx b/frontend/src/components/MeasurementStatistics.tsx index 28bc8736..71bf5c3a 100644 --- a/frontend/src/components/MeasurementStatistics.tsx +++ b/frontend/src/components/MeasurementStatistics.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {memo} from 'react'; import Paper from '@mui/material/Paper'; import {Grid, Table, TableBody, TableCell, TableContainer, TableRow,} from "@mui/material"; @@ -8,83 +8,78 @@ type MyProps = { s: measurement_statistics, }; -type MyState = {}; +function MeasurementStatistics({s}: MyProps) { + return + + + +
Measurement statistics

-class MeasurementStatistics extends Component { - - render() { - return - - - -
Measurement statistics

+ + + + + File prefix: + {(s.file_prefix !== undefined) ? s.file_prefix : "(images not written)"} + + + Run number: + {(s.run_number !== undefined) ? s.run_number : "(not set)"} + + + Experiment group: + {s.experiment_group} + - -
- - - File prefix: - {(this.props.s.file_prefix !== undefined) ? this.props.s.file_prefix : "(images not written)"} - - - Run number: - {(this.props.s.run_number !== undefined) ? this.props.s.run_number : "(not set)"} - - - Experiment group: - {this.props.s.experiment_group} - + + Images expected: + {s.images_expected} + + + Images collected: + {s.images_collected} + + + Images sent to writer: + {s.images_sent} + + + Images discarded by lossy compression: + {s.images_discarded_lossy_compression} + + + Compression ratio: + {(s.compression_ratio !== undefined) + ? (s.compression_ratio.toFixed(1)) + "x" : "-" } - - Images expected: - {this.props.s.images_expected} - - - Images collected: - {this.props.s.images_collected} - - - Images sent to writer: - {this.props.s.images_sent} - - - Images discarded by lossy compression: - {this.props.s.images_discarded_lossy_compression} - - - Compression ratio: - {(this.props.s.compression_ratio !== undefined) - ? (this.props.s.compression_ratio.toFixed(1)) + "x" : "-" } - - - - Data acquisition efficiency: - {(this.props.s.collection_efficiency !== undefined) - ? (Math.floor(this.props.s.collection_efficiency * 1000.0) / 1000.0).toFixed(3) : "-"} - - - Indexing rate: - {(this.props.s.indexing_rate !== undefined) - ? this.props.s.indexing_rate.toFixed(2) : "-"} - - - Background estimate: - {(this.props.s.bkg_estimate !== undefined) - ? this.props.s.bkg_estimate.toPrecision(7) : "-"} - - - Unit cell: - {this.props.s.unit_cell} - - -
-
-
-
- + + + Data acquisition efficiency: + {(s.collection_efficiency !== undefined) + ? (Math.floor(s.collection_efficiency * 1000.0) / 1000.0).toFixed(3) : "-"} + + + Indexing rate: + {(s.indexing_rate !== undefined) + ? s.indexing_rate.toFixed(2) : "-"} + + + Background estimate: + {(s.bkg_estimate !== undefined) + ? s.bkg_estimate.toPrecision(7) : "-"} + + + Unit cell: + {s.unit_cell} + + + + +
-
- } + + +
} -export default MeasurementStatistics; \ No newline at end of file +export default memo(MeasurementStatistics); diff --git a/frontend/src/components/NumberTextField.tsx b/frontend/src/components/NumberTextField.tsx index 35ec5e70..663ac939 100644 --- a/frontend/src/components/NumberTextField.tsx +++ b/frontend/src/components/NumberTextField.tsx @@ -1,10 +1,9 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, ReactNode, useEffect, useState} from 'react'; import { InputAdornment, SxProps, TextField, Theme } from "@mui/material"; -import { ReactNode } from "react"; type MyProps = { start_val?: number, @@ -21,94 +20,76 @@ type MyProps = { float?: boolean } -type MyState = { - text: string, - err: boolean, - val: number +function unitsNode(str: string | undefined): ReactNode | null { + if (str === undefined) + return null; + else if (str === "us") + return µs; + else if (str === "A-1") + return + -1 + ; + else + return {str}; } -class NumberTextField extends Component { - state : MyState = { - text: ".", - err: true, - val: 0 - } +function NumberTextField({start_val, default: defaultValue, label, callback, counter, min, max, sx, units, disabled, float}: MyProps) { + const [text, setText] = useState("."); + const [err, setErr] = useState(true); + const [val, setVal] = useState(0); - setup() { - let val : number = 0; - if (this.props.start_val !== undefined) - val = this.props.start_val; - else if (this.props.default !== undefined) - val = this.props.default; + // Something has to change in parent to trigger setup => otherwise this makes infinite loop. + // In case of error state start_val could stay same, but we want to return to "safe" set + // so there is external counter that if changed reset is triggered. + useEffect(() => { + let v : number = 0; + if (start_val !== undefined) + v = start_val; + else if (defaultValue !== undefined) + v = defaultValue; - if (this.props.float !== true) - val = Math.round(val); + if (float !== true) + v = Math.round(v); else - val = Math.round(val * 1000.0) / 1000.0 + v = Math.round(v * 1000.0) / 1000.0 - this.setState({ - err: false, - val: val, - text: val.toString() - }); - } + setErr(false); + setVal(v); + setText(v.toString()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [counter, start_val]); - componentDidMount() { - this.setup(); - } + const updateValue = (event: ChangeEvent) => { + let new_text = event.target.value; + if (!new_text) new_text = "0"; - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { - // Something has to change in parent function to trigger setup => otherwise this makes infinite loop - // In case of error state start_val could stay same, but we want to return to "safe" set - // so there is external counter that if changed reset is triggered - if ((prevProps.counter !== this.props.counter) || (prevProps.start_val !== this.props.start_val)) - this.setup(); - } + let num_val : number = Number(new_text); - updateValue = (event: React.ChangeEvent) => { - let text = event.target.value; - if (!text) text = "0"; - - let num_val : number = Number(text); - - let err : boolean = !Number.isFinite(num_val) - || (!this.props.float && !Number.isInteger(num_val)) - || ((this.props.min !== undefined) && (num_val < this.props.min)) - || ((this.props.max !== undefined) && (num_val > this.props.max)); + let new_err : boolean = !Number.isFinite(num_val) + || (!float && !Number.isInteger(num_val)) + || ((min !== undefined) && (num_val < min)) + || ((max !== undefined) && (num_val > max)); // If error I keep old numeric value to return (so there is always a proper value kept) - let new_val : number = err ? this.state.val : num_val; - this.setState({err: err, val: new_val, text: text}); - this.props.callback(new_val, err); - } + let new_val : number = new_err ? val : num_val; + setErr(new_err); + setVal(new_val); + setText(new_text); + callback(new_val, new_err); + }; - units(str: string | undefined): ReactNode | null { - if (str === undefined) - return null; - else if (str === "us") - return µs; - else if (str === "A-1") - return - -1 - ; - else - return {str}; - } - - render() { - return - } + return } -export default NumberTextField; \ No newline at end of file +export default memo(NumberTextField); diff --git a/frontend/src/components/PixelMask.tsx b/frontend/src/components/PixelMask.tsx index 7130fabd..d10a6df8 100644 --- a/frontend/src/components/PixelMask.tsx +++ b/frontend/src/components/PixelMask.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import {ChangeEvent, memo, useState} from 'react'; import Paper from '@mui/material/Paper'; import { @@ -16,67 +16,61 @@ type MyProps = { s?: pixel_mask_statistics }; -type MyState = { - file?: File -}; +function PixelMask({s}: MyProps) { + const [file, setFile] = useState(); -class PixelMask extends React.Component { - state: MyState = {}; - - handleUpload = () => { - if (this.state.file) - putConfigUserMaskTiff({ body: this.state.file }); + const handleUpload = () => { + if (file) + putConfigUserMaskTiff({ body: file }); }; - handleFileChange = (event: React.ChangeEvent) => { + const handleFileChange = (event: ChangeEvent) => { if (event.target.files && event.target.files[0]) { - this.setState({file: event.target.files[0]}); + setFile(event.target.files[0]); } }; - render() { - return -
Pixel mask

- - - - - - - - User mask: - {this.props.s?.user_mask ?? "N/A"} - - - Error pixels: - {this.props.s?.wrong_gain ?? "N/A"} - - - Noisy pixels: - {this.props.s?.too_high_pedestal_rms ?? "N/A"} - - -
-
-
- + return +
Pixel mask

+ + + + + + + + User mask: + {s?.user_mask ?? "N/A"} + + + Error pixels: + {s?.wrong_gain ?? "N/A"} + + + Noisy pixels: + {s?.too_high_pedestal_rms ?? "N/A"} + + +
+
-
- Download pixel mask
- Download user pixel mask

-

- -

-
- } + + +
+ Download pixel mask
+ Download user pixel mask

+

+ +

+
} -export default PixelMask; \ No newline at end of file +export default memo(PixelMask); diff --git a/frontend/src/components/PreviewImage.tsx b/frontend/src/components/PreviewImage.tsx index 11d193bb..bb84605d 100644 --- a/frontend/src/components/PreviewImage.tsx +++ b/frontend/src/components/PreviewImage.tsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import {ChangeEvent, memo, useEffect, useRef, useState} from 'react'; import Paper from "@mui/material/Paper"; import { Checkbox, @@ -37,16 +37,6 @@ type MyProps = { max_image_number: number | undefined } -type MyState = { - settings: preview_settings, - s: Blob | null, - s_url: string | null, - update: boolean, - connection_error: boolean, - image_id_mode: string, - image_id: number -} - function handleErrors(response: Response): Response { if (!response.ok) { throw Error(response.statusText); @@ -63,197 +53,35 @@ function stringToEnum(value: string): color_scale { return enumValue || color_scale.INDIGO; } -class PreviewImage extends Component { - interval: ReturnType | undefined; +const default_preview_settings: preview_settings = { + saturation: 10, + jpeg_quality: 90, + show_spots: true, + show_roi: false, + show_user_mask: false, + show_beam_center: true, + resolution_ring: 0.5, + scale: color_scale.INDIGO, + image_id: -1, + auto_contrast: true, + res_estimate: true +}; - state : MyState = { - s: null, - settings: { - saturation: 10, - jpeg_quality: 90, - show_spots: true, - show_roi: false, - show_user_mask: false, - show_beam_center: true, - resolution_ring: 0.5, - scale: color_scale.INDIGO, - image_id: -1, - auto_contrast: true, - res_estimate: true - }, - s_url: null, - update: true, - connection_error: true, - image_id_mode: 'last', - image_id: 0 - } +function PreviewImage({measuring, min_image_number, max_image_number}: MyProps) { + const [settings, setSettings] = useState(default_preview_settings); + const [sUrl, setSUrl] = useState(null); + const [update, setUpdate] = useState(true); + const [connectionError, setConnectionError] = useState(true); + const [imageIdMode, setImageIdMode] = useState('last'); + const [imageId, setImageId] = useState(0); - update_image_id_mode = (event: React.ChangeEvent) => { - let image_id : number; - switch (event.target.value) { - case "last": - image_id = -1; - break; - case "last_indexed": - image_id = -2; - break; - case "select": - image_id = this.state.image_id; - break; - default: - return; - } + // Refs let the polling interval read the latest settings without being + // re-created on every settings change, and let cleanup revoke the live URL. + const sUrlRef = useRef(null); + const settingsRef = useRef(settings); + settingsRef.current = settings; - let s : preview_settings = {...this.state.settings, image_id}; - this.setState({settings: s, image_id_mode: event.target.value}); - this.getValues(s); - } - - updateToggle = (event: React.ChangeEvent) => { - this.setState({ - update: event.target.checked - }); - if (event.target.checked) - this.getValues(); - } - - setSaturation = (event: Event, newValue: number | number[]) => { - let s : preview_settings = { - ...this.state.settings, - saturation: newValue as number - }; - this.setState({settings: s}); - this.getValues(s); - } - - setSaturationText = (event: React.ChangeEvent) => { - let newValue = 0; - if (event.target.value) - newValue = Number(event.target.value); - if (newValue < 0) - newValue = 0; - - let s : preview_settings = { - ...this.state.settings, - saturation: newValue as number - }; - this.setState({settings: s}); - this.getValues(s); - }; - - showSpotsToggle = (event: React.ChangeEvent) => { - let s : preview_settings = { - ...this.state.settings, - show_spots: event.target.checked - }; - this.setState({settings: s}); - this.getValues(s); - } - - resEstToggle = (event: React.ChangeEvent) => { - let s: preview_settings = { - ...this.state.settings, - res_estimate: event.target.checked - } - this.setState({settings: s}); - this.getValues(s); - } - - autoContrastToggle = (event: React.ChangeEvent) => { - let s: preview_settings = { - ...this.state.settings, - auto_contrast: event.target.checked - } - this.setState({settings: s}); - this.getValues(s); - } - - showROIToggle = (event: React.ChangeEvent) => { - let s : preview_settings = { - ...this.state.settings, - show_roi: event.target.checked - }; - this.setState({settings: s}); - this.getValues(s); - } - - showBeamCenterToggle = (event: React.ChangeEvent) => { - let s : preview_settings = { - ...this.state.settings, - show_beam_center: event.target.checked - }; - this.setState({settings: s}); - this.getValues(s); - } - - setResolutionRing = (event: Event, newValue: number | number[]) => { - let s : preview_settings = { - ...this.state.settings, - resolution_ring: newValue as number - }; - this.setState({settings: s}); - this.getValues(s); - } - - setResolutionRingText = (event: React.ChangeEvent) => { - let newValue = 0.5; - if (event.target.value) - newValue = Number(event.target.value); - if (newValue < 0.5) - newValue = 0.5; - if (newValue > 50.0) - newValue = 50.0; - - let s : preview_settings = { - ...this.state.settings, - resolution_ring: newValue as number - }; - this.setState({settings: s}); - this.getValues(s); - }; - - setImageID = (event: Event, newValue: number | number[]) => { - if (this.state.image_id_mode == "select") { - let s : preview_settings = { - ...this.state.settings, - image_id: newValue as number - }; - this.setState({settings: s, image_id: newValue as number}); - this.getValues(s); - } else { - this.setState({image_id: newValue as number}); - } - } - - setImageIdText = (event: React.ChangeEvent) => { - let newValue = 0; - if (event.target.value) - newValue = Number(event.target.value); - if (newValue < 0) - newValue = 0; - - if (this.state.image_id_mode == "select") { - let s : preview_settings = { - ...this.state.settings, - image_id: newValue as number - }; - this.setState({settings: s, image_id: newValue as number}); - this.getValues(s); - } else { - this.setState({image_id: newValue as number}); - } - }; - - showUserMaskToggle = (event: React.ChangeEvent) => { - let s : preview_settings = { - ...this.state.settings, - show_user_mask: event.target.checked - }; - this.setState({settings: s}); - this.getValues(s); - } - - getImage(s: preview_settings,) { + const getImage = (s: preview_settings) => { let url = `/image_buffer/image.jpeg`; url += `?id=${s.image_id}`; if (!s.auto_contrast) @@ -273,190 +101,321 @@ class PreviewImage extends Component { .then(handleErrors) .then(data => data.blob()) .then(data => { - const url = URL.createObjectURL(data); - let tmp = this.state.s_url; - this.setState({s: data, s_url: url, connection_error: false}); + const objectUrl = URL.createObjectURL(data); + const tmp = sUrlRef.current; + sUrlRef.current = objectUrl; + setSUrl(objectUrl); + setConnectionError(false); if (tmp !== null) URL.revokeObjectURL(tmp); }).catch(error => { - this.setState({connection_error: true}); + setConnectionError(true); }) - } + }; - getValues(s: preview_settings = this.state.settings) { - this.getImage(s); - } + const getValues = (s: preview_settings = settings) => { + getImage(s); + }; - componentDidMount() { - this.getValues(); - this.interval = setInterval(() => { - if (this.state.update && this.props.measuring) - this.getValues() + // Initial fetch + revoke the live object URL on unmount. + useEffect(() => { + getImage(settingsRef.current); + return () => { + if (sUrlRef.current !== null) + URL.revokeObjectURL(sUrlRef.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Auto-update poll; reads the latest settings via ref. + useEffect(() => { + const id = setInterval(() => { + if (update && measuring) + getImage(settingsRef.current); }, 2000); - } + return () => clearInterval(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [update, measuring]); - componentWillUnmount() { - clearInterval(this.interval); - let tmp = this.state.s_url; - if (tmp !== null) - URL.revokeObjectURL(tmp); - } + const update_image_id_mode = (event: ChangeEvent) => { + let image_id : number; + switch (event.target.value) { + case "last": + image_id = -1; + break; + case "last_indexed": + image_id = -2; + break; + case "select": + image_id = imageId; + break; + default: + return; + } - render() { - return
+ let s : preview_settings = {...settings, image_id}; + setSettings(s); + setImageIdMode(event.target.value); + getValues(s); + }; - + const updateToggle = (event: ChangeEvent) => { + setUpdate(event.target.checked); + if (event.target.checked) + getValues(); + }; - - - {(!this.state.connection_error && (this.state.s_url !== null)) ? - - - - Live preview - - - :
Preview image not available
- } -
-
- - -
- -
Preview image selection
- - - } label="Last image" /> - } label="Last indexed image" /> - } label="Choose image" /> - - - - - - -
Preview image settings
- + const setSaturation = (event: Event, newValue: number | number[]) => { + let s : preview_settings = {...settings, saturation: newValue as number}; + setSettings(s); + getValues(s); + }; - - } label="Auto-update image"/> - - } label="Auto-contrast"/> - - } label="Show spots"/> - - } label="Show ROI"/> - - } label="Show resolution estimate (auto ring)"/> - - } label="Show beam center"/> - - } label="Show user mask"/> - - - Color map - - - - + const setSaturationText = (event: ChangeEvent) => { + let newValue = 0; + if (event.target.value) + newValue = Number(event.target.value); + if (newValue < 0) + newValue = 0; - -
Saturation level + let s : preview_settings = {...settings, saturation: newValue as number}; + setSettings(s); + getValues(s); + }; - - - - -
Resolution Ring -
-
-
+ const showSpotsToggle = (event: ChangeEvent) => { + let s : preview_settings = {...settings, show_spots: event.target.checked}; + setSettings(s); + getValues(s); + }; + + const resEstToggle = (event: ChangeEvent) => { + let s: preview_settings = {...settings, res_estimate: event.target.checked}; + setSettings(s); + getValues(s); + }; + + const autoContrastToggle = (event: ChangeEvent) => { + let s: preview_settings = {...settings, auto_contrast: event.target.checked}; + setSettings(s); + getValues(s); + }; + + const showROIToggle = (event: ChangeEvent) => { + let s : preview_settings = {...settings, show_roi: event.target.checked}; + setSettings(s); + getValues(s); + }; + + const showBeamCenterToggle = (event: ChangeEvent) => { + let s : preview_settings = {...settings, show_beam_center: event.target.checked}; + setSettings(s); + getValues(s); + }; + + const setResolutionRing = (event: Event, newValue: number | number[]) => { + let s : preview_settings = {...settings, resolution_ring: newValue as number}; + setSettings(s); + getValues(s); + }; + + const setResolutionRingText = (event: ChangeEvent) => { + let newValue = 0.5; + if (event.target.value) + newValue = Number(event.target.value); + if (newValue < 0.5) + newValue = 0.5; + if (newValue > 50.0) + newValue = 50.0; + + let s : preview_settings = {...settings, resolution_ring: newValue as number}; + setSettings(s); + getValues(s); + }; + + const setImageIDSlider = (event: Event, newValue: number | number[]) => { + if (imageIdMode == "select") { + let s : preview_settings = {...settings, image_id: newValue as number}; + setSettings(s); + setImageId(newValue as number); + getValues(s); + } else { + setImageId(newValue as number); + } + }; + + const setImageIdText = (event: ChangeEvent) => { + let newValue = 0; + if (event.target.value) + newValue = Number(event.target.value); + if (newValue < 0) + newValue = 0; + + if (imageIdMode == "select") { + let s : preview_settings = {...settings, image_id: newValue as number}; + setSettings(s); + setImageId(newValue as number); + getValues(s); + } else { + setImageId(newValue as number); + } + }; + + const showUserMaskToggle = (event: ChangeEvent) => { + let s : preview_settings = {...settings, show_user_mask: event.target.checked}; + setSettings(s); + getValues(s); + }; + + return
+ + + + + + {(!connectionError && (sUrl !== null)) ? + + + + Live preview + + + :
Preview image not available
+ } +
-
+ + +
+ +
Preview image selection
+ + + } label="Last image" /> + } label="Last indexed image" /> + } label="Choose image" /> + + + + + + +
Preview image settings
+ - } + + } label="Auto-update image"/> + + } label="Auto-contrast"/> + + } label="Show spots"/> + + } label="Show ROI"/> + + } label="Show resolution estimate (auto ring)"/> + + } label="Show beam center"/> + + } label="Show user mask"/> + + + Color map + + + + + + +
Saturation level + + + + + +
Resolution Ring +
+
+
+
+
} -export default PreviewImage; +export default memo(PreviewImage); diff --git a/frontend/src/components/ROI.tsx b/frontend/src/components/ROI.tsx index 76c7b754..882257a2 100644 --- a/frontend/src/components/ROI.tsx +++ b/frontend/src/components/ROI.tsx @@ -1,10 +1,8 @@ -import React from 'react'; +import {memo, ReactNode, useEffect, useRef, useState} from 'react'; import Paper from '@mui/material/Paper'; import { - putConfigRoi, - instrument_metadata, - pixel_mask_statistics, roi_azimuthal, + roi_azimuthal, roi_box, roi_circle, roi_definitions @@ -16,7 +14,6 @@ import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import ErrorIcon from '@mui/icons-material/Error'; import ButtonWithSnackbar from "./ButtonWithSnackbar"; import _ from "lodash"; -import { ReactNode } from "react"; type MyProps = { s?: roi_definitions @@ -26,247 +23,214 @@ type BoxWrapper = roi_box & {id: number}; type CircleWrapper = roi_circle & {id: number}; type AzimWrapper = roi_azimuthal & {id: number}; -type MyState = { - last_downloaded_s: roi_definitions; - box: BoxWrapper[]; - circle: CircleWrapper[]; - azim: AzimWrapper[]; - box_err?: string; - circle_err?: string; - azim_err?: string | ReactNode; -}; - function getRandomInt(max : number) { return Math.floor(Math.random() * max); } -class ROI extends React.Component { - counter: number = 0; +function render_err(msg?: string | ReactNode) : ReactNode { + if (msg === undefined) + return


+ return

 {msg}
; +} - state : MyState = { - last_downloaded_s: { - box: {rois: []}, - circle: {rois: []}, - azim: {rois: []} - }, - box: [], - circle: [], - azim: [] - } +const default_roi_definitions: roi_definitions = { + box: {rois: []}, + circle: {rois: []}, + azim: {rois: []} +}; - getValues () { - if (this.props.s !== undefined) { - let tmp: roi_definitions = this.props.s; - if (!_.isEqual(tmp, this.state.last_downloaded_s)) - this.setState({ - circle_err: undefined, - circle: (this.props.s.circle.rois === undefined) ? [] : - this.props.s.circle.rois.map( - (elem) : CircleWrapper => {return {id: this.counter++, ...elem}} - ), - box_err: undefined, - box: (this.props.s.box.rois === undefined) ? [] : - this.props.s.box.rois.map( - (elem) : BoxWrapper => {return {id: this.counter++, ...elem}} - ), - azim: (this.props.s.azim.rois === undefined) ? [] : - this.props.s.azim.rois.map( - (elem) : AzimWrapper => {return {id: this.counter++, ...elem}} - ), - azim_err: undefined, - last_downloaded_s: tmp - }); +function ROI({s: serverS}: MyProps) { + const counter = useRef(0); + + const [lastDownloadedS, setLastDownloadedS] = useState(default_roi_definitions); + const [box, setBox] = useState([]); + const [circle, setCircle] = useState([]); + const [azim, setAzim] = useState([]); + const [boxErr, setBoxErr] = useState(); + const [circleErr, setCircleErr] = useState(); + const [azimErr, setAzimErr] = useState(); + + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + setCircleErr(undefined); + setCircle((serverS.circle.rois === undefined) ? [] : + serverS.circle.rois.map((elem) : CircleWrapper => ({id: counter.current++, ...elem}))); + setBoxErr(undefined); + setBox((serverS.box.rois === undefined) ? [] : + serverS.box.rois.map((elem) : BoxWrapper => ({id: counter.current++, ...elem}))); + setAzim((serverS.azim.rois === undefined) ? [] : + serverS.azim.rois.map((elem) : AzimWrapper => ({id: counter.current++, ...elem}))); + setAzimErr(undefined); + setLastDownloadedS(serverS); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } - - componentDidUpdate() { - this.getValues(); - } - - checkDuplicatesBox(input : BoxWrapper[]) { + const checkDuplicatesBox = (input : BoxWrapper[]) => { let roi_names : string[] = []; input.map(element => roi_names.push(element.name)); - this.state.circle.map(element => roi_names.push(element.name)); - this.state.azim.map(element => roi_names.push(element.name)); + circle.map(element => roi_names.push(element.name)); + azim.map(element => roi_names.push(element.name)); const duplicates = roi_names.filter((item, index) => roi_names.indexOf(item) !== index); return (duplicates.length !== 0); - } + }; - checkDuplicatesCircle(input : CircleWrapper[]) { + const checkDuplicatesCircle = (input : CircleWrapper[]) => { let roi_names : string[] = []; input.map(element => roi_names.push(element.name)); - this.state.box.map(element => roi_names.push(element.name)); - this.state.azim.map(element => roi_names.push(element.name)); + box.map(element => roi_names.push(element.name)); + azim.map(element => roi_names.push(element.name)); const duplicates = roi_names.filter((item, index) => roi_names.indexOf(item) !== index); return (duplicates.length !== 0); - } + }; - checkDuplicatesAzim(input : AzimWrapper[]) { + const checkDuplicatesAzim = (input : AzimWrapper[]) => { let roi_names : string[] = []; input.map(element => roi_names.push(element.name)); - this.state.box?.map(element => roi_names.push(element.name)); - this.state.circle.map(element => roi_names.push(element.name)); + box?.map(element => roi_names.push(element.name)); + circle.map(element => roi_names.push(element.name)); const duplicates = roi_names.filter((item, index) => roi_names.indexOf(item) !== index); return (duplicates.length !== 0); - } + }; - validateBoxInteger(input: BoxWrapper[]) { - return input.every((x) : boolean => + const validateBoxInteger = (input: BoxWrapper[]) => + input.every((x) : boolean => Number.isInteger(x.min_x_pxl) && Number.isInteger(x.max_x_pxl) && Number.isInteger(x.min_y_pxl) && Number.isInteger(x.max_y_pxl)); - } - validateBoxBounds(input: BoxWrapper[]) { - return input.every((x) : boolean => + const validateBoxBounds = (input: BoxWrapper[]) => + input.every((x) : boolean => (x.min_x_pxl >= 0) && (x.max_x_pxl >= x.min_x_pxl) && (x.min_y_pxl >= 0) && (x.max_y_pxl >= x.min_y_pxl)); - } - validateCircleValues(input: CircleWrapper[]) { - return input.every((x) : boolean => x.radius_pxl >= 0); - } + const validateCircleValues = (input: CircleWrapper[]) => + input.every((x) : boolean => x.radius_pxl >= 0); - validateAzimValues(input: AzimWrapper[]) { - return input.every((x) : boolean => x.q_min_recipA >= 0 && x.q_max_recipA > x.q_min_recipA); - } + const validateAzimValues = (input: AzimWrapper[]) => + input.every((x) : boolean => x.q_min_recipA >= 0 && x.q_max_recipA > x.q_min_recipA); - validateBox (input:BoxWrapper[]) : undefined | string { - if (this.checkDuplicatesBox(input)) + const validateBox = (input:BoxWrapper[]) : undefined | string => { + if (checkDuplicatesBox(input)) return "Duplicate ROI names."; - if (!this.validateBoxInteger(input)) + if (!validateBoxInteger(input)) return "Box bounds are not integer values."; - if (!this.validateBoxBounds(input)) + if (!validateBoxBounds(input)) return "Box bounds are not correct (0 <= min <= max)"; return undefined; - } + }; - validateCircle (input:CircleWrapper[]) : undefined | string { - if (this.checkDuplicatesCircle(input)) + const validateCircle = (input:CircleWrapper[]) : undefined | string => { + if (checkDuplicatesCircle(input)) return "Duplicate ROI names."; - if (!this.validateCircleValues(input)) + if (!validateCircleValues(input)) return "Circle ROI radius must be positive."; return undefined; - } + }; - validateAzim (input:AzimWrapper[]) : undefined | string | ReactNode { - if (this.checkDuplicatesAzim(input)) + const validateAzim = (input:AzimWrapper[]) : undefined | string | ReactNode => { + if (checkDuplicatesAzim(input)) return "Duplicate ROI names."; - if (!this.validateAzimValues(input)) + if (!validateAzimValues(input)) return
Azimuthal integration parameters must be 0 < Qmin < Qmax.
; return undefined; - } + }; - uploadButton = () => { this.putValues(); } - downloadButton = () => { this.getValues(); } - - addBoxButton = () => { - this.setState({ - box: [...this.state.box, - { - id: this.counter++, - name: "roi" + getRandomInt(99999), - min_x_pxl: 0, - max_x_pxl: 0, - min_y_pxl: 0, - max_y_pxl: 0 - } - ] - }); - } - - addCircleButton = () => { - this.setState({ - circle: [ - ...this.state.circle, - { - id: this.counter++, - name: "roi" + getRandomInt(65536), - center_x_pxl: 0, - center_y_pxl: 0, - radius_pxl: 10 - } - ] - }); - } - - - addAzimButton = () => { - this.setState({ - azim: [ - ...this.state.azim, - { - id: this.counter++, - name: "roi" + getRandomInt(65536), - q_min_recipA: 1, - q_max_recipA: 3 - } - ] - }); - } - roiStruct = () : roi_definitions => { - return { - box: { - rois: this.state.box.map((elem): roi_box => elem) - }, - circle: { - rois: this.state.circle.map((elem): roi_circle => elem) - }, - azim: { - rois: this.state.azim.map((elem): roi_azimuthal => elem) + const addBoxButton = () => { + setBox([...box, + { + id: counter.current++, + name: "roi" + getRandomInt(99999), + min_x_pxl: 0, + max_x_pxl: 0, + min_y_pxl: 0, + max_y_pxl: 0 } + ]); + }; + + const addCircleButton = () => { + setCircle([ + ...circle, + { + id: counter.current++, + name: "roi" + getRandomInt(65536), + center_x_pxl: 0, + center_y_pxl: 0, + radius_pxl: 10 + } + ]); + }; + + const addAzimButton = () => { + setAzim([ + ...azim, + { + id: counter.current++, + name: "roi" + getRandomInt(65536), + q_min_recipA: 1, + q_max_recipA: 3 + } + ]); + }; + + const roiStruct = () : roi_definitions => ({ + box: { + rois: box.map((elem): roi_box => elem) + }, + circle: { + rois: circle.map((elem): roi_circle => elem) + }, + azim: { + rois: azim.map((elem): roi_azimuthal => elem) } - } + }); - putValues = () => { - putConfigRoi({ body: this.roiStruct(), throwOnError: true }).catch(error => {} ); - } - - - - handleDeleteBoxROIClick = (id: GridRowModel) => () => { - let new_state : BoxWrapper[] = this.state.box.filter((row) => row.id !== id.id); - let err = this.validateBox(new_state); - this.setState({box_err: err, box: new_state}); + const handleDeleteBoxROIClick = (id: GridRowModel) => () => { + let new_state : BoxWrapper[] = box.filter((row) => row.id !== id.id); + let err = validateBox(new_state); + setBoxErr(err); + setBox(new_state); }; - handleDeleteCircleROIClick = (id: GridRowModel) => () => { - let new_state : CircleWrapper[] = this.state.circle.filter((row) => row.id !== id.id); - let err = this.validateCircle(new_state); - this.setState({circle_err: err, circle: new_state}); + const handleDeleteCircleROIClick = (id: GridRowModel) => () => { + let new_state : CircleWrapper[] = circle.filter((row) => row.id !== id.id); + let err = validateCircle(new_state); + setCircleErr(err); + setCircle(new_state); }; - handleDeleteAzimROIClick = (id: GridRowModel) => () => { - let new_state : AzimWrapper[] = this.state.azim.filter((row) => row.id !== id.id); - let err = this.validateAzim(new_state); - this.setState({azim_err: err, azim: new_state}); + const handleDeleteAzimROIClick = (id: GridRowModel) => () => { + let new_state : AzimWrapper[] = azim.filter((row) => row.id !== id.id); + let err = validateAzim(new_state); + setAzimErr(err); + setAzim(new_state); }; - processRowBoxUpdate = (newRow: BoxWrapper, oldRow: BoxWrapper) => { - let new_state = this.state.box.map((item) => item.id === oldRow.id ? newRow : item); - let err = this.validateBox(new_state); - this.setState({ box_err: err, box: new_state }); + const processRowBoxUpdate = (newRow: BoxWrapper, oldRow: BoxWrapper) => { + let new_state = box.map((item) => item.id === oldRow.id ? newRow : item); + let err = validateBox(new_state); + setBoxErr(err); + setBox(new_state); return newRow; }; - processRowCircleUpdate = (newRow: CircleWrapper, oldRow: CircleWrapper) => { - let new_state = this.state.circle.map((item) => item === oldRow ? newRow : item); - let err = this.validateCircle(new_state); - this.setState({ circle_err: err, circle: new_state }); + const processRowCircleUpdate = (newRow: CircleWrapper, oldRow: CircleWrapper) => { + let new_state = circle.map((item) => item === oldRow ? newRow : item); + let err = validateCircle(new_state); + setCircleErr(err); + setCircle(new_state); return newRow; }; - processRowAzimUpdate = (newRow: AzimWrapper, oldRow: AzimWrapper) => { - let new_state = this.state.azim.map((item) => item === oldRow ? newRow : item); - let err = this.validateAzim(new_state); - this.setState({ azim_err: err, azim: new_state }); + const processRowAzimUpdate = (newRow: AzimWrapper, oldRow: AzimWrapper) => { + let new_state = azim.map((item) => item === oldRow ? newRow : item); + let err = validateAzim(new_state); + setAzimErr(err); + setAzim(new_state); return newRow; }; - columns_box : GridColDef[] = [ + const columns_box : GridColDef[] = [ { field: 'name', type: 'string', headerName: 'Name' , editable: true, width: 200}, { field: 'min_x_pxl', type: 'number', headerName: 'Min X', editable: true}, { field: 'max_x_pxl', type: 'number', headerName: 'Max X', editable: true}, @@ -278,14 +242,14 @@ class ROI extends React.Component { } label="Delete" - onClick={this.handleDeleteBoxROIClick(id)} + onClick={handleDeleteBoxROIClick(id)} color="inherit" />, ]; }} ]; - columns_circle : GridColDef[] = [ + const columns_circle : GridColDef[] = [ { field: 'name', type: 'string', headerName: 'Name' , editable: true, width: 200}, { field: 'center_x_pxl', type: 'number', headerName: 'X [pxl]', editable: true}, { field: 'center_y_pxl', type: 'number', headerName: 'Y [pxl]', editable: true}, @@ -296,14 +260,14 @@ class ROI extends React.Component { } label="Delete" - onClick={this.handleDeleteCircleROIClick(id)} + onClick={handleDeleteCircleROIClick(id)} color="inherit" />, ]; }} ]; - columns_azim : GridColDef[] = [ + const columns_azim : GridColDef[] = [ { field: 'name', type: 'string', headerName: 'Name' , editable: true, width: 200}, { field: 'q_min_recipA', type: 'number', editable: true, renderHeader: () => (
Qmin [Å⁻¹]
) }, @@ -357,94 +321,85 @@ class ROI extends React.Component { } label="Delete" - onClick={this.handleDeleteAzimROIClick(id)} + onClick={handleDeleteAzimROIClick(id)} color="inherit" />, ]; }} ]; - - render_err(msg?: string | ReactNode) : ReactNode { - if (msg === undefined) - return


- return

 {msg}
; - } - - render() { - return -
- - - - Box ROI -


- - row.id} - rows={this.state.box} - columns={this.columns_box} - editMode={"row"} - processRowUpdate={this.processRowBoxUpdate} - /> - {this.render_err(this.state.box_err)} - -
- - - Radial ROI -


- - - row.id} - rows={this.state.circle} - columns={this.columns_circle} - editMode={"row"} - processRowUpdate={this.processRowCircleUpdate} - />{this.render_err(this.state.circle_err)} - -
- - - Azimuthal ROI -


- - - row.id} - rows={this.state.azim} - columns={this.columns_azim} - editMode={"row"} - processRowUpdate={this.processRowAzimUpdate} - />{this.render_err(this.state.azim_err)} - -
- - - -


-    -    -    - +
+ + + + Box ROI +


+ + row.id} + rows={box} + columns={columns_box} + editMode={"row"} + processRowUpdate={processRowBoxUpdate} /> -

- -
- + {render_err(boxErr)} +
- - } + + + Radial ROI +


+ + + row.id} + rows={circle} + columns={columns_circle} + editMode={"row"} + processRowUpdate={processRowCircleUpdate} + />{render_err(circleErr)} + +
+ + + Azimuthal ROI +


+ + + row.id} + rows={azim} + columns={columns_azim} + editMode={"row"} + processRowUpdate={processRowAzimUpdate} + />{render_err(azimErr)} + +
+ + + +


+    +    +    + +

+ +
+ + + } -export default ROI; +export default memo(ROI); diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/StatusBar.tsx index fd651bfc..79249eef 100644 --- a/frontend/src/components/StatusBar.tsx +++ b/frontend/src/components/StatusBar.tsx @@ -1,18 +1,15 @@ -import React, {Component} from 'react'; +import {memo, ReactNode} from 'react'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import {broker_status} from "../client"; import ButtonWithSnackbar from "./ButtonWithSnackbar"; -import { ReactNode } from "react"; type MyProps = { s?: broker_status } -type MyState = {} - function FormatNumber(x: number) : string { if (x === undefined) return ""; @@ -22,50 +19,47 @@ function FormatNumber(x: number) : string { return x.toFixed(1); } -class StatusBar extends Component { - - statusDescription() : ReactNode { - if (this.props.s === undefined) +function StatusBar({s}: MyProps) { + const statusDescription = (): ReactNode => { + if (s === undefined) return <>Not connected; else return
- State: {this.props.s.state.toString()} - {(this.props.s.progress !== undefined) ? " (" + FormatNumber(this.props.s.progress * 100.0) + " %)" : ""} + State: {s.state.toString()} + {(s.progress !== undefined) ? " (" + FormatNumber(s.progress * 100.0) + " %)" : ""}
- } + }; - render() { - return - - - PSI Jungfraujoch - + return + + + PSI Jungfraujoch + - - {this.statusDescription()} - + + {statusDescription()} + - - -    -    - - - - } + + +    +    + + + } -export default StatusBar; +export default memo(StatusBar); diff --git a/frontend/src/components/ZeroMQPreview.tsx b/frontend/src/components/ZeroMQPreview.tsx index 268011e7..c5cc1642 100644 --- a/frontend/src/components/ZeroMQPreview.tsx +++ b/frontend/src/components/ZeroMQPreview.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import {memo, useEffect, useState} from 'react'; import Paper from '@mui/material/Paper'; import {FormControlLabel, Radio, RadioGroup, Stack} from "@mui/material"; @@ -12,142 +12,108 @@ type MyProps = { s?: zeromq_preview_settings }; -type MyState = { - s: zeromq_preview_settings, - last_downloaded_s: zeromq_preview_settings, - period_err: boolean, - download_counter: number, - freq_choice: string, - period_ms: number -}; - const default_zeromq_preview_settings : zeromq_preview_settings = { socket_address: "", enabled: true, period_ms: 1000 } -class ZeroMQPreview extends React.Component { - state : MyState = { - s: default_zeromq_preview_settings, - last_downloaded_s: default_zeromq_preview_settings, - period_err: false, - download_counter: 0, - freq_choice: "custom", - period_ms: default_zeromq_preview_settings.period_ms - } +function ZeroMQPreview({s: serverS}: MyProps) { + const [s, setS] = useState(default_zeromq_preview_settings); + const [lastDownloadedS, setLastDownloadedS] = useState(default_zeromq_preview_settings); + const [periodErr, setPeriodErr] = useState(false); + const [downloadCounter, setDownloadCounter] = useState(0); + const [freqChoice, setFreqChoice] = useState("custom"); + const [periodMs, setPeriodMs] = useState(default_zeromq_preview_settings.period_ms); - update_freq = (freq_choice: string) => { - switch (freq_choice) { + const update_freq = (choice: string) => { + switch (choice) { case "custom": - this.setState(prevState => ({ - s: {...prevState.s, enabled: true, period_ms: this.state.period_ms}, - freq_choice: "custom" - })); + setS(prev => ({...prev, enabled: true, period_ms: periodMs})); + setFreqChoice("custom"); break; case "all": - this.setState(prevState => ({ - s: {...prevState.s, enabled: true, period_ms: 0}, - freq_choice: "all" - })); + setS(prev => ({...prev, enabled: true, period_ms: 0})); + setFreqChoice("all"); break; default: - this.setState(prevState => ({ - s: {...prevState.s, enabled: false}, - freq_choice: "none" - })); + setS(prev => ({...prev, enabled: false})); + setFreqChoice("none"); break; } - } + }; - getValues = () => { - if (this.props.s !== undefined) { - let format_set: zeromq_preview_settings = this.props.s; - if (!_.isEqual(format_set, this.state.last_downloaded_s)) { - let x : number = this.state.period_ms; - let choice : string = "none"; - if (this.props.s.enabled) { - if (this.props.s.period_ms === 0) - choice = "all"; - else { - choice = "custom"; - x = this.props.s.period_ms; - } + useEffect(() => { + if ((serverS !== undefined) && !_.isEqual(serverS, lastDownloadedS)) { + let x : number = periodMs; + let choice : string = "none"; + if (serverS.enabled) { + if (serverS.period_ms === 0) + choice = "all"; + else { + choice = "custom"; + x = serverS.period_ms; } - - this.setState(prevState => ({ - s: format_set, - last_downloaded_s: format_set, - download_counter: prevState.download_counter + 1, - period_err: false, - freq_choice: choice, - period_ms: x - })); } + + setS(serverS); + setLastDownloadedS(serverS); + setDownloadCounter(c => c + 1); + setPeriodErr(false); + setFreqChoice(choice); + setPeriodMs(x); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serverS]); - componentDidMount() { - this.getValues(); - } + const empty = () : boolean => + (s.socket_address === undefined) || (s.socket_address === ""); - componentDidUpdate() { - this.getValues(); - } - - empty() : boolean { - return (this.state.s.socket_address === undefined) || (this.state.s.socket_address === ""); - } - - render() { - return -
- -
ZeroMQ preview settings
-
- {this.empty() ? "No preview available" : `ZeroMQ socket: ${this.state.s.socket_address}`} -
- - {this.update_freq(event.target.value)}} - > - } label="None" /> - } label="All images" /> - } label="Frequency" /> - - - { - this.setState(prevState => ({ - s: {...prevState.s, period_ms: val}, - period_err: err, - period_ms: val - })); - }} - disabled={this.state.freq_choice != "custom"} - fullWidth/> - -
-
-
- } + return +
+ +
ZeroMQ preview settings
+
+ {empty() ? "No preview available" : `ZeroMQ socket: ${s.socket_address}`} +
+ + {update_freq(event.target.value)}} + > + } label="None" /> + } label="All images" /> + } label="Frequency" /> + + + { + setS(prev => ({...prev, period_ms: val})); + setPeriodErr(err); + setPeriodMs(val); + }} + disabled={freqChoice != "custom"} + fullWidth/> + +
+
+
} -export default ZeroMQPreview; +export default memo(ZeroMQPreview); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index e9a5c1ea..ced52314 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,17 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App"; +import { client } from "./client/client.gen"; + +// Talk to the same origin that serves the frontend (was OpenAPI.BASE=''). +client.setConfig({ baseUrl: '' }); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false, refetchOnWindowFocus: false }, + }, +}); const rootElement = document.getElementById("root"); @@ -9,6 +20,8 @@ const root = createRoot(rootElement!); root.render( - + + + );