@datalayer/core 1.2.21 → 1.2.23

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,4 +1,3 @@
1
- import { AxiosRequestConfig } from 'axios';
2
1
  /**
3
2
  * Error wrapper for failed HTTP responses.
4
3
  * Includes response details, warnings, errors, and tracebacks.
@@ -44,15 +43,15 @@ export declare class RunResponseError extends Error {
44
43
  }
45
44
  /**
46
45
  * Error wrapper for network failures.
47
- * Thrown when HTTP request fails due to connectivity issues.
46
+ * Thrown when `fetch` could not reach the server at all.
48
47
  */
49
48
  export declare class NetworkError extends TypeError {
50
49
  /**
51
- * Creates a NetworkError from the original TypeError.
50
+ * Creates a NetworkError from the error `fetch` rejected with.
52
51
  *
53
52
  * @param original - The original network error
54
53
  */
55
- constructor(original: TypeError);
54
+ constructor(original: Error);
56
55
  }
57
56
  /**
58
57
  * Thrown when a request is made with a JWT that has already expired.
@@ -85,6 +84,15 @@ export declare class TokenExpiredError extends Error {
85
84
  readonly url: string;
86
85
  constructor(url: string, expiredAt: number);
87
86
  }
87
+ /**
88
+ * How the response body is read.
89
+ *
90
+ * `json` (the default) parses the body as JSON and falls back to the text
91
+ * when it does not parse, so an empty body or a plain-text answer still
92
+ * arrives as data rather than an error. `stream` hands back the body's
93
+ * `ReadableStream` unread.
94
+ */
95
+ export type DatalayerAPIResponseType = 'json' | 'text' | 'blob' | 'arraybuffer' | 'stream';
88
96
  /**
89
97
  * Options for Datalayer API requests.
90
98
  */
