mirror of https://github.com/Chocobozzz/PeerTube
Implement runner in client side
parent
e592df48c7
commit
118626c875
|
@ -1,5 +1,5 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { AuthService, ScreenService } from '@app/core'
|
import { AuthService, ScreenService, ServerService } from '@app/core'
|
||||||
import { ListOverflowItem } from '@app/shared/shared-main'
|
import { ListOverflowItem } from '@app/shared/shared-main'
|
||||||
import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
|
import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
|
||||||
import { UserRight } from '@shared/models'
|
import { UserRight } from '@shared/models'
|
||||||
|
@ -14,7 +14,8 @@ export class AdminComponent implements OnInit {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private screen: ScreenService
|
private screen: ScreenService,
|
||||||
|
private server: ServerService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
get isBroadcastMessageDisplayed () {
|
get isBroadcastMessageDisplayed () {
|
||||||
|
@ -22,6 +23,14 @@ export class AdminComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
this.server.configReloaded.subscribe(() => this.buildMenu())
|
||||||
|
|
||||||
|
this.buildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildMenu () {
|
||||||
|
this.menuEntries = []
|
||||||
|
|
||||||
this.buildOverviewItems()
|
this.buildOverviewItems()
|
||||||
this.buildFederationItems()
|
this.buildFederationItems()
|
||||||
this.buildModerationItems()
|
this.buildModerationItems()
|
||||||
|
@ -157,9 +166,23 @@ export class AdminComponent implements OnInit {
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isRemoteRunnersEnabled() && this.hasRunnersRight()) {
|
||||||
|
systemItems.children.push({
|
||||||
|
label: $localize`Remote runners`,
|
||||||
|
iconName: 'codesandbox',
|
||||||
|
routerLink: '/admin/system/runners/runners-list'
|
||||||
|
})
|
||||||
|
|
||||||
|
systemItems.children.push({
|
||||||
|
label: $localize`Runner jobs`,
|
||||||
|
iconName: 'globe',
|
||||||
|
routerLink: '/admin/system/runners/jobs-list'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasJobsRight()) {
|
if (this.hasJobsRight()) {
|
||||||
systemItems.children.push({
|
systemItems.children.push({
|
||||||
label: $localize`Jobs`,
|
label: $localize`Local jobs`,
|
||||||
iconName: 'circle-tick',
|
iconName: 'circle-tick',
|
||||||
routerLink: '/admin/system/jobs'
|
routerLink: '/admin/system/jobs'
|
||||||
})
|
})
|
||||||
|
@ -226,6 +249,10 @@ export class AdminComponent implements OnInit {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasRunnersRight () {
|
||||||
|
return this.auth.getUser().hasRight(UserRight.MANAGE_RUNNERS)
|
||||||
|
}
|
||||||
|
|
||||||
private hasDebugRight () {
|
private hasDebugRight () {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_DEBUG)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_DEBUG)
|
||||||
}
|
}
|
||||||
|
@ -241,4 +268,10 @@ export class AdminComponent implements OnInit {
|
||||||
private hasRegistrationsRight () {
|
private hasRegistrationsRight () {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isRemoteRunnersEnabled () {
|
||||||
|
const config = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
return config.transcoding.remoteRunners.enabled || config.live.transcoding.remoteRunners.enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,17 @@ import {
|
||||||
PluginShowInstalledComponent
|
PluginShowInstalledComponent
|
||||||
} from './plugins'
|
} from './plugins'
|
||||||
import { SharedAdminModule } from './shared'
|
import { SharedAdminModule } from './shared'
|
||||||
import { JobService, LogsComponent, LogsService } from './system'
|
import {
|
||||||
|
JobService,
|
||||||
|
LogsComponent,
|
||||||
|
LogsService,
|
||||||
|
RunnerJobListComponent,
|
||||||
|
RunnerListComponent,
|
||||||
|
RunnerRegistrationTokenListComponent,
|
||||||
|
RunnerService
|
||||||
|
} from './system'
|
||||||
import { DebugComponent, DebugService } from './system/debug'
|
import { DebugComponent, DebugService } from './system/debug'
|
||||||
import { JobsComponent } from './system/jobs/jobs.component'
|
import { JobsComponent } from './system/jobs'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -125,7 +133,11 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
EditHomepageComponent,
|
EditHomepageComponent,
|
||||||
|
|
||||||
RegistrationListComponent,
|
RegistrationListComponent,
|
||||||
ProcessRegistrationModalComponent
|
ProcessRegistrationModalComponent,
|
||||||
|
|
||||||
|
RunnerRegistrationTokenListComponent,
|
||||||
|
RunnerListComponent,
|
||||||
|
RunnerJobListComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -140,7 +152,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
PluginApiService,
|
PluginApiService,
|
||||||
EditConfigurationService,
|
EditConfigurationService,
|
||||||
VideoAdminService,
|
VideoAdminService,
|
||||||
AdminRegistrationService
|
AdminRegistrationService,
|
||||||
|
RunnerService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule { }
|
export class AdminModule { }
|
||||||
|
|
|
@ -190,6 +190,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
},
|
},
|
||||||
webtorrent: {
|
webtorrent: {
|
||||||
enabled: null
|
enabled: null
|
||||||
|
},
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
live: {
|
live: {
|
||||||
|
@ -208,7 +211,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||||
profile: null,
|
profile: null,
|
||||||
resolutions: {},
|
resolutions: {},
|
||||||
alwaysTranscodeOriginalResolution: null
|
alwaysTranscodeOriginalResolution: null,
|
||||||
|
remoteRunners: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStudio: {
|
videoStudio: {
|
||||||
|
|
|
@ -110,6 +110,20 @@
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getDisabledLiveTranscodingClass()">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="transcodingRemoteRunnersEnabled" formControlName="enabled"
|
||||||
|
i18n-labelText labelText="Enable remote runners"
|
||||||
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<span i18n>
|
||||||
|
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process live transcoding.
|
||||||
|
Remote runners has to register on your instance first.
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
|
<div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
|
||||||
<label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
|
<label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,20 @@
|
||||||
|
|
||||||
<ng-container ngProjectAs="extra">
|
<ng-container ngProjectAs="extra">
|
||||||
|
|
||||||
|
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getTranscodingDisabledClass()">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="transcodingRemoteRunnersEnabled" formControlName="enabled"
|
||||||
|
i18n-labelText labelText="Enable remote runners"
|
||||||
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<span i18n>
|
||||||
|
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process VOD transcoding.
|
||||||
|
Remote runners has to register on your instance first.
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="callout callout-light pt-2 pb-0">
|
<div class="callout callout-light pt-2 pb-0">
|
||||||
<label i18n>Input formats</label>
|
<label i18n>Input formats</label>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<p-table
|
<p-table
|
||||||
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './debug'
|
export * from './debug'
|
||||||
export * from './jobs'
|
export * from './jobs'
|
||||||
export * from './logs'
|
export * from './logs'
|
||||||
|
export * from './runners'
|
||||||
export * from './system.routes'
|
export * from './system.routes'
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './runner.service'
|
||||||
|
export * from './runner-job-list'
|
||||||
|
export * from './runner-list'
|
||||||
|
export * from './runner-registration-token-list'
|
||||||
|
export * from './runners.routes'
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './runner-job-list.component'
|
|
@ -0,0 +1,101 @@
|
||||||
|
<h1 class="d-flex justify-content-between">
|
||||||
|
<span class="text-nowrap me-2">
|
||||||
|
<my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
|
||||||
|
<span i18n>Runner jobs</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
|
||||||
|
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Remote runners</ng-container>
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p-table
|
||||||
|
[value]="runnerJobs" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
|
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} runner jobs"
|
||||||
|
[expandedRowKeys]="expandedRows" dataKey="uuid"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px"></th>
|
||||||
|
<th style="width: 120px;"></th>
|
||||||
|
<th i18n>UUID</th>
|
||||||
|
<th i18n>Type</th>
|
||||||
|
<th i18n pSortableColumn="state">State <p-sortIcon field="state"></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 i18n>Runner</th>
|
||||||
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="caption">
|
||||||
|
<div class="caption">
|
||||||
|
<div class="ms-auto d-flex">
|
||||||
|
<my-advanced-input-filter class="me-2" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||||
|
|
||||||
|
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-expanded="expanded" let-runnerJob>
|
||||||
|
<tr>
|
||||||
|
<td class="expand-cell" [pRowToggler]="runnerJob">
|
||||||
|
<my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="action-cell">
|
||||||
|
<my-action-dropdown
|
||||||
|
placement="bottom-right top-right left auto" container="body"
|
||||||
|
i18n-label label="Actions" [actions]="actions" [entry]="runnerJob"
|
||||||
|
></my-action-dropdown>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ runnerJob.uuid }}</td>
|
||||||
|
<td>{{ runnerJob.type }}</td>
|
||||||
|
<td>{{ runnerJob.state.label }}</td>
|
||||||
|
<td>{{ runnerJob.priority }}</td>
|
||||||
|
<td>{{ runnerJob.progress }}</td>
|
||||||
|
<td>{{ runnerJob.runner?.name }}</td>
|
||||||
|
<td>{{ runnerJob.createdAt | date: 'short' }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="rowexpansion" let-runnerJob>
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="mt-2 fs-7 font-monospace">
|
||||||
|
Parent job: {{ runnerJob.parent?.uuid || '-' }} <br />
|
||||||
|
Processed on {{ (runnerJob.startedAt || '-') }} <br />
|
||||||
|
Finished on {{ (runnerJob.finishedAt || '-') }} <br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong i18n>Payload:</strong>
|
||||||
|
<pre>{{ runnerJob.payload }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong i18n>Private payload:</strong>
|
||||||
|
<pre>{{ runnerJob.privatePayload }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre *ngIf="runnerJob.error" class=".text-danger" >{{ runnerJob.error }}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container i18n>No runner jobs found.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
|
import { RunnerJob } from '@shared/models'
|
||||||
|
import { RunnerJobFormatted, RunnerService } from '../runner.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-runner-job-list',
|
||||||
|
templateUrl: './runner-job-list.component.html'
|
||||||
|
})
|
||||||
|
export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnInit {
|
||||||
|
runnerJobs: RunnerJobFormatted[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
actions: DropdownAction<RunnerJob>[][] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private runnerService: RunnerService,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private confirmService: ConfirmService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.actions = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $localize`Cancel this job`,
|
||||||
|
handler: job => this.cancelJob(job)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentifier () {
|
||||||
|
return 'RunnerJobListComponent'
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelJob (job: RunnerJob) {
|
||||||
|
const res = await this.confirmService.confirm(
|
||||||
|
$localize`Do you really want to cancel this job? Children won't be processed.`,
|
||||||
|
$localize`Cancel job`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.runnerService.cancelJob(job)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reloadData()
|
||||||
|
this.notifier.success($localize`Job cancelled.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected reloadDataInternal () {
|
||||||
|
this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
|
||||||
|
.subscribe({
|
||||||
|
next: resultList => {
|
||||||
|
this.runnerJobs = resultList.data
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './runner-list.component'
|
|
@ -0,0 +1,61 @@
|
||||||
|
<h1 class="d-flex justify-content-between">
|
||||||
|
<span class="text-nowrap me-2">
|
||||||
|
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Remote runners</ng-container>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a routerLink="/admin/system/runners/registration-tokens-list" class="peertube-button-link peertube-button-icon grey-button">
|
||||||
|
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Runner registration tokens</ng-container>
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p-table
|
||||||
|
[value]="runners" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
|
[lazy]="true" (onLazyLoad)="loadLazy($event)"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} remote runners"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 120px;"></th>
|
||||||
|
<th i18n>Name</th>
|
||||||
|
<th i18n>Description</th>
|
||||||
|
<th i18n>IP</th>
|
||||||
|
<th i18n>Last contact</th>
|
||||||
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-runner>
|
||||||
|
<tr>
|
||||||
|
<td class="action-cell">
|
||||||
|
<my-action-dropdown
|
||||||
|
placement="bottom-right top-right left auto" container="body"
|
||||||
|
i18n-label label="Actions" [actions]="actions" [entry]="runner"
|
||||||
|
></my-action-dropdown>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ runner.name }}</td>
|
||||||
|
|
||||||
|
<td>{{ runner.description }}</td>
|
||||||
|
|
||||||
|
<td>{{ runner.ip }}</td>
|
||||||
|
|
||||||
|
<td>{{ runner.lastContact | date: 'short' }}</td>
|
||||||
|
|
||||||
|
<td>{{ runner.createdAt | date: 'short' }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container i18n>No remote runners found.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
|
import { Runner } from '@shared/models'
|
||||||
|
import { RunnerService } from '../runner.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-runner-list',
|
||||||
|
templateUrl: './runner-list.component.html'
|
||||||
|
})
|
||||||
|
export class RunnerListComponent extends RestTable <Runner> implements OnInit {
|
||||||
|
runners: Runner[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
actions: DropdownAction<Runner>[][] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private runnerService: RunnerService,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private confirmService: ConfirmService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.actions = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $localize`Remove`,
|
||||||
|
handler: runer => this.deleteRunner(runer)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentifier () {
|
||||||
|
return 'RunnerListComponent'
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRunner (runner: Runner) {
|
||||||
|
const res = await this.confirmService.confirm(
|
||||||
|
$localize`Do you really want to delete this runner? It won't be able to process jobs anymore.`,
|
||||||
|
$localize`Remove ${runner.name}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.runnerService.deleteRunner(runner)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reloadData()
|
||||||
|
this.notifier.success($localize`Runner removed.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected reloadDataInternal () {
|
||||||
|
this.runnerService.listRunners({ pagination: this.pagination, sort: this.sort })
|
||||||
|
.subscribe({
|
||||||
|
next: resultList => {
|
||||||
|
this.runners = resultList.data
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './runner-registration-token-list.component'
|
|
@ -0,0 +1,65 @@
|
||||||
|
<h1 class="d-flex justify-content-between">
|
||||||
|
<span class="text-nowrap me-2">
|
||||||
|
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Runner registration tokens</ng-container>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
|
||||||
|
<my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Remote runners</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p-table
|
||||||
|
[value]="registrationTokens" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||||
|
[lazy]="true" (onLazyLoad)="loadLazy($event)"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registration tokens"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 120px;"></th>
|
||||||
|
<th i18n>Token</th>
|
||||||
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
<th style="width: 160px;" i18n>Associated runners</th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="caption">
|
||||||
|
<div class="caption">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<my-button className="orange-button" i18n-label label="Generate token" icon="add" (click)="generateToken()"></my-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-registrationToken>
|
||||||
|
<tr>
|
||||||
|
<td class="action-cell">
|
||||||
|
<my-action-dropdown
|
||||||
|
placement="bottom-right top-right left auto" container="body"
|
||||||
|
i18n-label label="Actions" [actions]="actions" [entry]="registrationToken"
|
||||||
|
></my-action-dropdown>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ registrationToken.registrationToken }}</td>
|
||||||
|
|
||||||
|
<td>{{ registrationToken.createdAt | date: 'short' }}</td>
|
||||||
|
|
||||||
|
<td>{{ registrationToken.registeredRunnersCount }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container i18n>No registration token found for remote runners.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
|
import { DropdownAction } from '@app/shared/shared-main'
|
||||||
|
import { RunnerRegistrationToken } from '@shared/models'
|
||||||
|
import { RunnerService } from '../runner.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-runner-registration-token-list',
|
||||||
|
templateUrl: './runner-registration-token-list.component.html'
|
||||||
|
})
|
||||||
|
export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
|
||||||
|
registrationTokens: RunnerRegistrationToken[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
actions: DropdownAction<RunnerRegistrationToken>[][] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private runnerService: RunnerService,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private confirmService: ConfirmService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.actions = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: $localize`Remove this token`,
|
||||||
|
handler: token => this.removeToken(token)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentifier () {
|
||||||
|
return 'RunnerRegistrationTokenListComponent'
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken () {
|
||||||
|
this.runnerService.generateToken()
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reloadData()
|
||||||
|
this.notifier.success($localize`Registration token generated.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeToken (token: RunnerRegistrationToken) {
|
||||||
|
const res = await this.confirmService.confirm(
|
||||||
|
$localize`Do you really want to remove this registration token? All associated runners will also be removed.`,
|
||||||
|
$localize`Remove registration token`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.runnerService.removeToken(token)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reloadData()
|
||||||
|
this.notifier.success($localize`Registration token removed.`)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected reloadDataInternal () {
|
||||||
|
this.runnerService.listRegistrationTokens({ pagination: this.pagination, sort: this.sort })
|
||||||
|
.subscribe({
|
||||||
|
next: resultList => {
|
||||||
|
this.registrationTokens = resultList.data
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { catchError, forkJoin, map } from 'rxjs'
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
|
||||||
|
import { peertubeTranslate } from '@shared/core-utils'
|
||||||
|
import { ResultList } from '@shared/models/common'
|
||||||
|
import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners'
|
||||||
|
import { environment } from '../../../../environments/environment'
|
||||||
|
|
||||||
|
export type RunnerJobFormatted = RunnerJob & {
|
||||||
|
payload: string
|
||||||
|
privatePayload: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RunnerService {
|
||||||
|
private static BASE_RUNNER_URL = environment.apiUrl + '/api/v1/runners'
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authHttp: HttpClient,
|
||||||
|
private server: ServerService,
|
||||||
|
private restService: RestService,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) {}
|
||||||
|
|
||||||
|
listRegistrationTokens (options: {
|
||||||
|
pagination: RestPagination
|
||||||
|
sort: SortMeta
|
||||||
|
}) {
|
||||||
|
const { pagination, sort } = options
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
return this.authHttp.get<ResultList<RunnerRegistrationToken>>(RunnerService.BASE_RUNNER_URL + '/registration-tokens', { params })
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken () {
|
||||||
|
return this.authHttp.post(RunnerService.BASE_RUNNER_URL + '/registration-tokens/generate', {})
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToken (token: RunnerRegistrationToken) {
|
||||||
|
return this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/registration-tokens/' + token.id)
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listRunnerJobs (options: {
|
||||||
|
pagination: RestPagination
|
||||||
|
sort: SortMeta
|
||||||
|
search?: string
|
||||||
|
}) {
|
||||||
|
const { pagination, sort, search } = options
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
|
return forkJoin([
|
||||||
|
this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
|
||||||
|
this.server.getServerLocale()
|
||||||
|
]).pipe(
|
||||||
|
map(([ res, translations ]) => {
|
||||||
|
const newData = res.data.map(job => {
|
||||||
|
return {
|
||||||
|
...job,
|
||||||
|
|
||||||
|
state: {
|
||||||
|
id: job.state.id,
|
||||||
|
label: peertubeTranslate(job.state.label, translations)
|
||||||
|
},
|
||||||
|
payload: JSON.stringify(job.payload, null, 2),
|
||||||
|
privatePayload: JSON.stringify(job.privatePayload, null, 2)
|
||||||
|
} as RunnerJobFormatted
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: res.total,
|
||||||
|
data: newData
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map(res => this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'startedAt', 'finishedAt' ], 'precise')),
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelJob (job: RunnerJob) {
|
||||||
|
return this.authHttp.post(RunnerService.BASE_RUNNER_URL + '/jobs/' + job.uuid + '/cancel', {})
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listRunners (options: {
|
||||||
|
pagination: RestPagination
|
||||||
|
sort: SortMeta
|
||||||
|
}) {
|
||||||
|
const { pagination, sort } = options
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
return this.authHttp.get<ResultList<Runner>>(RunnerService.BASE_RUNNER_URL + '/', { params })
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRunner (runner: Runner) {
|
||||||
|
return this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/' + runner.id)
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Routes } from '@angular/router'
|
||||||
|
import { UserRightGuard } from '@app/core'
|
||||||
|
import { UserRight } from '@shared/models'
|
||||||
|
import { RunnerJobListComponent } from './runner-job-list'
|
||||||
|
import { RunnerListComponent } from './runner-list'
|
||||||
|
import { RunnerRegistrationTokenListComponent } from './runner-registration-token-list'
|
||||||
|
|
||||||
|
export const RunnersRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'runners',
|
||||||
|
canActivate: [ UserRightGuard ],
|
||||||
|
data: {
|
||||||
|
userRight: UserRight.MANAGE_RUNNERS
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'jobs-list',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'jobs-list',
|
||||||
|
component: RunnerJobListComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`List runner jobs`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'runners-list',
|
||||||
|
component: RunnerListComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`List remote runners`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'registration-tokens-list',
|
||||||
|
component: RunnerRegistrationTokenListComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`List registration runner tokens`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -4,6 +4,7 @@ import { UserRight } from '@shared/models'
|
||||||
import { DebugComponent } from './debug'
|
import { DebugComponent } from './debug'
|
||||||
import { JobsComponent } from './jobs/jobs.component'
|
import { JobsComponent } from './jobs/jobs.component'
|
||||||
import { LogsComponent } from './logs'
|
import { LogsComponent } from './logs'
|
||||||
|
import { RunnersRoutes } from './runners'
|
||||||
|
|
||||||
export const SystemRoutes: Routes = [
|
export const SystemRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -46,7 +47,9 @@ export const SystemRoutes: Routes = [
|
||||||
title: $localize`Debug`
|
title: $localize`Debug`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
...RunnersRoutes
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -43,7 +43,9 @@ export class LiveStreamInformationComponent {
|
||||||
[LiveVideoError.BLACKLISTED]: $localize`Live blacklisted`,
|
[LiveVideoError.BLACKLISTED]: $localize`Live blacklisted`,
|
||||||
[LiveVideoError.DURATION_EXCEEDED]: $localize`Max duration exceeded`,
|
[LiveVideoError.DURATION_EXCEEDED]: $localize`Max duration exceeded`,
|
||||||
[LiveVideoError.FFMPEG_ERROR]: $localize`Server error`,
|
[LiveVideoError.FFMPEG_ERROR]: $localize`Server error`,
|
||||||
[LiveVideoError.QUOTA_EXCEEDED]: $localize`Quota exceeded`
|
[LiveVideoError.QUOTA_EXCEEDED]: $localize`Quota exceeded`,
|
||||||
|
[LiveVideoError.RUNNER_JOB_CANCEL]: $localize`Runner job cancelled`,
|
||||||
|
[LiveVideoError.RUNNER_JOB_ERROR]: $localize`Error in runner job`
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors[session.error]
|
return errors[session.error]
|
||||||
|
|
|
@ -35,3 +35,7 @@
|
||||||
.peertube-radio-container {
|
.peertube-radio-container {
|
||||||
@include peertube-radio-container;
|
@include peertube-radio-container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.peertube-button-icon {
|
||||||
|
@include button-with-icon(18px, 3px, -1px);
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,7 @@
|
||||||
.fs-5-5 {
|
.fs-5-5 {
|
||||||
@include font-size(18px);
|
@include font-size(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fs-7 {
|
||||||
|
@include font-size(14px);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue