@oceanum/oceanumlab 4.6.1 → 4.6.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/lib/DatameshUI.d.ts +10 -0
- package/lib/DatameshUI.js +20 -2
- package/lib/DatameshWidget.d.ts +4 -0
- package/lib/DatameshWidget.js +6 -0
- package/lib/datameshUiUrl.d.ts +15 -0
- package/lib/datameshUiUrl.js +43 -0
- package/lib/index.js +14 -6
- package/package.json +1 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshUI.tsx +21 -2
- package/src/DatameshWidget.tsx +16 -0
- package/src/__tests__/datameshUiUrl.spec.ts +154 -0
- package/src/datameshUiUrl.ts +51 -0
- package/src/index.ts +20 -9
package/lib/DatameshUI.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
3
|
export declare class DatameshUI extends ReactWidget {
|
|
4
|
+
private _url;
|
|
5
|
+
/** `url` is the `datameshUiUrl` setting. */
|
|
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
|
+
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
13
|
+
get frame(): Window | null;
|
|
4
14
|
render(): React.ReactElement;
|
|
5
15
|
}
|
package/lib/DatameshUI.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
|
-
import {
|
|
3
|
+
import { datameshUiSrc } from './datameshUiUrl';
|
|
4
4
|
export class DatameshUI extends ReactWidget {
|
|
5
|
+
/** `url` is the `datameshUiUrl` setting. */
|
|
6
|
+
constructor(_url) {
|
|
7
|
+
super();
|
|
8
|
+
this._url = _url;
|
|
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
|
+
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
19
|
+
get frame() {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
return (_b = (_a = this.node.querySelector('iframe')) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
22
|
+
}
|
|
5
23
|
render() {
|
|
6
|
-
return (React.createElement("iframe", { src:
|
|
24
|
+
return (React.createElement("iframe", { src: datameshUiSrc(this._url), className: "datamesh-iframe" }));
|
|
7
25
|
}
|
|
8
26
|
}
|
package/lib/DatameshWidget.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ export interface IDatameshWidgetProps {
|
|
|
40
40
|
name: string;
|
|
41
41
|
icon: LabIcon;
|
|
42
42
|
openDatameshUI: any;
|
|
43
|
+
/** The `datameshUiUrl` setting. */
|
|
44
|
+
datameshUiUrl: () => string;
|
|
45
|
+
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
46
|
+
datameshUiFrame: () => Window | null;
|
|
43
47
|
commands: CommandRegistry;
|
|
44
48
|
getCurrentWidget: () => Widget;
|
|
45
49
|
}
|
package/lib/DatameshWidget.js
CHANGED
|
@@ -10,6 +10,7 @@ import React from 'react';
|
|
|
10
10
|
import { marked } from 'marked';
|
|
11
11
|
import { DatasourceItem } from './DatasourceItem';
|
|
12
12
|
import { OCEANUM_AI_BACKEND_URL } from './constants';
|
|
13
|
+
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
13
14
|
import { describeProgress, onProgress } from './progress';
|
|
14
15
|
// Configure marked for safe rendering
|
|
15
16
|
marked.setOptions({
|
|
@@ -541,6 +542,11 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
541
542
|
window.addEventListener('message', this.receiveIFrameMessage.bind(this), false);
|
|
542
543
|
}
|
|
543
544
|
receiveIFrameMessage(event) {
|
|
545
|
+
// Any page or frame can post to this window; only the Datamesh UI panel
|
|
546
|
+
// may change the workspace.
|
|
547
|
+
if (!isDatameshUiMessage(event, this.props.datameshUiUrl(), this.props.datameshUiFrame())) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
544
550
|
if (event.data && event.data.action === 'workspace-modify') {
|
|
545
551
|
this.datameshWorkspaceSpec = {
|
|
546
552
|
id: event.data.id,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
|
|
2
|
+
export declare function validDatameshUiUrl(url: string | null | undefined): URL;
|
|
3
|
+
/**
|
|
4
|
+
* The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
|
|
5
|
+
* Oceanum.io navigator. Any query the setting already has is kept.
|
|
6
|
+
*/
|
|
7
|
+
export declare function datameshUiSrc(url: string | null | undefined): string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether a `message` event was posted by the Datamesh UI panel.
|
|
10
|
+
*
|
|
11
|
+
* Any page or frame can post to this window, so a message counts only when it
|
|
12
|
+
* comes from the panel's iframe (`frame`, its `contentWindow`; `null` when the
|
|
13
|
+
* panel is not open) and from the configured Datamesh UI's origin.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isDatameshUiMessage(event: Pick<MessageEvent, 'origin' | 'source'>, url: string | null | undefined, frame: Window | null): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Datamesh UI panel's address, and which messages may come from it.
|
|
3
|
+
*
|
|
4
|
+
* The address is a setting (`datameshUiUrl`), so a deployment can point the
|
|
5
|
+
* panel at another Datamesh UI -- a dev one, say. A setting is typed in by
|
|
6
|
+
* hand, so anything that is not an http(s) URL falls back to the default
|
|
7
|
+
* rather than ending up in an iframe's `src`.
|
|
8
|
+
*/
|
|
9
|
+
import { DATAMESH_UI_SERVICE } from './constants';
|
|
10
|
+
/** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
|
|
11
|
+
export function validDatameshUiUrl(url) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL((url !== null && url !== void 0 ? url : '').trim());
|
|
14
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (_a) {
|
|
19
|
+
// Empty or not a URL: the default below.
|
|
20
|
+
}
|
|
21
|
+
return new URL(DATAMESH_UI_SERVICE);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
|
|
25
|
+
* Oceanum.io navigator. Any query the setting already has is kept.
|
|
26
|
+
*/
|
|
27
|
+
export function datameshUiSrc(url) {
|
|
28
|
+
const src = validDatameshUiUrl(url);
|
|
29
|
+
src.searchParams.set('embed', '1');
|
|
30
|
+
return src.toString();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a `message` event was posted by the Datamesh UI panel.
|
|
34
|
+
*
|
|
35
|
+
* Any page or frame can post to this window, so a message counts only when it
|
|
36
|
+
* comes from the panel's iframe (`frame`, its `contentWindow`; `null` when the
|
|
37
|
+
* panel is not open) and from the configured Datamesh UI's origin.
|
|
38
|
+
*/
|
|
39
|
+
export function isDatameshUiMessage(event, url, frame) {
|
|
40
|
+
return (!!frame &&
|
|
41
|
+
event.source === frame &&
|
|
42
|
+
event.origin === validDatameshUiUrl(url).origin);
|
|
43
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
|
15
15
|
import { notebookHost } from './notebookHost';
|
|
16
16
|
import { KernelHandoff } from './kernelHandoff';
|
|
17
17
|
import { runChatLoop, STOPPED } from './aiLoop';
|
|
18
|
-
import { MAX_OBSERVE_ROUNDS } from './constants';
|
|
18
|
+
import { DATAMESH_UI_SERVICE, MAX_OBSERVE_ROUNDS } from './constants';
|
|
19
19
|
import '../style/index.css';
|
|
20
20
|
import oceanumSvg from '../style/icons/oceanum.svg';
|
|
21
21
|
// Theme-adaptive icon using currentColor (auto-adapts to light/dark themes)
|
|
@@ -39,6 +39,9 @@ export const datamesh_connect_extension = {
|
|
|
39
39
|
],
|
|
40
40
|
activate: (app, palette, restorer, status, settingRegistry) => {
|
|
41
41
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
42
|
+
// The `datameshUiUrl` setting; the default until the settings load.
|
|
43
|
+
let datameshUiUrl = DATAMESH_UI_SERVICE;
|
|
44
|
+
const findDatameshUI = () => find(app.shell.widgets('main'), (widget) => widget.id === 'datamesh-ui');
|
|
42
45
|
//Try to get the datamesh token from the settings
|
|
43
46
|
const updateSettings = (set) => {
|
|
44
47
|
const token = set.get('datameshToken');
|
|
@@ -50,6 +53,12 @@ export const datamesh_connect_extension = {
|
|
|
50
53
|
}).then(res => console.log(res));
|
|
51
54
|
}
|
|
52
55
|
window.injectToken = set.get('injectToken').user;
|
|
56
|
+
// An open Datamesh UI panel follows a change of address.
|
|
57
|
+
datameshUiUrl = set.get('datameshUiUrl').composite;
|
|
58
|
+
const datameshUI = findDatameshUI();
|
|
59
|
+
if (datameshUI) {
|
|
60
|
+
datameshUI.url = datameshUiUrl;
|
|
61
|
+
}
|
|
53
62
|
};
|
|
54
63
|
//Try to get the datamesh token from the envars
|
|
55
64
|
Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)])
|
|
@@ -67,14 +76,11 @@ export const datamesh_connect_extension = {
|
|
|
67
76
|
};
|
|
68
77
|
const openDatameshUI = (event) => {
|
|
69
78
|
const widgetId = 'datamesh-ui';
|
|
70
|
-
|
|
71
|
-
return widget.id === widgetId;
|
|
72
|
-
});
|
|
73
|
-
if (openWidget) {
|
|
79
|
+
if (findDatameshUI()) {
|
|
74
80
|
app.shell.activateById(widgetId);
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
77
|
-
const datameshUIWidget = new DatameshUI();
|
|
83
|
+
const datameshUIWidget = new DatameshUI(datameshUiUrl);
|
|
78
84
|
datameshUIWidget.title.label = 'Oceanum Datamesh';
|
|
79
85
|
datameshUIWidget.id = widgetId;
|
|
80
86
|
datameshUIWidget.title.closable = true;
|
|
@@ -87,6 +93,8 @@ export const datamesh_connect_extension = {
|
|
|
87
93
|
name: 'Datamesh Connect',
|
|
88
94
|
icon: oceanumIcon,
|
|
89
95
|
openDatameshUI: openDatameshUI,
|
|
96
|
+
datameshUiUrl: () => datameshUiUrl,
|
|
97
|
+
datameshUiFrame: () => { var _a, _b; return (_b = (_a = findDatameshUI()) === null || _a === void 0 ? void 0 : _a.frame) !== null && _b !== void 0 ? _b : null; },
|
|
90
98
|
commands: app.commands,
|
|
91
99
|
getCurrentWidget
|
|
92
100
|
});
|
package/package.json
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
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
|
+
},
|
|
21
27
|
"autoRunCode": {
|
|
22
28
|
"type": "boolean",
|
|
23
29
|
"title": "Auto-run code",
|
package/src/DatameshUI.tsx
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
|
-
import {
|
|
3
|
+
import { datameshUiSrc } from './datameshUiUrl';
|
|
4
4
|
|
|
5
5
|
export class DatameshUI extends ReactWidget {
|
|
6
|
+
/** `url` is the `datameshUiUrl` setting. */
|
|
7
|
+
constructor(private _url: string) {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
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
|
+
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
21
|
+
get frame(): Window | null {
|
|
22
|
+
return this.node.querySelector('iframe')?.contentWindow ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
render(): React.ReactElement {
|
|
7
26
|
return (
|
|
8
27
|
<iframe
|
|
9
|
-
src={
|
|
28
|
+
src={datameshUiSrc(this._url)}
|
|
10
29
|
className="datamesh-iframe"
|
|
11
30
|
></iframe>
|
|
12
31
|
);
|
package/src/DatameshWidget.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { marked } from 'marked';
|
|
|
22
22
|
|
|
23
23
|
import { DatasourceItem } from './DatasourceItem';
|
|
24
24
|
import { OCEANUM_AI_BACKEND_URL } from './constants';
|
|
25
|
+
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
25
26
|
import { describeProgress, onProgress, type Progress } from './progress';
|
|
26
27
|
|
|
27
28
|
// Configure marked for safe rendering
|
|
@@ -813,6 +814,10 @@ export interface IDatameshWidgetProps {
|
|
|
813
814
|
name: string;
|
|
814
815
|
icon: LabIcon;
|
|
815
816
|
openDatameshUI: any;
|
|
817
|
+
/** The `datameshUiUrl` setting. */
|
|
818
|
+
datameshUiUrl: () => string;
|
|
819
|
+
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
820
|
+
datameshUiFrame: () => Window | null;
|
|
816
821
|
commands: CommandRegistry;
|
|
817
822
|
getCurrentWidget: () => Widget;
|
|
818
823
|
}
|
|
@@ -841,6 +846,17 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
841
846
|
}
|
|
842
847
|
|
|
843
848
|
receiveIFrameMessage(event: MessageEvent): void {
|
|
849
|
+
// Any page or frame can post to this window; only the Datamesh UI panel
|
|
850
|
+
// may change the workspace.
|
|
851
|
+
if (
|
|
852
|
+
!isDatameshUiMessage(
|
|
853
|
+
event,
|
|
854
|
+
this.props.datameshUiUrl(),
|
|
855
|
+
this.props.datameshUiFrame()
|
|
856
|
+
)
|
|
857
|
+
) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
844
860
|
if (event.data && event.data.action === 'workspace-modify') {
|
|
845
861
|
this.datameshWorkspaceSpec = {
|
|
846
862
|
id: event.data.id,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import schema from '../../schema/datamesh-connect.json';
|
|
2
|
+
import { DATAMESH_UI_SERVICE } from '../constants';
|
|
3
|
+
import {
|
|
4
|
+
datameshUiSrc,
|
|
5
|
+
isDatameshUiMessage,
|
|
6
|
+
validDatameshUiUrl
|
|
7
|
+
} from '../datameshUiUrl';
|
|
8
|
+
|
|
9
|
+
describe('validDatameshUiUrl', () => {
|
|
10
|
+
it('is the default the settings schema offers', () => {
|
|
11
|
+
expect(schema.properties.datameshUiUrl.default).toBe(DATAMESH_UI_SERVICE);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('takes an http(s) address as it is', () => {
|
|
15
|
+
expect(validDatameshUiUrl('https://ui.datamesh.oceanum.tech').href).toBe(
|
|
16
|
+
'https://ui.datamesh.oceanum.tech/'
|
|
17
|
+
);
|
|
18
|
+
expect(validDatameshUiUrl('http://localhost:3000').href).toBe(
|
|
19
|
+
'http://localhost:3000/'
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
['empty', ''],
|
|
25
|
+
['blank', ' '],
|
|
26
|
+
['missing', undefined],
|
|
27
|
+
['null', null],
|
|
28
|
+
['not a URL', 'not a url'],
|
|
29
|
+
['a relative path', '/datamesh'],
|
|
30
|
+
['a javascript: URL', 'javascript:alert(1)'],
|
|
31
|
+
['a data: URL', 'data:text/html,<script>alert(1)</script>'],
|
|
32
|
+
['a file: URL', 'file:///etc/passwd']
|
|
33
|
+
])('falls back to the default for %s', (_, url) => {
|
|
34
|
+
expect(validDatameshUiUrl(url).href).toBe(`${DATAMESH_UI_SERVICE}/`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('datameshUiSrc', () => {
|
|
39
|
+
it('opens the default Datamesh UI in embed mode', () => {
|
|
40
|
+
expect(datameshUiSrc(DATAMESH_UI_SERVICE)).toBe(
|
|
41
|
+
'https://ui.datamesh.oceanum.io/?embed=1'
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps the query and fragment a custom address already has', () => {
|
|
46
|
+
const src = new URL(
|
|
47
|
+
datameshUiSrc('https://ui.datamesh.oceanum.tech/app?workspace=abc#top')
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(src.origin).toBe('https://ui.datamesh.oceanum.tech');
|
|
51
|
+
expect(src.pathname).toBe('/app');
|
|
52
|
+
expect(src.searchParams.get('workspace')).toBe('abc');
|
|
53
|
+
expect(src.searchParams.get('embed')).toBe('1');
|
|
54
|
+
expect(src.hash).toBe('#top');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not add embed twice', () => {
|
|
58
|
+
const src = new URL(
|
|
59
|
+
datameshUiSrc('https://ui.datamesh.oceanum.io?embed=0')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(src.searchParams.getAll('embed')).toEqual(['1']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('opens the default for an invalid address', () => {
|
|
66
|
+
expect(datameshUiSrc('not a url')).toBe(
|
|
67
|
+
'https://ui.datamesh.oceanum.io/?embed=1'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('never puts a javascript: URL in the iframe', () => {
|
|
72
|
+
expect(datameshUiSrc('javascript:alert(1)')).toBe(
|
|
73
|
+
'https://ui.datamesh.oceanum.io/?embed=1'
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isDatameshUiMessage', () => {
|
|
79
|
+
const frame = {} as Window;
|
|
80
|
+
const other = {} as Window;
|
|
81
|
+
|
|
82
|
+
it('accepts a message from the panel at the configured origin', () => {
|
|
83
|
+
expect(
|
|
84
|
+
isDatameshUiMessage(
|
|
85
|
+
{ origin: 'https://ui.datamesh.oceanum.io', source: frame },
|
|
86
|
+
DATAMESH_UI_SERVICE,
|
|
87
|
+
frame
|
|
88
|
+
)
|
|
89
|
+
).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('compares origins, not whole addresses', () => {
|
|
93
|
+
expect(
|
|
94
|
+
isDatameshUiMessage(
|
|
95
|
+
{ origin: 'https://ui.datamesh.oceanum.tech', source: frame },
|
|
96
|
+
'https://ui.datamesh.oceanum.tech/app?workspace=abc',
|
|
97
|
+
frame
|
|
98
|
+
)
|
|
99
|
+
).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
['another site', 'https://evil.example'],
|
|
104
|
+
['the production UI when a dev one is configured', DATAMESH_UI_SERVICE],
|
|
105
|
+
['another port', 'https://ui.datamesh.oceanum.tech:8443'],
|
|
106
|
+
['plain http', 'http://ui.datamesh.oceanum.tech'],
|
|
107
|
+
['an opaque origin', 'null']
|
|
108
|
+
])('rejects a message from %s', (_, origin) => {
|
|
109
|
+
expect(
|
|
110
|
+
isDatameshUiMessage(
|
|
111
|
+
{ origin, source: frame },
|
|
112
|
+
'https://ui.datamesh.oceanum.tech',
|
|
113
|
+
frame
|
|
114
|
+
)
|
|
115
|
+
).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('checks the origin against the default for an invalid setting', () => {
|
|
119
|
+
expect(
|
|
120
|
+
isDatameshUiMessage(
|
|
121
|
+
{ origin: 'https://ui.datamesh.oceanum.io', source: frame },
|
|
122
|
+
'javascript:alert(1)',
|
|
123
|
+
frame
|
|
124
|
+
)
|
|
125
|
+
).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
isDatameshUiMessage(
|
|
128
|
+
{ origin: 'null', source: frame },
|
|
129
|
+
'javascript:alert(1)',
|
|
130
|
+
frame
|
|
131
|
+
)
|
|
132
|
+
).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects the right origin from another window', () => {
|
|
136
|
+
expect(
|
|
137
|
+
isDatameshUiMessage(
|
|
138
|
+
{ origin: 'https://ui.datamesh.oceanum.io', source: other },
|
|
139
|
+
DATAMESH_UI_SERVICE,
|
|
140
|
+
frame
|
|
141
|
+
)
|
|
142
|
+
).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('rejects every message while the panel is closed', () => {
|
|
146
|
+
expect(
|
|
147
|
+
isDatameshUiMessage(
|
|
148
|
+
{ origin: 'https://ui.datamesh.oceanum.io', source: null },
|
|
149
|
+
DATAMESH_UI_SERVICE,
|
|
150
|
+
null
|
|
151
|
+
)
|
|
152
|
+
).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Datamesh UI panel's address, and which messages may come from it.
|
|
3
|
+
*
|
|
4
|
+
* The address is a setting (`datameshUiUrl`), so a deployment can point the
|
|
5
|
+
* panel at another Datamesh UI -- a dev one, say. A setting is typed in by
|
|
6
|
+
* hand, so anything that is not an http(s) URL falls back to the default
|
|
7
|
+
* rather than ending up in an iframe's `src`.
|
|
8
|
+
*/
|
|
9
|
+
import { DATAMESH_UI_SERVICE } from './constants';
|
|
10
|
+
|
|
11
|
+
/** The Datamesh UI address to use for a `datameshUiUrl` setting value. */
|
|
12
|
+
export function validDatameshUiUrl(url: string | null | undefined): URL {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL((url ?? '').trim());
|
|
15
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// Empty or not a URL: the default below.
|
|
20
|
+
}
|
|
21
|
+
return new URL(DATAMESH_UI_SERVICE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The panel's iframe `src`: the Datamesh UI in embed mode, which hides the
|
|
26
|
+
* Oceanum.io navigator. Any query the setting already has is kept.
|
|
27
|
+
*/
|
|
28
|
+
export function datameshUiSrc(url: string | null | undefined): string {
|
|
29
|
+
const src = validDatameshUiUrl(url);
|
|
30
|
+
src.searchParams.set('embed', '1');
|
|
31
|
+
return src.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a `message` event was posted by the Datamesh UI panel.
|
|
36
|
+
*
|
|
37
|
+
* Any page or frame can post to this window, so a message counts only when it
|
|
38
|
+
* comes from the panel's iframe (`frame`, its `contentWindow`; `null` when the
|
|
39
|
+
* panel is not open) and from the configured Datamesh UI's origin.
|
|
40
|
+
*/
|
|
41
|
+
export function isDatameshUiMessage(
|
|
42
|
+
event: Pick<MessageEvent, 'origin' | 'source'>,
|
|
43
|
+
url: string | null | undefined,
|
|
44
|
+
frame: Window | null
|
|
45
|
+
): boolean {
|
|
46
|
+
return (
|
|
47
|
+
!!frame &&
|
|
48
|
+
event.source === frame &&
|
|
49
|
+
event.origin === validDatameshUiUrl(url).origin
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
|
21
21
|
import { notebookHost } from './notebookHost';
|
|
22
22
|
import { KernelHandoff } from './kernelHandoff';
|
|
23
23
|
import { runChatLoop, STOPPED } from './aiLoop';
|
|
24
|
-
import { MAX_OBSERVE_ROUNDS } from './constants';
|
|
24
|
+
import { DATAMESH_UI_SERVICE, MAX_OBSERVE_ROUNDS } from './constants';
|
|
25
25
|
|
|
26
26
|
import '../style/index.css';
|
|
27
27
|
|
|
@@ -57,6 +57,15 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
57
57
|
) => {
|
|
58
58
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
59
59
|
|
|
60
|
+
// The `datameshUiUrl` setting; the default until the settings load.
|
|
61
|
+
let datameshUiUrl = DATAMESH_UI_SERVICE;
|
|
62
|
+
|
|
63
|
+
const findDatameshUI = (): DatameshUI | undefined =>
|
|
64
|
+
find(
|
|
65
|
+
app.shell.widgets('main'),
|
|
66
|
+
(widget: Widget) => widget.id === 'datamesh-ui'
|
|
67
|
+
) as DatameshUI | undefined;
|
|
68
|
+
|
|
60
69
|
//Try to get the datamesh token from the settings
|
|
61
70
|
const updateSettings = (set: ISettingRegistry.ISettings) => {
|
|
62
71
|
const token = set.get('datameshToken');
|
|
@@ -68,6 +77,12 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
68
77
|
}).then(res => console.log(res));
|
|
69
78
|
}
|
|
70
79
|
window.injectToken = set.get('injectToken').user as boolean;
|
|
80
|
+
// An open Datamesh UI panel follows a change of address.
|
|
81
|
+
datameshUiUrl = set.get('datameshUiUrl').composite as string;
|
|
82
|
+
const datameshUI = findDatameshUI();
|
|
83
|
+
if (datameshUI) {
|
|
84
|
+
datameshUI.url = datameshUiUrl;
|
|
85
|
+
}
|
|
71
86
|
};
|
|
72
87
|
//Try to get the datamesh token from the envars
|
|
73
88
|
|
|
@@ -91,18 +106,12 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
91
106
|
|
|
92
107
|
const openDatameshUI = (event: any): void => {
|
|
93
108
|
const widgetId = 'datamesh-ui';
|
|
94
|
-
|
|
95
|
-
app.shell.widgets('main'),
|
|
96
|
-
(widget: Widget, index: number) => {
|
|
97
|
-
return widget.id === widgetId;
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
if (openWidget) {
|
|
109
|
+
if (findDatameshUI()) {
|
|
101
110
|
app.shell.activateById(widgetId);
|
|
102
111
|
return;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
const datameshUIWidget = new DatameshUI();
|
|
114
|
+
const datameshUIWidget = new DatameshUI(datameshUiUrl);
|
|
106
115
|
datameshUIWidget.title.label = 'Oceanum Datamesh';
|
|
107
116
|
datameshUIWidget.id = widgetId;
|
|
108
117
|
datameshUIWidget.title.closable = true;
|
|
@@ -116,6 +125,8 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
116
125
|
name: 'Datamesh Connect',
|
|
117
126
|
icon: oceanumIcon,
|
|
118
127
|
openDatameshUI: openDatameshUI,
|
|
128
|
+
datameshUiUrl: () => datameshUiUrl,
|
|
129
|
+
datameshUiFrame: () => findDatameshUI()?.frame ?? null,
|
|
119
130
|
commands: app.commands,
|
|
120
131
|
getCurrentWidget
|
|
121
132
|
});
|