@jbrowse/app-core 4.3.0 → 5.0.0-beta.10
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/AppSession/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
- package/esm/Assemblies/AssembliesMixin.js +154 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +11 -0
- package/esm/HistoryManagement/index.js +43 -10
- package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
- package/esm/JBrowseConfig/RootConfiguration.js +79 -7
- package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
- package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
- package/esm/JBrowseConfig/index.d.ts +175 -84
- package/esm/JBrowseConfig/index.js +64 -2
- package/esm/JBrowseModel/index.d.ts +175 -97
- package/esm/JBrowseModel/index.js +132 -31
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +2767 -0
- package/esm/JbApi/jbApi.js +1383 -0
- package/esm/RootMenu/index.d.ts +69 -3
- package/esm/RootMenu/index.js +96 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +161 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
- package/esm/SessionSpec/loadSessionSpec.js +473 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
- package/esm/SessionSpec/types.d.ts +37 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +260 -0
- package/esm/WorkspaceLayout/model.js +586 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +88 -0
- package/esm/WorkspaceLayout/spec.js +226 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +18 -9
- package/esm/index.js +16 -9
- package/esm/menus.d.ts +55 -68
- package/esm/menus.js +117 -86
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +93 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +39 -19
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +45 -31
- package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -11
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ThemeModeButton.d.ts +12 -0
- package/esm/ui/App/ThemeModeButton.js +34 -0
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +128 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +21 -11
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +48 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +4 -11
- package/esm/ui/App/ViewsContainer.js +6 -9
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/lazyParts.d.ts +32 -0
- package/esm/ui/App/lazyParts.js +8 -0
- package/esm/ui/App/preloadAppShell.d.ts +14 -0
- package/esm/ui/App/preloadAppShell.js +41 -0
- package/esm/ui/App/types.d.ts +17 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +35 -0
- package/esm/ui/App/useViewVisibility.js +88 -0
- package/esm/ui/App/viewTitle.d.ts +12 -0
- package/esm/ui/App/viewTitle.js +17 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerControls.d.ts +0 -5
- package/esm/ui/App/DrawerControls.js +0 -25
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/copyView.d.ts +0 -1
- package/esm/ui/App/copyView.js +0 -29
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
package/esm/ui/App/ViewMenu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { CascadingMenuButton } from '@jbrowse/core/ui';
|
|
3
3
|
import { getSession } from '@jbrowse/core/util';
|
|
4
|
+
import { renameIds } from '@jbrowse/core/util/types/mst';
|
|
4
5
|
import { getSnapshot } from '@jbrowse/mobx-state-tree';
|
|
5
6
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
6
7
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
@@ -11,106 +12,100 @@ import MenuIcon from '@mui/icons-material/Menu';
|
|
|
11
12
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
12
13
|
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
|
13
14
|
import { observer } from 'mobx-react';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
for (const viewIds of session.panelViewAssignments.values()) {
|
|
22
|
-
if (viewIds.includes(viewId)) {
|
|
23
|
-
return viewIds.length;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
const ViewMenu = observer(function ViewMenu({ model, IconProps, }) {
|
|
15
|
+
// takes the icon's class, not an SvgIconProps object: the object was built
|
|
16
|
+
// inline by ViewHeader and so was new on each of its renders, which defeated
|
|
17
|
+
// mobx-react's memo for this whole menu (a Tooltip and an IconButton) every
|
|
18
|
+
// time anything re-rendered the header
|
|
19
|
+
const ViewMenu = observer(function ViewMenu({ model, className, }) {
|
|
29
20
|
const session = getSession(model);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
: session.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
const moves = {
|
|
22
|
+
top: session.moveViewToTop,
|
|
23
|
+
up: session.moveViewUp,
|
|
24
|
+
down: session.moveViewDown,
|
|
25
|
+
bottom: session.moveViewToBottom,
|
|
26
|
+
};
|
|
27
|
+
// Give this view a home of its own: its own tab beside the rest, or its own
|
|
28
|
+
// split to their right. One path, whether or not the workspace is already up
|
|
29
|
+
// — the layout is session state either way, so there is nothing to defer and
|
|
30
|
+
// no second implementation for "the panels do not exist yet". That fork is
|
|
31
|
+
// the reason `init` existed.
|
|
32
|
+
const moveViewOut = (direction) => {
|
|
33
|
+
const allViewIds = session.views.map(v => v.id);
|
|
34
|
+
if (direction === 'tabs') {
|
|
35
|
+
session.moveViewToNewTab(model.id, allViewIds);
|
|
38
36
|
}
|
|
39
37
|
else {
|
|
40
|
-
|
|
38
|
+
session.moveViewToSplitRight(model.id, allViewIds);
|
|
41
39
|
}
|
|
40
|
+
session.setUseWorkspaces(true);
|
|
42
41
|
};
|
|
43
|
-
return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", menuItems: () =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
session.setUseWorkspaces(true);
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
...(viewCount > 2
|
|
72
|
-
? [
|
|
73
|
-
{
|
|
74
|
-
label: 'Move view to top',
|
|
75
|
-
icon: KeyboardDoubleArrowUpIcon,
|
|
76
|
-
onClick: () => {
|
|
77
|
-
moveView(session.moveViewToTopInPanel, session.moveViewToTop);
|
|
78
|
-
},
|
|
42
|
+
return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", tooltip: "View menu", menuItems: () => {
|
|
43
|
+
// The views this move is relative to: in a workspace, the ones sharing
|
|
44
|
+
// this view's panel; in the classic stack, all of them. `session.views`
|
|
45
|
+
// is the order either way, so the mode decides the SCOPE of a move and
|
|
46
|
+
// nothing else. There is one implementation of "move a view" again.
|
|
47
|
+
//
|
|
48
|
+
// Resolved here rather than during render. It scans every panel's
|
|
49
|
+
// assignment list and copies one of them, and reading those lists in
|
|
50
|
+
// render would also subscribe this menu to them, so a view moving
|
|
51
|
+
// between panels anywhere re-rendered every view's menu. None of it is
|
|
52
|
+
// needed until the menu opens.
|
|
53
|
+
const scopeIds = session.effectiveUseWorkspaces
|
|
54
|
+
? session.tabContainingView(model.id)?.tab.viewIds.slice()
|
|
55
|
+
: undefined;
|
|
56
|
+
const viewCount = scopeIds?.length ?? session.views.length;
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
label: 'View options',
|
|
60
|
+
type: 'subMenu',
|
|
61
|
+
subMenu: [
|
|
62
|
+
{
|
|
63
|
+
label: 'Copy view',
|
|
64
|
+
icon: ContentCopyIcon,
|
|
65
|
+
onClick: () => {
|
|
66
|
+
session.addView(model.type, renameIds(getSnapshot(model)));
|
|
79
67
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
icon: KeyboardArrowUpIcon,
|
|
87
|
-
onClick: () => {
|
|
88
|
-
moveView(session.moveViewUpInPanel, session.moveViewUp);
|
|
89
|
-
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Move to new tab',
|
|
71
|
+
icon: OpenInNewIcon,
|
|
72
|
+
onClick: () => {
|
|
73
|
+
moveViewOut('tabs');
|
|
90
74
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'Move to split view (right side of screen)',
|
|
78
|
+
icon: VerticalSplitIcon,
|
|
79
|
+
onClick: () => {
|
|
80
|
+
moveViewOut('horizontal');
|
|
97
81
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
82
|
+
},
|
|
83
|
+
// 'top'/'bottom' only mean something with a view above *and* below
|
|
84
|
+
...[
|
|
85
|
+
['top', 'Move view to top', KeyboardDoubleArrowUpIcon, 2],
|
|
86
|
+
['up', 'Move view up', KeyboardArrowUpIcon, 1],
|
|
87
|
+
['down', 'Move view down', KeyboardArrowDownIcon, 1],
|
|
88
|
+
[
|
|
89
|
+
'bottom',
|
|
90
|
+
'Move view to bottom',
|
|
91
|
+
KeyboardDoubleArrowDownIcon,
|
|
92
|
+
2,
|
|
93
|
+
],
|
|
94
|
+
].flatMap(([direction, label, icon, minViews]) => viewCount > minViews
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
label,
|
|
98
|
+
icon,
|
|
99
|
+
onClick: () => {
|
|
100
|
+
moves[direction](model.id, scopeIds);
|
|
101
|
+
},
|
|
107
102
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
...model.menuItems(),
|
|
108
|
+
];
|
|
109
|
+
}, children: _jsx(MenuIcon, { className: className, fontSize: "small" }) }));
|
|
115
110
|
});
|
|
116
111
|
export default ViewMenu;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
2
|
+
declare const ViewStack: ({ views, session, className, }: {
|
|
3
|
+
views: AbstractViewModel[];
|
|
4
|
+
session: SessionWithFocusedViewAndDrawerWidgets;
|
|
5
|
+
className?: string;
|
|
6
|
+
}) => import("react").JSX.Element;
|
|
7
|
+
export default ViewStack;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { SCROLL_PORT_HEIGHT_VAR, useScrollPortOverflow, } from '@jbrowse/core/util/hooks';
|
|
4
|
+
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import ViewContainer from './ViewContainer.js';
|
|
7
|
+
// What the trailing space is *for*, which is what sets it: `ViewHeader` brings a
|
|
8
|
+
// newly-added view in with `scrollIntoView({ block: 'center' })`, and centering
|
|
9
|
+
// the last view needs half a port of room below it. Half the port is therefore
|
|
10
|
+
// the amount that makes that gesture reach, and any more is room to scroll the
|
|
11
|
+
// content off its own surface.
|
|
12
|
+
//
|
|
13
|
+
// It is scrollable room rather than content, so anything measuring how tall the
|
|
14
|
+
// session is discounts it by its `view-stack-overscroll` testid — `overflow` in
|
|
15
|
+
// jbApi does, and a filmed take paid for that: counted as content, it told
|
|
16
|
+
// `jb.fitToWindow` the session was 300px taller than it was, and the dotplot
|
|
17
|
+
// absorbing the cut came out a third of the height that fit.
|
|
18
|
+
//
|
|
19
|
+
// Capped, because half a port stops being a courtesy on a tall screen. 300px is
|
|
20
|
+
// the cap the classic stack has always had and it stays the answer there — a
|
|
21
|
+
// full window is comfortably past twice it. What the cap was hiding is the
|
|
22
|
+
// workspace panel, where a 2x2 grid's cell can be shorter than 600px and a flat
|
|
23
|
+
// 300px let a view be dragged nearly clear of its own cell.
|
|
24
|
+
const MAX_OVERSCROLL = 300;
|
|
25
|
+
const useStyles = makeStyles()({
|
|
26
|
+
// trailing space so the last view can scroll up past the bottom of a
|
|
27
|
+
// container that would otherwise clip its lower edge.
|
|
28
|
+
//
|
|
29
|
+
// Rendered only once the views themselves overflow, and that is what makes
|
|
30
|
+
// the port's scrollbar honest: kept unconditionally, this alone overflowed
|
|
31
|
+
// the port, so every session — a single short view included — scrolled into
|
|
32
|
+
// nothing and drew a scrollbar saying so. It is outside the measured element
|
|
33
|
+
// for the same reason (see `useScrollPortOverflow`).
|
|
34
|
+
spacer: {
|
|
35
|
+
height: `min(${MAX_OVERSCROLL}px, calc(var(${SCROLL_PORT_HEIGHT_VAR}, 100vh) / 2))`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const ViewStack = observer(function ViewStack({ views, session, className, }) {
|
|
39
|
+
const { classes } = useStyles();
|
|
40
|
+
const viewsRef = useRef(null);
|
|
41
|
+
const scrollable = useScrollPortOverflow(viewsRef);
|
|
42
|
+
// View ids present at first render: these arrived with the page (session
|
|
43
|
+
// restore / initial load), so they must not steal scroll when they mount. A
|
|
44
|
+
// view whose id isn't in this set was added later and auto-scrolls into view.
|
|
45
|
+
const [initialViewIds] = useState(() => new Set(views.map(v => v.id)));
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: viewsRef, className: className, children: views.map(view => (_jsx(ViewContainer, { view: view, session: session, scrollOnMount: !initialViewIds.has(view.id) }, view.id))) }), scrollable ? (_jsx("div", { className: classes.spacer, "data-testid": "view-stack-overscroll" })) : null] }));
|
|
47
|
+
});
|
|
48
|
+
export default ViewStack;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
2
|
+
/**
|
|
3
|
+
* A view's body, and the boundary that keeps a view that throws from being an
|
|
4
|
+
* application-wide failure.
|
|
5
|
+
*
|
|
6
|
+
* Without it the next boundary up is the product's own — jbrowse-web's is in
|
|
7
|
+
* `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
|
|
8
|
+
* every other view the user had open. The reachable case is not hypothetical: a
|
|
9
|
+
* display's reads walk to `getContainingView` and throw when the walk finds no
|
|
10
|
+
* view, which is what ADR-069 is about.
|
|
11
|
+
*
|
|
12
|
+
* No reset keys: a view body unmounts on its own when the view is minimized or
|
|
13
|
+
* scrolls out of the mount band, so remounting is already how a stale banner
|
|
14
|
+
* clears. What the fallback adds is the deliberate ways out — retry, and close.
|
|
15
|
+
*/
|
|
2
16
|
declare const ViewWrapper: ({ view, session, }: {
|
|
3
17
|
view: AbstractViewModel;
|
|
4
18
|
session: SessionWithFocusedViewAndDrawerWidgets;
|
|
5
|
-
}) => import("react
|
|
19
|
+
}) => import("react").JSX.Element;
|
|
6
20
|
export default ViewWrapper;
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Suspense } from 'react';
|
|
3
|
+
import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
|
|
3
4
|
import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses';
|
|
4
5
|
import { getEnv } from '@jbrowse/core/util';
|
|
5
6
|
import { observer } from 'mobx-react';
|
|
6
|
-
|
|
7
|
+
import ViewErrorFallback from './ViewErrorFallback.js';
|
|
8
|
+
// Separate from the boundary below on purpose: `getViewType` throws when no
|
|
9
|
+
// plugin registered the type, and a lookup done in the boundary's own render is
|
|
10
|
+
// a lookup the boundary cannot catch. This is the one thing in a view's render
|
|
11
|
+
// path that fails without any of the view's own code running.
|
|
12
|
+
const ViewComponent = observer(function ViewComponent({ view, session, }) {
|
|
7
13
|
const { pluginManager } = getEnv(session);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const { ReactComponent } = pluginManager.getViewType(view.type);
|
|
15
|
+
return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, { variant: "h6", "data-view-component-pending": "true" }), children: _jsx(ReactComponent, { model: view, session: session }) }));
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* A view's body, and the boundary that keeps a view that throws from being an
|
|
19
|
+
* application-wide failure.
|
|
20
|
+
*
|
|
21
|
+
* Without it the next boundary up is the product's own — jbrowse-web's is in
|
|
22
|
+
* `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
|
|
23
|
+
* every other view the user had open. The reachable case is not hypothetical: a
|
|
24
|
+
* display's reads walk to `getContainingView` and throw when the walk finds no
|
|
25
|
+
* view, which is what ADR-069 is about.
|
|
26
|
+
*
|
|
27
|
+
* No reset keys: a view body unmounts on its own when the view is minimized or
|
|
28
|
+
* scrolls out of the mount band, so remounting is already how a stale banner
|
|
29
|
+
* clears. What the fallback adds is the deliberate ways out — retry, and close.
|
|
30
|
+
*/
|
|
31
|
+
const ViewWrapper = observer(function ViewWrapper({ view, session, }) {
|
|
32
|
+
return (_jsx(ErrorBoundary, { FallbackComponent: ({ error, resetErrorBoundary }) => (_jsx(ViewErrorFallback, { view: view, session: session, error: error, onRetry: resetErrorBoundary })), children: _jsx(ViewComponent, { view: view, session: session }) }));
|
|
17
33
|
});
|
|
18
34
|
export default ViewWrapper;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
session: SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
|
|
6
|
-
renameCurrentSession: (arg: string) => void;
|
|
7
|
-
snackbarMessages: SnackbarMessage[];
|
|
8
|
-
popSnackbarMessage: () => unknown;
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
declare const ViewsContainer: (props: Props) => import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
import type { AppSession } from './types.ts';
|
|
2
|
+
declare const ViewsContainer: ({ session, }: {
|
|
3
|
+
session: AppSession;
|
|
4
|
+
}) => import("react").JSX.Element;
|
|
12
5
|
export default ViewsContainer;
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const ViewLauncher = lazy(() => import("./ViewLauncher.js"));
|
|
5
|
+
import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
|
|
6
|
+
import { ClassicViewsContainer, ViewLauncher, WorkspaceContainer, } from './lazyParts.js';
|
|
8
7
|
const useStyles = makeStyles()({
|
|
9
8
|
viewsContainer: {
|
|
10
9
|
gridRow: 'components',
|
|
11
10
|
overflow: 'hidden',
|
|
12
11
|
},
|
|
13
12
|
});
|
|
14
|
-
const ViewsContainer = observer(function ViewsContainer(
|
|
15
|
-
const {
|
|
16
|
-
const { views } = session;
|
|
13
|
+
const ViewsContainer = observer(function ViewsContainer({ session, }) {
|
|
14
|
+
const { views, effectiveUseWorkspaces } = session;
|
|
17
15
|
const { classes } = useStyles();
|
|
18
|
-
|
|
19
|
-
return (_jsx("div", { className: classes.viewsContainer, children: views.length > 0 ? (_jsx(Suspense, { fallback: null, children: useWorkspaces ? (_jsx(TiledViewsContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session })) })) : (_jsx(Suspense, { fallback: null, children: _jsx(ViewLauncher, { ...props }) })) }));
|
|
16
|
+
return (_jsx("div", { className: classes.viewsContainer, children: _jsx(Suspense, { fallback: null, children: views.length > 0 ? (effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session) ? (_jsx(WorkspaceContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session }))) : (_jsx(ViewLauncher, { session: session })) }) }));
|
|
20
17
|
});
|
|
21
18
|
export default ViewsContainer;
|
package/esm/ui/App/index.d.ts
CHANGED
package/esm/ui/App/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { default as DialogQueue } from
|
|
1
|
+
export { App } from './App.js';
|
|
2
|
+
export { default as DialogQueue } from './DialogQueue.js';
|
|
3
|
+
export { preloadAppFrame, preloadAppShell } from './preloadAppShell.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dynamic import that survives one bad response for its chunk.
|
|
3
|
+
*
|
|
4
|
+
* The retry is not a hopeful second roll: `probeChunk` refetches with
|
|
5
|
+
* `cache: 'reload'`, which both reports what the server really says and
|
|
6
|
+
* replaces whatever the HTTP cache was holding, and webpack clears its
|
|
7
|
+
* `installedChunks` entry on failure — so the second `import()` is a real
|
|
8
|
+
* request against a repaired cache entry. React.lazy cannot do this itself: it
|
|
9
|
+
* stores the rejection and rethrows it for the life of the page without ever
|
|
10
|
+
* calling the factory again, so the recovery has to happen here, before the
|
|
11
|
+
* promise settles.
|
|
12
|
+
*/
|
|
13
|
+
export declare function lazyChunk<T>(name: string, load: () => Promise<T>): () => Promise<T>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a dynamic import so a failed chunk fetch says what actually happened.
|
|
3
|
+
*
|
|
4
|
+
* Webpack flattens every network-level failure into the same string —
|
|
5
|
+
* `Loading chunk X failed.\n(error: <url>)` — where `error` only means the
|
|
6
|
+
* script element fired `error` rather than `load` (a 404, a refused
|
|
7
|
+
* connection and a dropped socket are indistinguishable). That string is what
|
|
8
|
+
* jbrowse-web records via `markCrashedSession`, so it is also all a later boot
|
|
9
|
+
* can show. The immediate refetch below is the part that discriminates:
|
|
10
|
+
* webpack clears its `installedChunks` entry on failure, so the asset is
|
|
11
|
+
* really re-requested, and a 200 here means the asset was always fine and the
|
|
12
|
+
* one request was not.
|
|
13
|
+
*/
|
|
14
|
+
async function probeChunk(url) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(url, { cache: 'reload' });
|
|
17
|
+
const body = await response.text();
|
|
18
|
+
return `refetch: HTTP ${response.status} ${response.statusText}, ${response.headers.get('content-type')}, ${body.length} bytes`;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return `refetch also failed: ${e}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What the browser recorded for the request that failed — the status it
|
|
26
|
+
* actually saw and whether any bytes crossed the network. A `transferSize` of
|
|
27
|
+
* 0 alongside a body size says the response came from the cache rather than
|
|
28
|
+
* the server, which is the one thing `curl` can never show.
|
|
29
|
+
*/
|
|
30
|
+
function resourceTiming(url) {
|
|
31
|
+
const entry = performance
|
|
32
|
+
.getEntriesByName(url)
|
|
33
|
+
.find(e => e instanceof PerformanceResourceTiming);
|
|
34
|
+
return entry
|
|
35
|
+
? {
|
|
36
|
+
responseStatus: entry.responseStatus,
|
|
37
|
+
transferSize: entry.transferSize,
|
|
38
|
+
encodedBodySize: entry.encodedBodySize,
|
|
39
|
+
duration: Math.round(entry.duration),
|
|
40
|
+
}
|
|
41
|
+
: 'no resource timing entry';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The two fields webpack hangs off a ChunkLoadError: the URL it asked for, and
|
|
45
|
+
* whether the script element reported `error` (the request failed), `missing`
|
|
46
|
+
* (it loaded but registered nothing) or `timeout`.
|
|
47
|
+
*/
|
|
48
|
+
function chunkErrorDetail(error) {
|
|
49
|
+
return error instanceof Error &&
|
|
50
|
+
error.name === 'ChunkLoadError' &&
|
|
51
|
+
'request' in error &&
|
|
52
|
+
typeof error.request === 'string'
|
|
53
|
+
? {
|
|
54
|
+
url: error.request,
|
|
55
|
+
type: 'type' in error ? String(error.type) : 'unknown',
|
|
56
|
+
}
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A dynamic import that survives one bad response for its chunk.
|
|
61
|
+
*
|
|
62
|
+
* The retry is not a hopeful second roll: `probeChunk` refetches with
|
|
63
|
+
* `cache: 'reload'`, which both reports what the server really says and
|
|
64
|
+
* replaces whatever the HTTP cache was holding, and webpack clears its
|
|
65
|
+
* `installedChunks` entry on failure — so the second `import()` is a real
|
|
66
|
+
* request against a repaired cache entry. React.lazy cannot do this itself: it
|
|
67
|
+
* stores the rejection and rethrows it for the life of the page without ever
|
|
68
|
+
* calling the factory again, so the recovery has to happen here, before the
|
|
69
|
+
* promise settles.
|
|
70
|
+
*/
|
|
71
|
+
export function lazyChunk(name, load) {
|
|
72
|
+
return () => load().catch(async (error) => {
|
|
73
|
+
const detail = chunkErrorDetail(error);
|
|
74
|
+
if (detail) {
|
|
75
|
+
console.error(`chunk load failed: ${name}`, JSON.stringify({
|
|
76
|
+
...detail,
|
|
77
|
+
online: navigator.onLine,
|
|
78
|
+
at: new Date().toISOString(),
|
|
79
|
+
timing: resourceTiming(detail.url),
|
|
80
|
+
probe: await probeChunk(detail.url),
|
|
81
|
+
}, undefined, 2));
|
|
82
|
+
const retried = await load().catch((retryError) => {
|
|
83
|
+
console.error(`chunk retry failed: ${name}`, retryError);
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
console.error(`chunk retry succeeded: ${name}`);
|
|
87
|
+
return retried;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const DrawerWidget: import("react").LazyExoticComponent<({ session, }: {
|
|
2
|
+
session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
|
|
3
|
+
}) => import("react").JSX.Element> & {
|
|
4
|
+
preload: () => Promise<{
|
|
5
|
+
default: ({ session, }: {
|
|
6
|
+
session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
|
|
7
|
+
}) => import("react").JSX.Element;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export declare const ClassicViewsContainer: import("react").LazyExoticComponent<({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element> & {
|
|
11
|
+
preload: () => Promise<{
|
|
12
|
+
default: ({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export declare const WorkspaceContainer: import("react").LazyExoticComponent<({ session, }: {
|
|
16
|
+
session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
|
|
17
|
+
}) => import("react").JSX.Element> & {
|
|
18
|
+
preload: () => Promise<{
|
|
19
|
+
default: ({ session, }: {
|
|
20
|
+
session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
|
|
21
|
+
}) => import("react").JSX.Element;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
export declare const ViewLauncher: import("react").LazyExoticComponent<({ session, }: {
|
|
25
|
+
session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
|
|
26
|
+
}) => import("react").JSX.Element> & {
|
|
27
|
+
preload: () => Promise<{
|
|
28
|
+
default: ({ session, }: {
|
|
29
|
+
session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
|
|
30
|
+
}) => import("react").JSX.Element;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { lazyWithPreload } from '@jbrowse/core/util/lazyWithPreload';
|
|
2
|
+
import { lazyChunk } from './lazyChunk.js';
|
|
3
|
+
export const DrawerWidget = lazyWithPreload(lazyChunk('DrawerWidget', () => import('./DrawerWidget.js')));
|
|
4
|
+
export const ClassicViewsContainer = lazyWithPreload(lazyChunk('ClassicViewsContainer', () => import('./ClassicViewsContainer.js')));
|
|
5
|
+
export const WorkspaceContainer = lazyWithPreload(lazyChunk('WorkspaceContainer', () => import('../../WorkspaceLayout/WorkspaceContainer.js').then(m => ({
|
|
6
|
+
default: m.WorkspaceContainer,
|
|
7
|
+
}))));
|
|
8
|
+
export const ViewLauncher = lazyWithPreload(lazyChunk('ViewLauncher', () => import('./ViewLauncher.js')));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppSession } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Load the views container and the drawer while the session's own view code
|
|
4
|
+
* downloads, settling when both have. An app that renders after this renders
|
|
5
|
+
* its frame in one pass; otherwise the first render suspends on them and React
|
|
6
|
+
* holds the retry for 300 ms.
|
|
7
|
+
*/
|
|
8
|
+
export declare function preloadAppFrame(): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Request the app frame's lazy parts that `session` will show (its views
|
|
11
|
+
* container, the drawer and the widget open in it) as soon as the session
|
|
12
|
+
* exists. Rendering asks for each only when it reaches it, a round trip apiece.
|
|
13
|
+
*/
|
|
14
|
+
export declare function preloadAppShell(session: AppSession): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { preloadComponent } from '@jbrowse/core/util/preloadComponent';
|
|
2
|
+
import { getEnv } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
|
|
4
|
+
import { ClassicViewsContainer, DrawerWidget, WorkspaceContainer, } from './lazyParts.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load the views container and the drawer while the session's own view code
|
|
7
|
+
* downloads, settling when both have. An app that renders after this renders
|
|
8
|
+
* its frame in one pass; otherwise the first render suspends on them and React
|
|
9
|
+
* holds the retry for 300 ms.
|
|
10
|
+
*/
|
|
11
|
+
export async function preloadAppFrame() {
|
|
12
|
+
await Promise.allSettled([
|
|
13
|
+
ClassicViewsContainer.preload(),
|
|
14
|
+
DrawerWidget.preload(),
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Request the app frame's lazy parts that `session` will show (its views
|
|
19
|
+
* container, the drawer and the widget open in it) as soon as the session
|
|
20
|
+
* exists. Rendering asks for each only when it reaches it, a round trip apiece.
|
|
21
|
+
*/
|
|
22
|
+
export function preloadAppShell(session) {
|
|
23
|
+
if (session.views.length > 0) {
|
|
24
|
+
preloadComponent(session.effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session)
|
|
25
|
+
? WorkspaceContainer
|
|
26
|
+
: ClassicViewsContainer);
|
|
27
|
+
}
|
|
28
|
+
const { drawerVisible, visibleWidget } = session;
|
|
29
|
+
if (drawerVisible) {
|
|
30
|
+
preloadComponent(DrawerWidget);
|
|
31
|
+
}
|
|
32
|
+
if (drawerVisible && visibleWidget) {
|
|
33
|
+
const { pluginManager } = getEnv(session);
|
|
34
|
+
try {
|
|
35
|
+
preloadComponent(pluginManager.getWidgetType(visibleWidget.type).ReactComponent);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// an unknown widget type is reported when the drawer renders it
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/esm/ui/App/types.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
1
|
+
import type { Menu } from '../../menus.ts';
|
|
2
|
+
import type { ErrorDialogState, SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
|
|
3
|
+
import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
4
|
+
export { type Menu } from '../../menus.ts';
|
|
5
|
+
export interface ThemeSwitchSession {
|
|
6
|
+
themeMode: 'light' | 'dark' | 'system';
|
|
7
|
+
effectiveThemeMode: 'light' | 'dark';
|
|
8
|
+
themeIsDark: boolean;
|
|
9
|
+
systemThemeOverride?: 'light' | 'dark';
|
|
10
|
+
setSystemThemeOverride: (mode?: 'light' | 'dark') => void;
|
|
11
|
+
}
|
|
12
|
+
export type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets;
|
|
13
|
+
export type AppSession = WorkspaceSessionType & ThemeSwitchSession & {
|
|
14
|
+
menus: () => Menu[];
|
|
15
|
+
errorDialog: ErrorDialogState | undefined;
|
|
16
|
+
setErrorDialog: (state: ErrorDialogState | undefined) => void;
|
|
17
|
+
effectiveUseWorkspaces: boolean;
|
|
4
18
|
renameCurrentSession: (arg: string) => void;
|
|
5
19
|
snackbarMessages: SnackbarMessage[];
|
|
6
20
|
popSnackbarMessage: () => unknown;
|
package/esm/ui/App/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export {} from '../../menus.js';
|