diff options
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 { | 31 | my-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' | |||
5 | import { DropdownAction } from '@app/shared/shared-main' | 5 | import { DropdownAction } from '@app/shared/shared-main' |
6 | import { RunnerJob, RunnerJobState } from '@shared/models' | 6 | import { RunnerJob, RunnerJobState } from '@shared/models' |
7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' | 7 | import { RunnerJobFormatted, RunnerService } from '../runner.service' |
8 | import { 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 | |||
4 | my-copy-button { | ||
5 | @include margin-left(3px); | ||
6 | } | ||
7 | |||
8 | tr: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 | }) |
12 | export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit { | 13 | export 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' | |||
6 | import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core' |
7 | import { arrayify, peertubeTranslate } from '@shared/core-utils' | 7 | import { arrayify, peertubeTranslate } from '@shared/core-utils' |
8 | import { ResultList } from '@shared/models/common' | 8 | import { ResultList } from '@shared/models/common' |
9 | import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners' | 9 | import { Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models/runners' |
10 | import { environment } from '../../../../environments/environment' | 10 | import { environment } from '../../../../environments/environment' |
11 | 11 | ||
12 | export type RunnerJobFormatted = RunnerJob & { | 12 | export 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 { | 155 | my-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 | ||
8 | const debugLogger = debug('peertube:rest') | 8 | const debugLogger = debug('peertube:rest') |
9 | 9 | ||
10 | type ParseQueryHandlerResult = string | number | boolean | string[] | number[] | boolean[] | ||
11 | |||
10 | interface QueryStringFilterPrefixes { | 12 | interface 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 | ||
19 | type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>> | 21 | type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, ParseQueryHandlerResult | ParseQueryHandlerResult[]>> |
20 | type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string } | 22 | type 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 | ||
34 | div[role=menu] { | 34 | div[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 | |||
4 | button: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 | |||
13 | my-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 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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 @@ | |||
1 | export * from './action-dropdown.component' | 1 | export * from './action-dropdown.component' |
2 | export * from './button.component' | 2 | export * from './button.component' |
3 | export * from './copy-button.component' | ||
3 | export * from './delete-button.component' | 4 | export * from './delete-button.component' |
4 | export * from './edit-button.component' | 5 | export * 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' |
33 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 33 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
34 | import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' | 34 | import { ActionDropdownComponent, ButtonComponent, CopyButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' |
35 | import { CustomPageService } from './custom-page' | 35 | import { CustomPageService } from './custom-page' |
36 | import { DateToggleComponent } from './date' | 36 | import { DateToggleComponent } from './date' |
37 | import { FeedComponent } from './feeds' | 37 | import { 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' | |||
5 | import { generateRunnerJobToken } from '@server/helpers/token-generator' | 5 | import { generateRunnerJobToken } from '@server/helpers/token-generator' |
6 | import { MIMETYPES } from '@server/initializers/constants' | 6 | import { MIMETYPES } from '@server/initializers/constants' |
7 | import { sequelizeTypescript } from '@server/initializers/database' | 7 | import { sequelizeTypescript } from '@server/initializers/database' |
8 | import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners' | 8 | import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners' |
9 | import { | 9 | import { |
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 | ||
139 | runnerJobsRouter.delete('/jobs/:jobUUID', | ||
140 | authenticate, | ||
141 | ensureUserHasRight(UserRight.MANAGE_RUNNERS), | ||
142 | asyncMiddleware(runnerJobGetValidator), | ||
143 | asyncMiddleware(deleteRunnerJob) | ||
144 | ) | ||
145 | |||
137 | // --------------------------------------------------------------------------- | 146 | // --------------------------------------------------------------------------- |
138 | 147 | ||
139 | export { | 148 | export { |
@@ -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 | ||
386 | async 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 | |||
377 | async function listRunnerJobs (req: express.Request, res: express.Response) { | 401 | async 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 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | 1 | import { UploadFilesForCheck } from 'express' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | 3 | import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' |
4 | import { | 4 | import { |
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' |
14 | import { exists, isFileValid, isSafeFilename } from '../misc' | 14 | import { exists, isArray, isFileValid, isSafeFilename } from '../misc' |
15 | 15 | ||
16 | const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS | 16 | const 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 | ||
59 | function isRunnerJobStateValid (value: any) { | ||
60 | return exists(value) && RUNNER_JOB_STATES[value] !== undefined | ||
61 | } | ||
62 | |||
63 | function isRunnerJobArrayOfStateValid (value: any) { | ||
64 | return isArray(value) && value.every(v => isRunnerJobStateValid(v)) | ||
65 | } | ||
66 | |||
59 | // --------------------------------------------------------------------------- | 67 | // --------------------------------------------------------------------------- |
60 | 68 | ||
61 | export { | 69 | export { |
@@ -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' | |||
2 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | 2 | import { retryTransactionWrapper } from '@server/helpers/database-utils' |
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { sequelizeTypescript } from '@server/initializers/database' | 4 | import { sequelizeTypescript } from '@server/initializers/database' |
5 | import { MRunner } from '@server/types/models/runners' | 5 | import { MRunner, MRunnerJob } from '@server/types/models/runners' |
6 | import { RUNNER_JOBS } from '@server/initializers/constants' | 6 | import { RUNNER_JOBS } from '@server/initializers/constants' |
7 | import { RunnerJobState } from '@shared/models' | ||
7 | 8 | ||
8 | const lTags = loggerTagsFactory('runner') | 9 | const 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 | ||
36 | function 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 | |||
35 | export { | 46 | export { |
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | 3 | import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc' |
4 | import { | 4 | import { |
5 | isRunnerJobAbortReasonValid, | 5 | isRunnerJobAbortReasonValid, |
6 | isRunnerJobArrayOfStateValid, | ||
6 | isRunnerJobErrorMessageValid, | 7 | isRunnerJobErrorMessageValid, |
7 | isRunnerJobProgressValid, | 8 | isRunnerJobProgressValid, |
8 | isRunnerJobSuccessPayloadValid, | 9 | isRunnerJobSuccessPayloadValid, |
@@ -12,7 +13,9 @@ import { | |||
12 | import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners' | 13 | import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners' |
13 | import { cleanUpReqFiles } from '@server/helpers/express-utils' | 14 | import { cleanUpReqFiles } from '@server/helpers/express-utils' |
14 | import { LiveManager } from '@server/lib/live' | 15 | import { LiveManager } from '@server/lib/live' |
16 | import { runnerJobCanBeCancelled } from '@server/lib/runners' | ||
15 | import { RunnerJobModel } from '@server/models/runner/runner-job' | 17 | import { RunnerJobModel } from '@server/models/runner/runner-job' |
18 | import { arrayify } from '@shared/core-utils' | ||
16 | import { | 19 | import { |
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 | ||
137 | export 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 | |||
140 | export const runnerJobGetValidator = [ | 152 | export 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 @@ | |||
1 | import { FindOptions, Op, Transaction } from 'sequelize' | 1 | import { Op, Transaction } from 'sequelize' |
2 | import { | 2 | import { |
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' |
16 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | 16 | import { isArray, isUUIDValid } from '@server/helpers/custom-validators/misc' |
17 | import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' | 17 | import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants' |
18 | import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners' | 18 | import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners' |
19 | import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models' | 19 | import { 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 @@ | |||
1 | import { 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 */ |
2 | import { basename } from 'path' | ||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' | 3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' |
4 | import { | 4 | import { |
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 @@ | |||
1 | import { RunnerJobState } from './runner-job-state.model' | ||
2 | |||
1 | export interface ListRunnerJobsQuery { | 3 | export 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 | ||
28 | export class RunnerJobsCommand extends AbstractCommand { | 29 | export 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 |