mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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 <teabot@gitea.io>
This commit is contained in:
		@@ -92,6 +92,12 @@ it's recommended to use `const _promise = asyncFoo()` to tell readers
 | 
				
			|||||||
that this is done by purpose, we want to call the async function and ignore the Promise.
 | 
					that this is done by purpose, we want to call the async function and ignore the Promise.
 | 
				
			||||||
Some lint rules and IDEs also have warnings if the returned Promise is not handled.
 | 
					Some lint rules and IDEs also have warnings if the returned Promise is not handled.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fetching data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
 | 
				
			||||||
 | 
					accept a `data` option for the content, will automatically set CSFR token and return a
 | 
				
			||||||
 | 
					Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### HTML Attributes and `dataset`
 | 
					### HTML Attributes and `dataset`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes.
 | 
					The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,7 @@ import {createApp} from 'vue';
 | 
				
			|||||||
import {toggleElem} from '../utils/dom.js';
 | 
					import {toggleElem} from '../utils/dom.js';
 | 
				
			||||||
import {getCurrentLocale} from '../utils.js';
 | 
					import {getCurrentLocale} from '../utils.js';
 | 
				
			||||||
import {renderAnsi} from '../render/ansi.js';
 | 
					import {renderAnsi} from '../render/ansi.js';
 | 
				
			||||||
 | 
					import {POST} from '../modules/fetch.js';
 | 
				
			||||||
