@jbrowse/app-core 4.3.0 → 5.0.0-beta.2
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 +1598 -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 +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +173 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +180 -95
- package/esm/JBrowseModel/index.js +122 -21
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +325 -0
- package/esm/JbApi/jbApi.js +748 -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 +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +392 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +31 -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 +105 -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 +109 -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 +248 -0
- package/esm/WorkspaceLayout/model.js +562 -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 +92 -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 +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +43 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- 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/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- 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/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 +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- 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 +42 -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 +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- 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/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -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/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/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
package/esm/ui/App/ViewMenu.js
CHANGED
|
@@ -11,106 +11,101 @@ import MenuIcon from '@mui/icons-material/Menu';
|
|
|
11
11
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
12
12
|
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
|
13
13
|
import { observer } from 'mobx-react';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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, }) {
|
|
14
|
+
import { renameIds } from './copyView.js';
|
|
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(structuredClone(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,42 @@
|
|
|
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
|
+
// Capped, because half a port stops being a courtesy on a tall screen. 300px is
|
|
14
|
+
// the cap the classic stack has always had and it stays the answer there — a
|
|
15
|
+
// full window is comfortably past twice it. What the cap was hiding is the
|
|
16
|
+
// workspace panel, where a 2x2 grid's cell can be shorter than 600px and a flat
|
|
17
|
+
// 300px let a view be dragged nearly clear of its own cell.
|
|
18
|
+
const MAX_OVERSCROLL = 300;
|
|
19
|
+
const useStyles = makeStyles()({
|
|
20
|
+
// trailing space so the last view can scroll up past the bottom of a
|
|
21
|
+
// container that would otherwise clip its lower edge.
|
|
22
|
+
//
|
|
23
|
+
// Rendered only once the views themselves overflow, and that is what makes
|
|
24
|
+
// the port's scrollbar honest: kept unconditionally, this alone overflowed
|
|
25
|
+
// the port, so every session — a single short view included — scrolled into
|
|
26
|
+
// nothing and drew a scrollbar saying so. It is outside the measured element
|
|
27
|
+
// for the same reason (see `useScrollPortOverflow`).
|
|
28
|
+
spacer: {
|
|
29
|
+
height: `min(${MAX_OVERSCROLL}px, calc(var(${SCROLL_PORT_HEIGHT_VAR}, 100vh) / 2))`,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const ViewStack = observer(function ViewStack({ views, session, className, }) {
|
|
33
|
+
const { classes } = useStyles();
|
|
34
|
+
const viewsRef = useRef(null);
|
|
35
|
+
const scrollable = useScrollPortOverflow(viewsRef);
|
|
36
|
+
// View ids present at first render: these arrived with the page (session
|
|
37
|
+
// restore / initial load), so they must not steal scroll when they mount. A
|
|
38
|
+
// view whose id isn't in this set was added later and auto-scrolls into view.
|
|
39
|
+
const [initialViewIds] = useState(() => new Set(views.map(v => v.id)));
|
|
40
|
+
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 }) : null] }));
|
|
41
|
+
});
|
|
42
|
+
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,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
1
|
+
import type { AppSession } from './types.ts';
|
|
3
2
|
interface Props {
|
|
4
3
|
HeaderButtons?: React.ReactElement;
|
|
5
|
-
session:
|
|
6
|
-
renameCurrentSession: (arg: string) => void;
|
|
7
|
-
snackbarMessages: SnackbarMessage[];
|
|
8
|
-
popSnackbarMessage: () => unknown;
|
|
9
|
-
};
|
|
4
|
+
session: AppSession;
|
|
10
5
|
}
|
|
11
|
-
declare const ViewsContainer: (props: Props) => import("react
|
|
6
|
+
declare const ViewsContainer: (props: Props) => import("react").JSX.Element;
|
|
12
7
|
export default ViewsContainer;
|
|
@@ -2,9 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Suspense, lazy } from 'react';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
5
|
+
import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
|
|
6
|
+
import { lazyChunk } from './lazyChunk.js';
|
|
7
|
+
const ClassicViewsContainer = lazy(lazyChunk('ClassicViewsContainer', () => import('./ClassicViewsContainer.js')));
|
|
8
|
+
const WorkspaceContainer = lazy(lazyChunk('WorkspaceContainer', () => import('../../WorkspaceLayout/WorkspaceContainer.js').then(m => ({
|
|
9
|
+
default: m.WorkspaceContainer,
|
|
10
|
+
}))));
|
|
11
|
+
const ViewLauncher = lazy(lazyChunk('ViewLauncher', () => import('./ViewLauncher.js')));
|
|
8
12
|
const useStyles = makeStyles()({
|
|
9
13
|
viewsContainer: {
|
|
10
14
|
gridRow: 'components',
|
|
@@ -13,9 +17,8 @@ const useStyles = makeStyles()({
|
|
|
13
17
|
});
|
|
14
18
|
const ViewsContainer = observer(function ViewsContainer(props) {
|
|
15
19
|
const { session } = props;
|
|
16
|
-
const { views } = session;
|
|
20
|
+
const { views, effectiveUseWorkspaces } = session;
|
|
17
21
|
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 }) })) }));
|
|
22
|
+
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
23
|
});
|
|
21
24
|
export default ViewsContainer;
|
package/esm/ui/App/copyView.js
CHANGED
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
|
|
1
|
+
export { App } from './App.js';
|
|
2
|
+
export { default as AppReadyMarker } from './AppReadyMarker.js';
|
|
3
|
+
export { default as DialogQueue } from './DialogQueue.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
|
+
}
|
package/esm/ui/App/types.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
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 type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets & {
|
|
4
6
|
renameCurrentSession: (arg: string) => void;
|
|
5
7
|
snackbarMessages: SnackbarMessage[];
|
|
6
8
|
popSnackbarMessage: () => unknown;
|
|
9
|
+
orderViews: (ids: string[]) => void;
|
|
10
|
+
};
|
|
11
|
+
export type AppSession = WorkspaceSessionType & {
|
|
12
|
+
menus: () => Menu[];
|
|
13
|
+
errorDialog: ErrorDialogState | undefined;
|
|
14
|
+
setErrorDialog: (state: ErrorDialogState | undefined) => void;
|
|
15
|
+
effectiveUseWorkspaces: boolean;
|
|
7
16
|
};
|
package/esm/ui/App/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export {} from '../../menus.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-level lazy mounting. A view only mounts its (GPU-heavy) body when it is
|
|
3
|
+
* within `rootMargin` of the viewport, and collapses to a height-preserving
|
|
4
|
+
* spacer once scrolled far away. This bounds the number of simultaneously-live
|
|
5
|
+
* GPU canvases/contexts — the root cause of the "workspaces freeze" with many
|
|
6
|
+
* stacked views (one WebGL context per display canvas blows past the browser's
|
|
7
|
+
* per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
|
|
8
|
+
* context per display canvas").
|
|
9
|
+
*
|
|
10
|
+
* `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
|
|
11
|
+
* same way whether the views scroll inside the classic container or a workspace
|
|
12
|
+
* panel, so neither container needs to know about windowing. It also means
|
|
13
|
+
* **`rootMargin` has no effect** — an observer clips the target against each
|
|
14
|
+
* scrolling ancestor before intersecting with the root box that the margin
|
|
15
|
+
* expands, and both containers are `overflow-y: auto`. So this is a hard
|
|
16
|
+
* window with no hysteresis: a view is torn down the moment it leaves its
|
|
17
|
+
* container and rebuilt when it returns, which on a GPU backend costs a fresh
|
|
18
|
+
* WebGL2 context and a full shader recompile per display.
|
|
19
|
+
*
|
|
20
|
+
* That is deliberate as of 2026-08-05, not an oversight. Rooting the observer at
|
|
21
|
+
* the scroll port restores the band and measures as a wash on scroll cost while
|
|
22
|
+
* roughly doubling live contexts — and the ceiling is 16 live contexts, so the
|
|
23
|
+
* cap bites before the rebuild saving pays for itself.
|
|
24
|
+
* agent-docs/reference/GPU_CONTEXT_BUDGET.md has the numbers and the three
|
|
25
|
+
* other fixes that were measured and eliminated.
|
|
26
|
+
*
|
|
27
|
+
* Starts hidden so a cold load with N crammed views doesn't mount them all at
|
|
28
|
+
* once; the observer's first callback mounts only what's near the viewport.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useViewVisibility(rootMargin: string): {
|
|
31
|
+
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
32
|
+
visible: boolean;
|
|
33
|
+
measuredHeight: number | undefined;
|
|
34
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
// No IntersectionObserver (jsdom/SSR): start (and stay) visible, the
|
|
3
|
+
// pre-lazy-mount behavior, so tests and non-browser environments are unchanged.
|
|
4
|
+
function intersectionObserverAvailable() {
|
|
5
|
+
return typeof window !== 'undefined' && 'IntersectionObserver' in window;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* View-level lazy mounting. A view only mounts its (GPU-heavy) body when it is
|
|
9
|
+
* within `rootMargin` of the viewport, and collapses to a height-preserving
|
|
10
|
+
* spacer once scrolled far away. This bounds the number of simultaneously-live
|
|
11
|
+
* GPU canvases/contexts — the root cause of the "workspaces freeze" with many
|
|
12
|
+
* stacked views (one WebGL context per display canvas blows past the browser's
|
|
13
|
+
* per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
|
|
14
|
+
* context per display canvas").
|
|
15
|
+
*
|
|
16
|
+
* `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
|
|
17
|
+
* same way whether the views scroll inside the classic container or a workspace
|
|
18
|
+
* panel, so neither container needs to know about windowing. It also means
|
|
19
|
+
* **`rootMargin` has no effect** — an observer clips the target against each
|
|
20
|
+
* scrolling ancestor before intersecting with the root box that the margin
|
|
21
|
+
* expands, and both containers are `overflow-y: auto`. So this is a hard
|
|
22
|
+
* window with no hysteresis: a view is torn down the moment it leaves its
|
|
23
|
+
* container and rebuilt when it returns, which on a GPU backend costs a fresh
|
|
24
|
+
* WebGL2 context and a full shader recompile per display.
|
|
25
|
+
*
|
|
26
|
+
* That is deliberate as of 2026-08-05, not an oversight. Rooting the observer at
|
|
27
|
+
* the scroll port restores the band and measures as a wash on scroll cost while
|
|
28
|
+
* roughly doubling live contexts — and the ceiling is 16 live contexts, so the
|
|
29
|
+
* cap bites before the rebuild saving pays for itself.
|
|
30
|
+
* agent-docs/reference/GPU_CONTEXT_BUDGET.md has the numbers and the three
|
|
31
|
+
* other fixes that were measured and eliminated.
|
|
32
|
+
*
|
|
33
|
+
* Starts hidden so a cold load with N crammed views doesn't mount them all at
|
|
34
|
+
* once; the observer's first callback mounts only what's near the viewport.
|
|
35
|
+
*/
|
|
36
|
+
export function useViewVisibility(rootMargin) {
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
const [state, setState] = useState(() => ({
|
|
39
|
+
visible: !intersectionObserverAvailable(),
|
|
40
|
+
}));
|
|
41
|
+
// Toggle `visible` as the body scrolls in/out of the viewport (root: null),
|
|
42
|
+
// and record the height it had on the way out.
|
|
43
|
+
//
|
|
44
|
+
// The height comes off the entry the observer already handed us. At the
|
|
45
|
+
// moment the body stops intersecting it is still mounted at full height (the
|
|
46
|
+
// collapse is what this callback is about to cause), so the entry's rect is
|
|
47
|
+
// exactly what a ResizeObserver would have reported. Keeping one
|
|
48
|
+
// meant a second observer per view whose every fire re-rendered the whole
|
|
49
|
+
// view chrome, and a view's height moves constantly: every track added,
|
|
50
|
+
// resized, or grown by its own data.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const node = ref.current;
|
|
53
|
+
if (node && intersectionObserverAvailable()) {
|
|
54
|
+
const io = new IntersectionObserver(entries => {
|
|
55
|
+
const entry = entries.at(-1);
|
|
56
|
+
if (entry) {
|
|
57
|
+
const { isIntersecting } = entry;
|
|
58
|
+
setState(prev => {
|
|
59
|
+
if (prev.visible === isIntersecting) {
|
|
60
|
+
return prev;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
visible: isIntersecting,
|
|
64
|
+
// a minimized view measures 0 here (it renders no body at
|
|
65
|
+
// all), which must not become its remembered height
|
|
66
|
+
height: isIntersecting
|
|
67
|
+
? prev.height
|
|
68
|
+
: entry.boundingClientRect.height || prev.height,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}, { rootMargin });
|
|
73
|
+
io.observe(node);
|
|
74
|
+
return () => {
|
|
75
|
+
io.disconnect();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}, [rootMargin]);
|
|
80
|
+
// `measuredHeight` is undefined until the body has been seen at least once.
|
|
81
|
+
// The caller supplies the estimate, and supplies it lazily. See ViewContainer
|
|
82
|
+
return {
|
|
83
|
+
ref,
|
|
84
|
+
visible: state.visible,
|
|
85
|
+
measuredHeight: state.height,
|
|
86
|
+
};
|
|
87
|
+
}
|