aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/admin.component.ts39
-rw-r--r--client/src/app/+admin/admin.module.ts21
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html14
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html14
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html2
-rw-r--r--client/src/app/+admin/system/index.ts1
-rw-r--r--client/src/app/+admin/system/runners/index.ts5
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/index.ts1
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html101
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts76
-rw-r--r--client/src/app/+admin/system/runners/runner-list/index.ts1
-rw-r--r--client/src/app/+admin/system/runners/runner-list/runner-list.component.html61
-rw-r--r--client/src/app/+admin/system/runners/runner-list/runner-list.component.ts76
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/index.ts1
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html65
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts88
-rw-r--r--client/src/app/+admin/system/runners/runner.service.ts117
-rw-r--r--client/src/app/+admin/system/runners/runners.routes.ts53
-rw-r--r--client/src/app/+admin/system/system.routes.ts5
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.ts4
-rw-r--r--client/src/sass/class-helpers/_buttons.scss4
-rw-r--r--client/src/sass/class-helpers/_custom-bootstrap-helpers.scss4
23 files changed, 750 insertions, 11 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 630bfe253..d4d912c40 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { AuthService, ScreenService } from '@app/core' 2import { AuthService, ScreenService, ServerService } from '@app/core'
3import { ListOverflowItem } from '@app/shared/shared-main' 3import { ListOverflowItem } from '@app/shared/shared-main'
4import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component' 4import { TopMenuDropdownParam } from '@app/shared/shared-main/misc/top-menu-dropdown.component'
5import { UserRight } from '@shared/models' 5import { UserRight } from '@shared/models'
@@ -14,7 +14,8 @@ export class AdminComponent implements OnInit {
14 14
15 constructor ( 15 constructor (
16 private auth: AuthService, 16 private auth: AuthService,
17 private screen: ScreenService 17 private screen: ScreenService,
18 private server: ServerService
18 ) { } 19 ) { }
19 20
20 get isBroadcastMessageDisplayed () { 21 get isBroadcastMessageDisplayed () {
@@ -22,6 +23,14 @@ export class AdminComponent implements OnInit {
22 } 23 }
23 24
24 ngOnInit () { 25 ngOnInit () {
26 this.server.configReloaded.subscribe(() => this.buildMenu())
27
28 this.buildMenu()
29 }
30
31 private buildMenu () {
32 this.menuEntries = []
33
25 this.buildOverviewItems() 34 this.buildOverviewItems()
26 this.buildFederationItems() 35 this.buildFederationItems()
27 this.buildModerationItems() 36 this.buildModerationItems()
@@ -157,9 +166,23 @@ export class AdminComponent implements OnInit {
157 children: [] 166 children: []
158 } 167 }
159 168
169 if (this.isRemoteRunnersEnabled() && this.hasRunnersRight()) {
170 systemItems.children.push({
171 label: $localize`Remote runners`,
172 iconName: 'codesandbox',
173 routerLink: '/admin/system/runners/runners-list'
174 })
175
176 systemItems.children.push({
177 label: $localize`Runner jobs`,
178 iconName: 'globe',
179 routerLink: '/admin/system/runners/jobs-list'
180 })
181 }
182
160 if (this.hasJobsRight()) { 183 if (this.hasJobsRight()) {
161 systemItems.children.push({ 184 systemItems.children.push({
162 label: $localize`Jobs`, 185 label: $localize`Local jobs`,
163 iconName: 'circle-tick', 186 iconName: 'circle-tick',
164 routerLink: '/admin/system/jobs' 187 routerLink: '/admin/system/jobs'
165 }) 188 })
@@ -226,6 +249,10 @@ export class AdminComponent implements OnInit {
226 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS) 249 return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
227 } 250 }
228 251
252 private hasRunnersRight () {
253 return this.auth.getUser().hasRight(UserRight.MANAGE_RUNNERS)
254 }
255
229 private hasDebugRight () { 256 private hasDebugRight () {
230 return this.auth.getUser().hasRight(UserRight.MANAGE_DEBUG) 257 return this.auth.getUser().hasRight(UserRight.MANAGE_DEBUG)
231 } 258 }
@@ -241,4 +268,10 @@ export class AdminComponent implements OnInit {
241 private hasRegistrationsRight () { 268 private hasRegistrationsRight () {
242 return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS) 269 return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
243 } 270 }
271
272 private isRemoteRunnersEnabled () {
273 const config = this.server.getHTMLConfig()
274
275 return config.transcoding.remoteRunners.enabled || config.live.transcoding.remoteRunners.enabled
276 }
244} 277}
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 891ff4ed1..006cb025a 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -56,9 +56,17 @@ import {
56 PluginShowInstalledComponent 56 PluginShowInstalledComponent
57} from './plugins' 57} from './plugins'
58import { SharedAdminModule } from './shared' 58import { SharedAdminModule } from './shared'
59import { JobService, LogsComponent, LogsService } from './system' 59import {
60 JobService,
61 LogsComponent,
62 LogsService,
63 RunnerJobListComponent,
64 RunnerListComponent,
65 RunnerRegistrationTokenListComponent,
66 RunnerService
67} from './system'
60import { DebugComponent, DebugService } from './system/debug' 68import { DebugComponent, DebugService } from './system/debug'
61import { JobsComponent } from './system/jobs/jobs.component' 69import { JobsComponent } from './system/jobs'
62 70
63@NgModule({ 71@NgModule({
64 imports: [ 72 imports: [
@@ -125,7 +133,11 @@ import { JobsComponent } from './system/jobs/jobs.component'
125 EditHomepageComponent, 133 EditHomepageComponent,
126 134
127 RegistrationListComponent, 135 RegistrationListComponent,
128 ProcessRegistrationModalComponent 136 ProcessRegistrationModalComponent,
137
138 RunnerRegistrationTokenListComponent,
139 RunnerListComponent,
140 RunnerJobListComponent
129 ], 141 ],
130 142
131 exports: [ 143 exports: [
@@ -140,7 +152,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
140 PluginApiService, 152 PluginApiService,
141 EditConfigurationService, 153 EditConfigurationService,
142 VideoAdminService, 154 VideoAdminService,
143 AdminRegistrationService 155 AdminRegistrationService,
156 RunnerService
144 ] 157 ]
145}) 158})
146export class AdminModule { } 159export class AdminModule { }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 0526ed8f1..335aedb67 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -190,6 +190,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
190 }, 190 },
191 webtorrent: { 191 webtorrent: {
192 enabled: null 192 enabled: null
193 },
194 remoteRunners: {
195 enabled: null
193 } 196 }
194 }, 197 },
195 live: { 198 live: {
@@ -208,7 +211,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
208 threads: TRANSCODING_THREADS_VALIDATOR, 211 threads: TRANSCODING_THREADS_VALIDATOR,
209 profile: null, 212 profile: null,
210 resolutions: {}, 213 resolutions: {},
211 alwaysTranscodeOriginalResolution: null 214 alwaysTranscodeOriginalResolution: null,
215 remoteRunners: {
216 enabled: null
217 }
212 } 218 }
213 }, 219 },
214 videoStudio: { 220 videoStudio: {
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
index c90c34c80..34ce8efa6 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html
@@ -110,6 +110,20 @@
110 </my-peertube-checkbox> 110 </my-peertube-checkbox>
111 </div> 111 </div>
112 112
113 <div class="form-group" formGroupName="remoteRunners" [ngClass]="getDisabledLiveTranscodingClass()">
114 <my-peertube-checkbox
115 inputName="transcodingRemoteRunnersEnabled" formControlName="enabled"
116 i18n-labelText labelText="Enable remote runners"
117 >
118 <ng-container ngProjectAs="description">
119 <span i18n>
120 Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process live transcoding.
121 Remote runners has to register on your instance first.
122 </span>
123 </ng-container>
124 </my-peertube-checkbox>
125 </div>
126
113 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()"> 127 <div class="form-group" [ngClass]="getDisabledLiveTranscodingClass()">
114 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label> 128 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
115 129
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
index de9e7253e..c11f560dd 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html
@@ -37,6 +37,20 @@
37 37
38 <ng-container ngProjectAs="extra"> 38 <ng-container ngProjectAs="extra">
39 39
40 <div class="form-group" formGroupName="remoteRunners" [ngClass]="getTranscodingDisabledClass()">
41 <my-peertube-checkbox
42 inputName="transcodingRemoteRunnersEnabled" formControlName="enabled"
43 i18n-labelText labelText="Enable remote runners"
44 >
45 <ng-container ngProjectAs="description">
46 <span i18n>
47 Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process VOD transcoding.
48 Remote runners has to register on your instance first.
49 </span>
50 </ng-container>
51 </my-peertube-checkbox>
52 </div>
53
40 <div class="callout callout-light pt-2 pb-0"> 54 <div class="callout callout-light pt-2 pb-0">
41 <label i18n>Input formats</label> 55 <label i18n>Input formats</label>
42 56
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index eca79be71..1605190f6 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -5,7 +5,7 @@
5 5
6<p-table 6<p-table
7 [value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" 7 [value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
diff --git a/client/src/app/+admin/system/index.ts b/client/src/app/+admin/system/index.ts
index b398832cc..6d84dafa8 100644
--- a/client/src/app/+admin/system/index.ts
+++ b/client/src/app/+admin/system/index.ts
@@ -1,4 +1,5 @@
1export * from './debug' 1export * from './debug'
2export * from './jobs' 2export * from './jobs'
3export * from './logs' 3export * from './logs'
4export * from './runners'
4export * from './system.routes' 5export * from './system.routes'
diff --git a/client/src/app/+admin/system/runners/index.ts b/client/src/app/+admin/system/runners/index.ts
new file mode 100644
index 000000000..dbbf32fb5
--- /dev/null
+++ b/client/src/app/+admin/system/runners/index.ts
@@ -0,0 +1,5 @@
1export * from './runner.service'
2export * from './runner-job-list'
3export * from './runner-list'
4export * from './runner-registration-token-list'
5export * from './runners.routes'
diff --git a/client/src/app/+admin/system/runners/runner-job-list/index.ts b/client/src/app/+admin/system/runners/runner-job-list/index.ts
new file mode 100644
index 000000000..bdcf93234
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-job-list/index.ts
@@ -0,0 +1 @@
export * from './runner-job-list.component'
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
new file mode 100644
index 000000000..7858b4bca
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
@@ -0,0 +1,101 @@
1<h1 class="d-flex justify-content-between">
2 <span class="text-nowrap me-2">
3 <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
4 <span i18n>Runner jobs</span>
5 </span>
6
7 <a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
8 <my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
9 <ng-container i18n>Remote runners</ng-container>
10 </a>
11</h1>
12
13<p-table
14 [value]="runnerJobs" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
15 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
16 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
17 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
18 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} runner jobs"
19 [expandedRowKeys]="expandedRows" dataKey="uuid"
20>
21 <ng-template pTemplate="header">
22 <tr>
23 <th style="width: 40px"></th>
24 <th style="width: 120px;"></th>
25 <th i18n>UUID</th>
26 <th i18n>Type</th>
27 <th i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
28 <th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th>
29 <th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th>
30 <th i18n>Runner</th>
31 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
32 </tr>
33 </ng-template>
34
35 <ng-template pTemplate="caption">
36 <div class="caption">
37 <div class="ms-auto d-flex">
38 <my-advanced-input-filter class="me-2" (search)="onSearch($event)"></my-advanced-input-filter>
39
40 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
41 </div>
42 </div>
43 </ng-template>
44
45 <ng-template pTemplate="body" let-expanded="expanded" let-runnerJob>
46 <tr>
47 <td class="expand-cell" [pRowToggler]="runnerJob">
48 <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
49 </td>
50
51 <td class="action-cell">
52 <my-action-dropdown
53 placement="bottom-right top-right left auto" container="body"
54 i18n-label label="Actions" [actions]="actions" [entry]="runnerJob"
55 ></my-action-dropdown>
56 </td>
57
58 <td>{{ runnerJob.uuid }}</td>
59 <td>{{ runnerJob.type }}</td>
60 <td>{{ runnerJob.state.label }}</td>
61 <td>{{ runnerJob.priority }}</td>
62 <td>{{ runnerJob.progress }}</td>
63 <td>{{ runnerJob.runner?.name }}</td>
64 <td>{{ runnerJob.createdAt | date: 'short' }}</td>
65 </tr>
66 </ng-template>
67
68 <ng-template pTemplate="rowexpansion" let-runnerJob>
69 <tr>
70 <td colspan="9">
71 <div class="mt-2 fs-7 font-monospace">
72 Parent job: {{ runnerJob.parent?.uuid || '-' }} <br />
73 Processed on {{ (runnerJob.startedAt || '-') }} <br />
74 Finished on {{ (runnerJob.finishedAt || '-') }} <br />
75 </div>
76
77 <div class="mt-2">
78 <strong i18n>Payload:</strong>
79 <pre>{{ runnerJob.payload }}</pre>
80 </div>
81
82 <div class="mt-2">
83 <strong i18n>Private payload:</strong>
84 <pre>{{ runnerJob.privatePayload }}</pre>
85 </div>
86
87 <pre *ngIf="runnerJob.error" class=".text-danger" >{{ runnerJob.error }}</pre>
88 </td>
89 </tr>
90 </ng-template>
91
92 <ng-template pTemplate="emptymessage">
93 <tr>
94 <td colspan="9">
95 <div class="no-results">
96 <ng-container i18n>No runner jobs found.</ng-container>
97 </div>
98 </td>
99 </tr>
100 </ng-template>
101</p-table>
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
new file mode 100644
index 000000000..ea889f0f7
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
@@ -0,0 +1,76 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { DropdownAction } from '@app/shared/shared-main'
5import { RunnerJob } from '@shared/models'
6import { RunnerJobFormatted, RunnerService } from '../runner.service'
7
8@Component({
9 selector: 'my-runner-job-list',
10 templateUrl: './runner-job-list.component.html'
11})
12export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnInit {
13 runnerJobs: RunnerJobFormatted[] = []
14 totalRecords = 0
15
16 sort: SortMeta = { field: 'createdAt', order: -1 }
17 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
18
19 actions: DropdownAction<RunnerJob>[][] = []
20
21 constructor (
22 private runnerService: RunnerService,
23 private notifier: Notifier,
24 private confirmService: ConfirmService
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.actions = [
31 [
32 {
33 label: $localize`Cancel this job`,
34 handler: job => this.cancelJob(job)
35 }
36 ]
37 ]
38
39 this.initialize()
40 }
41
42 getIdentifier () {
43 return 'RunnerJobListComponent'
44 }
45
46 async cancelJob (job: RunnerJob) {
47 const res = await this.confirmService.confirm(
48 $localize`Do you really want to cancel this job? Children won't be processed.`,
49 $localize`Cancel job`
50 )
51
52 if (res === false) return
53
54 this.runnerService.cancelJob(job)
55 .subscribe({
56 next: () => {
57 this.reloadData()
58 this.notifier.success($localize`Job cancelled.`)
59 },
60
61 error: err => this.notifier.error(err.message)
62 })
63 }
64
65 protected reloadDataInternal () {
66 this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
67 .subscribe({
68 next: resultList => {
69 this.runnerJobs = resultList.data
70 this.totalRecords = resultList.total
71 },
72
73 error: err => this.notifier.error(err.message)
74 })
75 }
76}
diff --git a/client/src/app/+admin/system/runners/runner-list/index.ts b/client/src/app/+admin/system/runners/runner-list/index.ts
new file mode 100644
index 000000000..5c12bb6d6
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-list/index.ts
@@ -0,0 +1 @@
export * from './runner-list.component'
diff --git a/client/src/app/+admin/system/runners/runner-list/runner-list.component.html b/client/src/app/+admin/system/runners/runner-list/runner-list.component.html
new file mode 100644
index 000000000..606eb9afd
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-list/runner-list.component.html
@@ -0,0 +1,61 @@
1<h1 class="d-flex justify-content-between">
2 <span class="text-nowrap me-2">
3 <my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>Remote runners</ng-container>
5 </span>
6
7 <a routerLink="/admin/system/runners/registration-tokens-list" class="peertube-button-link peertube-button-icon grey-button">
8 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
9 <ng-container i18n>Runner registration tokens</ng-container>
10 </a>
11</h1>
12
13<p-table
14 [value]="runners" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
15 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
16 [lazy]="true" (onLazyLoad)="loadLazy($event)"
17 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
18 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} remote runners"
19>
20 <ng-template pTemplate="header">
21 <tr>
22 <th style="width: 120px;"></th>
23 <th i18n>Name</th>
24 <th i18n>Description</th>
25 <th i18n>IP</th>
26 <th i18n>Last contact</th>
27 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
28 </tr>
29 </ng-template>
30
31 <ng-template pTemplate="body" let-runner>
32 <tr>
33 <td class="action-cell">
34 <my-action-dropdown
35 placement="bottom-right top-right left auto" container="body"
36 i18n-label label="Actions" [actions]="actions" [entry]="runner"
37 ></my-action-dropdown>
38 </td>
39
40 <td>{{ runner.name }}</td>
41
42 <td>{{ runner.description }}</td>
43
44 <td>{{ runner.ip }}</td>
45
46 <td>{{ runner.lastContact | date: 'short' }}</td>
47
48 <td>{{ runner.createdAt | date: 'short' }}</td>
49 </tr>
50 </ng-template>
51
52 <ng-template pTemplate="emptymessage">
53 <tr>
54 <td colspan="6">
55 <div class="no-results">
56 <ng-container i18n>No remote runners found.</ng-container>
57 </div>
58 </td>
59 </tr>
60 </ng-template>
61</p-table>
diff --git a/client/src/app/+admin/system/runners/runner-list/runner-list.component.ts b/client/src/app/+admin/system/runners/runner-list/runner-list.component.ts
new file mode 100644
index 000000000..7566f967e
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-list/runner-list.component.ts
@@ -0,0 +1,76 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { DropdownAction } from '@app/shared/shared-main'
5import { Runner } from '@shared/models'
6import { RunnerService } from '../runner.service'
7
8@Component({
9 selector: 'my-runner-list',
10 templateUrl: './runner-list.component.html'
11})
12export class RunnerListComponent extends RestTable <Runner> implements OnInit {
13 runners: Runner[] = []
14 totalRecords = 0
15
16 sort: SortMeta = { field: 'createdAt', order: -1 }
17 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
18
19 actions: DropdownAction<Runner>[][] = []
20
21 constructor (
22 private runnerService: RunnerService,
23 private notifier: Notifier,
24 private confirmService: ConfirmService
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.actions = [
31 [
32 {
33 label: $localize`Remove`,
34 handler: runer => this.deleteRunner(runer)
35 }
36 ]
37 ]
38
39 this.initialize()
40 }
41
42 getIdentifier () {
43 return 'RunnerListComponent'
44 }
45
46 async deleteRunner (runner: Runner) {
47 const res = await this.confirmService.confirm(
48 $localize`Do you really want to delete this runner? It won't be able to process jobs anymore.`,
49 $localize`Remove ${runner.name}`
50 )
51
52 if (res === false) return
53
54 this.runnerService.deleteRunner(runner)
55 .subscribe({
56 next: () => {
57 this.reloadData()
58 this.notifier.success($localize`Runner removed.`)
59 },
60
61 error: err => this.notifier.error(err.message)
62 })
63 }
64
65 protected reloadDataInternal () {
66 this.runnerService.listRunners({ pagination: this.pagination, sort: this.sort })
67 .subscribe({
68 next: resultList => {
69 this.runners = resultList.data
70 this.totalRecords = resultList.total
71 },
72
73 error: err => this.notifier.error(err.message)
74 })
75 }
76}
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/index.ts b/client/src/app/+admin/system/runners/runner-registration-token-list/index.ts
new file mode 100644
index 000000000..8e77978b3
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/index.ts
@@ -0,0 +1 @@
export * from './runner-registration-token-list.component'
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
new file mode 100644
index 000000000..2fd23e2fc
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
@@ -0,0 +1,65 @@
1<h1 class="d-flex justify-content-between">
2 <span class="text-nowrap me-2">
3 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
4 <ng-container i18n>Runner registration tokens</ng-container>
5 </span>
6
7 <div>
8 <a routerLink="/admin/system/runners/runners-list" class="peertube-button-link peertube-button-icon grey-button">
9 <my-global-icon iconName="codesandbox" aria-hidden="true"></my-global-icon>
10 <ng-container i18n>Remote runners</ng-container>
11 </a>
12 </div>
13</h1>
14
15<p-table
16 [value]="registrationTokens" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
17 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
18 [lazy]="true" (onLazyLoad)="loadLazy($event)"
19 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
20 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registration tokens"
21>
22 <ng-template pTemplate="header">
23 <tr>
24 <th style="width: 120px;"></th>
25 <th i18n>Token</th>
26 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
27 <th style="width: 160px;" i18n>Associated runners</th>
28 </tr>
29 </ng-template>
30
31 <ng-template pTemplate="caption">
32 <div class="caption">
33 <div class="left-buttons">
34 <my-button className="orange-button" i18n-label label="Generate token" icon="add" (click)="generateToken()"></my-button>
35 </div>
36 </div>
37 </ng-template>
38
39 <ng-template pTemplate="body" let-registrationToken>
40 <tr>
41 <td class="action-cell">
42 <my-action-dropdown
43 placement="bottom-right top-right left auto" container="body"
44 i18n-label label="Actions" [actions]="actions" [entry]="registrationToken"
45 ></my-action-dropdown>
46 </td>
47
48 <td>{{ registrationToken.registrationToken }}</td>
49
50 <td>{{ registrationToken.createdAt | date: 'short' }}</td>
51
52 <td>{{ registrationToken.registeredRunnersCount }}</td>
53 </tr>
54 </ng-template>
55
56 <ng-template pTemplate="emptymessage">
57 <tr>
58 <td colspan="4">
59 <div class="no-results">
60 <ng-container i18n>No registration token found for remote runners.</ng-container>
61 </div>
62 </td>
63 </tr>
64 </ng-template>
65</p-table>
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
new file mode 100644
index 000000000..f03aab189
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
@@ -0,0 +1,88 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { DropdownAction } from '@app/shared/shared-main'
5import { RunnerRegistrationToken } from '@shared/models'
6import { RunnerService } from '../runner.service'
7
8@Component({
9 selector: 'my-runner-registration-token-list',
10 templateUrl: './runner-registration-token-list.component.html'
11})
12export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
13 registrationTokens: RunnerRegistrationToken[] = []
14 totalRecords = 0
15
16 sort: SortMeta = { field: 'createdAt', order: -1 }
17 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
18
19 actions: DropdownAction<RunnerRegistrationToken>[][] = []
20
21 constructor (
22 private runnerService: RunnerService,
23 private notifier: Notifier,
24 private confirmService: ConfirmService
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.actions = [
31 [
32 {
33 label: $localize`Remove this token`,
34 handler: token => this.removeToken(token)
35 }
36 ]
37 ]
38
39 this.initialize()
40 }
41
42 getIdentifier () {
43 return 'RunnerRegistrationTokenListComponent'
44 }
45
46 generateToken () {
47 this.runnerService.generateToken()
48 .subscribe({
49 next: () => {
50 this.reloadData()
51 this.notifier.success($localize`Registration token generated.`)
52 },
53
54 error: err => this.notifier.error(err.message)
55 })
56 }
57
58 async removeToken (token: RunnerRegistrationToken) {
59 const res = await this.confirmService.confirm(
60 $localize`Do you really want to remove this registration token? All associated runners will also be removed.`,
61 $localize`Remove registration token`
62 )
63
64 if (res === false) return
65
66 this.runnerService.removeToken(token)
67 .subscribe({
68 next: () => {
69 this.reloadData()
70 this.notifier.success($localize`Registration token removed.`)
71 },
72
73 error: err => this.notifier.error(err.message)
74 })
75 }
76
77 protected reloadDataInternal () {
78 this.runnerService.listRegistrationTokens({ pagination: this.pagination, sort: this.sort })
79 .subscribe({
80 next: resultList => {
81 this.registrationTokens = resultList.data
82 this.totalRecords = resultList.total
83 },
84
85 error: err => this.notifier.error(err.message)
86 })
87 }
88}
diff --git a/client/src/app/+admin/system/runners/runner.service.ts b/client/src/app/+admin/system/runners/runner.service.ts
new file mode 100644
index 000000000..05083318c
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner.service.ts
@@ -0,0 +1,117 @@
1
2import { SortMeta } from 'primeng/api'
3import { catchError, forkJoin, map } from 'rxjs'
4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
7import { peertubeTranslate } from '@shared/core-utils'
8import { ResultList } from '@shared/models/common'
9import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners'
10import { environment } from '../../../../environments/environment'
11
12export type RunnerJobFormatted = RunnerJob & {
13 payload: string
14 privatePayload: string
15}
16
17@Injectable()
18export class RunnerService {
19 private static BASE_RUNNER_URL = environment.apiUrl + '/api/v1/runners'
20
21 constructor (
22 private authHttp: HttpClient,
23 private server: ServerService,
24 private restService: RestService,
25 private restExtractor: RestExtractor
26 ) {}
27
28 listRegistrationTokens (options: {
29 pagination: RestPagination
30 sort: SortMeta
31 }) {
32 const { pagination, sort } = options
33
34 let params = new HttpParams()
35 params = this.restService.addRestGetParams(params, pagination, sort)
36
37 return this.authHttp.get<ResultList<RunnerRegistrationToken>>(RunnerService.BASE_RUNNER_URL + '/registration-tokens', { params })
38 .pipe(catchError(res => this.restExtractor.handleError(res)))
39 }
40
41 generateToken () {
42 return this.authHttp.post(RunnerService.BASE_RUNNER_URL + '/registration-tokens/generate', {})
43 .pipe(catchError(res => this.restExtractor.handleError(res)))
44 }
45
46 removeToken (token: RunnerRegistrationToken) {
47 return this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/registration-tokens/' + token.id)
48 .pipe(catchError(res => this.restExtractor.handleError(res)))
49 }
50
51 // ---------------------------------------------------------------------------
52
53 listRunnerJobs (options: {
54 pagination: RestPagination
55 sort: SortMeta
56 search?: string
57 }) {
58 const { pagination, sort, search } = options
59
60 let params = new HttpParams()
61 params = this.restService.addRestGetParams(params, pagination, sort)
62
63 if (search) params = params.append('search', search)
64
65 return forkJoin([
66 this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
67 this.server.getServerLocale()
68 ]).pipe(
69 map(([ res, translations ]) => {
70 const newData = res.data.map(job => {
71 return {
72 ...job,
73
74 state: {
75 id: job.state.id,
76 label: peertubeTranslate(job.state.label, translations)
77 },
78 payload: JSON.stringify(job.payload, null, 2),
79 privatePayload: JSON.stringify(job.privatePayload, null, 2)
80 } as RunnerJobFormatted
81 })
82
83 return {
84 total: res.total,
85 data: newData
86 }
87 }),
88 map(res => this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'startedAt', 'finishedAt' ], 'precise')),
89 catchError(res => this.restExtractor.handleError(res))
90 )
91 }
92
93 cancelJob (job: RunnerJob) {
94 return this.authHttp.post(RunnerService.BASE_RUNNER_URL + '/jobs/' + job.uuid + '/cancel', {})
95 .pipe(catchError(res => this.restExtractor.handleError(res)))
96 }
97
98 // ---------------------------------------------------------------------------
99
100 listRunners (options: {
101 pagination: RestPagination
102 sort: SortMeta
103 }) {
104 const { pagination, sort } = options
105
106 let params = new HttpParams()
107 params = this.restService.addRestGetParams(params, pagination, sort)
108
109 return this.authHttp.get<ResultList<Runner>>(RunnerService.BASE_RUNNER_URL + '/', { params })
110 .pipe(catchError(res => this.restExtractor.handleError(res)))
111 }
112
113 deleteRunner (runner: Runner) {
114 return this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/' + runner.id)
115 .pipe(catchError(res => this.restExtractor.handleError(res)))
116 }
117}
diff --git a/client/src/app/+admin/system/runners/runners.routes.ts b/client/src/app/+admin/system/runners/runners.routes.ts
new file mode 100644
index 000000000..fabe687d6
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runners.routes.ts
@@ -0,0 +1,53 @@
1import { Routes } from '@angular/router'
2import { UserRightGuard } from '@app/core'
3import { UserRight } from '@shared/models'
4import { RunnerJobListComponent } from './runner-job-list'
5import { RunnerListComponent } from './runner-list'
6import { RunnerRegistrationTokenListComponent } from './runner-registration-token-list'
7
8export const RunnersRoutes: Routes = [
9 {
10 path: 'runners',
11 canActivate: [ UserRightGuard ],
12 data: {
13 userRight: UserRight.MANAGE_RUNNERS
14 },
15 children: [
16 {
17 path: '',
18 redirectTo: 'jobs-list',
19 pathMatch: 'full'
20 },
21
22 {
23 path: 'jobs-list',
24 component: RunnerJobListComponent,
25 data: {
26 meta: {
27 title: $localize`List runner jobs`
28 }
29 }
30 },
31
32 {
33 path: 'runners-list',
34 component: RunnerListComponent,
35 data: {
36 meta: {
37 title: $localize`List remote runners`
38 }
39 }
40 },
41
42 {
43 path: 'registration-tokens-list',
44 component: RunnerRegistrationTokenListComponent,
45 data: {
46 meta: {
47 title: $localize`List registration runner tokens`
48 }
49 }
50 }
51 ]
52 }
53]
diff --git a/client/src/app/+admin/system/system.routes.ts b/client/src/app/+admin/system/system.routes.ts
index d180aa3b9..87e4b25b3 100644
--- a/client/src/app/+admin/system/system.routes.ts
+++ b/client/src/app/+admin/system/system.routes.ts
@@ -4,6 +4,7 @@ import { UserRight } from '@shared/models'
4import { DebugComponent } from './debug' 4import { DebugComponent } from './debug'
5import { JobsComponent } from './jobs/jobs.component' 5import { JobsComponent } from './jobs/jobs.component'
6import { LogsComponent } from './logs' 6import { LogsComponent } from './logs'
7import { RunnersRoutes } from './runners'
7 8
8export const SystemRoutes: Routes = [ 9export const SystemRoutes: Routes = [
9 { 10 {
@@ -46,7 +47,9 @@ export const SystemRoutes: Routes = [
46 title: $localize`Debug` 47 title: $localize`Debug`
47 } 48 }
48 } 49 }
49 } 50 },
51
52 ...RunnersRoutes
50 ] 53 ]
51 } 54 }
52] 55]
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.ts b/client/src/app/shared/shared-video-live/live-stream-information.component.ts
index 3dd59bb57..400a6fa01 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.ts
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.ts
@@ -43,7 +43,9 @@ export class LiveStreamInformationComponent {
43 [LiveVideoError.BLACKLISTED]: $localize`Live blacklisted`, 43 [LiveVideoError.BLACKLISTED]: $localize`Live blacklisted`,
44 [LiveVideoError.DURATION_EXCEEDED]: $localize`Max duration exceeded`, 44 [LiveVideoError.DURATION_EXCEEDED]: $localize`Max duration exceeded`,
45 [LiveVideoError.FFMPEG_ERROR]: $localize`Server error`, 45 [LiveVideoError.FFMPEG_ERROR]: $localize`Server error`,
46 [LiveVideoError.QUOTA_EXCEEDED]: $localize`Quota exceeded` 46 [LiveVideoError.QUOTA_EXCEEDED]: $localize`Quota exceeded`,
47 [LiveVideoError.RUNNER_JOB_CANCEL]: $localize`Runner job cancelled`,
48 [LiveVideoError.RUNNER_JOB_ERROR]: $localize`Error in runner job`
47 } 49 }
48 50
49 return errors[session.error] 51 return errors[session.error]
diff --git a/client/src/sass/class-helpers/_buttons.scss b/client/src/sass/class-helpers/_buttons.scss
index 436bb48f4..fdbf6f9d2 100644
--- a/client/src/sass/class-helpers/_buttons.scss
+++ b/client/src/sass/class-helpers/_buttons.scss
@@ -35,3 +35,7 @@
35.peertube-radio-container { 35.peertube-radio-container {
36 @include peertube-radio-container; 36 @include peertube-radio-container;
37} 37}
38
39.peertube-button-icon {
40 @include button-with-icon(18px, 3px, -1px);
41}
diff --git a/client/src/sass/class-helpers/_custom-bootstrap-helpers.scss b/client/src/sass/class-helpers/_custom-bootstrap-helpers.scss
index dfe6f9050..b39c4144f 100644
--- a/client/src/sass/class-helpers/_custom-bootstrap-helpers.scss
+++ b/client/src/sass/class-helpers/_custom-bootstrap-helpers.scss
@@ -6,3 +6,7 @@
6.fs-5-5 { 6.fs-5-5 {
7 @include font-size(18px); 7 @include font-size(18px);
8} 8}
9
10.fs-7 {
11 @include font-size(14px);
12}