diff --git a/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap b/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap new file mode 100644 index 0000000000..dff3f7f510 --- /dev/null +++ b/src/__test__/redux/actions/cellSets/__snapshots__/runCellSetsClustering.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`runCellSetsClustering action Dispatches all required actions to update cell sets clustering. 1`] = ` +Object { + "type": "cellSets/clusteringUpdating", +} +`; + +exports[`runCellSetsClustering action Dispatches all required actions to update cell sets clustering. 2`] = `undefined`; + +exports[`runCellSetsClustering action Dispatches all required actions to update cell sets clustering. 3`] = `undefined`; + +exports[`runCellSetsClustering action Dispatches error action when sendWord fails 1`] = ` +Object { + "type": "cellSets/clusteringUpdating", +} +`; + +exports[`runCellSetsClustering action Dispatches error action when sendWord fails 2`] = ` +Object { + "payload": Object { + "error": undefined, + "experimentId": "1234", + }, + "type": "cellSets/error", +} +`; diff --git a/src/__test__/redux/actions/cellSets/__snapshots__/updateCellSetsClustering.test.js.snap b/src/__test__/redux/actions/cellSets/__snapshots__/updateCellSetsClustering.test.js.snap deleted file mode 100644 index 807efb08c9..0000000000 --- a/src/__test__/redux/actions/cellSets/__snapshots__/updateCellSetsClustering.test.js.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`updateCellSetsClustering action Dispatches all required actions to update cell sets clustering. 1`] = ` -Object { - "type": "cellSets/clusteringUpdating", -} -`; - -exports[`updateCellSetsClustering action Dispatches all required actions to update cell sets clustering. 2`] = ` -Object { - "payload": Object { - "data": Array [ - Object { - "cellIds": Array [ - "1", - "2", - "3", - ], - "color": "#ff0000", - "name": "one", - }, - ], - "experimentId": "1234", - }, - "type": "cellSets/clusteringUpdated", -} -`; - -exports[`updateCellSetsClustering action Dispatches all required actions to update cell sets clustering. 3`] = ` -Object { - "payload": Object { - "experimentId": "1234", - "saved": Object {}, - }, - "type": "cellSets/save", -} -`; - -exports[`updateCellSetsClustering action Dispatches error action when the reset fails 1`] = ` -Object { - "type": "cellSets/clusteringUpdating", -} -`; - -exports[`updateCellSetsClustering action Dispatches error action when the reset fails 2`] = ` -Object { - "payload": Object { - "data": Array [ - Object { - "results": Object { - "error": "The backend returned an error", - }, - }, - ], - "experimentId": "1234", - }, - "type": "cellSets/clusteringUpdated", -} -`; diff --git a/src/__test__/redux/actions/cellSets/updateCellSetsClustering.test.js b/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js similarity index 71% rename from src/__test__/redux/actions/cellSets/updateCellSetsClustering.test.js rename to src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js index a9c5593f73..29f411c2f4 100644 --- a/src/__test__/redux/actions/cellSets/updateCellSetsClustering.test.js +++ b/src/__test__/redux/actions/cellSets/runCellSetsClustering.test.js @@ -1,22 +1,27 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import updateCellSetsClustering from '../../../../redux/actions/cellSets/updateCellSetsClustering'; +import runCellSetsClustering from '../../../../redux/actions/cellSets/runCellSetsClustering'; import initialState from '../../../../redux/reducers/cellSets/initialState'; -import { fetchCachedWork } from '../../../../utils/cacheRequest'; +import sendWork from '../../../../utils/sendWork'; enableFetchMocks(); const mockStore = configureStore([thunk]); jest.mock('localforage'); jest.mock('../../../../utils/cacheRequest'); +jest.mock('../../../../utils/sendWork', () => ({ + __esModule: true, // this property makes it work + default: jest.fn(), +})); + const startDate = '2021-01-01T00:00:00'; const backendStatusStore = { backendStatus: { status: { pipeline: { startDate } } }, }; -describe('updateCellSetsClustering action', () => { +describe('runCellSetsClustering action', () => { const experimentId = '1234'; beforeEach(() => { @@ -36,7 +41,7 @@ describe('updateCellSetsClustering action', () => { cellSets: { loading: true, error: false }, experimentSettings: backendStatusStore, }); - store.dispatch(updateCellSetsClustering(experimentId)); + store.dispatch(runCellSetsClustering(experimentId)); expect(store.getActions().length).toEqual(0); }); @@ -45,7 +50,7 @@ describe('updateCellSetsClustering action', () => { cellSets: { loading: false, error: true }, experimentSettings: backendStatusStore, }); - store.dispatch(updateCellSetsClustering(experimentId)); + store.dispatch(runCellSetsClustering(experimentId)); expect(store.getActions().length).toEqual(0); }); @@ -68,20 +73,14 @@ describe('updateCellSetsClustering action', () => { experimentSettings: backendStatusStore, }); - fetchCachedWork.mockImplementation(() => { - const resolveWith = { - name: 'one', color: '#ff0000', cellIds: ['1', '2', '3'], - }; - - return new Promise((resolve) => resolve(resolveWith)); - }); - const flushPromises = () => new Promise(setImmediate); - store.dispatch(updateCellSetsClustering(experimentId, 0.5)); + sendWork.mockImplementation(() => Promise.resolve()); + + store.dispatch(runCellSetsClustering(experimentId, 0.5)); - expect(fetchCachedWork).toHaveBeenCalledTimes(1); - expect(fetchCachedWork).toHaveBeenCalledWith(experimentId, 30, { + expect(sendWork).toHaveBeenCalledTimes(1); + expect(sendWork).toHaveBeenCalledWith(experimentId, 30, { name: 'ClusterCells', cellSetName: 'Louvain clusters', type: 'louvain', @@ -102,25 +101,20 @@ describe('updateCellSetsClustering action', () => { done(); }); - it('Dispatches error action when the reset fails', async () => { + it('Dispatches error action when sendWord fails', async () => { const store = mockStore({ cellSets: { ...initialState, loading: false }, experimentSettings: backendStatusStore, }); - fetchCachedWork.mockImplementation(() => { - const resolveWith = { - results: { error: 'The backend returned an error' }, - }; - return new Promise((resolve) => resolve(resolveWith)); - }); + sendWork.mockImplementation(() => Promise.reject()); const flushPromises = () => new Promise(setImmediate); - store.dispatch(updateCellSetsClustering(experimentId, 0.5)); + store.dispatch(runCellSetsClustering(experimentId, 0.5)); - expect(fetchCachedWork).toHaveBeenCalledTimes(1); - expect(fetchCachedWork).toHaveBeenCalledWith(experimentId, 30, { + expect(sendWork).toHaveBeenCalledTimes(1); + expect(sendWork).toHaveBeenCalledWith(experimentId, 30, { name: 'ClusterCells', cellSetName: 'Louvain clusters', type: 'louvain', diff --git a/src/components/data-processing/ConfigureEmbedding/CalculationConfig.jsx b/src/components/data-processing/ConfigureEmbedding/CalculationConfig.jsx index 5c04212198..dd9a89ddc0 100644 --- a/src/components/data-processing/ConfigureEmbedding/CalculationConfig.jsx +++ b/src/components/data-processing/ConfigureEmbedding/CalculationConfig.jsx @@ -14,7 +14,7 @@ import { saveProcessingSettings, } from '../../../redux/actions/experimentSettings'; -import updateCellSetsClustering from '../../../redux/actions/cellSets/updateCellSetsClustering'; +import runCellSetsClustering from '../../../redux/actions/cellSets/runCellSetsClustering'; import { loadEmbedding } from '../../../redux/actions/embedding'; import SliderWithInput from '../../SliderWithInput'; @@ -49,7 +49,7 @@ const CalculationConfig = (props) => { const { louvain: louvainSettings } = data?.clusteringSettings.methodSettings || {}; const debouncedCellSetClustering = useCallback( - _.debounce((resolution) => dispatch(updateCellSetsClustering(experimentId, resolution)), 1500), + _.debounce((resolution) => dispatch(runCellSetsClustering(experimentId, resolution)), 1500), [], ); @@ -231,7 +231,8 @@ const CalculationConfig = (props) => { The parameter is, in a sense, a guess about the number of close neighbors each cell has. In most implementations, perplexity defaults to 30. This focuses the attention of t-SNE on preserving the distances to its 30 nearest neighbors and puts virtually no weight on preserving distances to the remaining points. - The perplexity value has a complex effect on the resulting pictures.'> + The perplexity value has a complex effect on the resulting pictures.' + > @@ -247,7 +248,8 @@ const CalculationConfig = (props) => { onBlur={(e) => setLearningRate(e.target.value)} /> + If the learning rate is too low, most points may look compressed in a dense cloud with few outliers. usually in the range [10.0, 1000.0]' + > @@ -313,7 +315,6 @@ const CalculationConfig = (props) => { - {changes.embeddingSettings.method === 'umap' && renderUMAPSettings()} {changes.embeddingSettings.method === 'tsne' && renderTSNESettings()} diff --git a/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx b/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx index 1e99f43a56..c062978705 100644 --- a/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx +++ b/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx @@ -21,7 +21,6 @@ import { import PlotStyling from '../../plots/styling/PlotStyling'; import { filterCells } from '../../../utils/plotSpecs/generateEmbeddingCategoricalSpec'; -import { updateCellSetsClustering } from '../../../redux/actions/cellSets'; import { updateProcessingSettings } from '../../../redux/actions/experimentSettings'; import loadCellMeta from '../../../redux/actions/cellMeta'; import generateDataProcessingPlotUuid from '../../../utils/generateDataProcessingPlotUuid'; @@ -34,7 +33,11 @@ const ConfigureEmbedding = (props) => { const [plot, setPlot] = useState(null); const cellSets = useSelector((state) => state.cellSets); const cellMeta = useSelector((state) => state.cellMeta); - const { selectedConfigureEmbeddingPlot: selectedPlot } = useSelector((state) => state.experimentSettings.processing.meta); + + const { selectedConfigureEmbeddingPlot: selectedPlot } = useSelector( + (state) => state.experimentSettings.processing.meta, + ); + const filterName = 'configureEmbedding'; const router = useRouter(); @@ -43,11 +46,6 @@ const ConfigureEmbedding = (props) => { _.debounce((plotUuid) => dispatch(savePlotConfig(experimentId, plotUuid)), 2000), [], ); - const debouncedCellSetClustering = useCallback( - _.debounce((resolution) => dispatch(updateCellSetsClustering(experimentId, resolution)), 2000), - [], - ); - const continuousEmbeddingPlots = ['mitochondrialContent', 'doubletScores']; useEffect(() => { @@ -286,18 +284,9 @@ const ConfigureEmbedding = (props) => { && !cellSets.error && !cellSets.updateCellSetsClustering && selectedConfig - && plotData) { + && plotData + ) { setPlot(plots[selectedPlot].plot(selectedConfig, plotData)); - if (!selectedConfig.selectedCellSet) { return; } - - const propertiesArray = Object.keys(cellSets.properties); - const cellSetClusteringLength = propertiesArray.filter( - (cellSet) => cellSet === selectedConfig.selectedCellSet, - ).length; - - if (!cellSetClusteringLength) { - debouncedCellSetClustering(0.5); - } } }, [selectedConfig, cellSets, plotData]); diff --git a/src/components/data-processing/DataIntegration/DataIntegration.jsx b/src/components/data-processing/DataIntegration/DataIntegration.jsx index 95c33a6931..32f22ca4ef 100644 --- a/src/components/data-processing/DataIntegration/DataIntegration.jsx +++ b/src/components/data-processing/DataIntegration/DataIntegration.jsx @@ -3,19 +3,18 @@ import React, { } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { - Row, Col, Space, PageHeader, Collapse, Skeleton, Alert, + Row, Col, Space, PageHeader, Collapse, Alert, } from 'antd'; import _ from 'lodash'; import PropTypes from 'prop-types'; import { useRouter } from 'next/router'; import CalculationConfig from './CalculationConfig'; -import MiniPlot from '../../plots/MiniPlot'; - -import { updateCellSetsClustering } from '../../../redux/actions/cellSets'; - import PlotStyling from '../../plots/styling/PlotStyling'; +import MiniPlot from '../../plots/MiniPlot'; +import EmptyPlot from '../../plots/helpers/EmptyPlot'; + import { updatePlotConfig, loadPlotConfig, @@ -45,11 +44,6 @@ const DataIntegration = (props) => { _.debounce((plotUuid) => dispatch(savePlotConfig(experimentId, plotUuid)), 2000), [], ); - const debouncedCellSetClustering = useCallback( - _.debounce((resolution) => dispatch(updateCellSetsClustering(experimentId, resolution)), 2000), - [], - ); - const plots = { embedding: { title: 'Sample embedding', @@ -244,18 +238,9 @@ const DataIntegration = (props) => { && !cellSets.error && !cellSets.updateCellSetsClustering && selectedConfig - && plotData) { + && plotData + ) { setPlot(plots[selectedPlot].plot(selectedConfig, plotData, true)); - if (!selectedConfig.selectedCellSet) { return; } - - const propertiesArray = Object.keys(cellSets.properties); - const cellSetClusteringLength = propertiesArray.filter( - (cellSet) => cellSet === selectedConfig.selectedCellSet, - ).length; - - if (!cellSetClusteringLength) { - debouncedCellSetClustering(0.5); - } } }, [selectedConfig, cellSets, plotData, calculationConfig]); @@ -308,7 +293,7 @@ const DataIntegration = (props) => { if (!selectedConfig || disabledByConfigEmbedding) { return (
- +
); } @@ -348,7 +333,7 @@ const DataIntegration = (props) => { {plotObj.blockedByConfigureEmbedding && !configureEmbeddingFinished.current ? (
- +
) : ( diff --git a/src/redux/actions/cellSets/index.js b/src/redux/actions/cellSets/index.js index 129b53cccd..c3bb1b0835 100644 --- a/src/redux/actions/cellSets/index.js +++ b/src/redux/actions/cellSets/index.js @@ -1,5 +1,6 @@ import createCellSet from './createCellSet'; import deleteCellSet from './deleteCellSet'; +import runCellSetsClustering from './runCellSetsClustering'; import updateCellSetsClustering from './updateCellSetsClustering'; import loadCellSets from './loadCellSets'; @@ -13,6 +14,7 @@ import unhideAllCellSets from './unhideAllCellSets'; export { createCellSet, deleteCellSet, + runCellSetsClustering, updateCellSetsClustering, loadCellSets, saveCellSets, diff --git a/src/redux/actions/cellSets/runCellSetsClustering.js b/src/redux/actions/cellSets/runCellSetsClustering.js new file mode 100644 index 0000000000..467d4a25d9 --- /dev/null +++ b/src/redux/actions/cellSets/runCellSetsClustering.js @@ -0,0 +1,55 @@ +import { + CELL_SETS_ERROR, CELL_SETS_CLUSTERING_UPDATING, +} from '../../actionTypes/cellSets'; +import sendWork from '../../../utils/sendWork'; + +const REQUEST_TIMEOUT = 30; + +const runCellSetsClustering = (experimentId, resolution) => async (dispatch, getState) => { + const { + loading, error, + } = getState().cellSets; + + const { + backendStatus, + } = getState().experimentSettings; + + console.log('Running stuff3'); + + if (loading || error) { + return null; + } + + console.log('Running stuff2'); + + const body = { + name: 'ClusterCells', + cellSetName: 'Louvain clusters', + type: 'louvain', + cellSetKey: 'louvain', + config: { + resolution, + }, + }; + + console.log('Running stuff1'); + + dispatch({ + type: CELL_SETS_CLUSTERING_UPDATING, + }); + + try { + console.log('Running stuff'); + await sendWork(experimentId, REQUEST_TIMEOUT, body, backendStatus.status); + } catch (e) { + dispatch({ + type: CELL_SETS_ERROR, + payload: { + experimentId, + error: e, + }, + }); + } +}; + +export default runCellSetsClustering; diff --git a/src/redux/actions/cellSets/updateCellSetsClustering.js b/src/redux/actions/cellSets/updateCellSetsClustering.js index ba0d2e395b..82b37c8879 100644 --- a/src/redux/actions/cellSets/updateCellSetsClustering.js +++ b/src/redux/actions/cellSets/updateCellSetsClustering.js @@ -1,55 +1,16 @@ import { - CELL_SETS_ERROR, CELL_SETS_CLUSTERING_UPDATING, CELL_SETS_CLUSTERING_UPDATED, + CELL_SETS_ERROR, CELL_SETS_CLUSTERING_UPDATED, } from '../../actionTypes/cellSets'; -import { fetchCachedWork } from '../../../utils/cacheRequest'; -import saveCellSets from './saveCellSets'; - -const REQUEST_TIMEOUT = 30; - -const updateCellSetsClustering = (experimentId, resolution) => async (dispatch, getState) => { - const { - loading, error, - } = getState().cellSets; - - const { - backendStatus, - } = getState().experimentSettings; - - if (loading || error) { - return null; - } - - const body = { - name: 'ClusterCells', - cellSetName: 'Louvain clusters', - type: 'louvain', - cellSetKey: 'louvain', - config: { - resolution, - }, - }; - - dispatch({ - type: CELL_SETS_CLUSTERING_UPDATING, - }); +const updateCellSetsClustering = (experimentId, newCellSets) => async (dispatch) => { try { - const louvainSets = await fetchCachedWork( - experimentId, REQUEST_TIMEOUT, body, backendStatus.status, - ); - - const newCellSets = [ - louvainSets, - ]; - - await dispatch({ + dispatch({ type: CELL_SETS_CLUSTERING_UPDATED, payload: { experimentId, data: newCellSets, }, }); - dispatch(saveCellSets(experimentId)); } catch (e) { dispatch({ type: CELL_SETS_ERROR, diff --git a/src/utils/experimentUpdatesHandler.js b/src/utils/experimentUpdatesHandler.js index b0e1789ffd..f37b3b5308 100644 --- a/src/utils/experimentUpdatesHandler.js +++ b/src/utils/experimentUpdatesHandler.js @@ -1,14 +1,12 @@ import { updateProcessingSettings, updateBackendStatus } from '../redux/actions/experimentSettings'; import updatePlotData from '../redux/actions/componentConfig/updatePlotData'; -import { - CELL_SETS_CLUSTERING_UPDATED, CELL_SETS_ERROR, -} from '../redux/actionTypes/cellSets'; +import { updateCellSetsClustering } from '../redux/actions/cellSets'; const updateTypes = { QC: 'qc', GEM2S: 'gem2s', - DATA: 'data_update', + WORKER_DATA_UPDATE: 'workerDataUpdate', }; const experimentUpdatesHandler = (dispatch) => (experimentId, update) => { @@ -25,10 +23,10 @@ const experimentUpdatesHandler = (dispatch) => (experimentId, update) => { dispatch(updateBackendStatus(experimentId, update.status)); return onGEM2SUpdate(experimentId, update, dispatch); } - // this should be used to notify the UI that a request has changed and the UI is out-of-sync - case updateTypes.DATA: { + case updateTypes.WORKER_DATA_UPDATE: { return onWorkerUpdate(experimentId, update, dispatch); } + default: { console.log(`Error, unrecognized message type ${update.type}`); } @@ -66,23 +64,8 @@ const onWorkerUpdate = (experimentId, update, dispatch) => { const newCellSets = [ louvainSets, ]; - try { - dispatch({ - type: CELL_SETS_CLUSTERING_UPDATED, - payload: { - experimentId, - data: newCellSets, - }, - }); - } catch (e) { - dispatch({ - type: CELL_SETS_ERROR, - payload: { - experimentId, - error: e, - }, - }); - } + + dispatch(updateCellSetsClustering(experimentId, newCellSets)); } }; diff --git a/src/utils/sendWork.js b/src/utils/sendWork.js index d30131d681..58a7e882d4 100644 --- a/src/utils/sendWork.js +++ b/src/utils/sendWork.js @@ -21,9 +21,12 @@ const sendWork = async (experimentId, timeout, body, requestProps = {}) => { const authJWT = await getAuthJWT(); + const isOnlyForThisClient = body.name !== 'ClusterCells'; + const socketId = isOnlyForThisClient ? io.id : 'broadcast'; + const request = { uuid: requestUuid, - socketId: io.id, + socketId, experimentId, ...(authJWT && { Authorization: `Bearer ${authJWT}` }), timeout: timeoutDate, @@ -32,6 +35,12 @@ const sendWork = async (experimentId, timeout, body, requestProps = {}) => { }; io.emit('WorkRequest', request); + + // If it is not a single client request then it will be received + // in the ExperimentUpdates port instead of the WorkResponse one + // so we don't need to listen for it + if (!isOnlyForThisClient) { return; } + const responsePromise = new Promise((resolve, reject) => { io.on(`WorkResponse-${requestUuid}`, (res) => { const { response: { error } } = res;