@datalayer/core 1.2.10 → 1.2.12
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 +2 -2
- package/lib/api/iam/authentication.js +6 -4
- package/lib/api/iam/healthz.js +2 -2
- package/lib/api/iam/oauth2.js +18 -12
- package/lib/api/iam/secrets.js +1 -2
- package/lib/api/mcp/observability.d.ts +9 -2
- package/lib/api/mcp/observability.js +40 -15
- package/lib/client/auth/strategies.js +1 -1
- package/lib/client/mixins/IAMMixin.js +2 -2
- package/lib/components/checkout/StripeCheckout.d.ts +1 -1
- package/lib/components/checkout/StripeCheckout.js +1 -1
- package/lib/components/display/VisuallyHidden.d.ts +1 -2
- package/lib/components/sharing/sandboxSharing.js +7 -1
- package/lib/config/Configuration.d.ts +4 -4
- package/lib/config/Configuration.js +7 -7
- package/lib/config/index.d.ts +1 -0
- package/lib/config/index.js +1 -0
- package/lib/config/planes.d.ts +45 -0
- package/lib/config/planes.js +53 -0
- package/lib/hooks/useBackdrop.d.ts +4 -5
- package/lib/hooks/useCache.d.ts +29 -19
- package/lib/hooks/useCache.js +164 -189
- package/lib/hooks/useMcp.js +1 -1
- package/lib/hooks/useNavigate.js +10 -0
- package/lib/hooks/useWindowSize.js +1 -1
- package/lib/models/IAMProvidersSpecs.d.ts +10 -5
- package/lib/models/IAMProvidersSpecs.js +0 -23
- package/lib/navigation/adapters/react-router.d.ts +1 -1
- package/lib/navigation/adapters/react-router.js +3 -0
- package/lib/state/sessionEnd.js +1 -1
- package/lib/state/substates/CoreState.js +3 -16
- package/lib/state/substates/IAMState.d.ts +3 -3
- package/lib/state/substates/NavigationState.d.ts +82 -0
- package/lib/state/substates/NavigationState.js +255 -0
- package/lib/state/substates/index.d.ts +1 -0
- package/lib/state/substates/index.js +1 -0
- package/lib/utils/Screencapture.d.ts +15 -0
- package/lib/utils/Screencapture.js +26 -23
- package/lib/views/mcp/McpHome.js +1 -1
- package/package.json +6 -4
- package/scripts/generate-mcp-types.py +15 -9
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** The signed-in shell: the platform, the business cases, or the admin. */
|
|
2
|
+
export type NavigationView = 'home' | 'agentify' | 'admin';
|
|
3
|
+
/** The shells, and where each one opens when it has not been visited. */
|
|
4
|
+
export declare const NAVIGATION_VIEW_HOMES: Record<NavigationView, string>;
|
|
5
|
+
/** Well-known view identifiers for {@link NavigationState.tabsByView}. */
|
|
6
|
+
export declare const LIBRARY_VIEW = "library";
|
|
7
|
+
export declare const AGENTS_VIEW = "agents";
|
|
8
|
+
export declare const SETTINGS_VIEW = "settings";
|
|
9
|
+
export declare const DATALAYER_VIEW = "datalayer";
|
|
10
|
+
export declare const CONTENTS_VIEW = "contents";
|
|
11
|
+
export declare const TUTOR_VIEW = "tutor";
|
|
12
|
+
export type NavigationState = {
|
|
13
|
+
/** The shell the reader last chose. */
|
|
14
|
+
view: NavigationView;
|
|
15
|
+
/** State which shell is being shown. */
|
|
16
|
+
setView: (view: NavigationView) => void;
|
|
17
|
+
/** Swap the platform and the business cases. */
|
|
18
|
+
toggleView: () => void;
|
|
19
|
+
/** The page each shell was last on. */
|
|
20
|
+
lastRouteByView: Partial<Record<NavigationView, string>>;
|
|
21
|
+
/**
|
|
22
|
+
* Remember a page as the current shell's, or as another's when told.
|
|
23
|
+
*
|
|
24
|
+
* Called by {@link useNavigate} for every navigation, which is what makes
|
|
25
|
+
* this the one place the answer comes from.
|
|
26
|
+
*/
|
|
27
|
+
rememberRoute: (route: string, view?: NavigationView) => void;
|
|
28
|
+
/** Where a shell was left, or where it opens when it never was. */
|
|
29
|
+
routeForView: (view: NavigationView) => string;
|
|
30
|
+
/** Last selected tab, by view identifier. */
|
|
31
|
+
tabsByView: Record<string, string>;
|
|
32
|
+
/** Keep the tab a view was left on. */
|
|
33
|
+
setViewTab: (view: string, tab: string) => void;
|
|
34
|
+
/** The tab a view was left on, if any. */
|
|
35
|
+
getViewTab: (view: string) => string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The route a signed-out reader asked for, to resume after signing in.
|
|
38
|
+
*
|
|
39
|
+
* Never persisted: a reload must land on the default route, and the OAuth
|
|
40
|
+
* round trip carries its target in `post_auth_redirect` instead.
|
|
41
|
+
*/
|
|
42
|
+
requestedRoute?: string;
|
|
43
|
+
setRequestedRoute: (route: string) => void;
|
|
44
|
+
clearRequestedRoute: () => void;
|
|
45
|
+
/** The address the reader was on before the documentation. */
|
|
46
|
+
docsOrigin?: string;
|
|
47
|
+
setDocsOrigin: (origin: string) => void;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Whether a page is worth remembering as a shell's.
|
|
51
|
+
*
|
|
52
|
+
* Not the sign-in pages, the OAuth callbacks or the documentation: coming
|
|
53
|
+
* back to a shell should land on the work, not on the door the reader came
|
|
54
|
+
* through or on a page that belongs to no shell.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isRememberableRoute(route: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The shell a route belongs to, read off its first segment.
|
|
59
|
+
*
|
|
60
|
+
* The store used to trust whatever `view` said when a route was recorded,
|
|
61
|
+
* and `view` was only ever written by the header toggle. A reader who
|
|
62
|
+
* reached Home through a sidebar link while the toggle still said Agentify
|
|
63
|
+
* had their Home pages filed under Agentify — and the toggle, seeing
|
|
64
|
+
* "Agentify" already chosen, did nothing when they clicked it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function viewForRoute(route: string): NavigationView;
|
|
67
|
+
export declare const navigationStore: import("zustand").StoreApi<NavigationState>;
|
|
68
|
+
/**
|
|
69
|
+
* The hook, shaped like a `zustand` store created with `create`: callable
|
|
70
|
+
* with or without a selector, and carrying `getState`, `setState` and
|
|
71
|
+
* `subscribe` for the callers — a router guard, a test — that read it outside
|
|
72
|
+
* a render.
|
|
73
|
+
*/
|
|
74
|
+
export interface UseNavigationStore {
|
|
75
|
+
(): NavigationState;
|
|
76
|
+
<T>(selector: (state: NavigationState) => T): T;
|
|
77
|
+
getState: () => NavigationState;
|
|
78
|
+
setState: typeof navigationStore.setState;
|
|
79
|
+
subscribe: typeof navigationStore.subscribe;
|
|
80
|
+
}
|
|
81
|
+
export declare const useNavigationStore: UseNavigationStore;
|
|
82
|
+
export default useNavigationStore;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Where the reader is, where they were, and where they left each shell.
|
|
7
|
+
*
|
|
8
|
+
* One store for navigation, in core, so every surface reads and writes the
|
|
9
|
+
* same thing. It answers five questions that used to be four stores in the
|
|
10
|
+
* landing and a handful of ad-hoc effects:
|
|
11
|
+
*
|
|
12
|
+
* - **the shell** — the platform Home, Agentify, or Admin;
|
|
13
|
+
* - **the page each shell was left on**, so switching shells returns the
|
|
14
|
+
* reader to their work rather than to a landing page;
|
|
15
|
+
* {@link useNavigate} records it on every navigation;
|
|
16
|
+
* - **the tab** last chosen in each tabbed view;
|
|
17
|
+
* - **the route asked for** before signing in, to resume afterwards;
|
|
18
|
+
* - **the address before the documentation**, for its way back.
|
|
19
|
+
*
|
|
20
|
+
* **Where each is kept.** The shell rides in a cookie, so it is sent with
|
|
21
|
+
* every request and a server render can read it. The shells' last pages and
|
|
22
|
+
* the tabs are in `localStorage`, which a cookie should not carry. The
|
|
23
|
+
* requested route and the documentation's origin are memory only, on
|
|
24
|
+
* purpose: a reload must forget the route asked for before signing in.
|
|
25
|
+
*
|
|
26
|
+
* Every read and write goes through the storage in a `try`: a browser that
|
|
27
|
+
* refuses its storage forgets, and never throws into a render.
|
|
28
|
+
*
|
|
29
|
+
* @module state/substates/NavigationState
|
|
30
|
+
*/
|
|
31
|
+
import { useStore } from 'zustand';
|
|
32
|
+
import { createStore } from 'zustand/vanilla';
|
|
33
|
+
import { registerSessionState } from '../sessionEnd.js';
|
|
34
|
+
/** The shells, and where each one opens when it has not been visited. */
|
|
35
|
+
export const NAVIGATION_VIEW_HOMES = {
|
|
36
|
+
home: '/',
|
|
37
|
+
agentify: '/agentify',
|
|
38
|
+
admin: '/admin',
|
|
39
|
+
};
|
|
40
|
+
/** Well-known view identifiers for {@link NavigationState.tabsByView}. */
|
|
41
|
+
export const LIBRARY_VIEW = 'library';
|
|
42
|
+
export const AGENTS_VIEW = 'agents';
|
|
43
|
+
export const SETTINGS_VIEW = 'settings';
|
|
44
|
+
export const DATALAYER_VIEW = 'datalayer';
|
|
45
|
+
export const CONTENTS_VIEW = 'contents';
|
|
46
|
+
export const TUTOR_VIEW = 'tutor';
|
|
47
|
+
/** Where this store is kept, and what the landing kept it under before. */
|
|
48
|
+
const STORAGE_KEY = 'datalayer-navigation';
|
|
49
|
+
const SHELL_COOKIE = 'datalayer-signed-in-view';
|
|
50
|
+
const FORMER_TABS_KEY = 'datalayer_view_tabs';
|
|
51
|
+
/** Days a remembered shell lives in the cookie. */
|
|
52
|
+
const COOKIE_MAX_AGE_DAYS = 365;
|
|
53
|
+
const isView = (value) => value === 'home' || value === 'agentify' || value === 'admin';
|
|
54
|
+
function readCookie(name) {
|
|
55
|
+
if (typeof document === 'undefined') {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const match = document.cookie
|
|
59
|
+
.split('; ')
|
|
60
|
+
.find(row => row.startsWith(`${name}=`));
|
|
61
|
+
return match ? decodeURIComponent(match.slice(name.length + 1)) : undefined;
|
|
62
|
+
}
|
|
63
|
+
function writeCookie(name, value) {
|
|
64
|
+
if (typeof document === 'undefined') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
|
|
68
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
|
69
|
+
}
|
|
70
|
+
function readLocal(name) {
|
|
71
|
+
try {
|
|
72
|
+
return globalThis.localStorage?.getItem(name) ?? null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What the last visit left, from the two places it belongs in.
|
|
80
|
+
*
|
|
81
|
+
* The shell is taken from the cookie it has always used, so a reader who
|
|
82
|
+
* chose Agentify before this store existed still finds it. The tabs kept
|
|
83
|
+
* under the landing's former key are picked up once.
|
|
84
|
+
*/
|
|
85
|
+
function restore() {
|
|
86
|
+
let persisted = {};
|
|
87
|
+
const stored = readLocal(STORAGE_KEY);
|
|
88
|
+
if (stored) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(stored);
|
|
91
|
+
// The landing kept this under the same key through zustand's
|
|
92
|
+
// `persist`, which wraps what it writes in `{ state, version }`.
|
|
93
|
+
persisted = parsed?.state ?? parsed ?? {};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
persisted = {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const former = readLocal(FORMER_TABS_KEY);
|
|
101
|
+
if (former) {
|
|
102
|
+
try {
|
|
103
|
+
const { state } = JSON.parse(former);
|
|
104
|
+
persisted = { tabsByView: state?.tabsByView ?? {} };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
persisted = {};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const shell = readCookie(SHELL_COOKIE);
|
|
112
|
+
if (isView(shell)) {
|
|
113
|
+
persisted.view = shell;
|
|
114
|
+
}
|
|
115
|
+
return persisted;
|
|
116
|
+
}
|
|
117
|
+
function persist(state) {
|
|
118
|
+
const written = {
|
|
119
|
+
view: state.view,
|
|
120
|
+
lastRouteByView: state.lastRouteByView,
|
|
121
|
+
tabsByView: state.tabsByView,
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(written));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* the shells' pages are not remembered */
|
|
128
|
+
}
|
|
129
|
+
writeCookie(SHELL_COOKIE, state.view);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether a page is worth remembering as a shell's.
|
|
133
|
+
*
|
|
134
|
+
* Not the sign-in pages, the OAuth callbacks or the documentation: coming
|
|
135
|
+
* back to a shell should land on the work, not on the door the reader came
|
|
136
|
+
* through or on a page that belongs to no shell.
|
|
137
|
+
*/
|
|
138
|
+
export function isRememberableRoute(route) {
|
|
139
|
+
if (!route.startsWith('/')) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const path = route.split('?')[0];
|
|
143
|
+
return ![
|
|
144
|
+
'/signin',
|
|
145
|
+
'/signup',
|
|
146
|
+
'/login',
|
|
147
|
+
'/logout',
|
|
148
|
+
'/oauth2',
|
|
149
|
+
'/oauth',
|
|
150
|
+
'/docs',
|
|
151
|
+
'/join',
|
|
152
|
+
].some(prefix => path === prefix || path.startsWith(`${prefix}/`));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The shell a route belongs to, read off its first segment.
|
|
156
|
+
*
|
|
157
|
+
* The store used to trust whatever `view` said when a route was recorded,
|
|
158
|
+
* and `view` was only ever written by the header toggle. A reader who
|
|
159
|
+
* reached Home through a sidebar link while the toggle still said Agentify
|
|
160
|
+
* had their Home pages filed under Agentify — and the toggle, seeing
|
|
161
|
+
* "Agentify" already chosen, did nothing when they clicked it.
|
|
162
|
+
*/
|
|
163
|
+
export function viewForRoute(route) {
|
|
164
|
+
const path = route.split('?')[0];
|
|
165
|
+
if (path === '/agentify' || path.startsWith('/agentify/')) {
|
|
166
|
+
return 'agentify';
|
|
167
|
+
}
|
|
168
|
+
if (path === '/admin' || path.startsWith('/admin/')) {
|
|
169
|
+
return 'admin';
|
|
170
|
+
}
|
|
171
|
+
return 'home';
|
|
172
|
+
}
|
|
173
|
+
const initial = restore();
|
|
174
|
+
export const navigationStore = createStore((set, get) => ({
|
|
175
|
+
view: initial.view ?? 'home',
|
|
176
|
+
lastRouteByView: initial.lastRouteByView ?? {},
|
|
177
|
+
tabsByView: initial.tabsByView ?? {},
|
|
178
|
+
requestedRoute: undefined,
|
|
179
|
+
docsOrigin: undefined,
|
|
180
|
+
setView: view => {
|
|
181
|
+
set({ view });
|
|
182
|
+
persist(get());
|
|
183
|
+
},
|
|
184
|
+
toggleView: () => {
|
|
185
|
+
set(state => ({ view: state.view === 'home' ? 'agentify' : 'home' }));
|
|
186
|
+
persist(get());
|
|
187
|
+
},
|
|
188
|
+
rememberRoute: (route, view) => {
|
|
189
|
+
if (!isRememberableRoute(route)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// The route says which shell it belongs to; the caller may only insist
|
|
193
|
+
// when it knows better. And being on a shell's page *is* being in that
|
|
194
|
+
// shell, so the view follows.
|
|
195
|
+
const shell = view ?? viewForRoute(route);
|
|
196
|
+
// Told which shell, the caller is filing a page away, not going there.
|
|
197
|
+
const follow = view === undefined;
|
|
198
|
+
const current = get();
|
|
199
|
+
if (current.lastRouteByView[shell] === route &&
|
|
200
|
+
(!follow || current.view === shell)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
set(state => ({
|
|
204
|
+
view: follow ? shell : state.view,
|
|
205
|
+
lastRouteByView: { ...state.lastRouteByView, [shell]: route },
|
|
206
|
+
}));
|
|
207
|
+
persist(get());
|
|
208
|
+
},
|
|
209
|
+
routeForView: view => {
|
|
210
|
+
// Only a page of that shell: an earlier version filed pages under the
|
|
211
|
+
// shell the toggle named rather than the one the route belonged to, so a
|
|
212
|
+
// reader can still have `/items` written down as Agentify's page. Going
|
|
213
|
+
// "to Agentify" must never land on a Home page.
|
|
214
|
+
const remembered = get().lastRouteByView[view];
|
|
215
|
+
return remembered && viewForRoute(remembered) === view
|
|
216
|
+
? remembered
|
|
217
|
+
: NAVIGATION_VIEW_HOMES[view];
|
|
218
|
+
},
|
|
219
|
+
setViewTab: (view, tab) => {
|
|
220
|
+
if (get().tabsByView[view] === tab) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
set(state => ({ tabsByView: { ...state.tabsByView, [view]: tab } }));
|
|
224
|
+
persist(get());
|
|
225
|
+
},
|
|
226
|
+
getViewTab: view => get().tabsByView[view],
|
|
227
|
+
setRequestedRoute: route => set({ requestedRoute: route }),
|
|
228
|
+
clearRequestedRoute: () => set({ requestedRoute: undefined }),
|
|
229
|
+
setDocsOrigin: origin => set({ docsOrigin: origin }),
|
|
230
|
+
}));
|
|
231
|
+
// The pages each shell was left on, the tabs, the route asked for and the
|
|
232
|
+
// way back from the documentation belong to the person who was signed in:
|
|
233
|
+
// forgotten when the session ends. The shell itself stays — it is the
|
|
234
|
+
// browser's choice of surface, read by the anonymous landing too.
|
|
235
|
+
registerSessionState({
|
|
236
|
+
forget: () => {
|
|
237
|
+
navigationStore.setState({
|
|
238
|
+
lastRouteByView: {},
|
|
239
|
+
tabsByView: {},
|
|
240
|
+
requestedRoute: undefined,
|
|
241
|
+
docsOrigin: undefined,
|
|
242
|
+
});
|
|
243
|
+
persist(navigationStore.getState());
|
|
244
|
+
},
|
|
245
|
+
localStorageKeys: [FORMER_TABS_KEY],
|
|
246
|
+
});
|
|
247
|
+
function useNavigationState(selector) {
|
|
248
|
+
return useStore(navigationStore, selector ?? ((state) => state));
|
|
249
|
+
}
|
|
250
|
+
export const useNavigationStore = Object.assign(useNavigationState, {
|
|
251
|
+
getState: navigationStore.getState,
|
|
252
|
+
setState: navigationStore.setState,
|
|
253
|
+
subscribe: navigationStore.subscribe,
|
|
254
|
+
});
|
|
255
|
+
export default useNavigationStore;
|
|
@@ -1 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A picture of one rendered node, as a PNG data URL.
|
|
3
|
+
*
|
|
4
|
+
* This used `html2canvas`, which does not capture a node: it clones the
|
|
5
|
+
* **whole document** into a hidden iframe and paints all of it, then crops.
|
|
6
|
+
* On a rendered notebook that is every cell, every editor and every output,
|
|
7
|
+
* and the clone is synchronous — the page froze for ten seconds or more with
|
|
8
|
+
* the share dialog saying "preparing…", which reads as a hang, and on a
|
|
9
|
+
* longer notebook it was one. `html-to-image` serialises the node's own
|
|
10
|
+
* subtree into an SVG `foreignObject` and rasterises that: the work is
|
|
11
|
+
* proportional to the output being shared, and it yields to the page.
|
|
12
|
+
*
|
|
13
|
+
* Cross-origin pictures are fetched for the clone, as before, so a cell
|
|
14
|
+
* carrying an `<img>` from the web does not capture as a blank rectangle.
|
|
15
|
+
*/
|
|
1
16
|
export declare const takeHTMLNodeScreencapture: (node: HTMLDivElement) => Promise<string>;
|
|
@@ -2,29 +2,32 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { toPng } from 'html-to-image';
|
|
6
|
+
/**
|
|
7
|
+
* A picture of one rendered node, as a PNG data URL.
|
|
8
|
+
*
|
|
9
|
+
* This used `html2canvas`, which does not capture a node: it clones the
|
|
10
|
+
* **whole document** into a hidden iframe and paints all of it, then crops.
|
|
11
|
+
* On a rendered notebook that is every cell, every editor and every output,
|
|
12
|
+
* and the clone is synchronous — the page froze for ten seconds or more with
|
|
13
|
+
* the share dialog saying "preparing…", which reads as a hang, and on a
|
|
14
|
+
* longer notebook it was one. `html-to-image` serialises the node's own
|
|
15
|
+
* subtree into an SVG `foreignObject` and rasterises that: the work is
|
|
16
|
+
* proportional to the output being shared, and it yields to the page.
|
|
17
|
+
*
|
|
18
|
+
* Cross-origin pictures are fetched for the clone, as before, so a cell
|
|
19
|
+
* carrying an `<img>` from the web does not capture as a blank rectangle.
|
|
20
|
+
*/
|
|
6
21
|
export const takeHTMLNodeScreencapture = async (node) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const top = 0;
|
|
18
|
-
const left = 0;
|
|
19
|
-
const width = canvas.width;
|
|
20
|
-
const height = canvas.height;
|
|
21
|
-
croppedCanvas.width = width;
|
|
22
|
-
croppedCanvas.height = height;
|
|
23
|
-
croppedCanvasContext?.drawImage(canvas, left, top, width, height, left, top, width, height);
|
|
24
|
-
// const type = 'image/jpeg';
|
|
25
|
-
const type = 'image/png';
|
|
26
|
-
const quality = 1;
|
|
27
|
-
const outputshotData = croppedCanvas.toDataURL(type, quality);
|
|
28
|
-
return outputshotData;
|
|
22
|
+
const { backgroundColor } = getComputedStyle(node);
|
|
23
|
+
return toPng(node, {
|
|
24
|
+
// Crisp on a dense display, still a modest file for a text output.
|
|
25
|
+
pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
|
|
26
|
+
// Outputs are usually transparent over the page's canvas: give the
|
|
27
|
+
// picture that canvas rather than a transparent one.
|
|
28
|
+
backgroundColor: backgroundColor && backgroundColor !== 'rgba(0, 0, 0, 0)'
|
|
29
|
+
? backgroundColor
|
|
30
|
+
: getComputedStyle(document.body).backgroundColor || '#ffffff',
|
|
31
|
+
cacheBust: true,
|
|
29
32
|
});
|
|
30
33
|
};
|
package/lib/views/mcp/McpHome.js
CHANGED
|
@@ -75,7 +75,7 @@ const Summary = ({ icon: Icon, title, children, action, }) => (_jsxs(Box, { sx:
|
|
|
75
75
|
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Icon, { size: 16 }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: title })] }), _jsx(Box, { sx: { fontSize: 0, color: 'fg.muted', display: 'grid', gap: 1 }, children: children }), action && (_jsx(Box, { children: _jsx(Link, { as: "button", sx: { fontSize: 0, cursor: 'pointer' }, onClick: action.onClick, children: action.label }) }))] }));
|
|
76
76
|
export const McpHome = ({ routes, clientDocsBase = '/docs/mcp-clients', showTitle = true, showSummaries = true, showSetup = true, onOpenDocs, }) => {
|
|
77
77
|
const navigate = useNavigate();
|
|
78
|
-
const endpoint = useCoreStore(state => state.configuration.
|
|
78
|
+
const endpoint = useCoreStore(state => state.configuration.datalayerMcpServerUrl);
|
|
79
79
|
// Whether there is a token, never what it is: this page shows an address
|
|
80
80
|
// and a status, and a credential belongs on neither.
|
|
81
81
|
const signedIn = Boolean(useIAMStore(state => state.token));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
".",
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@datalayer/design": "^1.0.0",
|
|
113
113
|
"@datalayer/icons-react": "^1.0.7",
|
|
114
|
-
"@datalayer/jupyter-lexical": "^1.0.
|
|
115
|
-
"@datalayer/jupyter-react": "^2.0.
|
|
114
|
+
"@datalayer/jupyter-lexical": "^1.0.24",
|
|
115
|
+
"@datalayer/jupyter-react": "^2.0.15",
|
|
116
116
|
"@datalayer/primer-addons": "^1.0.15",
|
|
117
117
|
"@datalayer/primer-rjsf": "^1.0.3",
|
|
118
118
|
"@fluentui/react": "^8.125.3",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"diff": "^8.0.2",
|
|
156
156
|
"echarts": "^5.5.0",
|
|
157
157
|
"echarts-for-react": "^3.0.2",
|
|
158
|
-
"
|
|
158
|
+
"html-to-image": "^1.11.13",
|
|
159
159
|
"jwt-decode": "^2.2.0",
|
|
160
160
|
"localforage": "^1.10.0",
|
|
161
161
|
"lucide-react": "^0.542.0",
|
|
@@ -200,6 +200,8 @@
|
|
|
200
200
|
"@storybook/builder-vite": "^9.1.1",
|
|
201
201
|
"@storybook/react-vite": "^9.1.1",
|
|
202
202
|
"@tailwindcss/postcss": "^4.1.16",
|
|
203
|
+
"@testing-library/dom": "^10.4.1",
|
|
204
|
+
"@testing-library/react": "^16.3.3",
|
|
203
205
|
"@types/node": "^20.11.0",
|
|
204
206
|
"@types/react": "19.2.18",
|
|
205
207
|
"@types/react-dom": "19.2.5",
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
# Distributed under the terms of the Modified BSD License.
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
|
-
Generate
|
|
10
|
-
|
|
9
|
+
Generate TypeScript wire types from the Jupyter MCP Server's OpenAPI document.
|
|
10
|
+
|
|
11
|
+
The output is deterministic, on the `generate-contents-types.py` pattern.
|
|
11
12
|
|
|
12
13
|
The gateway keeps no contract file: its FastAPI application *is* the
|
|
13
14
|
contract, and the document is read from it — imported from the sibling
|
|
14
15
|
checkout of the service when there is one, otherwise fetched from a running
|
|
15
|
-
gateway. `
|
|
16
|
+
gateway. `DATALAYER_MCP_OPENAPI` names either a file or an URL and
|
|
16
17
|
wins over both; the deployed gateway publishes its document without
|
|
17
18
|
authentication at ``/api/mcp/v1/openapi.json``.
|
|
18
19
|
|
|
@@ -40,9 +41,9 @@ from typing import Any
|
|
|
40
41
|
from urllib.request import Request, urlopen
|
|
41
42
|
|
|
42
43
|
ROOT = Path(__file__).parents[1]
|
|
43
|
-
DEFAULT_GATEWAY_CHECKOUT = ROOT.parents[2] / "k8s/services/
|
|
44
|
+
DEFAULT_GATEWAY_CHECKOUT = ROOT.parents[2] / "k8s/services/mcp-server"
|
|
44
45
|
DEFAULT_GATEWAY_OPENAPI_URL = "https://r1.datalayer.run/api/mcp/v1/openapi.json"
|
|
45
|
-
GATEWAY_APP = "
|
|
46
|
+
GATEWAY_APP = "datalayer_mcp_server.main:app"
|
|
46
47
|
OUTPUT = ROOT / "src/api/mcp/generated.ts"
|
|
47
48
|
#: The licence header every source file in this repository carries. It is
|
|
48
49
|
#: emitted here because `fix-license-header` adds it to whatever this writes,
|
|
@@ -106,13 +107,14 @@ def camel(name: str) -> str:
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
def _read_from_checkout(checkout: Path) -> dict[str, Any] | None:
|
|
109
|
-
"""
|
|
110
|
+
"""
|
|
111
|
+
The document of the application in a sibling checkout, in a subprocess.
|
|
110
112
|
|
|
111
113
|
A subprocess so the gateway's imports — its dependencies, its logging
|
|
112
114
|
configuration — never leak into this one, and so an environment that
|
|
113
115
|
cannot import it says so cleanly and the fetch takes over.
|
|
114
116
|
"""
|
|
115
|
-
if not (checkout / "
|
|
117
|
+
if not (checkout / "datalayer_mcp_server").is_dir():
|
|
116
118
|
return None
|
|
117
119
|
module, _, attribute = GATEWAY_APP.partition(":")
|
|
118
120
|
code = (
|
|
@@ -137,13 +139,17 @@ def _read_from_checkout(checkout: Path) -> dict[str, Any] | None:
|
|
|
137
139
|
|
|
138
140
|
|
|
139
141
|
def _read_from_url(url: str) -> dict[str, Any]:
|
|
140
|
-
|
|
142
|
+
if not url.startswith(("http://", "https://")):
|
|
143
|
+
raise ValueError(f"Not an HTTP(S) URL: {url}")
|
|
144
|
+
with urlopen( # nosec B310 - the scheme is checked above
|
|
145
|
+
Request(url, headers={"Accept": "application/json"}), timeout=15
|
|
146
|
+
) as response:
|
|
141
147
|
return json.load(response)
|
|
142
148
|
|
|
143
149
|
|
|
144
150
|
def read_openapi() -> dict[str, Any]:
|
|
145
151
|
"""The document, from the override, the sibling checkout or the deployed gateway."""
|
|
146
|
-
override = os.environ.get("
|
|
152
|
+
override = os.environ.get("DATALAYER_MCP_OPENAPI", "").strip()
|
|
147
153
|
if override.startswith(("http://", "https://")):
|
|
148
154
|
return _read_from_url(override)
|
|
149
155
|
if override:
|