From 8c62c3ceeabe5d469c5c4032eb25eae86ec89589 Mon Sep 17 00:00:00 2001 From: richardlt Date: Tue, 26 Mar 2024 14:06:15 +0000 Subject: [PATCH] feat(ui): allow to stop and restart workflow run's jobs Signed-off-by: richardlt --- engine/api/v2_workflow_run.go | 10 +- engine/api/v2_workflow_run_job_routines.go | 2 + .../service/workflowv2/workflow.service.ts | 8 + .../projectv2/run/project.run.component.ts | 47 ++++-- .../app/views/projectv2/run/project.run.html | 59 +++++--- .../app/views/projectv2/run/project.run.scss | 142 +++++++++++++----- .../app/views/projectv2/run/run-job-logs.html | 2 +- .../run-result-tests.component.ts | 2 +- 8 files changed, 190 insertions(+), 82 deletions(-) diff --git a/engine/api/v2_workflow_run.go b/engine/api/v2_workflow_run.go index a9ef3a9fa0..c1fe4b95cd 100644 --- a/engine/api/v2_workflow_run.go +++ b/engine/api/v2_workflow_run.go @@ -278,6 +278,8 @@ func (api *API) postStopJobHandler() ([]service.RbacChecker, service.Handler) { defer tx.Rollback() // nolint runJob.Status = sdk.V2WorkflowRunJobStatusStopped + now := time.Now() + runJob.Ended = &now if err := workflow_v2.UpdateJobRun(ctx, tx, runJob); err != nil { return err } @@ -596,6 +598,8 @@ func (api *API) postStopWorkflowRunHandler() ([]service.RbacChecker, service.Han for _, rj := range runJobs { rj.Status = sdk.V2WorkflowRunJobStatusStopped + now := time.Now() + rj.Ended = &now tx, err := api.mustDB().Begin() if err != nil { @@ -787,12 +791,12 @@ func (api *API) putWorkflowRunV2Handler() ([]service.RbacChecker, service.Handle runJobToRestart := make(map[string]sdk.V2WorkflowRunJob) for _, rj := range runJobs { runJobsMap[rj.ID] = rj - if rj.Status == sdk.V2WorkflowRunJobStatusFail { + if rj.Status == sdk.V2WorkflowRunJobStatusFail || rj.Status == sdk.V2WorkflowRunJobStatusStopped { runJobToRestart[rj.ID] = rj } } if len(runJobToRestart) == 0 { - return sdk.NewErrorFrom(sdk.ErrInvalidData, "workflow doesn't contains failed jobs") + return sdk.NewErrorFrom(sdk.ErrInvalidData, "workflow doesn't contains failed or stopped jobs") } runJobsToKeep := workflow_v2.RetrieveJobToKeep(ctx, wr.WorkflowData.Workflow, runJobsMap, runJobToRestart) @@ -811,7 +815,7 @@ func (api *API) putWorkflowRunV2Handler() ([]service.RbacChecker, service.Handle WorkflowRunID: wr.ID, IssuedAt: time.Now(), Level: sdk.WorkflowRunInfoLevelInfo, - Message: u.GetFullname() + " restarted all failed jobs", + Message: u.GetFullname() + " restarted all failed and stopped jobs", } if err := workflow_v2.InsertRunInfo(ctx, tx, &runInfo); err != nil { return err diff --git a/engine/api/v2_workflow_run_job_routines.go b/engine/api/v2_workflow_run_job_routines.go index 30cec536a7..b529604ca8 100644 --- a/engine/api/v2_workflow_run_job_routines.go +++ b/engine/api/v2_workflow_run_job_routines.go @@ -227,6 +227,8 @@ func (api *API) stopDeadJob(ctx context.Context, store cache.Store, db *gorp.DbM log.Info(ctx, fmt.Sprintf("stopDeadJob: stopping job %s/%s on workflow %s run %d", runJob.JobID, runJob.ID, runJob.WorkflowName, runJob.RunNumber)) runJob.Status = sdk.V2WorkflowRunJobStatusStopped + now := time.Now() + runJob.Ended = &now if err := workflow_v2.UpdateJobRun(ctx, tx, runJob); err != nil { return err diff --git a/ui/src/app/service/workflowv2/workflow.service.ts b/ui/src/app/service/workflowv2/workflow.service.ts index 6c75920ca9..c82c9bea19 100644 --- a/ui/src/app/service/workflowv2/workflow.service.ts +++ b/ui/src/app/service/workflowv2/workflow.service.ts @@ -14,6 +14,14 @@ export class V2WorkflowRunService { return this._http.get(`/v2/project/${projKey}/run/${runIdentifier}`); } + restart(projKey: string, runIdentifier: string): Observable { + return this._http.put(`/v2/project/${projKey}/run/${runIdentifier}/restart`, null); + } + + stop(projKey: string, runIdentifier: string) { + return this._http.post(`/v2/project/${projKey}/run/${runIdentifier}/stop`, null); + } + getJobs(r: V2WorkflowRun, attempt: number = null): Observable> { let params = new HttpParams(); if (attempt) { diff --git a/ui/src/app/views/projectv2/run/project.run.component.ts b/ui/src/app/views/projectv2/run/project.run.component.ts index 9456664c2f..6dfe0a99b5 100644 --- a/ui/src/app/views/projectv2/run/project.run.component.ts +++ b/ui/src/app/views/projectv2/run/project.run.component.ts @@ -98,6 +98,7 @@ export class ProjectV2WorkflowRunComponent implements OnDestroy { const projectKey = this._route.snapshot.parent.params['key']; const runIdentifier = this._route.snapshot.params['runIdentifier']; + delete this.selectedItemType; delete this.selectedJobGate; delete this.selectedJobRun; delete this.selectedJobRunInfos; @@ -109,14 +110,6 @@ export class ProjectV2WorkflowRunComponent implements OnDestroy { try { this.workflowRun = await lastValueFrom(this._workflowService.getRun(projectKey, runIdentifier)); this.selectedRunAttempt = this.workflowRun.run_attempt; - this.workflowRunInfos = await lastValueFrom(this._workflowService.getRunInfos(this.workflowRun)); - if (!!this.workflowRunInfos.find(i => i.level === 'warning' || i.level === 'error')) { - this.tabs = [{ - title: 'Problems', - key: 'problems', - default: true - }, ...this.defaultTabs]; - } } catch (e) { this._messageService.error(`Unable to get workflow run: ${e?.error?.error}`, { nzDuration: 2000 }); } @@ -139,16 +132,30 @@ export class ProjectV2WorkflowRunComponent implements OnDestroy { } catch (e) { this._messageService.error(`Unable to get results: ${e?.error?.error}`, { nzDuration: 2000 }); } + try { + this.workflowRunInfos = await lastValueFrom(this._workflowService.getRunInfos(this.workflowRun)); + if (!!this.workflowRunInfos.find(i => i.level === 'warning' || i.level === 'error')) { + this.tabs = [{ + title: 'Problems', + key: 'problems', + default: true + }, ...this.defaultTabs]; + } + } catch (e) { + this._messageService.error(`Unable to get run infos: ${e?.error?.error}`, { nzDuration: 2000 }); + } + + await this.refreshPanel(); - this.refreshPanel(); + const jobsNotTerminated = this.jobs.filter(j => !PipelineStatus.isDone(j.status)).length > 0; - if (!PipelineStatus.isDone(this.workflowRun.status) && !this.pollSubs) { + if (jobsNotTerminated && !this.pollSubs) { this.pollSubs = interval(5000) .pipe(concatMap(_ => from(this.loadJobsAndResults()))) .subscribe(); } - if (PipelineStatus.isDone(this.workflowRun.status) && this.pollSubs) { + if (!jobsNotTerminated && this.pollSubs) { this.pollSubs.unsubscribe(); } @@ -293,10 +300,24 @@ export class ProjectV2WorkflowRunComponent implements OnDestroy { } } - changeRunAttempt(value: number): void { + async changeRunAttempt(value: number) { this.selectedRunAttempt = value; this._cd.markForCheck(); - this.loadJobsAndResults(); + await this.loadJobsAndResults(); + } + + async clickRestartJobs() { + const projectKey = this._route.snapshot.parent.params['key']; + const runIdentifier = this._route.snapshot.params['runIdentifier']; + await lastValueFrom(this._workflowService.restart(projectKey, runIdentifier)); + await this.load(); + } + + async clickStopRun() { + const projectKey = this._route.snapshot.parent.params['key']; + const runIdentifier = this._route.snapshot.params['runIdentifier']; + await lastValueFrom(this._workflowService.stop(projectKey, runIdentifier)); + await this.load(); } } \ No newline at end of file diff --git a/ui/src/app/views/projectv2/run/project.run.html b/ui/src/app/views/projectv2/run/project.run.html index a1c015240d..518900d6c8 100644 --- a/ui/src/app/views/projectv2/run/project.run.html +++ b/ui/src/app/views/projectv2/run/project.run.html @@ -1,29 +1,40 @@ -
- - - {{workflowRun.vcs_server}}/{{workflowRun.repository}}/{{workflowRun.workflow_name}} - #{{workflowRun.run_number}} - - - - - - - - Commit {{workflowRun.contexts.git.sha?.substring(0,8)}} by {{workflowRun.contexts.git.username}} on - repository - {{workflowRun.contexts.git.server}}/{{workflowRun.contexts.git.repository}} - - - +
- + + +
+ + + {{workflowRun.vcs_server}}/{{workflowRun.repository}}/{{workflowRun.workflow_name}} + #{{workflowRun.run_number}} + + + + + + + + Commit {{workflowRun.contexts.git.sha?.substring(0,8)}} by {{workflowRun.contexts.git.username}} + on + repository + {{workflowRun.contexts.git.server}}/{{workflowRun.contexts.git.repository}} + + + +
+ + +
+ +
diff --git a/ui/src/app/views/projectv2/run/project.run.scss b/ui/src/app/views/projectv2/run/project.run.scss index f6951deee3..e68830ba63 100644 --- a/ui/src/app/views/projectv2/run/project.run.scss +++ b/ui/src/app/views/projectv2/run/project.run.scss @@ -6,23 +6,7 @@ height: 100%; } -nz-page-header-title { - font-size: 16px; - display: flex; - flex-direction: row; - align-items: center; - - button, - nz-select { - margin-left: 5px; - } -} - -nz-page-header-content { - padding: 0 0 0 32px; -} - -.graph { +.content { flex: 1; display: flex; position: relative; @@ -30,9 +14,87 @@ nz-page-header-content { height: 100%; overflow: hidden; - .title { - position: absolute; - z-index: 1000; + .graph { + flex: 1; + position: relative; + display: flex; + flex-direction: column-reverse; + align-items: center; + width: 100%; + overflow: hidden; + + .title { + position: absolute; + top: 0; + left: 0; + z-index: 1000; + background-color: white; + border-bottom-right-radius: 40px; + + :host-context(.night) & { + background-color: #141414; + } + } + + .controls { + z-index: 1000; + height: 40px; + border: 2px solid $polar_grey_3; + border-radius: 10px; + margin-bottom: 20px; + padding: 10px; + display: flex; + flex-direction: row; + align-items: center; + font-size: 20px; + background-color: white; + + [nz-icon] { + color: $polar_grey_1; + cursor: pointer; + + :host-context(.night) & { + color: $darkTheme_grey_6; + } + + &:hover { + color: grey !important; + } + } + + :host-context(.night) & { + border-color: $darkTheme_grey_5; + background-color: $darkTheme_grey_1; + } + + [nz-icon]:not(:last-child) { + margin-right: 10px; + } + } + + app-stages-graph { + position: absolute; + top: 0; + bottom: 0; + width: 100%; + height: 100%s; + } + + nz-page-header-title { + font-size: 16px; + display: flex; + flex-direction: row; + align-items: center; + + button, + nz-select { + margin-left: 5px; + } + } + + nz-page-header-content { + padding: 0 0 0 32px; + } } &.disableSelection { @@ -72,30 +134,30 @@ nz-page-header-content { color: $darkTheme_blue; } } -} - -.bottom-panel { - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - .infos { + .bottom-panel { height: 100%; - overflow-y: auto; - padding-left: 10px; - list-style: none; - - .rightFloat { - float: right; - } + overflow: hidden; + display: flex; + flex-direction: column; + + .infos { + height: 100%; + overflow-y: auto; + padding-left: 10px; + list-style: none; + + .rightFloat { + float: right; + } - .content { - display: inline; + .content { + display: inline; + } } } -} -.result { - cursor: pointer; + .result { + cursor: pointer; + } } \ No newline at end of file diff --git a/ui/src/app/views/projectv2/run/run-job-logs.html b/ui/src/app/views/projectv2/run/run-job-logs.html index a31d1b44c4..7ca75ed969 100644 --- a/ui/src/app/views/projectv2/run/run-job-logs.html +++ b/ui/src/app/views/projectv2/run/run-job-logs.html @@ -14,7 +14,7 @@
{{logBlock.name}}
- {{logBlock.startDate | durationMs }} + {{logBlock.startDate | date: 'long' }}
{{logBlock.duration}}
Optional
diff --git a/ui/src/app/views/projectv2/run/run-result-tests/run-result-tests.component.ts b/ui/src/app/views/projectv2/run/run-result-tests/run-result-tests.component.ts index 003ec3705a..2a26f41bf1 100644 --- a/ui/src/app/views/projectv2/run/run-result-tests/run-result-tests.component.ts +++ b/ui/src/app/views/projectv2/run/run-result-tests/run-result-tests.component.ts @@ -55,7 +55,7 @@ export class RunResultTestsComponent implements OnInit, OnChanges { } initTestTree(): void { - const nodes = this.tests.test_suites.map(ts => { + const nodes = (this.tests.test_suites ?? []).map(ts => { let node = { title: ts.name, key: ts.name,