diff --git a/mxcubeweb/app.py b/mxcubeweb/app.py index 314e37f26..61058b4be 100644 --- a/mxcubeweb/app.py +++ b/mxcubeweb/app.py @@ -430,7 +430,7 @@ def init_logging(log_file, log_level, enabled_logger_list): else: logger.disabled = True - for name in ("httpx", "httpcore"): + for name in ("httpx", "httpcore", "geventwebsocket.handler"): logger = logging.getLogger(name) logger.setLevel(logging.ERROR) logger.propagate = False diff --git a/mxcubeweb/core/components/lims.py b/mxcubeweb/core/components/lims.py index 05f133afe..2f40ea74c 100644 --- a/mxcubeweb/core/components/lims.py +++ b/mxcubeweb/core/components/lims.py @@ -5,6 +5,7 @@ import re import sys from http import HTTPStatus +from time import sleep from urllib.parse import urljoin import httpx @@ -276,13 +277,89 @@ def synch_with_lims(self, lims_name): # session_id is not used, so we can pass None as second argument to # 'db_connection.get_samples' - samples_info_list = HWR.beamline.lims.get_samples(lims_name) + # Try 3 times in case the robot client connection fails + for _ in range(3): + samples_info_list = HWR.beamline.sample_changer.refresh_puck_info() + if not samples_info_list: + logging.getLogger("MX3.HWR").info( + "[LIMS] No sample info retrieved from LIMS, retrying..." + ) + sleep(0.5) + continue + + break + + _sample_list = {} + _sample_order = [] + for sample_info in samples_info_list: + sample_location = sample_info.get("sampleLocation") + puck_location = sample_info.get("containerSampleChangerLocation") + if sample_location is None or puck_location is None: + continue + + try: + port_int = int(sample_location) + except (TypeError, ValueError): + continue + + # The UI filters samples by (cell_no, puck_no) and expects these to + # match the SC contents tree (cell=1 for single-cell changers). + cell_no = 1 + puck_no = 1 + container_loc_key = str(puck_location) + try: + basket = int(puck_location) + except (TypeError, ValueError): + basket = None + else: + if HWR.beamline.sample_changer.__class__.__TYPE__ in [ + "Flex Sample Changer", + "FlexHCD", + "RoboDiff", + ]: + cell_no = int(math.ceil((basket) / 3.0)) + puck_no = basket - 3 * (cell_no - 1) + container_loc_key = f"{cell_no}:{puck_no}" + else: + puck_no = basket + + sample_list_key = f"{container_loc_key}:{port_int:02d}" + sample_name = sample_info.get("sampleName") or "" + _sample_list[sample_list_key] = { + "sampleID": sample_list_key, + "location": sample_list_key, + "sampleName": sample_name, + "crystalUUID": sample_list_key, + "proteinAcronym": "", + "code": sample_info["code"], + "limsID": sample_info.get("sampleId"), + "loadable": True, + "state": 0, + "tasks": [], + "type": "Sample", + "cell_no": cell_no, + "puck_no": puck_no, + "defaultPrefix": sample_name, + "defaultSubDir": sample_name + "/", + } + + _sample_order.append(sample_list_key) + + self.app.SAMPLE_LIST = { + "sampleList": _sample_list, + "sampleOrder": _sample_order, + } + for sample_info in samples_info_list: - sample_info["limsID"] = sample_info.pop("sampleId") + sample_info["limsID"] = sample_info.pop("sampleId", None) sample_info["defaultPrefix"] = self.get_default_prefix(sample_info) sample_info["defaultSubDir"] = self.get_default_subdir(sample_info) - if not VALID_SAMPLE_NAME_REGEXP.match(sample_info["sampleName"]): + sample_name = sample_info.get("sampleName") + if not isinstance(sample_name, str) or not sample_name: + continue + + if not VALID_SAMPLE_NAME_REGEXP.match(sample_name): raise AttributeError( "sample name for sample %s contains an incorrect character" % sample_info diff --git a/poetry.lock b/poetry.lock index 568e44236..7c0b7530a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4070,7 +4070,7 @@ resolved_reference = "4ef9fe8c7caf5e5d776e96c6689ce308bb1369d1" [[package]] name = "mxcubecore" -version = "1.195.0.56" +version = "1.195.0.57" description = "Core libraries for the MXCuBE application" optional = false python-versions = ">=3.8,<3.12" @@ -4114,8 +4114,8 @@ tango = ["PyTango (>=9.3.6,<10.0.0)"] [package.source] type = "git" url = "https://github.com/AustralianSynchrotron/mxcubecore.git" -reference = "v1.195.0.56" -resolved_reference = "b2bf3cd70fb30b849082b9df6b6ea870d4378dac" +reference = "v1.195.0.57" +resolved_reference = "c78445aa6d2c06cc025328a191ca094325f000d2" [[package]] name = "myst-parser" @@ -8084,4 +8084,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.12" -content-hash = "d0fa4da3081561f5aa1529abddde3c062254d8012c7d14414835f7d0a03f0ec6" +content-hash = "df05ab2abf35f0d90600656c47f20725e3b48737259068ba437e0e9bd7c67165" diff --git a/pyproject.toml b/pyproject.toml index 964d3552e..b2ffb46b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mxcubeweb" -version = "4.238.0.53" +version = "4.238.0.54" license = "LGPL-3.0-or-later" description = "MXCuBE Web user interface" authors = ["The MXCuBE collaboration "] @@ -41,7 +41,7 @@ pydantic = ">=2.8.2,<2.11.0" PyDispatcher = "^2.0.6" pytz = "^2022.6" tzlocal = "^4.2" -mxcubecore = {git = "https://github.com/AustralianSynchrotron/mxcubecore.git", rev = "v1.195.0.56", extras=["prefect"]} +mxcubecore = {git = "https://github.com/AustralianSynchrotron/mxcubecore.git", rev = "v1.195.0.57", extras=["prefect"]} bcrypt = "^4.0.1" authlib = "^1.3.0" diff --git a/ui/src/actions/sampleGrid.js b/ui/src/actions/sampleGrid.js index 31648e2cf..f805f5db9 100644 --- a/ui/src/actions/sampleGrid.js +++ b/ui/src/actions/sampleGrid.js @@ -1,7 +1,7 @@ import { showErrorPanel } from './general'; -import { setQueue } from './queue'; // eslint-disable-line import/no-cycle import { fetchSamplesList, sendSyncWithCrims } from '../api/sampleChanger'; import { fetchLimsSamples } from '../api/lims'; +import { refresh as refreshSampleChanger } from './sampleChanger'; import { hideWaitDialog, showWaitDialog } from './waitDialog'; export function updateSampleList(sampleList, order) { @@ -71,7 +71,9 @@ export function getSamplesList() { const json = await fetchSamplesList(); const { sampleList, sampleOrder } = json; dispatch(updateSampleList(sampleList, sampleOrder)); - dispatch(setQueue(json)); + + // Refresh sample changer after updating sample list + await dispatch(refreshSampleChanger()); } catch { dispatch(showErrorPanel(true, 'Could not get samples list')); } @@ -87,7 +89,9 @@ export function syncSamples(lims) { try { const json = await fetchLimsSamples(lims); dispatch(updateSampleList(json.sampleList, json.sampleOrder)); - dispatch(setQueue(json)); + + // Refresh sample changer after updating sample list + await dispatch(refreshSampleChanger()); } catch (error) { dispatch( showErrorPanel( diff --git a/ui/src/components/BeamlineCamera/BeamlineCamera.jsx b/ui/src/components/BeamlineCamera/BeamlineCamera.jsx index 9b9138d77..58d149c9a 100644 --- a/ui/src/components/BeamlineCamera/BeamlineCamera.jsx +++ b/ui/src/components/BeamlineCamera/BeamlineCamera.jsx @@ -173,4 +173,4 @@ function CameraStreamImg({ src, alt, width, height }) { style={{ background: '#222' }} /> ); -} \ No newline at end of file +} diff --git a/ui/src/components/LoginForm/LoginForm.jsx b/ui/src/components/LoginForm/LoginForm.jsx index 6cd0afc79..eee714fb7 100644 --- a/ui/src/components/LoginForm/LoginForm.jsx +++ b/ui/src/components/LoginForm/LoginForm.jsx @@ -26,9 +26,9 @@ function LoginForm() { const useSSO = useSelector((state) => state.login.useSSO); // Console was showing this warning: - // Can't perform a React state update on an unmounted component. - // This is a no-op, but it indicates a memory leak in your application. - // To fix, cancel all subscriptions and asynchronous tasks + // Can't perform a React state update on an unmounted component. + // This is a no-op, but it indicates a memory leak in your application. + // To fix, cancel all subscriptions and asynchronous tasks // in a useEffect cleanup function. // This fixes the warning: diff --git a/ui/src/containers/SampleListViewContainer.jsx b/ui/src/containers/SampleListViewContainer.jsx index 19d210146..b7d523bfb 100644 --- a/ui/src/containers/SampleListViewContainer.jsx +++ b/ui/src/containers/SampleListViewContainer.jsx @@ -627,13 +627,9 @@ class SampleListViewContainer extends React.Component { return (