@oceanum/oceanumlab 4.7.2 → 4.8.0
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/lib/DatameshWidget.d.ts +11 -0
- package/lib/DatameshWidget.js +24 -3
- package/lib/StoredNotebooks.d.ts +20 -2
- package/lib/StoredNotebooks.js +95 -6
- package/lib/auth/tokens.d.ts +6 -0
- package/lib/index.js +35 -1
- package/lib/share/client.d.ts +3 -1
- package/lib/share/client.js +5 -4
- package/lib/share/notebook.d.ts +32 -0
- package/lib/share/notebook.js +86 -8
- package/lib/share/oceanumOnly.d.ts +25 -2
- package/lib/share/oceanumOnly.js +39 -2
- package/lib/share/plugin.d.ts +2 -0
- package/lib/share/plugin.js +109 -11
- package/lib/share/upload.d.ts +6 -0
- package/lib/share/upload.js +27 -0
- package/package.json +1 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshWidget.tsx +58 -0
- package/src/StoredNotebooks.tsx +216 -6
- package/src/__tests__/OceanumPanel.spec.tsx +99 -1
- package/src/__tests__/StoredNotebooks.spec.tsx +374 -3
- package/src/__tests__/oceanumOnly.spec.ts +127 -3
- package/src/__tests__/settingsSchema.spec.ts +11 -1
- package/src/__tests__/shareClient.spec.ts +14 -0
- package/src/__tests__/shareNotebook.spec.ts +173 -0
- package/src/__tests__/sharePlugin.spec.ts +245 -7
- package/src/auth/tokens.ts +6 -0
- package/src/index.ts +35 -1
- package/src/share/client.ts +5 -2
- package/src/share/notebook.ts +119 -10
- package/src/share/oceanumOnly.ts +55 -4
- package/src/share/plugin.ts +135 -11
- package/src/share/upload.ts +27 -0
- package/style/index.css +101 -2
package/lib/DatameshWidget.d.ts
CHANGED
|
@@ -57,6 +57,17 @@ export interface IDatameshWidgetProps {
|
|
|
57
57
|
auth?: IOceanumAuth | null;
|
|
58
58
|
/** Open a stored notebook by spec store id. */
|
|
59
59
|
openStoredNotebook?: (id: string) => void;
|
|
60
|
+
/** Open a copy of an example by spec store id, named from its title. */
|
|
61
|
+
openExample?: (id: string, title: string) => void;
|
|
62
|
+
/** Upload a notebook from the user's computer. */
|
|
63
|
+
uploadNotebook?: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* The `showExamples` setting: whether the Notebooks tab lists the examples; null
|
|
66
|
+
* until the settings have loaded.
|
|
67
|
+
*/
|
|
68
|
+
showExamples?: () => boolean | null;
|
|
69
|
+
/** Save the `showExamples` setting, from the switch on the Examples heading. */
|
|
70
|
+
setShowExamples?: (show: boolean) => void;
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* A widget for Datamesh Connections.
|
package/lib/DatameshWidget.js
CHANGED
|
@@ -324,6 +324,26 @@ function useAiAccess(commands, settings) {
|
|
|
324
324
|
}, [commands, settings]);
|
|
325
325
|
return access;
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* The `showExamples` setting, read again whenever the settings change. True where the
|
|
329
|
+
* host passes no such setting; null while the host's setting has not loaded.
|
|
330
|
+
*/
|
|
331
|
+
function useShowExamples(settingsChanged, read) {
|
|
332
|
+
const [show, setShow] = React.useState(() => read ? read() : true);
|
|
333
|
+
React.useEffect(() => {
|
|
334
|
+
if (!read) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const update = () => setShow(read());
|
|
338
|
+
// Anything that changed between the first render and now.
|
|
339
|
+
update();
|
|
340
|
+
settingsChanged.connect(update);
|
|
341
|
+
return () => {
|
|
342
|
+
settingsChanged.disconnect(update);
|
|
343
|
+
};
|
|
344
|
+
}, [settingsChanged, read]);
|
|
345
|
+
return show;
|
|
346
|
+
}
|
|
327
347
|
/**
|
|
328
348
|
* Shows a message prompting the user to sign in to Oceanum.io, or to configure
|
|
329
349
|
* their Datamesh token where the site has no sign-in. Only renders when there
|
|
@@ -611,8 +631,9 @@ function AIChatPanel({ commands, settings }) {
|
|
|
611
631
|
* credential to work with — otherwise it would open on the same nothing the panel
|
|
612
632
|
* rendered before there were tabs, except now with a label promising otherwise.
|
|
613
633
|
*/
|
|
614
|
-
function PanelTabs({ auth, commands, settings, renderDatamesh, openStoredNotebook, selected, onSelect }) {
|
|
634
|
+
function PanelTabs({ auth, commands, settings, renderDatamesh, openStoredNotebook, openExample, uploadNotebook, showExamples, setShowExamples, selected, onSelect }) {
|
|
615
635
|
const access = useAiAccess(commands, settings);
|
|
636
|
+
const examplesShown = useShowExamples(settings.settingsChanged, showExamples);
|
|
616
637
|
const tabs = [
|
|
617
638
|
{
|
|
618
639
|
id: 'notebooks',
|
|
@@ -620,7 +641,7 @@ function PanelTabs({ auth, commands, settings, renderDatamesh, openStoredNoteboo
|
|
|
620
641
|
render: () =>
|
|
621
642
|
// An auth with no environment is a provider that has nothing to sign in to (the
|
|
622
643
|
// server has sign-in turned off). Asking the user to sign in would be a dead end.
|
|
623
|
-
(auth === null || auth === void 0 ? void 0 : auth.environment) ? (React.createElement(StoredNotebooks, { auth: auth, onOpen: openStoredNotebook && (item => openStoredNotebook(item.id)), onSignIn: () => {
|
|
644
|
+
(auth === null || auth === void 0 ? void 0 : auth.environment) ? (React.createElement(StoredNotebooks, { auth: auth, onOpen: openStoredNotebook && (item => openStoredNotebook(item.id)), onOpenExample: openExample && (item => openExample(item.id, item.title)), onUpload: uploadNotebook, showExamples: examplesShown, onShowExamplesChange: setShowExamples, onSignIn: () => {
|
|
624
645
|
startSignIn(commands, auth).catch(error => {
|
|
625
646
|
console.warn('Oceanum.io sign-in could not start.', error);
|
|
626
647
|
});
|
|
@@ -693,7 +714,7 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
693
714
|
React.createElement("span", { className: "oceanum-sidebar-title" }, "Oceanum.io")),
|
|
694
715
|
React.createElement(UseSignal, { signal: this.tabChanged, initialArgs: this.selectedTab }, () => {
|
|
695
716
|
var _a;
|
|
696
|
-
return (React.createElement(PanelTabs, { auth: (_a = this.props.auth) !== null && _a !== void 0 ? _a : null, commands: this.props.commands, settings: this.props, renderDatamesh: () => this.renderDatamesh(), openStoredNotebook: this.props.openStoredNotebook, selected: this.selectedTab, onSelect: id => this.selectTab(id) }));
|
|
717
|
+
return (React.createElement(PanelTabs, { auth: (_a = this.props.auth) !== null && _a !== void 0 ? _a : null, commands: this.props.commands, settings: this.props, renderDatamesh: () => this.renderDatamesh(), openStoredNotebook: this.props.openStoredNotebook, openExample: this.props.openExample, uploadNotebook: this.props.uploadNotebook, showExamples: this.props.showExamples, setShowExamples: this.props.setShowExamples, selected: this.selectedTab, onSelect: id => this.selectTab(id) }));
|
|
697
718
|
})));
|
|
698
719
|
}
|
|
699
720
|
/**
|
package/lib/StoredNotebooks.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { IOceanumAuth } from './auth/tokens';
|
|
3
|
-
import { ISpecSummary } from './share/notebook';
|
|
3
|
+
import { INotebookDemoItem, ISpecSummary } from './share/notebook';
|
|
4
|
+
/** Example rows carry their record id here, not in SPEC_ID_ATTRIBUTE: see Examples. */
|
|
5
|
+
export declare const EXAMPLE_ID_ATTRIBUTE = "data-example-id";
|
|
4
6
|
export interface IStoredNotebooksProps {
|
|
5
7
|
auth: IOceanumAuth;
|
|
6
8
|
/**
|
|
@@ -8,6 +10,22 @@ export interface IStoredNotebooksProps {
|
|
|
8
10
|
* the list then reads rather than offering a click that does nothing.
|
|
9
11
|
*/
|
|
10
12
|
onOpen?: (item: ISpecSummary) => void;
|
|
13
|
+
/** Open a copy of an example, as `onOpen` does for a stored notebook. */
|
|
14
|
+
onOpenExample?: (item: INotebookDemoItem) => void;
|
|
15
|
+
/** Upload a notebook from the user's computer; no Upload button without it. */
|
|
16
|
+
onUpload?: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the examples are listed, or folded under their heading. Default true. Null
|
|
19
|
+
* while the setting is still loading: the Examples wait for it, so examples a user
|
|
20
|
+
* has hidden never show for a moment first.
|
|
21
|
+
*/
|
|
22
|
+
showExamples?: boolean | null;
|
|
23
|
+
/**
|
|
24
|
+
* Record the user's choice to show or hide the examples. The Examples heading offers
|
|
25
|
+
* its switch only where this is given: a switch that could not keep its setting
|
|
26
|
+
* would flip back on the next load.
|
|
27
|
+
*/
|
|
28
|
+
onShowExamplesChange?: (show: boolean) => void;
|
|
11
29
|
/**
|
|
12
30
|
* Start signing in the way this host shows it; without it the auth's own `signIn()`
|
|
13
31
|
* is used, which on a JupyterLab server shows no code dialog.
|
|
@@ -17,4 +35,4 @@ export interface IStoredNotebooksProps {
|
|
|
17
35
|
/**
|
|
18
36
|
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
19
37
|
*/
|
|
20
|
-
export declare function StoredNotebooks({ auth, onOpen, onSignIn }: IStoredNotebooksProps): React.ReactElement;
|
|
38
|
+
export declare function StoredNotebooks({ auth, onOpen, onOpenExample, onUpload, showExamples, onShowExamplesChange, onSignIn }: IStoredNotebooksProps): React.ReactElement;
|
package/lib/StoredNotebooks.js
CHANGED
|
@@ -1,8 +1,82 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { SpecStoreClient } from './share/client';
|
|
3
3
|
import { notebooksChanged } from './share/events';
|
|
4
|
-
import { partitionSummaries } from './share/notebook';
|
|
4
|
+
import { parseNotebookDemo, partitionSummaries } from './share/notebook';
|
|
5
5
|
import { SPEC_ID_ATTRIBUTE } from './share/plugin';
|
|
6
|
+
/** Example rows carry their record id here, not in SPEC_ID_ATTRIBUTE: see Examples. */
|
|
7
|
+
export const EXAMPLE_ID_ATTRIBUTE = 'data-example-id';
|
|
8
|
+
/**
|
|
9
|
+
* Read the Notebook Demo record, if the deployment names one. Never rejects: a failure
|
|
10
|
+
* here is shown in the Examples section and leaves the rest of the tab alone.
|
|
11
|
+
*/
|
|
12
|
+
async function loadExamples(auth, specsUrl, id) {
|
|
13
|
+
if (!id) {
|
|
14
|
+
return { state: 'none' };
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const client = new SpecStoreClient({
|
|
18
|
+
specsUrl,
|
|
19
|
+
getAccessToken: () => auth.getAccessToken(),
|
|
20
|
+
specType: 'notebook-demo'
|
|
21
|
+
});
|
|
22
|
+
const record = await client.get(id);
|
|
23
|
+
return { state: 'ready', sections: parseNotebookDemo(record.spec) };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
state: 'error',
|
|
28
|
+
message: error instanceof Error ? error.message : 'Could not reach Oceanum.io.'
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The curated examples. Their rows deliberately leave out SPEC_ID_ATTRIBUTE and the
|
|
34
|
+
* stored-notebook row class: the share plugin's row menu (Open, Rename, Share, Delete)
|
|
35
|
+
* is bound to those, and none of it applies to Oceanum's own example records.
|
|
36
|
+
*/
|
|
37
|
+
function ExampleSections({ examples, onOpen, show, onShowChange }) {
|
|
38
|
+
// Not yet known, or hidden with no switch to bring them back: nothing to show.
|
|
39
|
+
if (examples.state === 'none' || show === null || (!show && !onShowChange)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const count = examples.state === 'ready'
|
|
43
|
+
? examples.sections.reduce((total, section) => total + section.items.length, 0)
|
|
44
|
+
: null;
|
|
45
|
+
if (count === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const heading = (React.createElement("div", { className: "oceanum-notebooks-heading" },
|
|
49
|
+
"Examples",
|
|
50
|
+
React.createElement("span", { className: "oceanum-notebooks-heading-end" },
|
|
51
|
+
count !== null && (React.createElement("span", { className: "oceanum-notebooks-count" }, count)),
|
|
52
|
+
onShowChange && (React.createElement("button", { type: "button", role: "switch", "aria-checked": show, "aria-label": "Show examples", title: "Show examples", className: "oceanum-notebooks-switch", onClick: () => onShowChange(!show) },
|
|
53
|
+
React.createElement("span", { className: "oceanum-notebooks-switch-thumb" }))))));
|
|
54
|
+
// Hidden: the heading stays, so the switch is there to bring them back.
|
|
55
|
+
if (!show) {
|
|
56
|
+
return React.createElement("div", { className: "oceanum-notebooks-section" }, heading);
|
|
57
|
+
}
|
|
58
|
+
if (examples.state === 'error') {
|
|
59
|
+
return (React.createElement("div", { className: "oceanum-notebooks-section" },
|
|
60
|
+
heading,
|
|
61
|
+
React.createElement("div", { className: "oceanum-text-error" },
|
|
62
|
+
"The examples could not be loaded. ",
|
|
63
|
+
examples.message)));
|
|
64
|
+
}
|
|
65
|
+
return (React.createElement("div", { className: "oceanum-notebooks-section" },
|
|
66
|
+
heading,
|
|
67
|
+
examples.sections.map((section, index) => (React.createElement("div", { key: index },
|
|
68
|
+
React.createElement("div", { className: "oceanum-notebooks-subheading" }, section.title),
|
|
69
|
+
React.createElement("ul", { className: "oceanum-notebooks-list" }, section.items.map((item, position) => {
|
|
70
|
+
const attributes = {
|
|
71
|
+
[EXAMPLE_ID_ATTRIBUTE]: item.id,
|
|
72
|
+
title: item.summary
|
|
73
|
+
};
|
|
74
|
+
const name = (React.createElement("span", { className: "oceanum-notebooks-name" }, item.title));
|
|
75
|
+
return (
|
|
76
|
+
// A record may list the same example twice; the id alone is no key.
|
|
77
|
+
React.createElement("li", { key: `${position}:${item.id}` }, onOpen ? (React.createElement("button", { type: "button", className: "oceanum-notebooks-example", onClick: () => onOpen(item), ...attributes }, name)) : (React.createElement("div", { className: "oceanum-notebooks-example oceanum-notebooks-item-static", ...attributes }, name))));
|
|
78
|
+
})))))));
|
|
79
|
+
}
|
|
6
80
|
function Section({ title, items, empty, onOpen }) {
|
|
7
81
|
// Each row carries its record id so the share plugin's context menu (Open, Rename,
|
|
8
82
|
// Share, Delete) can tell which one was clicked.
|
|
@@ -24,7 +98,8 @@ function Section({ title, items, empty, onOpen }) {
|
|
|
24
98
|
/**
|
|
25
99
|
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
26
100
|
*/
|
|
27
|
-
export function StoredNotebooks({ auth, onOpen, onSignIn }) {
|
|
101
|
+
export function StoredNotebooks({ auth, onOpen, onOpenExample, onUpload, showExamples = true, onShowExamplesChange, onSignIn }) {
|
|
102
|
+
var _a;
|
|
28
103
|
const [load, setLoad] = React.useState({ state: 'loading' });
|
|
29
104
|
const [user, setUser] = React.useState(auth.user);
|
|
30
105
|
// Bumped whenever the store changes, so the list is fetched again.
|
|
@@ -47,19 +122,25 @@ export function StoredNotebooks({ auth, onOpen, onSignIn }) {
|
|
|
47
122
|
let cancelled = false;
|
|
48
123
|
const specs = (_b = (_a = auth.urls) === null || _a === void 0 ? void 0 : _a.specs) !== null && _b !== void 0 ? _b : null;
|
|
49
124
|
if (!user || !specs) {
|
|
50
|
-
setLoad({ state: 'ready', items: [] });
|
|
125
|
+
setLoad({ state: 'ready', items: [], examples: { state: 'none' } });
|
|
51
126
|
return;
|
|
52
127
|
}
|
|
53
128
|
setLoad({ state: 'loading' });
|
|
54
129
|
void (async () => {
|
|
130
|
+
var _a, _b;
|
|
55
131
|
try {
|
|
56
132
|
const client = new SpecStoreClient({
|
|
57
133
|
specsUrl: specs,
|
|
58
134
|
getAccessToken: () => auth.getAccessToken()
|
|
59
135
|
});
|
|
60
|
-
|
|
136
|
+
// Only the record the deployment names: every signed-in user can create
|
|
137
|
+
// notebook-demo records, so the examples never come from a listing.
|
|
138
|
+
const [items, examples] = await Promise.all([
|
|
139
|
+
client.list(),
|
|
140
|
+
loadExamples(auth, specs, (_b = (_a = auth.environment) === null || _a === void 0 ? void 0 : _a.notebookDemo) !== null && _b !== void 0 ? _b : null)
|
|
141
|
+
]);
|
|
61
142
|
if (!cancelled) {
|
|
62
|
-
setLoad({ state: 'ready', items });
|
|
143
|
+
setLoad({ state: 'ready', items, examples });
|
|
63
144
|
}
|
|
64
145
|
}
|
|
65
146
|
catch (error) {
|
|
@@ -106,7 +187,15 @@ export function StoredNotebooks({ auth, onOpen, onSignIn }) {
|
|
|
106
187
|
return React.createElement("div", { className: "oceanum-text-error" }, load.message);
|
|
107
188
|
}
|
|
108
189
|
const { mine, shared } = partitionSummaries(load.items, user.email);
|
|
190
|
+
// A public example is readable by everyone, so the listing returns it as shared with
|
|
191
|
+
// every signed-in user; it is shown under Examples instead.
|
|
192
|
+
const exampleIds = new Set(load.examples.state === 'ready'
|
|
193
|
+
? load.examples.sections.flatMap(section => section.items.map(item => item.id))
|
|
194
|
+
: []);
|
|
109
195
|
return (React.createElement("div", { className: "oceanum-notebooks" },
|
|
196
|
+
onUpload && ((_a = auth.urls) === null || _a === void 0 ? void 0 : _a.specs) ? (React.createElement("div", { className: "oceanum-notebooks-actions" },
|
|
197
|
+
React.createElement("button", { type: "button", className: "oceanum-notebooks-upload", title: "Upload a notebook from this computer, to save on Oceanum.io", onClick: onUpload }, "Upload notebook\u2026"))) : null,
|
|
110
198
|
React.createElement(Section, { title: "My notebooks", items: mine, empty: "You have not saved any notebooks yet.", onOpen: onOpen }),
|
|
111
|
-
React.createElement(Section, { title: "Shared with me", items: shared, empty: "No notebooks have been shared with you.", onOpen: onOpen })
|
|
199
|
+
React.createElement(Section, { title: "Shared with me", items: shared.filter(item => !exampleIds.has(item.id)), empty: "No notebooks have been shared with you.", onOpen: onOpen }),
|
|
200
|
+
React.createElement(ExampleSections, { examples: load.examples, onOpen: onOpenExample, show: showExamples, onShowChange: onShowExamplesChange })));
|
|
112
201
|
}
|
package/lib/auth/tokens.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export interface IOceanumEnvironment {
|
|
|
31
31
|
* absent) keeps JupyterLab's file management alongside the Oceanum commands.
|
|
32
32
|
*/
|
|
33
33
|
readonly fileManagement?: 'local' | 'oceanum';
|
|
34
|
+
/**
|
|
35
|
+
* The spec store id of this deployment's Notebook Demo record, the curated list the
|
|
36
|
+
* Notebooks tab shows as Examples. Absent where a deployment has none, and never set
|
|
37
|
+
* by a JupyterLab server's sign-in.
|
|
38
|
+
*/
|
|
39
|
+
readonly notebookDemo?: string;
|
|
34
40
|
}
|
|
35
41
|
/** Service base URLs for an environment, without trailing slashes. */
|
|
36
42
|
export interface IOceanumServiceUrls {
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { ConversationPin } from './conversationPin';
|
|
|
15
15
|
import { deviceAuthPlugin } from './auth/plugin';
|
|
16
16
|
import { IOceanumAuth } from './auth/tokens';
|
|
17
17
|
import { CommandIDs as ShareCommandIDs, sharePlugin } from './share/plugin';
|
|
18
|
+
import { OCEANUM_PANEL_ID } from './share/oceanumOnly';
|
|
18
19
|
import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
19
20
|
import { notebookHost } from './notebookHost';
|
|
20
21
|
import { KernelHandoff } from './kernelHandoff';
|
|
@@ -55,6 +56,9 @@ export const datamesh_connect_extension = {
|
|
|
55
56
|
const aiBackendUrl = (_d = (_c = auth === null || auth === void 0 ? void 0 : auth.urls) === null || _c === void 0 ? void 0 : _c.ai) !== null && _d !== void 0 ? _d : OCEANUM_AI_BACKEND_URL;
|
|
56
57
|
// The `datameshToken` setting for the AI chat; empty until the settings load.
|
|
57
58
|
let datameshTokenSetting = '';
|
|
59
|
+
// The `showExamples` setting for the Notebooks tab; null until the settings load, so
|
|
60
|
+
// examples a user has hidden do not show for a moment on every reload.
|
|
61
|
+
let showExamplesSetting = null;
|
|
58
62
|
// Tells the sidebar the settings above have changed.
|
|
59
63
|
const settingsChanged = new Signal(app);
|
|
60
64
|
const findDatameshUI = () => find(app.shell.widgets('main'), (widget) => widget.id === 'datamesh-ui');
|
|
@@ -70,6 +74,7 @@ export const datamesh_connect_extension = {
|
|
|
70
74
|
}
|
|
71
75
|
window.injectToken = set.get('injectToken').user;
|
|
72
76
|
datameshTokenSetting = set.get('datameshToken').composite;
|
|
77
|
+
showExamplesSetting = set.get('showExamples').composite;
|
|
73
78
|
settingsChanged.emit();
|
|
74
79
|
};
|
|
75
80
|
//Try to get the datamesh token from the envars
|
|
@@ -82,6 +87,10 @@ export const datamesh_connect_extension = {
|
|
|
82
87
|
})
|
|
83
88
|
.catch(reason => {
|
|
84
89
|
console.error(`Something went wrong when reading the Oceanumlab settings.\n${reason}`);
|
|
90
|
+
// Unreadable settings fall back to the default rather than leaving the examples
|
|
91
|
+
// waiting for a value that will never come.
|
|
92
|
+
showExamplesSetting = true;
|
|
93
|
+
settingsChanged.emit();
|
|
85
94
|
});
|
|
86
95
|
const getCurrentWidget = () => {
|
|
87
96
|
return app.shell.currentWidget;
|
|
@@ -117,9 +126,34 @@ export const datamesh_connect_extension = {
|
|
|
117
126
|
app.commands.execute(ShareCommandIDs.open, { id }).catch(error => {
|
|
118
127
|
console.error('Oceanum: could not open the stored notebook.', error);
|
|
119
128
|
});
|
|
129
|
+
},
|
|
130
|
+
openExample: (id, title) => {
|
|
131
|
+
app.commands
|
|
132
|
+
.execute(ShareCommandIDs.openExample, { id, title })
|
|
133
|
+
.catch(error => {
|
|
134
|
+
console.error('Oceanum: could not open the example.', error);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
uploadNotebook: () => {
|
|
138
|
+
app.commands.execute(ShareCommandIDs.upload).catch(error => {
|
|
139
|
+
console.error('Oceanum: could not upload the notebook.', error);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
showExamples: () => showExamplesSetting,
|
|
143
|
+
// Saved as a user setting, so the choice outlasts the page; the settings'
|
|
144
|
+
// `changed` signal brings the new value back to the panel. The registry waits
|
|
145
|
+
// for the plugin's settings to load, so a click before then is not lost.
|
|
146
|
+
setShowExamples: show => {
|
|
147
|
+
settingRegistry.set(PLUGIN_ID, 'showExamples', show).catch(error => {
|
|
148
|
+
// The switch moves only once the setting is saved, so say why it did not.
|
|
149
|
+
console.error('Oceanum: could not save the Examples setting.', error);
|
|
150
|
+
Notification.error('Could not save whether to show the examples.', {
|
|
151
|
+
autoClose: 5000
|
|
152
|
+
});
|
|
153
|
+
});
|
|
120
154
|
}
|
|
121
155
|
});
|
|
122
|
-
datameshConnectWidget.id =
|
|
156
|
+
datameshConnectWidget.id = OCEANUM_PANEL_ID;
|
|
123
157
|
datameshConnectWidget.title.icon = oceanumIcon;
|
|
124
158
|
datameshConnectWidget.title.caption = 'Datamesh Connect';
|
|
125
159
|
restorer.add(datameshConnectWidget, 'datamesh-connect');
|
package/lib/share/client.d.ts
CHANGED
|
@@ -40,9 +40,11 @@ export interface ISpecStoreClientOptions {
|
|
|
40
40
|
specsUrl: string;
|
|
41
41
|
/** Returns a current access token or `null`. Called for every request. */
|
|
42
42
|
getAccessToken: () => Promise<string | null>;
|
|
43
|
+
/** The spec type the client reads and writes; `notebook` unless given. */
|
|
44
|
+
specType?: string;
|
|
43
45
|
fetch?: typeof fetch;
|
|
44
46
|
}
|
|
45
|
-
/** Client for
|
|
47
|
+
/** Client for one spec type of the Oceanum spec store, `notebook` by default. */
|
|
46
48
|
export declare class SpecStoreClient {
|
|
47
49
|
constructor(options: ISpecStoreClientOptions);
|
|
48
50
|
/** Every notebook the user may read, including public ones. */
|
package/lib/share/client.js
CHANGED
|
@@ -52,13 +52,14 @@ function isSummary(value) {
|
|
|
52
52
|
function isRecord(value) {
|
|
53
53
|
return isSummary(value) && 'spec' in value;
|
|
54
54
|
}
|
|
55
|
-
/** Client for
|
|
55
|
+
/** Client for one spec type of the Oceanum spec store, `notebook` by default. */
|
|
56
56
|
export class SpecStoreClient {
|
|
57
57
|
constructor(options) {
|
|
58
|
-
var _a;
|
|
59
|
-
|
|
58
|
+
var _a, _b;
|
|
59
|
+
const specType = (_a = options.specType) !== null && _a !== void 0 ? _a : 'notebook';
|
|
60
|
+
this._base = `${options.specsUrl.replace(/\/+$/, '')}/specs/${specType}`;
|
|
60
61
|
this._getAccessToken = options.getAccessToken;
|
|
61
|
-
this._fetch = (
|
|
62
|
+
this._fetch = (_b = options.fetch) !== null && _b !== void 0 ? _b : ((input, init) => fetch(input, init));
|
|
62
63
|
}
|
|
63
64
|
/** Every notebook the user may read, including public ones. */
|
|
64
65
|
async list() {
|
package/lib/share/notebook.d.ts
CHANGED
|
@@ -93,11 +93,24 @@ export declare function withoutTrust(content: INotebookContent): INotebookConten
|
|
|
93
93
|
* dropped; if it is still too large, the result is not ok.
|
|
94
94
|
*/
|
|
95
95
|
export declare function buildSpecBody(content: INotebookContent, path: string, specsUrl: string, maxBytes?: number): BuildResult;
|
|
96
|
+
/**
|
|
97
|
+
* A notebook uploaded from the user's computer, untrusted and linked to nothing: a file
|
|
98
|
+
* someone else downloaded from a record must not save over that record, and its outputs
|
|
99
|
+
* must not run scripts on the origin that holds the Oceanum.io session. Throws if the
|
|
100
|
+
* text is not an nbformat 4 notebook; the message never quotes the file.
|
|
101
|
+
*/
|
|
102
|
+
export declare function notebookFromUpload(text: string): INotebookContent;
|
|
96
103
|
/**
|
|
97
104
|
* The notebook stored in a record, with its `oceanum` link set. Throws if the record
|
|
98
105
|
* does not hold an nbformat 4 notebook.
|
|
99
106
|
*/
|
|
100
107
|
export declare function notebookFromRecord(record: ISpecRecord, specsUrl: string): INotebookContent;
|
|
108
|
+
/**
|
|
109
|
+
* A copy of the notebook stored in a record, linked to nothing: untrusted, and without
|
|
110
|
+
* any `oceanum` link, so saving it creates a record of the user's own instead of
|
|
111
|
+
* writing to this one. Throws if the record does not hold an nbformat 4 notebook.
|
|
112
|
+
*/
|
|
113
|
+
export declare function unlinkedNotebookFromRecord(record: ISpecRecord): INotebookContent;
|
|
101
114
|
/** Make a record name safe to use as a single path segment. */
|
|
102
115
|
export declare function sanitizeName(name: string): string;
|
|
103
116
|
/**
|
|
@@ -128,6 +141,25 @@ export declare function partitionSummaries(items: readonly ISpecSummary[], email
|
|
|
128
141
|
mine: ISpecSummary[];
|
|
129
142
|
shared: ISpecSummary[];
|
|
130
143
|
};
|
|
144
|
+
/** The `kind` of a Notebook Demo record's spec. */
|
|
145
|
+
export declare const NOTEBOOK_DEMO_KIND = "notebook-demo";
|
|
146
|
+
/** One example in a Notebook Demo record: a notebook record and how to list it. */
|
|
147
|
+
export interface INotebookDemoItem {
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
summary?: string;
|
|
151
|
+
}
|
|
152
|
+
/** A titled group of examples. */
|
|
153
|
+
export interface INotebookDemoSection {
|
|
154
|
+
title: string;
|
|
155
|
+
items: INotebookDemoItem[];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The sections of a Notebook Demo record's spec, keeping only what can be listed: items
|
|
159
|
+
* with a record id and a title, in sections with a title and at least one such item.
|
|
160
|
+
* A spec of another kind or version gives no sections, with a warning.
|
|
161
|
+
*/
|
|
162
|
+
export declare function parseNotebookDemo(spec: unknown): INotebookDemoSection[];
|
|
131
163
|
/** Parse a spec store timestamp, which is UTC but usually has no zone designator. */
|
|
132
164
|
export declare function parseTimestamp(value: string): Date | null;
|
|
133
165
|
/**
|
package/lib/share/notebook.js
CHANGED
|
@@ -106,27 +106,65 @@ export function buildSpecBody(content, path, specsUrl, maxBytes = MAX_BODY_BYTES
|
|
|
106
106
|
return { ok: false, bytes: strippedBytes };
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* The notebook stored in a record,
|
|
110
|
-
*
|
|
109
|
+
* The notebook stored in a record, untrusted. Throws if the record does not hold an
|
|
110
|
+
* nbformat 4 notebook.
|
|
111
111
|
*/
|
|
112
|
-
|
|
112
|
+
function recordNotebook(record) {
|
|
113
113
|
const spec = record.spec;
|
|
114
|
-
if (!
|
|
115
|
-
spec.nbformat !== 4 ||
|
|
116
|
-
!Array.isArray(spec.cells) ||
|
|
117
|
-
!isObject(spec.metadata)) {
|
|
114
|
+
if (!isNotebook(spec)) {
|
|
118
115
|
throw new Error('This Oceanum.io record is not a valid notebook.');
|
|
119
116
|
}
|
|
117
|
+
return withoutTrust(spec);
|
|
118
|
+
}
|
|
119
|
+
function isNotebook(value) {
|
|
120
|
+
return (isObject(value) &&
|
|
121
|
+
value.nbformat === 4 &&
|
|
122
|
+
Array.isArray(value.cells) &&
|
|
123
|
+
value.cells.every(cell => isObject(cell) && typeof cell.cell_type === 'string') &&
|
|
124
|
+
isObject(value.metadata));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A notebook uploaded from the user's computer, untrusted and linked to nothing: a file
|
|
128
|
+
* someone else downloaded from a record must not save over that record, and its outputs
|
|
129
|
+
* must not run scripts on the origin that holds the Oceanum.io session. Throws if the
|
|
130
|
+
* text is not an nbformat 4 notebook; the message never quotes the file.
|
|
131
|
+
*/
|
|
132
|
+
export function notebookFromUpload(text) {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(text);
|
|
136
|
+
}
|
|
137
|
+
catch (_a) {
|
|
138
|
+
throw new Error('The file is not a Jupyter notebook: it is not JSON.');
|
|
139
|
+
}
|
|
140
|
+
if (!isNotebook(parsed)) {
|
|
141
|
+
throw new Error('The file is not a Jupyter notebook in nbformat 4.');
|
|
142
|
+
}
|
|
143
|
+
return withoutLink(withoutTrust(parsed));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The notebook stored in a record, with its `oceanum` link set. Throws if the record
|
|
147
|
+
* does not hold an nbformat 4 notebook.
|
|
148
|
+
*/
|
|
149
|
+
export function notebookFromRecord(record, specsUrl) {
|
|
120
150
|
const link = { spec_id: record.id, specs_url: specsUrl };
|
|
121
151
|
if (record.description) {
|
|
122
152
|
link.description = record.description;
|
|
123
153
|
}
|
|
124
|
-
const content =
|
|
154
|
+
const content = recordNotebook(record);
|
|
125
155
|
return {
|
|
126
156
|
...content,
|
|
127
157
|
metadata: { ...content.metadata, [METADATA_KEY]: { ...link } }
|
|
128
158
|
};
|
|
129
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A copy of the notebook stored in a record, linked to nothing: untrusted, and without
|
|
162
|
+
* any `oceanum` link, so saving it creates a record of the user's own instead of
|
|
163
|
+
* writing to this one. Throws if the record does not hold an nbformat 4 notebook.
|
|
164
|
+
*/
|
|
165
|
+
export function unlinkedNotebookFromRecord(record) {
|
|
166
|
+
return withoutLink(recordNotebook(record));
|
|
167
|
+
}
|
|
130
168
|
/** Make a record name safe to use as a single path segment. */
|
|
131
169
|
export function sanitizeName(name) {
|
|
132
170
|
const clean = name
|
|
@@ -199,6 +237,46 @@ export function partitionSummaries(items, email) {
|
|
|
199
237
|
shared: sorted.filter(item => !isMine(item))
|
|
200
238
|
};
|
|
201
239
|
}
|
|
240
|
+
/** The `kind` of a Notebook Demo record's spec. */
|
|
241
|
+
export const NOTEBOOK_DEMO_KIND = 'notebook-demo';
|
|
242
|
+
function nonBlank(value) {
|
|
243
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The sections of a Notebook Demo record's spec, keeping only what can be listed: items
|
|
247
|
+
* with a record id and a title, in sections with a title and at least one such item.
|
|
248
|
+
* A spec of another kind or version gives no sections, with a warning.
|
|
249
|
+
*/
|
|
250
|
+
export function parseNotebookDemo(spec) {
|
|
251
|
+
if (!isObject(spec) ||
|
|
252
|
+
spec.kind !== NOTEBOOK_DEMO_KIND ||
|
|
253
|
+
spec.version !== 1 ||
|
|
254
|
+
!Array.isArray(spec.sections)) {
|
|
255
|
+
console.warn('Oceanum: ignoring a Notebook Demo record this version cannot read.');
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const sections = [];
|
|
259
|
+
for (const section of spec.sections) {
|
|
260
|
+
if (!isObject(section) ||
|
|
261
|
+
!nonBlank(section.title) ||
|
|
262
|
+
!Array.isArray(section.items)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const items = [];
|
|
266
|
+
for (const item of section.items) {
|
|
267
|
+
if (!isObject(item) || !isSpecId(item.id) || !nonBlank(item.title)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
items.push(typeof item.summary === 'string'
|
|
271
|
+
? { id: item.id, title: item.title, summary: item.summary }
|
|
272
|
+
: { id: item.id, title: item.title });
|
|
273
|
+
}
|
|
274
|
+
if (items.length > 0) {
|
|
275
|
+
sections.push({ title: section.title, items });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return sections;
|
|
279
|
+
}
|
|
202
280
|
/** Parse a spec store timestamp, which is UTC but usually has no zone designator. */
|
|
203
281
|
export function parseTimestamp(value) {
|
|
204
282
|
const zoned = /(Z|[+-]\d{2}:?\d{2})$/i.test(value) ? value : `${value}Z`;
|
|
@@ -11,19 +11,42 @@ import type { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
|
|
|
11
11
|
import type { Menu, Widget } from '@lumino/widgets';
|
|
12
12
|
/** The widget id `@jupyterlab/filebrowser-extension:browser` gives the file browser. */
|
|
13
13
|
export declare const FILE_BROWSER_ID = "filebrowser";
|
|
14
|
+
/** The widget id of this extension's Oceanum.io panel, which takes the browser's place. */
|
|
15
|
+
export declare const OCEANUM_PANEL_ID = "datamesh-connect";
|
|
14
16
|
/**
|
|
15
17
|
* File menu entries that manage local files. Save, Save All and Rename stay: saving is
|
|
16
18
|
* routed to Oceanum, and renaming the local file is how a notebook gets its name.
|
|
17
19
|
*/
|
|
18
20
|
export declare const LOCAL_FILE_COMMANDS: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* File > New entries for files that are not notebooks. The spec store holds notebooks
|
|
23
|
+
* only, so such a file could never be saved anywhere the user can see.
|
|
24
|
+
*/
|
|
25
|
+
export declare const NON_NOTEBOOK_NEW_COMMANDS: readonly string[];
|
|
19
26
|
/**
|
|
20
27
|
* Take the file browser out of the sidebar; true if it was there. It is closed, not
|
|
21
28
|
* disposed: other extensions hold it through IDefaultFileBrowser and still use it to
|
|
22
|
-
* open paths.
|
|
29
|
+
* open paths. If it was the open panel, `instead` is opened in its place; a sidebar the
|
|
30
|
+
* user had collapsed stays collapsed.
|
|
23
31
|
*/
|
|
24
32
|
export declare function hideFileBrowser(shell: {
|
|
25
33
|
widgets(area: 'left'): Iterable<Widget>;
|
|
26
|
-
|
|
34
|
+
activateById(id: string): void;
|
|
35
|
+
}, instead?: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Put a left-sidebar widget first, and keep it open if it was. True if it moved.
|
|
38
|
+
*
|
|
39
|
+
* A rank alone is not enough: the layout restorer replays the sidebar order each user
|
|
40
|
+
* last had, so a returning user would keep the panel wherever it used to be. Adding it
|
|
41
|
+
* again after the restore re-inserts it by rank, and the shell saves the new order.
|
|
42
|
+
*/
|
|
43
|
+
export declare function moveToTop(shell: {
|
|
44
|
+
widgets(area: 'left'): Iterable<Widget>;
|
|
45
|
+
add(widget: Widget, area: 'left', options: {
|
|
46
|
+
rank: number;
|
|
47
|
+
}): void;
|
|
48
|
+
activateById(id: string): void;
|
|
49
|
+
}, id: string): boolean;
|
|
27
50
|
/**
|
|
28
51
|
* Submenus of local files. "Open Recent" is built by the document manager rather than
|
|
29
52
|
* declared in a schema, and its menu has no id, so it is known by the commands it holds.
|
package/lib/share/oceanumOnly.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { MessageLoop } from '@lumino/messaging';
|
|
2
2
|
/** The widget id `@jupyterlab/filebrowser-extension:browser` gives the file browser. */
|
|
3
3
|
export const FILE_BROWSER_ID = 'filebrowser';
|
|
4
|
+
/** The widget id of this extension's Oceanum.io panel, which takes the browser's place. */
|
|
5
|
+
export const OCEANUM_PANEL_ID = 'datamesh-connect';
|
|
4
6
|
/**
|
|
5
7
|
* File menu entries that manage local files. Save, Save All and Rename stay: saving is
|
|
6
8
|
* routed to Oceanum, and renaming the local file is how a notebook gets its name.
|
|
@@ -14,20 +16,55 @@ export const LOCAL_FILE_COMMANDS = [
|
|
|
14
16
|
'docmanager:duplicate',
|
|
15
17
|
'docmanager:download'
|
|
16
18
|
];
|
|
19
|
+
/**
|
|
20
|
+
* File > New entries for files that are not notebooks. The spec store holds notebooks
|
|
21
|
+
* only, so such a file could never be saved anywhere the user can see.
|
|
22
|
+
*/
|
|
23
|
+
export const NON_NOTEBOOK_NEW_COMMANDS = [
|
|
24
|
+
'fileeditor:create-new',
|
|
25
|
+
'fileeditor:create-new-markdown-file'
|
|
26
|
+
];
|
|
17
27
|
/**
|
|
18
28
|
* Take the file browser out of the sidebar; true if it was there. It is closed, not
|
|
19
29
|
* disposed: other extensions hold it through IDefaultFileBrowser and still use it to
|
|
20
|
-
* open paths.
|
|
30
|
+
* open paths. If it was the open panel, `instead` is opened in its place; a sidebar the
|
|
31
|
+
* user had collapsed stays collapsed.
|
|
21
32
|
*/
|
|
22
|
-
export function hideFileBrowser(shell) {
|
|
33
|
+
export function hideFileBrowser(shell, instead) {
|
|
23
34
|
for (const widget of shell.widgets('left')) {
|
|
24
35
|
if (widget.id === FILE_BROWSER_ID) {
|
|
36
|
+
const wasOpen = widget.isVisible;
|
|
25
37
|
widget.close();
|
|
38
|
+
if (wasOpen && instead) {
|
|
39
|
+
shell.activateById(instead);
|
|
40
|
+
}
|
|
26
41
|
return true;
|
|
27
42
|
}
|
|
28
43
|
}
|
|
29
44
|
return false;
|
|
30
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Put a left-sidebar widget first, and keep it open if it was. True if it moved.
|
|
48
|
+
*
|
|
49
|
+
* A rank alone is not enough: the layout restorer replays the sidebar order each user
|
|
50
|
+
* last had, so a returning user would keep the panel wherever it used to be. Adding it
|
|
51
|
+
* again after the restore re-inserts it by rank, and the shell saves the new order.
|
|
52
|
+
*/
|
|
53
|
+
export function moveToTop(shell, id) {
|
|
54
|
+
const widgets = Array.from(shell.widgets('left'));
|
|
55
|
+
const index = widgets.findIndex(widget => widget.id === id);
|
|
56
|
+
if (index <= 0) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const widget = widgets[index];
|
|
60
|
+
// Adding hides the widget, so an open panel has to be opened again.
|
|
61
|
+
const wasOpen = widget.isVisible;
|
|
62
|
+
shell.add(widget, 'left', { rank: 0 });
|
|
63
|
+
if (wasOpen) {
|
|
64
|
+
shell.activateById(id);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
31
68
|
/**
|
|
32
69
|
* Submenus of local files. "Open Recent" is built by the document manager rather than
|
|
33
70
|
* declared in a schema, and its menu has no id, so it is known by the commands it holds.
|