@vitessce/vit-s 3.6.7 → 3.6.8

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/src/TitleInfo.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import React, { useState, useMemo } from 'react';
2
2
  import clsx from 'clsx';
3
- import { makeStyles, MenuItem, IconButton, Link,
3
+ import {
4
+ makeStyles,
5
+ MenuItem,
6
+ IconButton,
7
+ Link,
8
+ List,
9
+ Alert,
4
10
  CloudDownload as CloudDownloadIcon,
5
11
  ArrowDropDown as ArrowDropDownIcon,
6
12
  ArrowDropUp as ArrowDropUpIcon,
7
13
  Settings as SettingsIcon,
8
14
  Close as CloseIcon,
9
15
  Help as HelpIcon,
16
+ Warning as WarningIcon,
10
17
  } from '@vitessce/styles';
11
18
 
12
19
  import { TOOLTIP_ANCESTOR } from './classNames.js';
@@ -54,6 +61,18 @@ const useStyles = makeStyles()(theme => ({
54
61
  boxShadow: '0 0 10px rgba(0, 0, 0, 0.2)',
55
62
  border: '10px solid grey',
56
63
  },
64
+ errorList: {
65
+ width: '100%',
66
+ maxWidth: 300,
67
+ padding: '0 4px',
68
+ },
69
+ errorListAlert: {
70
+ marginTop: '2px',
71
+ marginBottom: '2px',
72
+ },
73
+ errorListItemText: {
74
+ fontSize: '12px',
75
+ },
57
76
  }));
58
77
 
