@opencloning/ui 1.3.3 → 1.4.1

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.
@@ -0,0 +1,466 @@
1
+ import React from 'react';
2
+ import { ConfigProvider } from '../../providers/ConfigProvider';
3
+ import { localFilesHttpClient } from '../../hooks/useServerStaticFiles';
4
+ import ServerStaticFileSelect from './ServerStaticFileSelect';
5
+
6
+ // A common intercept does not work here, because we are requesting
7
+ // static files, and cypress intercepts don't work.
8
+
9
+ const config = {
10
+ backendUrl: 'http://localhost:8000',
11
+ staticContentPath: 'collection',
12
+ showAppBar: false,
13
+ noExternalRequests: false,
14
+ enableAssembler: true,
15
+ enablePlannotate: false,
16
+ };
17
+
18
+ export const dummyIndex = {
19
+ sequences: [
20
+ {
21
+ name: 'Example sequence 1',
22
+ path: 'example.fa',
23
+ categories: ['Test category'],
24
+ },
25
+ {
26
+ name: 'Example sequence 2',
27
+ path: 'example2.gb',
28
+ categories: ['Test category2'],
29
+ },
30
+ {
31
+ name: 'Example sequence 3',
32
+ path: 'example3.fa',
33
+ categories: ['Test category'],
34
+ },
35
+ ],
36
+ syntaxes: [
37
+ {
38
+ name: 'Example syntax 1',
39
+ path: 'example.json',
40
+ },
41
+ {
42
+ name: 'Example syntax 2',
43
+ path: 'example2.json',
44
+ },
45
+ ],
46
+ };
47
+
48
+ describe('<ServerStaticFileSelect />', () => {
49
+ it('loads index, fetches file and calls onFileSelected', () => {
50
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
51
+ if (url.endsWith('/index.json')) {
52
+ return Promise.resolve({
53
+ data: dummyIndex,
54
+ });
55
+ }
56
+ if (url.endsWith('/example.fa')) {
57
+ return Promise.resolve({ data: 'ATGC' });
58
+ }
59
+ throw new Error(`Unexpected URL: ${url}`);
60
+ });
61
+ cy.wrap(httpGet).as('httpGet');
62
+
63
+ const onFileSelected = cy.spy().as('onFileSelected');
64
+
65
+ cy.mount(
66
+ <ConfigProvider config={config}>
67
+ <ServerStaticFileSelect
68
+ onFileSelected={onFileSelected}
69
+ />
70
+ </ConfigProvider>,
71
+ );
72
+
73
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
74
+
75
+ // Button is disabled
76
+ cy.contains('button', 'Submit').should('be.disabled');
77
+
78
+ // Select sequence
79
+ cy.get('#option-select').click();
80
+ cy.contains('Example sequence 1').click();
81
+
82
+ // Submit form
83
+ cy.contains('button', 'Submit').click();
84
+
85
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'example.fa');
86
+
87
+ cy.get('@onFileSelected').should('have.been.calledOnce');
88
+ cy.get('@onFileSelected').should((spy) => {
89
+ const [file] = spy.lastCall.args;
90
+ expect(file.name).to.equal('example.fa');
91
+ expect(file.type).to.equal('text/plain');
92
+ expect(file.size).to.equal(4);
93
+ });
94
+ });
95
+
96
+ it('handles index load failure and allows retry', () => {
97
+ let firstCall = true;
98
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
99
+ if (!url.endsWith('/index.json')) {
100
+ throw new Error(`Unexpected URL: ${url}`);
101
+ }
102
+ if (firstCall) {
103
+ firstCall = false;
104
+ return Promise.reject(new Error('Network error'));
105
+ }
106
+ return Promise.resolve({
107
+ data: dummyIndex,
108
+ });
109
+ });
110
+ cy.wrap(httpGet).as('httpGet');
111
+
112
+ cy.mount(
113
+ <ConfigProvider config={config}>
114
+ <ServerStaticFileSelect
115
+ onFileSelected={cy.spy()}
116
+ />
117
+ </ConfigProvider>,
118
+ );
119
+
120
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
121
+
122
+ // First request failed, should show retry alert
123
+ cy.get('.MuiAlert-colorError').should('exist');
124
+ cy.contains('Retry').click();
125
+
126
+ // Second request succeeds
127
+ cy.get('@httpGet').should('have.been.calledTwice');
128
+
129
+ // Error alert should disappear and form should be visible
130
+ cy.get('.MuiAlert-colorError').should('not.exist');
131
+ cy.get('#option-select').should('exist');
132
+ });
133
+
134
+ it('handles invalid sequence and file request failure', () => {
135
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
136
+ if (url.endsWith('/index.json')) {
137
+ return Promise.resolve({
138
+ data: {
139
+ sequences: [
140
+ {
141
+ name: 'No path sequence',
142
+ categories: ['Test category'],
143
+ },
144
+ {
145
+ name: 'Valid path sequence',
146
+ path: 'valid.fa',
147
+ categories: ['Test category'],
148
+ },
149
+ ],
150
+ },
151
+ });
152
+ }
153
+ if (url.endsWith('/valid.fa')) {
154
+ return Promise.reject(new Error('Server error'));
155
+ }
156
+ throw new Error(`Unexpected URL: ${url}`);
157
+ });
158
+ cy.wrap(httpGet).as('httpGet');
159
+
160
+ const onFileSelected = cy.spy().as('onFileSelected');
161
+
162
+ cy.mount(
163
+ <ConfigProvider config={config}>
164
+ <ServerStaticFileSelect
165
+ onFileSelected={onFileSelected}
166
+ />
167
+ </ConfigProvider>,
168
+ );
169
+
170
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
171
+
172
+ // Select category
173
+ cy.get('#category-select').click();
174
+ cy.contains('Test category').click();
175
+
176
+ // First: select malformed sequence (no path)
177
+ cy.get('#option-select').click();
178
+ cy.contains('No path sequence').click();
179
+ cy.contains('button', 'Submit').click();
180
+
181
+ cy.get('.MuiAlert-colorError')
182
+ .contains('Malformatted option, must have a path')
183
+ .should('exist');
184
+ cy.get('@onFileSelected').should('not.have.been.called');
185
+
186
+ // Then: select valid sequence but make file request fail
187
+ cy.get('#option-select').click();
188
+ cy.contains('Valid path sequence').click();
189
+ cy.contains('button', 'Submit').click();
190
+
191
+ cy.get('.MuiAlert-colorError')
192
+ .contains('Error requesting file')
193
+ .should('exist');
194
+ cy.get('@onFileSelected').should('not.have.been.called');
195
+ });
196
+ it('handles filtering by categories', () => {
197
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
198
+ if (url.endsWith('/index.json')) {
199
+ return Promise.resolve({
200
+ data: dummyIndex,
201
+ });
202
+ }
203
+ });
204
+ cy.wrap(httpGet).as('httpGet');
205
+
206
+ cy.mount(
207
+ <ConfigProvider config={config}>
208
+ <ServerStaticFileSelect
209
+ onFileSelected={cy.spy()}
210
+ />
211
+ </ConfigProvider>,
212
+ );
213
+
214
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
215
+
216
+ // Select category
217
+ cy.get('#category-select').click();
218
+ cy.contains('Test category').click();
219
+
220
+ // Only one sequence should be shown
221
+ cy.get('#option-select').click();
222
+ cy.contains('Example sequence 1').should('exist');
223
+ cy.contains('Example sequence 2').should('not.exist');
224
+ cy.contains('Example sequence 3').should('exist');
225
+ // Select sequence
226
+ cy.contains('Example sequence 1').click();
227
+
228
+ // Select All
229
+ cy.get('#category-select').click();
230
+ cy.contains('All').click();
231
+
232
+ // Nothing should be selected
233
+ cy.get('#option-select').should('have.value', '');
234
+ });
235
+ it('works with syntaxes', () => {
236
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
237
+ if (url.endsWith('/index.json')) {
238
+ return Promise.resolve({
239
+ data: dummyIndex,
240
+ });
241
+ }
242
+ if (url.endsWith('/example.json')) {
243
+ return Promise.resolve({ data: '{"name": "Example syntax 1"}' });
244
+ }
245
+ throw new Error(`Unexpected URL: ${url}`);
246
+ });
247
+ cy.wrap(httpGet).as('httpGet');
248
+
249
+ const onFileSelected = cy.spy().as('onFileSelected');
250
+ cy.mount(
251
+ <ConfigProvider config={config}>
252
+ <ServerStaticFileSelect
253
+ onFileSelected={onFileSelected}
254
+ type="syntax"
255
+ />
256
+ </ConfigProvider>,
257
+ );
258
+
259
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
260
+
261
+ // Select syntax
262
+ cy.get('#option-select').click();
263
+ cy.contains('Example syntax 1').click();
264
+
265
+ // Submit form
266
+ cy.contains('button', 'Submit').click();
267
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'example.json');
268
+
269
+ cy.get('@onFileSelected').should('have.been.calledOnce');
270
+ cy.get('@onFileSelected').should((spy) => {
271
+ const [file] = spy.lastCall.args;
272
+ expect(file.name).to.equal('example.json');
273
+ expect(file.type).to.equal('text/plain');
274
+ expect(file.size).to.equal(28);
275
+ });
276
+ });
277
+ it('allows selecting all sequences with the Select all option when multiple', () => {
278
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
279
+ if (url.endsWith('/index.json')) {
280
+ return Promise.resolve({
281
+ data: dummyIndex,
282
+ });
283
+ }
284
+ if (url.endsWith('/example.fa')) {
285
+ return Promise.resolve({ data: 'ATGC' });
286
+ }
287
+ if (url.endsWith('/example2.gb')) {
288
+ return Promise.resolve({ data: 'ATGCA' });
289
+ }
290
+ if (url.endsWith('/example3.fa')) {
291
+ return Promise.resolve({ data: 'ATGCG' });
292
+ }
293
+ throw new Error(`Unexpected URL: ${url}`);
294
+ });
295
+ cy.wrap(httpGet).as('httpGet');
296
+
297
+ const onFileSelected = cy.spy().as('onFileSelected');
298
+ cy.mount(
299
+ <ConfigProvider config={config}>
300
+ <ServerStaticFileSelect
301
+ onFileSelected={onFileSelected}
302
+ multiple
303
+ />
304
+ </ConfigProvider>,
305
+ );
306
+
307
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
308
+
309
+ // Use the Select all option to select all sequences
310
+ cy.get('#option-select').click();
311
+ cy.contains('Select all').click();
312
+ cy.get('body').click(0, 0);
313
+
314
+ // Submit form
315
+ cy.contains('button', 'Submit').click();
316
+
317
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'example.fa');
318
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'example2.gb');
319
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'example3.fa');
320
+
321
+ cy.get('@onFileSelected').should('have.been.calledOnce');
322
+ cy.get('@onFileSelected').should((spy) => {
323
+ const [files] = spy.lastCall.args;
324
+ expect(files).to.have.length(3);
325
+ expect(files[0].name).to.equal('example.fa');
326
+ expect(files[0].type).to.equal('text/plain');
327
+ expect(files[1].name).to.equal('example2.gb');
328
+ expect(files[1].type).to.equal('text/plain');
329
+ expect(files[2].name).to.equal('example3.fa');
330
+ expect(files[2].type).to.equal('text/plain');
331
+ });
332
+ });
333
+ it('clicking select all only selects the sequences that were filtered by category', () => {
334
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
335
+ if (url.endsWith('/index.json')) {
336
+ return Promise.resolve({
337
+ data: dummyIndex,
338
+ });
339
+ }
340
+ if (url.endsWith('/example.fa')) {
341
+ return Promise.resolve({ data: 'ATGC' });
342
+ }
343
+ if (url.endsWith('/example3.fa')) {
344
+ return Promise.resolve({ data: 'ATGCG' });
345
+ }
346
+ });
347
+ cy.wrap(httpGet).as('httpGet');
348
+ const onFileSelected = cy.spy().as('onFileSelected');
349
+ cy.mount(
350
+ <ConfigProvider config={config}>
351
+ <ServerStaticFileSelect
352
+ onFileSelected={onFileSelected}
353
+ multiple
354
+ />
355
+ </ConfigProvider>,
356
+ );
357
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
358
+
359
+ // Select category
360
+ cy.get('#category-select').click();
361
+ cy.contains('Test category').click();
362
+
363
+ // Select all
364
+ cy.get('#option-select').click();
365
+ cy.contains('Select all').click();
366
+
367
+ // Only one sequence should be shown
368
+ cy.get('#option-select').click();
369
+ cy.contains('Example sequence 1').should('exist');
370
+ cy.contains('Example sequence 2').should('not.exist');
371
+ cy.contains('Example sequence 3').should('exist');
372
+
373
+ // Click outside to close select element
374
+ cy.get('body').click(0, 0);
375
+ cy.contains('button', 'Submit').click();
376
+
377
+ cy.get('@httpGet').should('have.been.calledThrice');
378
+ cy.get('@onFileSelected').should('have.been.calledOnce');
379
+ cy.get('@onFileSelected').should((spy) => {
380
+ const [files] = spy.lastCall.args;
381
+ expect(files).to.have.length(2);
382
+ expect(files[0].name).to.equal('example.fa');
383
+ expect(files[1].name).to.equal('example3.fa');
384
+ });
385
+ })
386
+ it('works with multiple', () => {
387
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
388
+ if (url.endsWith('/index.json')) {
389
+ return Promise.resolve({
390
+ data: dummyIndex,
391
+ });
392
+ }
393
+ if (url.endsWith('/example.fa')) {
394
+ return Promise.resolve({ data: 'ATGC' });
395
+ }
396
+ if (url.endsWith('/example2.gb')) {
397
+ return Promise.resolve({ data: 'ATGCA' });
398
+ }
399
+ throw new Error(`Unexpected URL: ${url}`);
400
+ });
401
+ cy.wrap(httpGet).as('httpGet');
402
+ const onFileSelected = cy.spy().as('onFileSelected');
403
+ cy.mount(
404
+ <ConfigProvider config={config}>
405
+ <ServerStaticFileSelect
406
+ onFileSelected={onFileSelected}
407
+ multiple
408
+ />
409
+ </ConfigProvider>,
410
+ );
411
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
412
+
413
+ // Select sequence
414
+ cy.get('#option-select').click();
415
+ cy.contains('Example sequence 1').click();
416
+ cy.contains('Example sequence 2').click();
417
+ // Click outside to close select element
418
+ cy.get('body').click(0, 0);
419
+ cy.contains('button', 'Submit').click();
420
+
421
+ cy.get('@httpGet').should('have.been.calledThrice');
422
+ cy.get('@onFileSelected').should('have.been.calledOnce');
423
+ cy.get('@onFileSelected').should((spy) => {
424
+ const [files] = spy.lastCall.args;
425
+ expect(files).to.have.length(2);
426
+ expect(files[0].name).to.equal('example.fa');
427
+ expect(files[0].type).to.equal('text/plain');
428
+ expect(files[0].size).to.equal(4);
429
+ expect(files[1].name).to.equal('example2.gb');
430
+ expect(files[1].type).to.equal('text/plain');
431
+ expect(files[1].size).to.equal(5);
432
+ });
433
+ });
434
+
435
+ it('can filter with multiple', () => {
436
+ const httpGet = cy.stub(localFilesHttpClient, 'get').callsFake((url) => {
437
+ if (url.endsWith('/index.json')) {
438
+ return Promise.resolve({
439
+ data: dummyIndex,
440
+ });
441
+ }
442
+ });
443
+
444
+ cy.wrap(httpGet).as('httpGet');
445
+ cy.mount(
446
+ <ConfigProvider config={config}>
447
+ <ServerStaticFileSelect
448
+ onFileSelected={cy.spy()}
449
+ multiple
450
+ />
451
+ </ConfigProvider>,
452
+ );
453
+ cy.get('@httpGet').should('have.been.calledWithMatch', 'index.json');
454
+
455
+ // Select category
456
+ cy.get('#category-select').click();
457
+ cy.contains('Test category').click();
458
+
459
+ // Only one sequence should be shown
460
+ cy.get('#option-select').click();
461
+ cy.contains('Example sequence 1').should('exist');
462
+ cy.contains('Example sequence 2').should('not.exist');
463
+ cy.contains('Example sequence 3').should('exist');
464
+
465
+ });
466
+ });
@@ -0,0 +1,134 @@
1
+ import React from 'react'
2
+ import useServerStaticFiles from '../../hooks/useServerStaticFiles';
3
+ import RequestStatusWrapper from './RequestStatusWrapper';
4
+ import { Alert, Autocomplete, Button, FormControl, TextField } from '@mui/material';
5
+
6
+ function ServerStaticFileSelect({ onFileSelected, multiple = false, type = 'sequence' }) {
7
+ const [selectedCategory, setSelectedCategory] = React.useState('');
8
+ const [selectedOptions, setSelectedOptions] = React.useState(multiple ? [] : null);
9
+ const [error, setError] = React.useState(null);
10
+
11
+ const localFiles = useServerStaticFiles();
12
+ const{ index, indexRequestStatus, indexRetry, requestFile } = localFiles;
13
+
14
+ const options = React.useMemo(() => {
15
+ if (index === null) return [];
16
+ if (type !== 'sequence') {
17
+ return index.syntaxes;
18
+ }
19
+ const prePendArray = multiple ? ['__all__'] : [];
20
+ if (selectedCategory === '') {
21
+ return [...prePendArray, ...index.sequences];
22
+ }
23
+ return [...prePendArray, ...index.sequences.filter((sequence) => sequence.categories?.includes(selectedCategory))];
24
+ }, [type, index, selectedCategory, multiple]);
25
+
26
+ const categoryOptions = React.useMemo(() => {
27
+ if (!index?.categories) return [];
28
+ return ['All', ...index.categories];
29
+ }, [index]);
30
+
31
+ const optionToFile = React.useCallback(async (option) => {
32
+ if (!option.path) {
33
+ throw new Error('Malformatted option, must have a path');
34
+ }
35
+ let fileContent = null;
36
+ try {
37
+ fileContent = await requestFile(option.path);
38
+ } catch (error) {
39
+ throw new Error(`Error requesting file: ${error.message}`);
40
+ }
41
+ if (typeof fileContent !== 'string') {
42
+ fileContent = JSON.stringify(fileContent);
43
+ }
44
+ const file = new File([fileContent], option.path, { type: 'text/plain' });
45
+ return file;
46
+ }, [requestFile]);
47
+
48
+ const onSubmit = React.useCallback(async (e) => {
49
+ e.preventDefault();
50
+ setError(null);
51
+ try {
52
+ if (!multiple) {
53
+ const file = await optionToFile(selectedOptions);
54
+ onFileSelected(file);
55
+ } else {
56
+ const files = await Promise.all(selectedOptions.map((option) => optionToFile(option)));
57
+ onFileSelected(files);
58
+ }
59
+ } catch (error) {
60
+ setError(error.message);
61
+ }
62
+ }, [multiple, selectedOptions, onFileSelected, optionToFile]);
63
+
64
+ const onCategoryChange = React.useCallback((event, newValue) => {
65
+ setSelectedOptions(multiple ? [] : null);
66
+ if (!newValue || newValue === 'All') {
67
+ setSelectedCategory('');
68
+ } else {
69
+ setSelectedCategory(newValue);
70
+ }
71
+ }, [multiple]);
72
+
73
+ const buttonDisabled = multiple ? selectedOptions.length === 0 : !selectedOptions;
74
+
75
+ const label = type === 'sequence' ? 'Sequence' : 'Syntax';
76
+
77
+ const onOptionsChange = React.useCallback((event, value) => {
78
+ console.log('onOptionsChange', value);
79
+ if (multiple && type === 'sequence') {
80
+ if (value.includes('__all__')) {
81
+ const allSequences = options.filter((option) => option !== '__all__');
82
+ setSelectedOptions(allSequences);
83
+ return;
84
+ }
85
+ }
86
+ setSelectedOptions(value);
87
+ }, [multiple, type, options]);
88
+
89
+ return (
90
+ <RequestStatusWrapper requestStatus={indexRequestStatus} retry={indexRetry}>
91
+ {error && <Alert severity="error">{error}</Alert>}
92
+ <form onSubmit={onSubmit}>
93
+ {type === 'sequence' && (
94
+ <FormControl fullWidth sx={{ my: 1 }}>
95
+ <Autocomplete
96
+ id="category-select"
97
+ options={categoryOptions}
98
+ value={selectedCategory === '' ? 'All' : selectedCategory}
99
+ onChange={onCategoryChange}
100
+ renderInput={(params) => (
101
+ <TextField
102
+ {...params}
103
+ label="Category"
104
+ />
105
+ )}
106
+ />
107
+ </FormControl>
108
+ )}
109
+ <FormControl fullWidth sx={{ my: 1 }}>
110
+ <Autocomplete
111
+ id="option-select"
112
+ multiple={multiple}
113
+ options={options}
114
+ value={selectedOptions}
115
+ onChange={onOptionsChange}
116
+ getOptionLabel={(option) => (option === '__all__' ? 'Select all' : option?.name || option?.path || '')}
117
+ disableCloseOnSelect={multiple}
118
+ renderInput={(params) => (
119
+ <TextField
120
+ {...params}
121
+ label={label}
122
+ />
123
+ )}
124
+ />
125
+ </FormControl>
126
+ <FormControl fullWidth>
127
+ <Button disabled={buttonDisabled} type="submit" variant="contained" color="primary">Submit</Button>
128
+ </FormControl>
129
+ </form>
130
+ </RequestStatusWrapper>
131
+ );
132
+ }
133
+
134
+ export default ServerStaticFileSelect
@@ -21,6 +21,7 @@ import SourceCopySequence from './SourceCopySequence';
21
21
  import SourceReverseComplement from './SourceReverseComplement';
