aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/system/jobs/job.service.ts11
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html33
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts16
-rw-r--r--server/controllers/api/jobs.ts23
-rw-r--r--server/helpers/custom-validators/jobs.ts1
-rw-r--r--server/lib/job-queue/job-queue.ts13
-rw-r--r--server/middlewares/validators/jobs.ts18
-rw-r--r--support/doc/api/openapi.yaml24
8 files changed, 113 insertions, 26 deletions
diff --git a/client/src/app/+admin/system/jobs/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts
index 1ac50f050..4b4a8914f 100644
--- a/client/src/app/+admin/system/jobs/job.service.ts
+++ b/client/src/app/+admin/system/jobs/job.service.ts
@@ -19,13 +19,20 @@ export class JobService {
19 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor
20 ) {} 20 ) {}
21 21
22 getJobs (jobState: JobStateClient, jobType: JobTypeClient, pagination: RestPagination, sort: SortMeta): Observable<ResultList<Job>> { 22 getJobs (options: {
23 jobState?: JobStateClient,
24 jobType: JobTypeClient,
25 pagination: RestPagination,
26 sort: SortMeta
27 }): Observable<ResultList<Job>> {
28 const { jobState, jobType, pagination, sort } = options
29
23 let params = new HttpParams() 30 let params = new HttpParams()
24 params = this.restService.addRestGetParams(params, pagination, sort) 31 params = this.restService.addRestGetParams(params, pagination, sort)
25 32
26 if (jobType !== 'all') params = params.append('jobType', jobType) 33 if (jobType !== 'all') params = params.append('jobType', jobType)
27 34
28 return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL + '/' + jobState, { params }) 35 return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL + `/${jobState ? jobState : ''}`, { params })
29 .pipe( 36 .pipe(
30 map(res => { 37 map(res => {
31 return this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ]) 38 return this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ])
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html
index e06156a9e..2d60e7b9e 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.html
+++ b/client/src/app/+admin/system/jobs/jobs.component.html
@@ -17,6 +17,9 @@
17 [clearable]="false" 17 [clearable]="false"
18 [searchable]="false" 18 [searchable]="false"
19 > 19 >
20 <ng-option value="all">
21 <span i18n="Selector for the list displaying jobs, filtering by their state">any</span>
22 </ng-option>
20 <ng-option *ngFor="let state of jobStates" [value]="state"> 23 <ng-option *ngFor="let state of jobStates" [value]="state">
21 <span class="badge" [ngClass]="getJobStateClass(state)">{{ state }}</span> 24 <span class="badge" [ngClass]="getJobStateClass(state)">{{ state }}</span>
22 </ng-option> 25 </ng-option>
@@ -37,27 +40,31 @@
37 <th style="width: 40px"></th> 40 <th style="width: 40px"></th>
38 <th style="width: calc(100% - 390px)" class="job-id" i18n>ID</th> 41 <th style="width: calc(100% - 390px)" class="job-id" i18n>ID</th>
39 <th style="width: 200px" class="job-type" i18n>Type</th> 42 <th style="width: 200px" class="job-type" i18n>Type</th>
43 <th style="width: 200px" class="job-type" i18n *ngIf="jobState === 'all'">State</th>
40 <th style="width: 150px" class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 44 <th style="width: 150px" class="job-date" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
41 </tr> 45 </tr>
42 </ng-template> 46 </ng-template>
43 47
44 <ng-template pTemplate="body" let-expanded="expanded" let-job> 48 <ng-template pTemplate="body" let-expanded="expanded" let-job>
45 <tr> 49 <tr>
46 <td class="expand-cell" [pRowToggler]="job" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body"> 50 <td class="expand-cell c-hand" [pRowToggler]="job" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
47 <span class="expander"> 51 <span class="expander">
48 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> 52 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
49 </span> 53 </span>
50 </td> 54 </td>
51 55
52 <td class="job-id" [pRowToggler]="job" [title]="job.id">{{ job.id }}</td> 56 <td class="job-id c-hand" [pRowToggler]="job" [title]="job.id">{{ job.id }}</td>
53 <td class="job-type" [pRowToggler]="job">{{ job.type }}</td> 57 <td class="job-type c-hand" [pRowToggler]="job">{{ job.type }}</td>
54 <td class="job-date" [pRowToggler]="job">{{ job.createdAt | date: 'short' }}</td> 58 <td class="job-type c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
59 <span class="badge" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
60 </td>
61 <td class="job-date c-hand" [pRowToggler]="job">{{ job.createdAt | date: 'short' }}</td>
55 </tr> 62 </tr>
56 </ng-template> 63 </ng-template>
57 64
58 <ng-template pTemplate="rowexpansion" let-job> 65 <ng-template pTemplate="rowexpansion" let-job>
59 <tr> 66 <tr>
60 <td colspan="4"> 67 <td [attr.colspan]="getColspan()">
61 <pre>{{ [ 68 <pre>{{ [
62 'Job: ' + job.id, 69 'Job: ' + job.id,
63 'Type: ' + job.type, 70 'Type: ' + job.type,
@@ -67,12 +74,12 @@
67 </td> 74 </td>
68 </tr> 75 </tr>
69 <tr> 76 <tr>
70 <td colspan="4"> 77 <td [attr.colspan]="getColspan()">
71 <pre>{{ job.data }}</pre> 78 <pre>{{ job.data }}</pre>
72 </td> 79 </td>
73 </tr> 80 </tr>
74 <tr class="job-error" *ngIf="job.error"> 81 <tr class="job-error" *ngIf="job.error">
75 <td colspan="4"> 82 <td [attr.colspan]="getColspan()">
76 <pre>{{ job.error }}</pre> 83 <pre>{{ job.error }}</pre>
77 </td> 84 </td>
78 </tr> 85 </tr>
@@ -80,11 +87,17 @@
80 87
81 <ng-template pTemplate="emptymessage"> 88 <ng-template pTemplate="emptymessage">
82 <tr> 89 <tr>
83 <td colspan="4"> 90 <td [attr.colspan]="getColspan()">
84 <div class="no-results"> 91 <div class="no-results">
85 <div class="d-block"> 92 <div class="d-block">
86 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container> 93 <ng-container *ngIf="jobState === 'all'">
87 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container> 94 <ng-container *ngIf="jobType === 'all'" i18n>No jobs found.</ng-container>
95 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found.</ng-container>
96 </ng-container>
97 <ng-container *ngIf="jobState !== 'all'">
98 <ng-container *ngIf="jobType === 'all'" i18n>No <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container>
99 <ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container>
100 </ng-container>
88 </div> 101 </div>
89 </div> 102 </div>
90 </td> 103 </td>
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index f8e12d1b6..b7f18067b 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -16,7 +16,7 @@ export class JobsComponent extends RestTable implements OnInit {
16 private static LOCAL_STORAGE_STATE = 'jobs-list-state' 16 private static LOCAL_STORAGE_STATE = 'jobs-list-state'
17 private static LOCAL_STORAGE_TYPE = 'jobs-list-type' 17 private static LOCAL_STORAGE_TYPE = 'jobs-list-type'
18 18
19 jobState: JobStateClient = 'waiting' 19 jobState?: JobStateClient | 'all'
20 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] 20 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ]
21 21
22 jobType: JobTypeClient = 'all' 22 jobType: JobTypeClient = 'all'
@@ -73,6 +73,10 @@ export class JobsComponent extends RestTable implements OnInit {
73 } 73 }
74 } 74 }
75 75
76 getColspan () {
77 return this.jobState === 'all' ? 5 : 4
78 }
79
76 onJobStateOrTypeChanged () { 80 onJobStateOrTypeChanged () {
77 this.pagination.start = 0 81 this.pagination.start = 0
78 82
@@ -81,8 +85,16 @@ export class JobsComponent extends RestTable implements OnInit {
81 } 85 }
82 86
83 protected loadData () { 87 protected loadData () {
88 let jobState = this.jobState as JobState
89 if (this.jobState === 'all') jobState = null
90
84 this.jobsService 91 this.jobsService
85 .getJobs(this.jobState, this.jobType, this.pagination, this.sort) 92 .getJobs({
93 jobState,
94 jobType: this.jobType,
95 pagination: this.pagination,
96 sort: this.sort
97 })
86 .subscribe( 98 .subscribe(
87 resultList => { 99 resultList => {
88 this.jobs = resultList.data 100 this.jobs = resultList.data
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index ed6c94533..1131a44d6 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -12,11 +12,23 @@ import {
12 setDefaultSort 12 setDefaultSort
13} from '../../middlewares' 13} from '../../middlewares'
14import { paginationValidator } from '../../middlewares/validators' 14import { paginationValidator } from '../../middlewares/validators'
15import { listJobsValidator } from '../../middlewares/validators/jobs' 15import { listJobsStateValidator, listJobsValidator } from '../../middlewares/validators/jobs'
16import { isArray } from '../../helpers/custom-validators/misc' 16import { isArray } from '../../helpers/custom-validators/misc'
17import { jobStates } from '@server/helpers/custom-validators/jobs'
17 18
18const jobsRouter = express.Router() 19const jobsRouter = express.Router()
19 20
21jobsRouter.get('/',
22 authenticate,
23 ensureUserHasRight(UserRight.MANAGE_JOBS),
24 paginationValidator,
25 jobsSortValidator,
26 setDefaultSort,
27 setDefaultPagination,
28 listJobsValidator,
29 asyncMiddleware(listJobs)
30)
31
20jobsRouter.get('/:state', 32jobsRouter.get('/:state',
21 authenticate, 33 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 34 ensureUserHasRight(UserRight.MANAGE_JOBS),
@@ -25,6 +37,7 @@ jobsRouter.get('/:state',
25 setDefaultSort, 37 setDefaultSort,
26 setDefaultPagination, 38 setDefaultPagination,
27 listJobsValidator, 39 listJobsValidator,
40 listJobsStateValidator,
28 asyncMiddleware(listJobs) 41 asyncMiddleware(listJobs)
29) 42)
30 43
@@ -37,7 +50,7 @@ export {
37// --------------------------------------------------------------------------- 50// ---------------------------------------------------------------------------
38 51
39async function listJobs (req: express.Request, res: express.Response) { 52async function listJobs (req: express.Request, res: express.Response) {
40 const state = req.params.state as JobState 53 const state = req.params.state as JobState || jobStates
41 const asc = req.query.sort === 'createdAt' 54 const asc = req.query.sort === 'createdAt'
42 const jobType = req.query.jobType 55 const jobType = req.query.jobType
43 56
@@ -52,7 +65,11 @@ async function listJobs (req: express.Request, res: express.Response) {
52 65
53 const result: ResultList<Job> = { 66 const result: ResultList<Job> = {
54 total, 67 total,
55 data: jobs.map(j => formatJob(j, state)) 68 data: Array.isArray(state)
69 ? await Promise.all(
70 jobs.map(async j => formatJob(j, await j.getState() as JobState))
71 )
72 : jobs.map(j => formatJob(j, state))
56 } 73 }
57 return res.json(result) 74 return res.json(result)
58} 75}
diff --git a/server/helpers/custom-validators/jobs.ts b/server/helpers/custom-validators/jobs.ts
index dd33e85a3..72dc73ee4 100644
--- a/server/helpers/custom-validators/jobs.ts
+++ b/server/helpers/custom-validators/jobs.ts
@@ -15,6 +15,7 @@ function isValidJobType (value: any) {
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17export { 17export {
18 jobStates,
18 isValidJobState, 19 isValidJobState,
19 isValidJobType 20 isValidJobType
20} 21}
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 8d97434ac..49f06584d 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -154,13 +154,13 @@ class JobQueue {
154 } 154 }
155 155
156 async listForApi (options: { 156 async listForApi (options: {
157 state: JobState 157 state: JobState | JobState[]
158 start: number 158 start: number
159 count: number 159 count: number
160 asc?: boolean 160 asc?: boolean
161 jobType: JobType 161 jobType: JobType
162 }): Promise<Bull.Job[]> { 162 }): Promise<Bull.Job[]> {
163 const { state, start, count, asc, jobType } = options 163 const { state = Array.isArray(options.state) ? options.state : [ options.state ], start, count, asc, jobType } = options
164 let results: Bull.Job[] = [] 164 let results: Bull.Job[] = []
165 165
166 const filteredJobTypes = this.filterJobTypes(jobType) 166 const filteredJobTypes = this.filterJobTypes(jobType)
@@ -172,7 +172,7 @@ class JobQueue {
172 continue 172 continue
173 } 173 }
174 174
175 const jobs = await queue.getJobs([ state ], 0, start + count, asc) 175 const jobs = await queue.getJobs(state as Bull.JobStatus[], 0, start + count, asc)
176 results = results.concat(jobs) 176 results = results.concat(jobs)
177 } 177 }
178 178
@@ -188,7 +188,8 @@ class JobQueue {
188 return results.slice(start, start + count) 188 return results.slice(start, start + count)
189 } 189 }
190 190
191 async count (state: JobState, jobType?: JobType): Promise<number> { 191 async count (state: JobState | JobState[], jobType?: JobType): Promise<number> {
192 const states = Array.isArray(state) ? state : [ state ]
192 let total = 0 193 let total = 0
193 194
194 const filteredJobTypes = this.filterJobTypes(jobType) 195 const filteredJobTypes = this.filterJobTypes(jobType)
@@ -202,7 +203,9 @@ class JobQueue {
202 203
203 const counts = await queue.getJobCounts() 204 const counts = await queue.getJobCounts()
204 205
205 total += counts[state] 206 for (const s of states) {
207 total += counts[s]
208 }
206 } 209 }
207 210
208 return total 211 return total
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts
index b57615dbc..0fc183c1a 100644
--- a/server/middlewares/validators/jobs.ts
+++ b/server/middlewares/validators/jobs.ts
@@ -5,8 +5,6 @@ import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const listJobsValidator = [ 7const listJobsValidator = [
8 param('state')
9 .custom(isValidJobState).not().isEmpty().withMessage('Should have a valid job state'),
10 query('jobType') 8 query('jobType')
11 .optional() 9 .optional()
12 .custom(isValidJobType).withMessage('Should have a valid job state'), 10 .custom(isValidJobType).withMessage('Should have a valid job state'),
@@ -20,8 +18,22 @@ const listJobsValidator = [
20 } 18 }
21] 19]
22 20
21const listJobsStateValidator = [
22 param('state')
23 .custom(isValidJobState).not().isEmpty().withMessage('Should have a valid job state'),
24
25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params })
27
28 if (areValidationErrors(req, res)) return
29
30 return next()
31 }
32]
33
23// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
24 35
25export { 36export {
26 listJobsValidator 37 listJobsValidator,
38 listJobsStateValidator
27} 39}
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index ba420b4a9..c06bffb0a 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -356,15 +356,17 @@ paths:
356 - name: state 356 - name: state
357 in: path 357 in: path
358 required: true 358 required: true
359 description: The state of the job 359 description: The state of the job ('' for for no filter)
360 schema: 360 schema:
361 type: string 361 type: string
362 enum: 362 enum:
363 - ''
363 - active 364 - active
364 - completed 365 - completed
365 - failed 366 - failed
366 - waiting 367 - waiting
367 - delayed 368 - delayed
369 - $ref: '#/components/parameters/jobType'
368 - $ref: '#/components/parameters/start' 370 - $ref: '#/components/parameters/start'
369 - $ref: '#/components/parameters/count' 371 - $ref: '#/components/parameters/count'
370 - $ref: '#/components/parameters/sort' 372 - $ref: '#/components/parameters/sort'
@@ -3780,6 +3782,26 @@ components:
3780 schema: 3782 schema:
3781 type: string 3783 type: string
3782 example: peertube-plugin-auth-ldap 3784 example: peertube-plugin-auth-ldap
3785 jobType:
3786 name: jobType
3787 in: query
3788 required: false
3789 description: job type
3790 schema:
3791 type: string
3792 enum:
3793 - activitypub-follow
3794 - activitypub-http-broadcast
3795 - activitypub-http-fetcher
3796 - activitypub-http-unicast
3797 - email
3798 - video-transcoding
3799 - video-file-import
3800 - video-import
3801 - videos-views
3802 - activitypub-refresher
3803 - video-redundancy
3804 - video-live-ending
3783 securitySchemes: 3805 securitySchemes:
3784 OAuth2: 3806 OAuth2:
3785 description: > 3807 description: >