diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 925ff7e087470..774fd25dfcac1 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -5,8 +5,7 @@ import {createApp} from 'vue'; import {toggleElem} from '../utils/dom.js'; import {getCurrentLocale} from '../utils.js'; import {renderAnsi} from '../render/ansi.js'; - -const {csrfToken} = window.config; +import {POST, isNetworkError} from '../modules/fetch.js'; const sfc = { name: 'RepoActionView', @@ -145,11 +144,11 @@ const sfc = { }, // cancel a run cancelRun() { - this.fetchPost(`${this.run.link}/cancel`); + POST(`${this.run.link}/cancel`); }, // approve a run approveRun() { - this.fetchPost(`${this.run.link}/approve`); + POST(`${this.run.link}/approve`); }, createLogLine(line, startTime, stepIndex) { @@ -203,10 +202,9 @@ const sfc = { // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc return {step: idx, cursor: it.cursor, expanded: it.expanded}; }); - const resp = await this.fetchPost( - `${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, - JSON.stringify({logCursors}), - ); + const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, { + json: {logCursors}, + }); return await resp.json(); }, @@ -216,7 +214,7 @@ const sfc = { this.loading = true; // refresh artifacts if upload-artifact step done - const resp = await this.fetchPost(`${this.actionsURL}/runs/${this.runIndex}/artifacts`); + const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`); const artifacts = await resp.json(); this.artifacts = artifacts['artifacts'] || []; @@ -244,23 +242,16 @@ const sfc = { clearInterval(this.intervalID); this.intervalID = null; } + } catch (err) { + // avoid error while unloading page with fetch in progress + if (!isNetworkError(err.message)) { + throw err; + } } finally { this.loading = false; } }, - - fetchPost(url, body) { - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Csrf-Token': csrfToken, - }, - body, - }); - }, - isDone(status) { return ['success', 'skipped', 'failure', 'cancelled'].includes(status); }, diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js new file mode 100644 index 0000000000000..f9d353fb40ee3 --- /dev/null +++ b/web_src/js/modules/fetch.js @@ -0,0 +1,31 @@ +const {csrfToken} = window.config; + +function request(url, {headers, json, ...other} = {}) { + return window.fetch(url, { + headers: { + 'x-csrf-token': csrfToken, + ...(json && {'content-type': 'application/json'}), + ...headers, + }, + ...(json && {body: JSON.stringify(json)}), + ...other, + }); +} + +export const GET = (url, opts) => request(url, {method: 'GET', ...opts}); +export const POST = (url, opts) => request(url, {method: 'POST', ...opts}); +export const PATCH = (url, opts) => request(url, {method: 'PATCH', ...opts}); +export const PUT = (url, opts) => request(url, {method: 'PUT', ...opts}); +export const DELETE = (url, opts) => request(url, {method: 'DELETE', ...opts}); + +// network errors are currently only detectable by error message +// based on https://github.com/sindresorhus/p-retry/blob/main/index.js +const networkErrorMsgs = new Set([ + 'Failed to fetch', // Chrome + 'NetworkError when attempting to fetch resource.', // Firefox + 'The Internet connection appears to be offline.', // Safari +]); + +export function isNetworkError(msg) { + return networkErrorMsgs.has(msg); +} diff --git a/web_src/js/modules/fetch.test.js b/web_src/js/modules/fetch.test.js new file mode 100644 index 0000000000000..3f076b3ad9864 --- /dev/null +++ b/web_src/js/modules/fetch.test.js @@ -0,0 +1,14 @@ +import {test, expect} from 'vitest'; +import {GET, POST, PATCH, PUT, DELETE, isNetworkError} from './fetch.js'; + +test('exports', () => { + expect(GET).toBeTruthy(); + expect(POST).toBeTruthy(); + expect(PATCH).toBeTruthy(); + expect(PUT).toBeTruthy(); + expect(DELETE).toBeTruthy(); +}); + +test('isNetworkError', () => { + expect(isNetworkError('Failed to fetch')).toEqual(true); +});