@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.
- package/lib/api/DatalayerApi.d.ts +16 -7
- package/lib/api/DatalayerApi.js +157 -121
- package/lib/api/index.d.ts +1 -1
- package/lib/client/mixins/IAMMixin.js +1 -1
- package/lib/hooks/useUpload.d.ts +7 -0
- package/lib/hooks/useUpload.js +13 -17
- package/lib/index.d.ts +1 -1
- package/lib/models/UserDTO.d.ts +1 -0
- package/lib/models/UserDTO.js +2 -1
- package/lib/otel/hooks/index.js +1 -1
- package/lib/state/substates/NavigationState.d.ts +6 -0
- package/lib/state/substates/NavigationState.js +42 -1
- package/package.json +1 -2
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
/**
|
|
105
|
-
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
|
package/lib/api/DatalayerApi.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
212
|
-
const
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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 (!
|
|
236
|
-
|
|
293
|
+
if (!requestHeaders.has('Pragma')) {
|
|
294
|
+
requestHeaders.set('Pragma', 'no-cache');
|
|
237
295
|
}
|
|
238
296
|
}
|
|
239
297
|
if (token) {
|
|
240
|
-
|
|
298
|
+
requestHeaders.set('Authorization', `Bearer ${token}`);
|
|
241
299
|
}
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
axiosConfig.headers['Accept'] = 'application/json';
|
|
310
|
+
if (!requestHeaders.has('Content-Type')) {
|
|
311
|
+
requestHeaders.set('Content-Type', 'application/json');
|
|
254
312
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
313
|
+
if (body === undefined) {
|
|
314
|
+
requestBody = undefined;
|
|
257
315
|
}
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
384
|
+
while (response.status === 202) {
|
|
343
385
|
await sleep(sleepTimeout);
|
|
344
386
|
sleepTimeout *= 2;
|
|
345
|
-
|
|
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
|
|
389
|
+
return response;
|
|
354
390
|
}
|
package/lib/api/index.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
}
|
package/lib/hooks/useUpload.d.ts
CHANGED
|
@@ -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;
|
package/lib/hooks/useUpload.js
CHANGED
|
@@ -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
|
|
7
|
-
// import useRun from "./useRun.js";
|
|
6
|
+
import { requestDatalayerAPI } from '../api/DatalayerApi.js';
|
|
8
7
|
import { useIAMStore } from '../state/index.js';
|
|
9
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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';
|
package/lib/models/UserDTO.d.ts
CHANGED
|
@@ -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;
|
package/lib/models/UserDTO.js
CHANGED
|
@@ -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;
|
package/lib/otel/hooks/index.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|