@knime/hub-features 1.3.1 → 1.5.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 +12 -0
- package/package.json +1 -1
- package/src/common/constants.ts +1 -0
- package/src/components/versions/ManageVersions.vue +1 -1
- package/src/components/versions/VersionItem.vue +11 -7
- package/src/components/versions/index.ts +1 -0
- package/src/components/versions/useVersionsApi.ts +175 -0
- package/src/index.ts +2 -0
- package/src/useDownloadArtifact/index.ts +1 -0
- package/src/useDownloadArtifact/useDownloadArtifact.ts +317 -0
- package/src/useFileUpload/useFileUpload.ts +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @knime/hub-features
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 75dffab: Add useVersionsApi composable
|
|
8
|
+
|
|
9
|
+
## 1.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 14f573e: add useDownloadArtifact composable that can be used to download artifacts from the hub and returns a reactive state over ongoing downloads that are being prepared for download
|
|
14
|
+
|
|
3
15
|
## 1.3.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/hub-features",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Vue components & composables for shared hub features",
|
|
5
5
|
"homepage": "https://knime.github.io/webapps-common/",
|
|
6
6
|
"license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_API_BASE_URL = "/_/api";
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
type ManageVersionsProps = {
|
|
20
20
|
hasUnversionedChanges: boolean;
|
|
21
|
-
unversionedSavepoint?: ItemSavepoint & WithAvatar & WithLabels;
|
|
21
|
+
unversionedSavepoint?: (ItemSavepoint & WithAvatar & WithLabels) | null;
|
|
22
22
|
currentVersion: NamedItemVersion["version"] | null;
|
|
23
23
|
versionHistory: Array<NamedItemVersion & WithAvatar & WithLabels>;
|
|
24
24
|
loading: boolean;
|
|
@@ -92,11 +92,11 @@ const onLabelLeave = () => {
|
|
|
92
92
|
|
|
93
93
|
<template>
|
|
94
94
|
<div>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
>
|
|
95
|
+
<div
|
|
96
|
+
:class="['version-item-container', isSelected && 'selected']"
|
|
97
|
+
@click="toggleVersionSelection"
|
|
98
|
+
>
|
|
99
|
+
<Tooltip ref="tooltip" class="tooltip" :text="tooltipText">
|
|
100
100
|
<div class="left">
|
|
101
101
|
<h6>
|
|
102
102
|
{{ version.title }}
|
|
@@ -136,8 +136,8 @@ const onLabelLeave = () => {
|
|
|
136
136
|
</nav>
|
|
137
137
|
</div>
|
|
138
138
|
</div>
|
|
139
|
-
</
|
|
140
|
-
</
|
|
139
|
+
</Tooltip>
|
|
140
|
+
</div>
|
|
141
141
|
</div>
|
|
142
142
|
</template>
|
|
143
143
|
|
|
@@ -153,6 +153,10 @@ const onLabelLeave = () => {
|
|
|
153
153
|
gap: 15px;
|
|
154
154
|
transition: background-color 0.25s ease;
|
|
155
155
|
|
|
156
|
+
& .tooltip {
|
|
157
|
+
display: flex;
|
|
158
|
+
}
|
|
159
|
+
|
|
156
160
|
& p {
|
|
157
161
|
font-size: 11px;
|
|
158
162
|
line-height: 1.5;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* global RequestInit */
|
|
2
|
+
import { merge } from "lodash-es";
|
|
3
|
+
|
|
4
|
+
import type { HubAvatarData } from "@knime/hub-features";
|
|
5
|
+
import { VERSION_DEFAULT_LIMIT } from "@knime/hub-features/versions";
|
|
6
|
+
import type {
|
|
7
|
+
AssignedLabel,
|
|
8
|
+
ItemSavepoint,
|
|
9
|
+
NamedItemVersion,
|
|
10
|
+
WithAvatar,
|
|
11
|
+
WithLabels,
|
|
12
|
+
} from "@knime/hub-features/versions";
|
|
13
|
+
|
|
14
|
+
type UseVersionsApiOptions = {
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const useVersionsApi = ({ baseUrl }: UseVersionsApiOptions) => {
|
|
19
|
+
const doHubRequest = (path: string, requestOptions: RequestInit = {}) => {
|
|
20
|
+
const res = `${baseUrl}${path}`;
|
|
21
|
+
consola.trace(`useVersionsApi::fetching '${res}'`, requestOptions);
|
|
22
|
+
|
|
23
|
+
const defaults = {
|
|
24
|
+
method: "GET",
|
|
25
|
+
};
|
|
26
|
+
return fetch(res, {
|
|
27
|
+
...defaults,
|
|
28
|
+
...requestOptions,
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const doHubRequestJson = async (
|
|
33
|
+
path: string,
|
|
34
|
+
requestOptions: RequestInit = {},
|
|
35
|
+
) => {
|
|
36
|
+
merge(requestOptions, {
|
|
37
|
+
headers: { "Content-Type": "application/json" as const },
|
|
38
|
+
});
|
|
39
|
+
const response = await doHubRequest(path, requestOptions);
|
|
40
|
+
return response.json();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const fetchVersions = ({
|
|
44
|
+
itemId,
|
|
45
|
+
loadAll,
|
|
46
|
+
}: {
|
|
47
|
+
itemId: string;
|
|
48
|
+
loadAll: boolean;
|
|
49
|
+
}) => {
|
|
50
|
+
let path = `/repository/${itemId}/versions`;
|
|
51
|
+
|
|
52
|
+
if (loadAll) {
|
|
53
|
+
path += "?limit=-1";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return doHubRequestJson(path) as Promise<{
|
|
57
|
+
totalCount: number;
|
|
58
|
+
versions: Array<NamedItemVersion>;
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const fetchResourceLabels = ({
|
|
63
|
+
resourceType,
|
|
64
|
+
resourceId,
|
|
65
|
+
}: {
|
|
66
|
+
resourceType: "savepoint";
|
|
67
|
+
resourceId: string;
|
|
68
|
+
}) => {
|
|
69
|
+
return doHubRequestJson(
|
|
70
|
+
`/validation/validation/resources/${resourceType}/${resourceId}/labels`,
|
|
71
|
+
{},
|
|
72
|
+
).catch(() => ({ assignedLabels: [] })) as Promise<{
|
|
73
|
+
assignedLabels: Array<AssignedLabel>;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const deleteVersion = ({
|
|
78
|
+
projectItemId,
|
|
79
|
+
version,
|
|
80
|
+
}: {
|
|
81
|
+
projectItemId: string;
|
|
82
|
+
version: NamedItemVersion["version"];
|
|
83
|
+
}) => {
|
|
84
|
+
return doHubRequest(`/repository/${projectItemId}/versions/${version}`, {
|
|
85
|
+
method: "DELETE",
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const createVersion = ({
|
|
90
|
+
projectItemId,
|
|
91
|
+
title,
|
|
92
|
+
description,
|
|
93
|
+
}: {
|
|
94
|
+
projectItemId: string;
|
|
95
|
+
title: string;
|
|
96
|
+
description: string;
|
|
97
|
+
}): Promise<NamedItemVersion> => {
|
|
98
|
+
return doHubRequestJson(`/repository/${projectItemId}/versions`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
title,
|
|
102
|
+
description,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getAvatar = async ({
|
|
108
|
+
accountName,
|
|
109
|
+
}: {
|
|
110
|
+
accountName: string;
|
|
111
|
+
}): Promise<HubAvatarData> => {
|
|
112
|
+
const accountInfo = await doHubRequestJson(
|
|
113
|
+
`/accounts/name/${accountName}`,
|
|
114
|
+
{
|
|
115
|
+
headers: {
|
|
116
|
+
Prefer: "representation=minimal",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
kind: accountInfo.type === "TEAM" ? "group" : "account",
|
|
123
|
+
name: accountInfo.name,
|
|
124
|
+
image: {
|
|
125
|
+
url: accountInfo.avatarUrl,
|
|
126
|
+
altText: `${accountInfo.realName ?? accountInfo.name} profile image`,
|
|
127
|
+
},
|
|
128
|
+
} satisfies HubAvatarData;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const loadSavepointMetadata = async (
|
|
132
|
+
savepoint: ItemSavepoint,
|
|
133
|
+
): Promise<WithAvatar & WithLabels> => {
|
|
134
|
+
return {
|
|
135
|
+
avatar: await getAvatar({
|
|
136
|
+
accountName: savepoint.version?.author ?? savepoint.author,
|
|
137
|
+
}),
|
|
138
|
+
labels: savepoint.itemVersionId
|
|
139
|
+
? await fetchResourceLabels({
|
|
140
|
+
resourceType: "savepoint",
|
|
141
|
+
resourceId: savepoint.itemVersionId,
|
|
142
|
+
}).then((response) => response.assignedLabels)
|
|
143
|
+
: [],
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const fetchItemSavepoints = ({
|
|
148
|
+
itemId,
|
|
149
|
+
limit,
|
|
150
|
+
}: {
|
|
151
|
+
itemId: string;
|
|
152
|
+
limit?: number;
|
|
153
|
+
}) => {
|
|
154
|
+
return doHubRequestJson(
|
|
155
|
+
`/repository/${itemId}/savepoints?limit=${
|
|
156
|
+
limit ?? VERSION_DEFAULT_LIMIT
|
|
157
|
+
}`,
|
|
158
|
+
) as Promise<{
|
|
159
|
+
totalCount: number;
|
|
160
|
+
savepoints: Array<ItemSavepoint>;
|
|
161
|
+
}>;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
fetchVersions,
|
|
166
|
+
fetchResourceLabels,
|
|
167
|
+
fetchItemSavepoints,
|
|
168
|
+
loadSavepointMetadata,
|
|
169
|
+
deleteVersion,
|
|
170
|
+
createVersion,
|
|
171
|
+
getAvatar,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type VersionsAPI = ReturnType<typeof useVersionsApi>;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useDownloadArtifact";
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { FetchError, type FetchOptions } from "ofetch";
|
|
3
|
+
|
|
4
|
+
import { sleep } from "@knime/utils";
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_API_BASE_URL } from "../common/constants";
|
|
7
|
+
import { getFetchClient } from "../common/ofetchClient";
|
|
8
|
+
import { rfcErrors } from "../rfcErrors";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_RETRIES = null;
|
|
11
|
+
const DEFAULT_POLLING_INTERVAL = 2000;
|
|
12
|
+
|
|
13
|
+
type ArtifactStatusResponse = {
|
|
14
|
+
downloadId?: string;
|
|
15
|
+
status?: string;
|
|
16
|
+
statusMessage?: string;
|
|
17
|
+
lastUpdated?: string;
|
|
18
|
+
downloadUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type RequestDownloadResponse = {
|
|
22
|
+
downloadId: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The version of the artifact to download.
|
|
27
|
+
*/
|
|
28
|
+
type ItemVersion = number | "current-state" | "most-recent";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tracks the state of each concurrent download.
|
|
32
|
+
*/
|
|
33
|
+
type DownloadItem = {
|
|
34
|
+
downloadId: string;
|
|
35
|
+
itemId: string;
|
|
36
|
+
status: "READY" | "IN_PROGRESS" | "FAILED" | "CANCELLED";
|
|
37
|
+
version?: ItemVersion;
|
|
38
|
+
downloadUrl?: string;
|
|
39
|
+
failureDetails?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type UseDownloadArtifactOptions = {
|
|
43
|
+
/**
|
|
44
|
+
* The maximum number of times to poll for the download URL. Default is `null` (no limit).
|
|
45
|
+
*/
|
|
46
|
+
maxRetries?: number;
|
|
47
|
+
/**
|
|
48
|
+
* The interval in milliseconds between each poll. Default is `2000` ms.
|
|
49
|
+
*/
|
|
50
|
+
pollingInterval?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Custom options to pass to the fetch client.
|
|
53
|
+
* @example { baseURL: "/_/api" }
|
|
54
|
+
*/
|
|
55
|
+
customFetchClientOptions?: FetchOptions;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This composable supports multiple concurrent downloads. Each time you call ```start```, the following behavior applies:
|
|
60
|
+
*
|
|
61
|
+
* 1. **Immediate-ready artifact**: If the first status check for the artifact returns `READY`, then there's no polling and no need
|
|
62
|
+
* to add it to `downloadItems`. The download URL is opened in the same tab immediately.
|
|
63
|
+
*
|
|
64
|
+
* 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`
|
|
65
|
+
* and repeatedly poll until it becomes `READY`, the polling times out, or the user aborts.
|
|
66
|
+
*
|
|
67
|
+
* **Usage**:
|
|
68
|
+
* ```
|
|
69
|
+
* const { start, cancel, downloadItems } = useDownloadArtifact({ ...options });
|
|
70
|
+
*
|
|
71
|
+
* // start a download
|
|
72
|
+
* await start({ itemId: "1234", version: 1 });
|
|
73
|
+
*
|
|
74
|
+
* // if needed, cancel it:
|
|
75
|
+
* cancel("1234");
|
|
76
|
+
*
|
|
77
|
+
* // downloadItems (as a computed array) will reflect the current statuses.
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const useDownloadArtifact = (
|
|
81
|
+
options: UseDownloadArtifactOptions = {},
|
|
82
|
+
) => {
|
|
83
|
+
const downloadItems = ref<Record<string, DownloadItem>>({});
|
|
84
|
+
const abortControllers: Record<string, AbortController> = {};
|
|
85
|
+
|
|
86
|
+
const $ofetch = getFetchClient(options.customFetchClientOptions);
|
|
87
|
+
const baseUrl =
|
|
88
|
+
options.customFetchClientOptions?.baseURL ?? DEFAULT_API_BASE_URL;
|
|
89
|
+
|
|
90
|
+
// Use fallback values if none are provided
|
|
91
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
92
|
+
const pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
93
|
+
|
|
94
|
+
const cancel = (downloadId: string) => {
|
|
95
|
+
const itemController = abortControllers[downloadId];
|
|
96
|
+
if (itemController) {
|
|
97
|
+
itemController.abort();
|
|
98
|
+
delete abortControllers[downloadId];
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const removeItem = (downloadId: string) => {
|
|
103
|
+
cancel(downloadId);
|
|
104
|
+
delete downloadItems.value[downloadId];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const fetchDownloadStatus = ({
|
|
108
|
+
downloadId,
|
|
109
|
+
signal,
|
|
110
|
+
}: {
|
|
111
|
+
downloadId: string;
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
}) => {
|
|
114
|
+
return $ofetch<ArtifactStatusResponse>(
|
|
115
|
+
`${baseUrl}/downloads/${downloadId}/status`,
|
|
116
|
+
{
|
|
117
|
+
method: "GET",
|
|
118
|
+
signal,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Makes a request to the download status endpoint for the given download item.
|
|
125
|
+
* Returns a boolean that is true if a terminal state has been reached (download is ready,
|
|
126
|
+
* download failed, or user aborted download)
|
|
127
|
+
* @param downloadId the `downloadId`
|
|
128
|
+
* @param signal the abort signal passed to the request to be able to abort it
|
|
129
|
+
* @returns a boolean indicating whether a terminal state has been reached
|
|
130
|
+
*/
|
|
131
|
+
const handlePollingResult = async ({
|
|
132
|
+
downloadId,
|
|
133
|
+
signal,
|
|
134
|
+
}: {
|
|
135
|
+
downloadId: string;
|
|
136
|
+
signal: AbortSignal;
|
|
137
|
+
}) => {
|
|
138
|
+
try {
|
|
139
|
+
const statusResponse = await fetchDownloadStatus({
|
|
140
|
+
downloadId,
|
|
141
|
+
signal,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (statusResponse?.status === "READY" && statusResponse?.downloadUrl) {
|
|
145
|
+
downloadItems.value[downloadId].status = "READY";
|
|
146
|
+
downloadItems.value[downloadId].downloadUrl =
|
|
147
|
+
statusResponse.downloadUrl;
|
|
148
|
+
window.open(statusResponse.downloadUrl, "_parent");
|
|
149
|
+
return true;
|
|
150
|
+
} else if (statusResponse?.status === "FAILED") {
|
|
151
|
+
let details = "Download failed";
|
|
152
|
+
if (statusResponse?.statusMessage) {
|
|
153
|
+
details += `: ${statusResponse.statusMessage}`;
|
|
154
|
+
}
|
|
155
|
+
throw new Error(details);
|
|
156
|
+
}
|
|
157
|
+
} catch (error: unknown) {
|
|
158
|
+
consola.error("Error fetching status:", error);
|
|
159
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
|
|
160
|
+
const isAbortError =
|
|
161
|
+
error instanceof DOMException && error.name === "AbortError";
|
|
162
|
+
|
|
163
|
+
if (!isAbortError) {
|
|
164
|
+
downloadItems.value[downloadId].status = "FAILED";
|
|
165
|
+
downloadItems.value[downloadId].failureDetails =
|
|
166
|
+
error instanceof Error ? error.message : (error as string);
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const pollDownloadItem = async ({
|
|
174
|
+
itemId,
|
|
175
|
+
downloadId,
|
|
176
|
+
version,
|
|
177
|
+
}: {
|
|
178
|
+
itemId: string;
|
|
179
|
+
downloadId: string;
|
|
180
|
+
version?: ItemVersion;
|
|
181
|
+
}) => {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
abortControllers[downloadId] = controller;
|
|
184
|
+
|
|
185
|
+
const signal = controller.signal;
|
|
186
|
+
|
|
187
|
+
downloadItems.value[downloadId] = {
|
|
188
|
+
itemId,
|
|
189
|
+
...(version && { version }),
|
|
190
|
+
downloadId,
|
|
191
|
+
status: "IN_PROGRESS",
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const abortListener = () => {
|
|
195
|
+
controller.signal.removeEventListener("abort", abortListener);
|
|
196
|
+
if (!downloadItems.value[downloadId]) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
downloadItems.value[downloadId].status = "CANCELLED";
|
|
200
|
+
downloadItems.value[downloadId].failureDetails =
|
|
201
|
+
"Download was cancelled manually";
|
|
202
|
+
};
|
|
203
|
+
controller.signal.addEventListener("abort", abortListener);
|
|
204
|
+
|
|
205
|
+
let tries = 0;
|
|
206
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
207
|
+
while (maxRetries === null || tries < maxRetries) {
|
|
208
|
+
tries++;
|
|
209
|
+
await sleep(pollingInterval);
|
|
210
|
+
|
|
211
|
+
const isTerminalState = await handlePollingResult({ downloadId, signal });
|
|
212
|
+
|
|
213
|
+
if (isTerminalState) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
delete abortControllers[downloadId];
|
|
219
|
+
|
|
220
|
+
if (maxRetries !== null && tries >= maxRetries) {
|
|
221
|
+
downloadItems.value[downloadId].status = "FAILED";
|
|
222
|
+
downloadItems.value[downloadId].failureDetails = "Download timed out";
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const start = async ({
|
|
227
|
+
itemId,
|
|
228
|
+
version,
|
|
229
|
+
}: {
|
|
230
|
+
itemId: string;
|
|
231
|
+
version?: ItemVersion;
|
|
232
|
+
}) => {
|
|
233
|
+
try {
|
|
234
|
+
const response = await $ofetch<RequestDownloadResponse>(
|
|
235
|
+
`${baseUrl}/repository/${itemId}/artifact`,
|
|
236
|
+
{
|
|
237
|
+
method: "GET",
|
|
238
|
+
query: { version },
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
const downloadId = response.downloadId;
|
|
242
|
+
|
|
243
|
+
const downloadStatusResponse = await fetchDownloadStatus({ downloadId });
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
// download is ready immediately, no polling needed
|
|
247
|
+
downloadStatusResponse?.status === "READY" &&
|
|
248
|
+
downloadStatusResponse?.downloadUrl
|
|
249
|
+
) {
|
|
250
|
+
window.open(downloadStatusResponse.downloadUrl, "_parent");
|
|
251
|
+
} else {
|
|
252
|
+
await pollDownloadItem({
|
|
253
|
+
itemId,
|
|
254
|
+
version,
|
|
255
|
+
downloadId,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch (error: unknown) {
|
|
259
|
+
// here only errors thrown by the initial requests are caught;
|
|
260
|
+
// errors occurring while polling will be handled in pollDownloadItem
|
|
261
|
+
if (error instanceof FetchError) {
|
|
262
|
+
throw rfcErrors.tryParse(error);
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const totalItemsBeingZipped = computed(
|
|
269
|
+
() =>
|
|
270
|
+
Object.values(downloadItems.value).filter(
|
|
271
|
+
({ status }) => status === "IN_PROGRESS",
|
|
272
|
+
).length,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const resetState = () => {
|
|
276
|
+
for (const [downloadId, controller] of Object.entries(abortControllers)) {
|
|
277
|
+
controller?.abort();
|
|
278
|
+
delete abortControllers[downloadId];
|
|
279
|
+
}
|
|
280
|
+
downloadItems.value = {};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const downloadItemsArray = computed(() => Object.values(downloadItems.value));
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
/**
|
|
287
|
+
* Initiates a download of the given itemId and version.
|
|
288
|
+
*/
|
|
289
|
+
start,
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Cancels the ongoing download for a given itemId (if any).
|
|
293
|
+
* Sets the status in `downloadItems` to `CANCELLED`.
|
|
294
|
+
*/
|
|
295
|
+
cancel,
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Removes item from `downloadItems`.
|
|
299
|
+
*/
|
|
300
|
+
removeItem,
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A computed array of all current items that are being zipped and prepared for download.
|
|
304
|
+
*/
|
|
305
|
+
downloadItems: downloadItemsArray,
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The total number of zipping items in progress.
|
|
309
|
+
*/
|
|
310
|
+
totalItemsBeingZipped,
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Resets the state of the composable, clearing all downloads and controllers.
|
|
314
|
+
*/
|
|
315
|
+
resetState,
|
|
316
|
+
};
|
|
317
|
+
};
|
|
@@ -4,13 +4,12 @@ import { FetchError, type FetchOptions } from "ofetch";
|
|
|
4
4
|
import { useUploadManager } from "@knime/components";
|
|
5
5
|
import { getFileMimeType, knimeFileFormats, promise } from "@knime/utils";
|
|
6
6
|
|
|
7
|
+
import { DEFAULT_API_BASE_URL } from "../common/constants";
|
|
7
8
|
import { getFetchClient } from "../common/ofetchClient";
|
|
8
9
|
import { rfcErrors } from "../rfcErrors";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_MAX_UPLOAD_QUEUE_SIZE = 10;
|
|
11
12
|
|
|
12
|
-
const DEFAULT_API_BASE_URL = "/_/api";
|
|
13
|
-
|
|
14
13
|
const DEFAULT_RETRY_DELAY_MS = 50;
|
|
15
14
|
|
|
16
15
|
type UseFileUploadOptions = {
|