diff --git a/package.json b/package.json index 7b3aa46..bccf2c5 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "ajv": "^6.12.6", "esbuild@<=0.24.2": ">=0.25.0", "minimatch@>=9.0.0 <9.0.6": ">=9.0.6", - "minimatch@>=9.0.0 <9.0.7": ">=9.0.7" + "minimatch@>=9.0.0 <9.0.7": ">=9.0.7", + "serialize-javascript@<=7.0.2": ">=7.0.3" } }, "imports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de9b661..d16d05f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: esbuild@<=0.24.2: ">=0.25.0" minimatch@>=9.0.0 <9.0.6: ">=9.0.6" minimatch@>=9.0.0 <9.0.7: ">=9.0.7" + serialize-javascript@<=7.0.2: ">=7.0.3" importers: .: @@ -750,31 +751,31 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@floating-ui/core@1.7.5": + "@floating-ui/core@1.7.4": resolution: { - integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==, + integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==, } - "@floating-ui/dom@1.7.6": + "@floating-ui/dom@1.7.5": resolution: { - integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==, + integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==, } - "@floating-ui/react-dom@2.1.8": + "@floating-ui/react-dom@2.1.7": resolution: { - integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==, + integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==, } peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - "@floating-ui/utils@0.2.11": + "@floating-ui/utils@0.2.10": resolution: { - integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==, + integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==, } "@graphql-typed-document-node/core@3.2.0": @@ -2591,10 +2592,10 @@ packages: } engines: { node: ">=6" } - caniuse-lite@1.0.30001776: + caniuse-lite@1.0.30001775: resolution: { - integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==, + integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==, } canvas-fit@1.5.0: @@ -3194,10 +3195,10 @@ packages: integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==, } - electron-to-chromium@1.5.307: + electron-to-chromium@1.5.302: resolution: { - integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==, + integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==, } element-size@1.1.1: @@ -4851,20 +4852,7 @@ packages: { integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, } - - minimatch@9.0.3: - resolution: - { - integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==, - } - engines: { node: ">=16 || 14 >=14.17" } - - minimatch@9.0.9: - resolution: - { - integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==, - } - engines: { node: ">=16 || 14 >=14.17" } + engines: { node: 18 || 20 || >=22 } minimatch@3.1.5: resolution: @@ -4994,10 +4982,10 @@ packages: } hasBin: true - node-releases@2.0.36: + node-releases@2.0.27: resolution: { - integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==, + integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, } normalize-svg-path@0.1.0: @@ -5349,10 +5337,10 @@ packages: integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==, } - postcss@8.5.8: + postcss@8.5.6: resolution: { - integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==, + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, } engines: { node: ^10 || ^12 || >=14 } @@ -5483,10 +5471,10 @@ packages: peerDependencies: react: ">=16.13.1" - react-icons@5.6.0: + react-icons@5.5.0: resolution: { - integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==, + integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==, } peerDependencies: react: "*" @@ -5853,6 +5841,13 @@ packages: engines: { node: ">=10" } hasBin: true + serialize-javascript@7.0.4: + resolution: + { + integrity: sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==, + } + engines: { node: ">=20.0.0" } + set-function-length@1.2.2: resolution: { @@ -6235,10 +6230,10 @@ packages: } engines: { node: ">=6" } - terser-webpack-plugin@5.3.17: + terser-webpack-plugin@5.3.16: resolution: { - integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==, + integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==, } engines: { node: ">= 10.13.0" } peerDependencies: @@ -7014,8 +7009,8 @@ snapshots: dependencies: "@babel/runtime": 7.28.6 "@base-ui/utils": 0.2.5(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - "@floating-ui/react-dom": 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - "@floating-ui/utils": 0.2.11 + "@floating-ui/react-dom": 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@floating-ui/utils": 0.2.10 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tabbable: 6.4.0 @@ -7026,7 +7021,7 @@ snapshots: "@base-ui/utils@0.2.5(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@babel/runtime": 7.28.6 - "@floating-ui/utils": 0.2.11 + "@floating-ui/utils": 0.2.10 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) reselect: 5.1.1 @@ -7100,7 +7095,7 @@ snapshots: "@mui/material": 6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) "@types/utif": 3.0.6 react: 18.3.1 - react-icons: 5.6.0(react@18.3.1) + react-icons: 5.5.0(react@18.3.1) utif: 3.1.0 "@emotion/babel-plugin@11.13.5": @@ -7289,22 +7284,22 @@ snapshots: "@eslint/js@9.39.3": {} - "@floating-ui/core@1.7.5": + "@floating-ui/core@1.7.4": dependencies: - "@floating-ui/utils": 0.2.11 + "@floating-ui/utils": 0.2.10 - "@floating-ui/dom@1.7.6": + "@floating-ui/dom@1.7.5": dependencies: - "@floating-ui/core": 1.7.5 - "@floating-ui/utils": 0.2.11 + "@floating-ui/core": 1.7.4 + "@floating-ui/utils": 0.2.10 - "@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + "@floating-ui/react-dom@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: - "@floating-ui/dom": 1.7.6 + "@floating-ui/dom": 1.7.5 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - "@floating-ui/utils@0.2.11": {} + "@floating-ui/utils@0.2.10": {} "@graphql-typed-document-node/core@3.2.0(graphql@15.10.1)": dependencies: @@ -8434,9 +8429,9 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001776 - electron-to-chromium: 1.5.307 - node-releases: 2.0.36 + caniuse-lite: 1.0.30001775 + electron-to-chromium: 1.5.302 + node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-from@1.1.2: {} @@ -8471,7 +8466,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001776: {} + caniuse-lite@1.0.30001775: {} canvas-fit@1.5.0: dependencies: @@ -8606,12 +8601,12 @@ snapshots: css-loader@7.1.4(webpack@5.104.1): dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) - postcss-modules-scope: 3.2.1(postcss@8.5.8) - postcss-modules-values: 4.0.0(postcss@8.5.8) + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: @@ -8801,7 +8796,7 @@ snapshots: earcut@3.0.2: {} - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.302: {} element-size@1.1.1: {} @@ -9596,9 +9591,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.8): + icss-utils@5.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.8 + postcss: 8.5.6 ieee754@1.2.1: {} @@ -10056,7 +10051,7 @@ snapshots: node-gyp-build@4.8.4: {} - node-releases@2.0.36: {} + node-releases@2.0.27: {} normalize-svg-path@0.1.0: {} @@ -10276,26 +10271,26 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.8): + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): dependencies: - postcss: 8.5.8 + postcss: 8.5.6 - postcss-modules-local-by-default@4.2.0(postcss@8.5.8): + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.8): + postcss-modules-scope@3.2.1(postcss@8.5.6): dependencies: - postcss: 8.5.8 + postcss: 8.5.6 postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.5.8): + postcss-modules-values@4.0.0(postcss@8.5.6): dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser@7.1.1: dependencies: @@ -10304,7 +10299,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.8: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -10389,7 +10384,7 @@ snapshots: "@babel/runtime": 7.28.6 react: 18.3.1 - react-icons@5.6.0(react@18.3.1): + react-icons@5.5.0(react@18.3.1): dependencies: react: 18.3.1 @@ -10681,6 +10676,8 @@ snapshots: semver@7.7.4: {} + serialize-javascript@7.0.4: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -10924,11 +10921,12 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.17(webpack@5.104.1): + terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: "@jridgewell/trace-mapping": 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 + serialize-javascript: 7.0.4 terser: 5.46.0 webpack: 5.104.1 @@ -11155,7 +11153,7 @@ snapshots: vite@5.4.21(@types/node@22.19.13)(terser@5.46.0): dependencies: esbuild: 0.27.3 - postcss: 8.5.8 + postcss: 8.5.6 rollup: 4.59.0 optionalDependencies: "@types/node": 22.19.13 @@ -11264,7 +11262,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(webpack@5.104.1) + terser-webpack-plugin: 5.3.16(webpack@5.104.1) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: diff --git a/src/blueapi/BlueapiComponents.tsx b/src/blueapi/BlueapiComponents.tsx index c11fae6..e3ef084 100644 --- a/src/blueapi/BlueapiComponents.tsx +++ b/src/blueapi/BlueapiComponents.tsx @@ -30,6 +30,7 @@ type RunPlanButtonProps = { sx?: object; tooltipSx?: object; typographySx?: object; + onSuccess?: () => void | Promise; // Optional callback after plan succeeds }; export function RunPlanButton(props: RunPlanButtonProps) { @@ -63,13 +64,17 @@ export function RunPlanButton(props: RunPlanButtonProps) { planName: props.planName, planParams: params, instrumentSession: instrumentSession, - }).catch((error) => { - setSeverity("error"); - setMsg( - `Failed to run plan ${props.planName}, see console and logs for full error`, - ); - console.log(`${msg}. Reason: ${error}`); - }); + }) + .then(() => { + props.onSuccess?.(); + }) + .catch((error) => { + setSeverity("error"); + setMsg( + `Failed to run plan ${props.planName}, see console and logs for full error`, + ); + console.log(`${msg}. Reason: ${error}`); + }); } catch (error) { setSeverity("error"); setMsg( diff --git a/src/components/OavVideoStream.tsx b/src/components/OavVideoStream.tsx index 82defa4..8e5f439 100644 --- a/src/components/OavVideoStream.tsx +++ b/src/components/OavVideoStream.tsx @@ -116,8 +116,6 @@ function VideoBoxWithOverlay(props: { drawCanvas(canvasRef, props.crosshairX, props.crosshairY); }, [props.crosshairX, props.crosshairY, width, height]); - console.info(); - return ( void; +}; + +export const BeamCenterContext = createContext({ + data: null, + refetch: () => {}, +}); diff --git a/src/context/beamcenter/BeamCenterProvider.test.tsx b/src/context/beamcenter/BeamCenterProvider.test.tsx new file mode 100644 index 0000000..4cca674 --- /dev/null +++ b/src/context/beamcenter/BeamCenterProvider.test.tsx @@ -0,0 +1,80 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; +import { useContext } from "react"; +import "@testing-library/jest-dom/vitest"; +import { BeamCenterProvider } from "./BeamCenterProvider"; +import { BeamCenterContext } from "./BeamCenterContext"; +import { useConfigCall } from "#/config_server/configServer.ts"; +import type { UseQueryResult } from "react-query"; + +vi.mock("#/config_server/configServer.ts", () => ({ + useConfigCall: vi.fn(), +})); + +const TestConsumer = () => { + const value = useContext(BeamCenterContext); + return ( + <> +
{value.data}
+ + + ); +}; + +describe("BeamCenterProvider", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const mockRefetch = vi.fn(); + const mockQueryResult = { + data: "mock config text", + refetch: mockRefetch, + }; + + beforeEach(() => + vi + .mocked(useConfigCall) + .mockReturnValue( + mockQueryResult as unknown as UseQueryResult, + ), + ); + + it("calls useConfigCall with the correct endpoint", () => { + render( + + + , + ); + + expect(useConfigCall).toHaveBeenCalledWith( + "/dls_sw/i24/software/daq_configuration/domain/display.configuration", + ); + }); + + it("provides the data to consumers via context", () => { + render( + + + , + ); + + expect(screen.getByTestId("context-value")).toHaveTextContent( + "mock config text", + ); + }); + + it("passes refetch function through context and it can be called", () => { + render( + + + , + ); + + fireEvent.click(screen.getByTestId("refetch-button")); + expect(mockRefetch).toHaveBeenCalled(); + }); +}); diff --git a/src/context/beamcenter/BeamCenterProvider.tsx b/src/context/beamcenter/BeamCenterProvider.tsx new file mode 100644 index 0000000..a9db5f2 --- /dev/null +++ b/src/context/beamcenter/BeamCenterProvider.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from "react"; +import { useConfigCall } from "#/config_server/configServer.ts"; +import { BeamCenterContext } from "./BeamCenterContext"; + +const DISPLAY_CONFIG_ENDPOINT = + "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; + +export const BeamCenterProvider = ({ children }: { children: ReactNode }) => { + const beamCenterQuery = useConfigCall(DISPLAY_CONFIG_ENDPOINT); + + return ( + + {children} + + ); +}; diff --git a/src/routes/BeamlineI24.tsx b/src/routes/BeamlineI24.tsx index 40bea1b..c684cbb 100644 --- a/src/routes/BeamlineI24.tsx +++ b/src/routes/BeamlineI24.tsx @@ -2,9 +2,10 @@ import { BeamlineStatsTabPanel } from "#/screens/BeamlineStats.tsx"; import { DetectorMotionTabPanel } from "#/screens/DetectorMotion.tsx"; import { FallbackScreen } from "#/screens/FallbackScreen.tsx"; import { OavMover } from "#/screens/OavMover/OAVStageController.tsx"; +import { BeamCenterProvider } from "#/context/beamcenter/BeamCenterProvider.tsx"; import { Box, Tab, Tabs, useTheme } from "@mui/material"; -import React from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { useState } from "react"; interface TabPanelProps { children?: React.ReactNode; @@ -37,7 +38,7 @@ function CustomTabPanel(props: TabPanelProps) { export function BeamlineI24() { const theme = useTheme(); - const [tab, setTab] = React.useState(0); + const [tab, setTab] = useState(0); const handleChange = (_event: React.SyntheticEvent, newTab: number) => { setTab(newTab); @@ -73,7 +74,9 @@ export function BeamlineI24() { - + + +
diff --git a/src/screens/OavMover/OAVMoveController.tsx b/src/screens/OavMover/OAVMoveController.tsx index 9c139a8..1fd4897 100644 --- a/src/screens/OavMover/OAVMoveController.tsx +++ b/src/screens/OavMover/OAVMoveController.tsx @@ -1,4 +1,6 @@ import { RunPlanButton } from "#/blueapi/BlueapiComponents.tsx"; +import { useContext } from "react"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; import { KeyboardDoubleArrowUp, KeyboardArrowUp, @@ -37,7 +39,7 @@ const arrowsScreenSizing = { }, }; -function BlockMove(props: TabPanelProps) { +function BlockMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -48,6 +50,7 @@ function BlockMove(props: TabPanelProps) { planName={"move_block_on_arrow_click"} planParams={{ direction: "up" }} btnVariant="outlined" + onSuccess={props.onMoveSuccess} /> ); } -function NudgeMove(props: TabPanelProps) { +function NudgeMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -87,6 +93,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "up", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -110,6 +119,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "left", size_of_move: "small" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -125,6 +136,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "right", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> ); } -function WindowMove(props: TabPanelProps) { +function WindowMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -158,6 +172,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "up", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -181,6 +198,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "left", size_of_move: "small" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -196,6 +215,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "right", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> ); } -function FocusMove(props: TabPanelProps) { +function FocusMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; const focus_move = [ { direction: "in", size_of_move: "big", label: "IN x3" }, @@ -244,6 +266,7 @@ function FocusMove(props: TabPanelProps) { size_of_move: move.size_of_move, }} btnVariant="outlined" + onSuccess={props.onMoveSuccess} /> ))} @@ -252,7 +275,7 @@ function FocusMove(props: TabPanelProps) { export function MoveArrows() { const theme = useTheme(); - + const beamCenterQuery = useContext(BeamCenterContext); const [value, setValue] = useState(0); const isSmall = useMediaQuery(theme.breakpoints.down("xl")); @@ -292,10 +315,28 @@ export function MoveArrows() { - - - - + { + beamCenterQuery?.refetch(); + }} + /> + beamCenterQuery?.refetch()} + /> + beamCenterQuery?.refetch()} + /> + beamCenterQuery?.refetch()} + /> ); } diff --git a/src/screens/OavMover/OAVStageController.test.tsx b/src/screens/OavMover/OAVStageController.test.tsx new file mode 100644 index 0000000..e4acd21 --- /dev/null +++ b/src/screens/OavMover/OAVStageController.test.tsx @@ -0,0 +1,119 @@ +import { renderHook } from "@testing-library/react"; +import { describe, it, vi, beforeEach, expect } from "vitest"; +import { useZoomAndCrosshair } from "./OAVStageController"; +import { useParsedPvConnection } from "#/pv/util.ts"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; +import type { RawValue } from "#/pv/types.ts"; + +vi.mock("#/pv/util.ts", () => ({ + ...vi.importActual("#/pv/util.ts"), + useParsedPvConnection: vi.fn(), + forceString: (x: RawValue | string | number) => String(x), +})); + +type validateZoomTestType = { + zoomLevel: string; + expectedX: number; + expectedY: number; +}; + +describe("useZoomAndCrosshair", () => { + const mockRefetch = vi.fn(); + const mockBeamCenterData = [ + "zoomLevel = 1.0", + "crosshairX = 561", + "crosshairY = 321", + "topLeftX = 611", + "topLeftY = 441", + "bottomRightX = 631", + "bottomRightY = 461", + "zoomLevel = 2.0", + "crosshairX = 562", + "crosshairY = 322", + "topLeftX = 612", + "topLeftY = 442", + "bottomRightX = 632", + "bottomRightY = 462", + "zoomLevel = 3.0", + "crosshairX = 563", + "crosshairY = 323", + "topLeftX = 613", + "topLeftY = 443", + "bottomRightX = 633", + "bottomRightY = 463", + ].join("\n"); + + beforeEach(() => { + vi.mocked(useParsedPvConnection).mockReturnValue("2.0"); + }); + + it.each` + zoomLevel | expectedX | expectedY + ${"1.0"} | ${561} | ${321} + ${"2.0"} | ${562} | ${322} + ${"3.0"} | ${563} | ${323} + `( + "returns ( $expectedX , $expectedY ) for zoom level '$zoomLevel'", + ({ zoomLevel, expectedX, expectedY }: validateZoomTestType) => { + vi.mocked(useParsedPvConnection).mockReturnValue(zoomLevel); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBe(expectedX); + expect(result.current.crosshairY).toBe(expectedY); + }, + ); + + it("returns NaN for crosshair if zoomIndex is not found", () => { + vi.mocked(useParsedPvConnection).mockReturnValue("99.0"); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBeNaN(); + expect(result.current.crosshairY).toBeNaN(); + }); + + it("returns NaN if beamCenter data is missing", () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBeNaN(); + expect(result.current.crosshairY).toBeNaN(); + }); + + it("calls refetch when zoom level changes", () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + renderHook(() => useZoomAndCrosshair(), { wrapper }); + vi.mocked(useParsedPvConnection).mockReturnValue("3.0"); + expect(mockRefetch).toHaveBeenCalled(); + }); +}); diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 64020af..08675e2 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -1,25 +1,33 @@ import { Grid2, useTheme } from "@mui/material"; +import { useContext, useRef } from "react"; import { OAVSideBar } from "./OAVSideBar"; import { submitAndRunPlanImmediately } from "#/blueapi/blueapi.ts"; import { readVisitFromPv, parseInstrumentSession } from "#/blueapi/visit.ts"; import { OavVideoStream } from "#/components/OavVideoStream.tsx"; -import { useConfigCall } from "#/config_server/configServer.ts"; import { forceString, useParsedPvConnection } from "#/pv/util.ts"; import { ZoomLevels } from "#/pv/enumPvValues.ts"; -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; -const DISPLAY_CONFIG_ENDPOINT = - "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; +const ZOOM_PV = "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT"; +const BEAM_CENTER_LINES_PER_ZOOM = 7; -export function OavMover() { - const beamCenterQuery = useConfigCall(DISPLAY_CONFIG_ENDPOINT); +export function useZoomAndCrosshair() { + const beamCenterQuery = useContext(BeamCenterContext); const currentZoomValue = String( useParsedPvConnection({ - pv: "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT", + pv: ZOOM_PV, label: "zoom-level", transformValue: forceString, }), ); + + const beamCenterQueryRef = useRef(beamCenterQuery); + + useEffect(() => { + beamCenterQueryRef.current.refetch(); + }, [currentZoomValue]); + const zoomIndex = ZoomLevels.findIndex( (element: string) => element == currentZoomValue, ); @@ -30,8 +38,8 @@ export function OavMover() { } const lines = beamCenterQuery.data.split("\n"); - const xLine = lines[zoomIndex * 7 + 1]; - const yLine = lines[zoomIndex * 7 + 2]; + const xLine = lines[zoomIndex * BEAM_CENTER_LINES_PER_ZOOM + 1]; + const yLine = lines[zoomIndex * BEAM_CENTER_LINES_PER_ZOOM + 2]; if (!xLine || !yLine) { return [NaN, NaN]; @@ -40,12 +48,31 @@ export function OavMover() { return [Number(xLine.split(" ")[2]), Number(yLine.split(" ")[2])]; }, [beamCenterQuery.data, zoomIndex]); - // #Issue 86: Remove these constants - https://github.com/DiamondLightSource/mx-daq-ui/issues/86 - const pixelsPerMicron = 1.25; + return { crosshairX, crosshairY }; +} + +export function OavMover() { + const { crosshairX, crosshairY } = useZoomAndCrosshair(); + const theme = useTheme(); const bgColor = theme.palette.background.paper; const fullVisit = readVisitFromPv(); + const beamCenterQuery = useContext(BeamCenterContext); + + function onCoordClick(x: number, y: number) { + submitAndRunPlanImmediately({ + planName: "move_on_oav_view_click", + planParams: { position: [x, y] }, + instrumentSession: parseInstrumentSession(fullVisit), + }).catch((error) => { + console.log( + `Failed to run plan, see console and logs for full error. Reason: ${error}`, + ); + }); + + beamCenterQuery.refetch(); + } return ( @@ -55,27 +82,7 @@ export function OavMover() { label="I24 OAV image stream" crosshairX={crosshairX} crosshairY={crosshairY} - onCoordClick={(x: number, y: number) => { - const [x_um, y_um] = [x / pixelsPerMicron, y / pixelsPerMicron]; - console.log( - `Clicked on position (${x}, ${y}) (px relative to beam centre) in original stream. Relative position in um (${x_um}, ${y_um}). Submitting to BlueAPI...`, - ); - const [x_int, y_int] = [Math.round(x), Math.round(y)]; - if (Number.isNaN(x_um) || Number.isNaN(y_um)) { - console.log("Not submitting plan while disconnected from PVs!"); - } else { - // This is an example but not useful for actual production use. - submitAndRunPlanImmediately({ - planName: "gui_gonio_move_on_click", - planParams: { position_px: [x_int, y_int] }, - instrumentSession: parseInstrumentSession(fullVisit), - }).catch((error) => { - console.log( - `Failed to run plan gui_gonio_move_on_click, see console and logs for full error. Reason: ${error}`, - ); - }); - } - }} + onCoordClick={onCoordClick} />