Skip to content

Commit

Permalink
Merge branch 'master' into send-embedding-method
Browse files Browse the repository at this point in the history
  • Loading branch information
saracastel authored Nov 28, 2023
2 parents 7175c4a + 83d3ddd commit 546ee71
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import '__test__/test-utils/setupTests';
import { screen, render, waitFor } from '@testing-library/react';
import {
screen, render, waitFor, within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import _ from 'lodash';
import fake from '__test__/test-utils/constants';
Expand Down Expand Up @@ -204,4 +206,31 @@ describe('DataIntegration', () => {
// The legend alert plot text should appear
expect(screen.getByText(/We have hidden the plot legend, because it is too large and it interferes with the display of the plot/)).toBeInTheDocument();
});

it('Renders the elbow plot by default when the experiment is single sample', async () => {
// Remove all sample other than the first one.
// This way, getIsUnisample() will detect the exp as unisample
const unisampleCellSetsData = _.cloneDeep(cellSetsData);
_.find(unisampleCellSetsData.cellSets, { key: 'sample' }).children.splice(1);

const mockSingleSampleApiResponses = {
...generateDefaultMockAPIResponses(fake.EXPERIMENT_ID),
[`experiments/${fake.EXPERIMENT_ID}/cellSets$`]: () => promiseResponse(
JSON.stringify(unisampleCellSetsData),
),
};

fetchMock
.mockReset()
.mockIf(/.*/, mockAPI(mockSingleSampleApiResponses));

storeState = makeStore();
await storeState.dispatch(loadBackendStatus(fake.EXPERIMENT_ID));
await storeState.dispatch(loadProcessingSettings(fake.EXPERIMENT_ID));
await storeState.dispatch(loadCellSets(fake.EXPERIMENT_ID));

await renderDataIntegration(storeState);

expect(screen.getByRole('radio', { name: 'Elbow plot showing principal components' })).toBeChecked();
});
});
14 changes: 14 additions & 0 deletions src/__test__/redux/reducers/samplesReducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,4 +398,18 @@ describe('samplesReducer', () => {

expect(newState).toMatchSnapshot();
});

