diff options
-rw-r--r-- | client/src/app/+admin/system/jobs/job.service.ts | 11 | ||||
-rw-r--r-- | client/src/app/+admin/system/jobs/jobs.component.html | 33 | ||||
-rw-r--r-- | client/src/app/+admin/system/jobs/jobs.component.ts | 16 | ||||
-rw-r--r-- | server/controllers/api/jobs.ts | 23 | ||||
-rw-r--r-- | server/helpers/custom-validators/jobs.ts | 1 | ||||
-rw-r--r-- | server/lib/job-queue/job-queue.ts | 13 | ||||
-rw-r--r-- | server/middlewares/validators/jobs.ts | 18 | ||||
-rw-r--r-- | support/doc/api/openapi.yaml | 24 |
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' |
14 | import { paginationValidator } from '../../middlewares/validators' | 14 | import { paginationValidator } from '../../middlewares/validators' |
15 | import { listJobsValidator } from '../../middlewares/validators/jobs' | 15 | import { listJobsStateValidator, listJobsValidator } from '../../middlewares/validators/jobs' |
16 | import { isArray } from '../../helpers/custom-validators/misc' | 16 | import { isArray } from '../../helpers/custom-validators/misc' |
17 | import { jobStates } from '@server/helpers/custom-validators/jobs' | ||
17 | 18 | ||
18 | const jobsRouter = express.Router() | 19 | const jobsRouter = express.Router() |
19 | 20 | ||
21 | jobsRouter.get('/', | ||
22 | authenticate, | ||
23 | ensureUserHasRight(UserRight.MANAGE_JOBS), | ||
24 | paginationValidator, | ||
25 | jobsSortValidator, | ||
26 | setDefaultSort, | ||
27 | setDefaultPagination, | ||
28 | listJobsValidator, | ||
29 | asyncMiddleware(listJobs) | ||
30 | ) | ||
31 | |||
20 | jobsRouter.get('/:state', | 32 | jobsRouter.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 | ||
39 | async function listJobs (req: express.Request, res: express.Response) { | 52 | async 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 | ||
17 | export { | 17 | export { |
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' | |||
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
7 | const listJobsValidator = [ | 7 | const 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 | ||
21 | const 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 | ||
25 | export { | 36 | export { |
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 | |||
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: > |