@knime/hub-features 1.35.3 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @knime/hub-features
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - c0933b6: **Breaking:** removed `useFileUpload`. It is available from
8
+ `@knime/product-features/data-transfer`.
9
+
10
+ `@knime/utils` now comes from the registry rather than the private copy in this
11
+ repository.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [c0933b6]
16
+ - @knime/components@2.0.0
17
+
18
+ ## 1.35.4
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [97ad603]
23
+ - @knime/utils@1.11.2
24
+ - @knime/components@1.46.13
25
+
3
26
  ## 1.35.3
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/hub-features",
3
- "version": "1.35.3",
3
+ "version": "2.0.0",
4
4
  "description": "Vue components & composables for shared hub features",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,15 +33,15 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@floating-ui/vue": "1.1.8",
36
+ "@knime/utils": "2.0.0",
36
37
  "@vueuse/components": "~13.8.0",
37
38
  "@vueuse/core": "~13.8.0",
38
39
  "@vueuse/shared": "~13.8.0",
39
40
  "lodash-es": "4.18.1",
40
41
  "ofetch": "^1.4.1",
41
42
  "typescript": "^5.9.3",
42
- "@knime/components": "1.46.12",
43
- "@knime/styles": "1.15.4",
44
- "@knime/utils": "1.11.1"
43
+ "@knime/components": "2.0.0",
44
+ "@knime/styles": "1.15.4"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "consola": "3.x",
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "./rfcErrors";
2
- export * from "./useFileUpload";
3
2
  export * from "./useDownloadArtifact";
4
3
  export * from "./components/avatars";
5
4
  export * from "./embeddingSDK";
