@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.
@@ -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 { DATAMESH_UI_SERVICE } from './constants';
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: `${DATAMESH_UI_SERVICE}`, className: "datamesh-iframe" }));
24
+ return (React.createElement("iframe", { src: datameshUiSrc(this._url), className: "datamesh-iframe" }));
7
25
  }
8
26
  }
@@ -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
  }
@@ -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
- const openWidget = find(app.shell.widgets('main'), (widget, index) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oceanum/oceanumlab",
3
- "version": "4.6.1",
3
+ "version": "4.6.2",
4
4
  "description": "A Jupyterlab extension to interact with the Oceanum.io platform",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -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",
@@ -1,12 +1,31 @@
1
1
  import React from 'react';
2
2
  import { ReactWidget } from '@jupyterlab/apputils';
3
- import { DATAMESH_UI_SERVICE } from './constants';
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={`${DATAMESH_UI_SERVICE}`}
28
+ src={datameshUiSrc(this._url)}
10
29
  className="datamesh-iframe"
11
30
  ></iframe>
12
31
  );
@@ -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
- const openWidget = find(
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
  });