mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00: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