@knime/product-features 2.2.2 → 2.3.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/core-CVWznjtP.js +739 -0
  3. package/dist/core-CVWznjtP.js.map +1 -0
  4. package/dist/dataTransfer/core.js +2 -0
  5. package/dist/dataTransfer.css +12 -12
  6. package/dist/dataTransfer.js +420 -767
  7. package/dist/dataTransfer.js.map +1 -1
  8. package/dist/src/dataTransfer/core.d.ts +2 -0
  9. package/dist/src/dataTransfer/core.d.ts.map +1 -0
  10. package/dist/src/dataTransfer/index.d.ts +1 -2
  11. package/dist/src/dataTransfer/index.d.ts.map +1 -1
  12. package/dist/src/dataTransfer/upload/{ui/UploadProgressPanel.vue.d.ts → UploadProgressPanel.vue.d.ts} +2 -0
  13. package/dist/src/dataTransfer/upload/UploadProgressPanel.vue.d.ts.map +1 -0
  14. package/dist/src/dataTransfer/upload/{ui/UploadProgressPanelItem.vue.d.ts → UploadProgressPanelItem.vue.d.ts} +2 -1
  15. package/dist/src/dataTransfer/upload/UploadProgressPanelItem.vue.d.ts.map +1 -0
  16. package/dist/src/dataTransfer/upload/core/index.d.ts +3 -0
  17. package/dist/src/dataTransfer/upload/core/index.d.ts.map +1 -0
  18. package/dist/src/dataTransfer/upload/core/uploadClient/createUploadStatusPoller.d.ts +26 -0
  19. package/dist/src/dataTransfer/upload/core/uploadClient/createUploadStatusPoller.d.ts.map +1 -0
  20. package/dist/src/dataTransfer/upload/core/uploadClient/index.d.ts +3 -0
  21. package/dist/src/dataTransfer/upload/core/uploadClient/index.d.ts.map +1 -0
  22. package/dist/src/dataTransfer/upload/core/uploadClient/uploadClient.d.ts +59 -0
  23. package/dist/src/dataTransfer/upload/core/uploadClient/uploadClient.d.ts.map +1 -0
  24. package/dist/src/dataTransfer/upload/{manager/index.d.ts → core/uploadManager/chunkedUploadManager.d.ts} +21 -60
  25. package/dist/src/dataTransfer/upload/core/uploadManager/chunkedUploadManager.d.ts.map +1 -0
  26. package/dist/src/dataTransfer/upload/core/uploadManager/config.d.ts +12 -0
  27. package/dist/src/dataTransfer/upload/core/uploadManager/config.d.ts.map +1 -0
  28. package/dist/src/dataTransfer/upload/core/uploadManager/index.d.ts +5 -0
  29. package/dist/src/dataTransfer/upload/core/uploadManager/index.d.ts.map +1 -0
  30. package/dist/src/dataTransfer/upload/core/uploadManager/state.d.ts +30 -0
  31. package/dist/src/dataTransfer/upload/core/uploadManager/state.d.ts.map +1 -0
  32. package/dist/src/dataTransfer/upload/core/uploadManager/streamingUploadManager.d.ts +52 -0
  33. package/dist/src/dataTransfer/upload/core/uploadManager/streamingUploadManager.d.ts.map +1 -0
  34. package/dist/src/dataTransfer/upload/core/uploadManager/types.d.ts +46 -0
  35. package/dist/src/dataTransfer/upload/core/uploadManager/types.d.ts.map +1 -0
  36. package/dist/src/dataTransfer/upload/core/uploadManager/uploadPartBytes.d.ts +17 -0
  37. package/dist/src/dataTransfer/upload/core/uploadManager/uploadPartBytes.d.ts.map +1 -0
  38. package/dist/src/dataTransfer/upload/index.d.ts +6 -0
  39. package/dist/src/dataTransfer/upload/index.d.ts.map +1 -0
  40. package/dist/src/dataTransfer/upload/types.d.ts +80 -0
  41. package/dist/src/dataTransfer/upload/types.d.ts.map +1 -0
  42. package/dist/src/dataTransfer/upload/useFileUpload.d.ts +5 -13
  43. package/dist/src/dataTransfer/upload/useFileUpload.d.ts.map +1 -1
  44. package/dist/src/dataTransfer/upload/{ui/useUploadManager.d.ts → useUploadManager.d.ts} +17 -5
  45. package/dist/src/dataTransfer/upload/useUploadManager.d.ts.map +1 -0
  46. package/package.json +8 -4
  47. package/dist/src/dataTransfer/upload/manager/index.d.ts.map +0 -1
  48. package/dist/src/dataTransfer/upload/ui/UploadProgressPanel.vue.d.ts.map +0 -1
  49. package/dist/src/dataTransfer/upload/ui/UploadProgressPanelItem.vue.d.ts.map +0 -1
  50. package/dist/src/dataTransfer/upload/ui/index.d.ts +0 -5
  51. package/dist/src/dataTransfer/upload/ui/index.d.ts.map +0 -1
  52. package/dist/src/dataTransfer/upload/ui/types.d.ts +0 -60
  53. package/dist/src/dataTransfer/upload/ui/types.d.ts.map +0 -1
  54. package/dist/src/dataTransfer/upload/ui/useUploadManager.d.ts.map +0 -1
@@ -1,9 +1,10 @@
1
1
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
2
- import { a as getFileExtension, c as promise_exports, l as sleep, o as getFileMimeType, r as formatFileSize, s as knimeFileFormats } from "./dist-DvDWkkNg.js";
2
+ import { a as getFileExtension, c as promise_exports, l as sleep, r as formatFileSize, s as knimeFileFormats } from "./dist-DvDWkkNg.js";
3
3
  import { t as rfcErrors } from "./rfcErrors-B2gYd5aG.js";
4
- import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, openBlock, reactive, readonly, ref, renderList, renderSlot, toDisplayString, unref, useId, useModel, vShow, watch, withCtx, withDirectives } from "vue";
4
+ import { a as createUploadManager, c as API_BASE_URL, n as createUploadClient, o as createHttpClient, s as defaultConfig, t as createUploadStatusPoller } from "./core-CVWznjtP.js";
5
+ import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, useId, useModel, vShow, watch, withCtx, withDirectives } from "vue";
5
6
  import { KdsBadge, KdsButton, KdsIcon, KdsLoadingSkeleton, KdsProgressBar, vKdsTooltip } from "@knime/kds-components";
6
- import { FetchError, ofetch } from "ofetch";
7
+ import { FetchError } from "ofetch";
7
8
  import './dataTransfer.css';//#region src/dataTransfer/shared/ui/CollapsiblePanel/useIconFlipper.ts