@@ -1 +0,0 @@
1
- export * from "./useFileUpload";
@@ -1,348 +0,0 @@
1
- import { computed, reactive, readonly, ref } from "vue";
2
- import { FetchError, type FetchOptions } from "ofetch";
3
-
4
- import { useUploadManager } from "@knime/components";
5
- import {
6
- formatFileSize,
7
- getFileMimeType,
8
- knimeFileFormats,
9
- promise,
10
- } from "@knime/utils";
11
-
12
- import { createHttpClient } from "../httpClient/createHttpClient";
13
- import { rfcErrors } from "../rfcErrors";
14
-
15
- const DEFAULT_MAX_UPLOAD_QUEUE_SIZE = 10;
16
- const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 5 * 1024 * 1024 * 1024;
17
-
18
- const DEFAULT_RETRY_DELAY_MS = 50;
19
-
20
- // Catalog & artifacts services currently have a limit for 5 GiB for
21
- // upload and copy/move operations but it is not communicated to clients.
22
- // Repeat the constant here to be able to still make a pre-check. See NXT-4510.
23
- const getUploadSizeLimitBytes = () => DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
24
-
25
- type UseFileUploadOptions = {
26
- /**
27
- * Max number of concurrent uploads allowed in the upload queue.
28
- * When an upload is started with a number of files that exceed the pending
29
- * uploads then only files up-to the max queue size will be processed and the rest
30
- * will be ignored. This will also call the `onUploadQueueSizeExceeded` callback
31
- */
32
- maxUploadQueueSize?: number;
33
- onFileUploadComplete?: (payload: {
34
- uploadId: string;
35
- filePartIds: Record<number, string>;
36
- parentId: string;
37
- name: string;
38
- }) => void;
39
- onFileUploadFailed?: (payload: {
40
- uploadId: string;
41
- error: Error;
42
- parentId: string;
43
- }) => void;
44
- onUploadQueueSizeExceeded?: (maxQueueSize: number) => void;
45
- /**
46
- * Options to customize the fetch client used internally to make http requests.
47
- * e.g: headers, baseUrl, etc
48
- */
49
- customFetchClientOptions?: FetchOptions;
50
- };
51
-
52
- /**
53
- * This composable handles uploads of data to a hub repository item (space or workflow group).
54
- * It is a multi-step process which is described as follows:
55
- *
56
- * Step 1:
57
- * First we need to prepare an upload. The items will be uploaded to a `parent` container
58
- * identified by its id. Then, for each file we want to upload, we must provide paths relative
59
- * to the parent that identify each file. Example:
60
- * ```
61
- * {
62
- * "items": {
63
- * "my-file.txt": { itemContentType: "text/plain", itemContentSize: 5000 },
64
- * "folder/my-file.zip": { itemContentType: "application/zip", itemContentSize: 5000 },
65
- * }
66
- * }
67
- * ```
68
- * This describes 2 items, one placed in the parent's root and another inside a folder.
69
- * The response for this would look like:
70
- * ```
71
- * {
72
- * "items": {
73
- * "my-file.txt": { uploadId: "id1" },
74
- * "folder/my-file.zip": { uploadId: "id2" },
75
- * }
76
- * }
77
- * ```
78
- * Now, each corresponding `uploadId` uniquely identifies the upload process for
79
- * their respective files.
80
- *
81
- * Step 2:
82
- * Next, we need to start uploading the file parts. Technically, you _can_ just upload
83
- * each file as one big file "part", but practicaly that is not recommended because a hiccup
84
- * in the connection means you would have to start the upload from scratch; instead, we split
85
- * the files in smaller parts.
86
- * The upload manager helper will take care of splitting the file. However, the responsibility
87
- * of this composable is to supply a function to resolve the upload URL. This upload URL is obtained
88
- * by POSTing to an endpoint with the `uploadId` and the `partNumber` and this will return
89
- * the data required to make an upload to a presigned URL; this will be called for each file part.
90
- *
91
- * Step 3:
92
- * Completion:
93
- * Once the upload of a single file is complete, the upload manager will trigger the `onFileUploadComplete`
94
- * callback, which then will supply the ids of all the different parts that got uploaded for a given `uploadId`
95
- * Then we need to "complete" the upload by sending a request which provides the `uploadId` and a list of
96
- * the ids of all the parts. Note that some backends don't return the ids of the file parts after a successful
97
- * upload of a part, in this case empty strings or null can be used instead of the part id.
98
- *
99
- * Cancellation:
100
- * Alternative to completion, you can also cancel an upload. For this, the composable makes a DELETE request
101
- * providing the corresponding `uploadId` which will effectively cancel it.
102
- *
103
- *
104
- * Usage example:
105
- * ```
106
- * const { start, uploadItems, cancel } = useFileUpload();
107
- *
108
- * const someEventHandler = (parentId: string, files: File[]) => {
109
- * start(parentId, files);
110
- * }
111
- *
112
- * <template>
113
- * <!-- `uploadItems` will reactively update -->
114
- * <div v-for="item in uploadItems" :key="item.id">
115
- * <span>{{ item.name }} / {{ item.size }}</span>
116
- * <hr />
117
- * <span>Progress: {{ item.progress }}</span>
118
- * </div>
119
- * </template>
120
- * ```
121
- */
122
- export const useFileUpload = (options: UseFileUploadOptions = {}) => {
123
- const $ofetch = createHttpClient(options.customFetchClientOptions);
124
-
125
- const prepareUpload = (
126
- parentId: string,
127
- files: Array<File>,
128
- ): Promise<Array<{ uploadId: string; file: File }>> => {
129
- const fileDictionary = Object.fromEntries(
130
- files.map((file) => {
131
- // strip extension for workflows
132
- const name = knimeFileFormats.KNWF.getNameOrDefault(file, file.name);
133
-
134
- return [name, file];
135
- }),
136
- );
137
-
138
- // map file dictionary to body payload of the prepare upload request
139
- const items: Record<string, { itemContentType: string }> =
140
- Object.fromEntries(
141
- Object.entries(fileDictionary).map(([name, file]) => [
142
- name,
143
- {
144
- itemContentType: getFileMimeType(file),
145
- itemContentSize: file.size,
146
- },
147
- ]),
148
- );
149
-
150
- type PrepareUploadResponse = {
151
- items: Record<string, { uploadId: string }>;
152
- };
153
-
154
- return $ofetch<PrepareUploadResponse>(`/repository/${parentId}/manifest`, {
155
- method: "POST",
156
- body: { items },
157
- }).then(({ items }) => {
158
- return Object.keys(items).map((name) => {
159
- const { uploadId } = items[name];
160
- const file = fileDictionary[name];
161
- return { uploadId, file };
162
- });
163
- });
164
- };
165
-
166
- const resolveFilePartUploadURL = (uploadId: string, partNumber: number) => {
167
- type UploadURLResponse = {
168
- method: string;
169
- url: string;
170
- header: { Host: string };
171
- };
172
-
173
- return $ofetch<UploadURLResponse>(
174
- `/uploads/${uploadId}/parts/?partNumber=${partNumber}`,
175
- { method: "POST" },
176
- );
177
- };
178
-
179
- const completeUpload = (
180
- uploadId: string,
181
- partIds: Record<number, string>,
182
- ) => {
183
- return $ofetch(`/uploads/${uploadId}`, {
184
- method: "POST",
185
- body: partIds,
186
- });
187
- };
188
-
189
- const cancelUpload = (uploadId: string) => {
190
- return $ofetch(`/uploads/${uploadId}`, {
191
- method: "DELETE",
192
- });
193
- };
194
-
195
- // contains upload ids for files that have a processing step which was not finished yet
196
- const unprocessedUploads = reactive<Set<string>>(new Set());
197
- const isKNWFFile = (file: File) => knimeFileFormats.KNWF.matches(file);
198
-
199
- let useUploadManagerResult: ReturnType<typeof useUploadManager> | null = null;
200
-
201
- const setProcessingCompleted = ({ uploadId }: { uploadId: string }) => {
202
- unprocessedUploads.delete(uploadId);
203
- };
204
-
205
- const setProcessingFailed = ({
206
- uploadId,
207
- error,
208
- }: {
209
- uploadId: string;
210
- error?: Error;
211
- }) => {
212
- unprocessedUploads.delete(uploadId);
213
- useUploadManagerResult?.setFailed(
214
- uploadId,
215
- error ?? new Error("An error occurred when processing the file"),
216
- );
217
- };
218
-
219
- useUploadManagerResult = useUploadManager({
220
- resolveFilePartUploadURL,
221
-
222
- onFileUploadComplete: ({ uploadId, filePartIds, parentId, name }) => {
223
- promise
224
- .retryPromise({ fn: () => completeUpload(uploadId, filePartIds) })
225
- .then(() => {
226
- options.onFileUploadComplete?.({
227
- uploadId,
228
- filePartIds,
229
- parentId,
230
- name,
231
- });
232
- })
233
- .catch((error) => {
234
- consola.error("Error attempting to complete upload", { error });
235
- setProcessingFailed({ uploadId, error });
236
- });
237
- },
238
-
239
- onFileUploadFailed: ({ uploadId, error, parentId }) => {
240
- options?.onFileUploadFailed?.({ uploadId, error, parentId });
241
- },
242
- });
243
-
244
- const getEnqueueableFiles = (files: File[]): File[] => {
245
- const { totalPendingUploads } = useUploadManagerResult;
246
-
247
- const { maxUploadQueueSize = DEFAULT_MAX_UPLOAD_QUEUE_SIZE } = options;
248
-
249
- if (files.length + totalPendingUploads.value > maxUploadQueueSize) {
250
- options.onUploadQueueSizeExceeded?.(maxUploadQueueSize);
251
- }
252
-
253
- // only keep as many files as the queue allows
254
- return files.slice(0, maxUploadQueueSize - totalPendingUploads.value);
255
- };
256
-
257
- const { uploadState, ...uploadMangerResultRest } = useUploadManagerResult;
258
-
259
- const uploadItems = computed(() => {
260
- return Object.values(uploadState.value).map((uploadItem) => ({
261
- ...uploadItem,
262
- status:
263
- uploadItem.status === "complete" &&
264
- unprocessedUploads.has(uploadItem.id)
265
- ? "processing"
266
- : uploadItem.status,
267
- }));
268
- });
269
-
270
- const prepareQueueSize = ref(0);
271
-
272
- return {
273
- ...uploadMangerResultRest,
274
- uploadItems,
275
- unprocessedUploads: readonly(unprocessedUploads),
276
- setProcessingCompleted,
277
- setProcessingFailed,
278
-
279
- isPreparingUpload: computed(() => prepareQueueSize.value > 0),
280
- totalFilesBeingPrepared: computed(() => prepareQueueSize.value),
281
-
282
- start: async (
283
- parentId: string,
284
- files: File[],
285
- options: { isFileWithProcessing?: typeof isKNWFFile } = {},
286
- ) => {
287
- const { isFileWithProcessing = isKNWFFile } = options;
288
- const enqueableFiles = getEnqueueableFiles(files);
289
- const uploadSizeLimitBytes = getUploadSizeLimitBytes();
290
- const oversizedFiles = enqueableFiles.filter(
291
- (file) => file.size > uploadSizeLimitBytes,
292
- );
293
-
294
- if (oversizedFiles.length > 0) {
295
- throw new rfcErrors.RFCError({
296
- title: "Upload size limit exceeded",
297
- status: 413,
298
- details: oversizedFiles.map(
299
- (file) =>
300
- `${file.name} (${formatFileSize(file.size)}) exceeds the upload limit of ${formatFileSize(uploadSizeLimitBytes)}.`,
301
- ),
302
- });
303
- }
304
-
305
- try {
306
- if (enqueableFiles.length === 0) {
307
- return [];
308
- }
309
-
310
- prepareQueueSize.value += enqueableFiles.length;
311
-
312
- const uploadPayload = await promise.retryPromise({
313
- fn: () => prepareUpload(parentId, enqueableFiles),
314
- excludeError: (error: FetchError) =>
315
- // eslint-disable-next-line no-magic-numbers
316
- Boolean(error.statusCode && error.statusCode < 500),
317
- retryDelayMS: DEFAULT_RETRY_DELAY_MS,
318
- });
319
-
320
- uploadPayload.forEach(({ uploadId, file }) => {
321
- if (isFileWithProcessing(file)) {
322
- unprocessedUploads.add(uploadId);
323
- }
324
- });
325
-
326
- const uploadIds = uploadPayload.map((upload) => upload.uploadId);
327
-
328
- // accept floating promise, errors are handled inside the function
329
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
330
- useUploadManagerResult.start(parentId, uploadPayload);
331
-
332
- return uploadIds;
333
- } catch (error) {
334
- throw rfcErrors.tryParse(error) ?? error;
335
- } finally {
336
- // errors can only be thrown in the prepareUpload call, useUploadManagerResult.start does its own error handling
337
- prepareQueueSize.value -= enqueableFiles.length;
338
- }
339
- },
340
-
341
- cancel: (uploadId: string) => {
342
- useUploadManagerResult.cancel(uploadId);
343
- cancelUpload(uploadId).catch((error: unknown) => {
344
- consola.error("There was a problem cancelling the upload", { error });
345
- });
346
- },
347
- };
348
- };