22
22
  import SourceKnownGenomeRegion from './SourceKnownGenomeRegion';
23
23
  import { doesSourceHaveOutput } from '@opencloning/store/cloning_utils';
24
+ import SourceServerStaticFile from './SourceServerStaticFile';
24
25
 
25
26
  // There are several types of source, this components holds the common part,
26
27
  // which for now is a select element to pick which kind of source is created
@@ -49,6 +50,8 @@ function Source({ sourceId }) {
49
50
  /* eslint-disable */
50
51
  case 'UploadedFileSource':
51
52
  specificSource = <SourceFile {...{ source, requestStatus, sendPostRequest }} />; break;
53
+ case 'LocalFileSource':
54
+ specificSource = <SourceServerStaticFile {...{ source, requestStatus, sendPostRequest }} />; break;
52
55
  case 'RestrictionEnzymeDigestionSource':
53
56
  specificSource = <SourceRestriction {...{ source, requestStatus, sendPostRequest }} />; break;
54
57
  case 'RepositoryIdSource':
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+ import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
3
+ import { Alert, LinearProgress } from '@mui/material';
4
+
5
+ function SourceServerStaticFile({ source, requestStatus, sendPostRequest }) {
6
+ const onFileSelected = React.useCallback((file) => {
7
+ const requestData = new FormData();
8
+ requestData.append('file', file);
9
+ const config = {
10
+ headers: {
11
+ 'content-type': 'multipart/form-data',
12
+ },
13
+ };
14
+ sendPostRequest({ endpoint: 'read_from_file', requestData, config, source });
15
+ }, [sendPostRequest, source]);
16
+
17
+ return (
18
+ <>
19
+ <ServerStaticFileSelect onFileSelected={onFileSelected} />
20
+ { requestStatus.status === 'loading' && <LinearProgress /> }
21
+ { requestStatus.status === 'error' && <Alert severity="error">{requestStatus.message}</Alert> }
22
+ </>
23
+ );
24
+ }
25
+
26
+ export default SourceServerStaticFile
@@ -13,7 +13,7 @@ function SourceTypeSelector({ source }) {
13
13
  const dispatch = useDispatch();
14
14
  const database = useDatabase();
15
15
  const sourceIsPrimerDesign = useSelector((state) => Boolean(state.cloning.sequences.find((e) => e.id === source.id)?.primer_design));
16
- const { noExternalRequests, enablePlannotate } = useConfig();
16
+ const { noExternalRequests, enablePlannotate, staticContentPath } = useConfig();
17
17
 
18
18
  const onChange = (event) => {
19
19
  // Clear the source other than these fields
@@ -28,6 +28,9 @@ function SourceTypeSelector({ source }) {
28
28
  const options = [];
29
29
  if (inputSequences.length === 0) {
30
30
  options.push(<MenuItem key="UploadedFileSource" value="UploadedFileSource">Submit file</MenuItem>);
31
+ if (staticContentPath) {
32
+ options.push(<MenuItem key="LocalFileSource" value="LocalFileSource">Local server file</MenuItem>);
33
+ }
31
34
  if (!noExternalRequests) {
32
35
  options.push(<MenuItem key="RepositoryIdSource" value="RepositoryIdSource">Repository</MenuItem>);
33
36
  options.push(<MenuItem key="GenomeCoordinatesSource" value="GenomeCoordinatesSource">Genome region</MenuItem>);
@@ -0,0 +1,28 @@
1
+ import React from 'react'
2
+
3
+ function useRequestForEffect({ requestFunction, onSuccess }) {
4
+ const [requestStatus, setRequestStatus] = React.useState({ status: 'loading', message: '' });
5
+ const [connectAttempt, setConnectAttempt] = React.useState(0);
6
+
7
+ const retry = React.useCallback(() => {
8
+ setConnectAttempt((prev) => prev + 1);
9
+ }, []);
10
+
11
+ React.useEffect(() => {
12
+ const fetchData = async () => {
13
+ setRequestStatus({ status: 'loading', message: '' });
14
+ try {
15
+ const resp = await requestFunction();
16
+ setRequestStatus({ status: 'success', message: '' });
17
+ onSuccess(resp);
18
+ } catch (error) {
19
+ setRequestStatus({ status: 'error', message: error.message });
20
+ }
21
+ };
22
+ fetchData();
23
+ }, [connectAttempt, requestFunction, onSuccess]);
24
+
25
+ return React.useMemo(() => ({ requestStatus, retry }), [requestStatus, retry]);
26
+ }
27
+
28
+ export default useRequestForEffect