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;