@@ -93,7 +101,7 @@ export interface IRequestDatalayerAPIOptions {
93
101
  url: string;
94
102
  /** HTTP method (GET, POST, PUT, DELETE, etc.) */
95
103
  method?: string;
96
- /** Request body (JSON object or FormData) */
104
+ /** Request body (JSON object, FormData, string, or raw bytes) */
97
105
  body?: any;
98
106
  /** Custom HTTP headers */
99
107
  headers?: Record<string, string>;
@@ -101,8 +109,8 @@ export interface IRequestDatalayerAPIOptions {
101
109
  token?: string;
102
110
  /** AbortSignal for request cancellation */
103
111
  signal?: AbortSignal;
104
- /** Axios response representation for binary or streaming transports. */
105
- responseType?: AxiosRequestConfig['responseType'];
112
+ /** How to read the body; `json` when omitted. */
113
+ responseType?: DatalayerAPIResponseType;
106
114
  }
107
115
  export interface IDatalayerAPIResponse<T> {
108
116
  data: T;
@@ -118,6 +126,7 @@ export interface IDatalayerAPIResponse<T> {
118
126
  * @returns Promise resolving to response data
119
127
  * @throws {NetworkError} On network failures
120
128
  * @throws {RunResponseError} On HTTP error responses
129
+ * @throws {TokenExpiredError} On a 401 explained by an expired token
121
130
  *
122
131
  * @example
123
132
  * ```typescript
@@ -9,7 +9,6 @@
9
9
  * @module api/DatalayerApi
10
10
  */
11
11
  import { URLExt } from '@jupyterlab/coreutils';
12
- import axios from 'axios';
13
12
  import { sleep } from '../utils/Sleep.js';
14
13
  import { getJwtExpiryMs, isJwtExpired } from '../utils/Jwt.js';
15
14
  function isFormDataBody(body) {
@@ -106,11 +105,11 @@ export class RunResponseError extends Error {
106
105
  }
107
106
  /**
108
107
  * Error wrapper for network failures.
109
- * Thrown when HTTP request fails due to connectivity issues.
108
+ * Thrown when `fetch` could not reach the server at all.
110
109
  */
111
110
  export class NetworkError extends TypeError {
112
111
  /**
113
- * Creates a NetworkError from the original TypeError.
112
+ * Creates a NetworkError from the error `fetch` rejected with.
114
113
  *
115
114
  * @param original - The original network error
116
115
  */
@@ -189,6 +188,80 @@ function throwIfTokenExpired(status, token, url) {
189
188
  }
190
189
  throw new TokenExpiredError(url, getJwtExpiryMs(token) ?? Date.now());
191
190
  }
191
+ /**
192
+ * Bodies `fetch` sends as they are. Everything else is a JSON value and is
193
+ * stringified.
194
+ */
195
+ function isRawBody(body) {
196
+ return (typeof body === 'string' ||
197
+ (typeof Blob !== 'undefined' && body instanceof Blob) ||
198
+ body instanceof ArrayBuffer ||
199
+ ArrayBuffer.isView(body) ||
200
+ (typeof URLSearchParams !== 'undefined' &&
201
+ body instanceof URLSearchParams) ||
202
+ (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream));
203
+ }
204
+ /**
205
+ * The response headers as a plain record, keyed in lower case the way
206
+ * `Headers` reports them.
207
+ */
208
+ function headersToRecord(headers) {
209
+ const record = {};
210
+ headers.forEach((value, name) => {
211
+ record[name] = value;
212
+ });
213
+ return record;
214
+ }
215
+ /**
216
+ * Reads the body the way `responseType` asks for.
217
+ */
218
+ async function readBody(response, responseType) {
219
+ switch (responseType) {
220
+ case 'text':
221
+ return response.text();
222
+ case 'blob':
223
+ return response.blob();
224
+ case 'arraybuffer':
225
+ return response.arrayBuffer();
226
+ case 'stream':
227
+ return response.body;
228
+ default: {
229
+ const text = await response.text();
230
+ try {
231
+ return JSON.parse(text);
232
+ }
233
+ catch {
234
+ return text;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Sends one request and returns the response when the server accepted it.
241
+ *
242
+ * A failure to reach the server at all arrives as `NetworkError`; an abort
243
+ * through the caller's signal is rethrown as it came, since the caller asked
244
+ * for it. A non-2xx answer becomes `TokenExpiredError` when a 401 is explained
245
+ * by the token we sent, and `RunResponseError` otherwise.
246
+ */
247
+ async function send(url, init, token) {
248
+ let response;
249
+ try {
250
+ response = await fetch(url, init);
251
+ }
252
+ catch (error) {
253
+ // By name: a DOMException is not an `Error` in every realm.
254
+ if (error?.name === 'AbortError') {
255
+ throw error;
256
+ }
257
+ throw new NetworkError(error instanceof Error ? error : new TypeError(String(error)));
258
+ }
259
+ if (!response.ok) {
260
+ throwIfTokenExpired(response.status, token, url);
261
+ throw await RunResponseError.create(response);
262
+ }
263
+ return response;
264
+ }
192
265
  /**
193
266
  * Makes authenticated HTTP requests to Datalayer APIs.
194
267
  * Handles JSON and FormData, includes auth headers, and manages redirects.
@@ -197,6 +270,7 @@ function throwIfTokenExpired(status, token, url) {
197
270
  * @returns Promise resolving to response data
198
271
  * @throws {NetworkError} On network failures
199
272
  * @throws {RunResponseError} On HTTP error responses
273
+ * @throws {TokenExpiredError} On a 401 explained by an expired token
200
274
  *
201
275
  * @example
202
276
  * ```typescript
@@ -208,147 +282,109 @@ function throwIfTokenExpired(status, token, url) {
208
282
  * ```
209
283
  */
210
284
  export async function requestDatalayerAPIWithResponse({ url, method, body, token, signal, responseType, headers = {}, }) {
211
- // Handle FormData differently from JSON
212
- const isFormData = isFormDataBody(body);
213
- // Prepare axios config
214
- const axiosConfig = {
215
- url,
216
- method: (method ?? 'GET'),
217
- headers: { ...headers },
218
- withCredentials: true, // equivalent to credentials: 'include'
219
- signal,
220
- responseType,
221
- // CORS mode is handled automatically by axios
222
- // Cache control headers
223
- };
224
- // In Vitest+jsdom, axios may pick the XHR adapter and fail with browser-like
225
- // network restrictions. Force the fetch adapter for integration reliability.
226
- if (typeof process !== 'undefined' && process.env.VITEST) {
227
- axiosConfig.adapter = 'fetch';
228
- }
229
- // Add cache control headers only for GET requests (equivalent to cache: 'no-store')
230
- if (method === 'GET' || !method) {
231
- if (!axiosConfig.headers['Cache-Control']) {
232
- axiosConfig.headers['Cache-Control'] =
233
- 'no-store, no-cache, must-revalidate';
285
+ const requestMethod = method ?? 'GET';
286
+ const requestHeaders = new Headers(headers);
287
+ // A GET must not be answered from a cache (the equivalent of
288
+ // `cache: 'no-store'`, said in headers so proxies hear it too).
289
+ if (requestMethod === 'GET') {
290
+ if (!requestHeaders.has('Cache-Control')) {
291
+ requestHeaders.set('Cache-Control', 'no-store, no-cache, must-revalidate');
234
292
  }
235
- if (!axiosConfig.headers['Pragma']) {
236
- axiosConfig.headers['Pragma'] = 'no-cache';
293
+ if (!requestHeaders.has('Pragma')) {
294
+ requestHeaders.set('Pragma', 'no-cache');
237
295
  }
238
296
  }
239
297
  if (token) {
240
- axiosConfig.headers['Authorization'] = `Bearer ${token}`;
298
+ requestHeaders.set('Authorization', `Bearer ${token}`);
241
299
  }
242
- if (isFormData) {
243
- // For FormData: let axios handle Content-Type automatically
244
- axiosConfig.data = body;
245
- // Don't set Content-Type - axios will set multipart/form-data with boundary
246
- if (!axiosConfig.headers['Accept']) {
247
- axiosConfig.headers['Accept'] = 'application/json';
248
- }
300
+ if (!requestHeaders.has('Accept')) {
301
+ requestHeaders.set('Accept', 'application/json');
302
+ }
303
+ let requestBody;
304
+ if (isFormDataBody(body)) {
305
+ // No Content-Type: the browser sets multipart/form-data with its
306
+ // boundary.
307
+ requestBody = body;
249
308
  }
250
309
  else {
251
- // For regular JSON requests
252
- if (!axiosConfig.headers['Accept']) {
253
- axiosConfig.headers['Accept'] = 'application/json';
310
+ if (!requestHeaders.has('Content-Type')) {
311
+ requestHeaders.set('Content-Type', 'application/json');
254
312
  }
255
- if (!axiosConfig.headers['Content-Type']) {
256
- axiosConfig.headers['Content-Type'] = 'application/json';
313
+ if (body === undefined) {
314
+ requestBody = undefined;
257
315
  }
258
- axiosConfig.data = body;
259
- }
260
- try {
261
- const response = await axios(axiosConfig);
262
- if (response.status < 300) {
263
- // Handle redirections if needed.
264
- if (response.status === 202 && response.headers.location) {
265
- const data = await handleAxiosRedirection(response, axiosConfig);
266
- return {
267
- data: data,
268
- status: response.status,
269
- statusText: response.statusText,
270
- headers: { ...response.headers },
271
- };
272
- }
316
+ else if (isRawBody(body)) {
317
+ requestBody = body;
273
318
  }
274
319
  else {
275
- throwIfTokenExpired(response.status, token, url);
276
- const adaptedResponse = {
277
- ok: false,
278
- status: response.status,
279
- statusText: response.statusText,
280
- json: async () => response?.data,
281
- text: async () => JSON.stringify(response?.data),
282
- };
283
- throw await RunResponseError.create(adaptedResponse);
320
+ requestBody = JSON.stringify(body);
284
321
  }
285
- return {
286
- data: response.data,
287
- status: response.status,
288
- statusText: response.statusText,
289
- headers: { ...response.headers },
290
- };
291
322
  }
292
- catch (error) {
293
- if (axios.isAxiosError(error)) {
294
- if (error.response) {
295
- throwIfTokenExpired(error.response.status, token, url);
296
- // Convert axios error to our RunResponseError format
297
- const adaptedResponse = {
298
- ok: false,
299
- status: error.response.status,
300
- statusText: error.response.statusText,
301
- json: async () => error.response?.data,
302
- text: async () => JSON.stringify(error.response?.data),
303
- };
304
- throw await RunResponseError.create(adaptedResponse);
305
- }
306
- throw new NetworkError(error);
307
- }
308
- throw error;
323
+ const init = {
324
+ method: requestMethod,
325
+ headers: requestHeaders,
326
+ body: requestBody,
327
+ // Sends the browser's cookies along, cross-origin included.
328
+ credentials: 'include',
329
+ signal,
330
+ };
331
+ let response = await send(url, init, token);
332
+ // A 202 with a Location is the server saying the work is still running
333
+ // there: poll it until it is done.
334
+ if (response.status === 202 && response.headers.get('location')) {
335
+ response = await followAsyncRedirect(response, init, url, token);
309
336
  }
337
+ return {
338
+ data: (await readBody(response, responseType)),
339
+ status: response.status,
340
+ statusText: response.statusText,
341
+ headers: headersToRecord(response.headers),
342
+ };
310
343
  }
311
344
  export async function requestDatalayerAPI(options) {
312
345
  return (await requestDatalayerAPIWithResponse(options)).data;
313
346
  }
314
- async function handleAxiosRedirection(response, originalConfig) {
315
- let redirect = response.headers.location;
316
- if (redirect) {
317
- const baseUrl = originalConfig.url ?? '';
318
- const normalizedRedirect = String(redirect).replace(/^([a-z][a-z0-9+.-]*):\/(?!\/)/i, '$1://');
319
- try {
320
- const resolved = new URL(normalizedRedirect, baseUrl);
321
- const base = new URL(baseUrl, typeof window !== 'undefined' ? window.location.origin : undefined);
322
- // If a proxy emits an http Location for the same host while the
323
- // original request is https, force https to avoid mixed-content errors
324
- // that browsers often report as CORS/network failures.
325
- if (base.protocol === 'https:' &&
326
- resolved.protocol === 'http:' &&
327
- resolved.hostname === base.hostname) {
328
- resolved.protocol = 'https:';
329
- if (resolved.port === '80') {
330
- resolved.port = '';
331
- }
347
+ /**
348
+ * Resolves the Location of a 202 against the request's URL.
349
+ *
350
+ * A proxy that emits an http Location for the same host while the original
351
+ * request was https gets it forced back to https, since the browser would
352
+ * refuse the mixed content and report it as a CORS or network failure.
353
+ */
354
+ function resolveLocation(location, baseUrl) {
355
+ const normalized = location.replace(/^([a-z][a-z0-9+.-]*):\/(?!\/)/i, '$1://');
356
+ try {
357
+ const resolved = new URL(normalized, baseUrl);
358
+ const base = new URL(baseUrl, typeof window !== 'undefined' ? window.location.origin : undefined);
359
+ if (base.protocol === 'https:' &&
360
+ resolved.protocol === 'http:' &&
361
+ resolved.hostname === base.hostname) {
362
+ resolved.protocol = 'https:';
363
+ if (resolved.port === '80') {
364
+ resolved.port = '';
332
365
  }
333
- redirect = resolved.toString();
334
- }
335
- catch {
336
- const parsedURL = URLExt.parse(baseUrl);
337
- const fallbackBase = parsedURL.protocol + '//' + parsedURL.hostname;
338
- redirect = URLExt.join(fallbackBase, normalizedRedirect);
339
366
  }
367
+ return resolved.toString();
368
+ }
369
+ catch {
370
+ const parsedURL = URLExt.parse(baseUrl);
371
+ const fallbackBase = parsedURL.protocol + '//' + parsedURL.hostname;
372
+ return URLExt.join(fallbackBase, normalized);
340
373
  }
374
+ }
375
+ /**
376
+ * Polls the Location of a 202 with GETs, backing off from a second and
377
+ * doubling, until the answer is no longer a 202. The polls carry the
378
+ * original headers and credentials, never the body.
379
+ */
380
+ async function followAsyncRedirect(response, init, url, token) {
381
+ const location = resolveLocation(response.headers.get('location'), url);
382
+ const pollInit = { ...init, method: 'GET', body: undefined };
341
383
  let sleepTimeout = 1000;
342
- while (response.status === 202 && redirect) {
384
+ while (response.status === 202) {
343
385
  await sleep(sleepTimeout);
344
386
  sleepTimeout *= 2;
345
- const redirectConfig = {
346
- ...originalConfig,
347
- url: redirect,
348
- method: 'GET',
349
- data: undefined, // Don't send body on redirect
350
- };
351
- response = await axios(redirectConfig);
387
+ response = await send(location, pollInit, token);
352
388
  }
353
- return response.data;
389
+ return response;
354
390
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export { requestDatalayerAPI, requestDatalayerAPIWithResponse, RunResponseError, NetworkError, TokenExpiredError, } from './DatalayerApi';
10
10
  export type { IRequestDatalayerAPIOptions } from './DatalayerApi';
11
- export type { IDatalayerAPIResponse } from './DatalayerApi';
11
+ export type { IDatalayerAPIResponse, DatalayerAPIResponseType, } from './DatalayerApi';
12
12
  export * as iam from './iam';
13
13
  export * as contents from './contents';
14
14
  export * as evals from './evals';
@@ -46,7 +46,7 @@ export function IAMMixin(Base) {
46
46
  email_s: response.profile.email_s || response.profile.email,
47
47
  first_name_t: response.profile.first_name_t || response.profile.first_name || '',
48
48
  last_name_t: response.profile.last_name_t || response.profile.last_name || '',
49
- // Use avatar_url_s if available, otherwise leave undefined for fallback
49
+ // The avatar's URL when the profile has one; without, the user's avatarUrl is ''.
50
50
  avatar_url_s: response.profile.avatar_url_s || response.profile.avatar_url,
51
51
  };
52
52
  }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Posts a form to `url` through the API transport.
3
+ *
4
+ * `progress` is 0 while the upload is in flight and 100 once the server has
5
+ * answered: `fetch` reports no upload progress, so there is nothing in
6
+ * between to show.
7
+ */
1
8
  export declare const useUploadForm: (url: string) => {
2
9
  uploadAndSubmit: (formData: FormData) => Promise<any>;
3
10
  isSuccess: boolean;
@@ -3,16 +3,20 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  import { useState } from 'react';
6
- import axios from 'axios';
7
- // import useRun from "./useRun.js";
6
+ import { requestDatalayerAPI } from '../api/DatalayerApi.js';
8
7
  import { useIAMStore } from '../state/index.js';
9
- // TODO reuse useRun hook.
8
+ /**
9
+ * Posts a form to `url` through the API transport.
10
+ *
11
+ * `progress` is 0 while the upload is in flight and 100 once the server has
12
+ * answered: `fetch` reports no upload progress, so there is nothing in
13
+ * between to show.
14
+ */
10
15
  export const useUploadForm = (url) => {
11
16
  const [isSuccess, setIsSuccess] = useState(false);
12
17
  const [isLoading, setIsLoading] = useState(false);
13
18
  const [progress, setProgress] = useState(0);
14
19
  const { token } = useIAMStore();
15
- // const { requestRun } = useRun();
16
20
  const reset = () => {
17
21
  setIsSuccess(false);
18
22
  setIsLoading(false);
@@ -21,19 +25,11 @@ export const useUploadForm = (url) => {
21
25
  const uploadAndSubmit = async (formData) => {
22
26
  setIsLoading(true);
23
27
  try {
24
- const { data } = await axios.post(url, formData, {
25
- headers: {
26
- 'Content-Type': 'multipart/form-data',
27
- Authorization: `Bearer ${token}`,
28
- },
29
- onUploadProgress: progressEvent => {
30
- const progress = (progressEvent.loaded / progressEvent.total) * 50;
31
- setProgress(progress);
32
- },
33
- onDownloadProgress: progressEvent => {
34
- const progress = 50 + (progressEvent.loaded / progressEvent.total) * 50;
35
- setProgress(progress);
36
- },
28
+ const data = await requestDatalayerAPI({
29
+ url,
30
+ method: 'POST',
31
+ body: formData,
32
+ token,
37
33
  });
38
34
  setProgress(100);
39
35
  await new Promise(resolve => {
package/lib/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export * from './navigation';
5
5
  export * from './hooks';
6
6
  export { requestDatalayerAPI, requestDatalayerAPIWithResponse, RunResponseError, NetworkError, } from './api/DatalayerApi';
7
7
  export type { IRequestDatalayerAPIOptions } from './api/DatalayerApi';
8
- export type { IDatalayerAPIResponse } from './api/DatalayerApi';
8
+ export type { IDatalayerAPIResponse, DatalayerAPIResponseType, } from './api/DatalayerApi';
9
9
  export { API_BASE_PATHS } from './api/constants';
10
10
  export * as iamApi from './api/iam';
11
11
  export * as contentsApi from './api/contents';
@@ -68,6 +68,7 @@ export declare class UserDTO {
68
68
  get firstName(): string;
69
69
  get lastName(): string;
70
70
  get displayName(): string;
71
+ /** The avatar's URL, or `''` when the profile has none: a person without a picture is still a user. */
71
72
  get avatarUrl(): string;
72
73
  get avatarIcon(): string | undefined;
73
74
  get banner(): string | undefined;
@@ -40,8 +40,9 @@ export class UserDTO {
40
40
  get displayName() {
41
41
  return `${this.firstName} ${this.lastName}`.trim();
42
42
  }
43
+ /** The avatar's URL, or `''` when the profile has none: a person without a picture is still a user. */
43
44
  get avatarUrl() {
44
- return this._data.avatar_url_s;
45
+ return this._data.avatar_url_s ?? '';
45
46
  }
46
47
  get avatarIcon() {
47
48
  return this._data.avatar_icon_s;
@@ -30,7 +30,7 @@ export function setOtelOnUnauthorized(cb) {
30
30
  }
31
31
  /**
32
32
  * Lightweight fetch helper for OTEL API calls.
33
- * Uses plain `fetch` to avoid pulling in `@jupyterlab/coreutils` / axios.
33
+ * Uses plain `fetch` to avoid pulling in `@jupyterlab/coreutils`.
34
34
  *
35
35
  * If the response is **401 Unauthorized** and a global `onUnauthorized`
36
36
  * handler has been registered via {@link setOtelOnUnauthorized}, the
@@ -70,6 +70,12 @@ export type NavigationState = {
70
70
  * through or on a page that belongs to no shell.
71
71
  */
72
72
  export declare function isRememberableRoute(route: string): boolean;
73
+ /**
74
+ * Whether a page is a hosted application, run at its address, rather than a
75
+ * page of the Apps shell. It is visited, not returned to: the Apps shell
76
+ * never reopens on one, and one remembered before is dropped on load.
77
+ */
78
+ export declare function isHostedAppRoute(route: string): boolean;
73
79
  /**
74
80
  * Whether a page belongs to no shell: the settings, the documentation and
75
81
  * the decks.
@@ -199,7 +199,48 @@ export function isRememberableRoute(route) {
199
199
  '/oauth',
200
200
  '/join',
201
201
  ].some(prefix => path === prefix || path.startsWith(`${prefix}/`)) &&
202
- !isShellNeutralRoute(route));
202
+ !isShellNeutralRoute(route) &&
203
+ !isHostedAppRoute(route));
204
+ }
205
+ /**
206
+ * The Apps shell's own pages under `/apps/<section>`. Any other first segment
207
+ * is an application's hosted address (`/apps/<slug>`, its preview at
208
+ * `/apps/<slug>/preview/<version>`).
209
+ */
210
+ const APPS_SHELL_SECTIONS = new Set([
211
+ 'activity',
212
+ 'agents',
213
+ 'apps',
214
+ 'components',
215
+ 'connections',
216
+ 'contents',
217
+ 'context',
218
+ 'deployments',
219
+ 'evalsets',
220
+ 'examples',
221
+ 'home',
222
+ 'models',
223
+ 'readiness',
224
+ 'reports',
225
+ 'rules',
226
+ 'runs',
227
+ 'shared',
228
+ 'skills',
229
+ 'usage',
230
+ 'workspace',
231
+ ]);
232
+ /**
233
+ * Whether a page is a hosted application, run at its address, rather than a
234
+ * page of the Apps shell. It is visited, not returned to: the Apps shell
235
+ * never reopens on one, and one remembered before is dropped on load.
236
+ */
237
+ export function isHostedAppRoute(route) {
238
+ const segments = pathOf(route).split('/').filter(Boolean);
239
+ return (segments[0] === 'apps' &&
240
+ segments.length >= 2 &&
241
+ !APPS_SHELL_SECTIONS.has(segments[1]) &&
242
+ (segments.length === 2 ||
243
+ (segments.length === 4 && segments[2] === 'preview')));
203
244
  }
204
245
  /**
205
246
  * Whether a page belongs to no shell: the settings, the documentation and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.2.21",
3
+ "version": "1.2.23",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -148,7 +148,6 @@
148
148
  "@tanstack/react-query": "^5.90.6",
149
149
  "@toon-format/toon": "^1.3.0",
150
150
  "ansi-to-html": "^0.7.2",
151
- "axios": "^1.7.7",
152
151
  "boring-avatars": "^2.0.1",
153
152
  "date-fns": "^2.29.3",
154
153
  "deepmerge": "^4.3.1",