diff --git a/Dockerfile.api b/Dockerfile.api index c33b40d..e2a47ec 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -1,4 +1,4 @@ -FROM node:20.11.0-slim +FROM node:20.13.1-bookworm-slim WORKDIR /app diff --git a/Dockerfile.serv b/Dockerfile.serv index 57f213f..19f2543 100644 --- a/Dockerfile.serv +++ b/Dockerfile.serv @@ -1,10 +1,10 @@ -FROM node:20.11.0-slim as frontend +FROM node:20.13.1-bookworm-slim as frontend WORKDIR /app COPY ./nomad-front-end/package.json . -RUN npm install --force +RUN npm install COPY ./nomad-front-end . diff --git a/Dockerfile.serv-tls b/Dockerfile.serv-tls index a862f82..c2d1b1c 100644 --- a/Dockerfile.serv-tls +++ b/Dockerfile.serv-tls @@ -1,10 +1,10 @@ -FROM node:20.11.0-slim as frontend +FROM node:20.13.1-bookworm-slim as frontend WORKDIR /app COPY ./nomad-front-end/package.json . -RUN npm install --force +RUN npm install COPY ./nomad-front-end . diff --git a/nomad-front-end/Dockerfile b/nomad-front-end/Dockerfile index ea3a2ad..4c59cc3 100644 --- a/nomad-front-end/Dockerfile +++ b/nomad-front-end/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.11.0-slim +FROM node:20.13.1-bookworm-slim WORKDIR /app diff --git a/nomad-front-end/package.json b/nomad-front-end/package.json index 2604330..a57eb34 100644 --- a/nomad-front-end/package.json +++ b/nomad-front-end/package.json @@ -1,7 +1,7 @@ { "name": "nomad-front-end", "private": false, - "version": "3.5.3", + "version": "3.5.4-beta", "type": "module", "scripts": { "start": "env-cmd -f ../env/frontend.env vite", diff --git a/nomad-front-end/src/App.jsx b/nomad-front-end/src/App.jsx index 5e2bb32..de7d385 100644 --- a/nomad-front-end/src/App.jsx +++ b/nomad-front-end/src/App.jsx @@ -22,6 +22,7 @@ import Error404 from './components/Errors/Error404' import Error403 from './components/Errors/Error403' import Credits from './components/Credits/Credits' import Reset from './containers/Reset/Reset' +import Resubmit from './containers/Resubmit/Resubmit' const { Header, Sider, Content, Footer } = Layout @@ -184,6 +185,16 @@ const App = props => { ) } /> + + ) : ( + + ) + } + /> { }, { title: 'Description', - dataIndex: 'description' + dataIndex: 'description', + sorter: (a, b) => a.description.localeCompare(b.description) }, { title: 'Cost [£]', diff --git a/nomad-front-end/src/components/Forms/BookExperimentsForm/BookExperimentsForm.jsx b/nomad-front-end/src/components/Forms/BookExperimentsForm/BookExperimentsForm.jsx index 521c973..73ed402 100644 --- a/nomad-front-end/src/components/Forms/BookExperimentsForm/BookExperimentsForm.jsx +++ b/nomad-front-end/src/components/Forms/BookExperimentsForm/BookExperimentsForm.jsx @@ -13,7 +13,8 @@ import { message, Modal, Checkbox, - Tooltip + Tooltip, + Popconfirm } from 'antd' import moment from 'moment' @@ -23,6 +24,7 @@ import EditParamsModal from '../../Modals/EditParamsModal/EditPramsModal' import nightIcon from '../../../assets/night-mode.svg' import classes from './BookExperimentsForm.module.css' +import { all } from 'axios' const { Option } = Select @@ -45,9 +47,22 @@ const BookExperimentsForm = props => { const [exptState, setExptState] = useState({}) const [totalExptState, setTotalExptState] = useState({}) - const { inputData, allowanceData, fetchAllowance, token, accessLevel } = props + const { inputData, allowanceData, fetchAllowance, token, accessLevel, formValues } = props const priorityAccess = accessLevel === 'user-a' || accessLevel === 'admin' + const resubmit = formValues + + //This hook is used to cancel booked holders on the form component dismount + // It uses deleteHolders function in submit controller at the backend which has + // 120s timeout to allow for iconNMR to pickup submit file + useEffect(() => { + return () => { + props.cancelHolders( + token, + inputData.map(i => i.key) + ) + } + }, []) //Hook to create state for dynamic ExpNo part of form from inputData //InputData gets updated every time new holder is booked @@ -62,7 +77,7 @@ const BookExperimentsForm = props => { } else { newFormState.push({ key: i.key, - expCount: 1 + expCount: resubmit ? i.expCount : 1 }) } }) @@ -72,23 +87,56 @@ const BookExperimentsForm = props => { if (instrIds.size !== 0) { fetchAllowance(token, Array.from(instrIds)) } + + //setting up form values and expTime if form used for resubmit + if (resubmit) { + form.setFieldsValue(formValues) + } + // formState can't be dependency as it gets updated in the hook. That would trigger loop. // eslint-disable-next-line - }, [inputData]) + }, [inputData, formValues]) //This hook creates initial totalExpT state with overhead time for each entry useEffect(() => { - const newTotalExptState = { ...totalExptState } - if (allowanceData.length !== 0) { - formState.forEach(entry => { - const instrId = entry.key.split('-')[0] - const { overheadTime } = allowanceData.find(i => i.instrId === instrId) - if (!newTotalExptState[entry.key]) { - newTotalExptState[entry.key] = overheadTime + if (resubmit) { + const expTimeStateEntries = [] + const totalExpTimeStateEntries = [] + + if (allowanceData.length > 0) { + for (let sampleKey in formValues) { + let expTimeSum = allowanceData[0].overheadTime + //selecting first element of allowanceData array works as + //only holders on one instrument can be selected for resubmit + for (let expNo in formValues[sampleKey].exps) { + expTimeStateEntries.push([ + sampleKey + '#' + expNo, + formValues[sampleKey].exps[expNo].expTime + ]) + expTimeSum += moment + .duration(formValues[sampleKey].exps[expNo].expTime, 'HH:mm:ss') + .asSeconds() + } + totalExpTimeStateEntries.push([sampleKey, expTimeSum]) } - }) + } + + setExptState(Object.fromEntries(expTimeStateEntries)) + setTotalExptState(Object.fromEntries(totalExpTimeStateEntries)) + } else { + const newTotalExptState = { ...totalExptState } + if (allowanceData.length !== 0 && !resubmit) { + formState.forEach(entry => { + const instrId = entry.key.split('-')[0] + const { overheadTime } = allowanceData.find(i => i.instrId === instrId) + if (!newTotalExptState[entry.key]) { + newTotalExptState[entry.key] = overheadTime + } + }) + } + setTotalExptState(newTotalExptState) } - setTotalExptState(newTotalExptState) + // eslint-disable-next-line }, [allowanceData]) @@ -129,7 +177,6 @@ const BookExperimentsForm = props => { const onParamSetChange = (sampleKey, expNo, paramSetName) => { form.resetFields([[sampleKey, 'exps', expNo, 'params']]) const key = sampleKey + '#' + expNo - const paramSet = props.paramSetsData.find(paramSet => paramSet.name === paramSetName) if (paramSet.defaultParams.length < 4) { @@ -453,19 +500,22 @@ const BookExperimentsForm = props => { ))} {priorityAccess && checkBoxes} - - - + {!resubmit && ( + + + + )} { ) : (
{formItems} - - - + + + + + {resubmit && ( + + navigate('/dashboard')} + > + + + + )} + + { avatarSrc = submitIcon break + case location.pathname === '/resubmit': + headerTitle = 'Resubmit Experiments' + avatarSrc = submitIcon + break + case location.pathname === '/batch-submit': headerTitle = 'Batch Submit' avatarSrc = batchSubmitIcon diff --git a/nomad-front-end/src/components/StatusDrawer/StatusDrawer.jsx b/nomad-front-end/src/components/StatusDrawer/StatusDrawer.jsx index 3a779e3..71b2ae3 100644 --- a/nomad-front-end/src/components/StatusDrawer/StatusDrawer.jsx +++ b/nomad-front-end/src/components/StatusDrawer/StatusDrawer.jsx @@ -1,10 +1,11 @@ import React, { useState } from 'react' -import { Drawer, Button, Row, Col, message } from 'antd' +import { Drawer, Button, Space, message } from 'antd' import { connect } from 'react-redux' +import { useNavigate } from 'react-router-dom' import DrawerTable from './DrawerTable/DrawerTable' import SubmitModal from '../Modals/SubmitModal/SubmitModal' -import { postPending, signOutHandler, postPendingAuth } from '../../store/actions' +import { postPending, signOutHandler, postPendingAuth, resubmitHolders } from '../../store/actions' const StatusDrawer = props => { const { id, visible, tableData, dataLoading } = props.status @@ -13,6 +14,8 @@ const StatusDrawer = props => { const [modalVisible, setModalVisible] = useState(false) const [modalData, setModalData] = useState({}) + const navigate = useNavigate() + let title = '' let buttons = null const headerClass = { @@ -22,9 +25,6 @@ const StatusDrawer = props => { } const btnClickHandler = type => { - if (selectedHolders.length === 0) { - return message.warning('No holders selected!') - } if (authToken) { pendingHandler(authToken, type, selectedHolders) if (accessLvl !== 'admin') { @@ -42,6 +42,28 @@ const StatusDrawer = props => { } } + const editHandler = () => { + console.log(selectedHolders) + const usernames = new Set() + const instrIds = new Set() + selectedHolders.map(row => { + usernames.add(row.username) + instrIds.add(row).instrIds + }) + + if (usernames.size !== 1 || instrIds.size !== 1) { + return message.error('Holders for multiple users or instruments selected!') + } + + props.resubmitHandler(authToken, { + username: selectedHolders[0].username, + checkedHolders: selectedHolders.map(i => i.holder), + instrId: selectedHolders[0].instrId + }) + + navigate('/resubmit') + } + switch (id) { case 'errors': title = 'Errors' @@ -58,16 +80,24 @@ const StatusDrawer = props => { headerClass.backgroundColor = '#ffffb8' headerClass.borderBottom += '#fadb14' buttons = ( - - - - - - - - + + + + + ) break default: @@ -116,7 +146,8 @@ const mapDispatchToProps = dispatch => { return { pendingHandler: (token, type, data) => dispatch(postPending(token, type, data)), pendingAuthHandler: (type, data) => dispatch(postPendingAuth(type, data)), - logoutHandler: token => dispatch(signOutHandler(token)) + logoutHandler: token => dispatch(signOutHandler(token)), + resubmitHandler: (token, data) => dispatch(resubmitHolders(token, data)) } } diff --git a/nomad-front-end/src/components/StatusTabs/StatusBanner/StatusBanner.jsx b/nomad-front-end/src/components/StatusTabs/StatusBanner/StatusBanner.jsx index 343a756..1780858 100644 --- a/nomad-front-end/src/components/StatusTabs/StatusBanner/StatusBanner.jsx +++ b/nomad-front-end/src/components/StatusTabs/StatusBanner/StatusBanner.jsx @@ -1,10 +1,17 @@ import React from 'react' import { connect } from 'react-redux' import { Alert, Row, Col, Tag, Switch, Button, Space, Modal } from 'antd' +import { useNavigate } from 'react-router-dom' import TrafficLights from '../../TrafficLights/TrafficLights' -import { deleteExperiments, resetQueue, toggleAvailableOnDash } from '../../../store/actions' +import { + deleteExperiments, + resetCheckedHolders, + resetQueue, + resubmitHolders, + toggleAvailableOnDash +} from '../../../store/actions' import classes from './StatusBanner.module.css' @@ -13,10 +20,16 @@ const StatusBanner = props => { const bannerType = props.data.available ? 'success' : 'error' const { authToken, instrId, checkedHolders, accessLvl, data, tabData } = props + const navigate = useNavigate() + const submittedCheckedHolders = checkedHolders.filter(holder => { return tabData.find(row => row.holder === holder && row.status === 'Submitted') }) + const editableHolders = checkedHolders.filter(holder => { + return tabData.find(row => row.holder === holder && row.status !== 'Running') + }) + const switchElement = ( { const cancelButton = ( ) + const resubmitButton = ( + + ) + return ( { {cancelButton} + {resubmitButton} {accessLvl === 'admin' && resetButton} {accessLvl === 'admin' && switchElement} @@ -124,7 +172,9 @@ const mapDispatchToProps = dispatch => { toggleAvailable: (instrId, token) => dispatch(toggleAvailableOnDash(instrId, token)), deleteHoldersHandler: (token, instrId, holders) => dispatch(deleteExperiments(token, instrId, holders)), - resetInstr: (token, instrId) => dispatch(resetQueue(token, instrId)) + resetInstr: (token, instrId) => dispatch(resetQueue(token, instrId)), + resetChecked: () => dispatch(resetCheckedHolders()), + resubmitHandler: (token, data) => dispatch(resubmitHolders(token, data)) } } diff --git a/nomad-front-end/src/containers/Dashboard/Dashboard.jsx b/nomad-front-end/src/containers/Dashboard/Dashboard.jsx index 81985c9..6ece7f2 100644 --- a/nomad-front-end/src/containers/Dashboard/Dashboard.jsx +++ b/nomad-front-end/src/containers/Dashboard/Dashboard.jsx @@ -2,11 +2,12 @@ import React, { Fragment, useState, useEffect, useRef } from 'react' import { connect } from 'react-redux' import { - fetchStatusSummary, - fetchStatusTable, - closeDashDrawer, - statusUpdate, - toggleAvailableSwitchSuccess + fetchStatusSummary, + fetchStatusTable, + closeDashDrawer, + statusUpdate, + toggleAvailableSwitchSuccess, + resetCheckedHolders } from '../../store/actions' import socket from '../../socketConnection' @@ -17,102 +18,104 @@ import StatusDrawer from '../../components/StatusDrawer/StatusDrawer' import './Dashboard.css' const Dashboard = props => { - const [activeTab, setActiveTab] = useState('0') - const { fetchStatusSum, fetchStatusTable, statusSummary } = props + const [activeTab, setActiveTab] = useState('0') + const { fetchStatusSum, fetchStatusTable, statusSummary } = props - const activeTabIdRef = useRef(null) + const activeTabIdRef = useRef(null) - useEffect(() => { - window.scrollTo(0, 0) - fetchStatusSum() - fetchStatusTable('0') - }, [fetchStatusSum, fetchStatusTable]) + useEffect(() => { + window.scrollTo(0, 0) + fetchStatusSum() + fetchStatusTable('0') + }, [fetchStatusSum, fetchStatusTable]) - //Hook sets active tab to 1st instrument in the array when the page get reloaded - useEffect(() => { - if (activeTab === '0' && statusSummary.length > 0) { - setActiveTab(statusSummary[0].key) - } - }, [activeTab, statusSummary]) + //Hook sets active tab to 1st instrument in the array when the page get reloaded + useEffect(() => { + if (activeTab === '0' && statusSummary.length > 0) { + setActiveTab(statusSummary[0].key) + } + }, [activeTab, statusSummary]) - //Hook creates reference to instrument ID in activeTab and activeTab index value that is used in subsequent hook to reload the active tab if it gets updated. - useEffect(() => { - if (props.statusSummary.length > 0) { - activeTabIdRef.current = { - // instrId: props.statusSummary[activeTab]._id, - instrId: activeTab, - activeTab - } - } - }, [props.statusSummary, activeTab]) + //Hook creates reference to instrument ID in activeTab and activeTab index value that is used in subsequent hook to reload the active tab if it gets updated. + useEffect(() => { + if (props.statusSummary.length > 0) { + activeTabIdRef.current = { + // instrId: props.statusSummary[activeTab]._id, + instrId: activeTab, + activeTab + } + } + }, [props.statusSummary, activeTab]) - //Hook for socket.io that gets triggered when status of an instrument is updated by tracker at the backend - useEffect(() => { - socket.on('statusUpdate', data => { - props.statUpdate(data) - const { instrId, activeTab } = activeTabIdRef.current - if (instrId === data.instrId) { - fetchStatusTable(activeTab) - } - }) + //Hook for socket.io that gets triggered when status of an instrument is updated by tracker at the backend + useEffect(() => { + socket.on('statusUpdate', data => { + props.statUpdate(data) + const { instrId, activeTab } = activeTabIdRef.current + if (instrId === data.instrId) { + fetchStatusTable(activeTab) + } + }) - socket.on('availableUpdate', data => { - props.toggleAvailableSuccess(data) - }) - return () => { - socket.removeAllListeners('statusUpdate') - } - // useEffect for socket.io function must have empty dependency array otherwise the triggers infinite loop!!! - // eslint-disable-next-line - }, []) + socket.on('availableUpdate', data => { + props.toggleAvailableSuccess(data) + }) + return () => { + socket.removeAllListeners('statusUpdate') + } + // useEffect for socket.io function must have empty dependency array otherwise the triggers infinite loop!!! + // eslint-disable-next-line + }, []) - const tabChangeHandler = key => { - fetchStatusTable(key) - setActiveTab(key) - } + const tabChangeHandler = key => { + fetchStatusTable(key) + setActiveTab(key) + props.resetChecked() + } - return ( - - - {props.showCards ? ( - - ) : null} - -
- -
- -
- ) + return ( + + + {props.showCards ? ( + + ) : null} + +
+ +
+ +
+ ) } const mapStateToProps = state => { - return { - showCards: state.dash.showCards, - statusSummary: state.dash.statusSummaryData, - statusTable: state.dash.statusTableData, - tableLoading: state.dash.tableLoading, - drawerState: state.dash.drawerState, - accessLevel: state.auth.accessLevel, - authToken: state.auth.token, - username: state.auth.username - } + return { + showCards: state.dash.showCards, + statusSummary: state.dash.statusSummaryData, + statusTable: state.dash.statusTableData, + tableLoading: state.dash.tableLoading, + drawerState: state.dash.drawerState, + accessLevel: state.auth.accessLevel, + authToken: state.auth.token, + username: state.auth.username + } } const mapDispatchToProps = dispatch => { - return { - fetchStatusSum: () => dispatch(fetchStatusSummary()), - onCloseDrawer: () => dispatch(closeDashDrawer()), - fetchStatusTable: key => dispatch(fetchStatusTable(key)), - statUpdate: data => dispatch(statusUpdate(data)), - toggleAvailableSuccess: payload => dispatch(toggleAvailableSwitchSuccess(payload)) - } + return { + fetchStatusSum: () => dispatch(fetchStatusSummary()), + onCloseDrawer: () => dispatch(closeDashDrawer()), + fetchStatusTable: key => dispatch(fetchStatusTable(key)), + statUpdate: data => dispatch(statusUpdate(data)), + toggleAvailableSuccess: payload => dispatch(toggleAvailableSwitchSuccess(payload)), + resetChecked: () => dispatch(resetCheckedHolders()) + } } export default connect(mapStateToProps, mapDispatchToProps)(Dashboard) diff --git a/nomad-front-end/src/containers/Resubmit/Resubmit.jsx b/nomad-front-end/src/containers/Resubmit/Resubmit.jsx new file mode 100644 index 0000000..6e42e14 --- /dev/null +++ b/nomad-front-end/src/containers/Resubmit/Resubmit.jsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react' +import { connect } from 'react-redux' + +import BookExperimentsForm from '../../components/Forms/BookExperimentsForm/BookExperimentsForm' + +import { + fetchParamSets, + fetchAllowance, + bookExperiments, + resetResubmit, + signOutHandler, + cancelBookedHolders +} from '../../store/actions' + +const Resubmit = props => { + const { reservedHolders, formValues, userId } = props.resubmitData + const { fetchParamSets, authToken, allowance, accessLvl, logoutHandler } = props + + useEffect(() => { + return () => { + props.resetState() + + if (accessLvl !== 'admin' && authToken) { + logoutHandler(authToken) + } + } + }, []) + + useEffect(() => { + fetchParamSets(authToken, { instrumentId: null, searchValue: '' }) + }, [fetchParamSets, authToken]) + + return ( +
+ {reservedHolders.length !== 0 || props.loading ? ( + + ) : null} +
+ ) +} + +const mapStateToProps = state => ({ + resubmitData: state.submit.resubmitData, + loading: state.submit.loading, + accessLvl: state.auth.accessLevel, + authToken: state.auth.token, + paramSets: state.paramSets.paramSetsData, + allowance: state.submit.allowance +}) + +const mapDispatchToProps = dispatch => { + return { + fetchParamSets: (token, searchParams) => dispatch(fetchParamSets(token, searchParams)), + fetchAllow: (token, instrIds) => dispatch(fetchAllowance(token, instrIds)), + bookExpsHandler: (token, data, user) => dispatch(bookExperiments(token, data, user)), + resetState: () => dispatch(resetResubmit()), + logoutHandler: token => dispatch(signOutHandler(token)), + cancelHolders: (token, keys) => dispatch(cancelBookedHolders(token, keys)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Resubmit) diff --git a/nomad-front-end/src/store/actions/actionTypes.js b/nomad-front-end/src/store/actions/actionTypes.js index 8029bb9..d8c27d1 100644 --- a/nomad-front-end/src/store/actions/actionTypes.js +++ b/nomad-front-end/src/store/actions/actionTypes.js @@ -35,6 +35,7 @@ export const UPDATE_CHECKBOX_STATUS_TAB = 'UPDATE_CHECKBOX_STATUS_TAB' export const DELETE_HOLDERS_SUCCESS = 'DELETE_HOLDERS_SUCCESS' export const UPDATE_PENDING_CHECKED = 'UPDATE_PENDING_CHECKED' export const POST_PENDING_SUCCESS = 'POST_PENDING_SUCCESS' +export const RESET_CHECKED_HOLDERS = 'RESET_CHECKED_HOLDERS' //INSTRUMENTS export const FETCH_INSTRUMENTS_TABLE_START = 'FETCH_INSTRUMENTS_TABLE_START' @@ -107,6 +108,8 @@ export const CANCEL_HOLDER_SUCCESS = 'CANCEL_HOLDER_SUCCESS' export const CANCEL_BOOKED_HOLDERS_SUCCESS = 'CANCEL_BOOKED_HOLDERS_SUCCESS' export const BOOK_EXPERIMENTS_SUCCESS = 'BOOK_EXPERIMENTS_SUCCESS' export const FETCH_ALLOWANCE_SUCCESS = 'FETCH_ALLOWANCE_SUCCESS' +export const RESUBMIT_HOLDERS_SUCCESS = 'RESUBMIT_HOLDERS_SUCCESS' +export const RESET_RESUBMIT_DATA = 'RESET_RESUBMIT_DATA' //MESSAGE export const SEND_MESSAGE_START = 'SEND_MESSAGE_START' diff --git a/nomad-front-end/src/store/actions/dashboard.js b/nomad-front-end/src/store/actions/dashboard.js index c91510d..90fcb26 100644 --- a/nomad-front-end/src/store/actions/dashboard.js +++ b/nomad-front-end/src/store/actions/dashboard.js @@ -3,210 +3,214 @@ import axios from '../../axios-instance' import errorHandler from './errorHandler' export const toggleCards = () => { - return { - type: actionTypes.TOGGLE_CARDS - } + return { + type: actionTypes.TOGGLE_CARDS + } } export const openDashDrawerStart = payload => ({ - type: actionTypes.OPEN_DASH_DRAWER_START, - id: payload + type: actionTypes.OPEN_DASH_DRAWER_START, + id: payload }) export const openDashDrawerSuccess = payload => ({ - type: actionTypes.FETCH_DASH_DRAWER_SUCCESS, - data: payload + type: actionTypes.FETCH_DASH_DRAWER_SUCCESS, + data: payload }) export const openDashDrawer = id => { - return dispatch => { - dispatch(openDashDrawerStart(id)) - axios - .get('/dash/drawer-table/' + id) - .then(res => { - dispatch(openDashDrawerSuccess(res.data)) - dispatch(autoCloseModal()) - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + dispatch(openDashDrawerStart(id)) + axios + .get('/dash/drawer-table/' + id) + .then(res => { + dispatch(openDashDrawerSuccess(res.data)) + dispatch(autoCloseModal()) + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const closeDashDrawer = () => ({ - type: actionTypes.CLOSE_DASH_DRAWER + type: actionTypes.CLOSE_DASH_DRAWER }) export const autoCloseModal = () => { - return dispatch => { - setTimeout(() => dispatch(closeDashDrawer()), 120000) - } + return dispatch => { + setTimeout(() => dispatch(closeDashDrawer()), 120000) + } } export const fetchStatusSummarySuccess = payload => ({ - type: actionTypes.FETCH_STATUS_SUMMARY_SUCCESS, - data: payload + type: actionTypes.FETCH_STATUS_SUMMARY_SUCCESS, + data: payload }) export const fetchStatusSummary = () => { - return dispatch => { - axios - .get('/dash/status-summary') - .then(res => dispatch(fetchStatusSummarySuccess(res.data))) - .catch(err => dispatch(errorHandler(err))) - } + return dispatch => { + axios + .get('/dash/status-summary') + .then(res => dispatch(fetchStatusSummarySuccess(res.data))) + .catch(err => dispatch(errorHandler(err))) + } } export const fetchStatusTableStart = () => ({ - type: actionTypes.FETCH_STATUS_TABLE_START + type: actionTypes.FETCH_STATUS_TABLE_START }) export const fetchStatusTableSuccess = payload => ({ - type: actionTypes.FETCH_STATUS_TABLE_SUCCESS, - data: payload + type: actionTypes.FETCH_STATUS_TABLE_SUCCESS, + data: payload }) export const fetchStatusTable = tab => { - return dispatch => { - dispatch(fetchStatusTableStart()) - axios - .get('/dash/status-table/' + tab) - .then(res => dispatch(fetchStatusTableSuccess(res.data))) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + dispatch(fetchStatusTableStart()) + axios + .get('/dash/status-table/' + tab) + .then(res => dispatch(fetchStatusTableSuccess(res.data))) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const statusUpdate = data => ({ - type: actionTypes.STATUS_UPDATE, - data + type: actionTypes.STATUS_UPDATE, + data }) export const toggleAvailableSwitchSuccess = payload => { - return { - type: actionTypes.TOGGLE_AVAILABLE_SUCCESS_DASH, - data: payload - } + return { + type: actionTypes.TOGGLE_AVAILABLE_SUCCESS_DASH, + data: payload + } } export const toggleAvailableOnDash = (id, token) => { - return dispatch => { - axios - .patch(`/admin/instruments/toggle-available/${id}`, null, { - headers: { Authorization: 'Bearer ' + token } - }) - .then(res => { - // dispatch(toggleAvailableSwitchSuccess(res.data)) - //state gets updated through sockets calling toggleAvailableSwitchSuccess - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + axios + .patch(`/admin/instruments/toggle-available/${id}`, null, { + headers: { Authorization: 'Bearer ' + token } + }) + .then(res => { + // dispatch(toggleAvailableSwitchSuccess(res.data)) + //state gets updated through sockets calling toggleAvailableSwitchSuccess + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const updateCheckboxStatusTab = payload => ({ - type: actionTypes.UPDATE_CHECKBOX_STATUS_TAB, - payload + type: actionTypes.UPDATE_CHECKBOX_STATUS_TAB, + payload }) export const deleteHoldersSuccess = payload => ({ - type: actionTypes.DELETE_HOLDERS_SUCCESS, - payload + type: actionTypes.DELETE_HOLDERS_SUCCESS, + payload }) export const deleteExperiments = (token, instrId, holders) => { - return dispatch => { - axios - .delete('/submit/experiments/' + instrId, { - data: holders, - headers: { Authorization: 'Bearer ' + token } - }) - .then(res => { - if (res.status === 200) { - dispatch(deleteHoldersSuccess(holders)) - } - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + axios + .delete('/submit/experiments/' + instrId, { + data: holders, + headers: { Authorization: 'Bearer ' + token } + }) + .then(res => { + if (res.status === 200) { + dispatch(deleteHoldersSuccess(holders)) + } + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const resetQueue = (token, instrId) => { - return dispatch => { - axios - .put('/submit/reset/' + instrId, null, { - headers: { Authorization: 'Bearer ' + token } - }) - .then(res => { - dispatch(deleteHoldersSuccess(res.data)) - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + axios + .put('/submit/reset/' + instrId, null, { + headers: { Authorization: 'Bearer ' + token } + }) + .then(res => { + dispatch(deleteHoldersSuccess(res.data)) + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const updatePendingChecked = payload => ({ - type: actionTypes.UPDATE_PENDING_CHECKED, - payload + type: actionTypes.UPDATE_PENDING_CHECKED, + payload }) export const postPendingSuccess = () => ({ - type: actionTypes.POST_PENDING_SUCCESS + type: actionTypes.POST_PENDING_SUCCESS }) export const postPending = (token, type, data) => { - return dispatch => { - axios - .post( - '/submit/pending/' + type, - { data: skimPendingData(data) }, - { - headers: { Authorization: 'Bearer ' + token } - } - ) - .then(res => { - if (res.status === 200) { - dispatch(postPendingSuccess()) - } - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + axios + .post( + '/submit/pending/' + type, + { data: skimPendingData(data) }, + { + headers: { Authorization: 'Bearer ' + token } + } + ) + .then(res => { + if (res.status === 200) { + dispatch(postPendingSuccess()) + } + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } export const postPendingAuth = (type, inputData) => { - return dispatch => { - axios - .post('/submit/pending-auth/' + type, { - username: inputData.username, - password: inputData.password, - data: skimPendingData(inputData.holders) - }) - .then(res => { - if (res.status === 200) { - dispatch(postPendingSuccess()) - } - }) - .catch(err => { - dispatch(errorHandler(err)) - }) - } + return dispatch => { + axios + .post('/submit/pending-auth/' + type, { + username: inputData.username, + password: inputData.password, + data: skimPendingData(inputData.holders) + }) + .then(res => { + if (res.status === 200) { + dispatch(postPendingSuccess()) + } + }) + .catch(err => { + dispatch(errorHandler(err)) + }) + } } +export const resetCheckedHolders = () => ({ + type: actionTypes.RESET_CHECKED_HOLDERS +}) + //Helper function for restructuring pending data object const skimPendingData = data => { - const result = {} - data.forEach(i => { - if (!result[i.instrId]) { - result[i.instrId] = [i.holder] - } else { - result[i.instrId].push(i.holder) - } - }) - return result + const result = {} + data.forEach(i => { + if (!result[i.instrId]) { + result[i.instrId] = [i.holder] + } else { + result[i.instrId].push(i.holder) + } + }) + return result } diff --git a/nomad-front-end/src/store/actions/index.js b/nomad-front-end/src/store/actions/index.js index 4f032f2..caf2237 100644 --- a/nomad-front-end/src/store/actions/index.js +++ b/nomad-front-end/src/store/actions/index.js @@ -25,7 +25,8 @@ export { postPendingAuth, toggleAvailableSwitchSuccess, deleteExperiments, - resetQueue + resetQueue, + resetCheckedHolders } from './dashboard' export { @@ -93,7 +94,9 @@ export { cancelBookedHolders, bookExperiments, cancelBookedHoldersSuccess, - fetchAllowance + fetchAllowance, + resubmitHolders, + resetResubmit } from './submit' export { sendMessage } from './message' diff --git a/nomad-front-end/src/store/actions/submit.js b/nomad-front-end/src/store/actions/submit.js index 86145d9..eeb4c35 100644 --- a/nomad-front-end/src/store/actions/submit.js +++ b/nomad-front-end/src/store/actions/submit.js @@ -121,3 +121,30 @@ export const fetchAllowance = (token, instrIds) => { }) } } + +export const resubmitHoldersSuccess = payload => ({ + type: actionTypes.RESUBMIT_HOLDERS_SUCCESS, + payload +}) + +export const resubmitHolders = (token, dataObj) => { + return dispatch => { + axios + .post('/submit/resubmit', dataObj, { + headers: { Authorization: 'Bearer ' + token } + }) + .then(res => { + if (res.status === 200) { + dispatch(resubmitHoldersSuccess(res.data)) + } + }) + .catch(err => { + console.log(err) + dispatch(errorHandler(err)) + }) + } +} + +export const resetResubmit = () => ({ + type: actionTypes.RESET_RESUBMIT_DATA +}) diff --git a/nomad-front-end/src/store/reducers/dashboard.js b/nomad-front-end/src/store/reducers/dashboard.js index 5a97c14..c2ae24f 100644 --- a/nomad-front-end/src/store/reducers/dashboard.js +++ b/nomad-front-end/src/store/reducers/dashboard.js @@ -157,6 +157,12 @@ const reducer = (state = initialState, action) => { drawerState: { ...state.drawerState, visible: false, pendingChecked: [] } } + case actionTypes.RESET_CHECKED_HOLDERS: + return { ...state, statusTabChecked: [] } + + case actionTypes.RESET_RESUBMIT_DATA: + return { ...state, drawerState: { ...state.drawerState, visible: false, pendingChecked: [] } } + default: return state } diff --git a/nomad-front-end/src/store/reducers/submit.js b/nomad-front-end/src/store/reducers/submit.js index 74fd909..0ee03c4 100644 --- a/nomad-front-end/src/store/reducers/submit.js +++ b/nomad-front-end/src/store/reducers/submit.js @@ -4,7 +4,8 @@ import * as actionTypes from '../actions/actionTypes' const initialState = { loading: false, bookedHolders: [], - allowance: [] + allowance: [], + resubmitData: { reservedHolders: [], formValues: {}, userId: undefined } } const reducer = (state = initialState, { type, payload }) => { @@ -52,12 +53,54 @@ const reducer = (state = initialState, { type, payload }) => { bookedHolders: [] } - // case actionTypes.CLEAR_BOOKED_HOLDERS: - // return { ...state, bookedHolders: [] } - case actionTypes.FETCH_ALLOWANCE_SUCCESS: return { ...state, allowance: payload } + case actionTypes.RESUBMIT_HOLDERS_SUCCESS: + const { experimentData } = payload + const reservedHoldersSet = new Set() + experimentData.forEach(exp => { + reservedHoldersSet.add(exp.holder) + }) + const reservedHolders = Array.from(reservedHoldersSet).map(holder => ({ + holder: +holder, + instId: payload.instrument._id, + instrument: payload.instrument.name, + key: payload.instrument._id + '-' + holder, + paramsEditing: payload.instrument.paramsEditing, + expCount: experimentData.filter(i => i.holder === holder).length + })) + + let formValues = {} + reservedHolders.forEach(entry => { + const exps = experimentData.filter(i => i.holder === entry.holder.toString()) + + const expsEntries = exps.map(exp => [ + exp.expNo, + { + paramSet: exp.parameterSet, + params: exp.parameters, + expTime: exp.time + } + ]) + + formValues[entry.key] = { + title: exps[0].title, + solvent: exps[0].solvent, + night: exps[0].night, + priority: exps[0].priority, + exps: Object.fromEntries(expsEntries) + } + }) + + return { + ...state, + resubmitData: { reservedHolders, formValues, userId: payload.userId } + } + + case actionTypes.RESET_RESUBMIT_DATA: + return { ...state, resubmitData: { reservedHolders: [], formValues: {}, userId: undefined } } + default: return state } diff --git a/nomad-rest-api/Dockerfile b/nomad-rest-api/Dockerfile index 44a53c7..f5e0770 100644 --- a/nomad-rest-api/Dockerfile +++ b/nomad-rest-api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.11.0-slim +FROM node:20.13.1-bookworm-slim WORKDIR /app diff --git a/nomad-rest-api/controllers/auth.js b/nomad-rest-api/controllers/auth.js index 6fac0bf..8655d8a 100644 --- a/nomad-rest-api/controllers/auth.js +++ b/nomad-rest-api/controllers/auth.js @@ -26,7 +26,6 @@ export async function postLogin(req, res) { } const token = await user.generateAuthToken() - return res.send({ username: user.username, accessLevel: user.accessLevel, diff --git a/nomad-rest-api/controllers/submit.js b/nomad-rest-api/controllers/submit.js index 0893f30..45eb461 100644 --- a/nomad-rest-api/controllers/submit.js +++ b/nomad-rest-api/controllers/submit.js @@ -207,7 +207,11 @@ export const deleteExps = (req, res) => { res.send() } catch (error) { console.log(error) - res.status(500).send() + if (error.toString().includes('Client disconnected')) { + res.status(503).send('Client disconnected') + } else { + res.sendStatus(500) + } } } @@ -245,7 +249,11 @@ export const putReset = async (req, res) => { res.status(200).json(holdersToDelete) } catch (error) { console.log(error) - res.status(500).send() + if (error.toString().includes('Client disconnected')) { + res.status(503).send('Client disconnected') + } else { + res.sendStatus(500) + } } } @@ -335,14 +343,55 @@ export const getAllowance = async (req, res) => { } } +export async function postResubmit(req, res) { + try { + const { instrId, checkedHolders, username } = req.body + const submitter = getSubmitter() + + const { status } = await Instrument.findById(req.body.instrId, 'status') + + const experimentData = status.statusTable + .filter(entry => checkedHolders.find(holder => holder === entry.holder)) + .map(entry => ({ ...entry, title: entry.title.split('||')[0] })) + + if (experimentData.length === 0) { + return res.status(422).send({ errors: [{ msg: 'Experiments not found in status table' }] }) + } + + emitDeleteExps(instrId, checkedHolders, res) + submitter.updateBookedHolders( + instrId, + checkedHolders.map(i => +i) + ) + + const user = await User.findOne({ username }) + const instrument = await Instrument.findById(instrId, 'name paramsEditing') + if (!user) { + return res.status(404).send({ message: 'User not found' }) + } + if (!instrument) { + return res.status(404).send({ message: 'Instrument not found' }) + } + + res.status(200).json({ userId: user._id, instrument, experimentData }) + } catch (error) { + console.log(error) + if (error.toString().includes('Client disconnected')) { + res.status(503).send('Client disconnected') + } else { + res.sendStatus(500) + } + } +} + //Helper function that sends array of holders to be deleted to the client const emitDeleteExps = (instrId, holders, res) => { const submitter = getSubmitter() const { socketId } = submitter.state.get(instrId) if (!socketId) { - console.log('Error: Client disconnected') - return res.status(503).send('Client disconnected') + throw new Error('Client disconnected') + // return res.status(503).send('Client disconnected') } getIO().to(socketId).emit('delete', JSON.stringify(holders)) diff --git a/nomad-rest-api/package.json b/nomad-rest-api/package.json index 5cd8a87..03c97c5 100644 --- a/nomad-rest-api/package.json +++ b/nomad-rest-api/package.json @@ -1,6 +1,6 @@ { "name": "nomad-rest-api", - "version": "3.5.3", + "version": "3.5.4-beta", "description": "REST API back-end for NOMAD system", "main": "server.js", "type": "module", @@ -44,4 +44,4 @@ "supertest": "^7.0.0", "vitest": "^1.3.1" } -} +} \ No newline at end of file diff --git a/nomad-rest-api/routes/submit.js b/nomad-rest-api/routes/submit.js index c6846cd..4f2557b 100644 --- a/nomad-rest-api/routes/submit.js +++ b/nomad-rest-api/routes/submit.js @@ -10,7 +10,8 @@ import { deleteExps, putReset, postPending, - getAllowance + getAllowance, + postResubmit } from '../controllers/submit.js' const router = Router() @@ -33,4 +34,6 @@ router.post('/pending-auth/:type', postPending) router.get('/allowance/', auth, getAllowance) +router.post('/resubmit', auth, postResubmit) + export default router