8
9
  var useIconFlipper = (options) => {
9
10
  return { iconFlipStyles: computed(() => {
@@ -157,170 +158,6 @@ function _sfc_render(_ctx, _cache) {
157
158
  }
158
159
  var ProgressList_default = /*#__PURE__*/ _plugin_vue_export_helper_default(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-8f14a3cb"]]);
159
160
  //#endregion
160
- //#region src/dataTransfer/upload/ui/UploadProgressPanelItem.vue
161
- var UploadProgressPanelItem_default = /* @__PURE__ */ defineComponent({
162
- __name: "UploadProgressPanelItem",
163
- props: {
164
- item: {},
165
- allowCancel: { type: Boolean }
166
- },
167
- emits: ["cancel"],
168
- setup(__props, { emit: __emit }) {
169
- const props = __props;
170
- const emit = __emit;
171
- const hasProgress = (item) => {
172
- return item.status === "inprogress" || item.status === "complete";
173
- };
174
- const totalFormatted = computed(() => {
175
- return formatFileSize(props.item.size);
176
- });
177
- const progressedFormatted = computed(() => {
178
- if (hasProgress(props.item)) return formatFileSize(props.item.size / 100 * (props.item.progress ?? 0));
179
- return 0;
180
- });
181
- const subtitle = computed(() => props.item.status === "inprogress" ? `${progressedFormatted.value} of ${totalFormatted.value}` : `${totalFormatted.value}`);
182
- const statusMapper = {
183
- inprogress: [
184
- "Uploading",
185
- "info",
186
- "reload"
187
- ],
188
- failed: [
189
- "Failed",
190
- "error",
191
- "circle-error"
192
- ],
193
- complete: [
194
- "Uploaded",
195
- "success",
196
- "circle-success"
197
- ],
198
- cancelled: [
199
- "Cancelled",
200
- "error",
201
- "circle-error"
202
- ],
203
- processing: [
204
- "Processing",
205
- "info",
206
- "reload"
207
- ]
208
- };
209
- const itemIcon = computed(() => {
210
- return getFileExtension(props.item.name) === knimeFileFormats.KNWF.extension ? "workflow" : "file";
211
- });
212
- const badge = computed(() => {
213
- if (!props.item.status) return;
214
- const [text, variant, icon] = statusMapper[props.item.status];
215
- return {
216
- text,
217
- variant,
218
- icon
219
- };
220
- });
221
- const shouldShowCancelAction = computed(() => props.allowCancel && props.item.status === "inprogress");
222
- const actions = computed(() => {
223
- return shouldShowCancelAction.value ? [{
224
- id: "cancel",
225
- icon: "x-close",
226
- label: "Cancel",
227
- handler: () => emit("cancel")
228
- }] : [];
229
- });
230
- return (_ctx, _cache) => {
231
- return openBlock(), createBlock(unref(ProgressItem_default), {
232
- id: __props.item.id,
233
- headline: __props.item.name,
234
- subtitle: subtitle.value,
235
- icon: itemIcon.value,
236
- progress: hasProgress(__props.item) ? __props.item.progress : void 0,
237
- badge: badge.value,
238
- actions: actions.value
239
- }, null, 8, [
240
- "id",
241
- "headline",
242
- "subtitle",
243
- "icon",
244
- "progress",
245
- "badge",
246
- "actions"
247
- ]);
248
- };
249
- }
250
- });
251
- //#endregion
252
- //#region src/dataTransfer/upload/ui/UploadProgressPanel.vue?vue&type=script&setup=true&lang.ts
253
- var _hoisted_1$1 = { class: "upload-panel-content" };
254
- var _hoisted_2 = { class: "skeleton-text-content" };
255
- //#endregion
256
- //#region src/dataTransfer/upload/ui/UploadProgressPanel.vue
257
- var UploadProgressPanel_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
258
- __name: "UploadProgressPanel",
259
- props: /*@__PURE__*/ mergeModels({
260
- items: {},
261
- allowCancel: {
262
- type: Boolean,
263
- default: true
264
- },
265
- allowRemove: { type: Boolean },
266
- placeholderItems: { default: 0 }
267
- }, {
268
- "expanded": {
269
- type: Boolean,
270
- default: true
271
- },
272
- "expandedModifiers": {}
273
- }),
274
- emits: /*@__PURE__*/ mergeModels(["cancel", "close"], ["update:expanded"]),
275
- setup(__props, { emit: __emit }) {
276
- const emit = __emit;
277
- const expanded = useModel(__props, "expanded");
278
- const totalItems = computed(() => __props.items.length);
279
- const uploadedItems = computed(() => __props.items.filter(({ status }) => status === "complete").length);
280
- const headline = computed(() => {
281
- const suffix = `file${totalItems.value > 1 ? "s" : ""}`;
282
- return `Uploaded ${uploadedItems.value} of ${totalItems.value} ${suffix}`;
283
- });
284
- const hasSomeItemInProgress = computed(() => __props.items.some(({ status }) => status === "inprogress" || status === "processing") || __props.placeholderItems > 0);
285
- return (_ctx, _cache) => {
286
- return openBlock(), createBlock(unref(CollapsiblePanel_default), {
287
- modelValue: expanded.value,
288
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => expanded.value = $event),
289
- headline: headline.value,
290
- class: "upload-panel",
291
- "can-be-dismissed": !hasSomeItemInProgress.value,
292
- onClose: _cache[1] || (_cache[1] = ($event) => emit("close"))
293
- }, {
294
- default: withCtx(() => [createElementVNode("div", _hoisted_1$1, [createVNode(unref(ProgressList_default), null, {
295
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
296
- return openBlock(), createBlock(UploadProgressPanelItem_default, {
297
- key: item.id,
298
- item,
299
- "allow-cancel": __props.allowCancel,
300
- onCancel: ($event) => emit("cancel", item)
301
- }, null, 8, [
302
- "item",
303
- "allow-cancel",
304
- "onCancel"
305
- ]);
306
- }), 128))]),
307
- _: 1
308
- }), __props.placeholderItems > 0 ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(__props.placeholderItems, (skeleton) => {
309
- return openBlock(), createElementBlock("div", {
310
- key: skeleton,
311
- class: "skeleton-container"
312
- }, [createVNode(unref(KdsLoadingSkeleton), { variant: "icon-large" }), createElementVNode("div", _hoisted_2, [createVNode(unref(KdsLoadingSkeleton), { variant: "label" }), createVNode(unref(KdsLoadingSkeleton), { variant: "text" })])]);
313
- }), 128)) : createCommentVNode("", true)])]),
314
- _: 1
315
- }, 8, [
316
- "modelValue",
317
- "headline",
318
- "can-be-dismissed"
319
- ]);
320
- };
321
- }
322
- }), [["__scopeId", "data-v-15b3eb7d"]]);
323
- //#endregion
324
161
  //#region src/dataTransfer/download/ui/DownloadProgressPanelItem.vue
