@scm-manager/ui-api 2.30.2-20220101-174008 → 2.30.2-20220102-103117
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/package.json +2 -2
- package/src/apiclient.test.ts +22 -11
- package/src/apiclient.ts +33 -17
- package/src/errors.ts +9 -0
- package/src/plugins.ts +50 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scm-manager/ui-api",
|
|
3
|
-
"version": "2.30.2-
|
|
3
|
+
"version": "2.30.2-20220102-103117",
|
|
4
4
|
"description": "React hook api for the SCM-Manager backend",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"files": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"react-test-renderer": "^17.0.1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@scm-manager/ui-types": "^2.30.2-
|
|
28
|
+
"@scm-manager/ui-types": "^2.30.2-20220102-103117",
|
|
29
29
|
"fetch-mock-jest": "^1.5.1",
|
|
30
30
|
"gitdiff-parser": "^0.1.2",
|
|
31
31
|
"query-string": "6.14.1",
|
package/src/apiclient.test.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { apiClient, createUrl, extractXsrfTokenFromCookie } from "./apiclient";
|
|
26
26
|
import fetchMock from "fetch-mock";
|
|
27
|
-
import { BackendError } from "./errors";
|
|
27
|
+
import { BackendError, BadGatewayError } from "./errors";
|
|
28
28
|
|
|
29
29
|
describe("create url", () => {
|
|
30
30
|
it("should not change absolute urls", () => {
|
|
@@ -45,9 +45,9 @@ describe("error handling tests", () => {
|
|
|
45
45
|
context: [
|
|
46
46
|
{
|
|
47
47
|
type: "planet",
|
|
48
|
-
id: "earth"
|
|
49
|
-
}
|
|
50
|
-
]
|
|
48
|
+
id: "earth"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
afterEach(() => {
|
|
@@ -55,9 +55,9 @@ describe("error handling tests", () => {
|
|
|
55
55
|
fetchMock.restore();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it("should create a normal error, if the content type is not scmm-error",
|
|
58
|
+
it("should create a normal error, if the content type is not scmm-error", done => {
|
|
59
59
|
fetchMock.getOnce("/api/v2/error", {
|
|
60
|
-
status: 404
|
|
60
|
+
status: 404
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
apiClient.get("/error").catch((err: Error) => {
|
|
@@ -67,13 +67,24 @@ describe("error handling tests", () => {
|
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it("should create
|
|
70
|
+
it("should create a bad gateway error", done => {
|
|
71
|
+
fetchMock.getOnce("/api/v2/error", {
|
|
72
|
+
status: 502
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
apiClient.get("/error").catch((err: Error) => {
|
|
76
|
+
expect(err).toBeInstanceOf(BadGatewayError);
|
|
77
|
+
done();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should create an backend error, if the content type is scmm-error", done => {
|
|
71
82
|
fetchMock.getOnce("/api/v2/error", {
|
|
72
83
|
status: 404,
|
|
73
84
|
headers: {
|
|
74
|
-
"Content-Type": "application/vnd.scmm-error+json;v=2"
|
|
85
|
+
"Content-Type": "application/vnd.scmm-error+json;v=2"
|
|
75
86
|
},
|
|
76
|
-
body: earthNotFoundError
|
|
87
|
+
body: earthNotFoundError
|
|
77
88
|
});
|
|
78
89
|
|
|
79
90
|
apiClient.get("/error").catch((err: BackendError) => {
|
|
@@ -87,8 +98,8 @@ describe("error handling tests", () => {
|
|
|
87
98
|
expect(err.context).toEqual([
|
|
88
99
|
{
|
|
89
100
|
type: "planet",
|
|
90
|
-
id: "earth"
|
|
91
|
-
}
|
|
101
|
+
id: "earth"
|
|
102
|
+
}
|
|
92
103
|
]);
|
|
93
104
|
done();
|
|
94
105
|
});
|
package/src/apiclient.ts
CHANGED
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
import { contextPath } from "./urls";
|
|
26
26
|
import {
|
|
27
27
|
BackendErrorContent,
|
|
28
|
+
BadGatewayError,
|
|
28
29
|
createBackendError,
|
|
29
30
|
ForbiddenError,
|
|
30
31
|
isBackendError,
|
|
31
32
|
TOKEN_EXPIRED_ERROR_CODE,
|
|
32
33
|
TokenExpiredError,
|
|
33
|
-
UnauthorizedError
|
|
34
|
+
UnauthorizedError
|
|
34
35
|
} from "./errors";
|
|
35
36
|
|
|
36
37
|
type SubscriptionEvent = {
|
|
@@ -62,7 +63,12 @@ type SubscriptionArgument = MessageListeners | SubscriptionContext;
|
|
|
62
63
|
|
|
63
64
|
type Cancel = () => void;
|
|
64
65
|
|
|
65
|
-
const sessionId = (
|
|
66
|
+
const sessionId = (
|
|
67
|
+
Date.now().toString(36) +
|
|
68
|
+
Math.random()
|
|
69
|
+
.toString(36)
|
|
70
|
+
.substr(2, 5)
|
|
71
|
+
).toUpperCase();
|
|
66
72
|
|
|
67
73
|
const extractXsrfTokenFromJwt = (jwt: string) => {
|
|
68
74
|
const parts = jwt.split(".");
|
|
@@ -97,7 +103,7 @@ const createRequestHeaders = () => {
|
|
|
97
103
|
// identify the web interface
|
|
98
104
|
"X-SCM-Client": "WUI",
|
|
99
105
|
// identify the window session
|
|
100
|
-
"X-SCM-Session-ID": sessionId
|
|
106
|
+
"X-SCM-Session-ID": sessionId
|
|
101
107
|
};
|
|
102
108
|
|
|
103
109
|
const xsrf = extractXsrfToken();
|
|
@@ -107,10 +113,10 @@ const createRequestHeaders = () => {
|
|
|
107
113
|
return headers;
|
|
108
114
|
};
|
|
109
115
|
|
|
110
|
-
const applyFetchOptions: (p: RequestInit) => RequestInit =
|
|
116
|
+
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
|
111
117
|
if (o.headers) {
|
|
112
118
|
o.headers = {
|
|
113
|
-
...createRequestHeaders()
|
|
119
|
+
...createRequestHeaders()
|
|
114
120
|
};
|
|
115
121
|
} else {
|
|
116
122
|
o.headers = createRequestHeaders();
|
|
@@ -134,6 +140,8 @@ function handleFailure(response: Response) {
|
|
|
134
140
|
throw new UnauthorizedError("Unauthorized", 401);
|
|
135
141
|
} else if (response.status === 403) {
|
|
136
142
|
throw new ForbiddenError("Forbidden", 403);
|
|
143
|
+
} else if (response.status === 502) {
|
|
144
|
+
throw new BadGatewayError("Bad Gateway", 502);
|
|
137
145
|
} else if (isBackendError(response)) {
|
|
138
146
|
return response.json().then((content: BackendErrorContent) => {
|
|
139
147
|
throw createBackendError(content, response.status);
|
|
@@ -169,7 +177,9 @@ class ApiClient {
|
|
|
169
177
|
requestListeners: RequestListener[] = [];
|
|
170
178
|
|
|
171
179
|
get = (url: string): Promise<Response> => {
|
|
172
|
-
return this.request(url, applyFetchOptions({}))
|
|
180
|
+
return this.request(url, applyFetchOptions({}))
|
|
181
|
+
.then(handleFailure)
|
|
182
|
+
.catch(this.notifyAndRethrow);
|
|
173
183
|
};
|
|
174
184
|
|
|
175
185
|
post = (
|
|
@@ -196,7 +206,7 @@ class ApiClient {
|
|
|
196
206
|
const options: RequestInit = {
|
|
197
207
|
method: "POST",
|
|
198
208
|
body: formData,
|
|
199
|
-
headers: additionalHeaders
|
|
209
|
+
headers: additionalHeaders
|
|
200
210
|
};
|
|
201
211
|
return this.httpRequestWithBinaryBody(options, url);
|
|
202
212
|
};
|
|
@@ -207,18 +217,22 @@ class ApiClient {
|
|
|
207
217
|
|
|
208
218
|
head = (url: string) => {
|
|
209
219
|
let options: RequestInit = {
|
|
210
|
-
method: "HEAD"
|
|
220
|
+
method: "HEAD"
|
|
211
221
|
};
|
|
212
222
|
options = applyFetchOptions(options);
|
|
213
|
-
return this.request(url, options)
|
|
223
|
+
return this.request(url, options)
|
|
224
|
+
.then(handleFailure)
|
|
225
|
+
.catch(this.notifyAndRethrow);
|
|
214
226
|
};
|
|
215
227
|
|
|
216
228
|
delete = (url: string): Promise<Response> => {
|
|
217
229
|
let options: RequestInit = {
|
|
218
|
-
method: "DELETE"
|
|
230
|
+
method: "DELETE"
|
|
219
231
|
};
|
|
220
232
|
options = applyFetchOptions(options);
|
|
221
|
-
return this.request(url, options)
|
|
233
|
+
return this.request(url, options)
|
|
234
|
+
.then(handleFailure)
|
|
235
|
+
.catch(this.notifyAndRethrow);
|
|
222
236
|
};
|
|
223
237
|
|
|
224
238
|
httpRequestWithJSONBody = (
|
|
@@ -230,7 +244,7 @@ class ApiClient {
|
|
|
230
244
|
): Promise<Response> => {
|
|
231
245
|
const options: RequestInit = {
|
|
232
246
|
method: method,
|
|
233
|
-
headers: additionalHeaders
|
|
247
|
+
headers: additionalHeaders
|
|
234
248
|
};
|
|
235
249
|
if (payload) {
|
|
236
250
|
options.body = JSON.stringify(payload);
|
|
@@ -246,7 +260,7 @@ class ApiClient {
|
|
|
246
260
|
) => {
|
|
247
261
|
const options: RequestInit = {
|
|
248
262
|
method: method,
|
|
249
|
-
headers: additionalHeaders
|
|
263
|
+
headers: additionalHeaders
|
|
250
264
|
};
|
|
251
265
|
options.body = payload;
|
|
252
266
|
return this.httpRequestWithBinaryBody(options, url, "text/plain");
|
|
@@ -262,12 +276,14 @@ class ApiClient {
|
|
|
262
276
|
options.headers["Content-Type"] = contentType;
|
|
263
277
|
}
|
|
264
278
|
|
|
265
|
-
return this.request(url, options)
|
|
279
|
+
return this.request(url, options)
|
|
280
|
+
.then(handleFailure)
|
|
281
|
+
.catch(this.notifyAndRethrow);
|
|
266
282
|
};
|
|
267
283
|
|
|
268
284
|
subscribe(url: string, argument: SubscriptionArgument): Cancel {
|
|
269
285
|
const es = new EventSource(createUrlWithIdentifiers(url), {
|
|
270
|
-
withCredentials: true
|
|
286
|
+
withCredentials: true
|
|
271
287
|
});
|
|
272
288
|
|
|
273
289
|
let listeners: MessageListeners;
|
|
@@ -308,11 +324,11 @@ class ApiClient {
|
|
|
308
324
|
};
|
|
309
325
|
|
|
310
326
|
private notifyRequestListeners = (url: string, options: RequestInit) => {
|
|
311
|
-
this.requestListeners.forEach(
|
|
327
|
+
this.requestListeners.forEach(requestListener => requestListener(url, options));
|
|
312
328
|
};
|
|
313
329
|
|
|
314
330
|
private notifyAndRethrow = (error: Error): never => {
|
|
315
|
-
this.errorListeners.forEach(
|
|
331
|
+
this.errorListeners.forEach(errorListener => errorListener(error));
|
|
316
332
|
throw error;
|
|
317
333
|
};
|
|
318
334
|
}
|
package/src/errors.ts
CHANGED
|
@@ -79,6 +79,15 @@ export class UnauthorizedError extends Error {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export class BadGatewayError extends Error {
|
|
83
|
+
statusCode: number;
|
|
84
|
+
|
|
85
|
+
constructor(message: string, statusCode: number) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.statusCode = statusCode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
export class TokenExpiredError extends UnauthorizedError {}
|
|
83
92
|
|
|
84
93
|
export class ForbiddenError extends Error {
|
package/src/plugins.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { isPluginCollection, PendingPlugins, Plugin, PluginCollection } from "@s
|
|
|
27
27
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
|
28
28
|
import { apiClient } from "./apiclient";
|
|
29
29
|
import { requiredLink } from "./links";
|
|
30
|
+
import { BadGatewayError } from "./errors";
|
|
30
31
|
|
|
31
32
|
type WaitForRestartOptions = {
|
|
32
33
|
initialDelay?: number;
|
|
@@ -37,31 +38,38 @@ const waitForRestartAfter = (
|
|
|
37
38
|
promise: Promise<any>,
|
|
38
39
|
{ initialDelay = 1000, timeout = 500 }: WaitForRestartOptions = {}
|
|
39
40
|
): Promise<void> => {
|
|
40
|
-
const endTime = Number(new Date()) +
|
|
41
|
+
const endTime = Number(new Date()) + 4 * 60 * 1000;
|
|
41
42
|
let started = false;
|
|
42
43
|
|
|
43
|
-
const executor =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
};
|
|
44
|
+
const executor = <T = any>(data: T) => (resolve: (result: T) => void, reject: (error: Error) => void) => {
|
|
45
|
+
// we need some initial delay
|
|
46
|
+
if (!started) {
|
|
47
|
+
started = true;
|
|
48
|
+
setTimeout(executor(data), initialDelay, resolve, reject);
|
|
49
|
+
} else {
|
|
50
|
+
apiClient
|
|
51
|
+
.get("")
|
|
52
|
+
.then(() => resolve(data))
|
|
53
|
+
.catch(() => {
|
|
54
|
+
if (Number(new Date()) < endTime) {
|
|
55
|
+
setTimeout(executor(data), timeout, resolve, reject);
|
|
56
|
+
} else {
|
|
57
|
+
reject(new Error("timeout reached"));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
63
62
|
|
|
64
|
-
return promise
|
|
63
|
+
return promise
|
|
64
|
+
.catch(err => {
|
|
65
|
+
if (err instanceof BadGatewayError) {
|
|
66
|
+
// in some rare cases the reverse proxy stops forwarding traffic to scm before the response is returned
|
|
67
|
+
// in such a case the reverse proxy returns 502 (bad gateway), so we treat 502 not as error
|
|
68
|
+
return "ok";
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
})
|
|
72
|
+
.then(data => new Promise<void>(executor(data)));
|
|
65
73
|
};
|
|
66
74
|
|
|
67
75
|
export type UseAvailablePluginsOptions = {
|
|
@@ -72,10 +80,10 @@ export const useAvailablePlugins = ({ enabled }: UseAvailablePluginsOptions = {}
|
|
|
72
80
|
const indexLink = useRequiredIndexLink("availablePlugins");
|
|
73
81
|
return useQuery<PluginCollection, Error>(
|
|
74
82
|
["plugins", "available"],
|
|
75
|
-
() => apiClient.get(indexLink).then(
|
|
83
|
+
() => apiClient.get(indexLink).then(response => response.json()),
|
|
76
84
|
{
|
|
77
85
|
enabled,
|
|
78
|
-
retry: 3
|
|
86
|
+
retry: 3
|
|
79
87
|
}
|
|
80
88
|
);
|
|
81
89
|
};
|
|
@@ -88,10 +96,10 @@ export const useInstalledPlugins = ({ enabled }: UseInstalledPluginsOptions = {}
|
|
|
88
96
|
const indexLink = useRequiredIndexLink("installedPlugins");
|
|
89
97
|
return useQuery<PluginCollection, Error>(
|
|
90
98
|
["plugins", "installed"],
|
|
91
|
-
() => apiClient.get(indexLink).then(
|
|
99
|
+
() => apiClient.get(indexLink).then(response => response.json()),
|
|
92
100
|
{
|
|
93
101
|
enabled,
|
|
94
|
-
retry: 3
|
|
102
|
+
retry: 3
|
|
95
103
|
}
|
|
96
104
|
);
|
|
97
105
|
};
|
|
@@ -100,10 +108,10 @@ export const usePendingPlugins = (): ApiResult<PendingPlugins> => {
|
|
|
100
108
|
const indexLink = useIndexLink("pendingPlugins");
|
|
101
109
|
return useQuery<PendingPlugins, Error>(
|
|
102
110
|
["plugins", "pending"],
|
|
103
|
-
() => apiClient.get(indexLink!).then(
|
|
111
|
+
() => apiClient.get(indexLink!).then(response => response.json()),
|
|
104
112
|
{
|
|
105
113
|
enabled: !!indexLink,
|
|
106
|
-
retry: 3
|
|
114
|
+
retry: 3
|
|
107
115
|
}
|
|
108
116
|
);
|
|
109
117
|
};
|
|
@@ -135,19 +143,19 @@ export const useInstallPlugin = () => {
|
|
|
135
143
|
return promise;
|
|
136
144
|
},
|
|
137
145
|
{
|
|
138
|
-
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
146
|
+
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
139
147
|
}
|
|
140
148
|
);
|
|
141
149
|
return {
|
|
142
150
|
install: (plugin: Plugin, restartOptions: RestartOptions = {}) =>
|
|
143
151
|
mutate({
|
|
144
152
|
plugin,
|
|
145
|
-
restartOptions
|
|
153
|
+
restartOptions
|
|
146
154
|
}),
|
|
147
155
|
isLoading,
|
|
148
156
|
error,
|
|
149
157
|
data,
|
|
150
|
-
isInstalled: !!data
|
|
158
|
+
isInstalled: !!data
|
|
151
159
|
};
|
|
152
160
|
};
|
|
153
161
|
|
|
@@ -162,18 +170,18 @@ export const useUninstallPlugin = () => {
|
|
|
162
170
|
return promise;
|
|
163
171
|
},
|
|
164
172
|
{
|
|
165
|
-
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
173
|
+
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
166
174
|
}
|
|
167
175
|
);
|
|
168
176
|
return {
|
|
169
177
|
uninstall: (plugin: Plugin, restartOptions: RestartOptions = {}) =>
|
|
170
178
|
mutate({
|
|
171
179
|
plugin,
|
|
172
|
-
restartOptions
|
|
180
|
+
restartOptions
|
|
173
181
|
}),
|
|
174
182
|
isLoading,
|
|
175
183
|
error,
|
|
176
|
-
isUninstalled: !!data
|
|
184
|
+
isUninstalled: !!data
|
|
177
185
|
};
|
|
178
186
|
};
|
|
179
187
|
|
|
@@ -196,18 +204,18 @@ export const useUpdatePlugins = () => {
|
|
|
196
204
|
return promise;
|
|
197
205
|
},
|
|
198
206
|
{
|
|
199
|
-
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
207
|
+
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
200
208
|
}
|
|
201
209
|
);
|
|
202
210
|
return {
|
|
203
211
|
update: (plugin: Plugin | PluginCollection, restartOptions: RestartOptions = {}) =>
|
|
204
212
|
mutate({
|
|
205
213
|
plugins: plugin,
|
|
206
|
-
restartOptions
|
|
214
|
+
restartOptions
|
|
207
215
|
}),
|
|
208
216
|
isLoading,
|
|
209
217
|
error,
|
|
210
|
-
isUpdated: !!data
|
|
218
|
+
isUpdated: !!data
|
|
211
219
|
};
|
|
212
220
|
};
|
|
213
221
|
|
|
@@ -222,7 +230,7 @@ export const useExecutePendingPlugins = () => {
|
|
|
222
230
|
({ pending, restartOptions }) =>
|
|
223
231
|
waitForRestartAfter(apiClient.post(requiredLink(pending, "execute")), restartOptions),
|
|
224
232
|
{
|
|
225
|
-
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
233
|
+
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
226
234
|
}
|
|
227
235
|
);
|
|
228
236
|
return {
|
|
@@ -230,22 +238,22 @@ export const useExecutePendingPlugins = () => {
|
|
|
230
238
|
mutate({ pending, restartOptions }),
|
|
231
239
|
isLoading,
|
|
232
240
|
error,
|
|
233
|
-
isExecuted: !!data
|
|
241
|
+
isExecuted: !!data
|
|
234
242
|
};
|
|
235
243
|
};
|
|
236
244
|
|
|
237
245
|
export const useCancelPendingPlugins = () => {
|
|
238
246
|
const queryClient = useQueryClient();
|
|
239
247
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, PendingPlugins>(
|
|
240
|
-
|
|
248
|
+
pending => apiClient.post(requiredLink(pending, "cancel")),
|
|
241
249
|
{
|
|
242
|
-
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
250
|
+
onSuccess: () => queryClient.invalidateQueries("plugins")
|
|
243
251
|
}
|
|
244
252
|
);
|
|
245
253
|
return {
|
|
246
254
|
update: (pending: PendingPlugins) => mutate(pending),
|
|
247
255
|
isLoading,
|
|
248
256
|
error,
|
|
249
|
-
isCancelled: !!data
|
|
257
|
+
isCancelled: !!data
|
|
250
258
|
};
|
|
251
259
|
};
|