Skip to content

Commit

Permalink
feat(ui): allow to stop and restart workflow run's jobs
Browse files Browse the repository at this point in the history
Signed-off-by: richardlt <[email protected]>
  • Loading branch information
richardlt committed Mar 26, 2024
1 parent 49b4e8d commit 8c62c3c
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 82 deletions.
10 changes: 7 additions & 3 deletions engine/api/v2_workflow_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions engine/api/v2_workflow_run_job_routines.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions ui/src/app/service/workflowv2/workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export class V2WorkflowRunService {
return this._http.get<V2WorkflowRun>(`/v2/project/${projKey}/run/${runIdentifier}`);
}

restart(projKey: string, runIdentifier: string): Observable<V2WorkflowRun> {
return this._http.put<V2WorkflowRun>(`/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<Array<V2WorkflowRunJob>> {
let params = new HttpParams();
if (attempt) {
Expand Down
47 changes: 34 additions & 13 deletions ui/src/app/views/projectv2/run/project.run.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = [<Tab>{
title: 'Problems',
key: 'problems',
default: true
}, ...this.defaultTabs];
}
} catch (e) {
this._messageService.error(`Unable to get workflow run: ${e?.error?.error}`, { nzDuration: 2000 });
}
Expand All @@ -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 = [<Tab>{
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();
}

Expand Down Expand Up @@ -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();
}

}
59 changes: 35 additions & 24 deletions ui/src/app/views/projectv2/run/project.run.html
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
<ng-container *ngIf="workflowRun">
<div class="graph" [class.disableSelection]="resizing">
<nz-page-header class="title" nzBackIcon (nzBack)="onBack()">
<nz-page-header-title>
{{workflowRun.vcs_server}}/{{workflowRun.repository}}/{{workflowRun.workflow_name}}
#{{workflowRun.run_number}}
<nz-select *ngIf="workflowRun.run_attempt > 1 && selectedRunAttempt" [ngModel]="selectedRunAttempt"
(ngModelChange)="changeRunAttempt($event)" nzSize="small" title="Select run attempt">
<nz-option *ngFor="let item of [].constructor(workflowRun.run_attempt); let i = index"
[nzValue]="workflowRun.run_attempt-i" [nzLabel]="workflowRun.run_attempt-i"></nz-option>
</nz-select>
<button nz-button nzType="default" nzSize="small" title="Show workflow sources"
(click)="openPanel('workflow')"><span nz-icon nzType="file-text" nzTheme="outline"></span></button>
</nz-page-header-title>
<nz-page-header-content>
<span nz-typography nzType="secondary">
Commit {{workflowRun.contexts.git.sha?.substring(0,8)}} by {{workflowRun.contexts.git.username}} on
repository
{{workflowRun.contexts.git.server}}/{{workflowRun.contexts.git.repository}}
</span>
</nz-page-header-content>
</nz-page-header>
<div class="content" [class.disableSelection]="resizing">

<app-stages-graph [workflow]="workflowGraph" [runJobs]="jobs" [workflowRun]="workflowRun"
(onSelectJobGate)="openPanel('gate', $event)" (onSelectJobRun)="openPanel('job', $event)"
(onSelectHook)="openPanel('hook', $event)" #graph></app-stages-graph>
<!-- GRAPH -->

<div class="graph">
<nz-page-header class="title" nzBackIcon (nzBack)="onBack()">
<nz-page-header-title>
{{workflowRun.vcs_server}}/{{workflowRun.repository}}/{{workflowRun.workflow_name}}
#{{workflowRun.run_number}}
<nz-select *ngIf="workflowRun.run_attempt > 1 && selectedRunAttempt" [ngModel]="selectedRunAttempt"
(ngModelChange)="changeRunAttempt($event)" nzSize="small" title="Select run attempt">
<nz-option *ngFor="let item of [].constructor(workflowRun.run_attempt); let i = index"
[nzValue]="workflowRun.run_attempt-i" [nzLabel]="workflowRun.run_attempt-i"></nz-option>
</nz-select>
<button nz-button nzType="default" nzSize="small" title="Show workflow sources"
(click)="openPanel('workflow')"><span nz-icon nzType="file-text"
nzTheme="outline"></span></button>
</nz-page-header-title>
<nz-page-header-content>
<span nz-typography nzType="secondary">
Commit {{workflowRun.contexts.git.sha?.substring(0,8)}} by {{workflowRun.contexts.git.username}}
on
repository
{{workflowRun.contexts.git.server}}/{{workflowRun.contexts.git.repository}}
</span>
</nz-page-header-content>
</nz-page-header>
<div class="controls">
<span nz-icon nzType="play-circle" nzTheme="outline" title="Restart failed jobs"
(click)="clickRestartJobs()"></span>
<span nz-icon nzType="stop" nzTheme="outline" title="Stop workflow run" (click)="clickStopRun()"></span>
</div>
<app-stages-graph [workflow]="workflowGraph" [runJobs]="jobs" [workflowRun]="workflowRun"
(onSelectJobGate)="openPanel('gate', $event)" (onSelectJobRun)="openPanel('job', $event)"
(onSelectHook)="openPanel('hook', $event)" #graph></app-stages-graph>
</div>

<!-- BOTTOM PANELS -->

Expand Down
142 changes: 102 additions & 40 deletions ui/src/app/views/projectv2/run/project.run.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,95 @@
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;
flex-direction: column;
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 {
Expand Down Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion ui/src/app/views/projectv2/run/run-job-logs.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="value">{{logBlock.name}}</div>
<div class="extra">
<div *ngIf="logBlock.startDate && !logBlock.duration">
{{logBlock.startDate | durationMs }}
{{logBlock.startDate | date: 'long' }}
</div>
<div *ngIf="logBlock.duration" title="Step duration">{{logBlock.duration}}</div>
<div [class.orange]="logBlock.optional && logBlock.failed" *ngIf="logBlock.optional">Optional</div>
Expand Down
Loading

0 comments on commit 8c62c3c

Please sign in to comment.