From aa2361fe7afba892269ea8644d386494db230bc6 Mon Sep 17 00:00:00 2001 From: Jakub Kowalczyk Date: Wed, 9 Aug 2023 15:06:09 +0200 Subject: [PATCH] UI: adds configurable 'video feed' widgets Adds support for 'video feed' widgets. Each widget provide access to a video feed from 'Data collection' tab. This is typically used for giving users an easy way to access hutch view cameras. Adds new 'camera_setup' config to mxcube-web/ui.yaml. This element allows to configure multiple video feeds to be made accessable from 'Data collection' tab. Each configured video feed creates a button with a camera icon. Clicking the button will show/hide the video feed. It also allows to create a pop-out windows with the video feed. --- mxcube3/core/models/configmodels.py | 4 + .../mxcube-web/ui.yaml | 16 ++- .../BeamlineCamera/BeamlineCamera.jsx | 128 ++++++++++++++++++ .../BeamlineCamera/picture_in_picture.svg | 1 + ui/src/components/BeamlineCamera/style.css | 8 ++ ui/src/containers/BeamlineSetupContainer.jsx | 28 ++++ 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/BeamlineCamera/BeamlineCamera.jsx create mode 100644 ui/src/components/BeamlineCamera/picture_in_picture.svg create mode 100644 ui/src/components/BeamlineCamera/style.css diff --git a/mxcube3/core/models/configmodels.py b/mxcube3/core/models/configmodels.py index 62ffdb185..8dc718d8b 100644 --- a/mxcube3/core/models/configmodels.py +++ b/mxcube3/core/models/configmodels.py @@ -36,6 +36,10 @@ class UIComponentModel(BaseModel): precision: Optional[int] suffix: Optional[str] format: Optional[str] + url: Optional[str] + description: Optional[str] + width: Optional[int] + height: Optional[int] # Set internaly not to be set through configuration value_type: Optional[str] diff --git a/test/HardwareObjectsMockup.xml/mxcube-web/ui.yaml b/test/HardwareObjectsMockup.xml/mxcube-web/ui.yaml index fc32dd6f3..9d805003b 100644 --- a/test/HardwareObjectsMockup.xml/mxcube-web/ui.yaml +++ b/test/HardwareObjectsMockup.xml/mxcube-web/ui.yaml @@ -144,4 +144,18 @@ beamline_setup: attribute: flux precision: 2 suffix: ph/s - format: expo \ No newline at end of file + format: expo + +camera_setup: + id: camera_setup + components: + - label: Camera 1 + attribute: jpg + url: http://localhost:8080/mjpg/video.mjpg?streamprofile=mxcube + width: 800 + height: 450 + - label: Camera 2 + attribute: jpg + url: http://localhost:9090/mjpg/video.mjpg?streamprofile=mxcube + width: 800 + height: 450 diff --git a/ui/src/components/BeamlineCamera/BeamlineCamera.jsx b/ui/src/components/BeamlineCamera/BeamlineCamera.jsx new file mode 100644 index 000000000..a391c369c --- /dev/null +++ b/ui/src/components/BeamlineCamera/BeamlineCamera.jsx @@ -0,0 +1,128 @@ +import React, { useState } from 'react'; +import { Badge, Button, OverlayTrigger, Popover } from 'react-bootstrap'; +import './style.css'; +import pip from './picture_in_picture.svg'; + +const BeamlineCamera = ({ + labelText, + url, + width, + height, + optionsOverlay, + format, + description, +}) => { + const [showLabelOverlay, setShowLabelOverlay] = useState(false); + const [showValueOverlay, setShowValueOverlay] = useState(false); + + const onImageClick = (ev) => { + setShowValueOverlay(false); + window.open( + url, + 'webcam', + `toolbar=0,location=0,menubar=0,addressbar=0,height=${height},width=${width}`, + 'popup', + ); + }; + + const renderLabel = () => { + if (!optionsOverlay) { + return ( + + {labelText} + + ); + } + + return ( + +
setShowLabelOverlay(!showLabelOverlay)}> + + {labelText} + + +
+
+ ); + }; + + const msgLabelStyle = { + display: 'block', + fontSize: '100%', + borderRadius: '0px', + color: '#000', + }; + + const video = ( +
+ {format != 'mp4' ? ( + {labelText} + ) : ( + + )} + +
+ ); + + return ( +
+ {renderLabel()} + + {video} + + } + > +
setShowValueOverlay(!showValueOverlay)}> + + + +
+
+
+ ); +}; + +BeamlineCamera.defaultProps = { + labelText: '', + width: 0, + height: 0, + url: '', + optionsOverlay: false, + format: '', +}; + +export default BeamlineCamera; diff --git a/ui/src/components/BeamlineCamera/picture_in_picture.svg b/ui/src/components/BeamlineCamera/picture_in_picture.svg new file mode 100644 index 000000000..c56135cbb --- /dev/null +++ b/ui/src/components/BeamlineCamera/picture_in_picture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/components/BeamlineCamera/style.css b/ui/src/components/BeamlineCamera/style.css new file mode 100644 index 000000000..aeb9f9f6a --- /dev/null +++ b/ui/src/components/BeamlineCamera/style.css @@ -0,0 +1,8 @@ +.inout-switch { + width: 7em; +} + +.inout-switch .badge { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/ui/src/containers/BeamlineSetupContainer.jsx b/ui/src/containers/BeamlineSetupContainer.jsx index ac5ebf1a0..1120a0aad 100644 --- a/ui/src/containers/BeamlineSetupContainer.jsx +++ b/ui/src/containers/BeamlineSetupContainer.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { Navbar, Nav, Table, Popover } from 'react-bootstrap'; import PopInput from '../components/PopInput/PopInput'; import BeamlineActions from './BeamlineActionsContainer'; +import BeamlineCamera from '../components/BeamlineCamera/BeamlineCamera'; import InOutSwitch from '../components/InOutSwitch/InOutSwitch'; import SampleChangerSwitch from '../components/SampleChangerSwitch/SampleChangerSwitch'; import DeviceState from '../components/DeviceState/DeviceState'; @@ -134,6 +135,32 @@ class BeamlineSetupContainer extends React.Component { return acts; } + createCameraComponent() { + const acts = []; + + const { uiproperties } = this.props; + + if (uiproperties.hasOwnProperty('camera_setup')) { + for (const [ + key, + camera, + ] of uiproperties.camera_setup.components.entries()) { + acts.push( + + + , + ); + } + } + return acts; + } + dmState() { return this.props.beamline.hardwareObjects.diffractometer.state; } @@ -247,6 +274,7 @@ class BeamlineSetupContainer extends React.Component { +