59
78
  function SettingsIconWithArrow({ open }) {
@@ -78,6 +97,7 @@ function PlotOptions(props) {
78
97
  buttonIcon={buttonIcon}
79
98
  buttonClassName={classes.iconButton}
80
99
  placement="bottom-end"
100
+ title="Plot Options"
81
101
  aria-label="Open plot options menu"
82
102
  >
83
103
  {options}
@@ -106,6 +126,7 @@ function DownloadOptions(props) {
106
126
  buttonIcon={buttonIcon}
107
127
  buttonClassName={classes.iconButton}
108
128
  placement="bottom-end"
129
+ title="Download Options"
109
130
  aria-label="Open download options menu"
110
131
  >
111
132
  {urls.map(({ url, name }) => (
@@ -130,6 +151,7 @@ function HelpButton(props) {
130
151
  buttonIcon={<HelpIcon />}
131
152
  buttonClassName={classes.iconButton}
132
153
  placement="bottom-end"
154
+ title="Help Info"
133
155
  aria-label="Open help info"
134
156
  withPaper={false}
135
157
  >
@@ -138,6 +160,38 @@ function HelpButton(props) {
138
160
  );
139
161
  }
140
162
 
163
+ function ErrorInfo(props) {
164
+ const { errors } = props;
165
+ const [open, setOpen] = useState(false);
166
+ const { classes } = useStyles();
167
+ return (
168
+ <PopperMenu
169
+ open={open}
170
+ setOpen={setOpen}
171
+ buttonIcon={<WarningIcon color="error" />}
172
+ buttonClassName={classes.iconButton}
173
+ placement="bottom-end"
174
+ title="View Errors"
175
+ aria-label="Open error info"
176
+ >
177
+ <List className={classes.errorList}>
178
+ {errors.map((error, index) => (
179
+ <Alert
180
+ // eslint-disable-next-line react/no-array-index-key
181
+ key={`${index}-${error.name}-${error.message}`}
182
+ severity="error"
183
+ className={classes.errorListAlert}
184
+ slots={{ message: 'span' }}
185
+ slotProps={{ message: { className: classes.errorListItemText } }}
186
+ >
187
+ {error.name}: {error.message}
188
+ </Alert>
189
+ ))}
190
+ </List>
191
+ </PopperMenu>
192
+ );
193
+ }
194
+
141
195
 
142
196
  function ClosePaneButton(props) {
143
197
  const { removeGridComponent } = props;
@@ -147,7 +201,7 @@ function ClosePaneButton(props) {
147
201
  onClick={removeGridComponent}
148
202
  size="small"
149
203
  className={classes.iconButton}
150
- title="close"
204
+ title="Close View"
151
205
  aria-label="Close panel button"
152
206
  >
153
207
  <CloseIcon />
@@ -159,9 +213,11 @@ export function TitleInfo(props) {
159
213
  const {
160
214
  title, info, children, isScroll, isSpatial, removeGridComponent, urls,
161
215
  isReady, options, closeButtonVisible = true, downloadButtonVisible = true,
162
- helpText, withPadding = true,
216
+ helpText, withPadding = true, errors: errorsProp,
163
217
  } = props;
164
218
 
219
+ const errors = errorsProp?.filter(Boolean);
220
+
165
221
  const { classes } = useTitleStyles();
166
222
 
167
223
  return (
@@ -183,6 +239,11 @@ export function TitleInfo(props) {
183
239
  urls={urls}
184
240
  />
185
241
  ) : null}
242
+ {Array.isArray(errors) && errors.length > 0 ? (
243
+ <ErrorInfo
244
+ errors={errors}
245
+ />
246
+ ) : null}
186
247
  {helpText ? (
187
248
  <HelpButton
188
249
  helpText={helpText}
package/src/VitS.js CHANGED
@@ -5,12 +5,14 @@ import {
5
5
  createCache,
6
6
  } from '@vitessce/styles';
7
7
  import {
8
+ QueryCache,
8
9
  QueryClient,
9
10
  QueryClientProvider,
10
11
  } from '@tanstack/react-query';
11
12
  import { isEqual } from 'lodash-es';
12
13
  import { buildConfigSchema, latestConfigSchema } from '@vitessce/schemas';
13
14
  import {
15
+ log,
14
16
  setLogLevel, setDebugMode,
15
17
  DEFAULT_LOG_LEVEL, DEFAULT_DEBUG_MODE,
16
18
  } from '@vitessce/globals';
@@ -21,7 +23,6 @@ import {
21
23
  AuxiliaryProvider,
22
24
  createAuxiliaryStore,
23
25
  } from './state/hooks.js';
24
-
25
26
  import VitessceGrid from './VitessceGrid.js';
26
27
  import { Warning } from './Warning.js';
27
28
  import { DebugWindow } from './DebugWindow.js';
@@ -206,6 +207,15 @@ export function VitS(props) {
206
207
  retry: 2,
207
208
  },
208
209
  },
210
+ // Reference: https://tkdodo.eu/blog/react-query-error-handling#the-global-callbacks
211
+ queryCache: new QueryCache({
212
+ onError: (error) => {
213
+ log.error(error);
214
+ if (onWarn) {
215
+ onWarn(error.message);
216
+ }
217
+ },
218
+ }),
209
219
  // TODO: should the queryClient be shared? Or have an option to be shared
210
220
  // (e.g., based on remountOnUidChange)?
211
221
  }), [configKey]);
@@ -4,32 +4,15 @@ import {
4
4
  capitalize,
5
5
  getInitialCoordinationScopePrefix,
6
6
  } from '@vitessce/utils';
7
- import { log } from '@vitessce/globals';
8
7
  import { STATUS } from '@vitessce/constants-internal';
9
8
  import {
10
- AbstractLoaderError,
11
9
  LoaderNotFoundError,
12
- } from '@vitessce/abstract';
10
+ } from '@vitessce/error';
13
11
  import {
14
12
  getMatchingLoader,
15
13
  useMatchingLoader,
16
- useSetWarning,
17
14
  } from './state/hooks.js';
18
15
 
19
- /**
20
- * Warn via publishing to the console
21
- * and to the global warning store.
22
- * @param {AbstractLoaderError} error An error instance.
23
- */
24
- export function warn(error, setWarning) {
25
- setWarning(error.message);
26
- log.warn(error.message);
27
- log.error(error.stack);
28
- if (error instanceof AbstractLoaderError) {
29
- error.warnInConsole();
30
- }
31
- }
32
-
33
16
  /**
34
17
  * Initialize values in the coordination space.
35
18
  * @param {object} values Object where
@@ -76,7 +59,7 @@ export async function dataQueryFn(ctx) {
76
59
  // No loader was found.
77
60
  if (isRequired) {
78
61
  // Status: error
79
- throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
62
+ throw new LoaderNotFoundError(`Loader not found for parameters: ${dataset}, ${dataType}, ${JSON.stringify(matchOn)}`);
80
63
  } else {
81
64
  // Status: success
82
65
  return { data: placeholderObject };
@@ -100,7 +83,7 @@ export async function dataQueryFn(ctx) {
100
83
  * @param {object} initialCoordinationValues Object where
101
84
  * keys are coordination type names with the prefix 'initialize',
102
85
  * values are initialization preferences as boolean values.
103
- * @returns {array} [data, status, urls] where
86
+ * @returns {array} [data, status, urls, error] where
104
87
  * cells is an object and cellsCount is the
105
88
  * number of items in the cells object.
106
89
  */
@@ -108,7 +91,6 @@ export function useDataType(
108
91
  dataType, loaders, dataset, isRequired,
109
92
  coordinationSetters, initialCoordinationValues, matchOn,
110
93
  ) {
111
- const setWarning = useSetWarning();
112
94
  const placeholderObject = useMemo(() => ({}), []);
113
95
  const dataQuery = useQuery({
114
96
  // TODO: only enable when loaders has been initialized?
@@ -130,8 +112,6 @@ export function useDataType(
130
112
 
131
113
  const coordinationValues = data?.coordinationValues;
132
114
  const urls = data?.urls;
133
- const requestInit = data?.requestInit;
134
-
135
115
 
136
116
  useEffect(() => {
137
117
  initCoordinationSpace(
@@ -141,14 +121,8 @@ export function useDataType(
141
121
  );
142
122
  }, [coordinationValues]);
143
123
 
144
- useEffect(() => {
145
- if (error) {
146
- warn(error, setWarning);
147
- }
148
- }, [error, setWarning]);
149
-
150
124
  const dataStatus = isFetching ? STATUS.LOADING : status;
151
- return [loadedData, dataStatus, urls, requestInit];
125
+ return [loadedData, dataStatus, urls, error];
152
126
  }
153
127
 
154
128
  /**
@@ -168,9 +142,10 @@ export function useDataType(
168
142
  * @param {object} initialCoordinationValues Object where
169
143
  * keys are coordination type names with the prefix 'initialize',
170
144
  * values are initialization preferences as boolean values.
171
- * @returns {array} [cells, cellsCount] where
172
- * cells is an object and cellsCount is the
173
- * number of items in the cells object.
145
+ * @param {object} matchOnObj Coordination values used to obtain a matching loader.
146
+ * @param {function} mergeCoordination Function to merge coordination values.
147
+ * @param {string} viewUid The view UID to use for merging coordination values.
148
+ * @returns {array} [data, status, urls, errors]
174
149
  */
175
150
  export function useDataTypeMulti(
176
151
  dataType, loaders, dataset, isRequired,
@@ -178,8 +153,6 @@ export function useDataTypeMulti(
178
153
  mergeCoordination, viewUid,
179
154
  ) {
180
155
  const placeholderObject = useMemo(() => ({}), []);
181
- const setWarning = useSetWarning();
182
-
183
156
  const matchOnEntries = matchOnObj ? Object.entries(matchOnObj) : [];
184
157
  const dataQueries = useQueries({
185
158
  // eslint-disable-next-line no-unused-vars
@@ -201,6 +174,7 @@ export function useDataTypeMulti(
201
174
 
202
175
  const anyLoading = dataQueries.some(q => q.isFetching);
203
176
  const anyError = dataQueries.some(q => q.isError);
177
+ const errors = anyError ? dataQueries.filter(q => q.isError).map(q => q.error) : [];
204
178
  // eslint-disable-next-line no-nested-ternary
205
179
  const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
206
180
  const isSuccess = dataStatus === STATUS.SUCCESS;
@@ -229,14 +203,6 @@ export function useDataTypeMulti(
229
203
  // eslint-disable-next-line react-hooks/exhaustive-deps
230
204
  }, [isSuccess]);
231
205
 
232
- useEffect(() => {
233
- dataQueries.map(q => q.error).filter(e => Boolean(e)).forEach((error) => {
234
- warn(error, setWarning);
235
- });
236
- // Deliberate dependency omissions: use indirect dependencies for efficiency.
237
- // eslint-disable-next-line react-hooks/exhaustive-deps
238
- }, [anyError, setWarning]);
239
-
240
206
  // Convert data to object keyed by scopeKey.
241
207
  const data = useMemo(() => Object.fromEntries(
242
208
  matchOnEntries.map(([scopeKey], i) => ([scopeKey, dataQueries[i].data?.data])),
@@ -253,7 +219,7 @@ export function useDataTypeMulti(
253
219
  // eslint-disable-next-line react-hooks/exhaustive-deps
254
220
  ), [dataQueries]);
255
221
 
256
- return [data, dataStatus, urls];
222
+ return [data, dataStatus, urls, errors];
257
223
  }
258
224
 
259
225
  export function useHasLoader(loaders, dataset, dataType, matchOn) {
@@ -1,16 +1,14 @@
1
- import { useEffect, useMemo } from 'react';
1
+ import { useMemo } from 'react';
2
2
  import { useQueries } from '@tanstack/react-query';
3
3
  import { extent } from 'd3-array';
4
4
  import { DataType, STATUS } from '@vitessce/constants-internal';
5
5
  import {
6
6
  LoaderNotFoundError,
7
- } from '@vitessce/abstract';
7
+ } from '@vitessce/error';
8
8
  import {
9
9
  getMatchingLoader,
10
- useSetWarning,
11
10
  } from './state/hooks.js';
12
11
  import {
13
- warn,
14
12
  dataQueryFn,
15
13
  } from './data-hook-utils.js';
16
14
 
@@ -324,18 +322,26 @@ async function featureSelectionQueryFn(ctx) {
324
322
  }
325
323
  // No loader was found.
326
324
  if (isRequired) {
327
- throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
325
+ throw new LoaderNotFoundError(`Loader not found for parameters: ${dataset}, ${dataType}, ${JSON.stringify(matchOn)}`);
328
326
  } else {
329
327
  return { data: null, dataKey: null };
330
328
  }
331
329
  }
332
330
 
331
+ /**
332
+ * Get feature values for a multi-level feature selection.
333
+ * @param {object} loaders
334
+ * @param {string} dataset
335
+ * @param {boolean} isRequired
336
+ * @param {object} matchOnObj
337
+ * @param {array} selections
338
+ * @param {number} depth
339
+ * @returns [data, geneName, extents, normData, status, errors]
340
+ */
333
341
  export function useFeatureSelectionMultiLevel(
334
342
  loaders, dataset, isRequired, matchOnObj, selections,
335
343
  depth,
336
344
  ) {
337
- const setWarning = useSetWarning();
338
-
339
345
  // Create a flat list of tuples (queryKey, scopeInfo).
340
346
  const queryKeyScopeTuples = useMemo(() => getFeatureSelectionQueryKeyScopeTuples(
341
347
  selections, matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired,
@@ -353,6 +359,7 @@ export function useFeatureSelectionMultiLevel(
353
359
 
354
360
  const anyLoading = featureQueries.some(q => q.isFetching);
355
361
  const anyError = featureQueries.some(q => q.isError);
362
+ const errors = anyError ? featureQueries.filter(q => q.isError).map(q => q.error) : [];
356
363
  // eslint-disable-next-line no-nested-ternary
357
364
  const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
358
365
  const flatGeneData = featureQueries.map(q => q.data?.data || null);
@@ -360,17 +367,6 @@ export function useFeatureSelectionMultiLevel(
360
367
  const flatExtents = featureQueries.map(q => q.data?.dataExtent || null);
361
368
  const flatNormData = featureQueries.map(q => q.data?.normData || null);
362
369
 
363
- useEffect(() => {
364
- featureQueries
365
- .map(q => q.error)
366
- .filter(e => Boolean(e))
367
- .forEach((error) => {
368
- warn(error, setWarning);
369
- });
370
- // Deliberate dependency omissions: use indirect dependencies for efficiency.
371
- // eslint-disable-next-line react-hooks/exhaustive-deps
372
- }, [anyError, setWarning]);
373
-
374
370
  // Need to re-nest the geneData and the loadedGeneName info.
375
371
  const [geneData, loadedGeneName, extents, normData] = useMemo(() => {
376
372
  const nestedGeneData = nestFeatureSelectionQueryResults(queryKeyScopeTuples, flatGeneData);
@@ -390,7 +386,7 @@ export function useFeatureSelectionMultiLevel(
390
386
  // eslint-disable-next-line react-hooks/exhaustive-deps
391
387
  }, [featureQueries.reduce((a, h) => a + h.dataUpdatedAt, 0), queryKeyScopeTuples]);
392
388
 
393
- return [geneData, loadedGeneName, extents, normData, dataStatus];
389
+ return [geneData, loadedGeneName, extents, normData, dataStatus, errors];
394
390
  }
395
391
 
396
392
  async function matrixIndicesQueryFn(ctx) {
@@ -424,18 +420,25 @@ async function matrixIndicesQueryFn(ctx) {
424
420
  }
425
421
  // No loader was found.
426
422
  if (isRequired) {
427
- throw new LoaderNotFoundError(loaders, dataset, dataType, matchOn);
423
+ throw new LoaderNotFoundError(`Loader not found for parameters: ${dataset}, ${dataType}, ${JSON.stringify(matchOn)}`);
428
424
  } else {
429
425
  return { data: null, dataKey: null };
430
426
  }
431
427
  }
432
428
 
429
+ /**
430
+ * Get matrix indices for a multi-level coordination.
431
+ * @param {object} loaders
432
+ * @param {string} dataset
433
+ * @param {boolean} isRequired
434
+ * @param {object} matchOnObj
435
+ * @param {number} depth
436
+ * @returns [data, dataStatus, errors]
437
+ */
433
438
  export function useObsFeatureMatrixIndicesMultiLevel(
434
439
  loaders, dataset, isRequired, matchOnObj,
435
440
  depth,
436
441
  ) {
437
- const setWarning = useSetWarning();
438
-
439
442
  // Create a flat list of tuples (queryKey, scopeInfo).
440
443
  const queryKeyScopeTuples = useMemo(() => getMatrixIndicesQueryKeyScopeTuples(
441
444
  matchOnObj, depth, dataset, DataType.OBS_FEATURE_MATRIX, isRequired,
@@ -453,21 +456,11 @@ export function useObsFeatureMatrixIndicesMultiLevel(
453
456
 
454
457
  const anyLoading = indicesQueries.some(q => q.isFetching);
455
458
  const anyError = indicesQueries.some(q => q.isError);
459
+ const errors = anyError ? indicesQueries.filter(q => q.isError).map(q => q.error) : [];
456
460
  // eslint-disable-next-line no-nested-ternary
457
461
  const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
458
462
  const flatIndicesData = indicesQueries.map(q => q.data?.data || null);
459
463
 
460
- useEffect(() => {
461
- indicesQueries
462
- .map(q => q.error)
463
- .filter(e => Boolean(e))
464
- .forEach((error) => {
465
- warn(error, setWarning);
466
- });
467
- // Deliberate dependency omissions: use indirect dependencies for efficiency.
468
- // eslint-disable-next-line react-hooks/exhaustive-deps
469
- }, [anyError, setWarning]);
470
-
471
464
  // Need to re-nest the geneData and the loadedGeneName info.
472
465
  const indicesData = useMemo(() => {
473
466
  const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
@@ -484,15 +477,23 @@ export function useObsFeatureMatrixIndicesMultiLevel(
484
477
  // eslint-disable-next-line react-hooks/exhaustive-deps
485
478
  }, [indicesQueries.reduce((a, h) => a + h.dataUpdatedAt, 0), queryKeyScopeTuples]);
486
479
 
487
- return [indicesData, dataStatus];
480
+ return [indicesData, dataStatus, errors];
488
481
  }
489
482
 
483
+ /**
484
+ * Get data for multi-level coordination.
485
+ * @param {object} loaders
486
+ * @param {string} dataset
487
+ * @param {boolean} isRequired
488
+ * @param {object} matchOnObj
489
+ * @param {number} depth
490
+ * @param {string} dataType
491
+ * @returns {array} [data, status, errors]
492
+ */
490
493
  function useDataTypeMultiLevel(
491
494
  loaders, dataset, isRequired, matchOnObj,
492
495
  depth, dataType,
493
496
  ) {
494
- const setWarning = useSetWarning();
495
-
496
497
  // Create a flat list of tuples (queryKey, scopeInfo).
497
498
  const queryKeyScopeTuples = useMemo(() => getQueryKeyScopeTuples(
498
499
  matchOnObj, depth, dataset, dataType, isRequired,
@@ -510,21 +511,11 @@ function useDataTypeMultiLevel(
510
511
 
511
512
  const anyLoading = locationsQueries.some(q => q.isFetching);
512
513
  const anyError = locationsQueries.some(q => q.isError);
514
+ const errors = anyError ? locationsQueries.filter(q => q.isError).map(q => q.error) : [];
513
515
  // eslint-disable-next-line no-nested-ternary
514
516
  const dataStatus = anyLoading ? STATUS.LOADING : (anyError ? STATUS.ERROR : STATUS.SUCCESS);
515
517
  const flatIndicesData = locationsQueries.map(q => q.data?.data || null);
516
518
 
517
- useEffect(() => {
518
- locationsQueries
519
- .map(q => q.error)
520
- .filter(e => Boolean(e))
521
- .forEach((error) => {
522
- warn(error, setWarning);
523
- });
524
- // Deliberate dependency omissions: use indirect dependencies for efficiency.
525
- // eslint-disable-next-line react-hooks/exhaustive-deps
526
- }, [anyError, setWarning]);
527
-
528
519
  // Need to re-nest the geneData and the loadedGeneName info.
529
520
  const locationsData = useMemo(() => {
530
521
  const nestedIndicesData = nestQueryResults(queryKeyScopeTuples, flatIndicesData);
@@ -539,9 +530,18 @@ function useDataTypeMultiLevel(
539
530
  // eslint-disable-next-line react-hooks/exhaustive-deps
540
531
  }, [locationsQueries.reduce((a, h) => a + h.dataUpdatedAt, 0), queryKeyScopeTuples]);
541
532
 
542
- return [locationsData, dataStatus];
533
+ return [locationsData, dataStatus, errors];
543
534
  }
544
535
 
536
+ /**
537
+ * Wrapper around useDataTypeMultiLevel.
538
+ * @param {object} loaders
539
+ * @param {string} dataset
540
+ * @param {boolean} isRequired
541
+ * @param {object} matchOnObj
542
+ * @param {number} depth
543
+ * @returns {array} [data, status, errors]
544
+ */
545
545
  export function useObsLocationsMultiLevel(
546
546
  loaders, dataset, isRequired, matchOnObj,
547
547
  depth,
@@ -552,6 +552,15 @@ export function useObsLocationsMultiLevel(
552
552
  );
553
553
  }
554
554
 
555
+ /**
556
+ * Wrapper around useDataTypeMultiLevel.
557
+ * @param {object} loaders
558
+ * @param {string} dataset
559
+ * @param {boolean} isRequired
560
+ * @param {object} matchOnObj
561
+ * @param {number} depth
562
+ * @returns {array} [data, status, errors]
563
+ */
555
564
  export function useObsSetsMultiLevel(
556
565
  loaders, dataset, isRequired, matchOnObj,
557
566
  depth,
@@ -562,6 +571,15 @@ export function useObsSetsMultiLevel(
562
571
  );
563
572
  }
564
573
 
574
+ /**
575
+ * Wrapper around useDataTypeMultiLevel.
576
+ * @param {object} loaders
577
+ * @param {string} dataset
578
+ * @param {boolean} isRequired
579
+ * @param {object} matchOnObj
580
+ * @param {number} depth
581
+ * @returns {array} [data, status, errors]
582
+ */
565
583
  export function useObsLabelsMultiLevel(
566
584
  loaders, dataset, isRequired, matchOnObj,
567
585
  depth,