aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+accounts/accounts.component.html10
-rw-r--r--client/src/app/+accounts/accounts.component.scss8
-rw-r--r--client/src/app/+accounts/accounts.component.ts4
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html8
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts76
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html9
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss12
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts1
-rw-r--r--client/src/app/+admin/system/runners/runner.service.ts42
-rw-r--r--client/src/app/+video-channels/video-channels.component.html10
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss8
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts4
-rw-r--r--client/src/app/core/rest/rest.service.ts6
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.scss1
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.html11
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.ts4
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.html9
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.scss15
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.ts22
-rw-r--r--client/src/app/shared/shared-main/buttons/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts4
-rw-r--r--server/controllers/api/runners/jobs.ts29
-rw-r--r--server/helpers/custom-validators/runners/jobs.ts16
-rw-r--r--server/lib/runners/runner.ts16
-rw-r--r--server/middlewares/validators/runners/jobs.ts30
-rw-r--r--server/models/runner/runner-job.ts26
-rw-r--r--server/tests/api/check-params/runners.ts52
-rw-r--r--server/tests/api/runners/runner-common.ts51
-rw-r--r--shared/models/runners/list-runner-jobs-query.model.ts3
-rw-r--r--shared/server-commands/runners/runner-jobs-command.ts22
-rw-r--r--support/doc/api/openapi.yaml22
31 files changed, 445 insertions, 87 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 66d108134..1283105e9 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -25,11 +25,11 @@
25 25
26 <div class="actor-handle"> 26 <div class="actor-handle">
27 <span>@{{ account.nameWithHost }}</span> 27 <span>@{{ account.nameWithHost }}</span>
28 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 28
29 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title 29 <my-copy-button
30 > 30 [value]="account.nameWithHostForced" i18n-notification notification="Username copied"
31 <my-global-icon iconName="copy"></my-global-icon> 31 title="Copy account handle" i18n-title
32 </button> 32 ></my-copy-button>
33 </div> 33 </div>
34 34
35 <div class="actor-counters"> 35 <div class="actor-counters">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 56b952b65..aadd6f5c0 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -28,14 +28,8 @@
28 } 28 }
29} 29}
30 30
31.copy-button { 31my-copy-button {
32 @include margin-left(3px); 32 @include margin-left(3px);
33
34 border: 0;
35
36 my-global-icon {
37 width: 15px;
38 }
39} 33}
40 34
41.account-info { 35.account-info {
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 0033fbf59..6d912e325 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -115,10 +115,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
115 this.redirectService.redirectToHomepage() 115 this.redirectService.redirectToHomepage()
116 } 116 }
117 117
118 activateCopiedMessage () {
119 this.notifier.success($localize`Username copied`)
120 }
121
122 searchChanged (search: string) { 118 searchChanged (search: string) {
123 const queryParams = { search } 119 const queryParams = { search }
124 120
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
index a759e9186..1a40da0c0 100644
--- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
@@ -31,7 +31,7 @@
31 <th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th> 31 <th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th>
32 <th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th> 32 <th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th>
33 <th i18n>Runner</th> 33 <th i18n>Runner</th>
34 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 34 <th style="width: 200px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
35 </tr> 35 </tr>
36 </ng-template> 36 </ng-template>
37 37
@@ -47,7 +47,7 @@
47 </div> 47 </div>
48 48
49 <div class="ms-auto d-flex"> 49 <div class="ms-auto d-flex">
50 <my-advanced-input-filter class="me-2" (search)="onSearch($event)"></my-advanced-input-filter> 50 <my-advanced-input-filter class="me-2" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
51 51
52 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 52 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
53 </div> 53 </div>
@@ -73,7 +73,9 @@
73 73
74 <td>{{ runnerJob.uuid }}</td> 74 <td>{{ runnerJob.uuid }}</td>
75 <td>{{ runnerJob.type }}</td> 75 <td>{{ runnerJob.type }}</td>
76 <td>{{ runnerJob.state.label }}</td> 76 <td>
77 <span class="pt-badge" [ngClass]="getStateBadgeColor(runnerJob)">{{ runnerJob.state.label }}</span>
78 </td>
77 <td>{{ runnerJob.priority }}</td> 79 <td>{{ runnerJob.priority }}</td>
78 80
79 <td> 81 <td>
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
index 8994c1d00..2670eac86 100644
--- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
@@ -5,6 +5,7 @@ import { formatICU } from '@app/helpers'
5import { DropdownAction } from '@app/shared/shared-main' 5import { DropdownAction } from '@app/shared/shared-main'
6import { RunnerJob, RunnerJobState } from '@shared/models' 6import { RunnerJob, RunnerJobState } from '@shared/models'
7import { RunnerJobFormatted, RunnerService } from '../runner.service' 7import { RunnerJobFormatted, RunnerService } from '../runner.service'
8import { AdvancedInputFilter } from '@app/shared/shared-forms'
8 9
9@Component({ 10@Component({
10 selector: 'my-runner-job-list', 11 selector: 'my-runner-job-list',
@@ -20,6 +21,30 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
20 actions: DropdownAction<RunnerJob>[][] = [] 21 actions: DropdownAction<RunnerJob>[][] = []
21 bulkActions: DropdownAction<RunnerJob[]>[][] = [] 22 bulkActions: DropdownAction<RunnerJob[]>[][] = []
22 23
24 inputFilters: AdvancedInputFilter[] = [
25 {
26 title: $localize`Advanced filters`,
27 children: [
28 {
29 value: 'state:completed',
30 label: $localize`Completed jobs`
31 },
32 {
33 value: 'state:pending state:waiting-for-parent-job',
34 label: $localize`Pending jobs`
35 },
36 {
37 value: 'state:processing',
38 label: $localize`Jobs that are being processed`
39 },
40 {
41 value: 'state:errored state:parent-errored',
42 label: $localize`Failed jobs`
43 }
44 ]
45 }
46 ]
47
23 constructor ( 48 constructor (
24 private runnerService: RunnerService, 49 private runnerService: RunnerService,
25 private notifier: Notifier, 50 private notifier: Notifier,
@@ -36,6 +61,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
36 handler: job => this.cancelJobs([ job ]), 61 handler: job => this.cancelJobs([ job ]),
37 isDisplayed: job => this.canCancelJob(job) 62 isDisplayed: job => this.canCancelJob(job)
38 } 63 }
64 ],
65 [
66 {
67 label: $localize`Delete this job`,
68 handler: job => this.removeJobs([ job ])
69 }
39 ] 70 ]
40 ] 71 ]
41 72
@@ -46,6 +77,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
46 handler: jobs => this.cancelJobs(jobs), 77 handler: jobs => this.cancelJobs(jobs),
47 isDisplayed: jobs => jobs.every(j => this.canCancelJob(j)) 78 isDisplayed: jobs => jobs.every(j => this.canCancelJob(j))
48 } 79 }
80 ],
81 [
82 {
83 label: $localize`Delete`,
84 handler: jobs => this.removeJobs(jobs)
85 }
49 ] 86 ]
50 ] 87 ]
51 88
@@ -77,6 +114,45 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
77 }) 114 })
78 } 115 }
79 116
117 async removeJobs (jobs: RunnerJob[]) {
118 const message = formatICU(
119 $localize`Do you really want to remove {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be removed.`,
120 { count: jobs.length }
121 )
122
123 const res = await this.confirmService.confirm(message, $localize`Remove`)
124
125 if (res === false) return
126
127 this.runnerService.removeJobs(jobs)
128 .subscribe({
129 next: () => {
130 this.reloadData()
131 this.notifier.success($localize`Job(s) removed.`)
132 },
133
134 error: err => this.notifier.error(err.message)
135 })
136 }
137
138 getStateBadgeColor (job: RunnerJob) {
139 switch (job.state.id) {
140 case RunnerJobState.ERRORED:
141 case RunnerJobState.PARENT_ERRORED:
142 return 'badge-danger'
143
144 case RunnerJobState.COMPLETED:
145 return 'badge-success'
146
147 case RunnerJobState.PENDING:
148 case RunnerJobState.WAITING_FOR_PARENT_JOB:
149 return 'badge-warning'
150
151 default:
152 return 'badge-info'
153 }
154 }
155
80 protected reloadDataInternal () { 156 protected reloadDataInternal () {
81 this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search }) 157 this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
82 .subscribe({ 158 .subscribe({
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
index 3e5cea881..4ff2c3b4c 100644
--- a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
@@ -45,7 +45,14 @@
45 ></my-action-dropdown> 45 ></my-action-dropdown>
46 </td> 46 </td>
47 47
48 <td>{{ registrationToken.registrationToken }}</td> 48 <td>
49 {{ registrationToken.registrationToken }}
50
51 <my-copy-button
52 [value]="registrationToken.registrationToken" i18n-notification notification="Registration token copied"
53 i18n-title title="Copy registration token"
54 ></my-copy-button>
55 </td>
49 56
50 <td>{{ registrationToken.createdAt | date: 'short' }}</td> 57 <td>{{ registrationToken.createdAt | date: 'short' }}</td>
51 58
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss
new file mode 100644
index 000000000..1cfb2e65f
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss
@@ -0,0 +1,12 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4my-copy-button {
5 @include margin-left(3px);
6}
7
8tr:not(:hover) {
9 my-copy-button {
10 opacity: 0;
11 }
12}
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
index f03aab189..77908a2e1 100644
--- a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
@@ -7,6 +7,7 @@ import { RunnerService } from '../runner.service'
7 7
8@Component({ 8@Component({
9 selector: 'my-runner-registration-token-list', 9 selector: 'my-runner-registration-token-list',
10 styleUrls: [ './runner-registration-token-list.component.scss' ],
10 templateUrl: './runner-registration-token-list.component.html' 11 templateUrl: './runner-registration-token-list.component.html'
11}) 12})
12export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit { 13export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
diff --git a/client/src/app/+admin/system/runners/runner.service.ts b/client/src/app/+admin/system/runners/runner.service.ts
index 392ec82bc..3ab36c4ff 100644
--- a/client/src/app/+admin/system/runners/runner.service.ts
+++ b/client/src/app/+admin/system/runners/runner.service.ts
@@ -6,7 +6,7 @@ import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core' 6import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
7import { arrayify, peertubeTranslate } from '@shared/core-utils' 7import { arrayify, peertubeTranslate } from '@shared/core-utils'
8import { ResultList } from '@shared/models/common' 8import { ResultList } from '@shared/models/common'
9import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners' 9import { Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models/runners'
10import { environment } from '../../../../environments/environment' 10import { environment } from '../../../../environments/environment'
11 11
12export type RunnerJobFormatted = RunnerJob & { 12export type RunnerJobFormatted = RunnerJob & {
@@ -60,7 +60,9 @@ export class RunnerService {
60 let params = new HttpParams() 60 let params = new HttpParams()
61 params = this.restService.addRestGetParams(params, pagination, sort) 61 params = this.restService.addRestGetParams(params, pagination, sort)
62 62
63 if (search) params = params.append('search', search) 63 if (search) {
64 params = this.buildParamsFromSearch(search, params)
65 }
64 66
65 return forkJoin([ 67 return forkJoin([
66 this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }), 68 this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
@@ -90,6 +92,31 @@ export class RunnerService {
90 ) 92 )
91 } 93 }
92 94
95 private buildParamsFromSearch (search: string, params: HttpParams) {
96 const filters = this.restService.parseQueryStringFilter(search, {
97 stateOneOf: {
98 prefix: 'state:',
99 multiple: true,
100 handler: v => {
101 if (v === 'completed') return RunnerJobState.COMPLETED
102 if (v === 'processing') return RunnerJobState.PROCESSING
103 if (v === 'errored') return RunnerJobState.ERRORED
104 if (v === 'pending') return RunnerJobState.PENDING
105 if (v === 'waiting-for-parent-job') return RunnerJobState.WAITING_FOR_PARENT_JOB
106 if (v === 'parent-errored') return RunnerJobState.PARENT_ERRORED
107
108 return undefined
109 }
110 }
111 })
112
113 console.log(filters)
114
115 return this.restService.addObjectParams(params, filters)
116 }
117
118 // ---------------------------------------------------------------------------
119
93 cancelJobs (jobsArg: RunnerJob | RunnerJob[]) { 120 cancelJobs (jobsArg: RunnerJob | RunnerJob[]) {
94 const jobs = arrayify(jobsArg) 121 const jobs = arrayify(jobsArg)
95 122
@@ -101,6 +128,17 @@ export class RunnerService {
101 ) 128 )
102 } 129 }
103 130
131 removeJobs (jobsArg: RunnerJob | RunnerJob[]) {
132 const jobs = arrayify(jobsArg)
133
134 return from(jobs)
135 .pipe(
136 concatMap(job => this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/jobs/' + job.uuid)),
137 toArray(),
138 catchError(err => this.restExtractor.handleError(err))
139 )
140 }
141
104 // --------------------------------------------------------------------------- 142 // ---------------------------------------------------------------------------
105 143
106 listRunners (options: { 144 listRunners (options: {
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index fff160f2e..228cc4edd 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -64,11 +64,11 @@
64 64
65 <div class="actor-handle"> 65 <div class="actor-handle">
66 <span>@{{ videoChannel.nameWithHost }}</span> 66 <span>@{{ videoChannel.nameWithHost }}</span>
67 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 67
68 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title 68 <my-copy-button
69 > 69 [value]="videoChannel.nameWithHostForced" i18n-notification notification="Handle copied"
70 <my-global-icon iconName="copy"></my-global-icon> 70 title="Copy channel handle" i18n-title
71 </button> 71 ></my-copy-button>
72 </div> 72 </div>
73 73
74 <div class="actor-counters"> 74 <div class="actor-counters">
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index aba266fcc..182e8d845 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -152,14 +152,8 @@
152 display: none; 152 display: none;
153} 153}
154 154
155.copy-button { 155my-copy-button {
156 @include margin-left(3px); 156 @include margin-left(3px);
157
158 border: 0;
159
160 my-global-icon {
161 width: 15px;
162 }
163} 157}
164 158
165@media screen and (max-width: 1400px) { 159@media screen and (max-width: 1400px) {
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index afbf96032..f5bea66ec 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -120,10 +120,6 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
120 return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) 120 return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
121 } 121 }
122 122
123 activateCopiedMessage () {
124 this.notifier.success($localize`Username copied`)
125 }
126
127 hasShowMoreDescription () { 123 hasShowMoreDescription () {
128 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100 124 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
129 } 125 }
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts
index d8b5ffb18..f07afb7e8 100644
--- a/client/src/app/core/rest/rest.service.ts
+++ b/client/src/app/core/rest/rest.service.ts
@@ -7,16 +7,18 @@ import { RestPagination } from './rest-pagination'
7 7
8const debugLogger = debug('peertube:rest') 8const debugLogger = debug('peertube:rest')
9 9
10type ParseQueryHandlerResult = string | number | boolean | string[] | number[] | boolean[]
11
10interface QueryStringFilterPrefixes { 12interface QueryStringFilterPrefixes {
11 [key: string]: { 13 [key: string]: {
12 prefix: string 14 prefix: string
13 handler?: (v: string) => string | number | boolean 15 handler?: (v: string) => ParseQueryHandlerResult
14 multiple?: boolean 16 multiple?: boolean
15 isBoolean?: boolean 17 isBoolean?: boolean
16 } 18 }
17} 19}
18 20
19type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>> 21type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, ParseQueryHandlerResult | ParseQueryHandlerResult[]>>
20type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string } 22type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
21 23
22@Injectable() 24@Injectable()
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
index 4efbeb85d..f5438ffdb 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
@@ -33,6 +33,5 @@ my-global-icon {
33 33
34div[role=menu] { 34div[role=menu] {
35 max-height: 50vh; 35 max-height: 50vh;
36 min-height: 200px;
37 overflow: auto; 36 overflow: auto;
38} 37}
diff --git a/client/src/app/shared/shared-forms/input-text.component.html b/client/src/app/shared/shared-forms/input-text.component.html
index abb53a085..4747e2f8f 100644
--- a/client/src/app/shared/shared-forms/input-text.component.html
+++ b/client/src/app/shared/shared-forms/input-text.component.html
@@ -11,13 +11,12 @@
11 <my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon> 11 <my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon>
12 </button> 12 </button>
13 13
14 <button 14 <my-copy-button
15 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 15 *ngIf="withCopy" [value]="input.value" i18n-notification notification="Copied"
16 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy" 16 [isInputGroup]="true" i18n
17 > 17 >
18 <my-global-icon iconName="copy"></my-global-icon> 18 COPY
19 <span class="copy-text">Copy</span> 19 </my-copy-button>
20 </button>
21</div> 20</div>
22 21
23<div *ngIf="formError" class="form-error">{{ formError }}</div> 22<div *ngIf="formError" class="form-error">{{ formError }}</div>
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts
index aa4a1cba8..be03f25b9 100644
--- a/client/src/app/shared/shared-forms/input-text.component.ts
+++ b/client/src/app/shared/shared-forms/input-text.component.ts
@@ -46,10 +46,6 @@ export class InputTextComponent implements ControlValueAccessor {
46 this.show = !this.show 46 this.show = !this.show
47 } 47 }
48 48
49 activateCopiedMessage () {
50 this.notifier.success($localize`Copied`)
51 }
52
53 propagateChange = (_: any) => { /* empty */ } 49 propagateChange = (_: any) => { /* empty */ }
54 50
55 writeValue (value: string) { 51 writeValue (value: string) {
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.html b/client/src/app/shared/shared-main/buttons/copy-button.component.html
new file mode 100644
index 000000000..a99c0a93a
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.html
@@ -0,0 +1,9 @@
1<button
2 class="btn btn-outline-secondary btn-sm copy-button"
3 [cdkCopyToClipboard]="value" (click)="activateCopiedMessage()"
4 [title]="title" [ngClass]="{ 'is-input-group': isInputGroup }"
5>
6 <my-global-icon iconName="copy"></my-global-icon>
7
8 <ng-content></ng-content>
9</button>
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.scss b/client/src/app/shared/shared-main/buttons/copy-button.component.scss
new file mode 100644
index 000000000..7e3720418
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.scss
@@ -0,0 +1,15 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4button:not(.is-input-group) {
5 border: 0;
6}
7
8.is-input-group {
9 border-top-left-radius: 0;
10 border-bottom-left-radius: 0;
11}
12
13my-global-icon {
14 width: 15px;
15}
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.ts b/client/src/app/shared/shared-main/buttons/copy-button.component.ts
new file mode 100644
index 000000000..aac9ab8b0
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.ts
@@ -0,0 +1,22 @@
1import { Component, Input } from '@angular/core'
2import { Notifier } from '@app/core'
3
4@Component({
5 selector: 'my-copy-button',
6 styleUrls: [ './copy-button.component.scss' ],
7 templateUrl: './copy-button.component.html'
8})
9export class CopyButtonComponent {
10 @Input() value: string
11 @Input() title: string
12 @Input() notification: string
13 @Input() isInputGroup = false
14
15 constructor (private notifier: Notifier) {
16
17 }
18
19 activateCopiedMessage () {
20 if (this.notification) this.notifier.success(this.notification)
21 }
22}
diff --git a/client/src/app/shared/shared-main/buttons/index.ts b/client/src/app/shared/shared-main/buttons/index.ts
index 775a47a39..75efbdea3 100644
--- a/client/src/app/shared/shared-main/buttons/index.ts
+++ b/client/src/app/shared/shared-main/buttons/index.ts
@@ -1,4 +1,5 @@
1export * from './action-dropdown.component' 1export * from './action-dropdown.component'
2export * from './button.component' 2export * from './button.component'
3export * from './copy-button.component'
3export * from './delete-button.component' 4export * from './delete-button.component'
4export * from './edit-button.component' 5export * from './edit-button.component'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 480277450..243394bda 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -31,7 +31,7 @@ import {
31 PeerTubeTemplateDirective 31 PeerTubeTemplateDirective
32} from './angular' 32} from './angular'
33import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 33import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
34import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' 34import { ActionDropdownComponent, ButtonComponent, CopyButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
35import { CustomPageService } from './custom-page' 35import { CustomPageService } from './custom-page'
36import { DateToggleComponent } from './date' 36import { DateToggleComponent } from './date'
37import { FeedComponent } from './feeds' 37import { FeedComponent } from './feeds'
@@ -100,6 +100,7 @@ import { VideoChannelService } from './video-channel'
100 100
101 ActionDropdownComponent, 101 ActionDropdownComponent,
102 ButtonComponent, 102 ButtonComponent,
103 CopyButtonComponent,
103 DeleteButtonComponent, 104 DeleteButtonComponent,
104 EditButtonComponent, 105 EditButtonComponent,
105 106
@@ -162,6 +163,7 @@ import { VideoChannelService } from './video-channel'
162 163
163 ActionDropdownComponent, 164 ActionDropdownComponent,
164 ButtonComponent, 165 ButtonComponent,
166 CopyButtonComponent,
165 DeleteButtonComponent, 167 DeleteButtonComponent,
166 EditButtonComponent, 168 EditButtonComponent,
167 169
diff --git a/server/controllers/api/runners/jobs.ts b/server/controllers/api/runners/jobs.ts
index be5911b53..e9e2ddf49 100644
--- a/server/controllers/api/runners/jobs.ts
+++ b/server/controllers/api/runners/jobs.ts
@@ -5,7 +5,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
5import { generateRunnerJobToken } from '@server/helpers/token-generator' 5import { generateRunnerJobToken } from '@server/helpers/token-generator'
6import { MIMETYPES } from '@server/initializers/constants' 6import { MIMETYPES } from '@server/initializers/constants'
7import { sequelizeTypescript } from '@server/initializers/database' 7import { sequelizeTypescript } from '@server/initializers/database'
8import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners' 8import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners'
9import { 9import {
10 apiRateLimiter, 10 apiRateLimiter,
11 asyncMiddleware, 11 asyncMiddleware,
@@ -23,6 +23,7 @@ import {
23 errorRunnerJobValidator, 23 errorRunnerJobValidator,
24 getRunnerFromTokenValidator, 24 getRunnerFromTokenValidator,
25 jobOfRunnerGetValidatorFactory, 25 jobOfRunnerGetValidatorFactory,
26 listRunnerJobsValidator,
26 runnerJobGetValidator, 27 runnerJobGetValidator,
27 successRunnerJobValidator, 28 successRunnerJobValidator,
28 updateRunnerJobValidator 29 updateRunnerJobValidator
@@ -131,9 +132,17 @@ runnerJobsRouter.get('/jobs',
131 runnerJobsSortValidator, 132 runnerJobsSortValidator,
132 setDefaultSort, 133 setDefaultSort,
133 setDefaultPagination, 134 setDefaultPagination,
135 listRunnerJobsValidator,
134 asyncMiddleware(listRunnerJobs) 136 asyncMiddleware(listRunnerJobs)
135) 137)
136 138
139runnerJobsRouter.delete('/jobs/:jobUUID',
140 authenticate,
141 ensureUserHasRight(UserRight.MANAGE_RUNNERS),
142 asyncMiddleware(runnerJobGetValidator),
143 asyncMiddleware(deleteRunnerJob)
144)
145
137// --------------------------------------------------------------------------- 146// ---------------------------------------------------------------------------
138 147
139export { 148export {
@@ -374,6 +383,21 @@ async function cancelRunnerJob (req: express.Request, res: express.Response) {
374 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 383 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
375} 384}
376 385
386async function deleteRunnerJob (req: express.Request, res: express.Response) {
387 const runnerJob = res.locals.runnerJob
388
389 logger.info('Deleting job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
390
391 if (runnerJobCanBeCancelled(runnerJob)) {
392 const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
393 await new RunnerJobHandler().cancel({ runnerJob })
394 }
395
396 await runnerJob.destroy()
397
398 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
399}
400
377async function listRunnerJobs (req: express.Request, res: express.Response) { 401async function listRunnerJobs (req: express.Request, res: express.Response) {
378 const query: ListRunnerJobsQuery = req.query 402 const query: ListRunnerJobsQuery = req.query
379 403
@@ -381,7 +405,8 @@ async function listRunnerJobs (req: express.Request, res: express.Response) {
381 start: query.start, 405 start: query.start,
382 count: query.count, 406 count: query.count,
383 sort: query.sort, 407 sort: query.sort,
384 search: query.search 408 search: query.search,
409 stateOneOf: query.stateOneOf
385 }) 410 })
386 411
387 return res.json({ 412 return res.json({
diff --git a/server/helpers/custom-validators/runners/jobs.ts b/server/helpers/custom-validators/runners/jobs.ts
index 725a7658f..6349e79ba 100644
--- a/server/helpers/custom-validators/runners/jobs.ts
+++ b/server/helpers/custom-validators/runners/jobs.ts
@@ -1,6 +1,6 @@
1import { UploadFilesForCheck } from 'express' 1import { UploadFilesForCheck } from 'express'
2import validator from 'validator' 2import validator from 'validator'
3import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' 3import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
4import { 4import {
5 LiveRTMPHLSTranscodingSuccess, 5 LiveRTMPHLSTranscodingSuccess,
6 RunnerJobSuccessPayload, 6 RunnerJobSuccessPayload,
@@ -11,7 +11,7 @@ import {
11 VODHLSTranscodingSuccess, 11 VODHLSTranscodingSuccess,
12 VODWebVideoTranscodingSuccess 12 VODWebVideoTranscodingSuccess
13} from '@shared/models' 13} from '@shared/models'
14import { exists, isFileValid, isSafeFilename } from '../misc' 14import { exists, isArray, isFileValid, isSafeFilename } from '../misc'
15 15
16const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS 16const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
17 17
@@ -56,6 +56,14 @@ function isRunnerJobErrorMessageValid (value: string) {
56 return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE) 56 return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
57} 57}
58 58
59function isRunnerJobStateValid (value: any) {
60 return exists(value) && RUNNER_JOB_STATES[value] !== undefined
61}
62
63function isRunnerJobArrayOfStateValid (value: any) {
64 return isArray(value) && value.every(v => isRunnerJobStateValid(v))
65}
66
59// --------------------------------------------------------------------------- 67// ---------------------------------------------------------------------------
60 68
61export { 69export {
@@ -65,7 +73,9 @@ export {
65 isRunnerJobTokenValid, 73 isRunnerJobTokenValid,
66 isRunnerJobErrorMessageValid, 74 isRunnerJobErrorMessageValid,
67 isRunnerJobProgressValid, 75 isRunnerJobProgressValid,
68 isRunnerJobAbortReasonValid 76 isRunnerJobAbortReasonValid,
77 isRunnerJobArrayOfStateValid,
78 isRunnerJobStateValid
69} 79}
70 80
71// --------------------------------------------------------------------------- 81// ---------------------------------------------------------------------------
diff --git a/server/lib/runners/runner.ts b/server/lib/runners/runner.ts
index 921cae6f2..947fdb3f0 100644
--- a/server/lib/runners/runner.ts
+++ b/server/lib/runners/runner.ts
@@ -2,8 +2,9 @@ import express from 'express'
2import { retryTransactionWrapper } from '@server/helpers/database-utils' 2import { retryTransactionWrapper } from '@server/helpers/database-utils'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { sequelizeTypescript } from '@server/initializers/database' 4import { sequelizeTypescript } from '@server/initializers/database'
5import { MRunner } from '@server/types/models/runners' 5import { MRunner, MRunnerJob } from '@server/types/models/runners'
6import { RUNNER_JOBS } from '@server/initializers/constants' 6import { RUNNER_JOBS } from '@server/initializers/constants'
7import { RunnerJobState } from '@shared/models'
7 8
8const lTags = loggerTagsFactory('runner') 9const lTags = loggerTagsFactory('runner')
9 10
@@ -32,6 +33,17 @@ function updateLastRunnerContact (req: express.Request, runner: MRunner) {
32 .finally(() => updatingRunner.delete(runner.id)) 33 .finally(() => updatingRunner.delete(runner.id))
33} 34}
34 35
36function runnerJobCanBeCancelled (runnerJob: MRunnerJob) {
37 const allowedStates = new Set<RunnerJobState>([
38 RunnerJobState.PENDING,
39 RunnerJobState.PROCESSING,
40 RunnerJobState.WAITING_FOR_PARENT_JOB
41 ])
42
43 return allowedStates.has(runnerJob.state)
44}
45
35export { 46export {
36 updateLastRunnerContact 47 updateLastRunnerContact,
48 runnerJobCanBeCancelled
37} 49}
diff --git a/server/middlewares/validators/runners/jobs.ts b/server/middlewares/validators/runners/jobs.ts
index 384b209ba..62f9340a5 100644
--- a/server/middlewares/validators/runners/jobs.ts
+++ b/server/middlewares/validators/runners/jobs.ts
@@ -1,8 +1,9 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isUUIDValid } from '@server/helpers/custom-validators/misc' 3import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc'
4import { 4import {
5 isRunnerJobAbortReasonValid, 5 isRunnerJobAbortReasonValid,
6 isRunnerJobArrayOfStateValid,
6 isRunnerJobErrorMessageValid, 7 isRunnerJobErrorMessageValid,
7 isRunnerJobProgressValid, 8 isRunnerJobProgressValid,
8 isRunnerJobSuccessPayloadValid, 9 isRunnerJobSuccessPayloadValid,
@@ -12,7 +13,9 @@ import {
12import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners' 13import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners'
13import { cleanUpReqFiles } from '@server/helpers/express-utils' 14import { cleanUpReqFiles } from '@server/helpers/express-utils'
14import { LiveManager } from '@server/lib/live' 15import { LiveManager } from '@server/lib/live'
16import { runnerJobCanBeCancelled } from '@server/lib/runners'
15import { RunnerJobModel } from '@server/models/runner/runner-job' 17import { RunnerJobModel } from '@server/models/runner/runner-job'
18import { arrayify } from '@shared/core-utils'
16import { 19import {
17 HttpStatusCode, 20 HttpStatusCode,
18 RunnerJobLiveRTMPHLSTranscodingPrivatePayload, 21 RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
@@ -119,13 +122,7 @@ export const cancelRunnerJobValidator = [
119 (req: express.Request, res: express.Response, next: express.NextFunction) => { 122 (req: express.Request, res: express.Response, next: express.NextFunction) => {
120 const runnerJob = res.locals.runnerJob 123 const runnerJob = res.locals.runnerJob
121 124
122 const allowedStates = new Set<RunnerJobState>([ 125 if (runnerJobCanBeCancelled(runnerJob) !== true) {
123 RunnerJobState.PENDING,
124 RunnerJobState.PROCESSING,
125 RunnerJobState.WAITING_FOR_PARENT_JOB
126 ])
127
128 if (allowedStates.has(runnerJob.state) !== true) {
129 return res.fail({ 126 return res.fail({
130 status: HttpStatusCode.BAD_REQUEST_400, 127 status: HttpStatusCode.BAD_REQUEST_400,
131 message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state', 128 message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
@@ -137,6 +134,21 @@ export const cancelRunnerJobValidator = [
137 } 134 }
138] 135]
139 136
137export const listRunnerJobsValidator = [
138 query('search')
139 .optional()
140 .custom(exists),
141
142 query('stateOneOf')
143 .optional()
144 .customSanitizer(arrayify)
145 .custom(isRunnerJobArrayOfStateValid),
146
147 (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 return next()
149 }
150]
151
140export const runnerJobGetValidator = [ 152export const runnerJobGetValidator = [
141 param('jobUUID').custom(isUUIDValid), 153 param('jobUUID').custom(isUUIDValid),
142 154
diff --git a/server/models/runner/runner-job.ts b/server/models/runner/runner-job.ts
index add6f9a43..f2ffd6a84 100644
--- a/server/models/runner/runner-job.ts
+++ b/server/models/runner/runner-job.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Op, Transaction } from 'sequelize' 1import { Op, Transaction } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BelongsTo, 4 BelongsTo,
@@ -13,7 +13,7 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { isUUIDValid } from '@server/helpers/custom-validators/misc' 16import { isArray, isUUIDValid } from '@server/helpers/custom-validators/misc'
17import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' 17import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
18import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners' 18import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners'
19import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models' 19import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models'
@@ -227,28 +227,38 @@ export class RunnerJobModel extends Model<Partial<AttributesOnly<RunnerJobModel>
227 count: number 227 count: number
228 sort: string 228 sort: string
229 search?: string 229 search?: string
230 stateOneOf?: RunnerJobState[]
230 }) { 231 }) {
231 const { start, count, sort, search } = options 232 const { start, count, sort, search, stateOneOf } = options
232 233
233 const query: FindOptions = { 234 const query = {
234 offset: start, 235 offset: start,
235 limit: count, 236 limit: count,
236 order: getSort(sort) 237 order: getSort(sort),
238 where: []
237 } 239 }
238 240
239 if (search) { 241 if (search) {
240 if (isUUIDValid(search)) { 242 if (isUUIDValid(search)) {
241 query.where = { uuid: search } 243 query.where.push({ uuid: search })
242 } else { 244 } else {
243 query.where = { 245 query.where.push({
244 [Op.or]: [ 246 [Op.or]: [
245 searchAttribute(search, 'type'), 247 searchAttribute(search, 'type'),
246 searchAttribute(search, '$Runner.name$') 248 searchAttribute(search, '$Runner.name$')
247 ] 249 ]
248 } 250 })
249 } 251 }
250 } 252 }
251 253
254 if (isArray(stateOneOf) && stateOneOf.length !== 0) {
255 query.where.push({
256 state: {
257 [Op.in]: stateOneOf
258 }
259 })
260 }
261
252 return Promise.all([ 262 return Promise.all([
253 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query), 263 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query),
254 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query) 264 RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query)
diff --git a/server/tests/api/check-params/runners.ts b/server/tests/api/check-params/runners.ts
index 9112ff716..7f9a0cd32 100644
--- a/server/tests/api/check-params/runners.ts
+++ b/server/tests/api/check-params/runners.ts
@@ -1,14 +1,14 @@
1import { basename } from 'path'
2/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { basename } from 'path'
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' 3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
4import { 4import {
5 HttpStatusCode, 5 HttpStatusCode,
6 isVideoStudioTaskIntro, 6 isVideoStudioTaskIntro,
7 RunnerJob, 7 RunnerJob,
8 RunnerJobState, 8 RunnerJobState,
9 RunnerJobStudioTranscodingPayload,
9 RunnerJobSuccessPayload, 10 RunnerJobSuccessPayload,
10 RunnerJobUpdatePayload, 11 RunnerJobUpdatePayload,
11 RunnerJobStudioTranscodingPayload,
12 VideoPrivacy, 12 VideoPrivacy,
13 VideoStudioTaskIntro 13 VideoStudioTaskIntro
14} from '@shared/models' 14} from '@shared/models'
@@ -236,6 +236,10 @@ describe('Test managing runners', function () {
236 await checkBadSortPagination(server.url, path, server.accessToken) 236 await checkBadSortPagination(server.url, path, server.accessToken)
237 }) 237 })
238 238
239 it('Should fail with an invalid state', async function () {
240 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
241 })
242
239 it('Should succeed to list with the correct params', async function () { 243 it('Should succeed to list with the correct params', async function () {
240 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) 244 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
241 }) 245 })
@@ -307,8 +311,48 @@ describe('Test managing runners', function () {
307 await checkBadSortPagination(server.url, path, server.accessToken) 311 await checkBadSortPagination(server.url, path, server.accessToken)
308 }) 312 })
309 313
310 it('Should succeed to list with the correct params', async function () { 314 it('Should fail with an invalid state', async function () {
311 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt' }) 315 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any })
316 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any })
317 })
318
319 it('Should succeed with the correct params', async function () {
320 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] })
321 })
322 })
323
324 describe('Delete', function () {
325 let jobUUID: string
326
327 before(async function () {
328 this.timeout(60000)
329
330 await server.videos.quickUpload({ name: 'video' })
331 await waitJobs([ server ])
332
333 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
334 jobUUID = availableJobs[0].uuid
335 })
336
337 it('Should fail without oauth token', async function () {
338 await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
339 })
340
341 it('Should fail without admin rights', async function () {
342 await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
343 })
344
345 it('Should fail with a bad job uuid', async function () {
346 await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
347 })
348
349 it('Should fail with an unknown job uuid', async function () {
350 const jobUUID = badUUID
351 await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
352 })
353
354 it('Should succeed with the correct params', async function () {
355 await server.runnerJobs.deleteByAdmin({ jobUUID })
312 }) 356 })
313 }) 357 })
314 358
diff --git a/server/tests/api/runners/runner-common.ts b/server/tests/api/runners/runner-common.ts
index 34a51abe7..7fed75f40 100644
--- a/server/tests/api/runners/runner-common.ts
+++ b/server/tests/api/runners/runner-common.ts
@@ -339,6 +339,30 @@ describe('Test runner common actions', function () {
339 339
340 expect(data).to.not.have.lengthOf(0) 340 expect(data).to.not.have.lengthOf(0)
341 expect(total).to.not.equal(0) 341 expect(total).to.not.equal(0)
342
343 for (const job of data) {
344 expect(job.type).to.include('hls')
345 }
346 }
347 })
348
349 it('Should filter jobs', async function () {
350 {
351 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
352
353 expect(data).to.not.have.lengthOf(0)
354 expect(total).to.not.equal(0)
355
356 for (const job of data) {
357 expect(job.state.label).to.equal('Waiting for parent job to finish')
358 }
359 }
360
361 {
362 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
363
364 expect(data).to.have.lengthOf(0)
365 expect(total).to.equal(0)
342 } 366 }
343 }) 367 })
344 }) 368 })
@@ -598,6 +622,33 @@ describe('Test runner common actions', function () {
598 }) 622 })
599 }) 623 })
600 624
625 describe('Remove', function () {
626
627 it('Should remove a pending job', async function () {
628 await server.videos.quickUpload({ name: 'video' })
629 await waitJobs([ server ])
630
631 {
632 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
633
634 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
635 jobUUID = pendingJob.uuid
636
637 await server.runnerJobs.deleteByAdmin({ jobUUID })
638 }
639
640 {
641 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
642
643 const parent = data.find(j => j.uuid === jobUUID)
644 expect(parent).to.not.exist
645
646 const children = data.filter(j => j.parent?.uuid === jobUUID)
647 expect(children).to.have.lengthOf(0)
648 }
649 })
650 })
651
601 describe('Stalled jobs', function () { 652 describe('Stalled jobs', function () {
602 653
603 it('Should abort stalled jobs', async function () { 654 it('Should abort stalled jobs', async function () {
diff --git a/shared/models/runners/list-runner-jobs-query.model.ts b/shared/models/runners/list-runner-jobs-query.model.ts
index a5b62c55d..ef19b31fa 100644
--- a/shared/models/runners/list-runner-jobs-query.model.ts
+++ b/shared/models/runners/list-runner-jobs-query.model.ts
@@ -1,6 +1,9 @@
1import { RunnerJobState } from './runner-job-state.model'
2
1export interface ListRunnerJobsQuery { 3export interface ListRunnerJobsQuery {
2 start?: number 4 start?: number
3 count?: number 5 count?: number
4 sort?: string 6 sort?: string
5 search?: string 7 search?: string
8 stateOneOf?: RunnerJobState[]
6} 9}
diff --git a/shared/server-commands/runners/runner-jobs-command.ts b/shared/server-commands/runners/runner-jobs-command.ts
index 26dbef77a..0a0ffb5d3 100644
--- a/shared/server-commands/runners/runner-jobs-command.ts
+++ b/shared/server-commands/runners/runner-jobs-command.ts
@@ -8,6 +8,7 @@ import {
8 isHLSTranscodingPayloadSuccess, 8 isHLSTranscodingPayloadSuccess,
9 isLiveRTMPHLSTranscodingUpdatePayload, 9 isLiveRTMPHLSTranscodingUpdatePayload,
10 isWebVideoOrAudioMergeTranscodingPayloadSuccess, 10 isWebVideoOrAudioMergeTranscodingPayloadSuccess,
11 ListRunnerJobsQuery,
11 RequestRunnerJobBody, 12 RequestRunnerJobBody,
12 RequestRunnerJobResult, 13 RequestRunnerJobResult,
13 ResultList, 14 ResultList,
@@ -27,19 +28,14 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
27 28
28export class RunnerJobsCommand extends AbstractCommand { 29export class RunnerJobsCommand extends AbstractCommand {
29 30
30 list (options: OverrideCommandOptions & { 31 list (options: OverrideCommandOptions & ListRunnerJobsQuery = {}) {
31 start?: number
32 count?: number
33 sort?: string
34 search?: string
35 } = {}) {
36 const path = '/api/v1/runners/jobs' 32 const path = '/api/v1/runners/jobs'
37 33
38 return this.getRequestBody<ResultList<RunnerJobAdmin>>({ 34 return this.getRequestBody<ResultList<RunnerJobAdmin>>({
39 ...options, 35 ...options,
40 36
41 path, 37 path,
42 query: pick(options, [ 'start', 'count', 'sort', 'search' ]), 38 query: pick(options, [ 'start', 'count', 'sort', 'search', 'stateOneOf' ]),
43 implicitToken: true, 39 implicitToken: true,
44 defaultExpectedStatus: HttpStatusCode.OK_200 40 defaultExpectedStatus: HttpStatusCode.OK_200
45 }) 41 })
@@ -57,6 +53,18 @@ export class RunnerJobsCommand extends AbstractCommand {
57 }) 53 })
58 } 54 }
59 55
56 deleteByAdmin (options: OverrideCommandOptions & { jobUUID: string }) {
57 const path = '/api/v1/runners/jobs/' + options.jobUUID
58
59 return this.deleteRequest({
60 ...options,
61
62 path,
63 implicitToken: true,
64 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
65 })
66 }
67
60 // --------------------------------------------------------------------------- 68 // ---------------------------------------------------------------------------
61 69
62 request (options: OverrideCommandOptions & RequestRunnerJobBody) { 70 request (options: OverrideCommandOptions & RequestRunnerJobBody) {
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 654bd7461..44daecf85 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -6088,6 +6088,21 @@ paths:
6088 '204': 6088 '204':
6089 description: successful operation 6089 description: successful operation
6090 6090
6091 /api/v1/runners/jobs/{jobUUID}:
6092 delete:
6093 summary: Delete a job
6094 description: The endpoint will first cancel the job if needed, and then remove it from the database. Children jobs will also be removed
6095 security:
6096 - OAuth2:
6097 - admin
6098 tags:
6099 - Runner Jobs
6100 parameters:
6101 - $ref: '#/components/parameters/jobUUID'
6102 responses:
6103 '204':
6104 description: successful operation
6105
6091 /api/v1/runners/jobs: 6106 /api/v1/runners/jobs:
6092 get: 6107 get:
6093 summary: List jobs 6108 summary: List jobs
@@ -6101,6 +6116,13 @@ paths:
6101 - $ref: '#/components/parameters/count' 6116 - $ref: '#/components/parameters/count'
6102 - $ref: '#/components/parameters/runnerJobSort' 6117 - $ref: '#/components/parameters/runnerJobSort'
6103 - $ref: '#/components/parameters/search' 6118 - $ref: '#/components/parameters/search'
6119 - name: stateOneOf
6120 in: query
6121 required: false
6122 schema:
6123 type: array
6124 items:
6125 $ref: '#/components/schemas/RunnerJobState'
6104 responses: 6126 responses:
6105 '200': 6127 '200':
6106 description: successful operation 6128 description: successful operation