it('Sample file update doesnt change anything if the sample no longer exists', () => {
const newState = samplesReducer(initialState, {
type: SAMPLES_FILE_UPDATE,
payload: {
sampleUuid: mockUuid1,
fileName,
fileDiff: mockFile,
lastModified: 'newLastModified',
},
});

expect(newState).toEqual(initialState);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
{
Expand All @@ -243,6 +244,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
{
Expand All @@ -252,6 +254,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
]
Expand Down Expand Up @@ -401,6 +404,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
{
Expand All @@ -410,6 +414,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
{
Expand All @@ -419,6 +424,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl",
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl1",
},
{
Expand All @@ -18,6 +19,7 @@ exports[`processUpload Uploads and updates redux correctly when there are no err
},
"method": "put",
"onUploadProgress": [Function],
"signal": AbortSignal {},
"url": "theSignedUrl2",
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'antd';
import SelectData from 'components/plots/styling/embedding-continuous/SelectData';

import { isUnisample } from 'utils/experimentPredicates';
import { getIsUnisample } from 'utils/experimentPredicates';

import CategoricalEmbeddingPlot from 'components/plots/CategoricalEmbeddingPlot';
import ContinuousEmbeddingPlot from 'components/plots/ContinuousEmbeddingPlot';
Expand Down Expand Up @@ -135,11 +135,11 @@ const ConfigureEmbedding = (props) => {
const { loading, data: plotData, error } = cellMeta[colouring];
const modifiedConfig = {
...config,
axes:{
axes: {
...config.axes,
yAxisText: config.axes.yAxisText || plotColouring,
}
}
},
};
return (
<ViolinFilterPlot
experimentId={experimentId}
Expand Down Expand Up @@ -406,7 +406,7 @@ const ConfigureEmbedding = (props) => {
);
}

if (plotColouring === 'sample' && cellSets.accessible && isUnisample(cellSets.hierarchy)
if (plotColouring === 'sample' && cellSets.accessible && getIsUnisample(cellSets.hierarchy)
) {
return (
<center>
Expand Down
15 changes: 10 additions & 5 deletions src/components/data-processing/DataIntegration/DataIntegration.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {
useState, useEffect, useRef, useCallback,
useState, useEffect, useRef, useCallback, useMemo,
} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
Expand All @@ -24,7 +24,7 @@ import ElbowPlot from 'components/plots/ElbowPlot';
import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid';
import EmptyPlot from 'components/plots/helpers/EmptyPlot';
import PlotStyling from 'components/plots/styling/PlotStyling';
import { isUnisample } from 'utils/experimentPredicates';
import { getIsUnisample } from 'utils/experimentPredicates';
import PlotLegendAlert, { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert';
import CalculationConfig from './CalculationConfig';

Expand All @@ -33,9 +33,14 @@ const DataIntegration = (props) => {
const {
experimentId, onConfigChange, stepDisabled, stepHadErrors, disableDataIntegration,
} = props;
const [selectedPlot, setSelectedPlot] = useState('embedding');
const [plot, setPlot] = useState(null);

const cellSets = useSelector(getCellSets());

const isUnisample = useMemo(() => getIsUnisample(cellSets.hierarchy));

const [selectedPlot, setSelectedPlot] = useState(isUnisample ? 'elbow' : 'embedding');
const [plot, setPlot] = useState(null);

const filterName = 'dataIntegration';
const configureEmbeddingFilterName = 'configureEmbedding';

Expand Down Expand Up @@ -275,7 +280,7 @@ const DataIntegration = (props) => {
);
}

if ((selectedPlot === 'embedding' || selectedPlot === 'frequency') && cellSets.accessible && isUnisample(cellSets.hierarchy)
if ((selectedPlot === 'embedding' || selectedPlot === 'frequency') && cellSets.accessible && isUnisample
) {
return (
<center>
Expand Down
5 changes: 4 additions & 1 deletion src/redux/actions/samples/createSampleFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const createSampleFile = (
sampleId,
type,
fileForApiV1,
abortController,
) => async (dispatch) => {
const updatedAt = dayjs().toISOString();

Expand All @@ -30,8 +31,10 @@ const createSampleFile = (
lastModified: updatedAt,
fileName: fileNameForApiV1[type],
fileDiff: {
upload: { status: UploadStatus.UPLOADING },
...fileForApiV1,
upload: {
status: UploadStatus.UPLOADING, progress: 0, abortController,
},
},
},
});
Expand Down
13 changes: 10 additions & 3 deletions src/redux/actions/samples/createSamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { defaultSampleOptions, sampleTemplate } from 'redux/reducers/samples/ini
import { sampleTech } from 'utils/constants';
import UploadStatus from 'utils/upload/UploadStatus';

import fileNameForApiV1 from 'utils/upload/fileNameForApiV1';
import getFileTypeV2 from 'utils/getFileTypeV2';

// If the sample name of new samples coincides with already existing
// ones we should not create new samples,
// just reuse their sampleIds and upload the new files
Expand Down Expand Up @@ -115,9 +118,13 @@ const createSamples = (
options,
metadata: experiment?.metadataKeys
.reduce((acc, curr) => ({ ...acc, [curr]: METADATA_DEFAULT_VALUE }), {}) || {},
files: Object.values(files).reduce(((acc, curr) => (
{ ...acc, [curr.name]: { upload: { status: UploadStatus.UPLOADING } } }
)), {}),
files: Object.values(files).reduce(((acc, curr) => {
const fileType = fileNameForApiV1[getFileTypeV2(curr.name, sampleTechnology)];

return (
{ ...acc, [fileType]: { upload: { status: UploadStatus.UPLOADING } } }
);
}), {}),
}));

dispatch({
Expand Down
14 changes: 5 additions & 9 deletions src/redux/actions/samples/deleteSamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ import endUserMessages from 'utils/endUserMessages';
import fetchAPI from 'utils/http/fetchAPI';
import handleError from 'utils/http/handleError';

const cancelUploads = async (files) => {
const promises = Object.values(files).map(({ upload }) => {
if (upload?.amplifyPromise) {
// return Storage.cancel(upload.amplifyPromise);
}
return Promise.resolve();
const cancelUploads = (files) => {
Object.values(files).forEach((file) => {
// eslint-disable-next-line no-unused-expressions
file.upload?.abortController?.abort();
});

return Promise.all(promises);
};

const deleteSamples = (
Expand All @@ -34,7 +30,7 @@ const deleteSamples = (
acc[samples[sampleUuid].experimentId] = [];
}

await cancelUploads(files);
cancelUploads(files);

return {
...acc,
Expand Down
6 changes: 6 additions & 0 deletions src/redux/reducers/samples/samplesFileUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const samplesFileUpdate = (state, action) => {
sampleUuid, fileName, fileDiff, lastModified,
} = action.payload;

// There's a possible race condition where a file update can reach this place
// after a sample is deleted and there's a crash. This check is in place to avoid that error.
if (_.isNil(state[sampleUuid])) {
return state;
}

const oldFile = state[sampleUuid].files?.[fileName];
let newFile = fileDiff;

Expand Down
4 changes: 2 additions & 2 deletions src/utils/experimentPredicates.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const isUnisample = (hierarchy) => hierarchy.find((rootNode) => rootNode.key === 'sample')?.children?.length === 1;
const getIsUnisample = (hierarchy) => hierarchy.find((rootNode) => rootNode.key === 'sample')?.children?.length === 1;

// eslint-disable-next-line import/prefer-default-export
export { isUnisample };
export { getIsUnisample };
18 changes: 15 additions & 3 deletions src/utils/upload/processMultipartUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import axios from 'axios';
const FILE_CHUNK_SIZE = 10000000;
const MAX_RETRIES = 2;

const putPartInS3 = async (blob, signedUrl, onUploadProgress, currentRetry = 0) => {
const putPartInS3 = async (
blob, signedUrl, onUploadProgress, currentRetry = 0, abortController = null,
) => {
try {
return await axios.request({
method: 'put',
data: blob,
url: signedUrl,
signal: abortController.signal,
headers: {
'Content-Type': 'application/octet-stream',
},
Expand All @@ -23,7 +26,9 @@ const putPartInS3 = async (blob, signedUrl, onUploadProgress, currentRetry = 0)
}
};

const processMultipartUpload = async (file, signedUrls, createOnUploadProgressForPart) => {
const processMultipartUpload = async (
file, signedUrls, createOnUploadProgressForPart, abortController,
) => {
const promises = [];

signedUrls.forEach((signedUrl, index) => {
Expand All @@ -33,7 +38,14 @@ const processMultipartUpload = async (file, signedUrls, createOnUploadProgressFo
? file.fileObject.slice(start, end)
: file.fileObject.slice(start);

const req = putPartInS3(blob, signedUrl, createOnUploadProgressForPart(index));
const req = putPartInS3(
blob,
signedUrl,
createOnUploadProgressForPart(index),
0,
abortController,
);

promises.push(req);
});

Expand Down
12 changes: 8 additions & 4 deletions src/utils/upload/processUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import endUserMessages from 'utils/endUserMessages';
import pushNotificationMessage from 'utils/pushNotificationMessage';

const prepareAndUploadFileToS3 = async (
file, uploadUrlParams, type, onStatusUpdate = () => { },
file, uploadUrlParams, type, onStatusUpdate = () => { }, abortController = null,
) => {
let parts = null;
const { signedUrls, uploadId, fileId } = uploadUrlParams;
Expand All @@ -34,7 +34,9 @@ const prepareAndUploadFileToS3 = async (
onStatusUpdate(UploadStatus.UPLOADING, percentProgress);
};
try {
parts = await processMultipartUpload(file, signedUrls, createOnUploadProgressForPart);
parts = await processMultipartUpload(
file, signedUrls, createOnUploadProgressForPart, abortController,
);
} catch (e) {
onStatusUpdate(UploadStatus.UPLOAD_ERROR);
return;
Expand Down Expand Up @@ -64,6 +66,8 @@ const prepareAndUploadFileToS3 = async (
const createAndUploadSampleFile = async (file, experimentId, sampleId, dispatch, selectedTech) => {
const fileType = getFileTypeV2(file.name, selectedTech);

const abortController = new AbortController();

let sampleFileId;

try {
Expand All @@ -73,6 +77,7 @@ const createAndUploadSampleFile = async (file, experimentId, sampleId, dispatch,
sampleId,
fileType,
file,
abortController,
),
);
} catch (e) {
Expand Down Expand Up @@ -114,8 +119,7 @@ const createAndUploadSampleFile = async (file, experimentId, sampleId, dispatch,
);

const uploadUrlParams = { signedUrls, uploadId, fileId: sampleFileId };

await prepareAndUploadFileToS3(file, uploadUrlParams, 'sample', updateSampleFileUploadProgress);
await prepareAndUploadFileToS3(file, uploadUrlParams, 'sample', updateSampleFileUploadProgress, abortController);
} catch (e) {
dispatch(updateSampleFileUpload(
experimentId, sampleId, fileType, UploadStatus.UPLOAD_ERROR,
Expand Down

0 comments on commit 546ee71

Please sign in to comment.