@oceanum/oceanumlab 4.7.1 → 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 +35 -5
- package/lib/StoredNotebooks.d.ts +27 -5
- package/lib/StoredNotebooks.js +141 -24
- package/lib/auth/serverAuth.js +1 -0
- package/lib/auth/startSignIn.d.ts +11 -0
- package/lib/auth/startSignIn.js +16 -0
- package/lib/auth/tokens.d.ts +13 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +43 -7
- package/lib/share/client.d.ts +86 -0
- package/lib/share/client.js +206 -0
- package/lib/share/events.d.ts +7 -0
- package/lib/share/events.js +9 -0
- package/lib/share/notebook.d.ts +216 -0
- package/lib/share/notebook.js +355 -0
- package/lib/share/oceanumOnly.d.ts +72 -0
- package/lib/share/oceanumOnly.js +127 -0
- package/lib/share/plugin.d.ts +23 -0
- package/lib/share/plugin.js +981 -0
- package/lib/share/upload.d.ts +6 -0
- package/lib/share/upload.js +27 -0
- package/lib/share/widgets.d.ts +25 -0
- package/lib/share/widgets.js +154 -0
- package/package.json +5 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshWidget.tsx +79 -2
- package/src/StoredNotebooks.tsx +274 -24
- package/src/__tests__/OceanumPanel.spec.tsx +159 -5
- package/src/__tests__/StoredNotebooks.spec.tsx +528 -0
- package/src/__tests__/oceanumOnly.spec.ts +317 -0
- package/src/__tests__/settingsSchema.spec.ts +11 -1
- package/src/__tests__/shareClient.spec.ts +328 -0
- package/src/__tests__/shareNotebook.spec.ts +624 -0
- package/src/__tests__/sharePlugin.spec.ts +664 -0
- package/src/auth/serverAuth.ts +1 -0
- package/src/auth/startSignIn.ts +23 -0
- package/src/auth/tokens.ts +13 -0
- package/src/index.ts +43 -7
- package/src/share/client.ts +291 -0
- package/src/share/events.ts +10 -0
- package/src/share/notebook.ts +525 -0
- package/src/share/oceanumOnly.ts +177 -0
- package/src/share/plugin.ts +1220 -0
- package/src/share/upload.ts +27 -0
- package/src/share/widgets.ts +217 -0
- package/style/index.css +212 -18
- package/lib/specStore.d.ts +0 -37
- package/lib/specStore.js +0 -57
- package/src/__tests__/specStore.spec.ts +0 -126
- package/src/specStore.ts +0 -82
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
|
@@ -9,9 +9,10 @@ import { Signal } from '@lumino/signaling';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { marked } from 'marked';
|
|
11
11
|
import { DatasourceItem } from './DatasourceItem';
|
|
12
|
+
import { startSignIn } from './auth/startSignIn';
|
|
12
13
|
import { StoredNotebooks } from './StoredNotebooks';
|
|
13
14
|
import { Tabs } from './Tabs';
|
|
14
|
-
import { SIGN_IN_COMMAND, aiBackendUrl, aiCredentialSource, canSignIn, pastedToken, resolveAiCredential, signInToken } from './aiBackend';
|
|
15
|
+
import { SIGN_IN_COMMAND, aiBackendUrl, aiCredentialSource, canSignIn, isSignedIn, pastedToken, resolveAiCredential, signInToken } from './aiBackend';
|
|
15
16
|
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
16
17
|
import { describeProgress, onProgress } from './progress';
|
|
17
18
|
// Configure marked for safe rendering
|
|
@@ -292,6 +293,7 @@ function readAiAccess(commands, settings) {
|
|
|
292
293
|
source: aiCredentialSource(datameshToken, commands),
|
|
293
294
|
pasted: pastedToken(datameshToken),
|
|
294
295
|
canSignIn: canSignIn(commands),
|
|
296
|
+
signedIn: isSignedIn(commands),
|
|
295
297
|
backend: aiBackendUrl(settings.aiBackendUrl())
|
|
296
298
|
};
|
|
297
299
|
}
|
|
@@ -322,6 +324,26 @@ function useAiAccess(commands, settings) {
|
|
|
322
324
|
}, [commands, settings]);
|
|
323
325
|
return access;
|
|
324
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
|
+
}
|
|
325
347
|
/**
|
|
326
348
|
* Shows a message prompting the user to sign in to Oceanum.io, or to configure
|
|
327
349
|
* their Datamesh token where the site has no sign-in. Only renders when there
|
|
@@ -609,8 +631,9 @@ function AIChatPanel({ commands, settings }) {
|
|
|
609
631
|
* credential to work with — otherwise it would open on the same nothing the panel
|
|
610
632
|
* rendered before there were tabs, except now with a label promising otherwise.
|
|
611
633
|
*/
|
|
612
|
-
function PanelTabs({ auth, commands, settings, renderDatamesh, selected, onSelect }) {
|
|
634
|
+
function PanelTabs({ auth, commands, settings, renderDatamesh, openStoredNotebook, openExample, uploadNotebook, showExamples, setShowExamples, selected, onSelect }) {
|
|
613
635
|
const access = useAiAccess(commands, settings);
|
|
636
|
+
const examplesShown = useShowExamples(settings.settingsChanged, showExamples);
|
|
614
637
|
const tabs = [
|
|
615
638
|
{
|
|
616
639
|
id: 'notebooks',
|
|
@@ -618,11 +641,18 @@ function PanelTabs({ auth, commands, settings, renderDatamesh, selected, onSelec
|
|
|
618
641
|
render: () =>
|
|
619
642
|
// An auth with no environment is a provider that has nothing to sign in to (the
|
|
620
643
|
// server has sign-in turned off). Asking the user to sign in would be a dead end.
|
|
621
|
-
(auth === null || auth === void 0 ? void 0 : auth.environment) ? (React.createElement(StoredNotebooks, { auth: auth
|
|
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: () => {
|
|
645
|
+
startSignIn(commands, auth).catch(error => {
|
|
646
|
+
console.warn('Oceanum.io sign-in could not start.', error);
|
|
647
|
+
});
|
|
648
|
+
} })) : (React.createElement("div", { className: "oceanum-text-empty" }, "Oceanum.io sign-in is not configured for this host."))
|
|
622
649
|
},
|
|
623
650
|
{ id: 'datamesh', label: 'Datamesh', render: renderDatamesh }
|
|
624
651
|
];
|
|
625
|
-
|
|
652
|
+
// Signed in, not merely holding a credential: a Datamesh token pasted into the
|
|
653
|
+
// settings would reach the backend, but the tab stays out of the way until someone
|
|
654
|
+
// has signed in to Oceanum.io.
|
|
655
|
+
if (access.signedIn) {
|
|
626
656
|
tabs.push({
|
|
627
657
|
id: 'ai',
|
|
628
658
|
label: 'Oceanum AI',
|
|
@@ -684,7 +714,7 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
684
714
|
React.createElement("span", { className: "oceanum-sidebar-title" }, "Oceanum.io")),
|
|
685
715
|
React.createElement(UseSignal, { signal: this.tabChanged, initialArgs: this.selectedTab }, () => {
|
|
686
716
|
var _a;
|
|
687
|
-
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(), 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) }));
|
|
688
718
|
})));
|
|
689
719
|
}
|
|
690
720
|
/**
|
package/lib/StoredNotebooks.d.ts
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { IOceanumAuth } from './auth/tokens';
|
|
3
|
-
import { ISpecSummary } from './
|
|
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
|
/**
|
|
7
|
-
* Open a stored notebook
|
|
8
|
-
*
|
|
9
|
-
* until OCE-182. The list then reads rather than offering an action that does nothing.
|
|
9
|
+
* Open a stored notebook. Undefined where nothing has registered a way to open one;
|
|
10
|
+
* the list then reads rather than offering a click that does nothing.
|
|
10
11
|
*/
|
|
11
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;
|
|
29
|
+
/**
|
|
30
|
+
* Start signing in the way this host shows it; without it the auth's own `signIn()`
|
|
31
|
+
* is used, which on a JupyterLab server shows no code dialog.
|
|
32
|
+
*/
|
|
33
|
+
onSignIn?: () => void;
|
|
12
34
|
}
|
|
13
35
|
/**
|
|
14
36
|
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
15
37
|
*/
|
|
16
|
-
export declare function StoredNotebooks({ auth, onOpen }: IStoredNotebooksProps): React.ReactElement;
|
|
38
|
+
export declare function StoredNotebooks({ auth, onOpen, onOpenExample, onUpload, showExamples, onShowExamplesChange, onSignIn }: IStoredNotebooksProps): React.ReactElement;
|
package/lib/StoredNotebooks.js
CHANGED
|
@@ -1,30 +1,120 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { SpecStoreClient } from './share/client';
|
|
3
|
+
import { notebooksChanged } from './share/events';
|
|
4
|
+
import { parseNotebookDemo, partitionSummaries } from './share/notebook';
|
|
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
|
+
}
|
|
3
80
|
function Section({ title, items, empty, onOpen }) {
|
|
81
|
+
// Each row carries its record id so the share plugin's context menu (Open, Rename,
|
|
82
|
+
// Share, Delete) can tell which one was clicked.
|
|
83
|
+
const rowAttributes = (item) => {
|
|
84
|
+
var _a;
|
|
85
|
+
return ({
|
|
86
|
+
[SPEC_ID_ATTRIBUTE]: item.id,
|
|
87
|
+
title: (_a = item.description) !== null && _a !== void 0 ? _a : undefined
|
|
88
|
+
});
|
|
89
|
+
};
|
|
4
90
|
return (React.createElement("div", { className: "oceanum-notebooks-section" },
|
|
5
91
|
React.createElement("div", { className: "oceanum-notebooks-heading" },
|
|
6
92
|
title,
|
|
7
93
|
React.createElement("span", { className: "oceanum-notebooks-count" }, items.length)),
|
|
8
|
-
items.length === 0 ? (React.createElement("div", { className: "oceanum-text-empty" }, empty)) : (React.createElement("ul", { className: "oceanum-notebooks-list" }, items.map(item => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled'))) : (React.createElement("div", { className: "oceanum-notebooks-item oceanum-notebooks-item-static", title: (_b = item.description) !== null && _b !== void 0 ? _b : undefined },
|
|
12
|
-
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled')))));
|
|
13
|
-
})))));
|
|
94
|
+
items.length === 0 ? (React.createElement("div", { className: "oceanum-text-empty" }, empty)) : (React.createElement("ul", { className: "oceanum-notebooks-list" }, items.map(item => (React.createElement("li", { key: item.id }, onOpen ? (React.createElement("button", { type: "button", className: "oceanum-notebooks-item", onClick: () => onOpen(item), ...rowAttributes(item) },
|
|
95
|
+
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled'))) : (React.createElement("div", { className: "oceanum-notebooks-item oceanum-notebooks-item-static", ...rowAttributes(item) },
|
|
96
|
+
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled'))))))))));
|
|
14
97
|
}
|
|
15
98
|
/**
|
|
16
99
|
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
17
100
|
*/
|
|
18
|
-
export function StoredNotebooks({ auth, onOpen }) {
|
|
101
|
+
export function StoredNotebooks({ auth, onOpen, onOpenExample, onUpload, showExamples = true, onShowExamplesChange, onSignIn }) {
|
|
102
|
+
var _a;
|
|
19
103
|
const [load, setLoad] = React.useState({ state: 'loading' });
|
|
20
104
|
const [user, setUser] = React.useState(auth.user);
|
|
105
|
+
// Bumped whenever the store changes, so the list is fetched again.
|
|
106
|
+
const [version, setVersion] = React.useState(0);
|
|
21
107
|
// Reload whenever the signed-in user changes, so signing in or out does not leave a
|
|
22
|
-
// stale list from the previous session on screen
|
|
108
|
+
// stale list from the previous session on screen; and whenever this extension has
|
|
109
|
+
// created, renamed or deleted a record.
|
|
23
110
|
React.useEffect(() => {
|
|
24
111
|
const onUser = () => setUser(auth.user);
|
|
112
|
+
const onChanged = () => setVersion(v => v + 1);
|
|
25
113
|
auth.userChanged.connect(onUser);
|
|
114
|
+
notebooksChanged.connect(onChanged);
|
|
26
115
|
return () => {
|
|
27
116
|
auth.userChanged.disconnect(onUser);
|
|
117
|
+
notebooksChanged.disconnect(onChanged);
|
|
28
118
|
};
|
|
29
119
|
}, [auth]);
|
|
30
120
|
React.useEffect(() => {
|
|
@@ -32,23 +122,25 @@ export function StoredNotebooks({ auth, onOpen }) {
|
|
|
32
122
|
let cancelled = false;
|
|
33
123
|
const specs = (_b = (_a = auth.urls) === null || _a === void 0 ? void 0 : _a.specs) !== null && _b !== void 0 ? _b : null;
|
|
34
124
|
if (!user || !specs) {
|
|
35
|
-
setLoad({ state: 'ready', items: [] });
|
|
125
|
+
setLoad({ state: 'ready', items: [], examples: { state: 'none' } });
|
|
36
126
|
return;
|
|
37
127
|
}
|
|
38
128
|
setLoad({ state: 'loading' });
|
|
39
129
|
void (async () => {
|
|
130
|
+
var _a, _b;
|
|
40
131
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
132
|
+
const client = new SpecStoreClient({
|
|
133
|
+
specsUrl: specs,
|
|
134
|
+
getAccessToken: () => auth.getAccessToken()
|
|
135
|
+
});
|
|
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
|
+
]);
|
|
50
142
|
if (!cancelled) {
|
|
51
|
-
setLoad({ state: 'ready', items });
|
|
143
|
+
setLoad({ state: 'ready', items, examples });
|
|
52
144
|
}
|
|
53
145
|
}
|
|
54
146
|
catch (error) {
|
|
@@ -65,9 +157,26 @@ export function StoredNotebooks({ auth, onOpen }) {
|
|
|
65
157
|
return () => {
|
|
66
158
|
cancelled = true;
|
|
67
159
|
};
|
|
68
|
-
}, [auth, user]);
|
|
160
|
+
}, [auth, user, version]);
|
|
69
161
|
if (!user) {
|
|
70
|
-
|
|
162
|
+
const signIn = () => {
|
|
163
|
+
if (onSignIn) {
|
|
164
|
+
onSignIn();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
auth.signIn().catch(error => {
|
|
168
|
+
console.warn('Oceanum.io sign-in could not start.', error);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
return (React.createElement("div", { className: "oceanum-text-empty" },
|
|
172
|
+
React.createElement("a", { role: "button", tabIndex: 0, onClick: signIn, onKeyDown: event => {
|
|
173
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
signIn();
|
|
176
|
+
}
|
|
177
|
+
} }, "Sign in"),
|
|
178
|
+
' ',
|
|
179
|
+
"to Oceanum.io to see your notebooks."));
|
|
71
180
|
}
|
|
72
181
|
if (load.state === 'loading') {
|
|
73
182
|
return React.createElement("div", { className: "oceanum-text-empty" }, "Loading\u2026");
|
|
@@ -77,8 +186,16 @@ export function StoredNotebooks({ auth, onOpen }) {
|
|
|
77
186
|
// the account has no notebooks when the load simply failed.
|
|
78
187
|
return React.createElement("div", { className: "oceanum-text-error" }, load.message);
|
|
79
188
|
}
|
|
80
|
-
const { mine, shared } =
|
|
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
|
+
: []);
|
|
81
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,
|
|
82
198
|
React.createElement(Section, { title: "My notebooks", items: mine, empty: "You have not saved any notebooks yet.", onOpen: onOpen }),
|
|
83
|
-
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 })));
|
|
84
201
|
}
|
package/lib/auth/serverAuth.js
CHANGED
|
@@ -81,6 +81,7 @@ export function readServerAuthOption(option, hostname) {
|
|
|
81
81
|
clientId: config.clientId,
|
|
82
82
|
oceanumDomain: typeof config.oceanumDomain === 'string' ? config.oceanumDomain : '',
|
|
83
83
|
signInRedirect: false,
|
|
84
|
+
fileManagement: config.fileManagement === 'oceanum' ? 'oceanum' : 'local',
|
|
84
85
|
urls
|
|
85
86
|
};
|
|
86
87
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
2
|
+
import type { IOceanumAuth } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* Start signing in the way this host shows it.
|
|
5
|
+
*
|
|
6
|
+
* On a JupyterLab server the device sign-in command both starts the flow and shows the
|
|
7
|
+
* code dialog; `auth.signIn()` alone starts the flow and shows nothing, so the user is
|
|
8
|
+
* left with a "Signing in…" top bar and no code. notebook.oceanum.io disables that
|
|
9
|
+
* command and provides an auth whose `signIn()` opens the Oceanum sign-in itself.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startSignIn(commands: CommandRegistry, auth: IOceanumAuth): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CommandIDs } from './accountWidget';
|
|
2
|
+
/**
|
|
3
|
+
* Start signing in the way this host shows it.
|
|
4
|
+
*
|
|
5
|
+
* On a JupyterLab server the device sign-in command both starts the flow and shows the
|
|
6
|
+
* code dialog; `auth.signIn()` alone starts the flow and shows nothing, so the user is
|
|
7
|
+
* left with a "Signing in…" top bar and no code. notebook.oceanum.io disables that
|
|
8
|
+
* command and provides an auth whose `signIn()` opens the Oceanum sign-in itself.
|
|
9
|
+
*/
|
|
10
|
+
export async function startSignIn(commands, auth) {
|
|
11
|
+
if (commands.hasCommand(CommandIDs.signIn)) {
|
|
12
|
+
await commands.execute(CommandIDs.signIn);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await auth.signIn();
|
|
16
|
+
}
|
package/lib/auth/tokens.d.ts
CHANGED
|
@@ -24,6 +24,19 @@ export interface IOceanumEnvironment {
|
|
|
24
24
|
* the site origin is an allowed callback URL.
|
|
25
25
|
*/
|
|
26
26
|
readonly signInRedirect: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether Oceanum.io is the only file management this host shows, as on
|
|
29
|
+
* notebook.oceanum.io: the file browser and the File menu's local-file entries go,
|
|
30
|
+
* and every local save is pushed to the spec store. `'local'` (the default when
|
|
31
|
+
* absent) keeps JupyterLab's file management alongside the Oceanum commands.
|
|
32
|
+
*/
|
|
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;
|
|
27
40
|
}
|
|
28
41
|
/** Service base URLs for an environment, without trailing slashes. */
|
|
29
42
|
export interface IOceanumServiceUrls {
|
package/lib/index.d.ts
CHANGED
|
@@ -19,5 +19,5 @@ export declare const oceanum_ai_extension: JupyterFrontEndPlugin<void>;
|
|
|
19
19
|
* JupyterLab shares `@oceanum/oceanumlab` between extensions as a singleton module.
|
|
20
20
|
*/
|
|
21
21
|
export * from './auth/tokens';
|
|
22
|
-
declare const _default: (JupyterFrontEndPlugin<
|
|
22
|
+
declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IOceanumAuth>)[];
|
|
23
23
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { reporterFor } from './progress';
|
|
|
14
14
|
import { ConversationPin } from './conversationPin';
|
|
15
15
|
import { deviceAuthPlugin } from './auth/plugin';
|
|
16
16
|
import { IOceanumAuth } from './auth/tokens';
|
|
17
|
+
import { CommandIDs as ShareCommandIDs, sharePlugin } from './share/plugin';
|
|
18
|
+
import { OCEANUM_PANEL_ID } from './share/oceanumOnly';
|
|
17
19
|
import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
18
20
|
import { notebookHost } from './notebookHost';
|
|
19
21
|
import { KernelHandoff } from './kernelHandoff';
|
|
@@ -54,6 +56,9 @@ export const datamesh_connect_extension = {
|
|
|
54
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;
|
|
55
57
|
// The `datameshToken` setting for the AI chat; empty until the settings load.
|
|
56
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;
|
|
57
62
|
// Tells the sidebar the settings above have changed.
|
|
58
63
|
const settingsChanged = new Signal(app);
|
|
59
64
|
const findDatameshUI = () => find(app.shell.widgets('main'), (widget) => widget.id === 'datamesh-ui');
|
|
@@ -69,6 +74,7 @@ export const datamesh_connect_extension = {
|
|
|
69
74
|
}
|
|
70
75
|
window.injectToken = set.get('injectToken').user;
|
|
71
76
|
datameshTokenSetting = set.get('datameshToken').composite;
|
|
77
|
+
showExamplesSetting = set.get('showExamples').composite;
|
|
72
78
|
settingsChanged.emit();
|
|
73
79
|
};
|
|
74
80
|
//Try to get the datamesh token from the envars
|
|
@@ -81,6 +87,10 @@ export const datamesh_connect_extension = {
|
|
|
81
87
|
})
|
|
82
88
|
.catch(reason => {
|
|
83
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();
|
|
84
94
|
});
|
|
85
95
|
const getCurrentWidget = () => {
|
|
86
96
|
return app.shell.currentWidget;
|
|
@@ -111,14 +121,39 @@ export const datamesh_connect_extension = {
|
|
|
111
121
|
settingsChanged,
|
|
112
122
|
commands: app.commands,
|
|
113
123
|
getCurrentWidget,
|
|
114
|
-
auth
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
auth,
|
|
125
|
+
openStoredNotebook: id => {
|
|
126
|
+
app.commands.execute(ShareCommandIDs.open, { id }).catch(error => {
|
|
127
|
+
console.error('Oceanum: could not open the stored notebook.', error);
|
|
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
|
+
});
|
|
154
|
+
}
|
|
120
155
|
});
|
|
121
|
-
datameshConnectWidget.id =
|
|
156
|
+
datameshConnectWidget.id = OCEANUM_PANEL_ID;
|
|
122
157
|
datameshConnectWidget.title.icon = oceanumIcon;
|
|
123
158
|
datameshConnectWidget.title.caption = 'Datamesh Connect';
|
|
124
159
|
restorer.add(datameshConnectWidget, 'datamesh-connect');
|
|
@@ -327,6 +362,7 @@ export const oceanum_ai_extension = {
|
|
|
327
362
|
export * from './auth/tokens';
|
|
328
363
|
export default [
|
|
329
364
|
deviceAuthPlugin,
|
|
365
|
+
sharePlugin,
|
|
330
366
|
datamesh_connect_extension,
|
|
331
367
|
oceanum_ai_extension
|
|
332
368
|
];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ISpecBody, ISpecPatch, ISpecRecord, ISpecSummary } from './notebook';
|
|
2
|
+
export type ErrorKind = 'signed-out' | 'expired' | 'forbidden' | 'not-found' | 'too-large' | 'network' | 'invalid' | 'unsupported' | 'server';
|
|
3
|
+
/** A spec store failure with a message that is safe to show to the user. */
|
|
4
|
+
export declare class SpecStoreError extends Error {
|
|
5
|
+
readonly kind: ErrorKind;
|
|
6
|
+
readonly status: number | null;
|
|
7
|
+
constructor(kind: ErrorKind, status?: number | null);
|
|
8
|
+
}
|
|
9
|
+
/** Map a failed HTTP status to an error. */
|
|
10
|
+
export declare function errorForStatus(status: number): SpecStoreError;
|
|
11
|
+
export type SharePermission = 'read' | 'write';
|
|
12
|
+
/** A permission grant, as accepted by `POST /specs/notebook/{id}/permissions`. */
|
|
13
|
+
export type PermissionGrant = {
|
|
14
|
+
type: 'user';
|
|
15
|
+
entity: string;
|
|
16
|
+
permission: SharePermission;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'public';
|
|
19
|
+
entity: '';
|
|
20
|
+
permission: 'read';
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* What the share dialog asks for: add grants, or remove one (e.g. stop public access).
|
|
24
|
+
*
|
|
25
|
+
* Granting carries a list because the dialog shares with several people at once, while
|
|
26
|
+
* the spec store takes one grant per request. `invalid` carries back anything typed that
|
|
27
|
+
* is not a shareable address, so the caller can name it rather than failing with
|
|
28
|
+
* "invalid email".
|
|
29
|
+
*/
|
|
30
|
+
export type ShareChoice = {
|
|
31
|
+
action: 'grant';
|
|
32
|
+
grants: PermissionGrant[];
|
|
33
|
+
invalid: string[];
|
|
34
|
+
} | {
|
|
35
|
+
action: 'revoke';
|
|
36
|
+
grant: PermissionGrant;
|
|
37
|
+
};
|
|
38
|
+
export interface ISpecStoreClientOptions {
|
|
39
|
+
/** Spec store base URL, e.g. `https://specs.oceanum.io`. */
|
|
40
|
+
specsUrl: string;
|
|
41
|
+
/** Returns a current access token or `null`. Called for every request. */
|
|
42
|
+
getAccessToken: () => Promise<string | null>;
|
|
43
|
+
/** The spec type the client reads and writes; `notebook` unless given. */
|
|
44
|
+
specType?: string;
|
|
45
|
+
fetch?: typeof fetch;
|
|
46
|
+
}
|
|
47
|
+
/** Client for one spec type of the Oceanum spec store, `notebook` by default. */
|
|
48
|
+
export declare class SpecStoreClient {
|
|
49
|
+
constructor(options: ISpecStoreClientOptions);
|
|
50
|
+
/** Every notebook the user may read, including public ones. */
|
|
51
|
+
list(): Promise<ISpecSummary[]>;
|
|
52
|
+
/** Read a record, anonymously when signed out. */
|
|
53
|
+
get(id: string): Promise<ISpecRecord>;
|
|
54
|
+
/** Create a record. The server mints the id: use the returned record's. */
|
|
55
|
+
create(body: ISpecBody): Promise<ISpecRecord>;
|
|
56
|
+
/** Replace a record's name, description and notebook. */
|
|
57
|
+
update(id: string, body: ISpecBody): Promise<ISpecRecord>;
|
|
58
|
+
/**
|
|
59
|
+
* Change some of a record's fields and leave the rest as they are. Needs the same
|
|
60
|
+
* write access as `update`, which the store enforces a patch as.
|
|
61
|
+
*/
|
|
62
|
+
patch(id: string, changes: ISpecPatch): Promise<ISpecRecord>;
|
|
63
|
+
/**
|
|
64
|
+
* Give a record a new name, without sending the notebook back with it.
|
|
65
|
+
*
|
|
66
|
+
* Falls back to reading the record and putting it back where the store has no PATCH
|
|
67
|
+
* (production had none when this was written). That fallback races another writer's
|
|
68
|
+
* change to the notebook, which is the whole reason to prefer a patch.
|
|
69
|
+
*/
|
|
70
|
+
rename(id: string, name: string): Promise<ISpecRecord>;
|
|
71
|
+
/** Delete a record (requires admin access to it). */
|
|
72
|
+
remove(id: string): Promise<void>;
|
|
73
|
+
/** Grant a permission on a record (requires admin access to it). */
|
|
74
|
+
addPermission(id: string, grant: PermissionGrant): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Remove a permission (requires admin access). The server matches the exact grant and
|
|
77
|
+
* answers 404 when there is no such permission.
|
|
78
|
+
*/
|
|
79
|
+
removePermission(id: string, grant: PermissionGrant): Promise<void>;
|
|
80
|
+
private _url;
|
|
81
|
+
private _record;
|
|
82
|
+
private _request;
|
|
83
|
+
private readonly _base;
|
|
84
|
+
private readonly _getAccessToken;
|
|
85
|
+
private readonly _fetch;
|
|
86
|
+
}
|