const {csrfToken} = window.config;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sfc = {
 | 
					const sfc = {
 | 
				
			||||||
  name: 'RepoActionView',
 | 
					  name: 'RepoActionView',
 | 
				
			||||||
@@ -145,11 +144,11 @@ const sfc = {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    // cancel a run
 | 
					    // cancel a run
 | 
				
			||||||
    cancelRun() {
 | 
					    cancelRun() {
 | 
				
			||||||
      this.fetchPost(`${this.run.link}/cancel`);
 | 
					      POST(`${this.run.link}/cancel`);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    // approve a run
 | 
					    // approve a run
 | 
				
			||||||
    approveRun() {
 | 
					    approveRun() {
 | 
				
			||||||
      this.fetchPost(`${this.run.link}/approve`);
 | 
					      POST(`${this.run.link}/approve`);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    createLogLine(line, startTime, stepIndex) {
 | 
					    createLogLine(line, startTime, stepIndex) {
 | 
				
			||||||
@@ -196,6 +195,11 @@ const sfc = {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fetchArtifacts() {
 | 
				
			||||||
 | 
					      const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
 | 
				
			||||||
 | 
					      return await resp.json();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fetchJob() {
 | 
					    async fetchJob() {
 | 
				
			||||||
      const logCursors = this.currentJobStepsStates.map((it, idx) => {
 | 
					      const logCursors = this.currentJobStepsStates.map((it, idx) => {
 | 
				
			||||||
        // cursor is used to indicate the last position of the logs
 | 
					        // cursor is used to indicate the last position of the logs
 | 
				
			||||||
@@ -203,10 +207,9 @@ const sfc = {
 | 
				
			|||||||
        // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
 | 
					        // 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};
 | 
					        return {step: idx, cursor: it.cursor, expanded: it.expanded};
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const resp = await this.fetchPost(
 | 
					      const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
 | 
				
			||||||
        `${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
 | 
					        data: {logCursors},
 | 
				
			||||||
        JSON.stringify({logCursors}),
 | 
					      });
 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return await resp.json();
 | 
					      return await resp.json();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -215,16 +218,21 @@ const sfc = {
 | 
				
			|||||||
      try {
 | 
					      try {
 | 
				
			||||||
        this.loading = true;
 | 
					        this.loading = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // refresh artifacts if upload-artifact step done
 | 
					        let job, artifacts;
 | 
				
			||||||
        const resp = await this.fetchPost(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
 | 
					        try {
 | 
				
			||||||
        const artifacts = await resp.json();
 | 
					          [job, artifacts] = await Promise.all([
 | 
				
			||||||
 | 
					            this.fetchJob(),
 | 
				
			||||||
 | 
					            this.fetchArtifacts(), // refresh artifacts if upload-artifact step done
 | 
				
			||||||
 | 
					          ]);
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					          if (!(err instanceof TypeError)) throw err; // avoid network error while unloading page
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.artifacts = artifacts['artifacts'] || [];
 | 
					        this.artifacts = artifacts['artifacts'] || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const response = await this.fetchJob();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // save the state to Vue data, then the UI will be updated
 | 
					        // save the state to Vue data, then the UI will be updated
 | 
				
			||||||
        this.run = response.state.run;
 | 
					        this.run = job.state.run;
 | 
				
			||||||
        this.currentJob = response.state.currentJob;
 | 
					        this.currentJob = job.state.currentJob;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // sync the currentJobStepsStates to store the job step states
 | 
					        // sync the currentJobStepsStates to store the job step states
 | 
				
			||||||
        for (let i = 0; i < this.currentJob.steps.length; i++) {
 | 
					        for (let i = 0; i < this.currentJob.steps.length; i++) {
 | 
				
			||||||
@@ -234,7 +242,7 @@ const sfc = {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // append logs to the UI
 | 
					        // append logs to the UI
 | 
				
			||||||
        for (const logs of response.logs.stepsLog) {
 | 
					        for (const logs of job.logs.stepsLog) {
 | 
				
			||||||
          // save the cursor, it will be passed to backend next time
 | 
					          // save the cursor, it will be passed to backend next time
 | 
				
			||||||
          this.currentJobStepsStates[logs.step].cursor = logs.cursor;
 | 
					          this.currentJobStepsStates[logs.step].cursor = logs.cursor;
 | 
				
			||||||
          this.appendLogs(logs.step, logs.lines, logs.started);
 | 
					          this.appendLogs(logs.step, logs.lines, logs.started);
 | 
				
			||||||
@@ -249,18 +257,6 @@ const sfc = {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    fetchPost(url, body) {
 | 
					 | 
				
			||||||
      return fetch(url, {
 | 
					 | 
				
			||||||
        method: 'POST',
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
          'X-Csrf-Token': csrfToken,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        body,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    isDone(status) {
 | 
					    isDone(status) {
 | 
				
			||||||
      return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
 | 
					      return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								web_src/js/modules/fetch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								web_src/js/modules/fetch.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import {isObject} from '../utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {csrfToken} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fetch wrapper, use below method name functions and the `data` option to pass in data
 | 
				
			||||||
 | 
					// which will automatically set an appropriate content-type header. For json content,
 | 
				
			||||||
 | 
					// only object and array types are currently supported.
 | 
				
			||||||
 | 
					function request(url, {headers, data, body, ...other} = {}) {
 | 
				
			||||||
 | 
					  let contentType;
 | 
				
			||||||
 | 
					  if (!body) {
 | 
				
			||||||
 | 
					    if (data instanceof FormData) {
 | 
				
			||||||
 | 
					      contentType = 'multipart/form-data';
 | 
				
			||||||
 | 
					      body = data;
 | 
				
			||||||
 | 
					    } else if (data instanceof URLSearchParams) {
 | 
				
			||||||
 | 
					      contentType = 'application/x-www-form-urlencoded';
 | 
				
			||||||
 | 
					      body = data;
 | 
				
			||||||
 | 
					    } else if (isObject(data) || Array.isArray(data)) {
 | 
				
			||||||
 | 
					      contentType = 'application/json';
 | 
				
			||||||
 | 
					      body = JSON.stringify(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return fetch(url, {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'x-csrf-token': csrfToken,
 | 
				
			||||||
 | 
					      ...(contentType && {'content-type': contentType}),
 | 
				
			||||||
 | 
					      ...headers,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    ...(body && {body}),
 | 
				
			||||||
 | 
					    ...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});
 | 
				
			||||||
							
								
								
									
										11
									
								
								web_src/js/modules/fetch.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web_src/js/modules/fetch.test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import {test, expect} from 'vitest';
 | 
				
			||||||
 | 
					import {GET, POST, PATCH, PUT, DELETE} from './fetch.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests here are only to satisfy the linter for unused functions
 | 
				
			||||||
 | 
					test('exports', () => {
 | 
				
			||||||
 | 
					  expect(GET).toBeTruthy();
 | 
				
			||||||
 | 
					  expect(POST).toBeTruthy();
 | 
				
			||||||
 | 
					  expect(PATCH).toBeTruthy();
 | 
				
			||||||
 | 
					  expect(PUT).toBeTruthy();
 | 
				
			||||||
 | 
					  expect(DELETE).toBeTruthy();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
		Reference in New Issue
	
	Block a user