Skip to content

Commit 6d96f0b

Browse files
silverwindGiteaBot
andauthored
Add fetch wrappers, ignore network errors in actions view (#26985)
1. Introduce lightweight `fetch` wrapper functions that automatically sets csfr token, content-type and use it in `RepoActionView.vue`. 2. Fix a specific issue on `RepoActionView.vue` where a fetch network error is shortly visible during page reload sometimes. It can be reproduced by F5-in in quick succession on the actions view page and was also producing a red error box on the page. Once approved, we can replace all current `fetch` uses in UI with this in another PR. --------- Co-authored-by: Giteabot <[email protected]>
1 parent 148c9c4 commit 6d96f0b

File tree

4 files changed

+79
-28
lines changed

4 files changed

+79
-28
lines changed

docs/content/contributing/guidelines-frontend.en-us.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ it's recommended to use `const _promise = asyncFoo()` to tell readers
9292
that this is done by purpose, we want to call the async function and ignore the Promise.
9393
Some lint rules and IDEs also have warnings if the returned Promise is not handled.
9494

95+
### Fetching data
96+
97+
To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
98+
accept a `data` option for the content, will automatically set CSFR token and return a
99+
Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
100+
95101
### HTML Attributes and `dataset`
96102

97103
The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes.

web_src/js/components/RepoActionView.vue

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {createApp} from 'vue';
55
import {toggleElem} from '../utils/dom.js';
66
import {getCurrentLocale} from '../utils.js';
77
import {renderAnsi} from '../render/ansi.js';
8-
9-
const {csrfToken} = window.config;
8+
import {POST} from '../modules/fetch.js';
109
1110
const sfc = {
1211
name: 'RepoActionView',
@@ -145,11 +144,11 @@ const sfc = {
145144
},
146145
// cancel a run
147146
cancelRun() {
148-
this.fetchPost(`${this.run.link}/cancel`);
147+
POST(`${this.run.link}/cancel`);
149148
},
150149
// approve a run
151150
approveRun() {
152-
this.fetchPost(`${this.run.link}/approve`);
151+
POST(`${this.run.link}/approve`);
153152
},
154153
155154
createLogLine(line, startTime, stepIndex) {
@@ -196,17 +195,21 @@ const sfc = {
196195
}
197196
},
198197
198+
async fetchArtifacts() {
199+
const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
200+
return await resp.json();
201+
},
202+
199203
async fetchJob() {
200204
const logCursors = this.currentJobStepsStates.map((it, idx) => {
201205
// cursor is used to indicate the last position of the logs
202206
// it's only used by backend, frontend just reads it and passes it back, it and can be any type.
203207
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
204208
return {step: idx, cursor: it.cursor, expanded: it.expanded};
205209
});
206-
const resp = await this.fetchPost(
207-
`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
208-
JSON.stringify({logCursors}),
209-
);
210+
const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
211+
data: {logCursors},
212+
});
210213
return await resp.json();
211214
},
212215
@@ -215,16 +218,21 @@ const sfc = {
215218
try {
216219
this.loading = true;
217220
218-
// refresh artifacts if upload-artifact step done
219-
const resp = await this.fetchPost(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
220-
const artifacts = await resp.json();
221-
this.artifacts = artifacts['artifacts'] || [];
221+
let job, artifacts;
222+
try {
223+
[job, artifacts] = await Promise.all([
224+
this.fetchJob(),
225+
this.fetchArtifacts(), // refresh artifacts if upload-artifact step done
226+
]);
227+
} catch (err) {
228+
if (!(err instanceof TypeError)) throw err; // avoid network error while unloading page
229+
}
222230
223-
const response = await this.fetchJob();
231+
this.artifacts = artifacts['artifacts'] || [];
224232
225233
// save the state to Vue data, then the UI will be updated
226-
this.run = response.state.run;
227-
this.currentJob = response.state.currentJob;
234+
this.run = job.state.run;
235+
this.currentJob = job.state.currentJob;
228236
229237
// sync the currentJobStepsStates to store the job step states
230238
for (let i = 0; i < this.currentJob.steps.length; i++) {
@@ -234,7 +242,7 @@ const sfc = {
234242
}
235243
}
236244
// append logs to the UI
237-
for (const logs of response.logs.stepsLog) {
245+
for (const logs of job.logs.stepsLog) {
238246
// save the cursor, it will be passed to backend next time
239247
this.currentJobStepsStates[logs.step].cursor = logs.cursor;
240248
this.appendLogs(logs.step, logs.lines, logs.started);
@@ -249,18 +257,6 @@ const sfc = {
249257
}
250258
},
251259
252-
253-
fetchPost(url, body) {
254-
return fetch(url, {
255-
method: 'POST',
256-
headers: {
257-
'Content-Type': 'application/json',
258-
'X-Csrf-Token': csrfToken,
259-
},
260-
body,
261-
});
262-
},
263-
264260
isDone(status) {
265261
return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
266262
},

web_src/js/modules/fetch.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {isObject} from '../utils.js';
2+
3+
const {csrfToken} = window.config;
4+
5+
// fetch wrapper, use below method name functions and the `data` option to pass in data
6+
// which will automatically set an appropriate content-type header. For json content,
7+
// only object and array types are currently supported.
8+
function request(url, {headers, data, body, ...other} = {}) {
9+
let contentType;
10+
if (!body) {
11+
if (data instanceof FormData) {
12+
contentType = 'multipart/form-data';
13+
body = data;
14+
} else if (data instanceof URLSearchParams) {
15+
contentType = 'application/x-www-form-urlencoded';
16+
body = data;
17+
} else if (isObject(data) || Array.isArray(data)) {
18+
contentType = 'application/json';
19+
body = JSON.stringify(data);
20+
}
21+
}
22+
23+
return fetch(url, {
24+
headers: {
25+
'x-csrf-token': csrfToken,
26+
...(contentType && {'content-type': contentType}),
27+
...headers,
28+
},
29+
...(body && {body}),
30+
...other,
31+
});
32+
}
33+
34+
export const GET = (url, opts) => request(url, {method: 'GET', ...opts});
35+
export const POST = (url, opts) => request(url, {method: 'POST', ...opts});
36+
export const PATCH = (url, opts) => request(url, {method: 'PATCH', ...opts});
37+
export const PUT = (url, opts) => request(url, {method: 'PUT', ...opts});
38+
export const DELETE = (url, opts) => request(url, {method: 'DELETE', ...opts});

web_src/js/modules/fetch.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {test, expect} from 'vitest';
2+
import {GET, POST, PATCH, PUT, DELETE} from './fetch.js';
3+
4+
// tests here are only to satisfy the linter for unused functions
5+
test('exports', () => {
6+
expect(GET).toBeTruthy();
7+
expect(POST).toBeTruthy();
8+
expect(PATCH).toBeTruthy();
9+
expect(PUT).toBeTruthy();
10+
expect(DELETE).toBeTruthy();
11+
});

0 commit comments

Comments
 (0)