@opencloning/ui 1.9.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/package.json +5 -6
- package/src/components/CloningSpeedDial.jsx +3 -5
- package/src/components/EditSequenceNameDialog.jsx +3 -5
- package/src/components/ExternalServicesStatusCheck.jsx +3 -5
- package/src/components/LoadCloningHistoryWrapper.jsx +5 -7
- package/src/components/assembler/Assembler.cy.jsx +184 -0
- package/src/components/assembler/Assembler.jsx +57 -11
- package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
- package/src/components/assembler/UploadPlasmidsButton.cy.jsx +88 -6
- package/src/components/assembler/UploadPlasmidsButton.jsx +137 -73
- package/src/components/assembler/assembler_utils.js +25 -12
- package/src/components/assembler/assembler_utils.test.js +40 -1
- package/src/components/assembler/useAssembler.js +24 -8
- package/src/components/assembler/usePlasmidsLogic.js +20 -2
- package/src/components/domesticate/DomesticateDialog.jsx +4 -5
- package/src/components/dummy/DummyInterface.js +6 -0
- package/src/components/form/EnzymeMultiSelect.jsx +3 -5
- package/src/components/navigation/MainAppBar.jsx +4 -2
- package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
- package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
- package/src/components/sources/CollectionSource.jsx +2 -2
- package/src/components/sources/Source.jsx +32 -32
- package/src/components/sources/SourceAnnotation.jsx +2 -2
- package/src/components/sources/SourceAssembly.jsx +7 -7
- package/src/components/sources/SourceDatabase.jsx +2 -2
- package/src/components/sources/SourceFile.jsx +2 -2
- package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
- package/src/components/sources/SourceGenomeRegion.jsx +29 -15
- package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
- package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
- package/src/components/sources/SourceManuallyTyped.jsx +2 -2
- package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
- package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
- package/src/components/sources/SourceRepositoryId.jsx +4 -4
- package/src/components/sources/SourceRestriction.jsx +2 -2
- package/src/components/sources/SourceReverseComplement.jsx +2 -2
- package/src/components/sources/SourceServerStaticFile.jsx +3 -3
- package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
- package/src/components/verification/VerificationFileDialog.jsx +12 -11
- package/src/hooks/index.js +1 -2
- package/src/hooks/useBackendHttpClient.js +17 -0
- package/src/hooks/useCloningHistoryLoader.js +5 -7
- package/src/hooks/useGatewaySites.js +3 -5
- package/src/hooks/useInitializeApp.js +3 -4
- package/src/hooks/useLoadDatabaseFile.js +2 -2
- package/src/hooks/useSequencingAlignment.js +4 -6
- package/src/hooks/useServerStaticFiles.js +2 -2
- package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
- package/src/hooks/useStoreEditor.js +2 -1
- package/src/hooks/useUrlParamsLoader.js +8 -5
- package/src/hooks/useValidateState.js +4 -6
- package/src/version.js +1 -1
- package/src/hooks/useBackendRoute.js +0 -23
- package/src/hooks/useHttpClient.js +0 -20
|
@@ -10,7 +10,7 @@ import { cloningActions } from '@opencloning/store/cloning';
|
|
|
10
10
|
import useStoreEditor from '../../hooks/useStoreEditor';
|
|
11
11
|
import LabelWithTooltip from '../form/LabelWithTooltip';
|
|
12
12
|
|
|
13
|
-
function SourcePCRorHybridization({ source, requestStatus,
|
|
13
|
+
function SourcePCRorHybridization({ source, requestStatus, sendSourceRequest }) {
|
|
14
14
|
// Represents a PCR if inputs != [], else is a oligo hybridization
|
|
15
15
|
|
|
16
16
|
const dispatch = useDispatch();
|
|
@@ -58,14 +58,14 @@ function SourcePCRorHybridization({ source, requestStatus, sendPostRequest }) {
|
|
|
58
58
|
if (!isPcr) {
|
|
59
59
|
requestData.source.input = [forwardPrimerId, reversePrimerId].map((id) => ({ sequence: id }));
|
|
60
60
|
const config = { params: { minimal_annealing: minimalAnnealingRef.current.value } };
|
|
61
|
-
|
|
61
|
+
sendSourceRequest({ endpoint: 'oligonucleotide_hybridization', requestData, config, source });
|
|
62
62
|
} else {
|
|
63
63
|
const config = { params: {
|
|
64
64
|
minimal_annealing: minimalAnnealingRef.current.value,
|
|
65
65
|
allowed_mismatches: allowedMismatchesRef.current.value,
|
|
66
66
|
} };
|
|
67
67
|
requestData.source.add_primer_features = addPrimerFeatures;
|
|
68
|
-
|
|
68
|
+
sendSourceRequest({ endpoint: 'pcr', requestData, config, source });
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
|
|
@@ -4,7 +4,7 @@ import { Alert } from '@mui/material';
|
|
|
4
4
|
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
5
5
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
6
6
|
|
|
7
|
-
function SourcePolymeraseExtension({ source, requestStatus,
|
|
7
|
+
function SourcePolymeraseExtension({ source, requestStatus, sendSourceRequest }) {
|
|
8
8
|
const { id: sourceId } = source;
|
|
9
9
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
10
10
|
const { overhang_crick_3prime, overhang_watson_3prime } = inputSequences[0];
|
|
@@ -16,7 +16,7 @@ function SourcePolymeraseExtension({ source, requestStatus, sendPostRequest }) {
|
|
|
16
16
|
sequences: inputSequences,
|
|
17
17
|
source: { id: sourceId, input: inputSequences.map((e) => ({ sequence: e.id })) },
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
sendSourceRequest({ endpoint: 'polymerase_extension', requestData, source });
|
|
20
20
|
};
|
|
21
21
|
// No need for MultipleOutputsSelector, since there is only one output
|
|
22
22
|
return (
|
|
@@ -3,10 +3,10 @@ import { IconButton, InputAdornment, InputLabel, MenuItem, TextField, FormContro
|
|
|
3
3
|
import { Clear as ClearIcon } from '@mui/icons-material';
|
|
4
4
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
5
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
6
|
-
import
|
|
6
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
7
7
|
import repositoryMetadata from './repositoryMetadata';
|
|
8
8
|
|
|
9
|
-
const httpClient =
|
|
9
|
+
const httpClient = getExternalHttpClient();
|
|
10
10
|
|
|
11
11
|
function validateRepositoryId(repositoryId, repository) {
|
|
12
12
|
switch (repository) {
|
|
@@ -243,7 +243,7 @@ function IndexJsonSelector({
|
|
|
243
243
|
|
|
244
244
|
// A component providing an interface for the user to type a repository ID
|
|
245
245
|
// and get a sequence
|
|
246
|
-
function SourceRepositoryId({ source, requestStatus,
|
|
246
|
+
function SourceRepositoryId({ source, requestStatus, sendSourceRequest }) {
|
|
247
247
|
const { id: sourceId } = source;
|
|
248
248
|
const [inputValue, setInputValue] = React.useState('');
|
|
249
249
|
const [repositoryType, setRepositoryType] = React.useState(source.type || '');
|
|
@@ -290,7 +290,7 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
|
|
|
290
290
|
extra.sequence_file_url = encodeURI(inputValue.url);
|
|
291
291
|
}
|
|
292
292
|
const requestData = { id: sourceId, ...extra, type: repositoryType };
|
|
293
|
-
|
|
293
|
+
sendSourceRequest({ endpoint: `repository_id/${repositoryMeta.slug}`, requestData, source });
|
|
294
294
|
};
|
|
295
295
|
const helperText = error || (repositoryMeta?.example && `Example: ${repositoryMeta.example}`);
|
|
296
296
|
return (
|
|
@@ -7,7 +7,7 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
|
7
7
|
// A component providing an interface for the user to perform a restriction reaction
|
|
8
8
|
// with one or more restriction enzymes, move between output fragments, and eventually
|
|
9
9
|
// select one as an output.
|
|
10
|
-
function SourceRestriction({ source, requestStatus,
|
|
10
|
+
function SourceRestriction({ source, requestStatus, sendSourceRequest }) {
|
|
11
11
|
const { id: sourceId } = source;
|
|
12
12
|
const [enzymes, setEnzymes] = React.useState([]);
|
|
13
13
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
@@ -19,7 +19,7 @@ function SourceRestriction({ source, requestStatus, sendPostRequest }) {
|
|
|
19
19
|
source: { id: sourceId, input: source.input },
|
|
20
20
|
sequences: inputSequences,
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
sendSourceRequest({ endpoint: 'restriction', requestData, source, config: { params: { restriction_enzymes: enzymes } } });
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -3,7 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
|
|
|
3
3
|
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
4
4
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
5
|
|
|
6
|
-
function SourceReverseComplement({ source, requestStatus,
|
|
6
|
+
function SourceReverseComplement({ source, requestStatus, sendSourceRequest }) {
|
|
7
7
|
const { id: sourceId, input } = source;
|
|
8
8
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
9
9
|
const onSubmit = (event) => {
|
|
@@ -13,7 +13,7 @@ function SourceReverseComplement({ source, requestStatus, sendPostRequest }) {
|
|
|
13
13
|
sequences: inputSequences,
|
|
14
14
|
source: { id: sourceId, input },
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
sendSourceRequest({ endpoint: 'reverse_complement', requestData, source });
|
|
17
17
|
};
|
|
18
18
|
// No need for MultipleOutputsSelector, since there is only one output
|
|
19
19
|
return (
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
3
3
|
import { Alert, LinearProgress } from '@mui/material';
|
|
4
4
|
|
|
5
|
-
function SourceServerStaticFile({ source, requestStatus,
|
|
5
|
+
function SourceServerStaticFile({ source, requestStatus, sendSourceRequest }) {
|
|
6
6
|
const onFileSelected = React.useCallback((file) => {
|
|
7
7
|
const requestData = new FormData();
|
|
8
8
|
requestData.append('file', file);
|
|
@@ -11,8 +11,8 @@ function SourceServerStaticFile({ source, requestStatus, sendPostRequest }) {
|
|
|
11
11
|
'content-type': 'multipart/form-data',
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
|
-
|
|
15
|
-
}, [
|
|
14
|
+
sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source });
|
|
15
|
+
}, [sendSourceRequest, source]);
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<>
|
|
@@ -4,6 +4,11 @@ import store from '@opencloning/store';
|
|
|
4
4
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
5
|
import { loadDataAndMount } from '../../../../../cypress/e2e/common_funcions_store';
|
|
6
6
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
7
|
+
import {
|
|
8
|
+
clearVerificationFileContents,
|
|
9
|
+
getVerificationFileContent,
|
|
10
|
+
setVerificationFileContent,
|
|
11
|
+
} from '@opencloning/utils/verificationFileStore';
|
|
7
12
|
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
8
13
|
import { Provider } from 'react-redux';
|
|
9
14
|
|
|
@@ -23,7 +28,7 @@ const base64str = 'aGVsbG8=';
|
|
|
23
28
|
|
|
24
29
|
describe('<VerificationFileDialog />', () => {
|
|
25
30
|
beforeEach(() => {
|
|
26
|
-
|
|
31
|
+
clearVerificationFileContents();
|
|
27
32
|
});
|
|
28
33
|
it('renders and calls setDialogOpen with false when clicking close button', () => {
|
|
29
34
|
// see: https://on.cypress.io/mounting-react
|
|
@@ -51,7 +56,7 @@ describe('<VerificationFileDialog />', () => {
|
|
|
51
56
|
it('displays existing files and can download and delete them', () => {
|
|
52
57
|
store.dispatch(setFiles(dummyFiles));
|
|
53
58
|
dummyFiles.forEach((file) => {
|
|
54
|
-
|
|
59
|
+
setVerificationFileContent(getVerificationFileName(file), base64str);
|
|
55
60
|
});
|
|
56
61
|
cy.mount(
|
|
57
62
|
<Provider store={store}>
|
|
@@ -69,16 +74,11 @@ describe('<VerificationFileDialog />', () => {
|
|
|
69
74
|
|
|
70
75
|
cy.get('[data-testid="DeleteIcon"]').first().click();
|
|
71
76
|
cy.get('table').contains('file2.txt');
|
|
72
|
-
cy.get('table').contains('file1.txt').should('not.exist')
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.should('be.null');
|
|
78
|
-
|
|
79
|
-
cy.window().its('sessionStorage')
|
|
80
|
-
.invoke('getItem', 'verification-1-file2.txt')
|
|
81
|
-
.should('not.be.null');
|
|
77
|
+
cy.get('table').contains('file1.txt').should('not.exist').then(() => {
|
|
78
|
+
// Check that the file is not in the verification file store
|
|
79
|
+
expect(getVerificationFileContent('verification-1-file1.txt')).to.be.null;
|
|
80
|
+
expect(getVerificationFileContent('verification-1-file2.txt')).to.not.be.null;
|
|
81
|
+
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it('can submit files and aligns them', () => {
|
|
@@ -105,9 +105,8 @@ describe('<VerificationFileDialog />', () => {
|
|
|
105
105
|
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
106
106
|
cy.expect(file.sequence_id).to.equal(2);
|
|
107
107
|
cy.expect(file.alignment).to.have.length(2);
|
|
108
|
-
// The file content is stored in
|
|
109
|
-
cy.
|
|
110
|
-
.invoke('getItem', getVerificationFileName(file))
|
|
108
|
+
// The file content is stored in the verification file store
|
|
109
|
+
cy.wrap(getVerificationFileContent(getVerificationFileName(file)))
|
|
111
110
|
.should('not.be.null', { timeout: 10000 });
|
|
112
111
|
|
|
113
112
|
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
@@ -133,9 +132,8 @@ describe('<VerificationFileDialog />', () => {
|
|
|
133
132
|
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
134
133
|
cy.expect(file.sequence_id).to.equal(2);
|
|
135
134
|
cy.expect(file.alignment).to.deep.equal(['A', 'C']);
|
|
136
|
-
// The file content is stored in
|
|
137
|
-
cy.
|
|
138
|
-
.invoke('getItem', getVerificationFileName(file))
|
|
135
|
+
// The file content is stored in the verification file store
|
|
136
|
+
cy.wrap(getVerificationFileContent(getVerificationFileName(file)))
|
|
139
137
|
.should('not.be.null', { timeout: 10000 });
|
|
140
138
|
});
|
|
141
139
|
});
|
|
@@ -187,9 +185,7 @@ describe('<VerificationFileDialog />', () => {
|
|
|
187
185
|
cy.contains('Request failed');
|
|
188
186
|
// It has not been added
|
|
189
187
|
cy.get('table').contains('BZO902-13409020-13409020.ab1').should('not.exist');
|
|
190
|
-
|
|
191
|
-
.invoke('getItem', 'verification-2-BZO902-13409020-13409020.ab1')
|
|
192
|
-
.should('be.null');
|
|
188
|
+
expect(getVerificationFileContent('verification-2-BZO902-13409020-13409020.ab1')).to.be.null;
|
|
193
189
|
|
|
194
190
|
// Connection error
|
|
195
191
|
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
@@ -21,12 +21,15 @@ import { isEqual } from 'lodash-es';
|
|
|
21
21
|
import { getTeselaJsonFromBase64, file2base64 } from '@opencloning/utils/readNwrite';
|
|
22
22
|
import SequencingFileRow from './SequencingFileRow';
|
|
23
23
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
24
|
-
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
25
24
|
import useStoreEditor from '../../hooks/useStoreEditor';
|
|
26
25
|
import LoadFromDatabaseButton from './LoadFromDatabaseButton';
|
|
27
26
|
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
28
|
-
import
|
|
27
|
+
import useBackendHttpClient from '../../hooks/useBackendHttpClient';
|
|
29
28
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
29
|
+
import {
|
|
30
|
+
getVerificationFileContent,
|
|
31
|
+
setVerificationFileContent,
|
|
32
|
+
} from '@opencloning/utils/verificationFileStore';
|
|
30
33
|
import ImportSequencingFilesInput from './ImportSequencingFilesInput';
|
|
31
34
|
|
|
32
35
|
const { addFile, removeFile: removeFileAction, removeFilesAssociatedToSequence, setMainSequenceId, setCurrentTab } = cloningActions;
|
|
@@ -43,9 +46,8 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
43
46
|
const dispatch = useDispatch();
|
|
44
47
|
const fileInputRef = useRef(null);
|
|
45
48
|
const { updateStoreEditor } = useStoreEditor();
|
|
46
|
-
const backendRoute = useBackendRoute();
|
|
47
49
|
const store = useStore();
|
|
48
|
-
const httpClient =
|
|
50
|
+
const httpClient = useBackendHttpClient();
|
|
49
51
|
|
|
50
52
|
const toggleMain = React.useCallback(() => {
|
|
51
53
|
dispatch(setMainSequenceId(id));
|
|
@@ -78,7 +80,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
78
80
|
const existingSequencingFilesInState = await Promise.all(store.getState().cloning.files
|
|
79
81
|
.filter((f) => f.sequence_id === id && f.file_type === 'Sequencing file')
|
|
80
82
|
.map(async (f) => {
|
|
81
|
-
const base64str =
|
|
83
|
+
const base64str = getVerificationFileContent(getVerificationFileName(f));
|
|
82
84
|
const trace = (await getTeselaJsonFromBase64(base64str, f.file_name)).sequence;
|
|
83
85
|
return { ...f, base64str, trace };
|
|
84
86
|
}));
|
|
@@ -94,7 +96,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
94
96
|
const allSequencingFiles = [...existingSequencingFilesInState, ...parsedSequencingFiles];
|
|
95
97
|
const alignments = [];
|
|
96
98
|
const traces = allSequencingFiles.map((f) => f.trace);
|
|
97
|
-
const resp = await httpClient.post(
|
|
99
|
+
const resp = await httpClient.post('align_sanger', { sequence, traces });
|
|
98
100
|
|
|
99
101
|
for (let i = 0; i < allSequencingFiles.length; i++) {
|
|
100
102
|
alignments.push({ ...allSequencingFiles[i], alignment: [resp.data[0], resp.data[i + 1]] });
|
|
@@ -107,10 +109,10 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
107
109
|
const { trace, base64str, ...rest } = alignment;
|
|
108
110
|
dispatch(addFile(rest));
|
|
109
111
|
});
|
|
110
|
-
alignments.forEach(({ base64str, file_name }) => {
|
|
112
|
+
alignments.forEach(({ base64str, file_name }) => { setVerificationFileContent(`verification-${id}-${file_name}`, base64str); });
|
|
111
113
|
});
|
|
112
114
|
setLoadingMessage('');
|
|
113
|
-
}, [id, dispatch, httpClient,
|
|
115
|
+
}, [id, dispatch, httpClient, store, sequence]);
|
|
114
116
|
|
|
115
117
|
const onFileChange = useCallback(async (files) => {
|
|
116
118
|
setLoadingMessage('Aligning...');
|
|
@@ -126,7 +128,6 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
126
128
|
|
|
127
129
|
const removeFile = useCallback((fileName) => {
|
|
128
130
|
dispatch(removeFileAction({ fileName, sequenceId: id }));
|
|
129
|
-
sessionStorage.removeItem(`verification-${id}-${fileName}`);
|
|
130
131
|
}, [id, dispatch]);
|
|
131
132
|
|
|
132
133
|
const handleClickUpload = useCallback(() => {
|
|
@@ -134,9 +135,9 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
134
135
|
}, [fileInputRef]);
|
|
135
136
|
|
|
136
137
|
const downloadFile = useCallback((fileName) => {
|
|
137
|
-
const base64Content =
|
|
138
|
+
const base64Content = getVerificationFileContent(`verification-${id}-${fileName}`);
|
|
138
139
|
if (!base64Content) {
|
|
139
|
-
setError(`File ${fileName} not found in
|
|
140
|
+
setError(`File ${fileName} not found in the verification file store`);
|
|
140
141
|
return;
|
|
141
142
|
}
|
|
142
143
|
const binaryString = atob(base64Content);
|
package/src/hooks/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { useConfig } from './useConfig.js';
|
|
2
2
|
export { default as useServerStaticFiles, localFilesHttpClient } from './useServerStaticFiles.js';
|
|
3
|
-
export { default as
|
|
4
|
-
export { default as useHttpClient } from './useHttpClient.js';
|
|
3
|
+
export { default as useBackendHttpClient } from './useBackendHttpClient.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getBackendHttpClient } from '@opencloning/utils/httpClients';
|
|
3
|
+
import { useConfig } from './useConfig';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Http client for the backend: urls are relative to config.backendUrl
|
|
7
|
+
* (e.g. httpClient.post('validate', body)), and requests can only reach the backend.
|
|
8
|
+
* For external urls use getExternalHttpClient() from @opencloning/utils/httpClients.
|
|
9
|
+
*/
|
|
10
|
+
export default function useBackendHttpClient() {
|
|
11
|
+
const { backendUrl, requiresBackendAuth = false } = useConfig();
|
|
12
|
+
|
|
13
|
+
return React.useMemo(
|
|
14
|
+
() => getBackendHttpClient(backendUrl, { authenticated: requiresBackendAuth }),
|
|
15
|
+
[backendUrl, requiresBackendAuth],
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -4,13 +4,12 @@ import { cloningActions } from '@opencloning/store/cloning';
|
|
|
4
4
|
import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
|
|
5
5
|
import { graftState, getGraftSequenceId, mergeStates } from '@opencloning/utils/network';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
loadFilesToVerificationStore,
|
|
8
8
|
parseHistoryFile,
|
|
9
9
|
updateVerificationFileNames,
|
|
10
10
|
} from '@opencloning/utils/readNwrite';
|
|
11
|
-
import useBackendRoute from './useBackendRoute';
|
|
12
11
|
import useCloningAlerts from './useCloningAlerts';
|
|
13
|
-
import
|
|
12
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
14
13
|
import useValidateState from './useValidateState';
|
|
15
14
|
|
|
16
15
|
const {
|
|
@@ -31,9 +30,8 @@ export default function useCloningHistoryLoader() {
|
|
|
31
30
|
const store = useStore();
|
|
32
31
|
const validateState = useValidateState();
|
|
33
32
|
const { addAlert } = useCloningAlerts();
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const snapgeneUrl = backendRoute('read_snapgene_history');
|
|
33
|
+
const httpClient = useBackendHttpClient();
|
|
34
|
+
const snapgeneUrl = 'read_snapgene_history';
|
|
37
35
|
|
|
38
36
|
const applyCloningStrategy = React.useCallback(async (cloningStrategy, options = {}) => {
|
|
39
37
|
const {
|
|
@@ -72,7 +70,7 @@ export default function useCloningHistoryLoader() {
|
|
|
72
70
|
dispatch(setCloningState(newState));
|
|
73
71
|
});
|
|
74
72
|
|
|
75
|
-
await
|
|
73
|
+
await loadFilesToVerificationStore(verificationFiles, idShift);
|
|
76
74
|
} catch (e) {
|
|
77
75
|
console.error(e);
|
|
78
76
|
dispatch(setCloningState(prevState));
|
|
@@ -2,16 +2,14 @@ import React, { useCallback, useState } from 'react';
|
|
|
2
2
|
import { useStore } from 'react-redux';
|
|
3
3
|
import error2String from '@opencloning/utils/error2String';
|
|
4
4
|
import { formatGatewaySites } from '@opencloning/store/cloning_utils';
|
|
5
|
-
import
|
|
6
|
-
import useHttpClient from './useHttpClient';
|
|
5
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
7
6
|
|
|
8
7
|
export default function useGatewaySites({ target, greedy }) {
|
|
9
8
|
const store = useStore();
|
|
10
9
|
const [requestStatus, setRequestStatus] = useState({ status: null, message: '' });
|
|
11
10
|
const [connectAttempt, setConnectAttempt] = useState(0);
|
|
12
11
|
const [sites, setSites] = useState([]);
|
|
13
|
-
const
|
|
14
|
-
const httpClient = useHttpClient();
|
|
12
|
+
const httpClient = useBackendHttpClient();
|
|
15
13
|
|
|
16
14
|
const attemptAgain = useCallback(() => {
|
|
17
15
|
setConnectAttempt((p) => p + 1);
|
|
@@ -20,7 +18,7 @@ export default function useGatewaySites({ target, greedy }) {
|
|
|
20
18
|
React.useEffect(() => {
|
|
21
19
|
async function getSitesInTarget() {
|
|
22
20
|
setRequestStatus({ status: 'loading', message: 'loading' });
|
|
23
|
-
const url =
|
|
21
|
+
const url = 'annotation/get_gateway_sites';
|
|
24
22
|
const state = store.getState();
|
|
25
23
|
const sequence = state.cloning.sequences.find((seq) => seq.id === target);
|
|
26
24
|
try {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { clearVerificationFileContents } from '@opencloning/utils/verificationFileStore';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hook to initialize application-level concerns
|
|
5
|
-
* - Clears
|
|
6
|
+
* - Clears the in-memory verification file store
|
|
6
7
|
*/
|
|
7
8
|
export default function useInitializeApp() {
|
|
8
9
|
|
|
9
10
|
useEffect(() => {
|
|
10
|
-
|
|
11
|
-
// eslint-disable-next-line no-undef
|
|
12
|
-
sessionStorage.clear();
|
|
11
|
+
clearVerificationFileContents();
|
|
13
12
|
}, []);
|
|
14
13
|
}
|
|
15
14
|
|
|
@@ -6,7 +6,7 @@ import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
|
6
6
|
import useDatabase from './useDatabase';
|
|
7
7
|
import { useStore } from 'react-redux';
|
|
8
8
|
|
|
9
|
-
export default function useLoadDatabaseFile({ source,
|
|
9
|
+
export default function useLoadDatabaseFile({ source, sendSourceRequest, setHistoryFileError }) {
|
|
10
10
|
const validateState = useValidateState();
|
|
11
11
|
const store = useStore();
|
|
12
12
|
const database = useDatabase();
|
|
@@ -88,7 +88,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
88
88
|
},
|
|
89
89
|
};
|
|
90
90
|
const modifySource = (s) => ({ ...s, database_id: databaseId });
|
|
91
|
-
|
|
91
|
+
sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source, modifySource });
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
return { loadDatabaseFile };
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { useMutation } from '@tanstack/react-query';
|
|
3
3
|
import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig } from '@opencloning/utils/alignmentUtils';
|
|
4
|
-
import
|
|
5
|
-
import useHttpClient from './useHttpClient';
|
|
4
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
6
5
|
|
|
7
6
|
export default function useSequencingAlignment({ onError } = {}) {
|
|
8
|
-
const
|
|
9
|
-
const httpClient = useHttpClient();
|
|
7
|
+
const httpClient = useBackendHttpClient();
|
|
10
8
|
|
|
11
9
|
const fetchAlignments = useCallback(async ({ sequenceModel, sequenceData, parsedFiles }) => {
|
|
12
10
|
// parsedFiles: [{ fileName, fileContent }] where fileContent is getTeselaJsonFromBase64 output
|
|
13
11
|
const traces = parsedFiles.map((f) => f.fileContent.sequence);
|
|
14
|
-
const resp = await httpClient.post(
|
|
12
|
+
const resp = await httpClient.post('align_sanger', { sequence: sequenceModel, traces });
|
|
15
13
|
|
|
16
14
|
const referenceTrack = buildReferenceTrack(sequenceData, resp.data[0]);
|
|
17
15
|
const otherTracks = parsedFiles.map((f, i) => {
|
|
@@ -20,7 +18,7 @@ export default function useSequencingAlignment({ onError } = {}) {
|
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
return buildAlignmentConfig(sequenceModel.id, sequenceData, [referenceTrack, ...otherTracks]);
|
|
23
|
-
}, [httpClient
|
|
21
|
+
}, [httpClient]);
|
|
24
22
|
|
|
25
23
|
return useMutation({ mutationFn: fetchAlignments, onError });
|
|
26
24
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
3
3
|
import { useConfig } from '../providers';
|
|
4
4
|
import useRequestForEffect from './useRequestForEffect';
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export const localFilesHttpClient =
|
|
7
|
+
export const localFilesHttpClient = getExternalHttpClient();
|
|
8
8
|
|
|
9
9
|
function sequencesToCategories(sequences) {
|
|
10
10
|
const allCategories = sequences
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import error2String from '@opencloning/utils/error2String';
|
|
3
|
-
import useBackendRoute from './useBackendRoute';
|
|
4
3
|
import useCloningAlerts from './useCloningAlerts';
|
|
5
|
-
import
|
|
4
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
6
5
|
|
|
7
|
-
export default function
|
|
6
|
+
export default function useSourceRequest() {
|
|
8
7
|
const [requestStatus, setRequestStatus] = useState({ status: null, message: '' });
|
|
9
8
|
const [sources, setSources] = useState([]);
|
|
10
9
|
const [sequences, setSequences] = useState([]);
|
|
11
|
-
const
|
|
12
|
-
const httpClient = useHttpClient();
|
|
10
|
+
const httpClient = useBackendHttpClient();
|
|
13
11
|
const { addAlert } = useCloningAlerts();
|
|
14
12
|
|
|
15
|
-
const
|
|
13
|
+
const sendSourceRequest = useCallback(async ({ endpoint, requestData, config = {}, source: { output }, modifySource = (s) => s }) => {
|
|
16
14
|
setRequestStatus({ status: 'loading', message: 'loading' });
|
|
17
15
|
|
|
18
16
|
// Url built like this in case trailing slash
|
|
19
|
-
const url =
|
|
17
|
+
const url = endpoint;
|
|
20
18
|
// paramsSerializer: { indexes: null } is to correctly serialize arrays in the URL
|
|
21
19
|
const fullConfig = { ...config, paramsSerializer: { indexes: null } };
|
|
22
20
|
try {
|
|
@@ -50,5 +48,5 @@ export default function useBackendAPI() {
|
|
|
50
48
|
}
|
|
51
49
|
}, []);
|
|
52
50
|
|
|
53
|
-
return { requestStatus, sources, sequences,
|
|
51
|
+
return { requestStatus, sources, sequences, sendSourceRequest };
|
|
54
52
|
}
|
|
@@ -2,6 +2,7 @@ import { useStore } from 'react-redux';
|
|
|
2
2
|
import { updateEditor, addAlignment } from '@teselagen/ove';
|
|
3
3
|
import { getPCRPrimers } from '@opencloning/store/cloning_utils';
|
|
4
4
|
import { getTeselaJsonFromBase64 } from '@opencloning/utils/readNwrite';
|
|
5
|
+
import { getVerificationFileContent } from '@opencloning/utils/verificationFileStore';
|
|
5
6
|
import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig, removePanelFromShown, updatePanelsShownWithAlignment } from '@opencloning/utils/alignmentUtils';
|
|
6
7
|
|
|
7
8
|
export default function useStoreEditor() {
|
|
@@ -31,7 +32,7 @@ export default function useStoreEditor() {
|
|
|
31
32
|
if (alignmentFiles.length > 0) {
|
|
32
33
|
const referenceTrack = buildReferenceTrack(sequenceData, alignmentFiles[0].alignment[0]);
|
|
33
34
|
const otherTracks = await Promise.all(alignmentFiles.map(async (aln) => {
|
|
34
|
-
const fileContent = await getTeselaJsonFromBase64(
|
|
35
|
+
const fileContent = await getTeselaJsonFromBase64(getVerificationFileContent(`verification-${id}-${aln.file_name}`), aln.file_name);
|
|
35
36
|
return buildAlignmentTrack(fileContent, aln);
|
|
36
37
|
}));
|
|
37
38
|
const alignmentConfig = buildAlignmentConfig(id, sequenceData, [referenceTrack, ...otherTracks]);
|
|
@@ -4,11 +4,15 @@ import { cloningActions } from '@opencloning/store/cloning';
|
|
|
4
4
|
import useDatabase from './useDatabase';
|
|
5
5
|
import useLoadDatabaseFile from './useLoadDatabaseFile';
|
|
6
6
|
import useCloningAlerts from './useCloningAlerts';
|
|
7
|
-
import useHttpClient from './useHttpClient';
|
|
8
7
|
import useValidateState from './useValidateState';
|
|
9
8
|
import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
10
9
|
import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
|
|
11
10
|
import { formatTemplate } from '@opencloning/utils/readNwrite';
|
|
11
|
+
import { encodePathSegment } from '@opencloning/utils/urlUtils';
|
|
12
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
13
|
+
|
|
14
|
+
// For public urls (not the backend)
|
|
15
|
+
const httpClient = getExternalHttpClient();
|
|
12
16
|
|
|
13
17
|
const { updateSource } = cloningActions;
|
|
14
18
|
|
|
@@ -21,9 +25,8 @@ export default function useUrlParamsLoader() {
|
|
|
21
25
|
const database = useDatabase();
|
|
22
26
|
const { addAlert } = useCloningAlerts();
|
|
23
27
|
const setHistoryFileError = (e) => addAlert({ message: e, severity: 'error' });
|
|
24
|
-
const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 },
|
|
28
|
+
const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendSourceRequest: null, setHistoryFileError });
|
|
25
29
|
const validateState = useValidateState();
|
|
26
|
-
const httpClient = useHttpClient();
|
|
27
30
|
const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
|
|
28
31
|
|
|
29
32
|
useEffect(() => {
|
|
@@ -46,7 +49,7 @@ export default function useUrlParamsLoader() {
|
|
|
46
49
|
}
|
|
47
50
|
} else if (urlParams.source === 'example' && urlParams.example) {
|
|
48
51
|
try {
|
|
49
|
-
const url = `${import.meta.env.BASE_URL}examples/${urlParams.example}`;
|
|
52
|
+
const url = `${import.meta.env.BASE_URL}examples/${encodePathSegment(urlParams.example)}`;
|
|
50
53
|
const fileName = urlParams.example;
|
|
51
54
|
if (fileName.endsWith('.zip')) {
|
|
52
55
|
const resp = await httpClient.get(url, { responseType: 'blob' });
|
|
@@ -75,7 +78,7 @@ export default function useUrlParamsLoader() {
|
|
|
75
78
|
} else if (urlParams.source === 'template' && urlParams.template && urlParams.key) {
|
|
76
79
|
try {
|
|
77
80
|
const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
|
|
78
|
-
const url = `${baseUrl}/processed/${urlParams.key}/templates/${urlParams.template}`;
|
|
81
|
+
const url = `${baseUrl}/processed/${encodePathSegment(urlParams.key)}/templates/${encodePathSegment(urlParams.template)}`;
|
|
79
82
|
const { data } = await httpClient.get(url);
|
|
80
83
|
const validatedData = await validateState(data);
|
|
81
84
|
const newState = formatTemplate(validatedData, url);
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import useCloningAlerts from './useCloningAlerts';
|
|
3
|
-
import
|
|
4
|
-
import useHttpClient from './useHttpClient';
|
|
3
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
5
4
|
|
|
6
5
|
export default function useValidateState() {
|
|
7
|
-
const backendRoute = useBackendRoute();
|
|
8
6
|
const { addAlert } = useCloningAlerts();
|
|
9
|
-
const httpClient =
|
|
7
|
+
const httpClient = useBackendHttpClient();
|
|
10
8
|
|
|
11
9
|
const validateState = React.useCallback(async (newState) => {
|
|
12
10
|
// Returns null if the state is valid, otherwise the new updated state
|
|
13
11
|
// It also adds alerts
|
|
14
12
|
try {
|
|
15
|
-
const response = await httpClient.post(
|
|
13
|
+
const response = await httpClient.post('validate', newState);
|
|
16
14
|
if (response.data !== null) {
|
|
17
15
|
response.headers['x-warning'].split(';').forEach((warning) => {
|
|
18
16
|
addAlert({
|
|
@@ -37,7 +35,7 @@ export default function useValidateState() {
|
|
|
37
35
|
}
|
|
38
36
|
return newState;
|
|
39
37
|
}
|
|
40
|
-
}, [addAlert,
|
|
38
|
+
}, [addAlert, httpClient]);
|
|
41
39
|
|
|
42
40
|
return validateState;
|
|
43
41
|
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version placeholder - replaced at publish time via prepack script
|
|
2
|
-
export const version = "1.
|
|
2
|
+
export const version = "2.1.0";
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { useConfig } from './useConfig';
|
|
2
|
-
|
|
3
|
-
export default function useBackendRoute() {
|
|
4
|
-
const { backendUrl: configBackendUrl } = useConfig();
|
|
5
|
-
|
|
6
|
-
if (!configBackendUrl) {
|
|
7
|
-
return () => {};
|
|
8
|
-
}
|
|
9
|
-
const backendUrl = configBackendUrl.endsWith('/') ? configBackendUrl : `${configBackendUrl}/`;
|
|
10
|
-
|
|
11
|
-
return function backendRoute(path) {
|
|
12
|
-
if (!backendUrl) {
|
|
13
|
-
throw new Error('Backend URL not set');
|
|
14
|
-
}
|
|
15
|
-
// console.log(new URL('/api', window.location.origin).href);
|
|
16
|
-
// console.log(new URL(import.meta.env.VITE_REACT_APP_BACKEND_URL, window.location.origin).href);
|
|
17
|
-
// This handles both the case where the backend url is absolute and the case where it is relative
|
|
18
|
-
const backendRoot = new URL(backendUrl, window.location.origin).href;
|
|
19
|
-
// Remove trailing slash from path
|
|
20
|
-
const sanitizedPath = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
21
|
-
return new URL(sanitizedPath, backendRoot).href;
|
|
22
|
-
};
|
|
23
|
-
}
|