Improve runner management
* Add ability to remove runner jobs * Add runner job state quick filter * Merge registration tokens and runners tables in the same page * Add copy button to copy registration token
This commit is contained in:
parent
f5af5feb5a
commit
f18003d0ac
31 changed files with 446 additions and 88 deletions
|
@ -25,11 +25,11 @@
|
||||||
|
|
||||||
<div class="actor-handle">
|
<div class="actor-handle">
|
||||||
<span>@{{ account.nameWithHost }}</span>
|
<span>@{{ account.nameWithHost }}</span>
|
||||||
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
|
||||||
class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
|
<my-copy-button
|
||||||
>
|
[value]="account.nameWithHostForced" i18n-notification notification="Username copied"
|
||||||
<my-global-icon iconName="copy"></my-global-icon>
|
title="Copy account handle" i18n-title
|
||||||
</button>
|
></my-copy-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-counters">
|
<div class="actor-counters">
|
||||||
|
|
|
@ -28,14 +28,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button {
|
my-copy-button {
|
||||||
@include margin-left(3px);
|
@include margin-left(3px);
|
||||||
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
my-global-icon {
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-info {
|
.account-info {
|
||||||
|
|
|
@ -115,10 +115,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
this.redirectService.redirectToHomepage()
|
this.redirectService.redirectToHomepage()
|
||||||
}
|
}
|
||||||
|
|
||||||
activateCopiedMessage () {
|
|
||||||
this.notifier.success($localize`Username copied`)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchChanged (search: string) {
|
searchChanged (search: string) {
|
||||||
const queryParams = { search }
|
const queryParams = { search }
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th>
|
<th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th>
|
||||||
<th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th>
|
<th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th>
|
||||||
<th i18n>Runner</th>
|
<th i18n>Runner</th>
|
||||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 200px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ms-auto d-flex">
|
<div class="ms-auto d-flex">
|
||||||
<my-advanced-input-filter class="me-2" (search)="onSearch($event)"></my-advanced-input-filter>
|
<my-advanced-input-filter class="me-2" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||||
|
|
||||||
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
|
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,7 +73,9 @@
|
||||||
|
|
||||||
<td>{{ runnerJob.uuid }}</td>
|
<td>{{ runnerJob.uuid }}</td>
|
||||||
<td>{{ runnerJob.type }}</td>
|
<td>{{ runnerJob.type }}</td>
|
||||||
<td>{{ runnerJob.state.label }}</td>
|
<td>
|
||||||
|
<span class="pt-badge" [ngClass]="getStateBadgeColor(runnerJob)">{{ runnerJob.state.label }}</span>
|
||||||
|
</td>
|
||||||
<td>{{ runnerJob.priority }}</td>
|
<td>{{ runnerJob.priority }}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { formatICU } from '@app/helpers'
|
||||||
import { DropdownAction } from '@app/shared/shared-main'
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
import { RunnerJob, RunnerJobState } from '@shared/models'
|
import { RunnerJob, RunnerJobState } from '@shared/models'
|
||||||
import { RunnerJobFormatted, RunnerService } from '../runner.service'
|
import { RunnerJobFormatted, RunnerService } from '../runner.service'
|
||||||
|
import { AdvancedInputFilter } from '@app/shared/shared-forms'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-runner-job-list',
|
selector: 'my-runner-job-list',
|
||||||
|
@ -20,6 +21,30 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
|
||||||
actions: DropdownAction<RunnerJob>[][] = []
|
actions: DropdownAction<RunnerJob>[][] = []
|
||||||
bulkActions: DropdownAction<RunnerJob[]>[][] = []
|
bulkActions: DropdownAction<RunnerJob[]>[][] = []
|
||||||
|
|
||||||
|
inputFilters: AdvancedInputFilter[] = [
|
||||||
|
{
|
||||||
|
title: $localize`Advanced filters`,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'state:completed',
|
||||||
|
label: $localize`Completed jobs`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'state:pending state:waiting-for-parent-job',
|
||||||
|
label: $localize`Pending jobs`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'state:processing',
|
||||||
|
label: $localize`Jobs that are being processed`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'state:errored state:parent-errored',
|
||||||
|
label: $localize`Failed jobs`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private runnerService: RunnerService,
|
private runnerService: RunnerService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
|
@ -36,6 +61,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
|
||||||
handler: job => this.cancelJobs([ job ]),
|
handler: job => this.cancelJobs([ job ]),
|
||||||
isDisplayed: job => this.canCancelJob(job)
|
isDisplayed: job => this.canCancelJob(job)
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $localize`Delete this job`,
|
||||||
|
handler: job => this.removeJobs([ job ])
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -46,6 +77,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
|
||||||
handler: jobs => this.cancelJobs(jobs),
|
handler: jobs => this.cancelJobs(jobs),
|
||||||
isDisplayed: jobs => jobs.every(j => this.canCancelJob(j))
|
isDisplayed: jobs => jobs.every(j => this.canCancelJob(j))
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $localize`Delete`,
|
||||||
|
handler: jobs => this.removeJobs(jobs)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -77,6 +114,45 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeJobs (jobs: RunnerJob[]) {
|
||||||
|
const message = formatICU(
|
||||||
|
$localize`Do you really want to remove {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be removed.`,
|
||||||
|
{ count: jobs.length }
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await this.confirmService.confirm(message, $localize`Remove`)
|
||||||
|
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.runnerService.removeJobs(jobs)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reloadData()
|
||||||
|
this.notifier.success($localize`Job(s) removed.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getStateBadgeColor (job: RunnerJob) {
|
||||||
|
switch (job.state.id) {
|
||||||
|
case RunnerJobState.ERRORED:
|
||||||
|
case RunnerJobState.PARENT_ERRORED:
|
||||||
|
return 'badge-danger'
|
||||||
|
|
||||||
|
case RunnerJobState.COMPLETED:
|
||||||
|
return 'badge-success'
|
||||||
|
|
||||||
|
case RunnerJobState.PENDING:
|
||||||
|
case RunnerJobState.WAITING_FOR_PARENT_JOB:
|
||||||
|
return 'badge-warning'
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'badge-info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected reloadDataInternal () {
|
protected reloadDataInternal () {
|
||||||
this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
|
this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
|
|
@ -45,7 +45,14 @@
|
||||||
></my-action-dropdown>
|
></my-action-dropdown>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ registrationToken.registrationToken }}</td>
|
<td>
|
||||||
|
{{ registrationToken.registrationToken }}
|
||||||
|
|
||||||
|
<my-copy-button
|
||||||
|
[value]="registrationToken.registrationToken" i18n-notification notification="Registration token copied"
|
||||||
|
i18n-title title="Copy registration token"
|
||||||
|
></my-copy-button>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>{{ registrationToken.createdAt | date: 'short' }}</td>
|
<td>{{ registrationToken.createdAt | date: 'short' }}</td>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
@use '_variables' as *;
|
||||||
|
@use '_mixins' as *;
|
||||||
|
|
||||||
|
my-copy-button {
|
||||||
|
@include margin-left(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:not(:hover) {
|
||||||
|
my-copy-button {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { RunnerService } from '../runner.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-runner-registration-token-list',
|
selector: 'my-runner-registration-token-list',
|
||||||
|
styleUrls: [ './runner-registration-token-list.component.scss' ],
|
||||||
templateUrl: './runner-registration-token-list.component.html'
|
templateUrl: './runner-registration-token-list.component.html'
|
||||||
})
|
})
|
||||||
export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
|
export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
|
import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
|
||||||
import { arrayify, peertubeTranslate } from '@shared/core-utils'
|
import { arrayify, peertubeTranslate } from '@shared/core-utils'
|
||||||
import { ResultList } from '@shared/models/common'
|
import { ResultList } from '@shared/models/common'
|
||||||
import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners'
|
import { Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models/runners'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
|
|
||||||
export type RunnerJobFormatted = RunnerJob & {
|
export type RunnerJobFormatted = RunnerJob & {
|
||||||
|
@ -60,7 +60,9 @@ export class RunnerService {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
if (search) params = params.append('search', search)
|
if (search) {
|
||||||
|
params = this.buildParamsFromSearch(search, params)
|
||||||
|
}
|
||||||
|
|
||||||
return forkJoin([
|
return forkJoin([
|
||||||
this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
|
this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
|
||||||
|
@ -90,6 +92,31 @@ export class RunnerService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildParamsFromSearch (search: string, params: HttpParams) {
|
||||||
|
const filters = this.restService.parseQueryStringFilter(search, {
|
||||||
|
stateOneOf: {
|
||||||
|
prefix: 'state:',
|
||||||
|
multiple: true,
|
||||||
|
handler: v => {
|
||||||
|
if (v === 'completed') return RunnerJobState.COMPLETED
|
||||||
|
if (v === 'processing') return RunnerJobState.PROCESSING
|
||||||
|
if (v === 'errored') return RunnerJobState.ERRORED
|
||||||
|
if (v === 'pending') return RunnerJobState.PENDING
|
||||||
|
if (v === 'waiting-for-parent-job') return RunnerJobState.WAITING_FOR_PARENT_JOB
|
||||||
|
if (v === 'parent-errored') return RunnerJobState.PARENT_ERRORED
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(filters)
|
||||||
|
|
||||||
|
return this.restService.addObjectParams(params, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
cancelJobs (jobsArg: RunnerJob | RunnerJob[]) {
|
cancelJobs (jobsArg: RunnerJob | RunnerJob[]) {
|
||||||
const jobs = arrayify(jobsArg)
|
const jobs = arrayify(jobsArg)
|
||||||
|
|
||||||
|
@ -101,6 +128,17 @@ export class RunnerService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeJobs (jobsArg: RunnerJob | RunnerJob[]) {
|
||||||
|
const jobs = arrayify(jobsArg)
|
||||||
|
|
||||||
|
return from(jobs)
|
||||||
|
.pipe(
|
||||||
|
concatMap(job => this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/jobs/' + job.uuid)),
|
||||||
|
toArray(),
|
||||||
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
listRunners (options: {
|
listRunners (options: {
|
||||||
|
|
|
@ -64,11 +64,11 @@
|
||||||
|
|
||||||
<div class="actor-handle">
|
<div class="actor-handle">
|
||||||
<span>@{{ videoChannel.nameWithHost }}</span>
|
<span>@{{ videoChannel.nameWithHost }}</span>
|
||||||
<button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
|
|
||||||
class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title
|
<my-copy-button
|
||||||
>
|
[value]="videoChannel.nameWithHostForced" i18n-notification notification="Handle copied"
|
||||||
<my-global-icon iconName="copy"></my-global-icon>
|
title="Copy channel handle" i18n-title
|
||||||
</button>
|
></my-copy-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-counters">
|
<div class="actor-counters">
|
||||||
|
|
|
@ -152,14 +152,8 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button {
|
my-copy-button {
|
||||||
@include margin-left(3px);
|
@include margin-left(3px);
|
||||||
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
my-global-icon {
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1400px) {
|
@media screen and (max-width: 1400px) {
|
||||||
|
|
|
@ -120,10 +120,6 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
||||||
return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
|
return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
activateCopiedMessage () {
|
|
||||||
this.notifier.success($localize`Username copied`)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasShowMoreDescription () {
|
hasShowMoreDescription () {
|
||||||
return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
|
return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,18 @@ import { RestPagination } from './rest-pagination'
|
||||||
|
|
||||||
const debugLogger = debug('peertube:rest')
|
const debugLogger = debug('peertube:rest')
|
||||||
|
|
||||||
|
type ParseQueryHandlerResult = string | number | boolean | string[] | number[] | boolean[]
|
||||||
|
|
||||||
interface QueryStringFilterPrefixes {
|
interface QueryStringFilterPrefixes {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
prefix: string
|
prefix: string
|
||||||
handler?: (v: string) => string | number | boolean
|
handler?: (v: string) => ParseQueryHandlerResult
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
isBoolean?: boolean
|
isBoolean?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>>
|
type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, ParseQueryHandlerResult | ParseQueryHandlerResult[]>>
|
||||||
type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
|
type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -33,6 +33,5 @@ my-global-icon {
|
||||||
|
|
||||||
div[role=menu] {
|
div[role=menu] {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
min-height: 200px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,12 @@
|
||||||
<my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon>
|
<my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<my-copy-button
|
||||||
*ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
|
*ngIf="withCopy" [value]="input.value" i18n-notification notification="Copied"
|
||||||
class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
|
[isInputGroup]="true" i18n
|
||||||
>
|
>
|
||||||
<my-global-icon iconName="copy"></my-global-icon>
|
COPY
|
||||||
<span class="copy-text">Copy</span>
|
</my-copy-button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="formError" class="form-error">{{ formError }}</div>
|
<div *ngIf="formError" class="form-error">{{ formError }}</div>
|
||||||
|
|
|
@ -46,10 +46,6 @@ export class InputTextComponent implements ControlValueAccessor {
|
||||||
this.show = !this.show
|
this.show = !this.show
|
||||||
}
|
}
|
||||||
|
|
||||||
activateCopiedMessage () {
|
|
||||||
this.notifier.success($localize`Copied`)
|
|
||||||
}
|
|
||||||
|
|
||||||
propagateChange = (_: any) => { /* empty */ }
|
propagateChange = (_: any) => { /* empty */ }
|
||||||
|
|
||||||
writeValue (value: string) {
|
writeValue (value: string) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-sm copy-button"
|
||||||
|
[cdkCopyToClipboard]="value" (click)="activateCopiedMessage()"
|
||||||
|
[title]="title" [ngClass]="{ 'is-input-group': isInputGroup }"
|
||||||
|
>
|
||||||
|
<my-global-icon iconName="copy"></my-global-icon>
|
||||||
|
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</button>
|
|
@ -0,0 +1,15 @@
|
||||||
|
@use '_variables' as *;
|
||||||
|
@use '_mixins' as *;
|
||||||
|
|
||||||
|
button:not(.is-input-group) {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-input-group {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 15px;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { Notifier } from '@app/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-copy-button',
|
||||||
|
styleUrls: [ './copy-button.component.scss' ],
|
||||||
|
templateUrl: './copy-button.component.html'
|
||||||
|
})
|
||||||
|
export class CopyButtonComponent {
|
||||||
|
@Input() value: string
|
||||||
|
@Input() title: string
|
||||||
|
@Input() notification: string
|
||||||
|
@Input() isInputGroup = false
|
||||||
|
|
||||||
|
constructor (private notifier: Notifier) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
activateCopiedMessage () {
|
||||||
|
if (this.notification) this.notifier.success(this.notification)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './action-dropdown.component'
|
export * from './action-dropdown.component'
|
||||||
export * from './button.component'
|
export * from './button.component'
|
||||||
|
export * from './copy-button.component'
|
||||||
export * from './delete-button.component'
|
export * from './delete-button.component'
|
||||||
export * from './edit-button.component'
|
export * from './edit-button.component'
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
PeerTubeTemplateDirective
|
PeerTubeTemplateDirective
|
||||||
} from './angular'
|
} from './angular'
|
||||||
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
|
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
|
||||||
import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
|
import { ActionDropdownComponent, ButtonComponent, CopyButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
|
||||||
import { CustomPageService } from './custom-page'
|
import { CustomPageService } from './custom-page'
|
||||||
import { DateToggleComponent } from './date'
|
import { DateToggleComponent } from './date'
|
||||||
import { FeedComponent } from './feeds'
|
import { FeedComponent } from './feeds'
|
||||||
|
@ -100,6 +100,7 @@ import { VideoChannelService } from './video-channel'
|
||||||
|
|
||||||
ActionDropdownComponent,
|
ActionDropdownComponent,
|
||||||
ButtonComponent,
|
ButtonComponent,
|
||||||
|
CopyButtonComponent,
|
||||||
DeleteButtonComponent,
|
DeleteButtonComponent,
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
|
|
||||||
|
@ -162,6 +163,7 @@ import { VideoChannelService } from './video-channel'
|
||||||
|
|
||||||
ActionDropdownComponent,
|
ActionDropdownComponent,
|
||||||
ButtonComponent,
|
ButtonComponent,
|
||||||
|
CopyButtonComponent,
|
||||||
DeleteButtonComponent,
|
DeleteButtonComponent,
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { generateRunnerJobToken } from '@server/helpers/token-generator'
|
import { generateRunnerJobToken } from '@server/helpers/token-generator'
|
||||||
import { MIMETYPES } from '@server/initializers/constants'
|
import { MIMETYPES } from '@server/initializers/constants'
|
||||||
import { sequelizeTypescript } from '@server/initializers/database'
|
import { sequelizeTypescript } from '@server/initializers/database'
|
||||||
import { getRunnerJobHandlerClass, updateLastRunnerContact } from '@server/lib/runners'
|
import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners'
|
||||||
import {
|
import {
|
||||||
apiRateLimiter,
|
apiRateLimiter,
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
errorRunnerJobValidator,
|
errorRunnerJobValidator,
|
||||||
getRunnerFromTokenValidator,
|
getRunnerFromTokenValidator,
|
||||||
jobOfRunnerGetValidatorFactory,
|
jobOfRunnerGetValidatorFactory,
|
||||||
|
listRunnerJobsValidator,
|
||||||
runnerJobGetValidator,
|
runnerJobGetValidator,
|
||||||
successRunnerJobValidator,
|
successRunnerJobValidator,
|
||||||
updateRunnerJobValidator
|
updateRunnerJobValidator
|
||||||
|
@ -131,9 +132,17 @@ runnerJobsRouter.get('/jobs',
|
||||||
runnerJobsSortValidator,
|
runnerJobsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
|
listRunnerJobsValidator,
|
||||||
asyncMiddleware(listRunnerJobs)
|
asyncMiddleware(listRunnerJobs)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
runnerJobsRouter.delete('/jobs/:jobUUID',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_RUNNERS),
|
||||||
|
asyncMiddleware(runnerJobGetValidator),
|
||||||
|
asyncMiddleware(deleteRunnerJob)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -374,6 +383,21 @@ async function cancelRunnerJob (req: express.Request, res: express.Response) {
|
||||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteRunnerJob (req: express.Request, res: express.Response) {
|
||||||
|
const runnerJob = res.locals.runnerJob
|
||||||
|
|
||||||
|
logger.info('Deleting job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type))
|
||||||
|
|
||||||
|
if (runnerJobCanBeCancelled(runnerJob)) {
|
||||||
|
const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob)
|
||||||
|
await new RunnerJobHandler().cancel({ runnerJob })
|
||||||
|
}
|
||||||
|
|
||||||
|
await runnerJob.destroy()
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
async function listRunnerJobs (req: express.Request, res: express.Response) {
|
async function listRunnerJobs (req: express.Request, res: express.Response) {
|
||||||
const query: ListRunnerJobsQuery = req.query
|
const query: ListRunnerJobsQuery = req.query
|
||||||
|
|
||||||
|
@ -381,7 +405,8 @@ async function listRunnerJobs (req: express.Request, res: express.Response) {
|
||||||
start: query.start,
|
start: query.start,
|
||||||
count: query.count,
|
count: query.count,
|
||||||
sort: query.sort,
|
sort: query.sort,
|
||||||
search: query.search
|
search: query.search,
|
||||||
|
stateOneOf: query.stateOneOf
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { UploadFilesForCheck } from 'express'
|
import { UploadFilesForCheck } from 'express'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants'
|
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
|
||||||
import {
|
import {
|
||||||
LiveRTMPHLSTranscodingSuccess,
|
LiveRTMPHLSTranscodingSuccess,
|
||||||
RunnerJobSuccessPayload,
|
RunnerJobSuccessPayload,
|
||||||
|
@ -11,7 +11,7 @@ import {
|
||||||
VODHLSTranscodingSuccess,
|
VODHLSTranscodingSuccess,
|
||||||
VODWebVideoTranscodingSuccess
|
VODWebVideoTranscodingSuccess
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { exists, isFileValid, isSafeFilename } from '../misc'
|
import { exists, isArray, isFileValid, isSafeFilename } from '../misc'
|
||||||
|
|
||||||
const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
|
const RUNNER_JOBS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.RUNNER_JOBS
|
||||||
|
|
||||||
|
@ -56,6 +56,14 @@ function isRunnerJobErrorMessageValid (value: string) {
|
||||||
return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
|
return validator.isLength(value, RUNNER_JOBS_CONSTRAINTS_FIELDS.ERROR_MESSAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRunnerJobStateValid (value: any) {
|
||||||
|
return exists(value) && RUNNER_JOB_STATES[value] !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunnerJobArrayOfStateValid (value: any) {
|
||||||
|
return isArray(value) && value.every(v => isRunnerJobStateValid(v))
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -65,7 +73,9 @@ export {
|
||||||
isRunnerJobTokenValid,
|
isRunnerJobTokenValid,
|
||||||
isRunnerJobErrorMessageValid,
|
isRunnerJobErrorMessageValid,
|
||||||
isRunnerJobProgressValid,
|
isRunnerJobProgressValid,
|
||||||
isRunnerJobAbortReasonValid
|
isRunnerJobAbortReasonValid,
|
||||||
|
isRunnerJobArrayOfStateValid,
|
||||||
|
isRunnerJobStateValid
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -2,8 +2,9 @@ import express from 'express'
|
||||||
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { sequelizeTypescript } from '@server/initializers/database'
|
import { sequelizeTypescript } from '@server/initializers/database'
|
||||||
import { MRunner } from '@server/types/models/runners'
|
import { MRunner, MRunnerJob } from '@server/types/models/runners'
|
||||||
import { RUNNER_JOBS } from '@server/initializers/constants'
|
import { RUNNER_JOBS } from '@server/initializers/constants'
|
||||||
|
import { RunnerJobState } from '@shared/models'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('runner')
|
const lTags = loggerTagsFactory('runner')
|
||||||
|
|
||||||
|
@ -32,6 +33,17 @@ function updateLastRunnerContact (req: express.Request, runner: MRunner) {
|
||||||
.finally(() => updatingRunner.delete(runner.id))
|
.finally(() => updatingRunner.delete(runner.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
function runnerJobCanBeCancelled (runnerJob: MRunnerJob) {
|
||||||
updateLastRunnerContact
|
const allowedStates = new Set<RunnerJobState>([
|
||||||
|
RunnerJobState.PENDING,
|
||||||
|
RunnerJobState.PROCESSING,
|
||||||
|
RunnerJobState.WAITING_FOR_PARENT_JOB
|
||||||
|
])
|
||||||
|
|
||||||
|
return allowedStates.has(runnerJob.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
updateLastRunnerContact,
|
||||||
|
runnerJobCanBeCancelled
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body, param } from 'express-validator'
|
import { body, param, query } from 'express-validator'
|
||||||
import { isUUIDValid } from '@server/helpers/custom-validators/misc'
|
import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
isRunnerJobAbortReasonValid,
|
isRunnerJobAbortReasonValid,
|
||||||
|
isRunnerJobArrayOfStateValid,
|
||||||
isRunnerJobErrorMessageValid,
|
isRunnerJobErrorMessageValid,
|
||||||
isRunnerJobProgressValid,
|
isRunnerJobProgressValid,
|
||||||
isRunnerJobSuccessPayloadValid,
|
isRunnerJobSuccessPayloadValid,
|
||||||
|
@ -12,7 +13,9 @@ import {
|
||||||
import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners'
|
import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners'
|
||||||
import { cleanUpReqFiles } from '@server/helpers/express-utils'
|
import { cleanUpReqFiles } from '@server/helpers/express-utils'
|
||||||
import { LiveManager } from '@server/lib/live'
|
import { LiveManager } from '@server/lib/live'
|
||||||
|
import { runnerJobCanBeCancelled } from '@server/lib/runners'
|
||||||
import { RunnerJobModel } from '@server/models/runner/runner-job'
|
import { RunnerJobModel } from '@server/models/runner/runner-job'
|
||||||
|
import { arrayify } from '@shared/core-utils'
|
||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
|
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
|
||||||
|
@ -119,13 +122,7 @@ export const cancelRunnerJobValidator = [
|
||||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
const runnerJob = res.locals.runnerJob
|
const runnerJob = res.locals.runnerJob
|
||||||
|
|
||||||
const allowedStates = new Set<RunnerJobState>([
|
if (runnerJobCanBeCancelled(runnerJob) !== true) {
|
||||||
RunnerJobState.PENDING,
|
|
||||||
RunnerJobState.PROCESSING,
|
|
||||||
RunnerJobState.WAITING_FOR_PARENT_JOB
|
|
||||||
])
|
|
||||||
|
|
||||||
if (allowedStates.has(runnerJob.state) !== true) {
|
|
||||||
return res.fail({
|
return res.fail({
|
||||||
status: HttpStatusCode.BAD_REQUEST_400,
|
status: HttpStatusCode.BAD_REQUEST_400,
|
||||||
message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
|
message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
|
||||||
|
@ -137,6 +134,21 @@ export const cancelRunnerJobValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const listRunnerJobsValidator = [
|
||||||
|
query('search')
|
||||||
|
.optional()
|
||||||
|
.custom(exists),
|
||||||
|
|
||||||
|
query('stateOneOf')
|
||||||
|
.optional()
|
||||||
|
.customSanitizer(arrayify)
|
||||||
|
.custom(isRunnerJobArrayOfStateValid),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export const runnerJobGetValidator = [
|
export const runnerJobGetValidator = [
|
||||||
param('jobUUID').custom(isUUIDValid),
|
param('jobUUID').custom(isUUIDValid),
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FindOptions, Op, Transaction } from 'sequelize'
|
import { Op, Transaction } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BelongsTo,
|
BelongsTo,
|
||||||
|
@ -13,7 +13,7 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { isUUIDValid } from '@server/helpers/custom-validators/misc'
|
import { isArray, isUUIDValid } from '@server/helpers/custom-validators/misc'
|
||||||
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
|
import { CONSTRAINTS_FIELDS, RUNNER_JOB_STATES } from '@server/initializers/constants'
|
||||||
import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners'
|
import { MRunnerJob, MRunnerJobRunner, MRunnerJobRunnerParent } from '@server/types/models/runners'
|
||||||
import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models'
|
import { RunnerJob, RunnerJobAdmin, RunnerJobPayload, RunnerJobPrivatePayload, RunnerJobState, RunnerJobType } from '@shared/models'
|
||||||
|
@ -227,28 +227,38 @@ export class RunnerJobModel extends Model<Partial<AttributesOnly<RunnerJobModel>
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
search?: string
|
search?: string
|
||||||
|
stateOneOf?: RunnerJobState[]
|
||||||
}) {
|
}) {
|
||||||
const { start, count, sort, search } = options
|
const { start, count, sort, search, stateOneOf } = options
|
||||||
|
|
||||||
const query: FindOptions = {
|
const query = {
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getSort(sort)
|
order: getSort(sort),
|
||||||
|
where: []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
if (isUUIDValid(search)) {
|
if (isUUIDValid(search)) {
|
||||||
query.where = { uuid: search }
|
query.where.push({ uuid: search })
|
||||||
} else {
|
} else {
|
||||||
query.where = {
|
query.where.push({
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
searchAttribute(search, 'type'),
|
searchAttribute(search, 'type'),
|
||||||
searchAttribute(search, '$Runner.name$')
|
searchAttribute(search, '$Runner.name$')
|
||||||
]
|
]
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isArray(stateOneOf) && stateOneOf.length !== 0) {
|
||||||
|
query.where.push({
|
||||||
|
state: {
|
||||||
|
[Op.in]: stateOneOf
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query),
|
RunnerJobModel.scope([ ScopeNames.WITH_RUNNER ]).count(query),
|
||||||
RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query)
|
RunnerJobModel.scope([ ScopeNames.WITH_RUNNER, ScopeNames.WITH_PARENT ]).findAll<MRunnerJobRunnerParent>(query)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { basename } from 'path'
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
import { basename } from 'path'
|
||||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
||||||
import {
|
import {
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
isVideoStudioTaskIntro,
|
isVideoStudioTaskIntro,
|
||||||
RunnerJob,
|
RunnerJob,
|
||||||
RunnerJobState,
|
RunnerJobState,
|
||||||
|
RunnerJobStudioTranscodingPayload,
|
||||||
RunnerJobSuccessPayload,
|
RunnerJobSuccessPayload,
|
||||||
RunnerJobUpdatePayload,
|
RunnerJobUpdatePayload,
|
||||||
RunnerJobStudioTranscodingPayload,
|
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoStudioTaskIntro
|
VideoStudioTaskIntro
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
|
@ -236,6 +236,10 @@ describe('Test managing runners', function () {
|
||||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid state', async function () {
|
||||||
|
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed to list with the correct params', async function () {
|
it('Should succeed to list with the correct params', async function () {
|
||||||
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
|
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
|
||||||
})
|
})
|
||||||
|
@ -307,8 +311,48 @@ describe('Test managing runners', function () {
|
||||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed to list with the correct params', async function () {
|
it('Should fail with an invalid state', async function () {
|
||||||
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt' })
|
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any })
|
||||||
|
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Delete', function () {
|
||||||
|
let jobUUID: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await server.videos.quickUpload({ name: 'video' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||||
|
jobUUID = availableJobs[0].uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without oauth token', async function () {
|
||||||
|
await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without admin rights', async function () {
|
||||||
|
await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad job uuid', async function () {
|
||||||
|
await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown job uuid', async function () {
|
||||||
|
const jobUUID = badUUID
|
||||||
|
await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.runnerJobs.deleteByAdmin({ jobUUID })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -339,6 +339,30 @@ describe('Test runner common actions', function () {
|
||||||
|
|
||||||
expect(data).to.not.have.lengthOf(0)
|
expect(data).to.not.have.lengthOf(0)
|
||||||
expect(total).to.not.equal(0)
|
expect(total).to.not.equal(0)
|
||||||
|
|
||||||
|
for (const job of data) {
|
||||||
|
expect(job.type).to.include('hls')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should filter jobs', async function () {
|
||||||
|
{
|
||||||
|
const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
|
||||||
|
|
||||||
|
expect(data).to.not.have.lengthOf(0)
|
||||||
|
expect(total).to.not.equal(0)
|
||||||
|
|
||||||
|
for (const job of data) {
|
||||||
|
expect(job.state.label).to.equal('Waiting for parent job to finish')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
|
||||||
|
|
||||||
|
expect(data).to.have.lengthOf(0)
|
||||||
|
expect(total).to.equal(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -598,6 +622,33 @@ describe('Test runner common actions', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Remove', function () {
|
||||||
|
|
||||||
|
it('Should remove a pending job', async function () {
|
||||||
|
await server.videos.quickUpload({ name: 'video' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
{
|
||||||
|
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||||
|
|
||||||
|
const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
|
||||||
|
jobUUID = pendingJob.uuid
|
||||||
|
|
||||||
|
await server.runnerJobs.deleteByAdmin({ jobUUID })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||||
|
|
||||||
|
const parent = data.find(j => j.uuid === jobUUID)
|
||||||
|
expect(parent).to.not.exist
|
||||||
|
|
||||||
|
const children = data.filter(j => j.parent?.uuid === jobUUID)
|
||||||
|
expect(children).to.have.lengthOf(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Stalled jobs', function () {
|
describe('Stalled jobs', function () {
|
||||||
|
|
||||||
it('Should abort stalled jobs', async function () {
|
it('Should abort stalled jobs', async function () {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { RunnerJobState } from './runner-job-state.model'
|
||||||
|
|
||||||
export interface ListRunnerJobsQuery {
|
export interface ListRunnerJobsQuery {
|
||||||
start?: number
|
start?: number
|
||||||
count?: number
|
count?: number
|
||||||
sort?: string
|
sort?: string
|
||||||
search?: string
|
search?: string
|
||||||
|
stateOneOf?: RunnerJobState[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
isHLSTranscodingPayloadSuccess,
|
isHLSTranscodingPayloadSuccess,
|
||||||
isLiveRTMPHLSTranscodingUpdatePayload,
|
isLiveRTMPHLSTranscodingUpdatePayload,
|
||||||
isWebVideoOrAudioMergeTranscodingPayloadSuccess,
|
isWebVideoOrAudioMergeTranscodingPayloadSuccess,
|
||||||
|
ListRunnerJobsQuery,
|
||||||
RequestRunnerJobBody,
|
RequestRunnerJobBody,
|
||||||
RequestRunnerJobResult,
|
RequestRunnerJobResult,
|
||||||
ResultList,
|
ResultList,
|
||||||
|
@ -27,19 +28,14 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||||
|
|
||||||
export class RunnerJobsCommand extends AbstractCommand {
|
export class RunnerJobsCommand extends AbstractCommand {
|
||||||
|
|
||||||
list (options: OverrideCommandOptions & {
|
list (options: OverrideCommandOptions & ListRunnerJobsQuery = {}) {
|
||||||
start?: number
|
|
||||||
count?: number
|
|
||||||
sort?: string
|
|
||||||
search?: string
|
|
||||||
} = {}) {
|
|
||||||
const path = '/api/v1/runners/jobs'
|
const path = '/api/v1/runners/jobs'
|
||||||
|
|
||||||
return this.getRequestBody<ResultList<RunnerJobAdmin>>({
|
return this.getRequestBody<ResultList<RunnerJobAdmin>>({
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
query: pick(options, [ 'start', 'count', 'sort', 'search' ]),
|
query: pick(options, [ 'start', 'count', 'sort', 'search', 'stateOneOf' ]),
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
@ -57,6 +53,18 @@ export class RunnerJobsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteByAdmin (options: OverrideCommandOptions & { jobUUID: string }) {
|
||||||
|
const path = '/api/v1/runners/jobs/' + options.jobUUID
|
||||||
|
|
||||||
|
return this.deleteRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
request (options: OverrideCommandOptions & RequestRunnerJobBody) {
|
request (options: OverrideCommandOptions & RequestRunnerJobBody) {
|
||||||
|
|
|
@ -6088,6 +6088,21 @@ paths:
|
||||||
'204':
|
'204':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
||||||
|
/api/v1/runners/jobs/{jobUUID}:
|
||||||
|
delete:
|
||||||
|
summary: Delete a job
|
||||||
|
description: The endpoint will first cancel the job if needed, and then remove it from the database. Children jobs will also be removed
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- admin
|
||||||
|
tags:
|
||||||
|
- Runner Jobs
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/jobUUID'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: successful operation
|
||||||
|
|
||||||
/api/v1/runners/jobs:
|
/api/v1/runners/jobs:
|
||||||
get:
|
get:
|
||||||
summary: List jobs
|
summary: List jobs
|
||||||
|
@ -6101,6 +6116,13 @@ paths:
|
||||||
- $ref: '#/components/parameters/count'
|
- $ref: '#/components/parameters/count'
|
||||||
- $ref: '#/components/parameters/runnerJobSort'
|
- $ref: '#/components/parameters/runnerJobSort'
|
||||||
- $ref: '#/components/parameters/search'
|
- $ref: '#/components/parameters/search'
|
||||||
|
- name: stateOneOf
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RunnerJobState'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
Loading…
Reference in a new issue