@jbrowse/product-core 5.0.0-beta.7 → 5.0.0-beta.9
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/esm/RootModel/BaseRootModel.d.ts +1 -4
- package/esm/RootModel/HierarchicalConfig.d.ts +21 -47
- package/esm/RootModel/PreferencesConfig.d.ts +18 -49
- package/esm/RootModel/PreferencesConfig.js +2 -3
- package/esm/RootModel/createConfigModel.d.ts +141 -124
- package/esm/RootModel/createConfigModel.js +26 -14
- package/esm/RootModel/menuItems.js +4 -6
- package/esm/Session/BaseSession.d.ts +30 -13
- package/esm/Session/BaseSession.js +50 -16
- package/esm/Session/Connections.d.ts +4 -3
- package/esm/Session/Connections.js +4 -3
- package/esm/Session/DrawerWidgets.d.ts +8 -1
- package/esm/Session/DrawerWidgets.js +19 -7
- package/esm/Session/MultipleViews.d.ts +13 -14
- package/esm/Session/Preferences.d.ts +8 -13
- package/esm/Session/SessionTracks.d.ts +32 -38
- package/esm/Session/SessionTracks.js +52 -115
- package/esm/Session/Themes.d.ts +9 -10
- package/esm/Session/Themes.js +9 -10
- package/esm/Session/TrackMenu.d.ts +1 -26
- package/esm/Session/TrackMenu.js +2 -33
- package/esm/Session/TrackMenuSessionMixin.js +2 -1
- package/esm/Session/Tracks.d.ts +8 -13
- package/esm/Session/copyTrackSnapshot.d.ts +21 -0
- package/esm/Session/copyTrackSnapshot.js +20 -0
- package/esm/Session/hydratedForms.d.ts +16 -0
- package/esm/Session/hydratedForms.js +13 -0
- package/esm/Session/index.d.ts +5 -1
- package/esm/Session/index.js +4 -1
- package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
- package/esm/Session/pluginExtraTrackItems.js +13 -0
- package/esm/controllerTracks.d.ts +13 -8
- package/esm/controllerTracks.js +18 -9
- package/esm/index.d.ts +6 -6
- package/esm/index.js +3 -3
- package/esm/loadPlugins.d.ts +7 -2
- package/esm/loadPlugins.js +6 -2
- package/esm/resolveAssemblies.d.ts +34 -11
- package/esm/resolveAssemblies.js +29 -11
- package/esm/rpcWorker.d.ts +12 -5
- package/esm/rpcWorker.js +1 -1
- package/esm/sessionMigrations/index.d.ts +5 -5
- package/esm/sessionMigrations/index.js +82 -19
- package/esm/sessionUrl.js +1 -1
- package/esm/sessionUtils.d.ts +6 -1
- package/esm/sessionUtils.js +42 -35
- package/esm/ui/AboutDialog.d.ts +10 -2
- package/esm/ui/AboutDialog.js +11 -2
- package/esm/ui/AboutDialogContents.js +3 -21
- package/esm/ui/AppReadyMarker.d.ts +45 -0
- package/esm/ui/AppReadyMarker.js +89 -0
- package/esm/ui/AssemblyInfoPanel.js +4 -10
- package/esm/ui/DescriptionPanel.js +2 -1
- package/esm/ui/FileInfoPanel.js +5 -10
- package/esm/ui/RefNameInfoDialog.js +10 -23
- package/esm/ui/aboutTestUtils.d.ts +140 -0
- package/esm/ui/aboutTestUtils.js +25 -0
- package/esm/ui/descriptionHtml.d.ts +5 -6
- package/esm/ui/descriptionHtml.js +5 -7
- package/esm/ui/index.d.ts +2 -0
- package/esm/ui/index.js +1 -0
- package/esm/ui/util.d.ts +9 -27
- package/esm/ui/util.js +12 -31
- package/package.json +4 -4
package/esm/sessionUtils.js
CHANGED
|
@@ -235,17 +235,14 @@ function withoutBaseAssemblies(sessionAssemblies, baseAssemblyNames) {
|
|
|
235
235
|
// `withoutBaseAssemblies` drops to keep the recipient's safeReferences
|
|
236
236
|
// unambiguous.
|
|
237
237
|
//
|
|
238
|
-
// Edited-ness is `
|
|
239
|
-
//
|
|
238
|
+
// Edited-ness is `editedDelta`, the same gate `splitTracksAgainstBase` uses, so
|
|
239
|
+
// an export can't call an assembly edited on a diff too thin to be worth
|
|
240
240
|
// shipping for a track.
|
|
241
|
-
function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies) {
|
|
241
|
+
function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies, form) {
|
|
242
242
|
const baseByName = new Map(baseAssemblies.map(a => [a.name, a]));
|
|
243
243
|
const edited = assemblies.flatMap(assembly => {
|
|
244
244
|
const base = baseByName.get(assembly.name);
|
|
245
|
-
return base &&
|
|
246
|
-
flattenTrackConfigDelta(base, diffTrackConfig(base, assembly)).length > 0
|
|
247
|
-
? [assembly.name]
|
|
248
|
-
: [];
|
|
245
|
+
return base && editedDelta(base, assembly, form) ? [assembly.name] : [];
|
|
249
246
|
});
|
|
250
247
|
const shadowed = sessionAssemblies.flatMap(a => {
|
|
251
248
|
const name = readAssemblyName(a);
|
|
@@ -275,17 +272,19 @@ function droppableTrackIds(tracks, sessionTracks) {
|
|
|
275
272
|
// (`addTrackTextSearchConf`), so indexing a remote BAM on desktop is enough to
|
|
276
273
|
// make an otherwise perfectly portable track non-portable and get it dropped
|
|
277
274
|
// whole. The data file is the track; the index is a search box over it. Shedding
|
|
278
|
-
// the index costs the recipient that box and nothing else
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
275
|
+
// the index costs the recipient that box and nothing else.
|
|
276
|
+
//
|
|
277
|
+
// A hub track takes the hub's own index in its place, so its delta records no
|
|
278
|
+
// change there and the recipient searches with the hub's index. Leaving the
|
|
279
|
+
// member out would be a reset of it.
|
|
280
|
+
function withoutLocalTextSearch(track, hubIndex) {
|
|
282
281
|
if (!isRecord(track) ||
|
|
283
282
|
!isRecord(track.textSearching) ||
|
|
284
283
|
analyzeWebPortability(track.textSearching).portable) {
|
|
285
284
|
return track;
|
|
286
285
|
}
|
|
287
286
|
const { textSearching, ...rest } = track;
|
|
288
|
-
return rest;
|
|
287
|
+
return (hubIndex === undefined ? rest : { ...rest, textSearching: hubIndex });
|
|
289
288
|
}
|
|
290
289
|
// Display names of the tracks `withoutLocalTextSearch` stripped, for the report.
|
|
291
290
|
function strippedTextIndexNames(before, after) {
|
|
@@ -329,23 +328,26 @@ function nonPortableUserTracks(report, droppable) {
|
|
|
329
328
|
// but the delta channel it travels in means "user override" on the far side, and
|
|
330
329
|
// a delta masks later admin changes to that track. Telling the two apart needs
|
|
331
330
|
// the base as it was at session creation, which nothing persists.
|
|
332
|
-
function splitTracksAgainstBase(tracks, baseTracks) {
|
|
331
|
+
function splitTracksAgainstBase(tracks, baseTracks, form) {
|
|
333
332
|
const baseById = new Map(baseTracks.map(t => [t.trackId, t]));
|
|
334
333
|
const addedTracks = tracks.filter(t => !baseById.has(t.trackId));
|
|
335
334
|
const editDeltas = Object.fromEntries(tracks.flatMap((track) => {
|
|
336
335
|
const base = baseById.get(track.trackId);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
const delta = diffTrackConfig(base, track);
|
|
341
|
-
// gate on real slot changes, not the identity keys / injected display
|
|
342
|
-
// stubs a raw diff carries (matches the web session's "is edited" test)
|
|
343
|
-
return flattenTrackConfigDelta(base, delta).length > 0
|
|
344
|
-
? [[track.trackId, delta]]
|
|
345
|
-
: [];
|
|
336
|
+
const delta = base && editedDelta(base, track, form);
|
|
337
|
+
return delta ? [[track.trackId, delta]] : [];
|
|
346
338
|
}));
|
|
347
339
|
return { addedTracks, editDeltas };
|
|
348
340
|
}
|
|
341
|
+
// The delta from `base` to `edited` with both in their hydrated form, or
|
|
342
|
+
// undefined when no setting changed — the web session's own "is edited" test,
|
|
343
|
+
// which a bare key count fails on the identity keys and display stubs
|
|
344
|
+
function editedDelta(base, edited, form) {
|
|
345
|
+
const hydratedBase = form(base);
|
|
346
|
+
const delta = diffTrackConfig(hydratedBase, form(edited));
|
|
347
|
+
return flattenTrackConfigDelta(hydratedBase, delta).length > 0
|
|
348
|
+
? delta
|
|
349
|
+
: undefined;
|
|
350
|
+
}
|
|
349
351
|
// Overlays edited-track deltas onto the session's trackConfigDeltas, preserving
|
|
350
352
|
// any the base session already carried. A no-op (returns the session unchanged)
|
|
351
353
|
// when there are no edits, so the exported snapshot stays minimal.
|
|
@@ -443,18 +445,27 @@ function withCarriedConfigs(session, plugins, connections) {
|
|
|
443
445
|
// Decides how to hand a desktop session to jbrowse-web. When the session was
|
|
444
446
|
// launched from a hosted hub config (sourceConfigUrl) that still covers all of
|
|
445
447
|
// its assemblies, that config is reused as the base and only user-added/edited
|
|
446
|
-
// tracks ride along; otherwise the session is made self-contained. `
|
|
447
|
-
//
|
|
448
|
+
// tracks ride along; otherwise the session is made self-contained. `base` holds
|
|
449
|
+
// the fetched hub config (already rebased with addRelativeUris by the caller),
|
|
448
450
|
// used to tell hub tracks from user-added ones and to diff edited hub tracks.
|
|
449
|
-
export function planWebExport(snapshot,
|
|
451
|
+
export function planWebExport(snapshot, base) {
|
|
450
452
|
const sourceConfigUrl = snapshot.configuration?.sourceConfigUrl;
|
|
451
453
|
const assemblies = snapshot.assemblies ?? [];
|
|
452
454
|
const defaultSession = snapshot.defaultSession ?? {};
|
|
453
455
|
const priorSessionAssemblies = asArray(defaultSession.sessionAssemblies);
|
|
454
456
|
const inputTracks = snapshot.tracks ?? [];
|
|
455
457
|
const inputSessionTracks = asArray(defaultSession.sessionTracks);
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
+
const baseConfig = base?.config;
|
|
459
|
+
const baseAssemblyNames = new Set((baseConfig?.assemblies ?? []).map(a => a.name));
|
|
460
|
+
const coveredByBase = !!sourceConfigUrl &&
|
|
461
|
+
!!base &&
|
|
462
|
+
assemblies.every(a => baseAssemblyNames.has(a.name));
|
|
463
|
+
const hubIndexes = new Map((coveredByBase ? (base.config.tracks ?? []) : []).map(t => [
|
|
464
|
+
t.trackId,
|
|
465
|
+
t.textSearching,
|
|
466
|
+
]));
|
|
467
|
+
const allTracks = inputTracks.map(t => withoutLocalTextSearch(t, hubIndexes.get(t.trackId)));
|
|
468
|
+
const allSessionTracks = inputSessionTracks.map(t => withoutLocalTextSearch(t));
|
|
458
469
|
// over the stripped lists, so a track whose only local file was its own search
|
|
459
470
|
// index is no longer read as one that has to be dropped
|
|
460
471
|
const report = analyzeWebPortability({
|
|
@@ -469,11 +480,7 @@ export function planWebExport(snapshot, baseConfig) {
|
|
|
469
480
|
};
|
|
470
481
|
const tracks = allTracks.filter(keep);
|
|
471
482
|
const priorSessionTracks = allSessionTracks.filter(keep);
|
|
472
|
-
|
|
473
|
-
const coveredByBase = !!sourceConfigUrl &&
|
|
474
|
-
!!baseConfig &&
|
|
475
|
-
assemblies.every(a => baseAssemblyNames.has(a.name));
|
|
476
|
-
// `baseConfig` is undefined only because the caller could not fetch
|
|
483
|
+
// `base` is undefined only because the caller could not fetch
|
|
477
484
|
// `sourceConfigUrl` — it is the same fetch that produces it — so the two ways
|
|
478
485
|
// a hub drops out of the plan are distinguishable here without a flag.
|
|
479
486
|
const selfContainedReason = coveredByBase
|
|
@@ -488,7 +495,7 @@ export function planWebExport(snapshot, baseConfig) {
|
|
|
488
495
|
// defined exactly when the hostedConfigBase strategy applies, so it doubles as
|
|
489
496
|
// the strategy flag for the shared tail below
|
|
490
497
|
const hosted = coveredByBase
|
|
491
|
-
? splitTracksAgainstBase(tracks,
|
|
498
|
+
? splitTracksAgainstBase(tracks, base.config.tracks ?? [], base.forms.track)
|
|
492
499
|
: undefined;
|
|
493
500
|
const session = withCarriedConfigs(hosted
|
|
494
501
|
? withDeltas({
|
|
@@ -525,8 +532,8 @@ export function planWebExport(snapshot, baseConfig) {
|
|
|
525
532
|
...strippedTextIndexNames(inputSessionTracks, allSessionTracks),
|
|
526
533
|
]),
|
|
527
534
|
],
|
|
528
|
-
revertedAssemblies:
|
|
529
|
-
? assembliesRevertedToBase(assemblies, priorSessionAssemblies,
|
|
535
|
+
revertedAssemblies: coveredByBase
|
|
536
|
+
? assembliesRevertedToBase(assemblies, priorSessionAssemblies, base.config.assemblies ?? [], base.forms.assembly)
|
|
530
537
|
: [],
|
|
531
538
|
unavailableAccounts: unavailableAccountIds(session, hosted ? (baseConfig?.internetAccounts ?? []) : []),
|
|
532
539
|
};
|
package/esm/ui/AboutDialog.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
|
|
2
|
+
import type { AbstractSessionModel } from '@jbrowse/core/util';
|
|
3
|
+
export default function AboutDialog({ config, session, handleClose, }: {
|
|
4
|
+
session: AbstractSessionModel;
|
|
5
|
+
/**
|
|
6
|
+
* the in-view track label passes `track.configuration`, a live node; the
|
|
7
|
+
* hierarchical selector passes a `session.tracks` entry, a plain object
|
|
8
|
+
* until something references the track
|
|
9
|
+
*/
|
|
10
|
+
config: AnyConfigurationModel | Record<string, unknown>;
|
|
3
11
|
handleClose: () => void;
|
|
4
12
|
}): import("react").JSX.Element;
|
package/esm/ui/AboutDialog.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
|
|
3
|
+
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
4
|
+
import { hydrateTrackConfig } from '@jbrowse/core/configuration';
|
|
2
5
|
import { PluggableComponent } from '@jbrowse/core/ui';
|
|
3
6
|
import Dialog from '@jbrowse/core/ui/Dialog';
|
|
4
7
|
import { getEnv } from '@jbrowse/core/util';
|
|
5
8
|
import { getTrackName } from '@jbrowse/core/util/tracks';
|
|
9
|
+
import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
6
10
|
import AboutContents from './AboutDialogContents.js';
|
|
7
11
|
export default function AboutDialog({ config, session, handleClose, }) {
|
|
8
|
-
const trackName = getTrackName(config, session);
|
|
9
12
|
const { pluginManager } = getEnv(session);
|
|
10
|
-
|
|
13
|
+
// hydrated once here, so every panel and every plugin on the two About points
|
|
14
|
+
// reads one live node: "Copy config" gives the same JSON whichever menu
|
|
15
|
+
// opened the dialog, and a jexl `formatAbout` slot has an env to evaluate in
|
|
16
|
+
const live = isStateTreeNode(config)
|
|
17
|
+
? config
|
|
18
|
+
: hydrateTrackConfig(pluginManager, config);
|
|
19
|
+
return (_jsx(Dialog, { open: true, onClose: handleClose, title: getTrackName(config, session), maxWidth: "xl", children: live ? (_jsx(PluggableComponent, { pluginManager: pluginManager, name: "Core-replaceAbout", component: AboutContents, props: { config: live, session } })) : (_jsx(BaseCard, { title: "Configuration", children: _jsx(Attributes, { attributes: config }) })) }));
|
|
11
20
|
}
|
|
@@ -2,11 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
|
|
4
4
|
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
5
|
-
import {
|
|
5
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
6
6
|
import PluggableComponents from '@jbrowse/core/ui/PluggableComponents';
|
|
7
7
|
import { getEnv } from '@jbrowse/core/util';
|
|
8
8
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
9
|
-
import { getSnapshot, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
10
9
|
import { observer } from 'mobx-react';
|
|
11
10
|
import AssemblyInfoPanel from './AssemblyInfoPanel.js';
|
|
12
11
|
import DescriptionPanel from './DescriptionPanel.js';
|
|
@@ -24,25 +23,8 @@ const AboutDialogContents = observer(function AboutDialogContents({ config, sess
|
|
|
24
23
|
const { classes } = useStyles();
|
|
25
24
|
const [showRefNames, setShowRefNames] = useState(false);
|
|
26
25
|
const { pluginManager } = getEnv(session);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// hierarchical selector passes a `session.tracks` entry, which is a
|
|
30
|
-
// `types.frozen` plain object until something references the track. Hydrating
|
|
31
|
-
// converges them, so "Copy config" gives the same JSON whichever menu you came
|
|
32
|
-
// from; it returns undefined for a config no plugin can build, and that falls
|
|
33
|
-
// back to copying it as authored.
|
|
34
|
-
const live = isStateTreeNode(config)
|
|
35
|
-
? config
|
|
36
|
-
: hydrateTrackConfig(pluginManager, config);
|
|
37
|
-
const conf = live
|
|
38
|
-
? getSnapshot(live)
|
|
39
|
-
: config;
|
|
40
|
-
const { config: shown, hideUris } = getAboutDialogConfig({
|
|
41
|
-
config,
|
|
42
|
-
session,
|
|
43
|
-
pluginManager,
|
|
44
|
-
});
|
|
45
|
-
return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: conf, hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
|
|
26
|
+
const { config: shown, hideUris } = getAboutDialogConfig({ config, session });
|
|
27
|
+
return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: readConfObject(config), hideUris: hideUris, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
|
|
46
28
|
/** #extensionPoint Core-extraAboutPanel | sync | Add extra panels to a track's About dialog */
|
|
47
29
|
name: "Core-extraAboutPanel", props: { session, config } }), hideUris ? null : _jsx(FileInfoPanel, { config: config, session: session }), showRefNames ? (_jsx(RefNameInfoDialog, { session: session, config: config, onClose: () => {
|
|
48
30
|
setShowRefNames(false);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AbstractSessionModel } from '@jbrowse/core/util/types';
|
|
2
|
+
/**
|
|
3
|
+
* One element that says whether the whole app has finished, and what is open.
|
|
4
|
+
*
|
|
5
|
+
* `[data-app-phase="ready"]` is the entire readiness contract for anything
|
|
6
|
+
* driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
|
|
7
|
+
* that selector and you are done; there is no chain to assemble and no
|
|
8
|
+
* per-display census to take.
|
|
9
|
+
*
|
|
10
|
+
* Why it exists when `data-view-phase` and `data-display-phase` already do:
|
|
11
|
+
* those are per-element and NEGATIVE. Asking them "is everything finished"
|
|
12
|
+
* means asserting that no element carries the loading value, which is equally
|
|
13
|
+
* true of an app that has not started — and the gap is real, measured at about
|
|
14
|
+
* a second on a two-track session. A positive attribute cannot be satisfied
|
|
15
|
+
* early, because something has to render it.
|
|
16
|
+
*
|
|
17
|
+
* The census attributes beside it — `data-app-views`, `data-app-assemblies`,
|
|
18
|
+
* `data-app-tracks` (the latter two JSON arrays) — publish WHAT is open the
|
|
19
|
+
* same way the phase publishes whether it is done, so an outside reader asking
|
|
20
|
+
* "is the track I requested actually open" reads one element instead of
|
|
21
|
+
* walking `window.JBrowseSession` with its own copy of the view nesting. The
|
|
22
|
+
* walk itself is `openViews`/`openTracks` over each view's declared
|
|
23
|
+
* `ownViews`/`ownTracks`, so this component knows nothing about which property
|
|
24
|
+
* a container view keeps its children on either.
|
|
25
|
+
*
|
|
26
|
+
* Its own component, and `hidden`, for two reasons: an observer here subscribes
|
|
27
|
+
* to every view's phase without re-rendering the app shell around it, and a
|
|
28
|
+
* marker that is never drawn cannot be mistaken for UI or shift a layout. It is
|
|
29
|
+
* still found by `querySelector`, which is what reads it.
|
|
30
|
+
*
|
|
31
|
+
* **What it is silent about**: a display in a TERMINAL state. `error` and
|
|
32
|
+
* `canceled` are finished states, so this reads `ready` over a display whose
|
|
33
|
+
* fetch failed or that the user stopped — correctly, since nothing is still
|
|
34
|
+
* working, and a user's cancel is durable until Retry. A capture wants more than
|
|
35
|
+
* that: it wants a picture, and neither banner is one. The capture census after
|
|
36
|
+
* this is what draws that line: `waitForDisplaysDone` keys on the
|
|
37
|
+
* `data-display-drawn` the two comparative canvases publish from their stricter
|
|
38
|
+
* `settled` gate (see `comparativeReadiness` in `@jbrowse/synteny-core`, which
|
|
39
|
+
* holds both answers and says why an error separates them), and the census
|
|
40
|
+
* names every `data-display-phase="canceled"`.
|
|
41
|
+
*/
|
|
42
|
+
declare const AppReadyMarker: ({ session, }: {
|
|
43
|
+
session: AbstractSessionModel;
|
|
44
|
+
}) => import("react").JSX.Element;
|
|
45
|
+
export default AppReadyMarker;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { openTracks, openViews } from '@jbrowse/core/util/openViews';
|
|
3
|
+
import { observer } from 'mobx-react';
|
|
4
|
+
function trackLoading(track) {
|
|
5
|
+
return track.displays.some(d => d.displayPhase === 'loading');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A view whose init failed (its assembly was never found) stays uninitialized
|
|
9
|
+
* for good and paints its error, so like a display's terminal phase it is
|
|
10
|
+
* finished, not pending: without this the whole app read `loading` for as long
|
|
11
|
+
* as that view was open, and every settle answered false with no reason.
|
|
12
|
+
*
|
|
13
|
+
* `pendingLaunch` is the third term and the one that was missing. `initialized`
|
|
14
|
+
* goes true mid-launch — LGV's flips the moment displayedRegions land, while
|
|
15
|
+
* the same apply pass still has the spec's tracks to attach — and `showLoading`
|
|
16
|
+
* goes false at the same moment, deliberately, so the reader sees the
|
|
17
|
+
* navigated view rather than a spinner while its tracks arrive. That left a
|
|
18
|
+
* window, exactly as wide as attaching the tracks, in which the marker said
|
|
19
|
+
* `ready` over a session missing everything it had been asked for.
|
|
20
|
+
* `whenViewSettled` states the same rule for the callers that await a single
|
|
21
|
+
* view, and says why in more detail. Not shared as one predicate: that one
|
|
22
|
+
* requires `initialized`, while absent here means not loading, and an adapter
|
|
23
|
+
* between the two null rules would read as agreement it does not have.
|
|
24
|
+
*
|
|
25
|
+
* `showImportForm` excuses the `initialized` term, and only that one. The
|
|
26
|
+
* comparative views spell `initialized` as "every row exists and is ready" —
|
|
27
|
+
* LinearSyntenyView's wants `views.length > 0`, dotplot's wants regions on both
|
|
28
|
+
* axes — so a view clicked into existence through `Add -> Linear synteny view`
|
|
29
|
+
* reads uninitialized for as long as its form is up, which is until a person
|
|
30
|
+
* fills it in. The app sat at `loading` behind every import-form figure. The
|
|
31
|
+
* other two terms keep their meaning: `showLoading` is the spinner the form
|
|
32
|
+
* yields to, and `showImportForm` is already false while a launch is pending.
|
|
33
|
+
*/
|
|
34
|
+
function viewLoading(view) {
|
|
35
|
+
const flags = view;
|
|
36
|
+
return (flags.error === undefined &&
|
|
37
|
+
(flags.showLoading === true ||
|
|
38
|
+
flags.pendingLaunch !== undefined ||
|
|
39
|
+
(flags.initialized === false && flags.showImportForm !== true)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One element that says whether the whole app has finished, and what is open.
|
|
43
|
+
*
|
|
44
|
+
* `[data-app-phase="ready"]` is the entire readiness contract for anything
|
|
45
|
+
* driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
|
|
46
|
+
* that selector and you are done; there is no chain to assemble and no
|
|
47
|
+
* per-display census to take.
|
|
48
|
+
*
|
|
49
|
+
* Why it exists when `data-view-phase` and `data-display-phase` already do:
|
|
50
|
+
* those are per-element and NEGATIVE. Asking them "is everything finished"
|
|
51
|
+
* means asserting that no element carries the loading value, which is equally
|
|
52
|
+
* true of an app that has not started — and the gap is real, measured at about
|
|
53
|
+
* a second on a two-track session. A positive attribute cannot be satisfied
|
|
54
|
+
* early, because something has to render it.
|
|
55
|
+
*
|
|
56
|
+
* The census attributes beside it — `data-app-views`, `data-app-assemblies`,
|
|
57
|
+
* `data-app-tracks` (the latter two JSON arrays) — publish WHAT is open the
|
|
58
|
+
* same way the phase publishes whether it is done, so an outside reader asking
|
|
59
|
+
* "is the track I requested actually open" reads one element instead of
|
|
60
|
+
* walking `window.JBrowseSession` with its own copy of the view nesting. The
|
|
61
|
+
* walk itself is `openViews`/`openTracks` over each view's declared
|
|
62
|
+
* `ownViews`/`ownTracks`, so this component knows nothing about which property
|
|
63
|
+
* a container view keeps its children on either.
|
|
64
|
+
*
|
|
65
|
+
* Its own component, and `hidden`, for two reasons: an observer here subscribes
|
|
66
|
+
* to every view's phase without re-rendering the app shell around it, and a
|
|
67
|
+
* marker that is never drawn cannot be mistaken for UI or shift a layout. It is
|
|
68
|
+
* still found by `querySelector`, which is what reads it.
|
|
69
|
+
*
|
|
70
|
+
* **What it is silent about**: a display in a TERMINAL state. `error` and
|
|
71
|
+
* `canceled` are finished states, so this reads `ready` over a display whose
|
|
72
|
+
* fetch failed or that the user stopped — correctly, since nothing is still
|
|
73
|
+
* working, and a user's cancel is durable until Retry. A capture wants more than
|
|
74
|
+
* that: it wants a picture, and neither banner is one. The capture census after
|
|
75
|
+
* this is what draws that line: `waitForDisplaysDone` keys on the
|
|
76
|
+
* `data-display-drawn` the two comparative canvases publish from their stricter
|
|
77
|
+
* `settled` gate (see `comparativeReadiness` in `@jbrowse/synteny-core`, which
|
|
78
|
+
* holds both answers and says why an error separates them), and the census
|
|
79
|
+
* names every `data-display-phase="canceled"`.
|
|
80
|
+
*/
|
|
81
|
+
const AppReadyMarker = observer(function AppReadyMarker({ session, }) {
|
|
82
|
+
const views = openViews(session);
|
|
83
|
+
const tracks = openTracks(session);
|
|
84
|
+
const loading = views.some(viewLoading) || tracks.some(trackLoading);
|
|
85
|
+
return (_jsx("span", { hidden: true, "data-testid": "app-ready-marker", "data-app-phase": loading ? 'loading' : 'ready', "data-app-views": session.views.length, "data-app-assemblies": JSON.stringify([
|
|
86
|
+
...new Set(views.flatMap(v => v.assemblyNames ?? [])),
|
|
87
|
+
]), "data-app-tracks": JSON.stringify(tracks.map(t => t.configuration.trackId ?? '(unnamed)')) }));
|
|
88
|
+
});
|
|
89
|
+
export default AppReadyMarker;
|
|
@@ -2,11 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
|
|
4
4
|
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
5
|
-
import { readConfObject
|
|
5
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
6
6
|
import { CopyToClipboardButton } from '@jbrowse/core/ui';
|
|
7
7
|
import { stripBaseUris } from '@jbrowse/core/util/addRelativeUris';
|
|
8
8
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
9
|
-
import { getParent, hasParent
|
|
9
|
+
import { getParent, hasParent } from '@jbrowse/mobx-state-tree';
|
|
10
10
|
import { Button } from '@mui/material';
|
|
11
11
|
import { observer } from 'mobx-react';
|
|
12
12
|
import RefNameAliasesDialog from './RefNameAliasesDialog.js';
|
|
@@ -39,15 +39,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
39
39
|
* holding it *is* its assembly, the same walk `getConfAssemblyNames` makes.
|
|
40
40
|
* Compared by identity against that node's `sequence`, so the card is the
|
|
41
41
|
* assembly of the track the dialog is already showing.
|
|
42
|
-
*
|
|
43
|
-
* Both menus that open this dialog hand over a live node here: the hierarchical
|
|
44
|
-
* selector reads `assemblyManager.get(name).configuration.sequence`, and the
|
|
45
|
-
* in-view track label's `track.configuration` resolves to that same node
|
|
46
|
-
* through `getTrackById`. The frozen `session.tracks` entry the rest of the
|
|
47
|
-
* dialog has to cope with is never a reference sequence track.
|
|
48
42
|
*/
|
|
49
43
|
function getAssemblyConf(config) {
|
|
50
|
-
const parent =
|
|
44
|
+
const parent = hasParent(config)
|
|
51
45
|
? getParent(config)
|
|
52
46
|
: undefined;
|
|
53
47
|
return parent?.sequence === config ? parent : undefined;
|
|
@@ -58,7 +52,7 @@ const AssemblyCard = observer(function AssemblyCard({ assemblyConf, session, hid
|
|
|
58
52
|
const conf = readConfObject(assemblyConf);
|
|
59
53
|
return (_jsxs(BaseCard, { title: "Assembly", children: [_jsxs("span", { className: classes.buttons, children: [_jsx(Button, { variant: "contained", color: "secondary", onClick: () => {
|
|
60
54
|
setShowAliases(true);
|
|
61
|
-
}, children: "Show ref name aliases" }), hideUris ? null : (_jsx(CopyToClipboardButton, { variant: "contained", value: () => JSON.stringify(stripBaseUris(structuredClone(conf)), null, 2), children: "Copy assembly config" }))] }), _jsx(Attributes, { attributes: conf, omit: hideFields, hideUris: hideUris }), showAliases ? (_jsx(RefNameAliasesDialog, { assemblyName:
|
|
55
|
+
}, children: "Show ref name aliases" }), hideUris ? null : (_jsx(CopyToClipboardButton, { variant: "contained", value: () => JSON.stringify(stripBaseUris(structuredClone(conf)), null, 2), children: "Copy assembly config" }))] }), _jsx(Attributes, { attributes: conf, omit: hideFields, hideUris: hideUris }), showAliases ? (_jsx(RefNameAliasesDialog, { assemblyName: readConfObject(assemblyConf, 'name'), session: session, onClose: () => {
|
|
62
56
|
setShowAliases(false);
|
|
63
57
|
} })) : null] }));
|
|
64
58
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
3
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
3
4
|
import { ErrorBanner, LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui';
|
|
4
5
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
5
6
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
@@ -18,7 +19,7 @@ const DescriptionCard = observer(function DescriptionCard({ url, }) {
|
|
|
18
19
|
return (_jsxs(BaseCard, { title: "Description", children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", component: "div", className: classes.provenance, children: ["From the track hub, written for the UCSC Genome Browser \u2014 some display options it describes differ in JBrowse.", ' ', _jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: "View original" })] }), error ? (_jsx(ErrorBanner, { error: error })) : isLoading || html === undefined ? (_jsx(LoadingEllipses, { message: "Loading description" })) : (_jsx(SanitizedHTML, { html: html }))] }));
|
|
19
20
|
});
|
|
20
21
|
const DescriptionPanel = observer(function DescriptionPanel({ config, }) {
|
|
21
|
-
const url = getDescriptionHtmlUrl(config);
|
|
22
|
+
const url = getDescriptionHtmlUrl(readConfObject(config, 'metadata'));
|
|
22
23
|
return url ? _jsx(DescriptionCard, { url: url }) : null;
|
|
23
24
|
});
|
|
24
25
|
export default DescriptionPanel;
|
package/esm/ui/FileInfoPanel.js
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes';
|
|
3
3
|
import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
|
|
4
|
-
import {
|
|
4
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
5
5
|
import { ErrorBanner, LoadingEllipses } from '@jbrowse/core/ui';
|
|
6
6
|
import { statusProgressLabel } from '@jbrowse/core/util';
|
|
7
7
|
import { useFetch } from '@jbrowse/core/util/useFetch';
|
|
8
8
|
import { observer } from 'mobx-react';
|
|
9
|
-
// Inline `observer(function(){})`, not a bare declaration: this reads
|
|
10
|
-
// observables (session.rpcManager, and config slots off what is usually a live
|
|
11
|
-
// MST node), and babel-plugin-react-compiler compiles the bare form and can
|
|
12
|
-
// memoize such a read into staleness. RefNameInfoDialog beside it is written
|
|
13
|
-
// the same way; see the React Compiler x MobX note in CLAUDE.md
|
|
14
9
|
const FileInfoPanel = observer(function FileInfoPanel({ config, session, }) {
|
|
15
10
|
const { rpcManager } = session;
|
|
16
|
-
const trackId =
|
|
11
|
+
const trackId = readConfObject(config, 'trackId');
|
|
17
12
|
const { data: info, error, isLoading, status, } = useFetch(['CoreGetInfo', trackId],
|
|
18
13
|
// reading a header can mean walking a v8 .hic's norm-vector index or a
|
|
19
14
|
// multi-megabyte VCF header, so it gets both handles: closing the About
|
|
20
15
|
// dialog stops it, and the wait says what it is
|
|
21
|
-
async (_name, _trackId,
|
|
22
|
-
adapterConfig:
|
|
23
|
-
|
|
16
|
+
async (_name, _trackId, signal, statusCallback) => (await rpcManager.call(trackId, 'CoreGetInfo', {
|
|
17
|
+
adapterConfig: readConfObject(config, 'adapter'),
|
|
18
|
+
signal,
|
|
24
19
|
statusCallback,
|
|
25
20
|
})));
|
|
26
21
|
const details = typeof info === 'string'
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
3
3
|
import { CopyToClipboardButton, Dialog, ErrorBanner, LoadingEllipses, } from '@jbrowse/core/ui';
|
|
4
4
|
import { createStatusFanOut, statusProgressLabel } from '@jbrowse/core/util';
|
|
5
5
|
import { getConfAssemblyNames } from '@jbrowse/core/util/tracks';
|
|
6
6
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
7
7
|
import { useFetch } from '@jbrowse/core/util/useFetch';
|
|
8
|
-
import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
9
8
|
import { DialogContent } from '@mui/material';
|
|
10
9
|
import { observer } from 'mobx-react';
|
|
11
10
|
const MAX_REF_NAMES = 10_000;
|
|
@@ -32,36 +31,24 @@ function formatRefNames(data, truncate) {
|
|
|
32
31
|
.filter(s => s !== '')
|
|
33
32
|
.join('\n');
|
|
34
33
|
}
|
|
35
|
-
/**
|
|
36
|
-
* A `ReferenceSequenceTrack` config declares no `assemblyNames` slot at all —
|
|
37
|
-
* `createReferenceSeqTrackConfig` omits it deliberately, because such a track's
|
|
38
|
-
* assembly is the config node holding it. `getConfAssemblyNames` is the shared
|
|
39
|
-
* resolver that walks to that parent, and reading the slot directly instead is
|
|
40
|
-
* what left "Show ref names" on every reference sequence track loading forever:
|
|
41
|
-
* `undefined` serialized into the fetch key, which `useFetch` reads as "don't
|
|
42
|
-
* fetch". It throws when a config has neither, which is why this is called
|
|
43
|
-
* inside the fetcher — an unanswerable question belongs in the error banner
|
|
44
|
-
* rather than thrown out of a render.
|
|
45
|
-
*/
|
|
46
|
-
function aboutAssemblyNames(config) {
|
|
47
|
-
return isStateTreeNode(config)
|
|
48
|
-
? getConfAssemblyNames(config)
|
|
49
|
-
: (readConfSlot(config, 'assemblyNames') ?? []);
|
|
50
|
-
}
|
|
51
34
|
const RefNameInfoDialog = observer(function RefNameInfoDialog({ config, session, onClose, }) {
|
|
52
35
|
const { classes } = useStyles();
|
|
53
36
|
const { rpcManager } = session;
|
|
54
|
-
const trackId =
|
|
55
|
-
const { data, error, isLoading, status } = useFetch(['CoreGetRefNames', trackId], (_name, _trackId,
|
|
37
|
+
const trackId = readConfObject(config, 'trackId');
|
|
38
|
+
const { data, error, isLoading, status } = useFetch(['CoreGetRefNames', trackId], (_name, _trackId, signal, statusCallback) => {
|
|
56
39
|
// one status slot per assembly, so N concurrent reads aggregate into one
|
|
57
40
|
// bar instead of the last writer winning
|
|
58
41
|
const slot = createStatusFanOut(statusCallback);
|
|
59
|
-
|
|
42
|
+
// A `ReferenceSequenceTrack` config declares no `assemblyNames` slot: its
|
|
43
|
+
// assembly is the config node holding it, which `getConfAssemblyNames`
|
|
44
|
+
// walks to. It throws when a config has neither, and inside the fetcher
|
|
45
|
+
// that lands in the error banner rather than out of a render
|
|
46
|
+
return Promise.all([...new Set(getConfAssemblyNames(config))].map(async (assemblyName) => [
|
|
60
47
|
assemblyName,
|
|
61
48
|
await rpcManager.call(trackId, 'CoreGetRefNames', {
|
|
62
|
-
adapterConfig:
|
|
49
|
+
adapterConfig: readConfObject(config, 'adapter'),
|
|
63
50
|
assemblyName,
|
|
64
|
-
|
|
51
|
+
signal,
|
|
65
52
|
statusCallback: slot(),
|
|
66
53
|
}),
|
|
67
54
|
]));
|