@parallelworks/ui 0.21.2 → 0.22.0

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.
@@ -133,6 +133,7 @@ export interface UIStrings {
133
133
  errorTitle: string;
134
134
  previewError: string;
135
135
  tooLarge: string;
136
+ tooLargeSize: (size: string) => string;
136
137
  downloadInstead: string;
137
138
  archiveTitle: string;
138
139
  archiveMessage: string;
@@ -137,6 +137,7 @@ var DEFAULTS = {
137
137
  errorTitle: "Couldn't load preview",
138
138
  previewError: "This file couldn't be previewed in your browser.",
139
139
  tooLarge: "This file is too large to preview in the browser.",
140
+ tooLargeSize: (size) => `This file is ${size}, too large to preview in the browser.`,
140
141
  downloadInstead: "Download instead",
141
142
  archiveTitle: "Can't preview archives",
142
143
  archiveMessage: "Archive files can't be previewed in the browser. Download the file to view its contents.",
@@ -8,7 +8,7 @@ import { positionKeys, withPositionKeys } from "../../components/keys.js";
8
8
  import Loader from "../../components/Loader.js";
9
9
  import Markdown from "../../components/Markdown.js";
10
10
  import { formatFileSize, getFileExtension } from "../lib/utils.js";
11
- import { CODE_PREVIEW_MAX_BYTES, CODE_PREVIEW_MAX_LINES, PDF_PREVIEW_MAX_BYTES, TABULAR_PREVIEW_MAX_BYTES, getMonacoLanguage } from "./previewType.js";
11
+ import { getMonacoLanguage, previewLimitsFor } from "./previewType.js";
12
12
  import { CorsIssuePreview } from "../helpers.js";
13
13
  import { joinSource, parseDelimited, parseNotebook, stripAnsi } from "./parse.js";
14
14
  import { countLines } from "./readCappedBytes.js";
@@ -69,7 +69,8 @@ function FilePreviewViewer({ node, kind, url, urlLoading, urlError, zoom = 100,
69
69
  node,
70
70
  url,
71
71
  zoom,
72
- onDownload
72
+ onDownload,
73
+ storage
73
74
  });
74
75
  case "video": return /* @__PURE__ */ jsx(VideoView, {
75
76
  node,
@@ -112,8 +113,12 @@ function FilePreviewViewer({ node, kind, url, urlLoading, urlError, zoom = 100,
112
113
  default: return null;
113
114
  }
114
115
  }
115
- function ImageView({ node, url, zoom, onDownload }) {
116
+ function ImageView({ node, url, zoom, onDownload, storage }) {
116
117
  const [hasError, setHasError] = useState(false);
118
+ if (typeof node.size === "number" && node.size > previewLimitsFor(storage).imageBytes) return /* @__PURE__ */ jsx(TooLargeInline, {
119
+ size: node.size,
120
+ onDownload
121
+ });
117
122
  if (hasError) return /* @__PURE__ */ jsx(PreviewError, { onDownload });
118
123
  const fit = zoom === 100;
119
124
  return /* @__PURE__ */ jsx("div", {
@@ -191,7 +196,7 @@ function PreviewError({ onDownload }) {
191
196
  }
192
197
  function PdfView({ node, url, onDownload, storage, onCorsRetry }) {
193
198
  const { blobUrl, loading, error, tooLarge, corsError } = useObjectBlobUrl(url, {
194
- maxBytes: PDF_PREVIEW_MAX_BYTES,
199
+ maxBytes: previewLimitsFor(storage).pdfBytes,
195
200
  knownSize: node.size,
196
201
  mimeType: "application/pdf",
197
202
  cacheKey: node.path
@@ -199,6 +204,7 @@ function PdfView({ node, url, onDownload, storage, onCorsRetry }) {
199
204
  return /* @__PURE__ */ jsx(PreviewFetchState, {
200
205
  loading,
201
206
  tooLarge,
207
+ size: node.size,
202
208
  error,
203
209
  corsError,
204
210
  storage,
@@ -215,9 +221,10 @@ function CodeView({ node, url, onDownload, storage, onCorsRetry }) {
215
221
  const t = useStrings().fileExplorer;
216
222
  const language = getMonacoLanguage(node);
217
223
  const [tail, setTail] = useState(false);
224
+ const limits = previewLimitsFor(storage);
218
225
  const { text, loading, error, tooLarge, truncated, corsError } = useObjectText(url, {
219
- maxBytes: CODE_PREVIEW_MAX_BYTES,
220
- maxLines: CODE_PREVIEW_MAX_LINES,
226
+ maxBytes: limits.codeBytes,
227
+ maxLines: limits.codeLines,
221
228
  knownSize: node.size,
222
229
  cacheKey: node.path,
223
230
  tail
@@ -262,6 +269,7 @@ function CodeView({ node, url, onDownload, storage, onCorsRetry }) {
262
269
  children: /* @__PURE__ */ jsx(PreviewFetchState, {
263
270
  loading,
264
271
  tooLarge,
272
+ size: node.size,
265
273
  error,
266
274
  corsError,
267
275
  storage,
@@ -292,15 +300,17 @@ function toFencedCode(text, language) {
292
300
  return `${fence}${language}\n${text}\n${fence}`;
293
301
  }
294
302
  function CsvView({ node, url, onDownload, storage, onCorsRetry }) {
303
+ const limits = previewLimitsFor(storage);
295
304
  const { text, loading, error, tooLarge, corsError } = useObjectText(url, {
296
- maxBytes: TABULAR_PREVIEW_MAX_BYTES,
297
- maxLines: 502,
305
+ maxBytes: limits.tabularBytes,
306
+ maxLines: limits.tabularRows + 2,
298
307
  knownSize: node.size,
299
308
  cacheKey: node.path
300
309
  });
301
310
  return /* @__PURE__ */ jsx(PreviewFetchState, {
302
311
  loading,
303
312
  tooLarge,
313
+ size: node.size,
304
314
  error,
305
315
  corsError,
306
316
  storage,
@@ -308,18 +318,19 @@ function CsvView({ node, url, onDownload, storage, onCorsRetry }) {
308
318
  onDownload,
309
319
  children: /* @__PURE__ */ jsx(CsvTable, {
310
320
  node,
311
- text: text ?? ""
321
+ text: text ?? "",
322
+ maxRows: limits.tabularRows
312
323
  })
313
324
  });
314
325
  }
315
- function CsvTable({ node, text }) {
326
+ function CsvTable({ node, text, maxRows }) {
316
327
  const t = useStrings().fileExplorer;
317
328
  const delimiter = getFileExtension(node.name) === "tsv" ? " " : ",";
318
- const rows = parseDelimited(text, delimiter, 502);
329
+ const rows = parseDelimited(text, delimiter, maxRows + 2);
319
330
  const header = rows[0] ?? [];
320
331
  const body = rows.slice(1);
321
- const shown = body.slice(0, 500);
322
- const truncated = body.length > 500;
332
+ const shown = body.slice(0, maxRows);
333
+ const truncated = body.length > maxRows;
323
334
  const columnCount = Math.max(header.length, ...shown.map((r) => r.length));
324
335
  return /* @__PURE__ */ jsxs("div", {
325
336
  className: "flex h-full w-full flex-col bg-(--theme-app-bg)",
@@ -355,13 +366,14 @@ function CsvTable({ node, text }) {
355
366
  }
356
367
  function NotebookView({ node, url, onDownload, storage, onCorsRetry }) {
357
368
  const { text, loading, error, tooLarge, corsError } = useObjectText(url, {
358
- maxBytes: TABULAR_PREVIEW_MAX_BYTES,
369
+ maxBytes: previewLimitsFor(storage).notebookBytes,
359
370
  knownSize: node.size,
360
371
  cacheKey: node.path
361
372
  });
362
373
  return /* @__PURE__ */ jsx(PreviewFetchState, {
363
374
  loading,
364
375
  tooLarge,
376
+ size: node.size,
365
377
  error,
366
378
  corsError,
367
379
  storage,
@@ -445,7 +457,7 @@ function PreviewLoading() {
445
457
  children: /* @__PURE__ */ jsx(Loader, { size: 36 })
446
458
  });
447
459
  }
448
- function PreviewFetchState({ loading, tooLarge, error, corsError, storage, onCorsRetry, onDownload, children }) {
460
+ function PreviewFetchState({ loading, tooLarge, size, error, corsError, storage, onCorsRetry, onDownload, children }) {
449
461
  if (loading) return /* @__PURE__ */ jsx(PreviewLoading, {});
450
462
  if (corsError) return /* @__PURE__ */ jsx("div", {
451
463
  className: "flex h-full w-full items-center justify-center p-6",
@@ -454,7 +466,10 @@ function PreviewFetchState({ loading, tooLarge, error, corsError, storage, onCor
454
466
  onRefresh: onCorsRetry
455
467
  })
456
468
  });
457
- if (tooLarge) return /* @__PURE__ */ jsx(TooLargeInline, { onDownload });
469
+ if (tooLarge) return /* @__PURE__ */ jsx(TooLargeInline, {
470
+ size,
471
+ onDownload
472
+ });
458
473
  if (error) return /* @__PURE__ */ jsx(PreviewError, { onDownload });
459
474
  return /* @__PURE__ */ jsx(Fragment$1, { children });
460
475
  }
@@ -486,7 +501,7 @@ function NotPreviewable({ icon, title, message, onDownload }) {
486
501
  })
487
502
  });
488
503
  }
489
- function TooLargeInline({ onDownload }) {
504
+ function TooLargeInline({ size, onDownload }) {
490
505
  const t = useStrings().fileExplorer;
491
506
  return /* @__PURE__ */ jsx("div", {
492
507
  className: "flex h-full items-center justify-center p-6",
@@ -494,7 +509,7 @@ function TooLargeInline({ onDownload }) {
494
509
  className: "flex max-w-md flex-col items-center gap-3 text-center",
495
510
  children: [/* @__PURE__ */ jsx("div", {
496
511
  className: "text-sm theme-muted-text",
497
- children: t.preview.tooLarge
512
+ children: typeof size === "number" ? t.preview.tooLargeSize(formatFileSize(size)) : t.preview.tooLarge
498
513
  }), onDownload && /* @__PURE__ */ jsxs("button", {
499
514
  type: "button",
500
515
  className: "btn btn-info cursor-pointer",
@@ -1,4 +1,4 @@
1
- import type { TreeNode } from '../lib/types.js';
1
+ import type { PreviewLimits, TreeNode, TStorage } from '../lib/types.js';
2
2
  import { getFileExtension } from '../lib/utils.js';
3
3
  export { getFileExtension };
4
4
  export type PreviewKind = 'image' | 'video' | 'audio' | 'pdf' | 'code' | 'csv' | 'notebook' | 'archive' | 'unsupported';
@@ -15,6 +15,8 @@ export declare const CSV_PREVIEW_MAX_ROWS = 500;
15
15
  export declare const CODE_PREVIEW_MAX_LINES = 5000;
16
16
  /** Fetched into a blob URL to render inline regardless of served content type; falls back to download above this. */
17
17
  export declare const PDF_PREVIEW_MAX_BYTES: number;
18
+ export declare const DEFAULT_PREVIEW_LIMITS: PreviewLimits;
19
+ export declare function previewLimitsFor(storage: TStorage | null | undefined): PreviewLimits;
18
20
  export declare function getMonacoLanguage(node: TreeNode): string;
19
21
  /** Content type wins when present; extension is the fallback. */
20
22
  export declare function getPreviewKind(node: TreeNode): PreviewKind;
@@ -12,6 +12,21 @@ var TABULAR_PREVIEW_MAX_BYTES = 20971520;
12
12
  var CODE_PREVIEW_MAX_LINES = 5e3;
13
13
  /** Fetched into a blob URL to render inline regardless of served content type; falls back to download above this. */
14
14
  var PDF_PREVIEW_MAX_BYTES = 52428800;
15
+ var DEFAULT_PREVIEW_LIMITS = {
16
+ codeBytes: CODE_PREVIEW_MAX_BYTES,
17
+ codeLines: CODE_PREVIEW_MAX_LINES,
18
+ tabularBytes: TABULAR_PREVIEW_MAX_BYTES,
19
+ tabularRows: 500,
20
+ imageBytes: Number.POSITIVE_INFINITY,
21
+ pdfBytes: PDF_PREVIEW_MAX_BYTES,
22
+ notebookBytes: TABULAR_PREVIEW_MAX_BYTES
23
+ };
24
+ function previewLimitsFor(storage) {
25
+ return {
26
+ ...DEFAULT_PREVIEW_LIMITS,
27
+ ...storage?.previewLimits
28
+ };
29
+ }
15
30
  var IMAGE_EXT = /* @__PURE__ */ new Set([
16
31
  "png",
17
32
  "jpg",
@@ -97,6 +112,8 @@ var LANGUAGE_BY_EXT = {
97
112
  tex: "plaintext",
98
113
  txt: "plaintext",
99
114
  log: "plaintext",
115
+ out: "plaintext",
116
+ err: "plaintext",
100
117
  gitignore: "plaintext"
101
118
  };
102
119
  var LANGUAGE_BY_FILENAME = {
@@ -148,4 +165,4 @@ function canPreview(node) {
148
165
  return kind !== "unsupported" && kind !== "archive";
149
166
  }
150
167
  //#endregion
151
- export { CODE_PREVIEW_MAX_BYTES, CODE_PREVIEW_MAX_LINES, PDF_PREVIEW_MAX_BYTES, PREVIEW_URL_EXPIRES_IN, TABULAR_PREVIEW_MAX_BYTES, canPreview, getFileExtension, getMonacoLanguage, getPreviewKind };
168
+ export { CODE_PREVIEW_MAX_BYTES, CODE_PREVIEW_MAX_LINES, DEFAULT_PREVIEW_LIMITS, PDF_PREVIEW_MAX_BYTES, PREVIEW_URL_EXPIRES_IN, TABULAR_PREVIEW_MAX_BYTES, canPreview, getFileExtension, getMonacoLanguage, getPreviewKind, previewLimitsFor };
@@ -19,6 +19,10 @@ export declare function lastLines(text: string, maxLines: number): {
19
19
  text: string;
20
20
  truncated: boolean;
21
21
  };
22
+ /** Range header for reading the start of an object that is larger than `maxBytes`,
23
+ * one byte past the cap so the read still sees the overflow and cuts on a line, as it
24
+ * does for an unranged body. The bytes past it never leave the server. */
25
+ export declare function headRangeFor(knownSize: number | undefined, maxBytes: number): string | null;
22
26
  /** Range header for reading the last `maxBytes` of an object. A closed range is a
23
27
  * CORS-safelisted header value, so it travels without a preflight that a bucket's
24
28
  * CORS rules may not answer; the suffix form is not safelisted, and is only reached
@@ -145,6 +145,12 @@ function lastLines(text, maxLines) {
145
145
  truncated: false
146
146
  };
147
147
  }
148
+ /** Range header for reading the start of an object that is larger than `maxBytes`,
149
+ * one byte past the cap so the read still sees the overflow and cuts on a line, as it
150
+ * does for an unranged body. The bytes past it never leave the server. */
151
+ function headRangeFor(knownSize, maxBytes) {
152
+ return typeof knownSize === "number" && knownSize > maxBytes + 1 ? `bytes=0-${maxBytes}` : null;
153
+ }
148
154
  /** Range header for reading the last `maxBytes` of an object. A closed range is a
149
155
  * CORS-safelisted header value, so it travels without a preflight that a bucket's
150
156
  * CORS rules may not answer; the suffix form is not safelisted, and is only reached
@@ -193,4 +199,4 @@ async function readTailText(response, maxBytes, maxLines, startsMidObject) {
193
199
  };
194
200
  }
195
201
  //#endregion
196
- export { countLines, lastLines, readCappedBytes, readCappedText, readTailText, tailRangeFor, tailWindow };
202
+ export { countLines, headRangeFor, lastLines, readCappedBytes, readCappedText, readTailText, tailRangeFor, tailWindow };
@@ -1,6 +1,6 @@
1
1
  import { CorsError } from "../lib/errors.js";
2
2
  import { fetchPreviewObject, loadPreviewContent, peekPreviewContent } from "./previewCache.js";
3
- import { readCappedText, readTailText, tailRangeFor, tailWindow } from "./readCappedBytes.js";
3
+ import { headRangeFor, readCappedText, readTailText, tailRangeFor, tailWindow } from "./readCappedBytes.js";
4
4
  import { useEffect, useState } from "react";
5
5
  //#region src/file-explorer/FilePreview/useObjectText.ts
6
6
  var IDLE = {
@@ -18,10 +18,10 @@ function toState(result) {
18
18
  };
19
19
  }
20
20
  async function readObjectText(signal, { url, cacheKey, maxBytes, maxLines, knownSize, tail }) {
21
- const range = tail ? tailRangeFor(knownSize, maxBytes) : null;
21
+ const range = tail ? tailRangeFor(knownSize, maxBytes) : headRangeFor(knownSize, maxBytes);
22
22
  const response = await fetchPreviewObject(cacheKey, url, signal, range ? { Range: range } : void 0);
23
23
  let startsMidObject = false;
24
- if (range) {
24
+ if (tail && range) {
25
25
  const window = tailWindow(response, maxBytes);
26
26
  if (!window) throw new Error("range request was not honored");
27
27
  startsMidObject = window.startsMidObject;
@@ -6,5 +6,5 @@ export { CorsError } from './lib/errors.js';
6
6
  export type { FileExplorerDeleteFilesResult, FileExplorerGetFileResult, IFileExplorerClient, IFileExplorerProvider, } from './lib/fileExplorer.js';
7
7
  export type { TListPageRequest } from './lib/paging.js';
8
8
  export { LIST_PAGE_SIZE } from './lib/paging.js';
9
- export type { ExplorerObject, TreeNode, TStorage, TStorageObject, } from './lib/types.js';
9
+ export type { ExplorerObject, PreviewLimits, TreeNode, TStorage, TStorageObject, } from './lib/types.js';
10
10
  export { attachmentDisposition, collectObjectKeysToDelete, getObjectKeyFromNode, isClientValid, removeLeadingAndTrailingSlashes, } from './lib/utils.js';
@@ -1,4 +1,14 @@
1
1
  import type { IFileExplorerClient, IFileExplorerProvider } from './fileExplorer.js';
2
+ /** Above these a file is not fetched for preview; the pane offers a download instead. */
3
+ export type PreviewLimits = {
4
+ codeBytes: number;
5
+ codeLines: number;
6
+ tabularBytes: number;
7
+ tabularRows: number;
8
+ imageBytes: number;
9
+ pdfBytes: number;
10
+ notebookBytes: number;
11
+ };
2
12
  export type TStorage = {
3
13
  id: string;
4
14
  name: string;
@@ -14,6 +24,7 @@ export type TStorage = {
14
24
  canUpload?: boolean | undefined;
15
25
  canShare?: boolean | undefined;
16
26
  canManageAccess?: boolean | undefined;
27
+ previewLimits?: Partial<PreviewLimits> | undefined;
17
28
  };
18
29
  /** A file the host is asked to name or download: its storage, key and name. */
19
30
  export interface ExplorerObject {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallelworks/ui",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "description": "React UI components: a theme contract, icons, primitives, lists, forms, a job graph, a code editor, a log viewer, a file explorer and an AI chat, each on its own subpath",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {