@oceanum/oceanumlab 4.7.2 → 4.9.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 +88 -7
- 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 +26 -0
- package/lib/share/notebook.js +66 -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 +114 -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 +197 -6
- package/src/__tests__/OceanumPanel.spec.tsx +94 -1
- package/src/__tests__/StoredNotebooks.spec.tsx +368 -4
- 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 +116 -0
- package/src/__tests__/sharePlugin.spec.ts +253 -8
- package/src/index.ts +35 -1
- package/src/share/client.ts +5 -2
- package/src/share/notebook.ts +88 -10
- package/src/share/oceanumOnly.ts +55 -4
- package/src/share/plugin.ts +143 -11
- package/src/share/upload.ts +27 -0
- package/style/index.css +94 -2
package/lib/share/plugin.js
CHANGED
|
@@ -9,11 +9,14 @@ import { startSignIn } from '../auth/startSignIn';
|
|
|
9
9
|
import { IOceanumAuth } from '../auth/tokens';
|
|
10
10
|
import { SpecStoreClient, SpecStoreError } from './client';
|
|
11
11
|
import { notebooksChanged } from './events';
|
|
12
|
-
import { buildSpecBody, isSpecId, METADATA_KEY, notebookFromRecord, notebookNamesFor, OCEANUM_DIR, partitionSummaries, QUERY_PARAM, readLink, reportForShareFailures, sanitizeName, shareLink, uniqueNotebookPath } from './notebook';
|
|
13
|
-
import { hideFileBrowser, pruneBeforeEachOpen, pruneFileMenu, routeSaves } from './oceanumOnly';
|
|
12
|
+
import { buildSpecBody, isSpecId, METADATA_KEY, notebookFromRecord, notebookFromUpload, notebookNamesFor, NOTEBOOK_DEMO_TYPE, OCEANUM_DIR, partitionSummaries, QUERY_PARAM, readLink, reportForShareFailures, sanitizeName, shareLink, uniqueNotebookPath, unlinkedNotebookFromRecord } from './notebook';
|
|
13
|
+
import { hideFileBrowser, moveToTop, NON_NOTEBOOK_NEW_COMMANDS, OCEANUM_PANEL_ID, pruneBeforeEachOpen, pruneFileMenu, routeSaves } from './oceanumOnly';
|
|
14
|
+
import { chooseNotebookFile } from './upload';
|
|
14
15
|
import { NotebookListBody, ShareBody } from './widgets';
|
|
15
16
|
export const CommandIDs = {
|
|
16
17
|
open: 'oceanum-share:open',
|
|
18
|
+
openExample: 'oceanum-share:open-example',
|
|
19
|
+
upload: 'oceanum-share:upload',
|
|
17
20
|
save: 'oceanum-share:save',
|
|
18
21
|
share: 'oceanum-share:share',
|
|
19
22
|
rename: 'oceanum-share:rename',
|
|
@@ -48,6 +51,11 @@ export const sharePlugin = {
|
|
|
48
51
|
client: new SpecStoreClient({
|
|
49
52
|
specsUrl,
|
|
50
53
|
getAccessToken: () => auth.getAccessToken()
|
|
54
|
+
}),
|
|
55
|
+
demos: new SpecStoreClient({
|
|
56
|
+
specsUrl,
|
|
57
|
+
getAccessToken: () => auth.getAccessToken(),
|
|
58
|
+
specType: NOTEBOOK_DEMO_TYPE
|
|
51
59
|
})
|
|
52
60
|
};
|
|
53
61
|
const oceanumOnly = store !== null && ((_c = auth.environment) === null || _c === void 0 ? void 0 : _c.fileManagement) === 'oceanum';
|
|
@@ -221,6 +229,19 @@ export const sharePlugin = {
|
|
|
221
229
|
}
|
|
222
230
|
return null;
|
|
223
231
|
}
|
|
232
|
+
/** Write a notebook to `path` on the local drive and open it. */
|
|
233
|
+
async function writeAndOpen(path, content) {
|
|
234
|
+
await docManager.services.contents.save(path, {
|
|
235
|
+
type: 'notebook',
|
|
236
|
+
format: 'json',
|
|
237
|
+
content
|
|
238
|
+
});
|
|
239
|
+
const widget = docManager.openOrReveal(path);
|
|
240
|
+
if (!widget) {
|
|
241
|
+
throw new Error(`Could not open ${path}.`);
|
|
242
|
+
}
|
|
243
|
+
return widget;
|
|
244
|
+
}
|
|
224
245
|
/** Write a record to the local drive as `Oceanum/<name>.ipynb` and open it. */
|
|
225
246
|
async function openRecord(s, record) {
|
|
226
247
|
var _a;
|
|
@@ -236,14 +257,7 @@ export const sharePlugin = {
|
|
|
236
257
|
// named "<name> (1)", and a record takes its name from the file on every save, so
|
|
237
258
|
// opening the same notebook twice would quietly rename it on Oceanum.
|
|
238
259
|
const path = (_a = (await pathHoldingRecord(s, record, names))) !== null && _a !== void 0 ? _a : uniqueNotebookPath(record.name, names);
|
|
239
|
-
await
|
|
240
|
-
type: 'notebook',
|
|
241
|
-
format: 'json',
|
|
242
|
-
content
|
|
243
|
-
});
|
|
244
|
-
if (!docManager.openOrReveal(path)) {
|
|
245
|
-
throw new Error(`Could not open ${path}.`);
|
|
246
|
-
}
|
|
260
|
+
await writeAndOpen(path, content);
|
|
247
261
|
}
|
|
248
262
|
async function openById(id) {
|
|
249
263
|
if (!store ||
|
|
@@ -257,6 +271,70 @@ export const sharePlugin = {
|
|
|
257
271
|
await reportError('Open from Oceanum failed', error);
|
|
258
272
|
}
|
|
259
273
|
}
|
|
274
|
+
// The copy each example was last opened into, so opening it again while that copy
|
|
275
|
+
// is still open brings it forward rather than writing another.
|
|
276
|
+
const exampleCopies = new Map();
|
|
277
|
+
// Examples whose copy is still being written, so a second click in that time does
|
|
278
|
+
// not write a second copy.
|
|
279
|
+
const openingExamples = new Set();
|
|
280
|
+
/**
|
|
281
|
+
* Open a copy of an example as a new file in the Oceanum folder, untrusted and
|
|
282
|
+
* linked to nothing. Not `openRecord`: that links the file to the example's record,
|
|
283
|
+
* which is Oceanum's and read-only to everyone else, so the first save would be
|
|
284
|
+
* refused. Unlinked, the first save creates a record of the user's own.
|
|
285
|
+
*/
|
|
286
|
+
async function openExample(id, title) {
|
|
287
|
+
if (!store ||
|
|
288
|
+
!(await ensureSignedIn('Opening notebooks needs an Oceanum.io account.'))) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const copy = exampleCopies.get(id);
|
|
292
|
+
if (copy && !copy.isDisposed) {
|
|
293
|
+
app.shell.activateById(copy.id);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (openingExamples.has(id)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
openingExamples.add(id);
|
|
300
|
+
try {
|
|
301
|
+
const record = await store.demos.get(id);
|
|
302
|
+
const content = unlinkedNotebookFromRecord(record);
|
|
303
|
+
const path = uniqueNotebookPath(title || record.name, await oceanumFolderNames());
|
|
304
|
+
exampleCopies.set(id, await writeAndOpen(path, content));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
await reportError('Open from Oceanum failed', error);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
openingExamples.delete(id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Bring a notebook from the user's computer into the Oceanum folder and open it,
|
|
315
|
+
* untrusted and linked to nothing. It reaches Oceanum on its first save, like any
|
|
316
|
+
* new notebook; nothing is sent on upload.
|
|
317
|
+
*/
|
|
318
|
+
async function uploadNotebook() {
|
|
319
|
+
if (!store ||
|
|
320
|
+
!(await ensureSignedIn('Uploading notebooks needs an Oceanum.io account.'))) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const file = await chooseNotebookFile();
|
|
325
|
+
if (!file) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const content = notebookFromUpload(await file.text());
|
|
329
|
+
const path = uniqueNotebookPath(file.name, await oceanumFolderNames());
|
|
330
|
+
await writeAndOpen(path, content);
|
|
331
|
+
// Outside Oceanum-only mode a plain Save stays local.
|
|
332
|
+
Notification.info(`Uploaded "${file.name}". ${oceanumOnly ? 'Save it' : 'Use Save to Oceanum'} to store it on Oceanum.io.`, { autoClose: 5000 });
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
await reportError('Upload failed', error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
260
338
|
async function openFromOceanum() {
|
|
261
339
|
var _a, _b;
|
|
262
340
|
if (!store ||
|
|
@@ -757,6 +835,23 @@ export const sharePlugin = {
|
|
|
757
835
|
await (id ? openById(id) : openFromOceanum());
|
|
758
836
|
}
|
|
759
837
|
});
|
|
838
|
+
// Run by the Notebooks tab's Examples; not offered in menus or the palette.
|
|
839
|
+
app.commands.addCommand(CommandIDs.openExample, {
|
|
840
|
+
label: 'Open a copy',
|
|
841
|
+
caption: 'Open a copy of an Oceanum example notebook',
|
|
842
|
+
isEnabled: () => signedIn(),
|
|
843
|
+
execute: async (args) => {
|
|
844
|
+
if (isSpecId(args.id)) {
|
|
845
|
+
await openExample(args.id, typeof args.title === 'string' ? args.title : '');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
app.commands.addCommand(CommandIDs.upload, {
|
|
850
|
+
label: label('Upload Notebook…'),
|
|
851
|
+
caption: 'Upload a notebook from this computer, to save on Oceanum.io',
|
|
852
|
+
isEnabled: () => signedIn(),
|
|
853
|
+
execute: () => uploadNotebook()
|
|
854
|
+
});
|
|
760
855
|
app.commands.addCommand(CommandIDs.save, {
|
|
761
856
|
label: label('Save to Oceanum'),
|
|
762
857
|
caption: 'Save the notebook to Oceanum.io',
|
|
@@ -808,6 +903,7 @@ export const sharePlugin = {
|
|
|
808
903
|
});
|
|
809
904
|
const commands = [
|
|
810
905
|
CommandIDs.open,
|
|
906
|
+
CommandIDs.upload,
|
|
811
907
|
CommandIDs.save,
|
|
812
908
|
CommandIDs.share,
|
|
813
909
|
CommandIDs.rename,
|
|
@@ -865,7 +961,9 @@ export const sharePlugin = {
|
|
|
865
961
|
});
|
|
866
962
|
if (oceanumOnly) {
|
|
867
963
|
void app.restored.then(() => {
|
|
868
|
-
hideFileBrowser(app.shell);
|
|
964
|
+
hideFileBrowser(app.shell, OCEANUM_PANEL_ID);
|
|
965
|
+
// Oceanum is where notebooks live now, so its panel leads the sidebar.
|
|
966
|
+
moveToTop(app.shell, OCEANUM_PANEL_ID);
|
|
869
967
|
// IFileMenu is the narrow interface; the menu itself is a Lumino Menu, which
|
|
870
968
|
// is what can drop items.
|
|
871
969
|
const fileMenu = mainMenu === null || mainMenu === void 0 ? void 0 : mainMenu.fileMenu;
|
|
@@ -873,6 +971,11 @@ export const sharePlugin = {
|
|
|
873
971
|
pruneFileMenu(fileMenu);
|
|
874
972
|
pruneBeforeEachOpen(fileMenu);
|
|
875
973
|
}
|
|
974
|
+
const newMenu = mainMenu === null || mainMenu === void 0 ? void 0 : mainMenu.fileMenu.newMenu;
|
|
975
|
+
if (newMenu instanceof Menu) {
|
|
976
|
+
pruneFileMenu(newMenu, NON_NOTEBOOK_NEW_COMMANDS);
|
|
977
|
+
pruneBeforeEachOpen(newMenu, NON_NOTEBOOK_NEW_COMMANDS);
|
|
978
|
+
}
|
|
876
979
|
routeSaves(tracker, pushQuietly);
|
|
877
980
|
});
|
|
878
981
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the user for a notebook file on their computer; null if they cancel.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the plugin because it is browser-only: the tests replace it.
|
|
5
|
+
*/
|
|
6
|
+
export function chooseNotebookFile() {
|
|
7
|
+
return new Promise(resolve => {
|
|
8
|
+
const input = document.createElement('input');
|
|
9
|
+
input.type = 'file';
|
|
10
|
+
input.accept = '.ipynb,application/x-ipynb+json';
|
|
11
|
+
input.hidden = true;
|
|
12
|
+
// Attached while the picker is open: some browsers (Safari) drop the `change`
|
|
13
|
+
// event of a detached input, or collect the input before it fires.
|
|
14
|
+
document.body.appendChild(input);
|
|
15
|
+
const settle = (file) => {
|
|
16
|
+
input.remove();
|
|
17
|
+
resolve(file);
|
|
18
|
+
};
|
|
19
|
+
input.addEventListener('change', () => { var _a, _b; return settle((_b = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null); }, {
|
|
20
|
+
once: true
|
|
21
|
+
});
|
|
22
|
+
// Browsers fire `cancel` when the picker is dismissed; without it the promise
|
|
23
|
+
// would simply never settle, which is harmless.
|
|
24
|
+
input.addEventListener('cancel', () => settle(null), { once: true });
|
|
25
|
+
input.click();
|
|
26
|
+
});
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
"title": "Iterate",
|
|
30
30
|
"description": "After running, send each cell's output back to the AI so it can fix an error or take the next step. Only takes effect with Auto-run code on.",
|
|
31
31
|
"default": false
|
|
32
|
+
},
|
|
33
|
+
"showExamples": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"title": "Show examples",
|
|
36
|
+
"description": "List Oceanum's example notebooks (its Notebook Demo specs) in the Notebooks tab. Also switched from the Examples heading.",
|
|
37
|
+
"default": true
|
|
32
38
|
}
|
|
33
39
|
},
|
|
34
40
|
"patternProperties": {
|
package/src/DatameshWidget.tsx
CHANGED
|
@@ -555,6 +555,34 @@ function useAiAccess(
|
|
|
555
555
|
return access;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* The `showExamples` setting, read again whenever the settings change. True where the
|
|
560
|
+
* host passes no such setting; null while the host's setting has not loaded.
|
|
561
|
+
*/
|
|
562
|
+
function useShowExamples(
|
|
563
|
+
settingsChanged: ISignal<unknown, void>,
|
|
564
|
+
read: (() => boolean | null) | undefined
|
|
565
|
+
): boolean | null {
|
|
566
|
+
const [show, setShow] = React.useState<boolean | null>(() =>
|
|
567
|
+
read ? read() : true
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
React.useEffect(() => {
|
|
571
|
+
if (!read) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const update = () => setShow(read());
|
|
575
|
+
// Anything that changed between the first render and now.
|
|
576
|
+
update();
|
|
577
|
+
settingsChanged.connect(update);
|
|
578
|
+
return () => {
|
|
579
|
+
settingsChanged.disconnect(update);
|
|
580
|
+
};
|
|
581
|
+
}, [settingsChanged, read]);
|
|
582
|
+
|
|
583
|
+
return show;
|
|
584
|
+
}
|
|
585
|
+
|
|
558
586
|
/**
|
|
559
587
|
* Shows a message prompting the user to sign in to Oceanum.io, or to configure
|
|
560
588
|
* their Datamesh token where the site has no sign-in. Only renders when there
|
|
@@ -966,6 +994,17 @@ export interface IDatameshWidgetProps {
|
|
|
966
994
|
auth?: IOceanumAuth | null;
|
|
967
995
|
/** Open a stored notebook by spec store id. */
|
|
968
996
|
openStoredNotebook?: (id: string) => void;
|
|
997
|
+
/** Open a copy of an example by spec store id, named from its title. */
|
|
998
|
+
openExample?: (id: string, title: string) => void;
|
|
999
|
+
/** Upload a notebook from the user's computer. */
|
|
1000
|
+
uploadNotebook?: () => void;
|
|
1001
|
+
/**
|
|
1002
|
+
* The `showExamples` setting: whether the Notebooks tab lists the examples; null
|
|
1003
|
+
* until the settings have loaded.
|
|
1004
|
+
*/
|
|
1005
|
+
showExamples?: () => boolean | null;
|
|
1006
|
+
/** Save the `showExamples` setting, from the switch on the Examples heading. */
|
|
1007
|
+
setShowExamples?: (show: boolean) => void;
|
|
969
1008
|
}
|
|
970
1009
|
|
|
971
1010
|
/**
|
|
@@ -983,6 +1022,10 @@ function PanelTabs({
|
|
|
983
1022
|
settings,
|
|
984
1023
|
renderDatamesh,
|
|
985
1024
|
openStoredNotebook,
|
|
1025
|
+
openExample,
|
|
1026
|
+
uploadNotebook,
|
|
1027
|
+
showExamples,
|
|
1028
|
+
setShowExamples,
|
|
986
1029
|
selected,
|
|
987
1030
|
onSelect
|
|
988
1031
|
}: {
|
|
@@ -991,10 +1034,15 @@ function PanelTabs({
|
|
|
991
1034
|
settings: IAiSettings;
|
|
992
1035
|
renderDatamesh: () => React.ReactElement;
|
|
993
1036
|
openStoredNotebook?: (id: string) => void;
|
|
1037
|
+
openExample?: (id: string, title: string) => void;
|
|
1038
|
+
uploadNotebook?: () => void;
|
|
1039
|
+
showExamples?: () => boolean | null;
|
|
1040
|
+
setShowExamples?: (show: boolean) => void;
|
|
994
1041
|
selected: string;
|
|
995
1042
|
onSelect: (id: string) => void;
|
|
996
1043
|
}): React.ReactElement {
|
|
997
1044
|
const access = useAiAccess(commands, settings);
|
|
1045
|
+
const examplesShown = useShowExamples(settings.settingsChanged, showExamples);
|
|
998
1046
|
const tabs: ITab[] = [
|
|
999
1047
|
{
|
|
1000
1048
|
id: 'notebooks',
|
|
@@ -1006,6 +1054,12 @@ function PanelTabs({
|
|
|
1006
1054
|
<StoredNotebooks
|
|
1007
1055
|
auth={auth}
|
|
1008
1056
|
onOpen={openStoredNotebook && (item => openStoredNotebook(item.id))}
|
|
1057
|
+
onOpenExample={
|
|
1058
|
+
openExample && (item => openExample(item.id, item.title))
|
|
1059
|
+
}
|
|
1060
|
+
onUpload={uploadNotebook}
|
|
1061
|
+
showExamples={examplesShown}
|
|
1062
|
+
onShowExamplesChange={setShowExamples}
|
|
1009
1063
|
onSignIn={() => {
|
|
1010
1064
|
startSignIn(commands, auth).catch(error => {
|
|
1011
1065
|
console.warn('Oceanum.io sign-in could not start.', error);
|
|
@@ -1147,6 +1201,10 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
1147
1201
|
settings={this.props}
|
|
1148
1202
|
renderDatamesh={() => this.renderDatamesh()}
|
|
1149
1203
|
openStoredNotebook={this.props.openStoredNotebook}
|
|
1204
|
+
openExample={this.props.openExample}
|
|
1205
|
+
uploadNotebook={this.props.uploadNotebook}
|
|
1206
|
+
showExamples={this.props.showExamples}
|
|
1207
|
+
setShowExamples={this.props.setShowExamples}
|
|
1150
1208
|
selected={this.selectedTab}
|
|
1151
1209
|
onSelect={id => this.selectTab(id)}
|
|
1152
1210
|
/>
|
package/src/StoredNotebooks.tsx
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
import { IOceanumAuth } from './auth/tokens';
|
|
4
|
-
import { SpecStoreClient } from './share/client';
|
|
4
|
+
import { SpecStoreClient, SpecStoreError } from './share/client';
|
|
5
5
|
import { notebooksChanged } from './share/events';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
demoItemsFromSummaries,
|
|
8
|
+
INotebookDemoItem,
|
|
9
|
+
ISpecSummary,
|
|
10
|
+
NOTEBOOK_DEMO_TYPE,
|
|
11
|
+
partitionSummaries
|
|
12
|
+
} from './share/notebook';
|
|
7
13
|
import { SPEC_ID_ATTRIBUTE } from './share/plugin';
|
|
8
14
|
|
|
15
|
+
/** Example rows carry their record id here, not in SPEC_ID_ATTRIBUTE: see Examples. */
|
|
16
|
+
export const EXAMPLE_ID_ATTRIBUTE = 'data-example-id';
|
|
17
|
+
|
|
9
18
|
export interface IStoredNotebooksProps {
|
|
10
19
|
auth: IOceanumAuth;
|
|
11
20
|
/**
|
|
@@ -13,6 +22,22 @@ export interface IStoredNotebooksProps {
|
|
|
13
22
|
* the list then reads rather than offering a click that does nothing.
|
|
14
23
|
*/
|
|
15
24
|
onOpen?: (item: ISpecSummary) => void;
|
|
25
|
+
/** Open a copy of an example, as `onOpen` does for a stored notebook. */
|
|
26
|
+
onOpenExample?: (item: INotebookDemoItem) => void;
|
|
27
|
+
/** Upload a notebook from the user's computer; no Upload button without it. */
|
|
28
|
+
onUpload?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the examples are listed, or folded under their heading. Default true. Null
|
|
31
|
+
* while the setting is still loading: the Examples wait for it, so examples a user
|
|
32
|
+
* has hidden never show for a moment first.
|
|
33
|
+
*/
|
|
34
|
+
showExamples?: boolean | null;
|
|
35
|
+
/**
|
|
36
|
+
* Record the user's choice to show or hide the examples. The Examples heading offers
|
|
37
|
+
* its switch only where this is given: a switch that could not keep its setting
|
|
38
|
+
* would flip back on the next load.
|
|
39
|
+
*/
|
|
40
|
+
onShowExamplesChange?: (show: boolean) => void;
|
|
16
41
|
/**
|
|
17
42
|
* Start signing in the way this host shows it; without it the auth's own `signIn()`
|
|
18
43
|
* is used, which on a JupyterLab server shows no code dialog.
|
|
@@ -20,10 +45,150 @@ export interface IStoredNotebooksProps {
|
|
|
20
45
|
onSignIn?: () => void;
|
|
21
46
|
}
|
|
22
47
|
|
|
48
|
+
/** The Notebook Demo specs the user can read, which are the Examples. */
|
|
49
|
+
type Examples =
|
|
50
|
+
| { state: 'none' }
|
|
51
|
+
| { state: 'error'; message: string }
|
|
52
|
+
| { state: 'ready'; items: INotebookDemoItem[] };
|
|
53
|
+
|
|
23
54
|
type Load =
|
|
24
55
|
| { state: 'loading' }
|
|
25
56
|
| { state: 'error'; message: string }
|
|
26
|
-
| { state: 'ready'; items: ISpecSummary[] };
|
|
57
|
+
| { state: 'ready'; items: ISpecSummary[]; examples: Examples };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* List the Notebook Demo specs, each one an example notebook. Only org oceanum may
|
|
61
|
+
* create them (spec store 0.6.2), so every one listed is Oceanum's. Never rejects: a
|
|
62
|
+
* failure here is shown in the Examples section and leaves the rest of the tab alone.
|
|
63
|
+
*/
|
|
64
|
+
async function loadExamples(
|
|
65
|
+
auth: IOceanumAuth,
|
|
66
|
+
specsUrl: string
|
|
67
|
+
): Promise<Examples> {
|
|
68
|
+
try {
|
|
69
|
+
const client = new SpecStoreClient({
|
|
70
|
+
specsUrl,
|
|
71
|
+
getAccessToken: () => auth.getAccessToken(),
|
|
72
|
+
specType: NOTEBOOK_DEMO_TYPE
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
state: 'ready',
|
|
76
|
+
items: demoItemsFromSummaries(await client.list())
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// A store with no Notebook Demo type has no examples, which is not an error.
|
|
80
|
+
if (error instanceof SpecStoreError && error.kind === 'not-found') {
|
|
81
|
+
return { state: 'none' };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
state: 'error',
|
|
85
|
+
message:
|
|
86
|
+
error instanceof Error ? error.message : 'Could not reach Oceanum.io.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The examples: Oceanum's Notebook Demo specs. Their rows deliberately leave out
|
|
93
|
+
* SPEC_ID_ATTRIBUTE and the stored-notebook row class: the share plugin's row menu
|
|
94
|
+
* (Open, Rename, Share, Delete) is bound to those, and none of it applies to
|
|
95
|
+
* Oceanum's own example records.
|
|
96
|
+
*/
|
|
97
|
+
function ExampleSections({
|
|
98
|
+
examples,
|
|
99
|
+
onOpen,
|
|
100
|
+
show,
|
|
101
|
+
onShowChange
|
|
102
|
+
}: {
|
|
103
|
+
examples: Examples;
|
|
104
|
+
onOpen?: (item: INotebookDemoItem) => void;
|
|
105
|
+
show: boolean | null;
|
|
106
|
+
onShowChange?: (show: boolean) => void;
|
|
107
|
+
}): React.ReactElement | null {
|
|
108
|
+
// Not yet known, or hidden with no switch to bring them back: nothing to show.
|
|
109
|
+
if (examples.state === 'none' || show === null || (!show && !onShowChange)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const count = examples.state === 'ready' ? examples.items.length : null;
|
|
113
|
+
if (count === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const heading = (
|
|
117
|
+
<div className="oceanum-notebooks-heading">
|
|
118
|
+
Examples
|
|
119
|
+
<span className="oceanum-notebooks-heading-end">
|
|
120
|
+
{count !== null && (
|
|
121
|
+
<span className="oceanum-notebooks-count">{count}</span>
|
|
122
|
+
)}
|
|
123
|
+
{onShowChange && (
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
role="switch"
|
|
127
|
+
aria-checked={show}
|
|
128
|
+
aria-label="Show examples"
|
|
129
|
+
title="Show examples"
|
|
130
|
+
className="oceanum-notebooks-switch"
|
|
131
|
+
onClick={() => onShowChange(!show)}
|
|
132
|
+
>
|
|
133
|
+
<span className="oceanum-notebooks-switch-thumb" />
|
|
134
|
+
</button>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
// Hidden: the heading stays, so the switch is there to bring them back.
|
|
140
|
+
if (!show) {
|
|
141
|
+
return <div className="oceanum-notebooks-section">{heading}</div>;
|
|
142
|
+
}
|
|
143
|
+
if (examples.state === 'error') {
|
|
144
|
+
return (
|
|
145
|
+
<div className="oceanum-notebooks-section">
|
|
146
|
+
{heading}
|
|
147
|
+
<div className="oceanum-text-error">
|
|
148
|
+
The examples could not be loaded. {examples.message}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return (
|
|
154
|
+
<div className="oceanum-notebooks-section">
|
|
155
|
+
{heading}
|
|
156
|
+
<ul className="oceanum-notebooks-list">
|
|
157
|
+
{examples.items.map(item => {
|
|
158
|
+
const attributes = {
|
|
159
|
+
[EXAMPLE_ID_ATTRIBUTE]: item.id,
|
|
160
|
+
title: item.summary
|
|
161
|
+
};
|
|
162
|
+
// Names come from the spec store: React escapes them, never set as HTML.
|
|
163
|
+
const name = (
|
|
164
|
+
<span className="oceanum-notebooks-name">{item.title}</span>
|
|
165
|
+
);
|
|
166
|
+
return (
|
|
167
|
+
<li key={item.id}>
|
|
168
|
+
{onOpen ? (
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="oceanum-notebooks-example"
|
|
172
|
+
onClick={() => onOpen(item)}
|
|
173
|
+
{...attributes}
|
|
174
|
+
>
|
|
175
|
+
{name}
|
|
176
|
+
</button>
|
|
177
|
+
) : (
|
|
178
|
+
<div
|
|
179
|
+
className="oceanum-notebooks-example oceanum-notebooks-item-static"
|
|
180
|
+
{...attributes}
|
|
181
|
+
>
|
|
182
|
+
{name}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</li>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</ul>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
27
192
|
|
|
28
193
|
function Section({
|
|
29
194
|
title,
|
|
@@ -90,6 +255,10 @@ function Section({
|
|
|
90
255
|
export function StoredNotebooks({
|
|
91
256
|
auth,
|
|
92
257
|
onOpen,
|
|
258
|
+
onOpenExample,
|
|
259
|
+
onUpload,
|
|
260
|
+
showExamples = true,
|
|
261
|
+
onShowExamplesChange,
|
|
93
262
|
onSignIn
|
|
94
263
|
}: IStoredNotebooksProps): React.ReactElement {
|
|
95
264
|
const [load, setLoad] = React.useState<Load>({ state: 'loading' });
|
|
@@ -115,7 +284,7 @@ export function StoredNotebooks({
|
|
|
115
284
|
let cancelled = false;
|
|
116
285
|
const specs = auth.urls?.specs ?? null;
|
|
117
286
|
if (!user || !specs) {
|
|
118
|
-
setLoad({ state: 'ready', items: [] });
|
|
287
|
+
setLoad({ state: 'ready', items: [], examples: { state: 'none' } });
|
|
119
288
|
return;
|
|
120
289
|
}
|
|
121
290
|
setLoad({ state: 'loading' });
|
|
@@ -125,9 +294,12 @@ export function StoredNotebooks({
|
|
|
125
294
|
specsUrl: specs,
|
|
126
295
|
getAccessToken: () => auth.getAccessToken()
|
|
127
296
|
});
|
|
128
|
-
const items = await
|
|
297
|
+
const [items, examples] = await Promise.all([
|
|
298
|
+
client.list(),
|
|
299
|
+
loadExamples(auth, specs)
|
|
300
|
+
]);
|
|
129
301
|
if (!cancelled) {
|
|
130
|
-
setLoad({ state: 'ready', items });
|
|
302
|
+
setLoad({ state: 'ready', items, examples });
|
|
131
303
|
}
|
|
132
304
|
} catch (error) {
|
|
133
305
|
if (!cancelled) {
|
|
@@ -189,6 +361,19 @@ export function StoredNotebooks({
|
|
|
189
361
|
const { mine, shared } = partitionSummaries(load.items, user.email);
|
|
190
362
|
return (
|
|
191
363
|
<div className="oceanum-notebooks">
|
|
364
|
+
{/* Without a spec store the upload command does nothing. */}
|
|
365
|
+
{onUpload && auth.urls?.specs ? (
|
|
366
|
+
<div className="oceanum-notebooks-actions">
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
className="oceanum-notebooks-upload"
|
|
370
|
+
title="Upload a notebook from this computer, to save on Oceanum.io"
|
|
371
|
+
onClick={onUpload}
|
|
372
|
+
>
|
|
373
|
+
Upload notebook…
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
) : null}
|
|
192
377
|
<Section
|
|
193
378
|
title="My notebooks"
|
|
194
379
|
items={mine}
|
|
@@ -201,6 +386,12 @@ export function StoredNotebooks({
|
|
|
201
386
|
empty="No notebooks have been shared with you."
|
|
202
387
|
onOpen={onOpen}
|
|
203
388
|
/>
|
|
389
|
+
<ExampleSections
|
|
390
|
+
examples={load.examples}
|
|
391
|
+
onOpen={onOpenExample}
|
|
392
|
+
show={showExamples}
|
|
393
|
+
onShowChange={onShowExamplesChange}
|
|
394
|
+
/>
|
|
204
395
|
</div>
|
|
205
396
|
);
|
|
206
397
|
}
|