325
162
  var DownloadProgressPanelItem_default = /* @__PURE__ */ defineComponent({
326
163
  __name: "DownloadProgressPanelItem",
@@ -395,7 +232,7 @@ var DownloadProgressPanelItem_default = /* @__PURE__ */ defineComponent({
395
232
  });
396
233
  //#endregion
397
234
  //#region src/dataTransfer/download/ui/DownloadProgressPanel.vue?vue&type=script&setup=true&lang.ts
398
- var _hoisted_1 = { class: "download-panel-content" };
235
+ var _hoisted_1$1 = { class: "download-panel-content" };
399
236
  //#endregion
400
237
  //#region src/dataTransfer/download/ui/DownloadProgressPanel.vue
401
238
  var DownloadProgressPanel_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
@@ -435,7 +272,7 @@ var DownloadProgressPanel_default = /*#__PURE__*/ _plugin_vue_export_helper_defa
435
272
  "can-be-dismissed": !hasItemsInProgress.value,
436
273
  onClose: _cache[1] || (_cache[1] = ($event) => emit("close"))
437
274
  }, {
438
- default: withCtx(() => [createElementVNode("div", _hoisted_1, [createVNode(unref(ProgressList_default), null, {
275
+ default: withCtx(() => [createElementVNode("div", _hoisted_1$1, [createVNode(unref(ProgressList_default), null, {
439
276
  default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
440
277
  return openBlock(), createBlock(DownloadProgressPanelItem_default, {
441
278
  key: item.downloadId,
@@ -460,358 +297,383 @@ var DownloadProgressPanel_default = /*#__PURE__*/ _plugin_vue_export_helper_defa
460
297
  }
461
298
  }), [["__scopeId", "data-v-53e10d80"]]);
462
299
  //#endregion
463
- //#region ../../node_modules/.pnpm/es-toolkit@1.49.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
464
- /**
465
- * Checks if a given value is a plain object.
466
- *
467
- * @param value - The value to check.
468
- * @returns True if the value is a plain object, otherwise false.
469
- *
470
- * @example
471
- * ```typescript
472
- * // ✅👇 True
473
- *
474
- * isPlainObject({ }); // ✅
475
- * isPlainObject({ key: 'value' }); // ✅
476
- * isPlainObject({ key: new Date() }); // ✅
477
- * isPlainObject(new Object()); // ✅
478
- * isPlainObject(Object.create(null)); // ✅
479
- * isPlainObject({ nested: { key: true} }); // ✅
480
- * isPlainObject(new Proxy({}, {})); // ✅
481
- * isPlainObject({ [Symbol('tag')]: 'A' }); // ✅
482
- *
483
- * // ✅👇 (cross-realms, node context, workers, ...)
484
- * const runInNewContext = await import('node:vm').then(
485
- * (mod) => mod.runInNewContext
486
- * );
487
- * isPlainObject(runInNewContext('({})')); // ✅
488
- *
489
- * // ❌👇 False
490
- *
491
- * class Test { };
492
- * isPlainObject(new Test()) // ❌
493
- * isPlainObject(10); // ❌
494
- * isPlainObject(null); // ❌
495
- * isPlainObject('hello'); // ❌
496
- * isPlainObject([]); // ❌
497
- * isPlainObject(new Date()); // ❌
498
- * isPlainObject(new Uint8Array([1])); // ❌
499
- * isPlainObject(Buffer.from('ABC')); // ❌
500
- * isPlainObject(Promise.resolve({})); // ❌
501
- * isPlainObject(Object.create({})); // ❌
502
- * isPlainObject(new (class Cls {})); // ❌
503
- * isPlainObject(globalThis); // ❌,
504
- * ```
505
- */
506
- function isPlainObject(value) {
507
- if (!value || typeof value !== "object") return false;
508
- const proto = Object.getPrototypeOf(value);
509
- if (!(proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null)) return false;
510
- return Object.prototype.toString.call(value) === "[object Object]";
511
- }
512
- //#endregion
513
- //#region ../../node_modules/.pnpm/es-toolkit@1.49.0/node_modules/es-toolkit/dist/_internal/isUnsafeProperty.mjs
514
- /**
515
- * Checks if a property key is unsafe to modify directly.
516
- *
517
- * This function is used in functions like `merge` to prevent prototype pollution attacks
518
- * by identifying property keys that could modify the object's prototype chain or constructor.
519
- *
520
- * @param key - The property key to check
521
- * @returns `true` if the property is unsafe to modify directly, `false` otherwise
522
- * @internal
523
- */
524
- function isUnsafeProperty(key) {
525
- return key === "__proto__";
526
- }
527
- //#endregion
528
- //#region ../../node_modules/.pnpm/es-toolkit@1.49.0/node_modules/es-toolkit/dist/object/merge.mjs
529
- /**
530
- * Merges the properties of the source object into the target object.
531
- *
532
- * This function performs a deep merge, meaning nested objects and arrays are merged recursively.
533
- * If a property in the source object is an array or an object and the corresponding property in the target object is also an array or object, they will be merged.
534
- * If a property in the source object is undefined, it will not overwrite a defined property in the target object.
535
- *
536
- * Note that this function mutates the target object.
537
- *
538
- * @param target - The target object into which the source object properties will be merged. This object is modified in place.
539
- * @param source - The source object whose properties will be merged into the target object.
540
- * @returns The updated target object with properties from the source object merged in.
541
- *
542
- * @template T - Type of the target object.
543
- * @template S - Type of the source object.
544
- *
545
- * @example
546
- * const target = { a: 1, b: { x: 1, y: 2 } };
547
- * const source = { b: { y: 3, z: 4 }, c: 5 };
548
- *
549
- * const result = merge(target, source);
550
- * console.log(result);
551
- * // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
552
- *
553
- * @example
554
- * const target = { a: [1, 2], b: { x: 1 } };
555
- * const source = { a: [3], b: { y: 2 } };
556
- *
557
- * const result = merge(target, source);
558
- * console.log(result);
559
- * // Output: { a: [3, 2], b: { x: 1, y: 2 } }
560
- *
561
- * @example
562
- * const target = { a: null };
563
- * const source = { a: [1, 2, 3] };
564
- *
565
- * const result = merge(target, source);
566
- * console.log(result);
567
- * // Output: { a: [1, 2, 3] }
568
- */
569
- function merge(target, source) {
570
- const sourceKeys = Object.keys(source);
571
- for (let i = 0; i < sourceKeys.length; i++) {
572
- const key = sourceKeys[i];
573
- if (isUnsafeProperty(key)) continue;
574
- const sourceValue = source[key];
575
- const targetValue = target[key];
576
- if (isMergeableValue(sourceValue) && isMergeableValue(targetValue)) target[key] = merge(targetValue, sourceValue);
577
- else if (Array.isArray(sourceValue)) target[key] = merge([], sourceValue);
578
- else if (isPlainObject(sourceValue)) target[key] = merge({}, sourceValue);
579
- else if (targetValue === void 0 || sourceValue !== void 0) target[key] = sourceValue;
580
- }
581
- return target;
582
- }
583
- function isMergeableValue(value) {
584
- return isPlainObject(value) || Array.isArray(value);
585
- }
586
- //#endregion
587
- //#region src/dataTransfer/shared/httpClient/constants.ts
588
- var API_BASE_URL = "/_/api";
589
- //#endregion
590
- //#region src/dataTransfer/shared/httpClient/createHttpClient.ts
591
- var defaultConfig = {
592
- baseURL: API_BASE_URL,
593
- headers: {
594
- "Content-Type": "application/json",
595
- Accept: "application/json; application/problem+json"
596
- }
597
- };
598
- var createHttpClient = (customOptions) => {
599
- const { baseURL = API_BASE_URL, ...otherCustomOptions } = customOptions ?? {};
600
- return ofetch.create(merge({
601
- baseURL,
602
- ...otherCustomOptions
603
- }, defaultConfig));
604
- };
605
- //#endregion
606
- //#region src/dataTransfer/upload/manager/index.ts
607
- var CHUNK_SIZE = 50 * 1024 * 1024;
608
- var DEFAULT_RETRY_COUNT = 5;
609
- var DEFAULT_RETRY_DELAY_MS$1 = 50;
610
- var uploadChunkWithProgress = (params) => {
611
- const { method, url, chunk, chunkIndex, onProgress, abortSignal } = params;
612
- const OK = 200;
613
- const CREATED = 201;
614
- return new Promise((resolve, reject) => {
615
- const xhr = new XMLHttpRequest();
616
- xhr.open(method, url, true);
617
- abortSignal.addEventListener("abort", () => xhr.abort());
618
- xhr.upload.onprogress = (event) => {
619
- if (event.lengthComputable) onProgress(event.loaded);
620
- };
621
- xhr.onload = () => {
622
- if (xhr.status === OK || xhr.status === CREATED) resolve({ partId: xhr.getResponseHeader("ETag")?.replace(/^(?:W\/)?"|"$/g, "") ?? "" });
623
- else {
624
- consola.error("Failed or unexpected XHR response", {
625
- xhrStatus: xhr.status,
626
- xhrResponse: xhr.response,
627
- chunkIndex
628
- });
629
- reject(/* @__PURE__ */ new Error("Failed to upload file"));
630
- }
631
- };
632
- xhr.onerror = (event) => {
633
- consola.error("Failed to upload chunk", {
634
- xhrErrorEvent: event,
635
- chunkIndex
636
- });
637
- reject(/* @__PURE__ */ new Error("Failed to upload file"));
638
- };
639
- xhr.send(chunk);
640
- });
300
+ //#region src/dataTransfer/download/useDownloadArtifact.ts
301
+ var DEFAULT_MAX_RETRIES = null;
302
+ var DEFAULT_POLLING_INTERVAL = 2e3;
303
+ var hasDownloadUrl = (response) => {
304
+ return "downloadUrl" in response;
641
305
  };
306
+ var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
642
307
  /**
643
- * Creates and returns an `uploadManager`, a comprehensive multi-file, multi-part upload handler.
644
- *
645
- * The `uploadManager` offers functionality for handling file uploads in parts and provides
646
- * several built-in mechanisms to ensure the process is reliable and adaptable.
647
- *
648
- * Key features include:
308
+ * This composable supports multiple concurrent downloads. Each time you call ```start```, the following behavior applies:
649
309
  *
650
- * - **Chunked File Uploads**: Splits provided files into manageable chunks and initiates the upload
651
- * for each part using the `resolveFilePartUploadURL` function provided in the configuration.
310
+ * 1. **Immediate-ready artifact**: If the first status check for the artifact returns `READY`, then there's no polling and no need
311
+ * to add it to `downloadItems`. The download URL is opened in the same tab immediately.
652
312
  *
653
- * - **Retry Mechanism**: Automatically retries failed upload attempts up to a maximum of 5 times
654
- * per failed request.
313
+ * 2. **Not-ready artifact**: If the artifact is not yet ready (e.g. needs zipping), then we add a record to `downloadItems` with status `IN_PROGRESS`
314
+ * and repeatedly poll until it becomes `READY`, the polling times out, or the user aborts.
655
315
  *
656
- * - **Upload Cancellation**: Allows cancellation of ongoing uploads, giving users control over the process.
316
+ * **Usage**:
317
+ * ```
318
+ * const { start, cancel, downloadItems } = useDownloadArtifact({ ...options });
657
319
  *
658
- * - **Progress Tracking**: Monitors the progress of each file part upload and triggers the `onProgress`
659
- * callback to provide updates.
320
+ * // start a download
321
+ * await start({ itemId: "1234", version: 1 });
660
322
  *
661
- * - **Error Handling**: The returned `uploadFiles` function does not throw errors directly. Instead,
662
- * it manages retries and stops any failed uploads after the retry limit is reached, marking them as failed.
663
- * This ensures that the promise it returns completes gracefully without uncaught exceptions.
323
+ * // if needed, cancel it:
324
+ * cancel("1234");
664
325
  *
326
+ * // downloadItems (as a computed array) will reflect the current statuses.
327
+ * ```
665
328
  */
666
- var createUploadManager = (config) => {
667
- const { resolveFilePartUploadURL: getUploadFilePartUrl, onFileUploadComplete, onFileUploadFailed } = config;
668
- const uploadStateMap = /* @__PURE__ */ new Map();
669
- const completedUploads = /* @__PURE__ */ new Set();
670
- const cancelledUploads = /* @__PURE__ */ new Set();
671
- const failedUploads = /* @__PURE__ */ new Set();
672
- const assertState = (uploadId, state) => uploadStateMap.has(uploadId) && uploadStateMap.get(uploadId).state === state;
673
- const getUploadState = (uploadId) => uploadStateMap.get(uploadId);
674
- const setUploadState = (uploadId, state) => {
675
- uploadStateMap.set(uploadId, {
676
- ...uploadStateMap.get(uploadId),
677
- ...state
329
+ var useDownloadArtifact = (options = {}) => {
330
+ const $ofetch = createHttpClient(options.customFetchClientOptions);
331
+ const downloadItems = ref({});
332
+ const abortControllers = {};
333
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
334
+ const pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
335
+ const cancel = (downloadId) => {
336
+ const itemController = abortControllers[downloadId];
337
+ if (itemController) {
338
+ itemController.abort();
339
+ delete abortControllers[downloadId];
340
+ }
341
+ };
342
+ const removeItem = (downloadId) => {
343
+ cancel(downloadId);
344
+ delete downloadItems.value[downloadId];
345
+ };
346
+ const fetchDownloadStatus = ({ downloadId, signal }) => {
347
+ return $ofetch(`/downloads/${downloadId}/status`, {
348
+ method: "GET",
349
+ signal
678
350
  });
679
351
  };
680
- const isFailed = (uploadId) => assertState(uploadId, "failed");
681
- const isCancelled = (uploadId) => assertState(uploadId, "cancelled");
682
- const getTotalFileParts = (file) => Math.ceil(file.size / CHUNK_SIZE);
683
- const abortInFlightUpload = (uploadId, reason) => {
684
- const currentState = uploadStateMap.get(uploadId);
685
- if (!currentState) return;
686
- currentState.abortController.abort(new promise_exports.AbortError(`Upload (${currentState.uploadId}) for file "${currentState.file.name}" aborted. Reason: "${reason}"`));
352
+ const openDownload = (downloadUrl) => {
353
+ const a = document.createElement("a");
354
+ a.target = "_blank";
355
+ a.href = downloadUrl;
356
+ a.download = "";
357
+ a.style.display = "none";
358
+ document.body.appendChild(a);
359
+ a.click();
360
+ document.body.removeChild(a);
687
361
  };
688
- const cancel = (uploadId) => {
689
- if (!uploadStateMap.has(uploadId)) {
690
- consola.error("cancel:: Invalid upload id");
691
- return;
362
+ /**
363
+ * Makes a request to the download status endpoint for the given download item.
364
+ * Returns a boolean that is true if a terminal state has been reached (download is ready,
365
+ * download failed, or user aborted download)
366
+ * @param downloadId the `downloadId`
367
+ * @param signal the abort signal passed to the request to be able to abort it
368
+ * @returns a boolean indicating whether a terminal state has been reached
369
+ */
370
+ const handlePollingResult = async ({ downloadId, signal }) => {
371
+ try {
372
+ const statusResponse = await fetchDownloadStatus({
373
+ downloadId,
374
+ signal
375
+ });
376
+ if (statusResponse?.status === "READY" && statusResponse?.downloadUrl) {
377
+ downloadItems.value[downloadId].status = "READY";
378
+ downloadItems.value[downloadId].downloadUrl = statusResponse.downloadUrl;
379
+ openDownload(statusResponse.downloadUrl);
380
+ return true;
381
+ } else if (statusResponse?.status === "FAILED") {
382
+ let details = "Download failed";
383
+ if (statusResponse?.statusMessage) details += `: ${statusResponse.statusMessage}`;
384
+ throw new Error(details);
385
+ }
386
+ } catch (error) {
387
+ consola.error("Error fetching status:", error);
388
+ if (!(isAbortError(error) || error instanceof FetchError && isAbortError(error.cause))) {
389
+ downloadItems.value[downloadId].status = "FAILED";
390
+ downloadItems.value[downloadId].failureDetails = error instanceof Error ? error.message : error;
391
+ }
392
+ return true;
692
393
  }
693
- setUploadState(uploadId, { state: "cancelled" });
694
- abortInFlightUpload(uploadId, "upload cancelled");
695
- cancelledUploads.add(uploadId);
394
+ return false;
696
395
  };
697
- const setFailed = (uploadId, error) => {
698
- if (!uploadStateMap.has(uploadId)) {
699
- consola.error("failed:: Invalid upload id");
700
- return;
396
+ const pollDownloadItem = async ({ itemId, downloadId, version, name }) => {
397
+ const controller = new AbortController();
398
+ abortControllers[downloadId] = controller;
399
+ const signal = controller.signal;
400
+ let itemName = `${name}`;
401
+ if (version) itemName += ` (${version})`;
402
+ downloadItems.value[downloadId] = {
403
+ itemId,
404
+ ...version && { version },
405
+ downloadId,
406
+ name: itemName,
407
+ status: "IN_PROGRESS"
408
+ };
409
+ const abortListener = () => {
410
+ controller.signal.removeEventListener("abort", abortListener);
411
+ if (!downloadItems.value[downloadId]) return;
412
+ downloadItems.value[downloadId].status = "CANCELLED";
413
+ downloadItems.value[downloadId].failureDetails = "Download was cancelled manually";
414
+ };
415
+ controller.signal.addEventListener("abort", abortListener);
416
+ let tries = 0;
417
+ while (maxRetries === null || tries < maxRetries) {
418
+ tries++;
419
+ await sleep(pollingInterval);
420
+ if (await handlePollingResult({
421
+ downloadId,
422
+ signal
423
+ })) break;
424
+ }
425
+ delete abortControllers[downloadId];
426
+ if (maxRetries !== null && tries >= maxRetries) {
427
+ downloadItems.value[downloadId].status = "FAILED";
428
+ downloadItems.value[downloadId].failureDetails = "Download timed out";
701
429
  }
702
- setUploadState(uploadId, { state: "failed" });
703
- onFileUploadFailed?.({
704
- uploadId,
705
- error
706
- });
707
- failedUploads.add(uploadId);
708
- if (completedUploads.has(uploadId)) completedUploads.delete(uploadId);
709
430
  };
710
- const uploadFileFromChunkIndex = async (uploadId, file, initialChunkIndex = 0) => {
711
- const totalChunks = getTotalFileParts(file);
712
- const totalBytes = file.size;
713
- const completedPartIds = {};
714
- const { abortController, runAbortablePromise } = promise_exports.createAbortablePromise();
715
- uploadStateMap.set(uploadId, {
716
- uploadId,
717
- file,
718
- state: "inprogress",
719
- uploadedByteCount: uploadStateMap.get(uploadId)?.uploadedByteCount ?? 0,
720
- completedChunkCount: uploadStateMap.get(uploadId)?.completedChunkCount ?? 0,
721
- abortController
722
- });
723
- let chunkIndex = initialChunkIndex;
724
- let uploadedBytes = uploadStateMap.get(uploadId)?.uploadedByteCount;
725
- while (chunkIndex < totalChunks) {
726
- if (isCancelled(uploadId) || isFailed(uploadId)) break;
727
- const start = chunkIndex * CHUNK_SIZE;
728
- const end = Math.min(start + CHUNK_SIZE, file.size);
729
- const chunk = file.slice(start, end);
730
- try {
731
- const { method, url } = await promise_exports.retryPromise({
732
- fn: () => getUploadFilePartUrl(uploadId, chunkIndex + 1),
733
- retryCount: DEFAULT_RETRY_COUNT,
734
- retryDelayMS: DEFAULT_RETRY_DELAY_MS$1
735
- });
736
- consola.trace("File part upload url resolved", {
737
- method,
738
- url
739
- });
740
- const onChunkUploadProgress = (uploadedChunkBytes) => {
741
- uploadedBytes = uploadedChunkBytes + start;
742
- setUploadState(uploadId, { uploadedByteCount: uploadedBytes });
743
- const progress = Math.round(uploadedBytes / totalBytes * 100);
744
- config.onProgress?.(uploadId, progress);
745
- };
746
- const filePartResponse = await runAbortablePromise(() => promise_exports.retryPromise({
747
- fn: () => uploadChunkWithProgress({
748
- method,
749
- url,
750
- chunk,
751
- chunkIndex,
752
- onProgress: onChunkUploadProgress,
753
- abortSignal: abortController.signal
754
- }),
755
- retryCount: DEFAULT_RETRY_COUNT,
756
- retryDelayMS: DEFAULT_RETRY_DELAY_MS$1
757
- }));
758
- consola.trace(`${file.name}: Chunk ${chunkIndex + 1} of ${totalChunks} uploaded successfully`);
759
- completedPartIds[chunkIndex + 1] = filePartResponse.partId;
760
- setUploadState(uploadId, { completedChunkCount: uploadStateMap.get(uploadId).completedChunkCount + 1 });
761
- } catch (error) {
762
- if (error instanceof promise_exports.AbortError) {
763
- consola.info("Aborted::", error);
764
- return;
765
- }
766
- setFailed(uploadId, error instanceof Error ? error : new Error(error));
431
+ const start = async ({ itemId, version, name }) => {
432
+ try {
433
+ const response = await $ofetch(`/repository/${itemId}/artifact`, {
434
+ method: "GET",
435
+ query: { version }
436
+ });
437
+ if (hasDownloadUrl(response)) {
438
+ openDownload(response.downloadUrl);
767
439
  return;
768
440
  }
769
- chunkIndex++;
441
+ await pollDownloadItem({
442
+ itemId,
443
+ version,
444
+ downloadId: response.downloadId,
445
+ name
446
+ });
447
+ } catch (error) {
448
+ throw rfcErrors.tryParse(error) ?? error;
770
449
  }
771
- if (isCancelled(uploadId) || isFailed(uploadId)) return;
772
- setUploadState(uploadId, {
773
- completedChunkCount: totalChunks,
774
- state: "complete"
775
- });
776
- completedUploads.add(uploadId);
777
- onFileUploadComplete?.({
778
- uploadId,
779
- filePartIds: completedPartIds
780
- });
781
450
  };
782
- const uploadFiles = (files) => {
783
- const result = [];
784
- for (const { uploadId, file } of files) result.push(uploadFileFromChunkIndex(uploadId, file));
785
- return Promise.allSettled(result).then(() => {
786
- const completed = [...completedUploads.values()];
787
- const cancelled = [...cancelledUploads.values()];
788
- const failed = [...failedUploads.values()];
789
- [
790
- ...completed,
791
- ...cancelled,
792
- ...failed
793
- ].forEach((uploadId) => uploadStateMap.delete(uploadId));
794
- completedUploads.clear();
795
- cancelledUploads.clear();
796
- failedUploads.clear();
797
- return {
798
- completed,
799
- cancelled,
800
- failed
801
- };
802
- });
451
+ const totalItemsBeingZipped = computed(() => Object.values(downloadItems.value).filter(({ status }) => status === "IN_PROGRESS").length);
452
+ const resetState = () => {
453
+ for (const [downloadId, controller] of Object.entries(abortControllers)) {
454
+ controller?.abort();
455
+ delete abortControllers[downloadId];
456
+ }
457
+ downloadItems.value = {};
803
458
  };
804
459
  return {
805
- uploadFiles,
806
- getUploadState,
807
- isFailed,
808
- isCancelled,
460
+ /**
461
+ * Initiates a download of the given itemId and version.
462
+ */
463
+ start,
464
+ /**
465
+ * Cancels the ongoing download for a given itemId (if any).
466
+ * Sets the status in `downloadItems` to `CANCELLED`.
467
+ */
809
468
  cancel,
810
- setFailed
469
+ /**
470
+ * Removes item from `downloadItems`.
471
+ */
472
+ removeItem,
473
+ /**
474
+ * A computed array of all current items that are being zipped and prepared for download.
475
+ */
476
+ downloadItems: computed(() => Object.values(downloadItems.value)),
477
+ /**
478
+ * The total number of zipping items in progress.
479
+ */
480
+ totalItemsBeingZipped,
481
+ /**
482
+ * Resets the state of the composable, clearing all downloads and controllers.
483
+ */
484
+ resetState,
485
+ /**
486
+ * Opens a download from the given url
487
+ */
488
+ openDownload
811
489
  };
812
490
  };
813
491
  //#endregion
814
- //#region src/dataTransfer/upload/ui/useUploadManager.ts
492
+ //#region src/dataTransfer/upload/UploadProgressPanelItem.vue
493
+ var UploadProgressPanelItem_default = /* @__PURE__ */ defineComponent({
494
+ __name: "UploadProgressPanelItem",
495
+ props: { item: {} },
496
+ emits: ["cancel", "viewDetails"],
497
+ setup(__props, { emit: __emit }) {
498
+ const props = __props;
499
+ const emit = __emit;
500
+ const subtitle = computed(() => {
501
+ if (props.item.type === "file") return `${formatFileSize(props.item.size / 100 * (props.item.progress ?? 0))} of ${formatFileSize(props.item.size)}`;
502
+ if (props.item.type === "directory") {
503
+ const total = props.item.totalEntries;
504
+ return `${props.item.uploadedEntries} of ${total} file${total > 1 ? "s" : ""}`;
505
+ }
506
+ return "";
507
+ });
508
+ const progressPercentageValue = computed(() => {
509
+ if (props.item.type === "file") return props.item.progress;
510
+ if (props.item.type === "directory") return props.item.uploadedEntries * 100 / props.item.totalEntries;
511
+ });
512
+ const statusMapper = {
513
+ inprogress: [
514
+ "Uploading",
515
+ "info",
516
+ "reload"
517
+ ],
518
+ failed: [
519
+ "Failed",
520
+ "error",
521
+ "circle-error"
522
+ ],
523
+ complete: [
524
+ "Uploaded",
525
+ "success",
526
+ "circle-success"
527
+ ],
528
+ cancelled: [
529
+ "Cancelled",
530
+ "error",
531
+ "circle-error"
532
+ ],
533
+ processing: [
534
+ "Processing",
535
+ "info",
536
+ "reload"
537
+ ],
538
+ "partial-success": [
539
+ "Partial success",
540
+ "warning",
541
+ "warning"
542
+ ]
543
+ };
544
+ const itemIcon = computed(() => {
545
+ if (props.item.type === "directory") return "folder";
546
+ return getFileExtension(props.item.name) === knimeFileFormats.KNWF.extension ? "workflow" : "file";
547
+ });
548
+ const badge = computed(() => {
549
+ if (!props.item.status) return;
550
+ const [text, variant, icon] = statusMapper[props.item.status];
551
+ return {
552
+ text,
553
+ variant,
554
+ icon
555
+ };
556
+ });
557
+ const isCancellable = computed(() => {
558
+ if (props.item.actions === void 0 || props.item.actions.cancellable === void 0) return true;
559
+ return props.item.actions.cancellable;
560
+ });
561
+ const shouldShowCancelAction = computed(() => props.item.status === "inprogress" && isCancellable.value);
562
+ const isInspectable = computed(() => {
563
+ if (props.item.actions === void 0 || props.item.actions.inspectable === void 0) return false;
564
+ return props.item.actions.inspectable;
565
+ });
566
+ const shouldShowViewDetailsAction = computed(() => props.item.status === "partial-success" && isInspectable.value);
567
+ const actions = computed(() => {
568
+ const result = [];
569
+ if (shouldShowCancelAction.value) result.push({
570
+ id: "cancel",
571
+ icon: "x-close",
572
+ label: "Cancel",
573
+ handler: () => emit("cancel")
574
+ });
575
+ if (shouldShowViewDetailsAction.value) result.push({
576
+ id: "view-details",
577
+ icon: "circle-info",
578
+ label: "View details",
579
+ handler: () => emit("viewDetails")
580
+ });
581
+ return result;
582
+ });
583
+ return (_ctx, _cache) => {
584
+ return openBlock(), createBlock(unref(ProgressItem_default), {
585
+ id: __props.item.id,
586
+ headline: __props.item.name,
587
+ subtitle: subtitle.value,
588
+ icon: itemIcon.value,
589
+ progress: progressPercentageValue.value,
590
+ badge: badge.value,
591
+ actions: actions.value
592
+ }, null, 8, [
593
+ "id",
594
+ "headline",
595
+ "subtitle",
596
+ "icon",
597
+ "progress",
598
+ "badge",
599
+ "actions"
600
+ ]);
601
+ };
602
+ }
603
+ });
604
+ //#endregion
605
+ //#region src/dataTransfer/upload/UploadProgressPanel.vue?vue&type=script&setup=true&lang.ts
606
+ var _hoisted_1 = { class: "upload-panel-content" };
607
+ var _hoisted_2 = { class: "skeleton-text-content" };
608
+ //#endregion
609
+ //#region src/dataTransfer/upload/UploadProgressPanel.vue
610
+ var UploadProgressPanel_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
611
+ __name: "UploadProgressPanel",
612
+ props: /*@__PURE__*/ mergeModels({
613
+ items: {},
614
+ placeholderItems: { default: 0 }
615
+ }, {
616
+ "expanded": {
617
+ type: Boolean,
618
+ default: true
619
+ },
620
+ "expandedModifiers": {}
621
+ }),
622
+ emits: /*@__PURE__*/ mergeModels([
623
+ "cancel",
624
+ "viewDetails",
625
+ "close"
626
+ ], ["update:expanded"]),
627
+ setup(__props, { emit: __emit }) {
628
+ const emit = __emit;
629
+ const expanded = useModel(__props, "expanded");
630
+ const totalItems = computed(() => __props.items.length);
631
+ const uploadedItems = computed(() => __props.items.filter(({ status }) => status === "complete").length);
632
+ const headline = computed(() => {
633
+ const suffix = `item${totalItems.value > 1 ? "s" : ""}`;
634
+ return `Uploaded ${uploadedItems.value} of ${totalItems.value} ${suffix}`;
635
+ });
636
+ const hasSomeItemInProgress = computed(() => __props.items.some(({ status }) => status === "inprogress" || status === "processing") || __props.placeholderItems > 0);
637
+ return (_ctx, _cache) => {
638
+ return openBlock(), createBlock(unref(CollapsiblePanel_default), {
639
+ modelValue: expanded.value,
640
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => expanded.value = $event),
641
+ headline: headline.value,
642
+ class: "upload-panel",
643
+ "can-be-dismissed": !hasSomeItemInProgress.value,
644
+ onClose: _cache[1] || (_cache[1] = ($event) => emit("close"))
645
+ }, {
646
+ default: withCtx(() => [createElementVNode("div", _hoisted_1, [createVNode(unref(ProgressList_default), null, {
647
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
648
+ return openBlock(), createBlock(UploadProgressPanelItem_default, {
649
+ key: item.id,
650
+ item,
651
+ onCancel: ($event) => emit("cancel", item),
652
+ onViewDetails: ($event) => emit("viewDetails", item)
653
+ }, null, 8, [
654
+ "item",
655
+ "onCancel",
656
+ "onViewDetails"
657
+ ]);
658
+ }), 128))]),
659
+ _: 1
660
+ }), __props.placeholderItems > 0 ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(__props.placeholderItems, (skeleton) => {
661
+ return openBlock(), createElementBlock("div", {
662
+ key: skeleton,
663
+ class: "skeleton-container"
664
+ }, [createVNode(unref(KdsLoadingSkeleton), { variant: "icon-large" }), createElementVNode("div", _hoisted_2, [createVNode(unref(KdsLoadingSkeleton), { variant: "label" }), createVNode(unref(KdsLoadingSkeleton), { variant: "text" })])]);
665
+ }), 128)) : createCommentVNode("", true)])]),
666
+ _: 1
667
+ }, 8, [
668
+ "modelValue",
669
+ "headline",
670
+ "can-be-dismissed"
671
+ ]);
672
+ };
673
+ }
674
+ }), [["__scopeId", "data-v-5b1b5ab8"]]);
675
+ //#endregion
676
+ //#region src/dataTransfer/upload/useUploadManager.ts
815
677
  /**
816
678
  * This composable wraps the `uploadManager` utility
817
679
  * in order to provide reactive state which will work tightly with the `UploadProgressPanel` component
@@ -820,17 +682,19 @@ var createUploadManager = (config) => {
820
682
  * or status of each item being uploaded changes
821
683
  */
822
684
  var useUploadManager = (options) => {
685
+ const { resolveCompleteStatus, ...restOptions } = options;
823
686
  const uploadState = ref({});
824
687
  const { uploadFiles, cancel, setFailed } = createUploadManager({
825
- ...options,
688
+ ...restOptions,
826
689
  onFileUploadComplete: (params) => {
827
690
  const { parentId, name } = uploadState.value[params.uploadId];
828
- uploadState.value[params.uploadId].status = "complete";
829
- options.onFileUploadComplete?.({
691
+ const payload = {
830
692
  ...params,
831
693
  parentId,
832
694
  name
833
- });
695
+ };
696
+ uploadState.value[params.uploadId].status = resolveCompleteStatus?.(payload) ?? "complete";
697
+ options.onFileUploadComplete?.(payload);
834
698
  },
835
699
  onFileUploadFailed: ({ uploadId, error }) => {
836
700
  const { parentId } = uploadState.value[uploadId];
@@ -857,13 +721,15 @@ var useUploadManager = (options) => {
857
721
  name: file.name,
858
722
  size: file.size,
859
723
  progress: 0,
724
+ type: "file",
860
725
  status: "inprogress",
861
726
  parentId
862
727
  };
863
728
  });
864
- await uploadFiles(uploadPayload);
729
+ return await uploadFiles(uploadPayload);
865
730
  } catch (error) {
866
731
  consola.error("Error uploading files", error);
732
+ return [];
867
733
  }
868
734
  };
869
735
  const canCancel = (uploadId) => uploadState.value[uploadId]?.status === "inprogress" || uploadState.value[uploadId]?.status === "failed";
@@ -893,7 +759,13 @@ var useUploadManager = (options) => {
893
759
  status: "failed",
894
760
  failureDetails: error.message
895
761
  };
896
- setFailed(uploadId, error);
762
+ setFailed(uploadId);
763
+ const { parentId } = uploadState.value[uploadId];
764
+ options.onFileUploadFailed?.({
765
+ parentId,
766
+ uploadId,
767
+ error
768
+ });
897
769
  }
898
770
  };
899
771
  };
@@ -954,70 +826,55 @@ var getUploadSizeLimitBytes = () => DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
954
826
  * Alternative to completion, you can also cancel an upload. For this, the composable makes a DELETE request
955
827
  * providing the corresponding `uploadId` which will effectively cancel it.
956
828
  *
957
- *
958
829
  * For a usage example, refer to the Docs page in storybook.
959
830
  */
960
831
  var useFileUpload = (options = {}) => {
961
- const $ofetch = createHttpClient(options.customFetchClientOptions);
962
- const prepareUpload = (parentId, files) => {
963
- const fileDictionary = Object.fromEntries(files.map((file) => {
964
- return [knimeFileFormats.KNWF.getNameOrDefault(file, file.name), file];
965
- }));
966
- const items = Object.fromEntries(Object.entries(fileDictionary).map(([name, file]) => [name, {
967
- itemContentType: getFileMimeType(file),
968
- itemContentSize: file.size
969
- }]));
970
- return $ofetch(`/repository/${parentId}/manifest`, {
971
- method: "POST",
972
- body: { items }
973
- }).then(({ items }) => {
974
- return Object.keys(items).map((name) => {
975
- const { uploadId } = items[name];
976
- return {
977
- uploadId,
978
- file: fileDictionary[name]
979
- };
980
- });
981
- });
982
- };
983
- const resolveFilePartUploadURL = (uploadId, partNumber) => {
984
- return $ofetch(`/uploads/${uploadId}/parts/?partNumber=${partNumber}`, { method: "POST" });
985
- };
986
- const completeUpload = (uploadId, partIds) => {
987
- return $ofetch(`/uploads/${uploadId}`, {
988
- method: "POST",
989
- body: partIds
990
- });
991
- };
992
- const cancelUpload = (uploadId) => {
993
- return $ofetch(`/uploads/${uploadId}`, { method: "DELETE" });
994
- };
995
- const unprocessedUploads = reactive(/* @__PURE__ */ new Set());
996
- const isKNWFFile = (file) => knimeFileFormats.KNWF.matches(file);
832
+ const uploadClient = createUploadClient({ customFetchClientOptions: options.customFetchClientOptions });
997
833
  let useUploadManagerResult = null;
998
- const setProcessingCompleted = ({ uploadId }) => {
999
- unprocessedUploads.delete(uploadId);
834
+ const getItemFromQueue = (uploadId) => {
835
+ const { uploadState } = useUploadManagerResult;
836
+ if (!uploadState.value[uploadId]) {
837
+ consola.warn(`Cannot get item in queue. Upload id "${uploadId}" doesn't exist`);
838
+ return;
839
+ }
840
+ return uploadState.value[uploadId];
1000
841
  };
1001
- const setProcessingFailed = ({ uploadId, error }) => {
1002
- unprocessedUploads.delete(uploadId);
1003
- useUploadManagerResult?.setFailed(uploadId, error ?? /* @__PURE__ */ new Error("An error occurred when processing the file"));
842
+ const updateItemInQueue = (uploadId, data) => {
843
+ const { uploadState } = useUploadManagerResult;
844
+ if (!uploadState.value[uploadId]) {
845
+ consola.warn(`Cannot update item in queue. Upload id "${uploadId}" doesn't exist`);
846
+ return;
847
+ }
848
+ uploadState.value = {
849
+ ...uploadState.value,
850
+ [uploadId]: {
851
+ ...uploadState.value[uploadId],
852
+ ...data
853
+ }
854
+ };
1004
855
  };
856
+ const statusPoller = createUploadStatusPoller(uploadClient, {
857
+ onComplete: (uploadId) => updateItemInQueue(uploadId, { status: "complete" }),
858
+ onFail: (uploadId, statusMessage) => updateItemInQueue(uploadId, {
859
+ status: "failed",
860
+ failureDetails: statusMessage
861
+ })
862
+ });
1005
863
  useUploadManagerResult = useUploadManager({
1006
- resolveFilePartUploadURL,
1007
- onFileUploadComplete: ({ uploadId, filePartIds, parentId, name }) => {
1008
- promise_exports.retryPromise({ fn: () => completeUpload(uploadId, filePartIds) }).then(() => {
864
+ resolveFilePartUploadURL: uploadClient.resolveFilePartUploadURL,
865
+ resolveCompleteStatus: ({ name }) => options.isFileWithProcessing?.(name) ? "processing" : "complete",
866
+ onFileUploadComplete: ({ uploadId, filePartIds, name, parentId }) => {
867
+ promise_exports.retryPromise({ fn: () => uploadClient.completeUpload(uploadId, filePartIds) }).then(() => {
868
+ if (options.isFileWithProcessing?.(name)) statusPoller.track(uploadId);
1009
869
  options.onFileUploadComplete?.({
1010
870
  uploadId,
1011
871
  filePartIds,
1012
- parentId,
1013
- name
872
+ name,
873
+ parentId
1014
874
  });
1015
875
  }).catch((error) => {
1016
876
  consola.error("Error attempting to complete upload", { error });
1017
- setProcessingFailed({
1018
- uploadId,
1019
- error
1020
- });
877
+ useUploadManagerResult.setFailed(uploadId, error);
1021
878
  });
1022
879
  },
1023
880
  onFileUploadFailed: ({ uploadId, error, parentId }) => {
@@ -1036,29 +893,16 @@ var useFileUpload = (options = {}) => {
1036
893
  return files.slice(0, effectiveQueueSize);
1037
894
  };
1038
895
  const { uploadState, ...uploadManagerResultRest } = useUploadManagerResult;
1039
- const uploadQueue = computed(() => {
1040
- return Object.values(uploadState.value).map((uploadItem) => {
1041
- if (uploadItem.status === "complete" && unprocessedUploads.has(uploadItem.id)) return {
1042
- id: uploadItem.id,
1043
- name: uploadItem.name,
1044
- parentId: uploadItem.parentId,
1045
- size: uploadItem.size,
1046
- status: "processing"
1047
- };
1048
- return uploadItem;
1049
- });
1050
- });
1051
896
  const prepareQueueSize = ref(0);
897
+ const uploadQueue = computed(() => Object.values(uploadState.value));
1052
898
  return {
1053
899
  ...uploadManagerResultRest,
1054
900
  uploadQueue,
1055
- unprocessedUploads: readonly(unprocessedUploads),
1056
- setProcessingCompleted,
1057
- setProcessingFailed,
901
+ getItemFromQueue,
902
+ updateItemInQueue,
1058
903
  isPreparingUpload: computed(() => prepareQueueSize.value > 0),
1059
904
  totalFilesBeingPrepared: computed(() => prepareQueueSize.value),
1060
- start: async (parentId, files, options = {}) => {
1061
- const { isFileWithProcessing = isKNWFFile } = options;
905
+ start: async (parentId, files) => {
1062
906
  const enqueueableFiles = getEnqueueableFiles(files);
1063
907
  const uploadSizeLimitBytes = getUploadSizeLimitBytes();
1064
908
  const oversizedFiles = enqueueableFiles.filter((file) => file.size > uploadSizeLimitBytes);
@@ -1070,14 +914,15 @@ var useFileUpload = (options = {}) => {
1070
914
  try {
1071
915
  if (enqueueableFiles.length === 0) return [];
1072
916
  prepareQueueSize.value += enqueueableFiles.length;
917
+ const items = enqueueableFiles.map((file) => ({
918
+ path: knimeFileFormats.KNWF.getNameOrDefault(file, file.name),
919
+ file
920
+ }));
1073
921
  const uploadPayload = await promise_exports.retryPromise({
1074
- fn: () => prepareUpload(parentId, enqueueableFiles),
922
+ fn: () => uploadClient.prepareUpload(parentId, items),
1075
923
  excludeError: (error) => Boolean(error.statusCode && error.statusCode < 500),
1076
924
  retryDelayMS: DEFAULT_RETRY_DELAY_MS
1077
925
  });
1078
- uploadPayload.forEach(({ uploadId, file }) => {
1079
- if (isFileWithProcessing(file)) unprocessedUploads.add(uploadId);
1080
- });
1081
926
  const uploadIds = uploadPayload.map((upload) => upload.uploadId);
1082
927
  useUploadManagerResult.start(parentId, uploadPayload);
1083
928
  return uploadIds;
@@ -1089,205 +934,13 @@ var useFileUpload = (options = {}) => {
1089
934
  },
1090
935
  cancel: (uploadId) => {
1091
936
  useUploadManagerResult.cancel(uploadId);
1092
- cancelUpload(uploadId).catch((error) => {
937
+ uploadClient.cancelUpload(uploadId).catch((error) => {
1093
938
  consola.error("There was a problem cancelling the upload", { error });
1094
939
  });
1095
940
  }
1096
941
  };
1097
942
  };
1098
943
  //#endregion
1099
- //#region src/dataTransfer/download/useDownloadArtifact.ts
1100
- var DEFAULT_MAX_RETRIES = null;
1101
- var DEFAULT_POLLING_INTERVAL = 2e3;
1102
- var hasDownloadUrl = (response) => {
1103
- return "downloadUrl" in response;
1104
- };
1105
- var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
1106
- /**
1107
- * This composable supports multiple concurrent downloads. Each time you call ```start```, the following behavior applies:
1108
- *
1109
- * 1. **Immediate-ready artifact**: If the first status check for the artifact returns `READY`, then there's no polling and no need
1110
- * to add it to `downloadItems`. The download URL is opened in the same tab immediately.
1111
- *
1112
- * 2. **Not-ready artifact**: If the artifact is not yet ready (e.g. needs zipping), then we add a record to `downloadItems` with status `IN_PROGRESS`
1113
- * and repeatedly poll until it becomes `READY`, the polling times out, or the user aborts.
1114
- *
1115
- * **Usage**:
1116
- * ```
1117
- * const { start, cancel, downloadItems } = useDownloadArtifact({ ...options });
1118
- *
1119
- * // start a download
1120
- * await start({ itemId: "1234", version: 1 });
1121
- *
1122
- * // if needed, cancel it:
1123
- * cancel("1234");
1124
- *
1125
- * // downloadItems (as a computed array) will reflect the current statuses.
1126
- * ```
1127
- */
1128
- var useDownloadArtifact = (options = {}) => {
1129
- const $ofetch = createHttpClient(options.customFetchClientOptions);
1130
- const downloadItems = ref({});
1131
- const abortControllers = {};
1132
- const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
1133
- const pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
1134
- const cancel = (downloadId) => {
1135
- const itemController = abortControllers[downloadId];
1136
- if (itemController) {
1137
- itemController.abort();
1138
- delete abortControllers[downloadId];
1139
- }
1140
- };
1141
- const removeItem = (downloadId) => {
1142
- cancel(downloadId);
1143
- delete downloadItems.value[downloadId];
1144
- };
1145
- const fetchDownloadStatus = ({ downloadId, signal }) => {
1146
- return $ofetch(`/downloads/${downloadId}/status`, {
1147
- method: "GET",
1148
- signal
1149
- });
1150
- };
1151
- const openDownload = (downloadUrl) => {
1152
- const a = document.createElement("a");
1153
- a.target = "_blank";
1154
- a.href = downloadUrl;
1155
- a.download = "";
1156
- a.style.display = "none";
1157
- document.body.appendChild(a);
1158
- a.click();
1159
- document.body.removeChild(a);
1160
- };
1161
- /**
1162
- * Makes a request to the download status endpoint for the given download item.
1163
- * Returns a boolean that is true if a terminal state has been reached (download is ready,
1164
- * download failed, or user aborted download)
1165
- * @param downloadId the `downloadId`
1166
- * @param signal the abort signal passed to the request to be able to abort it
1167
- * @returns a boolean indicating whether a terminal state has been reached
1168
- */
1169
- const handlePollingResult = async ({ downloadId, signal }) => {
1170
- try {
1171
- const statusResponse = await fetchDownloadStatus({
1172
- downloadId,
1173
- signal
1174
- });
1175
- if (statusResponse?.status === "READY" && statusResponse?.downloadUrl) {
1176
- downloadItems.value[downloadId].status = "READY";
1177
- downloadItems.value[downloadId].downloadUrl = statusResponse.downloadUrl;
1178
- openDownload(statusResponse.downloadUrl);
1179
- return true;
1180
- } else if (statusResponse?.status === "FAILED") {
1181
- let details = "Download failed";
1182
- if (statusResponse?.statusMessage) details += `: ${statusResponse.statusMessage}`;
1183
- throw new Error(details);
1184
- }
1185
- } catch (error) {
1186
- consola.error("Error fetching status:", error);
1187
- if (!(isAbortError(error) || error instanceof FetchError && isAbortError(error.cause))) {
1188
- downloadItems.value[downloadId].status = "FAILED";
1189
- downloadItems.value[downloadId].failureDetails = error instanceof Error ? error.message : error;
1190
- }
1191
- return true;
1192
- }
1193
- return false;
1194
- };
1195
- const pollDownloadItem = async ({ itemId, downloadId, version, name }) => {
1196
- const controller = new AbortController();
1197
- abortControllers[downloadId] = controller;
1198
- const signal = controller.signal;
1199
- let itemName = `${name}`;
1200
- if (version) itemName += ` (${version})`;
1201
- downloadItems.value[downloadId] = {
1202
- itemId,
1203
- ...version && { version },
1204
- downloadId,
1205
- name: itemName,
1206
- status: "IN_PROGRESS"
1207
- };
1208
- const abortListener = () => {
1209
- controller.signal.removeEventListener("abort", abortListener);
1210
- if (!downloadItems.value[downloadId]) return;
1211
- downloadItems.value[downloadId].status = "CANCELLED";
1212
- downloadItems.value[downloadId].failureDetails = "Download was cancelled manually";
1213
- };
1214
- controller.signal.addEventListener("abort", abortListener);
1215
- let tries = 0;
1216
- while (maxRetries === null || tries < maxRetries) {
1217
- tries++;
1218
- await sleep(pollingInterval);
1219
- if (await handlePollingResult({
1220
- downloadId,
1221
- signal
1222
- })) break;
1223
- }
1224
- delete abortControllers[downloadId];
1225
- if (maxRetries !== null && tries >= maxRetries) {
1226
- downloadItems.value[downloadId].status = "FAILED";
1227
- downloadItems.value[downloadId].failureDetails = "Download timed out";
1228
- }
1229
- };
1230
- const start = async ({ itemId, version, name }) => {
1231
- try {
1232
- const response = await $ofetch(`/repository/${itemId}/artifact`, {
1233
- method: "GET",
1234
- query: { version }
1235
- });
1236
- if (hasDownloadUrl(response)) {
1237
- openDownload(response.downloadUrl);
1238
- return;
1239
- }
1240
- await pollDownloadItem({
1241
- itemId,
1242
- version,
1243
- downloadId: response.downloadId,
1244
- name
1245
- });
1246
- } catch (error) {
1247
- throw rfcErrors.tryParse(error) ?? error;
1248
- }
1249
- };
1250
- const totalItemsBeingZipped = computed(() => Object.values(downloadItems.value).filter(({ status }) => status === "IN_PROGRESS").length);
1251
- const resetState = () => {
1252
- for (const [downloadId, controller] of Object.entries(abortControllers)) {
1253
- controller?.abort();
1254
- delete abortControllers[downloadId];
1255
- }
1256
- downloadItems.value = {};
1257
- };
1258
- return {
1259
- /**
1260
- * Initiates a download of the given itemId and version.
1261
- */
1262
- start,
1263
- /**
1264
- * Cancels the ongoing download for a given itemId (if any).
1265
- * Sets the status in `downloadItems` to `CANCELLED`.
1266
- */
1267
- cancel,
1268
- /**
1269
- * Removes item from `downloadItems`.
1270
- */
1271
- removeItem,
1272
- /**
1273
- * A computed array of all current items that are being zipped and prepared for download.
1274
- */
1275
- downloadItems: computed(() => Object.values(downloadItems.value)),
1276
- /**
1277
- * The total number of zipping items in progress.
1278
- */
1279
- totalItemsBeingZipped,
1280
- /**
1281
- * Resets the state of the composable, clearing all downloads and controllers.
1282
- */
1283
- resetState,
1284
- /**
1285
- * Opens a download from the given url
1286
- */
1287
- openDownload
1288
- };
1289
- };
1290
- //#endregion
1291
- export { API_BASE_URL, CollapsiblePanel_default as CollapsiblePanel, DownloadProgressPanel_default as DownloadProgressPanel, ProgressItem_default as ProgressItem, ProgressList_default as ProgressList, UploadProgressPanel_default as UploadProgressPanel, createHttpClient, defaultConfig, useDownloadArtifact, useFileUpload };
944
+ export { API_BASE_URL, CollapsiblePanel_default as CollapsiblePanel, DownloadProgressPanel_default as DownloadProgressPanel, ProgressItem_default as ProgressItem, ProgressList_default as ProgressList, UploadProgressPanel_default as UploadProgressPanel, createHttpClient, defaultConfig, useDownloadArtifact, useFileUpload, useUploadManager };
1292
945
 
1293
946
  //# sourceMappingURL=dataTransfer.js.map