@oceanum/oceanumlab 4.6.2 → 4.7.1
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 +24 -2
- package/lib/DatameshWidget.js +171 -45
- package/lib/StoredNotebooks.d.ts +16 -0
- package/lib/StoredNotebooks.js +84 -0
- package/lib/Tabs.d.ts +26 -0
- package/lib/Tabs.js +45 -0
- package/lib/aiBackend.d.ts +66 -0
- package/lib/aiBackend.js +101 -0
- 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 +253 -0
- package/lib/auth/tokens.d.ts +95 -0
- package/lib/auth/tokens.js +9 -0
- package/lib/chatRouter.d.ts +17 -1
- package/lib/chatRouter.js +34 -9
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +2 -2
- package/lib/datameshUiUrl.js +8 -16
- package/lib/httpUrl.d.ts +9 -0
- package/lib/httpUrl.js +20 -0
- package/lib/index.d.ts +8 -1
- package/lib/index.js +67 -15
- package/lib/specStore.d.ts +37 -0
- package/lib/specStore.js +57 -0
- package/package.json +9 -1
- package/schema/datamesh-connect.json +3 -6
- package/src/DatameshUI.tsx +1 -10
- package/src/DatameshWidget.tsx +294 -59
- package/src/StoredNotebooks.tsx +166 -0
- package/src/Tabs.tsx +102 -0
- package/src/__tests__/DatameshWidget.spec.tsx +393 -0
- package/src/__tests__/OceanumPanel.spec.tsx +101 -0
- package/src/__tests__/Tabs.spec.tsx +168 -0
- package/src/__tests__/aiBackend.spec.ts +271 -0
- package/src/__tests__/chatRouter.spec.ts +253 -17
- package/src/__tests__/datameshUiUrl.spec.ts +4 -3
- 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__/specStore.spec.ts +126 -0
- package/src/aiBackend.ts +150 -0
- 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 +341 -0
- package/src/auth/tokens.ts +104 -0
- package/src/chatRouter.ts +41 -9
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +8 -15
- package/src/httpUrl.ts +23 -0
- package/src/index.ts +99 -33
- package/src/specStore.ts +82 -0
- package/style/index.css +158 -0
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ILayoutRestorer, ILabStatus } from '@jupyterlab/application';
|
|
|
2
2
|
import { ICommandPalette, Notification } from '@jupyterlab/apputils';
|
|
3
3
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { find } from '@lumino/algorithm';
|
|
5
|
+
import { Signal } from '@lumino/signaling';
|
|
5
6
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
6
7
|
import { IStateDB } from '@jupyterlab/statedb';
|
|
7
8
|
import { INotebookTracker } from '@jupyterlab/notebook';
|
|
@@ -11,11 +12,13 @@ import { requestAPI } from './handler';
|
|
|
11
12
|
import { ChatRouter, ChatRouterError } from './chatRouter';
|
|
12
13
|
import { reporterFor } from './progress';
|
|
13
14
|
import { ConversationPin } from './conversationPin';
|
|
15
|
+
import { deviceAuthPlugin } from './auth/plugin';
|
|
16
|
+
import { IOceanumAuth } from './auth/tokens';
|
|
14
17
|
import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
15
18
|
import { notebookHost } from './notebookHost';
|
|
16
19
|
import { KernelHandoff } from './kernelHandoff';
|
|
17
20
|
import { runChatLoop, STOPPED } from './aiLoop';
|
|
18
|
-
import { DATAMESH_UI_SERVICE, MAX_OBSERVE_ROUNDS } from './constants';
|
|
21
|
+
import { DATAMESH_UI_SERVICE, MAX_OBSERVE_ROUNDS, OCEANUM_AI_BACKEND_URL } from './constants';
|
|
19
22
|
import '../style/index.css';
|
|
20
23
|
import oceanumSvg from '../style/icons/oceanum.svg';
|
|
21
24
|
// Theme-adaptive icon using currentColor (auto-adapts to light/dark themes)
|
|
@@ -37,10 +40,22 @@ export const datamesh_connect_extension = {
|
|
|
37
40
|
ISettingRegistry,
|
|
38
41
|
IStateDB
|
|
39
42
|
],
|
|
40
|
-
|
|
43
|
+
// Optional so the panel still loads on a host with no Oceanum environment; the
|
|
44
|
+
// Notebooks tab then explains why it is empty instead of the panel failing to start.
|
|
45
|
+
optional: [IOceanumAuth],
|
|
46
|
+
activate: (app, palette, restorer, status, settingRegistry, stateDB, auth) => {
|
|
47
|
+
var _a, _b, _c, _d;
|
|
41
48
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
42
|
-
//
|
|
43
|
-
|
|
49
|
+
// Where the Datamesh UI and Oceanum AI live is the deployment's business, so it comes
|
|
50
|
+
// from the sign-in environment (the Jupyter server's configuration, or the notebook
|
|
51
|
+
// site's) and falls back to production. These were user settings until 4.7.1; a
|
|
52
|
+
// user-editable service address is somewhere to send the user's credential.
|
|
53
|
+
const datameshUiUrl = (_b = (_a = auth === null || auth === void 0 ? void 0 : auth.urls) === null || _a === void 0 ? void 0 : _a.datameshUi) !== null && _b !== void 0 ? _b : DATAMESH_UI_SERVICE;
|
|
54
|
+
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
|
+
// The `datameshToken` setting for the AI chat; empty until the settings load.
|
|
56
|
+
let datameshTokenSetting = '';
|
|
57
|
+
// Tells the sidebar the settings above have changed.
|
|
58
|
+
const settingsChanged = new Signal(app);
|
|
44
59
|
const findDatameshUI = () => find(app.shell.widgets('main'), (widget) => widget.id === 'datamesh-ui');
|
|
45
60
|
//Try to get the datamesh token from the settings
|
|
46
61
|
const updateSettings = (set) => {
|
|
@@ -53,12 +68,8 @@ export const datamesh_connect_extension = {
|
|
|
53
68
|
}).then(res => console.log(res));
|
|
54
69
|
}
|
|
55
70
|
window.injectToken = set.get('injectToken').user;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const datameshUI = findDatameshUI();
|
|
59
|
-
if (datameshUI) {
|
|
60
|
-
datameshUI.url = datameshUiUrl;
|
|
61
|
-
}
|
|
71
|
+
datameshTokenSetting = set.get('datameshToken').composite;
|
|
72
|
+
settingsChanged.emit();
|
|
62
73
|
};
|
|
63
74
|
//Try to get the datamesh token from the envars
|
|
64
75
|
Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)])
|
|
@@ -95,15 +106,45 @@ export const datamesh_connect_extension = {
|
|
|
95
106
|
openDatameshUI: openDatameshUI,
|
|
96
107
|
datameshUiUrl: () => datameshUiUrl,
|
|
97
108
|
datameshUiFrame: () => { var _a, _b; return (_b = (_a = findDatameshUI()) === null || _a === void 0 ? void 0 : _a.frame) !== null && _b !== void 0 ? _b : null; },
|
|
109
|
+
datameshToken: () => datameshTokenSetting,
|
|
110
|
+
aiBackendUrl: () => aiBackendUrl,
|
|
111
|
+
settingsChanged,
|
|
98
112
|
commands: app.commands,
|
|
99
|
-
getCurrentWidget
|
|
113
|
+
getCurrentWidget,
|
|
114
|
+
auth
|
|
115
|
+
// No `openStoredNotebook`: nothing can open a spec store record by id yet.
|
|
116
|
+
// share-oceanum has `oceanum-share:open`, but it takes no arguments — it opens
|
|
117
|
+
// its own picker — so there is nothing to hand an id to. Opening arrives with
|
|
118
|
+
// the drive in OCE-182, where the contents API makes it the ordinary path.
|
|
119
|
+
// Until then the list reads rather than offering a click that does nothing.
|
|
100
120
|
});
|
|
101
121
|
datameshConnectWidget.id = 'datamesh-connect';
|
|
102
122
|
datameshConnectWidget.title.icon = oceanumIcon;
|
|
103
123
|
datameshConnectWidget.title.caption = 'Datamesh Connect';
|
|
104
124
|
restorer.add(datameshConnectWidget, 'datamesh-connect');
|
|
125
|
+
// The layout restorer restores the widget's place in the shell, not fields on it,
|
|
126
|
+
// so the chosen tab is kept here. Restoring is best effort: a missing or unreadable
|
|
127
|
+
// entry just leaves the default.
|
|
128
|
+
const TAB_STATE_KEY = `${PLUGIN_ID}:tab`;
|
|
129
|
+
void stateDB
|
|
130
|
+
.fetch(TAB_STATE_KEY)
|
|
131
|
+
.then(value => {
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
datameshConnectWidget.selectTab(value);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.catch(() => undefined);
|
|
137
|
+
datameshConnectWidget.tabChanged.connect((_, id) => {
|
|
138
|
+
void stateDB.save(TAB_STATE_KEY, id);
|
|
139
|
+
});
|
|
105
140
|
// Rank has been chosen somewhat arbitrarily to give priority to the running
|
|
106
141
|
// sessions widget in the sidebar.
|
|
142
|
+
//
|
|
143
|
+
// Oceanum Notebook wants this panel first instead, but that is a property of that
|
|
144
|
+
// distribution rather than of the extension: in a plain JupyterLab the file browser
|
|
145
|
+
// is the primary navigation surface and displacing it would be a regression. A
|
|
146
|
+
// distribution moves it with JupyterLab's own shell user-layout settings, keyed on
|
|
147
|
+
// this widget's id, rather than this extension hard-coding one host's preference.
|
|
107
148
|
app.shell.add(datameshConnectWidget, 'left', { rank: 900 });
|
|
108
149
|
app.commands.addCommand('datamesh-ui:open', {
|
|
109
150
|
execute: (args) => {
|
|
@@ -121,7 +162,8 @@ export const oceanum_ai_extension = {
|
|
|
121
162
|
id: '@oceanum/oceanumlab:ai-chat',
|
|
122
163
|
autoStart: true,
|
|
123
164
|
requires: [INotebookTracker, ISettingRegistry],
|
|
124
|
-
|
|
165
|
+
optional: [IOceanumAuth],
|
|
166
|
+
activate: (app, notebookTracker, settingRegistry, auth) => {
|
|
125
167
|
console.log('Oceanum AI chat extension is loaded');
|
|
126
168
|
const SETTINGS_ID = '@oceanum/oceanumlab:datamesh-connect';
|
|
127
169
|
settingRegistry
|
|
@@ -137,7 +179,7 @@ export const oceanum_ai_extension = {
|
|
|
137
179
|
// The panel the current request read its selected cell from, if the
|
|
138
180
|
// notebook was open: a code answer may replace that cell only there.
|
|
139
181
|
let readFrom = null;
|
|
140
|
-
const router = new ChatRouter(settings, async () => {
|
|
182
|
+
const router = new ChatRouter(settings, app.commands, async () => {
|
|
141
183
|
const open = await pin.current();
|
|
142
184
|
readFrom = open;
|
|
143
185
|
if (open) {
|
|
@@ -157,7 +199,7 @@ export const oceanum_ai_extension = {
|
|
|
157
199
|
// Gone: an answer's cells will go into a new notebook instead.
|
|
158
200
|
return null;
|
|
159
201
|
}
|
|
160
|
-
});
|
|
202
|
+
}, () => { var _a, _b; return (_b = (_a = auth === null || auth === void 0 ? void 0 : auth.urls) === null || _a === void 0 ? void 0 : _a.ai) !== null && _b !== void 0 ? _b : ''; });
|
|
161
203
|
// Asked only when an answer has blocks to place: brings the
|
|
162
204
|
// conversation's notebook to the front, opening it again if closed.
|
|
163
205
|
const handoff = new KernelHandoff(() => pin.show(), () => readFrom);
|
|
@@ -277,4 +319,14 @@ export const oceanum_ai_extension = {
|
|
|
277
319
|
});
|
|
278
320
|
}
|
|
279
321
|
};
|
|
280
|
-
|
|
322
|
+
/**
|
|
323
|
+
* The sign-in contract, exported so another extension can provide or consume it: a Lumino
|
|
324
|
+
* token is matched by object identity, so everyone has to import this one object, and
|
|
325
|
+
* JupyterLab shares `@oceanum/oceanumlab` between extensions as a singleton module.
|
|
326
|
+
*/
|
|
327
|
+
export * from './auth/tokens';
|
|
328
|
+
export default [
|
|
329
|
+
deviceAuthPlugin,
|
|
330
|
+
datamesh_connect_extension,
|
|
331
|
+
oceanum_ai_extension
|
|
332
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the Oceanum spec store's `notebook` records.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
|
|
5
|
+
* share-oceanum package has a fuller client (create, update, permissions), and when
|
|
6
|
+
* sharing moves here the two should become one rather than sitting side by side.
|
|
7
|
+
*/
|
|
8
|
+
/** A spec store list entry. The list omits the notebook body. */
|
|
9
|
+
export interface ISpecSummary {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
/** Set when the record is created and never updated, so this is a creation time. */
|
|
14
|
+
modified: string;
|
|
15
|
+
/** Null unless the caller created the record: the store hides other creators. */
|
|
16
|
+
creator: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Every notebook the signed-in user may read: their own and those shared with them.
|
|
20
|
+
*
|
|
21
|
+
* The store answers an unauthenticated caller with an empty list rather than an error,
|
|
22
|
+
* so a missing token is indistinguishable from an empty account — callers should check
|
|
23
|
+
* they are signed in before showing "no notebooks".
|
|
24
|
+
*/
|
|
25
|
+
export declare function listNotebooks(specsUrl: string, token: string, fetcher?: typeof fetch): Promise<ISpecSummary[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Split a listing into the caller's own notebooks and the rest, newest first.
|
|
28
|
+
*
|
|
29
|
+
* The store only sends `creator` on records the caller created, so anything else it
|
|
30
|
+
* returns is shared with them — directly, or because it is public. Addresses are
|
|
31
|
+
* compared case-insensitively: the store matches entities as case-sensitive globs, so
|
|
32
|
+
* the two sides can differ in case.
|
|
33
|
+
*/
|
|
34
|
+
export declare function partitionNotebooks(items: readonly ISpecSummary[], email: string | null): {
|
|
35
|
+
mine: ISpecSummary[];
|
|
36
|
+
shared: ISpecSummary[];
|
|
37
|
+
};
|
package/lib/specStore.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the Oceanum spec store's `notebook` records.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
|
|
5
|
+
* share-oceanum package has a fuller client (create, update, permissions), and when
|
|
6
|
+
* sharing moves here the two should become one rather than sitting side by side.
|
|
7
|
+
*/
|
|
8
|
+
function isSummary(value) {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const v = value;
|
|
13
|
+
return (typeof v.id === 'string' &&
|
|
14
|
+
typeof v.name === 'string' &&
|
|
15
|
+
typeof v.modified === 'string');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every notebook the signed-in user may read: their own and those shared with them.
|
|
19
|
+
*
|
|
20
|
+
* The store answers an unauthenticated caller with an empty list rather than an error,
|
|
21
|
+
* so a missing token is indistinguishable from an empty account — callers should check
|
|
22
|
+
* they are signed in before showing "no notebooks".
|
|
23
|
+
*/
|
|
24
|
+
export async function listNotebooks(specsUrl, token, fetcher = fetch) {
|
|
25
|
+
const url = `${specsUrl.replace(/\/+$/, '')}/specs/notebook`;
|
|
26
|
+
const response = await fetcher(url, {
|
|
27
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
// The store answers a bad or expired token with 400, and 403 both for no access
|
|
31
|
+
// and for a record that does not exist.
|
|
32
|
+
throw new Error(`Oceanum.io returned HTTP ${response.status}.`);
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
if (!Array.isArray(data)) {
|
|
36
|
+
throw new Error('Oceanum.io returned an unexpected response.');
|
|
37
|
+
}
|
|
38
|
+
return data.filter(isSummary);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Split a listing into the caller's own notebooks and the rest, newest first.
|
|
42
|
+
*
|
|
43
|
+
* The store only sends `creator` on records the caller created, so anything else it
|
|
44
|
+
* returns is shared with them — directly, or because it is public. Addresses are
|
|
45
|
+
* compared case-insensitively: the store matches entities as case-sensitive globs, so
|
|
46
|
+
* the two sides can differ in case.
|
|
47
|
+
*/
|
|
48
|
+
export function partitionNotebooks(items, email) {
|
|
49
|
+
var _a;
|
|
50
|
+
const me = (_a = email === null || email === void 0 ? void 0 : email.toLowerCase()) !== null && _a !== void 0 ? _a : null;
|
|
51
|
+
const sorted = [...items].sort((a, b) => b.modified.localeCompare(a.modified));
|
|
52
|
+
const isMine = (item) => { var _a; return me !== null && ((_a = item.creator) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === me; };
|
|
53
|
+
return {
|
|
54
|
+
mine: sorted.filter(isMine),
|
|
55
|
+
shared: sorted.filter(item => !isMine(item))
|
|
56
|
+
};
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oceanum/oceanumlab",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.1",
|
|
4
4
|
"description": "A Jupyterlab extension to interact with the Oceanum.io platform",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -60,8 +60,16 @@
|
|
|
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",
|
|
63
65
|
"@jupyterlab/notebook": "^4.6.3",
|
|
66
|
+
"@jupyterlab/services": "^7.6.3",
|
|
64
67
|
"@jupyterlab/settingregistry": "^4.6.3",
|
|
68
|
+
"@lumino/commands": "^2.3.3",
|
|
69
|
+
"@lumino/coreutils": "^2.2.2",
|
|
70
|
+
"@lumino/disposable": "^2.1.5",
|
|
71
|
+
"@lumino/signaling": "^2.1.5",
|
|
72
|
+
"@lumino/widgets": "^2.8.0",
|
|
65
73
|
"@phosphor-icons/react": "^2.1.10",
|
|
66
74
|
"marked": "^18.0.13"
|
|
67
75
|
},
|
|
@@ -18,12 +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
21
|
"autoRunCode": {
|
|
28
22
|
"type": "boolean",
|
|
29
23
|
"title": "Auto-run code",
|
|
@@ -37,5 +31,8 @@
|
|
|
37
31
|
"default": false
|
|
38
32
|
}
|
|
39
33
|
},
|
|
34
|
+
"patternProperties": {
|
|
35
|
+
"^(datameshUiUrl|aiBackendUrl)$": {}
|
|
36
|
+
},
|
|
40
37
|
"additionalProperties": false
|
|
41
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;
|