@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
package/README.md
CHANGED
|
@@ -19,6 +19,62 @@ To install the extension, execute:
|
|
|
19
19
|
pip install oceanumlab
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Oceanum.io sign-in
|
|
23
|
+
|
|
24
|
+
A **Sign in to Oceanum.io** control sits in the top bar. Signing in gives the Notebooks tab
|
|
25
|
+
your stored notebooks and lets Oceanum AI act as you, with no Datamesh token to paste.
|
|
26
|
+
|
|
27
|
+
It uses the OAuth **device grant**, run by oceanumlab's Jupyter server extension: click
|
|
28
|
+
**Sign in**, open the link it shows, check the code matches, and sign in on Oceanum.io as
|
|
29
|
+
usual. The dialog closes by itself when you are done. It works with no configuration, and
|
|
30
|
+
from any address — `localhost` on any port, a JupyterHub, a remote server.
|
|
31
|
+
|
|
32
|
+
That last point is why it works this way. A browser sign-in (a popup, a redirect, the
|
|
33
|
+
Oceanum widget) has to tell Auth0 which page to return to, Auth0 only accepts addresses
|
|
34
|
+
registered in advance, and it cannot wildcard a port or an arbitrary host. A JupyterLab's
|
|
35
|
+
address is not knowable ahead of time, so on anything but Oceanum's own sites a browser
|
|
36
|
+
sign-in fails with "Callback URL mismatch". The device grant has no callback.
|
|
37
|
+
|
|
38
|
+
The refresh token stays in the Jupyter server's memory and never reaches the page or the
|
|
39
|
+
disk, so restarting the server signs you out.
|
|
40
|
+
|
|
41
|
+
### Configuration
|
|
42
|
+
|
|
43
|
+
Sign-in is server configuration, not a frontend setting: which Auth0 tenant a notebook signs
|
|
44
|
+
in to, and which services its tokens are sent to, is the deployment's decision, and a
|
|
45
|
+
user-editable setting could be pointed at someone else's. In `jupyter_server_config.py` —
|
|
46
|
+
`~/.jupyter/` for one user, or `etc/jupyter/` under the environment prefix for everyone
|
|
47
|
+
using it; `jupyter --paths` lists both:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
c.OceanumLab.sign_in = "off" # "device" is the default
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To sign in to a deployment other than Oceanum production, replace the environment whole:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
c.OceanumLab.device_environment = {
|
|
57
|
+
"auth0Domain": "auth.example.org",
|
|
58
|
+
"clientId": "<an Auth0 Native application with the Device Code grant>",
|
|
59
|
+
"audience": "",
|
|
60
|
+
"oceanumDomain": "example.org",
|
|
61
|
+
"urls": {
|
|
62
|
+
"datamesh": "https://datamesh.example.org",
|
|
63
|
+
"specs": "https://specs.example.org",
|
|
64
|
+
"manage": "https://manage.example.org",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**This file is Python, not JSON** — `True`, not `true`. A syntax error there is quiet and
|
|
70
|
+
expensive: traitlets logs it once at startup and then skips the **whole** file, so the
|
|
71
|
+
setting silently does not apply. Check the server's startup log for
|
|
72
|
+
`oceanumlab: Oceanum.io sign-in by device code`.
|
|
73
|
+
|
|
74
|
+
notebook.oceanum.io is a JupyterLite site with no Jupyter server, so none of this applies
|
|
75
|
+
there. It signs in with the Oceanum widget, from its own extension, and lists
|
|
76
|
+
`@oceanum/oceanumlab:device-auth` under `disabledExtensions`.
|
|
77
|
+
|
|
22
78
|
## Uninstall
|
|
23
79
|
|
|
24
80
|
To remove the extension, execute:
|
|
@@ -31,27 +87,59 @@ pip uninstall oceanumlab
|
|
|
31
87
|
|
|
32
88
|
### Development install
|
|
33
89
|
|
|
34
|
-
|
|
90
|
+
The extension is built with [jupyter-builder](https://github.com/jupyterlab/jupyter-builder),
|
|
91
|
+
which bundles it with rspack. You will need:
|
|
92
|
+
|
|
93
|
+
- Python 3.10 or later
|
|
94
|
+
- Node.js 22.12 or later (or 20.19+), which rspack requires
|
|
35
95
|
|
|
36
|
-
The `jlpm` command is
|
|
37
|
-
|
|
38
|
-
|
|
96
|
+
The `jlpm` command is a pinned version of [yarn](https://yarnpkg.com/) that is
|
|
97
|
+
installed with jupyter-builder. You may use `yarn` or `npm` in lieu of `jlpm` below.
|
|
98
|
+
|
|
99
|
+
Create an environment, either with conda, which also installs Node.js and jupyter-builder:
|
|
39
100
|
|
|
40
101
|
```bash
|
|
41
102
|
# Clone the repo to your local environment
|
|
42
103
|
# Change directory to the oceanumlab directory
|
|
43
|
-
|
|
44
|
-
|
|
104
|
+
conda env create -f environment.yml
|
|
105
|
+
conda activate oceanumlab
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
or with a virtual environment, using your own Node.js:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python -m venv .venv
|
|
112
|
+
source .venv/bin/activate
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then install the extension in development mode:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Install the package in development mode. The dev extra installs jupyter-builder
|
|
119
|
+
# and the test extra installs the server test dependencies. This also enables the
|
|
120
|
+
# server extension.
|
|
121
|
+
pip install -e ".[dev,test]"
|
|
45
122
|
# Link your development version of the extension with JupyterLab
|
|
46
123
|
jupyter-builder develop . --overwrite
|
|
47
|
-
# Rebuild extension
|
|
124
|
+
# Rebuild the extension after making changes
|
|
125
|
+
# Unlike the steps above, which you only do once, do this every time you make a change
|
|
48
126
|
jlpm build
|
|
49
127
|
```
|
|
50
128
|
|
|
129
|
+
Every `pip install -e` puts a copy of the built extension back in place of the link,
|
|
130
|
+
so run `jupyter-builder develop . --overwrite` again after reinstalling.
|
|
131
|
+
|
|
132
|
+
The main build commands are:
|
|
133
|
+
|
|
134
|
+
- `jlpm build`: development build, with source maps
|
|
135
|
+
- `jlpm build:prod`: production build, as used for releases
|
|
136
|
+
- `jlpm clean:all`: remove the built TypeScript, the built extension and the lint caches
|
|
137
|
+
|
|
51
138
|
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
52
139
|
|
|
53
140
|
```bash
|
|
54
141
|
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
142
|
+
# (runs tsc and jupyter-builder in watch mode)
|
|
55
143
|
jlpm watch
|
|
56
144
|
# Run JupyterLab in another terminal
|
|
57
145
|
jupyter lab
|
|
@@ -71,9 +159,9 @@ jupyter lab build --minimize=False
|
|
|
71
159
|
pip uninstall oceanumlab
|
|
72
160
|
```
|
|
73
161
|
|
|
74
|
-
In development mode, you will also need to remove the symlink created by `jupyter-builder develop`
|
|
162
|
+
In development mode, you will also need to remove the symlink created by the `jupyter-builder develop`
|
|
75
163
|
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
|
|
76
|
-
folder is located. Then you can remove the symlink named
|
|
164
|
+
folder is located. Then you can remove the symlink named `@oceanum/oceanumlab` within that folder.
|
|
77
165
|
|
|
78
166
|
### Testing the extension
|
|
79
167
|
|
|
@@ -81,10 +169,13 @@ folder is located. Then you can remove the symlink named `oceanumlab` within tha
|
|
|
81
169
|
|
|
82
170
|
This extension is using [Pytest](https://docs.pytest.org/) for Python code testing.
|
|
83
171
|
|
|
84
|
-
|
|
172
|
+
The test dependencies are installed by `pip install -e ".[dev,test]"` above. If you
|
|
173
|
+
installed without the test extra, install them (needed only once) and restore the
|
|
174
|
+
front-end extension link:
|
|
85
175
|
|
|
86
176
|
```sh
|
|
87
|
-
pip install -e ".[test]"
|
|
177
|
+
pip install -e ".[dev,test]"
|
|
178
|
+
jupyter-builder develop . --overwrite
|
|
88
179
|
```
|
|
89
180
|
|
|
90
181
|
To execute them, run:
|
package/lib/DatameshUI.d.ts
CHANGED
|
@@ -2,13 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
3
|
export declare class DatameshUI extends ReactWidget {
|
|
4
4
|
private _url;
|
|
5
|
-
/** `url` is the
|
|
5
|
+
/** `url` is the deployment's Datamesh UI address (the sign-in environment's). */
|
|
6
6
|
constructor(_url: string);
|
|
7
|
-
/**
|
|
8
|
-
* Point the panel at the `datameshUiUrl` setting's new value. The iframe
|
|
9
|
-
* reloads only if its address actually changes.
|
|
10
|
-
*/
|
|
11
|
-
set url(url: string);
|
|
12
7
|
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
13
8
|
get frame(): Window | null;
|
|
14
9
|
render(): React.ReactElement;
|
package/lib/DatameshUI.js
CHANGED
|
@@ -2,19 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
3
|
import { datameshUiSrc } from './datameshUiUrl';
|
|
4
4
|
export class DatameshUI extends ReactWidget {
|
|
5
|
-
/** `url` is the
|
|
5
|
+
/** `url` is the deployment's Datamesh UI address (the sign-in environment's). */
|
|
6
6
|
constructor(_url) {
|
|
7
7
|
super();
|
|
8
8
|
this._url = _url;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Point the panel at the `datameshUiUrl` setting's new value. The iframe
|
|
12
|
-
* reloads only if its address actually changes.
|
|
13
|
-
*/
|
|
14
|
-
set url(url) {
|
|
15
|
-
this._url = url;
|
|
16
|
-
this.update();
|
|
17
|
-
}
|
|
18
10
|
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
19
11
|
get frame() {
|
|
20
12
|
var _a, _b;
|
package/lib/DatameshWidget.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
5
5
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
6
6
|
import { Widget } from '@lumino/widgets';
|
|
7
7
|
import React from 'react';
|
|
8
|
+
import { IOceanumAuth } from './auth/tokens';
|
|
8
9
|
export interface IDatasource {
|
|
9
10
|
id: string;
|
|
10
11
|
label: string;
|
|
@@ -40,18 +41,22 @@ export interface IDatameshWidgetProps {
|
|
|
40
41
|
name: string;
|
|
41
42
|
icon: LabIcon;
|
|
42
43
|
openDatameshUI: any;
|
|
43
|
-
/** The
|
|
44
|
+
/** The deployment's Datamesh UI address. */
|
|
44
45
|
datameshUiUrl: () => string;
|
|
45
46
|
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
46
47
|
datameshUiFrame: () => Window | null;
|
|
47
48
|
/** The `datameshToken` setting. */
|
|
48
49
|
datameshToken: () => string;
|
|
49
|
-
/** The
|
|
50
|
+
/** The deployment's Oceanum AI address. */
|
|
50
51
|
aiBackendUrl: () => string;
|
|
51
52
|
/** Emitted when the settings change. */
|
|
52
53
|
settingsChanged: ISignal<unknown, void>;
|
|
53
54
|
commands: CommandRegistry;
|
|
54
55
|
getCurrentWidget: () => Widget;
|
|
56
|
+
/** Oceanum.io sign-in, or null on a host with no Oceanum environment. */
|
|
57
|
+
auth?: IOceanumAuth | null;
|
|
58
|
+
/** Open a stored notebook by spec store id. */
|
|
59
|
+
openStoredNotebook?: (id: string) => void;
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
62
|
* A widget for Datamesh Connections.
|
|
@@ -65,5 +70,16 @@ export declare class DatameshConnectWidget extends ReactWidget {
|
|
|
65
70
|
constructor(props: IDatameshWidgetProps);
|
|
66
71
|
receiveIFrameMessage(event: MessageEvent): void;
|
|
67
72
|
renderDisplay(datameshWorkspace: IWorkspaceSpec): React.ReactElement;
|
|
73
|
+
/** The Datamesh workspace tab: what this panel showed above the divider. */
|
|
74
|
+
renderDatamesh(): React.ReactElement;
|
|
68
75
|
render(): React.ReactElement;
|
|
76
|
+
/**
|
|
77
|
+
* The visible tab. The widget only holds it and announces changes through
|
|
78
|
+
* `tabChanged`; the plugin persists it in IStateDB, because the layout restorer
|
|
79
|
+
* restores a widget's place in the shell rather than fields on it.
|
|
80
|
+
*/
|
|
81
|
+
get selectedTab(): string;
|
|
82
|
+
selectTab(id: string): void;
|
|
83
|
+
readonly tabChanged: Signal<this, string>;
|
|
84
|
+
private _selectedTab;
|
|
69
85
|
}
|
package/lib/DatameshWidget.js
CHANGED
|
@@ -9,7 +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 {
|
|
12
|
+
import { startSignIn } from './auth/startSignIn';
|
|
13
|
+
import { StoredNotebooks } from './StoredNotebooks';
|
|
14
|
+
import { Tabs } from './Tabs';
|
|
15
|
+
import { SIGN_IN_COMMAND, aiBackendUrl, aiCredentialSource, canSignIn, isSignedIn, pastedToken, resolveAiCredential, signInToken } from './aiBackend';
|
|
13
16
|
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
14
17
|
import { describeProgress, onProgress } from './progress';
|
|
15
18
|
// Configure marked for safe rendering
|
|
@@ -290,6 +293,7 @@ function readAiAccess(commands, settings) {
|
|
|
290
293
|
source: aiCredentialSource(datameshToken, commands),
|
|
291
294
|
pasted: pastedToken(datameshToken),
|
|
292
295
|
canSignIn: canSignIn(commands),
|
|
296
|
+
signedIn: isSignedIn(commands),
|
|
293
297
|
backend: aiBackendUrl(settings.aiBackendUrl())
|
|
294
298
|
};
|
|
295
299
|
}
|
|
@@ -342,9 +346,11 @@ function TokenConfigMessage({ commands, settings }) {
|
|
|
342
346
|
"."));
|
|
343
347
|
}
|
|
344
348
|
return (React.createElement("div", { className: "oceanum-token-config" },
|
|
345
|
-
"Set your
|
|
346
|
-
|
|
347
|
-
"
|
|
349
|
+
"Set your",
|
|
350
|
+
' ',
|
|
351
|
+
React.createElement("a", { onClick: () => commands.execute('settingeditor:open', { query: 'Oceanum' }) }, "Datamesh token"),
|
|
352
|
+
' ',
|
|
353
|
+
"to enable Oceanum.io services",
|
|
348
354
|
' ',
|
|
349
355
|
React.createElement("a", { href: "https://home.oceanum.io/account", target: "_blank", rel: "noopener noreferrer" }, "Get token here")));
|
|
350
356
|
}
|
|
@@ -596,6 +602,44 @@ function AIChatPanel({ commands, settings }) {
|
|
|
596
602
|
? commands.execute('oceanum-ai:stop')
|
|
597
603
|
: handleSubmit()), disabled: !loading && !input.trim(), title: loading ? 'Stop the current response' : undefined }, loading ? 'Stop' : 'Send'))));
|
|
598
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* The panel's tabs.
|
|
607
|
+
*
|
|
608
|
+
* A component rather than a list built in `render`, because whether Oceanum AI is
|
|
609
|
+
* reachable is not static: `useAiAccess` re-reads it when the settings change and when
|
|
610
|
+
* the host signals a sign-in or sign-out. An AI tab is only offered when the chat has a
|
|
611
|
+
* credential to work with — otherwise it would open on the same nothing the panel
|
|
612
|
+
* rendered before there were tabs, except now with a label promising otherwise.
|
|
613
|
+
*/
|
|
614
|
+
function PanelTabs({ auth, commands, settings, renderDatamesh, openStoredNotebook, selected, onSelect }) {
|
|
615
|
+
const access = useAiAccess(commands, settings);
|
|
616
|
+
const tabs = [
|
|
617
|
+
{
|
|
618
|
+
id: 'notebooks',
|
|
619
|
+
label: 'Notebooks',
|
|
620
|
+
render: () =>
|
|
621
|
+
// An auth with no environment is a provider that has nothing to sign in to (the
|
|
622
|
+
// 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: () => {
|
|
624
|
+
startSignIn(commands, auth).catch(error => {
|
|
625
|
+
console.warn('Oceanum.io sign-in could not start.', error);
|
|
626
|
+
});
|
|
627
|
+
} })) : (React.createElement("div", { className: "oceanum-text-empty" }, "Oceanum.io sign-in is not configured for this host."))
|
|
628
|
+
},
|
|
629
|
+
{ id: 'datamesh', label: 'Datamesh', render: renderDatamesh }
|
|
630
|
+
];
|
|
631
|
+
// Signed in, not merely holding a credential: a Datamesh token pasted into the
|
|
632
|
+
// settings would reach the backend, but the tab stays out of the way until someone
|
|
633
|
+
// has signed in to Oceanum.io.
|
|
634
|
+
if (access.signedIn) {
|
|
635
|
+
tabs.push({
|
|
636
|
+
id: 'ai',
|
|
637
|
+
label: 'Oceanum AI',
|
|
638
|
+
render: () => React.createElement(AIChatPanel, { commands: commands, settings: settings })
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return React.createElement(Tabs, { tabs: tabs, selected: selected, onSelect: onSelect });
|
|
642
|
+
}
|
|
599
643
|
/**
|
|
600
644
|
* A widget for Datamesh Connections.
|
|
601
645
|
*/
|
|
@@ -603,6 +647,10 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
603
647
|
constructor(props) {
|
|
604
648
|
super();
|
|
605
649
|
this.datameshWorkspaceSpec = null;
|
|
650
|
+
this.tabChanged = new Signal(this);
|
|
651
|
+
// Notebooks first: on notebook.oceanum.io this panel is how a user reaches their
|
|
652
|
+
// work, so it opens there rather than on Datamesh.
|
|
653
|
+
this._selectedTab = 'notebooks';
|
|
606
654
|
this.props = props;
|
|
607
655
|
this.renderSignal = new Signal(this);
|
|
608
656
|
this.renderDisplay = this.renderDisplay.bind(this);
|
|
@@ -628,18 +676,38 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
628
676
|
React.createElement("a", { onClick: this.props.openDatameshUI }, "Open"),
|
|
629
677
|
" the Oceanum Datamesh UI to add datasources."))));
|
|
630
678
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
React.createElement(this.props.icon.react, { tag: "span", width: "auto", height: "20px", verticalAlign: "middle" }),
|
|
635
|
-
React.createElement("span", { className: "oceanum-sidebar-title" }, "Oceanum.io")),
|
|
679
|
+
/** The Datamesh workspace tab: what this panel showed above the divider. */
|
|
680
|
+
renderDatamesh() {
|
|
681
|
+
return (React.createElement(React.Fragment, null,
|
|
636
682
|
React.createElement("div", { className: "datamesh-workspace-header" },
|
|
637
683
|
React.createElement("span", null, "Datamesh Workspace"),
|
|
638
684
|
React.createElement("div", { className: "open-datamesh-ui", onClick: this.props.openDatameshUI, title: "Open Datamesh UI" }, React.createElement(addIcon.react, { height: "20px", verticalAlign: "middle" }))),
|
|
639
685
|
React.createElement("div", { className: "datamesh-connect-workspace" },
|
|
640
686
|
React.createElement(UseSignal, { signal: this.renderSignal, initialArgs: null }, (_, datameshWorkspace) => this.renderDisplay(datameshWorkspace)),
|
|
641
|
-
React.createElement(TokenConfigMessage, { commands: this.props.commands, settings: this.props }))
|
|
642
|
-
|
|
643
|
-
|
|
687
|
+
React.createElement(TokenConfigMessage, { commands: this.props.commands, settings: this.props }))));
|
|
688
|
+
}
|
|
689
|
+
render() {
|
|
690
|
+
return (React.createElement("div", { className: "datamesh-connect" },
|
|
691
|
+
React.createElement("header", { className: "oceanum-sidebar-header" },
|
|
692
|
+
React.createElement(this.props.icon.react, { tag: "span", width: "auto", height: "20px", verticalAlign: "middle" }),
|
|
693
|
+
React.createElement("span", { className: "oceanum-sidebar-title" }, "Oceanum.io")),
|
|
694
|
+
React.createElement(UseSignal, { signal: this.tabChanged, initialArgs: this.selectedTab }, () => {
|
|
695
|
+
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) }));
|
|
697
|
+
})));
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* The visible tab. The widget only holds it and announces changes through
|
|
701
|
+
* `tabChanged`; the plugin persists it in IStateDB, because the layout restorer
|
|
702
|
+
* restores a widget's place in the shell rather than fields on it.
|
|
703
|
+
*/
|
|
704
|
+
get selectedTab() {
|
|
705
|
+
return this._selectedTab;
|
|
706
|
+
}
|
|
707
|
+
selectTab(id) {
|
|
708
|
+
if (id !== this._selectedTab) {
|
|
709
|
+
this._selectedTab = id;
|
|
710
|
+
this.tabChanged.emit(id);
|
|
711
|
+
}
|
|
644
712
|
}
|
|
645
713
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IOceanumAuth } from './auth/tokens';
|
|
3
|
+
import { ISpecSummary } from './share/notebook';
|
|
4
|
+
export interface IStoredNotebooksProps {
|
|
5
|
+
auth: IOceanumAuth;
|
|
6
|
+
/**
|
|
7
|
+
* Open a stored notebook. Undefined where nothing has registered a way to open one;
|
|
8
|
+
* the list then reads rather than offering a click that does nothing.
|
|
9
|
+
*/
|
|
10
|
+
onOpen?: (item: ISpecSummary) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Start signing in the way this host shows it; without it the auth's own `signIn()`
|
|
13
|
+
* is used, which on a JupyterLab server shows no code dialog.
|
|
14
|
+
*/
|
|
15
|
+
onSignIn?: () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
19
|
+
*/
|
|
20
|
+
export declare function StoredNotebooks({ auth, onOpen, onSignIn }: IStoredNotebooksProps): React.ReactElement;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { SpecStoreClient } from './share/client';
|
|
3
|
+
import { notebooksChanged } from './share/events';
|
|
4
|
+
import { partitionSummaries } from './share/notebook';
|
|
5
|
+
import { SPEC_ID_ATTRIBUTE } from './share/plugin';
|
|
6
|
+
function Section({ title, items, empty, onOpen }) {
|
|
7
|
+
// Each row carries its record id so the share plugin's context menu (Open, Rename,
|
|
8
|
+
// Share, Delete) can tell which one was clicked.
|
|
9
|
+
const rowAttributes = (item) => {
|
|
10
|
+
var _a;
|
|
11
|
+
return ({
|
|
12
|
+
[SPEC_ID_ATTRIBUTE]: item.id,
|
|
13
|
+
title: (_a = item.description) !== null && _a !== void 0 ? _a : undefined
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
return (React.createElement("div", { className: "oceanum-notebooks-section" },
|
|
17
|
+
React.createElement("div", { className: "oceanum-notebooks-heading" },
|
|
18
|
+
title,
|
|
19
|
+
React.createElement("span", { className: "oceanum-notebooks-count" }, items.length)),
|
|
20
|
+
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) },
|
|
21
|
+
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled'))) : (React.createElement("div", { className: "oceanum-notebooks-item oceanum-notebooks-item-static", ...rowAttributes(item) },
|
|
22
|
+
React.createElement("span", { className: "oceanum-notebooks-name" }, item.name || 'Untitled'))))))))));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The notebooks this user has stored on Oceanum.io, theirs and those shared with them.
|
|
26
|
+
*/
|
|
27
|
+
export function StoredNotebooks({ auth, onOpen, onSignIn }) {
|
|
28
|
+
const [load, setLoad] = React.useState({ state: 'loading' });
|
|
29
|
+
const [user, setUser] = React.useState(auth.user);
|
|
30
|
+
// Bumped whenever the store changes, so the list is fetched again.
|
|
31
|
+
const [version, setVersion] = React.useState(0);
|
|
32
|
+
// Reload whenever the signed-in user changes, so signing in or out does not leave a
|
|
33
|
+
// stale list from the previous session on screen; and whenever this extension has
|
|
34
|
+
// created, renamed or deleted a record.
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const onUser = () => setUser(auth.user);
|
|
37
|
+
const onChanged = () => setVersion(v => v + 1);
|
|
38
|
+
auth.userChanged.connect(onUser);
|
|
39
|
+
notebooksChanged.connect(onChanged);
|
|
40
|
+
return () => {
|
|
41
|
+
auth.userChanged.disconnect(onUser);
|
|
42
|
+
notebooksChanged.disconnect(onChanged);
|
|
43
|
+
};
|
|
44
|
+
}, [auth]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
const specs = (_b = (_a = auth.urls) === null || _a === void 0 ? void 0 : _a.specs) !== null && _b !== void 0 ? _b : null;
|
|
49
|
+
if (!user || !specs) {
|
|
50
|
+
setLoad({ state: 'ready', items: [] });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setLoad({ state: 'loading' });
|
|
54
|
+
void (async () => {
|
|
55
|
+
try {
|
|
56
|
+
const client = new SpecStoreClient({
|
|
57
|
+
specsUrl: specs,
|
|
58
|
+
getAccessToken: () => auth.getAccessToken()
|
|
59
|
+
});
|
|
60
|
+
const items = await client.list();
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
setLoad({ state: 'ready', items });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!cancelled) {
|
|
67
|
+
setLoad({
|
|
68
|
+
state: 'error',
|
|
69
|
+
message: error instanceof Error
|
|
70
|
+
? error.message
|
|
71
|
+
: 'Could not reach Oceanum.io.'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
return () => {
|
|
77
|
+
cancelled = true;
|
|
78
|
+
};
|
|
79
|
+
}, [auth, user, version]);
|
|
80
|
+
if (!user) {
|
|
81
|
+
const signIn = () => {
|
|
82
|
+
if (onSignIn) {
|
|
83
|
+
onSignIn();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
auth.signIn().catch(error => {
|
|
87
|
+
console.warn('Oceanum.io sign-in could not start.', error);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
return (React.createElement("div", { className: "oceanum-text-empty" },
|
|
91
|
+
React.createElement("a", { role: "button", tabIndex: 0, onClick: signIn, onKeyDown: event => {
|
|
92
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
signIn();
|
|
95
|
+
}
|
|
96
|
+
} }, "Sign in"),
|
|
97
|
+
' ',
|
|
98
|
+
"to Oceanum.io to see your notebooks."));
|
|
99
|
+
}
|
|
100
|
+
if (load.state === 'loading') {
|
|
101
|
+
return React.createElement("div", { className: "oceanum-text-empty" }, "Loading\u2026");
|
|
102
|
+
}
|
|
103
|
+
if (load.state === 'error') {
|
|
104
|
+
// Never render the sections alongside an error: their empty messages would claim
|
|
105
|
+
// the account has no notebooks when the load simply failed.
|
|
106
|
+
return React.createElement("div", { className: "oceanum-text-error" }, load.message);
|
|
107
|
+
}
|
|
108
|
+
const { mine, shared } = partitionSummaries(load.items, user.email);
|
|
109
|
+
return (React.createElement("div", { className: "oceanum-notebooks" },
|
|
110
|
+
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 })));
|
|
112
|
+
}
|
package/lib/Tabs.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ITab {
|
|
3
|
+
/** Stable key, also used as the restored selection value. */
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
render: () => React.ReactElement;
|
|
7
|
+
}
|
|
8
|
+
export interface ITabsProps {
|
|
9
|
+
tabs: readonly ITab[];
|
|
10
|
+
/** Selected tab id. Falls back to the first tab when it names no tab. */
|
|
11
|
+
selected: string;
|
|
12
|
+
onSelect: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A tab strip over a single visible pane.
|
|
16
|
+
*
|
|
17
|
+
* JupyterLab users work from the keyboard, so the strip follows the WAI-ARIA tabs
|
|
18
|
+
* pattern: one tab stop for the whole strip, arrows to move between tabs, Home and End
|
|
19
|
+
* to reach the ends.
|
|
20
|
+
*
|
|
21
|
+
* Every pane stays mounted and the inactive ones are hidden. Unmounting would be
|
|
22
|
+
* cheaper, but the AI pane holds the conversation in component state and the Datamesh
|
|
23
|
+
* pane holds a workspace subscription, so switching tabs would silently discard a chat
|
|
24
|
+
* the user is in the middle of.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Tabs({ tabs, selected, onSelect }: ITabsProps): React.ReactElement;
|
package/lib/Tabs.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A tab strip over a single visible pane.
|
|
4
|
+
*
|
|
5
|
+
* JupyterLab users work from the keyboard, so the strip follows the WAI-ARIA tabs
|
|
6
|
+
* pattern: one tab stop for the whole strip, arrows to move between tabs, Home and End
|
|
7
|
+
* to reach the ends.
|
|
8
|
+
*
|
|
9
|
+
* Every pane stays mounted and the inactive ones are hidden. Unmounting would be
|
|
10
|
+
* cheaper, but the AI pane holds the conversation in component state and the Datamesh
|
|
11
|
+
* pane holds a workspace subscription, so switching tabs would silently discard a chat
|
|
12
|
+
* the user is in the middle of.
|
|
13
|
+
*/
|
|
14
|
+
export function Tabs({ tabs, selected, onSelect }) {
|
|
15
|
+
const index = Math.max(0, tabs.findIndex(tab => tab.id === selected));
|
|
16
|
+
const active = tabs[index];
|
|
17
|
+
const onKeyDown = (event) => {
|
|
18
|
+
const last = tabs.length - 1;
|
|
19
|
+
let next = null;
|
|
20
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
21
|
+
next = index === last ? 0 : index + 1;
|
|
22
|
+
}
|
|
23
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
24
|
+
next = index === 0 ? last : index - 1;
|
|
25
|
+
}
|
|
26
|
+
else if (event.key === 'Home') {
|
|
27
|
+
next = 0;
|
|
28
|
+
}
|
|
29
|
+
else if (event.key === 'End') {
|
|
30
|
+
next = last;
|
|
31
|
+
}
|
|
32
|
+
if (next === null) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Otherwise the arrow keys also scroll the panel.
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
onSelect(tabs[next].id);
|
|
38
|
+
};
|
|
39
|
+
return (React.createElement("div", { className: "oceanum-tabs" },
|
|
40
|
+
React.createElement("div", { className: "oceanum-tabs-strip", role: "tablist", "aria-label": "Oceanum.io" }, tabs.map((tab, i) => (React.createElement("button", { key: tab.id, id: `oceanum-tab-${tab.id}`, className: i === index ? 'oceanum-tab oceanum-tab-selected' : 'oceanum-tab', role: "tab", type: "button", "aria-selected": i === index, "aria-controls": `oceanum-tabpanel-${tab.id}`,
|
|
41
|
+
// Only the selected tab is in the tab order, so Tab moves past the strip
|
|
42
|
+
// rather than through every tab in it.
|
|
43
|
+
tabIndex: i === index ? 0 : -1, onClick: () => onSelect(tab.id), onKeyDown: onKeyDown }, tab.label)))),
|
|
44
|
+
tabs.map(tab => (React.createElement("div", { key: tab.id, className: "oceanum-tabpanel", id: `oceanum-tabpanel-${tab.id}`, role: "tabpanel", "aria-labelledby": `oceanum-tab-${tab.id}`, hidden: tab.id !== active.id }, tab.render())))));
|
|
45
|
+
}
|
package/lib/aiBackend.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface IAiCredential {
|
|
|
35
35
|
headers: Record<string, string>;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* The backend address for
|
|
38
|
+
* The backend address for a configured value, without a trailing
|
|
39
39
|
* slash so a path can be appended. An invalid value falls back to the default.
|
|
40
40
|
*/
|
|
41
41
|
export declare function aiBackendUrl(value: string | null | undefined): string;
|
package/lib/aiBackend.js
CHANGED
|
@@ -14,7 +14,7 @@ export const SIGN_IN_COMMAND = 'oceanum-auth:sign-in';
|
|
|
14
14
|
/** The host's command that signs out. Visible only while signed in. */
|
|
15
15
|
export const SIGN_OUT_COMMAND = 'oceanum-auth:sign-out';
|
|
16
16
|
/**
|
|
17
|
-
* The backend address for
|
|
17
|
+
* The backend address for a configured value, without a trailing
|
|
18
18
|
* slash so a path can be appended. An invalid value falls back to the default.
|
|
19
19
|
*/
|
|
20
20
|
export function aiBackendUrl(value) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
2
|
+
import { Widget } from '@lumino/widgets';
|
|
3
|
+
import { IPendingSignIn, ServerAuth } from './serverAuth';
|
|
4
|
+
export declare const CommandIDs: {
|
|
5
|
+
readonly signIn: "oceanum-auth:sign-in";
|
|
6
|
+
readonly signOut: "oceanum-auth:sign-out";
|
|
7
|
+
readonly account: "oceanum-auth:account";
|
|
8
|
+
/**
|
|
9
|
+
* For other parts of oceanumlab and other extensions, such as the AI chat: resolves to
|
|
10
|
+
* the current access token, or `null` when signed out. Never rejects.
|
|
11
|
+
*/
|
|
12
|
+
readonly accessToken: "oceanum-auth:access-token";
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The top-bar account control: "Sign in" when signed out, the user's email with a sign-out
|
|
16
|
+
* menu when signed in.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AccountWidget extends Widget {
|
|
19
|
+
constructor(auth: ServerAuth, commands: CommandRegistry);
|
|
20
|
+
dispose(): void;
|
|
21
|
+
private _onClick;
|
|
22
|
+
private _render;
|
|
23
|
+
private _auth;
|
|
24
|
+
private _commands;
|
|
25
|
+
private _menu;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What the sign-in dialog shows: the code to confirm, and a link to the page that confirms
|
|
29
|
+
* it. A real link rather than `window.open`, which a popup blocker stops once there has
|
|
30
|
+
* been an `await` between the click and the call -- and there always has, because the code
|
|
31
|
+
* comes from the server.
|
|
32
|
+
*/
|
|
33
|
+
export declare class SignInBody extends Widget {
|
|
34
|
+
constructor(pending: IPendingSignIn);
|
|
35
|
+
}
|