import { UsersRoutes } from './users'
import { VideoAbusesRoutes } from './video-abuses'
import { VideoBlacklistRoutes } from './video-blacklist'
+import { JobsRoutes } from './jobs/job.routes'
const adminRoutes: Routes = [
{
...FollowsRoutes,
...UsersRoutes,
...VideoAbusesRoutes,
- ...VideoBlacklistRoutes
+ ...VideoBlacklistRoutes,
+ ...JobsRoutes
]
}
]
import { AdminComponent } from './admin.component'
import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows'
import { FollowingListComponent } from './follows/following-list/following-list.component'
+import { JobsComponent } from './jobs/job.component'
+import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
+import { JobService } from './jobs/shared/job.service'
import { UserAddComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users'
import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
VideoBlacklistListComponent,
VideoAbusesComponent,
- VideoAbuseListComponent
+ VideoAbuseListComponent,
+
+ JobsComponent,
+ JobsListComponent
],
exports: [
providers: [
FollowService,
- UserService
+ UserService,
+ JobService
]
})
export class AdminModule { }
--- /dev/null
+export * from './'
--- /dev/null
+import { Component } from '@angular/core'
+
+@Component({
+ template: '<router-outlet></router-outlet>'
+})
+export class JobsComponent {}
--- /dev/null
+import { Routes } from '@angular/router'
+
+import { UserRightGuard } from '../../core'
+import { FollowingAddComponent } from './following-add'
+import { UserRight } from '../../../../../shared'
+import { FollowingListComponent } from './following-list/following-list.component'
+import { JobsComponent } from './job.component'
+import { JobsListComponent } from './jobs-list/jobs-list.component'
+
+export const JobsRoutes: Routes = [
+ {
+ path: 'jobs',
+ component: JobsComponent,
+ canActivate: [ UserRightGuard ],
+ data: {
+ userRight: UserRight.MANAGE_JOBS
+ },
+ children: [
+ {
+ path: '',
+ redirectTo: 'list',
+ pathMatch: 'full'
+ },
+ {
+ path: 'list',
+ component: JobsListComponent,
+ data: {
+ meta: {
+ title: 'Jobs list'
+ }
+ }
+ }
+ ]
+ }
+]
--- /dev/null
+export * from './jobs-list.component'
--- /dev/null
+<div class="row">
+ <div class="content-padding">
+ <h3>Jobs list</h3>
+
+ <p-dataTable
+ [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+ sortField="createdAt" (onLazyLoad)="loadLazy($event)"
+ >
+ <p-column field="id" header="ID"></p-column>
+ <p-column field="category" header="Category"></p-column>
+ <p-column field="handlerName" header="Handler name"></p-column>
+ <p-column field="handlerInputData" header="Input data"></p-column>
+ <p-column field="state" header="State"></p-column>
+ <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
+ <p-column field="updatedAt" header="Updated date"></p-column>
+ </p-dataTable>
+ </div>
+</div>
--- /dev/null
+import { Component } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { SortMeta } from 'primeng/primeng'
+import { Job } from '../../../../../../shared/index'
+import { RestPagination, RestTable } from '../../../shared'
+import { JobService } from '../shared'
+import { RestExtractor } from '../../../shared/rest/rest-extractor.service'
+
+@Component({
+ selector: 'my-jobs-list',
+ templateUrl: './jobs-list.component.html',
+ styleUrls: [ ]
+})
+export class JobsListComponent extends RestTable {
+ jobs: Job[] = []
+ totalRecords = 0
+ rowsPerPage = 10
+ sort: SortMeta = { field: 'createdAt', order: 1 }
+ pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+ constructor (
+ private notificationsService: NotificationsService,
+ private restExtractor: RestExtractor,
+ private jobsService: JobService
+ ) {
+ super()
+ }
+
+ protected loadData () {
+ this.jobsService
+ .getJobs(this.pagination, this.sort)
+ .map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this)))
+ .subscribe(
+ resultList => {
+ this.jobs = resultList.data
+ this.totalRecords = resultList.total
+ },
+
+ err => this.notificationsService.error('Error', err.message)
+ )
+ }
+
+ private formatJob (job: Job) {
+ const handlerInputData = JSON.stringify(job.handlerInputData)
+
+ return Object.assign(job, {
+ handlerInputData
+ })
+ }
+}
--- /dev/null
+export * from './job.service'
--- /dev/null
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { SortMeta } from 'primeng/primeng'
+import 'rxjs/add/operator/catch'
+import 'rxjs/add/operator/map'
+import { Observable } from 'rxjs/Observable'
+import { ResultList } from '../../../../../../shared'
+import { Job } from '../../../../../../shared/models/job.model'
+
+import { RestExtractor, RestPagination, RestService } from '../../../shared'
+
+@Injectable()
+export class JobService {
+ private static BASE_JOB_URL = API_URL + '/api/v1/jobs'
+
+ constructor (
+ private authHttp: HttpClient,
+ private restService: RestService,
+ private restExtractor: RestExtractor
+ ) {}
+
+ getJobs (pagination: RestPagination, sort: SortMeta): Observable<ResultList<Job>> {
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination, sort)
+
+ return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params })
+ .map(res => this.restExtractor.convertResultListDateToHuman(res))
+ .catch(err => this.restExtractor.handleError(err))
+ }
+}
<span class="hidden-xs glyphicon glyphicon-eye-close"></span>
Video blacklist
</a>
+
+ <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active">
+ <span class="hidden-xs glyphicon glyphicon-tasks"></span>
+ Jobs
+ </a>
</div>
<div class="panel-block">
hasVideoBlacklistRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
}
+
+ hasJobsRight () {
+ return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
+ }
}
import { serverRouter } from './server'
import { usersRouter } from './users'
import { videosRouter } from './videos'
+import { jobsRouter } from './jobs'
const apiRouter = express.Router()
apiRouter.use('/config', configRouter)
apiRouter.use('/users', usersRouter)
apiRouter.use('/videos', videosRouter)
+apiRouter.use('/jobs', jobsRouter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)
--- /dev/null
+import * as express from 'express'
+import { asyncMiddleware, jobsSortValidator, setJobsSort, setPagination } from '../../middlewares'
+import { paginationValidator } from '../../middlewares/validators/pagination'
+import { database as db } from '../../initializers'
+import { getFormattedObjects } from '../../helpers/utils'
+import { authenticate } from '../../middlewares/oauth'
+import { ensureUserHasRight } from '../../middlewares/user-right'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
+
+const jobsRouter = express.Router()
+
+jobsRouter.get('/',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_JOBS),
+ paginationValidator,
+ jobsSortValidator,
+ setJobsSort,
+ setPagination,
+ asyncMiddleware(listJobs)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ jobsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function listJobs (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const resultList = await db.Job.listForApi(req.query.start, req.query.count, req.query.sort)
+
+ return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
// Sortable columns per schema
const SORTABLE_COLUMNS = {
USERS: [ 'id', 'username', 'createdAt' ],
+ JOBS: [ 'id', 'createdAt' ],
VIDEO_ABUSES: [ 'id', 'createdAt' ],
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
return next()
}
+function setJobsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+ if (!req.query.sort) req.query.sort = '-createdAt'
+
+ return next()
+}
+
function setVideoAbusesSort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.sort) req.query.sort = '-createdAt'
setVideosSort,
setBlacklistSort,
setFollowersSort,
- setFollowingSort
+ setFollowingSort,
+ setJobsSort
}
// Initialize constants here for better performances
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
+const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
+const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
videosSortValidator,
blacklistSortValidator,
followersSortValidator,
- followingSortValidator
+ followingSortValidator,
+ jobsSortValidator
}
// ---------------------------------------------------------------------------
-import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
-
-// Don't use barrel, import just what we need
-import { AccountInstance } from './account-interface'
-import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import * as Sequelize from 'sequelize'
import { ResultList } from '../../../shared/models/result-list.model'
import { UserRight } from '../../../shared/models/users/user-right.enum'
import { UserRole } from '../../../shared/models/users/user-role'
+import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import { AccountInstance } from './account-interface'
export namespace UserMethods {
export type HasRight = (this: UserInstance, right: UserRight) => boolean
+import * as Bluebird from 'bluebird'
import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model'
+import { ResultList } from '../../../shared/models/result-list.model'
export namespace JobMethods {
- export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
+ export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Bluebird<JobInstance[]>
+ export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<JobInstance> >
+
+ export type ToFormattedJSON = (this: JobInstance) => FormattedJob
}
export interface JobClass {
listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+ listForApi: JobMethods.ListForApi,
}
export interface JobAttributes {
state: JobState
+ category: JobCategory
handlerName: string
handlerInputData: any
}
id: number
createdAt: Date
updatedAt: Date
+
+ toFormattedJSON: JobMethods.ToFormattedJSON
}
export interface JobModel extends JobClass, Sequelize.Model<JobInstance, JobAttributes> {}
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
-
-import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
-
-import { addMethodsToModel } from '../utils'
-import {
- JobInstance,
- JobAttributes,
-
- JobMethods
-} from './job-interface'
import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { JOB_CATEGORIES, JOB_STATES } from '../../initializers'
+import { addMethodsToModel, getSort } from '../utils'
+import { JobAttributes, JobInstance, JobMethods } from './job-interface'
let Job: Sequelize.Model<JobInstance, JobAttributes>
let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+let listForApi: JobMethods.ListForApi
+let toFormattedJSON: JobMethods.ToFormattedJSON
export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Job = sequelize.define<JobInstance, JobAttributes>('Job',
}
)
- const classMethods = [ listWithLimitByCategory ]
- addMethodsToModel(Job, classMethods)
+ const classMethods = [
+ listWithLimitByCategory,
+ listForApi
+ ]
+ const instanceMethods = [
+ toFormattedJSON
+ ]
+ addMethodsToModel(Job, classMethods, instanceMethods)
return Job
}
+toFormattedJSON = function (this: JobInstance) {
+ return {
+ id: this.id,
+ state: this.state,
+ category: this.category,
+ handlerName: this.handlerName,
+ handlerInputData: this.handlerInputData,
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt
+ }
+}
+
// ---------------------------------------------------------------------------
listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
return Job.findAll(query)
}
+
+listForApi = function (start: number, count: number, sort: string) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: [ getSort(sort) ]
+ }
+
+ return Job.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+}
// Order of the tests we want to execute
import './follows'
+import './jobs'
import './users'
import './services'
import './videos'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+import * as request from 'supertest'
+
+import { createUser, flushTests, getUserAccessToken, killallServers, runServer, ServerInfo, setAccessTokensToServers } from '../../utils'
+
+describe('Test jobs API validators', function () {
+ const path = '/api/v1/jobs/'
+ let server: ServerInfo
+ let userAccessToken = ''
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(120000)
+
+ await flushTests()
+
+ server = await runServer(1)
+
+ await setAccessTokensToServers([ server ])
+
+ const user = {
+ username: 'user1',
+ password: 'my super password'
+ }
+ await createUser(server.url, server.accessToken, user.username, user.password)
+ userAccessToken = await getUserAccessToken(server, user)
+ })
+
+ describe('When listing jobs', function () {
+ it('Should fail with a bad start pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ start: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with a bad count pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ count: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with an incorrect sort', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ sort: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with a non authenticated user', async function () {
+ await request(server.url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .expect(401)
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await request(server.url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + userAccessToken)
+ .expect(403)
+ })
+ })
+
+ after(async function () {
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
import './video-transcoder'
import './multiple-servers'
import './follows'
+import './jobs'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushTests, killallServers, ServerInfo, setAccessTokensToServers, wait } from '../utils'
+import { doubleFollow } from '../utils/follows'
+import { getJobsList, getJobsListPaginationAndSort } from '../utils/jobs'
+import { flushAndRunMultipleServers } from '../utils/servers'
+import { uploadVideo } from '../utils/videos'
+import { dateIsValid } from '../utils/miscs'
+
+const expect = chai.expect
+
+describe('Test jobs', function () {
+ let servers: ServerInfo[]
+
+ before(async function () {
+ this.timeout(30000)
+
+ servers = await flushAndRunMultipleServers(2)
+
+ await setAccessTokensToServers(servers)
+
+ // Server 1 and server 2 follow each other
+ await doubleFollow(servers[0], servers[1])
+ })
+
+ it('Should create some jobs', async function () {
+ this.timeout(30000)
+
+ await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
+ await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
+
+ await wait(15000)
+ })
+
+ it('Should list jobs', async function () {
+ const res = await getJobsList(servers[1].url, servers[1].accessToken)
+ expect(res.body.total).to.be.above(2)
+ expect(res.body.data).to.have.length.above(2)
+ })
+
+ it('Should list jobs with sort and pagination', async function () {
+ const res = await getJobsListPaginationAndSort(servers[1].url, servers[1].accessToken, 4, 1, 'createdAt')
+ expect(res.body.total).to.be.above(2)
+ expect(res.body.data).to.have.lengthOf(1)
+
+ const job = res.body.data[0]
+ expect(job.state).to.equal('success')
+ expect(job.category).to.equal('transcoding')
+ expect(job.handlerName).to.have.length.above(3)
+ expect(dateIsValid(job.createdAt)).to.be.true
+ expect(dateIsValid(job.updatedAt)).to.be.true
+ })
+
+ after(async function () {
+ killallServers(servers)
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
])
// Wait request propagation
- await wait(20000)
+ await wait(10000)
return true
}
--- /dev/null
+import * as request from 'supertest'
+
+function getJobsList (url: string, accessToken: string) {
+ const path = '/api/v1/jobs'
+
+ return request(url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+function getJobsListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) {
+ const path = '/api/v1/jobs'
+
+ return request(url)
+ .get(path)
+ .query({ start })
+ .query({ count })
+ .query({ sort })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ getJobsList,
+ getJobsListPaginationAndSort
+}
export type JobState = 'pending' | 'processing' | 'error' | 'success'
export type JobCategory = 'transcoding' | 'activitypub-http'
+
+export interface Job {
+ id: number
+ state: JobState
+ category: JobCategory
+ handlerName: string
+ handlerInputData: any
+ createdAt: Date
+ updatedAt: Date
+}
MANAGE_SERVER_FOLLOW,
MANAGE_VIDEO_ABUSES,
MANAGE_VIDEO_BLACKLIST,
+ MANAGE_JOBS,
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL
}
import { UserRight } from './user-right.enum'
+import user from '../../../server/models/account/user'
// Keep the order
export enum UserRole {