@oceanum/oceanumlab 4.7.0 → 4.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -11
- package/lib/DatameshUI.d.ts +1 -6
- package/lib/DatameshUI.js +1 -9
- package/lib/DatameshWidget.d.ts +18 -2
- package/lib/DatameshWidget.js +80 -12
- package/lib/StoredNotebooks.d.ts +20 -0
- package/lib/StoredNotebooks.js +112 -0
- package/lib/Tabs.d.ts +26 -0
- package/lib/Tabs.js +45 -0
- package/lib/aiBackend.d.ts +1 -1
- package/lib/aiBackend.js +1 -1
- package/lib/auth/accountWidget.d.ts +35 -0
- package/lib/auth/accountWidget.js +95 -0
- package/lib/auth/claims.d.ts +3 -0
- package/lib/auth/claims.js +25 -0
- package/lib/auth/plugin.d.ts +9 -0
- package/lib/auth/plugin.js +126 -0
- package/lib/auth/serverAuth.d.ts +79 -0
- package/lib/auth/serverAuth.js +254 -0
- package/lib/auth/startSignIn.d.ts +11 -0
- package/lib/auth/startSignIn.js +16 -0
- package/lib/auth/tokens.d.ts +102 -0
- package/lib/auth/tokens.js +9 -0
- package/lib/chatRouter.d.ts +13 -1
- package/lib/chatRouter.js +9 -2
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +2 -2
- package/lib/datameshUiUrl.js +6 -6
- package/lib/index.d.ts +8 -1
- package/lib/index.js +58 -17
- package/lib/share/client.d.ts +84 -0
- package/lib/share/client.js +205 -0
- package/lib/share/events.d.ts +7 -0
- package/lib/share/events.js +9 -0
- package/lib/share/notebook.d.ts +184 -0
- package/lib/share/notebook.js +277 -0
- package/lib/share/oceanumOnly.d.ts +49 -0
- package/lib/share/oceanumOnly.js +90 -0
- package/lib/share/plugin.d.ts +21 -0
- package/lib/share/plugin.js +883 -0
- package/lib/share/widgets.d.ts +25 -0
- package/lib/share/widgets.js +154 -0
- package/package.json +13 -1
- package/schema/datamesh-connect.json +3 -12
- package/src/DatameshUI.tsx +1 -10
- package/src/DatameshWidget.tsx +143 -19
- package/src/StoredNotebooks.tsx +206 -0
- package/src/Tabs.tsx +102 -0
- package/src/__tests__/DatameshWidget.spec.tsx +33 -20
- package/src/__tests__/OceanumPanel.spec.tsx +157 -0
- package/src/__tests__/StoredNotebooks.spec.tsx +157 -0
- package/src/__tests__/Tabs.spec.tsx +168 -0
- package/src/__tests__/aiBackend.spec.ts +5 -6
- package/src/__tests__/chatRouter.spec.ts +9 -15
- package/src/__tests__/datameshUiUrl.spec.ts +4 -3
- package/src/__tests__/oceanumOnly.spec.ts +193 -0
- package/src/__tests__/pluginArity.spec.ts +24 -0
- package/src/__tests__/serverAuth.spec.ts +277 -0
- package/src/__tests__/settingsSchema.spec.ts +73 -0
- package/src/__tests__/shareClient.spec.ts +314 -0
- package/src/__tests__/shareNotebook.spec.ts +451 -0
- package/src/__tests__/sharePlugin.spec.ts +426 -0
- package/src/aiBackend.ts +1 -1
- package/src/auth/accountWidget.ts +113 -0
- package/src/auth/claims.ts +30 -0
- package/src/auth/plugin.ts +155 -0
- package/src/auth/serverAuth.ts +342 -0
- package/src/auth/startSignIn.ts +23 -0
- package/src/auth/tokens.ts +111 -0
- package/src/chatRouter.ts +8 -4
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +6 -6
- package/src/index.ts +87 -36
- package/src/share/client.ts +288 -0
- package/src/share/events.ts +10 -0
- package/src/share/notebook.ts +416 -0
- package/src/share/oceanumOnly.ts +126 -0
- package/src/share/plugin.ts +1096 -0
- package/src/share/widgets.ts +217 -0
- package/style/index.css +266 -13
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Dialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { Widget } from '@lumino/widgets';
|
|
3
|
+
import type { ShareChoice } from './client';
|
|
4
|
+
import { ISpecSummary } from './notebook';
|
|
5
|
+
/** Dialog body listing notebooks to open; its value is the selected record id. */
|
|
6
|
+
export declare class NotebookListBody extends Widget implements Dialog.IBodyWidget<string | null> {
|
|
7
|
+
constructor(mine: readonly ISpecSummary[], shared: readonly ISpecSummary[]);
|
|
8
|
+
getValue(): string | null;
|
|
9
|
+
private _addSection;
|
|
10
|
+
private _select;
|
|
11
|
+
private _selected;
|
|
12
|
+
}
|
|
13
|
+
export declare const EVERYONE_WARNING: string;
|
|
14
|
+
/** Dialog body for sharing; its value is the change to make, or `null` if incomplete. */
|
|
15
|
+
export declare class ShareBody extends Widget implements Dialog.IBodyWidget<ShareChoice | null> {
|
|
16
|
+
constructor(link: string);
|
|
17
|
+
getValue(): ShareChoice | null;
|
|
18
|
+
private _radio;
|
|
19
|
+
private static _count;
|
|
20
|
+
private readonly _person;
|
|
21
|
+
private readonly _everyone;
|
|
22
|
+
private readonly _stopEveryone;
|
|
23
|
+
private readonly _email;
|
|
24
|
+
private readonly _level;
|
|
25
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Clipboard } from '@jupyterlab/apputils';
|
|
2
|
+
import { Widget } from '@lumino/widgets';
|
|
3
|
+
import { parseShareEmails, parseTimestamp } from './notebook';
|
|
4
|
+
function element(tag, className, text) {
|
|
5
|
+
const node = document.createElement(tag);
|
|
6
|
+
node.className = className;
|
|
7
|
+
if (text !== undefined) {
|
|
8
|
+
// Names and emails come from other users: always set text, never HTML.
|
|
9
|
+
node.textContent = text;
|
|
10
|
+
}
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
/** Dialog body listing notebooks to open; its value is the selected record id. */
|
|
14
|
+
export class NotebookListBody extends Widget {
|
|
15
|
+
constructor(mine, shared) {
|
|
16
|
+
super();
|
|
17
|
+
this._selected = null;
|
|
18
|
+
this.addClass('jp-OceanumShare-body');
|
|
19
|
+
this._addSection('My notebooks', mine, false);
|
|
20
|
+
this._addSection('Shared with me', shared, true);
|
|
21
|
+
}
|
|
22
|
+
getValue() {
|
|
23
|
+
return this._selected;
|
|
24
|
+
}
|
|
25
|
+
_addSection(title, items, showCreator) {
|
|
26
|
+
this.node.appendChild(element('h3', 'jp-OceanumShare-heading', title));
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
this.node.appendChild(element('p', 'jp-OceanumShare-empty', 'None'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const list = element('ul', 'jp-OceanumShare-list');
|
|
32
|
+
list.setAttribute('role', 'listbox');
|
|
33
|
+
list.setAttribute('aria-label', title);
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const option = element('li', 'jp-OceanumShare-item');
|
|
36
|
+
option.setAttribute('role', 'option');
|
|
37
|
+
option.setAttribute('aria-selected', 'false');
|
|
38
|
+
option.tabIndex = 0;
|
|
39
|
+
option.appendChild(element('span', 'jp-OceanumShare-name', item.name));
|
|
40
|
+
const modified = parseTimestamp(item.modified);
|
|
41
|
+
const details = [
|
|
42
|
+
modified ? `Modified ${modified.toLocaleDateString()}` : '',
|
|
43
|
+
showCreator && item.creator ? item.creator : ''
|
|
44
|
+
].filter(Boolean);
|
|
45
|
+
option.appendChild(element('span', 'jp-OceanumShare-meta', details.join(' · ')));
|
|
46
|
+
const select = () => this._select(item.id, option);
|
|
47
|
+
option.addEventListener('click', select);
|
|
48
|
+
option.addEventListener('focus', select);
|
|
49
|
+
list.appendChild(option);
|
|
50
|
+
}
|
|
51
|
+
this.node.appendChild(list);
|
|
52
|
+
}
|
|
53
|
+
_select(id, option) {
|
|
54
|
+
this._selected = id;
|
|
55
|
+
for (const node of this.node.querySelectorAll('.jp-OceanumShare-item')) {
|
|
56
|
+
const selected = node === option;
|
|
57
|
+
node.classList.toggle('jp-mod-selected', selected);
|
|
58
|
+
node.setAttribute('aria-selected', String(selected));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const EVERYONE_WARNING = 'Anyone with the link can read this notebook, including people without an Oceanum ' +
|
|
63
|
+
'account. This cannot be limited to your organisation. Its name and your email ' +
|
|
64
|
+
'address also become visible to anyone who lists Oceanum notebooks.';
|
|
65
|
+
const PUBLIC_GRANT = {
|
|
66
|
+
type: 'public',
|
|
67
|
+
entity: '',
|
|
68
|
+
permission: 'read'
|
|
69
|
+
};
|
|
70
|
+
/** Dialog body for sharing; its value is the change to make, or `null` if incomplete. */
|
|
71
|
+
export class ShareBody extends Widget {
|
|
72
|
+
constructor(link) {
|
|
73
|
+
super();
|
|
74
|
+
this.addClass('jp-OceanumShare-body');
|
|
75
|
+
this.node.appendChild(element('h3', 'jp-OceanumShare-heading', 'Link'));
|
|
76
|
+
const row = element('div', 'jp-OceanumShare-linkRow');
|
|
77
|
+
const linkInput = element('input', 'jp-OceanumShare-link');
|
|
78
|
+
linkInput.readOnly = true;
|
|
79
|
+
linkInput.value = link;
|
|
80
|
+
linkInput.setAttribute('aria-label', 'Notebook link');
|
|
81
|
+
const copy = element('button', 'jp-mod-styled jp-OceanumShare-copy', 'Copy link');
|
|
82
|
+
copy.type = 'button';
|
|
83
|
+
copy.addEventListener('click', () => {
|
|
84
|
+
Clipboard.copyToSystem(link);
|
|
85
|
+
copy.textContent = 'Copied';
|
|
86
|
+
});
|
|
87
|
+
row.append(linkInput, copy);
|
|
88
|
+
this.node.appendChild(row);
|
|
89
|
+
this.node.appendChild(element('p', 'jp-OceanumShare-hint', 'The link opens the notebook for anyone it is shared with.'));
|
|
90
|
+
this.node.appendChild(element('h3', 'jp-OceanumShare-heading', 'Share with'));
|
|
91
|
+
const name = `jp-OceanumShare-who-${++ShareBody._count}`;
|
|
92
|
+
this._person = this._radio(name, 'A specific person', true);
|
|
93
|
+
const personRow = element('div', 'jp-OceanumShare-personRow');
|
|
94
|
+
this._email = element('textarea', 'jp-OceanumShare-email');
|
|
95
|
+
this._email.rows = 2;
|
|
96
|
+
this._email.placeholder = 'name@example.com, another@example.com';
|
|
97
|
+
this._email.setAttribute('aria-label', 'Email addresses');
|
|
98
|
+
this._level = element('select', 'jp-OceanumShare-level');
|
|
99
|
+
this._level.setAttribute('aria-label', 'Access');
|
|
100
|
+
for (const [value, label] of [
|
|
101
|
+
['read', 'can view'],
|
|
102
|
+
['write', 'can edit']
|
|
103
|
+
]) {
|
|
104
|
+
const option = element('option', '', label);
|
|
105
|
+
option.value = value;
|
|
106
|
+
this._level.appendChild(option);
|
|
107
|
+
}
|
|
108
|
+
personRow.append(this._email, this._level);
|
|
109
|
+
this.node.appendChild(personRow);
|
|
110
|
+
this.node.appendChild(element('p', 'jp-OceanumShare-hint',
|
|
111
|
+
// Not "new lines": JupyterLab's dialog swallows Enter before it reaches the
|
|
112
|
+
// textarea, so one cannot be typed here. Pasted ones still separate addresses.
|
|
113
|
+
'Separate several addresses with commas or spaces. Everyone you add gets the access chosen here.'));
|
|
114
|
+
this._everyone = this._radio(name, 'Everyone with the link', false);
|
|
115
|
+
this.node.appendChild(element('p', 'jp-OceanumShare-warning', EVERYONE_WARNING));
|
|
116
|
+
this._stopEveryone = this._radio(name, 'Stop sharing with everyone', false);
|
|
117
|
+
const update = () => {
|
|
118
|
+
this._email.disabled = !this._person.checked;
|
|
119
|
+
this._level.disabled = !this._person.checked;
|
|
120
|
+
};
|
|
121
|
+
for (const radio of [this._person, this._everyone, this._stopEveryone]) {
|
|
122
|
+
radio.addEventListener('change', update);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
getValue() {
|
|
126
|
+
if (this._everyone.checked) {
|
|
127
|
+
return { action: 'grant', grants: [PUBLIC_GRANT], invalid: [] };
|
|
128
|
+
}
|
|
129
|
+
if (this._stopEveryone.checked) {
|
|
130
|
+
return { action: 'revoke', grant: PUBLIC_GRANT };
|
|
131
|
+
}
|
|
132
|
+
const { emails, invalid } = parseShareEmails(this._email.value);
|
|
133
|
+
if (emails.length === 0 && invalid.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const permission = this._level.value === 'write' ? 'write' : 'read';
|
|
137
|
+
return {
|
|
138
|
+
action: 'grant',
|
|
139
|
+
grants: emails.map(entity => ({ type: 'user', entity, permission })),
|
|
140
|
+
invalid
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
_radio(name, text, checked) {
|
|
144
|
+
const label = element('label', 'jp-OceanumShare-choice');
|
|
145
|
+
const input = element('input', 'jp-OceanumShare-radio');
|
|
146
|
+
input.type = 'radio';
|
|
147
|
+
input.name = name;
|
|
148
|
+
input.checked = checked;
|
|
149
|
+
label.append(input, document.createTextNode(` ${text}`));
|
|
150
|
+
this.node.appendChild(label);
|
|
151
|
+
return input;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
ShareBody._count = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oceanum/oceanumlab",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.2",
|
|
4
4
|
"description": "A Jupyterlab extension to interact with the Oceanum.io platform",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -60,8 +60,20 @@
|
|
|
60
60
|
"@jupyterlab/application": "^4.6.3",
|
|
61
61
|
"@jupyterlab/apputils": "^4.7.3",
|
|
62
62
|
"@jupyterlab/cells": "^4.6.3",
|
|
63
|
+
"@jupyterlab/console": "^4.6.3",
|
|
64
|
+
"@jupyterlab/coreutils": "^6.6.3",
|
|
65
|
+
"@jupyterlab/docmanager": "^4.6.3",
|
|
66
|
+
"@jupyterlab/mainmenu": "^4.6.3",
|
|
67
|
+
"@jupyterlab/nbformat": "^4.6.3",
|
|
63
68
|
"@jupyterlab/notebook": "^4.6.3",
|
|
69
|
+
"@jupyterlab/services": "^7.6.3",
|
|
64
70
|
"@jupyterlab/settingregistry": "^4.6.3",
|
|
71
|
+
"@lumino/commands": "^2.3.3",
|
|
72
|
+
"@lumino/coreutils": "^2.2.2",
|
|
73
|
+
"@lumino/disposable": "^2.1.5",
|
|
74
|
+
"@lumino/messaging": "^2.0.3",
|
|
75
|
+
"@lumino/signaling": "^2.1.5",
|
|
76
|
+
"@lumino/widgets": "^2.8.0",
|
|
65
77
|
"@phosphor-icons/react": "^2.1.10",
|
|
66
78
|
"marked": "^18.0.13"
|
|
67
79
|
},
|
|
@@ -18,18 +18,6 @@
|
|
|
18
18
|
"description": "Inject Datamesh token into code.",
|
|
19
19
|
"default": false
|
|
20
20
|
},
|
|
21
|
-
"datameshUiUrl": {
|
|
22
|
-
"type": "string",
|
|
23
|
-
"title": "Datamesh UI URL",
|
|
24
|
-
"description": "Address of the Datamesh UI shown in the Datamesh panel. Use the address it is finally served from: the panel only accepts workspace updates from this address's origin, so an address that redirects to another origin will not update the workspace.",
|
|
25
|
-
"default": "https://ui.datamesh.oceanum.io"
|
|
26
|
-
},
|
|
27
|
-
"aiBackendUrl": {
|
|
28
|
-
"type": "string",
|
|
29
|
-
"title": "Oceanum AI URL",
|
|
30
|
-
"description": "Address of the Oceanum AI backend used by the AI chat.",
|
|
31
|
-
"default": "https://ai.oceanum.io"
|
|
32
|
-
},
|
|
33
21
|
"autoRunCode": {
|
|
34
22
|
"type": "boolean",
|
|
35
23
|
"title": "Auto-run code",
|
|
@@ -43,5 +31,8 @@
|
|
|
43
31
|
"default": false
|
|
44
32
|
}
|
|
45
33
|
},
|
|
34
|
+
"patternProperties": {
|
|
35
|
+
"^(datameshUiUrl|aiBackendUrl)$": {}
|
|
36
|
+
},
|
|
46
37
|
"additionalProperties": false
|
|
47
38
|
}
|
package/src/DatameshUI.tsx
CHANGED
|
@@ -3,20 +3,11 @@ import { ReactWidget } from '@jupyterlab/apputils';
|
|
|
3
3
|
import { datameshUiSrc } from './datameshUiUrl';
|
|
4
4
|
|
|
5
5
|
export class DatameshUI extends ReactWidget {
|
|
6
|
-
/** `url` is the
|
|
6
|
+
/** `url` is the deployment's Datamesh UI address (the sign-in environment's). */
|
|
7
7
|
constructor(private _url: string) {
|
|
8
8
|
super();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Point the panel at the `datameshUiUrl` setting's new value. The iframe
|
|
13
|
-
* reloads only if its address actually changes.
|
|
14
|
-
*/
|
|
15
|
-
set url(url: string) {
|
|
16
|
-
this._url = url;
|
|
17
|
-
this.update();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
11
|
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
21
12
|
get frame(): Window | null {
|
|
22
13
|
return this.node.querySelector('iframe')?.contentWindow ?? null;
|
package/src/DatameshWidget.tsx
CHANGED
|
@@ -21,11 +21,16 @@ import React from 'react';
|
|
|
21
21
|
import { marked } from 'marked';
|
|
22
22
|
|
|
23
23
|
import { DatasourceItem } from './DatasourceItem';
|
|
24
|
+
import { IOceanumAuth } from './auth/tokens';
|
|
25
|
+
import { startSignIn } from './auth/startSignIn';
|
|
26
|
+
import { StoredNotebooks } from './StoredNotebooks';
|
|
27
|
+
import { ITab, Tabs } from './Tabs';
|
|
24
28
|
import {
|
|
25
29
|
SIGN_IN_COMMAND,
|
|
26
30
|
aiBackendUrl,
|
|
27
31
|
aiCredentialSource,
|
|
28
32
|
canSignIn,
|
|
33
|
+
isSignedIn,
|
|
29
34
|
pastedToken,
|
|
30
35
|
resolveAiCredential,
|
|
31
36
|
signInToken,
|
|
@@ -476,7 +481,7 @@ interface IAiSettings {
|
|
|
476
481
|
* Not `window.datameshToken`, which keeps a token after it is cleared.
|
|
477
482
|
*/
|
|
478
483
|
datameshToken: () => string;
|
|
479
|
-
/** The
|
|
484
|
+
/** The deployment's Oceanum AI address. */
|
|
480
485
|
aiBackendUrl: () => string;
|
|
481
486
|
/** Emitted when the settings change. */
|
|
482
487
|
settingsChanged: ISignal<unknown, void>;
|
|
@@ -493,7 +498,9 @@ interface IAiAccess {
|
|
|
493
498
|
pasted: string;
|
|
494
499
|
/** Whether the host can sign the user in to Oceanum.io on this site. */
|
|
495
500
|
canSignIn: boolean;
|
|
496
|
-
/**
|
|
501
|
+
/** Whether the host says the user is signed in to Oceanum.io. */
|
|
502
|
+
signedIn: boolean;
|
|
503
|
+
/** The backend address: the deployment's Oceanum AI. */
|
|
497
504
|
backend: string;
|
|
498
505
|
}
|
|
499
506
|
|
|
@@ -506,6 +513,7 @@ function readAiAccess(
|
|
|
506
513
|
source: aiCredentialSource(datameshToken, commands),
|
|
507
514
|
pasted: pastedToken(datameshToken),
|
|
508
515
|
canSignIn: canSignIn(commands),
|
|
516
|
+
signedIn: isSignedIn(commands),
|
|
509
517
|
backend: aiBackendUrl(settings.aiBackendUrl())
|
|
510
518
|
};
|
|
511
519
|
}
|
|
@@ -583,8 +591,15 @@ function TokenConfigMessage({
|
|
|
583
591
|
|
|
584
592
|
return (
|
|
585
593
|
<div className="oceanum-token-config">
|
|
586
|
-
Set your
|
|
587
|
-
|
|
594
|
+
Set your{' '}
|
|
595
|
+
<a
|
|
596
|
+
onClick={() =>
|
|
597
|
+
commands.execute('settingeditor:open', { query: 'Oceanum' })
|
|
598
|
+
}
|
|
599
|
+
>
|
|
600
|
+
Datamesh token
|
|
601
|
+
</a>{' '}
|
|
602
|
+
to enable Oceanum.io services{' '}
|
|
588
603
|
<a
|
|
589
604
|
href="https://home.oceanum.io/account"
|
|
590
605
|
target="_blank"
|
|
@@ -935,18 +950,87 @@ export interface IDatameshWidgetProps {
|
|
|
935
950
|
name: string;
|
|
936
951
|
icon: LabIcon;
|
|
937
952
|
openDatameshUI: any;
|
|
938
|
-
/** The
|
|
953
|
+
/** The deployment's Datamesh UI address. */
|
|
939
954
|
datameshUiUrl: () => string;
|
|
940
955
|
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
941
956
|
datameshUiFrame: () => Window | null;
|
|
942
957
|
/** The `datameshToken` setting. */
|
|
943
958
|
datameshToken: () => string;
|
|
944
|
-
/** The
|
|
959
|
+
/** The deployment's Oceanum AI address. */
|
|
945
960
|
aiBackendUrl: () => string;
|
|
946
961
|
/** Emitted when the settings change. */
|
|
947
962
|
settingsChanged: ISignal<unknown, void>;
|
|
948
963
|
commands: CommandRegistry;
|
|
949
964
|
getCurrentWidget: () => Widget;
|
|
965
|
+
/** Oceanum.io sign-in, or null on a host with no Oceanum environment. */
|
|
966
|
+
auth?: IOceanumAuth | null;
|
|
967
|
+
/** Open a stored notebook by spec store id. */
|
|
968
|
+
openStoredNotebook?: (id: string) => void;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* The panel's tabs.
|
|
973
|
+
*
|
|
974
|
+
* A component rather than a list built in `render`, because whether Oceanum AI is
|
|
975
|
+
* reachable is not static: `useAiAccess` re-reads it when the settings change and when
|
|
976
|
+
* the host signals a sign-in or sign-out. An AI tab is only offered when the chat has a
|
|
977
|
+
* credential to work with — otherwise it would open on the same nothing the panel
|
|
978
|
+
* rendered before there were tabs, except now with a label promising otherwise.
|
|
979
|
+
*/
|
|
980
|
+
function PanelTabs({
|
|
981
|
+
auth,
|
|
982
|
+
commands,
|
|
983
|
+
settings,
|
|
984
|
+
renderDatamesh,
|
|
985
|
+
openStoredNotebook,
|
|
986
|
+
selected,
|
|
987
|
+
onSelect
|
|
988
|
+
}: {
|
|
989
|
+
auth: IOceanumAuth | null;
|
|
990
|
+
commands: CommandRegistry;
|
|
991
|
+
settings: IAiSettings;
|
|
992
|
+
renderDatamesh: () => React.ReactElement;
|
|
993
|
+
openStoredNotebook?: (id: string) => void;
|
|
994
|
+
selected: string;
|
|
995
|
+
onSelect: (id: string) => void;
|
|
996
|
+
}): React.ReactElement {
|
|
997
|
+
const access = useAiAccess(commands, settings);
|
|
998
|
+
const tabs: ITab[] = [
|
|
999
|
+
{
|
|
1000
|
+
id: 'notebooks',
|
|
1001
|
+
label: 'Notebooks',
|
|
1002
|
+
render: () =>
|
|
1003
|
+
// An auth with no environment is a provider that has nothing to sign in to (the
|
|
1004
|
+
// server has sign-in turned off). Asking the user to sign in would be a dead end.
|
|
1005
|
+
auth?.environment ? (
|
|
1006
|
+
<StoredNotebooks
|
|
1007
|
+
auth={auth}
|
|
1008
|
+
onOpen={openStoredNotebook && (item => openStoredNotebook(item.id))}
|
|
1009
|
+
onSignIn={() => {
|
|
1010
|
+
startSignIn(commands, auth).catch(error => {
|
|
1011
|
+
console.warn('Oceanum.io sign-in could not start.', error);
|
|
1012
|
+
});
|
|
1013
|
+
}}
|
|
1014
|
+
/>
|
|
1015
|
+
) : (
|
|
1016
|
+
<div className="oceanum-text-empty">
|
|
1017
|
+
Oceanum.io sign-in is not configured for this host.
|
|
1018
|
+
</div>
|
|
1019
|
+
)
|
|
1020
|
+
},
|
|
1021
|
+
{ id: 'datamesh', label: 'Datamesh', render: renderDatamesh }
|
|
1022
|
+
];
|
|
1023
|
+
// Signed in, not merely holding a credential: a Datamesh token pasted into the
|
|
1024
|
+
// settings would reach the backend, but the tab stays out of the way until someone
|
|
1025
|
+
// has signed in to Oceanum.io.
|
|
1026
|
+
if (access.signedIn) {
|
|
1027
|
+
tabs.push({
|
|
1028
|
+
id: 'ai',
|
|
1029
|
+
label: 'Oceanum AI',
|
|
1030
|
+
render: () => <AIChatPanel commands={commands} settings={settings} />
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
return <Tabs tabs={tabs} selected={selected} onSelect={onSelect} />;
|
|
950
1034
|
}
|
|
951
1035
|
|
|
952
1036
|
/**
|
|
@@ -1014,18 +1098,10 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
1014
1098
|
);
|
|
1015
1099
|
}
|
|
1016
1100
|
|
|
1017
|
-
|
|
1101
|
+
/** The Datamesh workspace tab: what this panel showed above the divider. */
|
|
1102
|
+
renderDatamesh(): React.ReactElement {
|
|
1018
1103
|
return (
|
|
1019
|
-
|
|
1020
|
-
<header className="oceanum-sidebar-header">
|
|
1021
|
-
<this.props.icon.react
|
|
1022
|
-
tag="span"
|
|
1023
|
-
width="auto"
|
|
1024
|
-
height="20px"
|
|
1025
|
-
verticalAlign="middle"
|
|
1026
|
-
/>
|
|
1027
|
-
<span className="oceanum-sidebar-title">Oceanum.io</span>
|
|
1028
|
-
</header>
|
|
1104
|
+
<>
|
|
1029
1105
|
<div className="datamesh-workspace-header">
|
|
1030
1106
|
<span>Datamesh Workspace</span>
|
|
1031
1107
|
<div
|
|
@@ -1047,9 +1123,57 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
1047
1123
|
settings={this.props}
|
|
1048
1124
|
/>
|
|
1049
1125
|
</div>
|
|
1050
|
-
|
|
1051
|
-
|
|
1126
|
+
</>
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
render(): React.ReactElement {
|
|
1131
|
+
return (
|
|
1132
|
+
<div className="datamesh-connect">
|
|
1133
|
+
<header className="oceanum-sidebar-header">
|
|
1134
|
+
<this.props.icon.react
|
|
1135
|
+
tag="span"
|
|
1136
|
+
width="auto"
|
|
1137
|
+
height="20px"
|
|
1138
|
+
verticalAlign="middle"
|
|
1139
|
+
/>
|
|
1140
|
+
<span className="oceanum-sidebar-title">Oceanum.io</span>
|
|
1141
|
+
</header>
|
|
1142
|
+
<UseSignal signal={this.tabChanged} initialArgs={this.selectedTab}>
|
|
1143
|
+
{(): React.ReactElement => (
|
|
1144
|
+
<PanelTabs
|
|
1145
|
+
auth={this.props.auth ?? null}
|
|
1146
|
+
commands={this.props.commands}
|
|
1147
|
+
settings={this.props}
|
|
1148
|
+
renderDatamesh={() => this.renderDatamesh()}
|
|
1149
|
+
openStoredNotebook={this.props.openStoredNotebook}
|
|
1150
|
+
selected={this.selectedTab}
|
|
1151
|
+
onSelect={id => this.selectTab(id)}
|
|
1152
|
+
/>
|
|
1153
|
+
)}
|
|
1154
|
+
</UseSignal>
|
|
1052
1155
|
</div>
|
|
1053
1156
|
);
|
|
1054
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* The visible tab. The widget only holds it and announces changes through
|
|
1161
|
+
* `tabChanged`; the plugin persists it in IStateDB, because the layout restorer
|
|
1162
|
+
* restores a widget's place in the shell rather than fields on it.
|
|
1163
|
+
*/
|
|
1164
|
+
get selectedTab(): string {
|
|
1165
|
+
return this._selectedTab;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
selectTab(id: string): void {
|
|
1169
|
+
if (id !== this._selectedTab) {
|
|
1170
|
+
this._selectedTab = id;
|
|
1171
|
+
this.tabChanged.emit(id);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
readonly tabChanged = new Signal<this, string>(this);
|
|
1176
|
+
// Notebooks first: on notebook.oceanum.io this panel is how a user reaches their
|
|
1177
|
+
// work, so it opens there rather than on Datamesh.
|
|
1178
|
+
private _selectedTab = 'notebooks';
|
|
1055
1179
|
}
|