mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +02:00
Server: Allow enabling and disabling tasks
This commit is contained in:
parent
cb4cf92206
commit
1379c9c706
Binary file not shown.
@ -285,18 +285,18 @@ async function main() {
|
||||
appLogger().info('Connection check:', connectionCheckLogInfo);
|
||||
const ctx = app.context as AppContext;
|
||||
|
||||
await setupAppContext(ctx, env, connectionCheck.connection, appLogger);
|
||||
|
||||
await initializeJoplinUtils(config(), ctx.joplinBase.models, ctx.joplinBase.services.mustache);
|
||||
|
||||
if (config().database.autoMigration) {
|
||||
appLogger().info('Auto-migrating database...');
|
||||
await migrateLatest(ctx.joplinBase.db);
|
||||
appLogger().info('Latest migration:', await latestMigration(ctx.joplinBase.db));
|
||||
await migrateLatest(connectionCheck.connection);
|
||||
appLogger().info('Latest migration:', await latestMigration(connectionCheck.connection));
|
||||
} else {
|
||||
appLogger().info('Skipped database auto-migration.');
|
||||
}
|
||||
|
||||
await setupAppContext(ctx, env, connectionCheck.connection, appLogger);
|
||||
|
||||
await initializeJoplinUtils(config(), ctx.joplinBase.models, ctx.joplinBase.services.mustache);
|
||||
|
||||
appLogger().info('Performing main storage check...');
|
||||
appLogger().info(await storageConnectionCheck(config().storageDriver, ctx.joplinBase.db, ctx.joplinBase.models));
|
||||
|
||||
|
17
packages/server/src/migrations/20221020143305_task_states.ts
Normal file
17
packages/server/src/migrations/20221020143305_task_states.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('task_states', (table: Knex.CreateTableBuilder) => {
|
||||
table.increments('id').unique().primary().notNullable();
|
||||
table.integer('task_id').unique().notNullable();
|
||||
table.specificType('running', 'smallint').defaultTo(0).notNullable();
|
||||
table.specificType('enabled', 'smallint').defaultTo(1).notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('task_states');
|
||||
}
|
@ -357,7 +357,7 @@ export default abstract class BaseModel<T> {
|
||||
return toSave;
|
||||
}
|
||||
|
||||
public async loadByIds(ids: string[], options: LoadOptions = {}): Promise<T[]> {
|
||||
public async loadByIds(ids: string[] | number[], options: LoadOptions = {}): Promise<T[]> {
|
||||
if (!ids.length) return [];
|
||||
ids = unique(ids);
|
||||
return this.db(this.tableName).select(options.fields || this.defaultFields).whereIn('id', ids);
|
||||
|
56
packages/server/src/models/TaskStateModel.ts
Normal file
56
packages/server/src/models/TaskStateModel.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { TaskId, TaskState } from '../services/database/types';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export default class TaskStateModel extends BaseModel<TaskState> {
|
||||
|
||||
public get tableName(): string {
|
||||
return 'task_states';
|
||||
}
|
||||
|
||||
protected hasUuid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async loadByTaskId(taskId: TaskId): Promise<TaskState> {
|
||||
return this.db(this.tableName).where('task_id', '=', taskId).first();
|
||||
}
|
||||
|
||||
public async loadByTaskIds(taskIds: TaskId[]): Promise<TaskState[]> {
|
||||
return this.db(this.tableName).whereIn('task_id', taskIds);
|
||||
}
|
||||
|
||||
public async init(taskId: TaskId) {
|
||||
const taskState: TaskState = await this.loadByTaskId(taskId);
|
||||
if (taskState) return taskState;
|
||||
|
||||
return this.save({
|
||||
task_id: taskId,
|
||||
enabled: 1,
|
||||
running: 0,
|
||||
});
|
||||
}
|
||||
|
||||
public async start(taskId: TaskId) {
|
||||
const state = await this.loadByTaskId(taskId);
|
||||
if (state.running) throw new Error(`Task is already running: ${taskId}`);
|
||||
await this.save({ id: state.id, running: 1 });
|
||||
}
|
||||
|
||||
public async stop(taskId: TaskId) {
|
||||
const state = await this.loadByTaskId(taskId);
|
||||
if (!state.running) throw new Error(`Task is not running: ${taskId}`);
|
||||
await this.save({ id: state.id, running: 0 });
|
||||
}
|
||||
|
||||
public async enable(taskId: TaskId, enabled: boolean = true) {
|
||||
const state = await this.loadByTaskId(taskId);
|
||||
if (state.enabled && enabled) throw new Error(`Task is already enabled: ${taskId}`);
|
||||
if (!state.enabled && !enabled) throw new Error(`Task is already disabled: ${taskId}`);
|
||||
await this.save({ id: state.id, enabled: enabled ? 1 : 0 });
|
||||
}
|
||||
|
||||
public async disable(taskId: TaskId) {
|
||||
await this.enable(taskId, false);
|
||||
}
|
||||
|
||||
}
|
@ -76,6 +76,7 @@ import LockModel from './LockModel';
|
||||
import StorageModel from './StorageModel';
|
||||
import UserDeletionModel from './UserDeletionModel';
|
||||
import BackupItemModel from './BackupItemModel';
|
||||
import TaskStateModel from './TaskStateModel';
|
||||
|
||||
export type NewModelFactoryHandler = (db: DbConnection)=> Models;
|
||||
|
||||
@ -175,6 +176,10 @@ export class Models {
|
||||
return new BackupItemModel(this.db_, this.newModelFactory, this.config_);
|
||||
}
|
||||
|
||||
public taskState() {
|
||||
return new TaskStateModel(this.db_, this.newModelFactory, this.config_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function newModelFactory(db: DbConnection, config: Config): Models {
|
||||
|
@ -11,44 +11,63 @@ import { formatDateTime } from '../../utils/time';
|
||||
import { createCsrfTag } from '../../utils/csrf';
|
||||
import { RunType } from '../../services/TaskService';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { NotificationLevel } from '../../services/database/types';
|
||||
import { NotificationLevel, TaskId } from '../../services/database/types';
|
||||
const prettyCron = require('prettycron');
|
||||
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.post('admin/tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
interface FormFields {
|
||||
startTaskButton: string;
|
||||
enableTaskButton: string;
|
||||
disableTaskButton: string;
|
||||
}
|
||||
|
||||
const user = ctx.joplin.owner;
|
||||
if (!user.is_admin) throw new ErrorForbidden();
|
||||
|
||||
const taskService = ctx.joplin.services.tasks;
|
||||
const fields: any = await bodyFields(ctx.req);
|
||||
const fields = await bodyFields<FormFields>(ctx.req);
|
||||
|
||||
const taskIds: TaskId[] = [];
|
||||
|
||||
for (const k of Object.keys(fields)) {
|
||||
if (k.startsWith('checkbox_')) {
|
||||
taskIds.push(Number(k.substr(9)));
|
||||
}
|
||||
}
|
||||
|
||||
const errors: Error[] = [];
|
||||
|
||||
if (fields.startTaskButton) {
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const k of Object.keys(fields)) {
|
||||
if (k.startsWith('checkbox_')) {
|
||||
const taskId = Number(k.substr(9));
|
||||
try {
|
||||
void taskService.runTask(taskId, RunType.Manual);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
void taskService.runTask(taskId, RunType.Manual);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
await ctx.joplin.models.notification().add(
|
||||
user.id,
|
||||
NotificationKey.Any,
|
||||
NotificationLevel.Error,
|
||||
`Some tasks could not be started: ${errors.join('. ')}`
|
||||
);
|
||||
} else if (fields.enableTaskButton || fields.disableTaskButton) {
|
||||
for (const taskId of taskIds) {
|
||||
try {
|
||||
await taskService.enableTask(taskId, !!fields.enableTaskButton);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ErrorBadRequest('Invalid action');
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
await ctx.joplin.models.notification().add(
|
||||
user.id,
|
||||
NotificationKey.Any,
|
||||
NotificationLevel.Error,
|
||||
`Some operations could not be performed: ${errors.join('. ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return redirect(ctx, makeUrl(UrlType.Tasks));
|
||||
});
|
||||
|
||||
@ -57,11 +76,12 @@ router.get('admin/tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
if (!user.is_admin) throw new ErrorForbidden();
|
||||
|
||||
const taskService = ctx.joplin.services.tasks;
|
||||
const states = await ctx.joplin.models.taskState().loadByTaskIds(taskService.taskIds);
|
||||
|
||||
const taskRows: Row[] = [];
|
||||
for (const [taskIdString, task] of Object.entries(taskService.tasks)) {
|
||||
const taskId = Number(taskIdString);
|
||||
const state = taskService.taskState(taskId);
|
||||
const state = states.find(s => s.task_id === taskId);
|
||||
const events = await taskService.taskLastEvents(taskId);
|
||||
|
||||
taskRows.push({
|
||||
@ -80,6 +100,9 @@ router.get('admin/tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
value: task.schedule,
|
||||
hint: prettyCron.toString(task.schedule),
|
||||
},
|
||||
{
|
||||
value: yesOrNo(state.enabled),
|
||||
},
|
||||
{
|
||||
value: yesOrNo(state.running),
|
||||
},
|
||||
@ -111,6 +134,10 @@ router.get('admin/tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
name: 'schedule',
|
||||
label: 'Schedule',
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
label: 'Enabled',
|
||||
},
|
||||
{
|
||||
name: 'running',
|
||||
label: 'Running',
|
||||
@ -134,7 +161,6 @@ router.get('admin/tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
postUrl: makeUrl(UrlType.Tasks),
|
||||
csrfTag: await createCsrfTag(ctx),
|
||||
},
|
||||
// cssFiles: ['index/tasks'],
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -38,7 +38,7 @@ describe('TaskService', function() {
|
||||
schedule: '',
|
||||
};
|
||||
|
||||
service.registerTask(task);
|
||||
await service.registerTask(task);
|
||||
|
||||
expect(service.tasks[123456]).toBeTruthy();
|
||||
await expectThrow(async () => service.registerTask(task));
|
||||
@ -47,6 +47,8 @@ describe('TaskService', function() {
|
||||
test('should run a task', async function() {
|
||||
const service = newService();
|
||||
|
||||
let taskStarted = false;
|
||||
let waitToFinish = true;
|
||||
let finishTask = false;
|
||||
let taskHasRan = false;
|
||||
|
||||
@ -56,7 +58,11 @@ describe('TaskService', function() {
|
||||
id: taskId,
|
||||
description: '',
|
||||
run: async (_models: Models) => {
|
||||
taskStarted = true;
|
||||
|
||||
const iid = setInterval(() => {
|
||||
if (waitToFinish) return;
|
||||
|
||||
if (finishTask) {
|
||||
clearInterval(iid);
|
||||
taskHasRan = true;
|
||||
@ -66,25 +72,57 @@ describe('TaskService', function() {
|
||||
schedule: '',
|
||||
};
|
||||
|
||||
service.registerTask(task);
|
||||
await service.registerTask(task);
|
||||
|
||||
expect(service.taskState(taskId).running).toBe(false);
|
||||
expect((await service.taskState(taskId)).running).toBe(0);
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
void service.runTask(taskId, RunType.Manual);
|
||||
expect(service.taskState(taskId).running).toBe(true);
|
||||
while (!taskStarted) {
|
||||
await msleep(1);
|
||||
}
|
||||
|
||||
expect((await service.taskState(taskId)).running).toBe(1);
|
||||
waitToFinish = false;
|
||||
|
||||
while (!taskHasRan) {
|
||||
await msleep(1);
|
||||
finishTask = true;
|
||||
}
|
||||
|
||||
expect(service.taskState(taskId).running).toBe(false);
|
||||
expect((await service.taskState(taskId)).running).toBe(0);
|
||||
|
||||
const events = await service.taskLastEvents(taskId);
|
||||
expect(events.taskStarted.created_time).toBeGreaterThanOrEqual(startTime.getTime());
|
||||
expect(events.taskCompleted.created_time).toBeGreaterThan(startTime.getTime());
|
||||
});
|
||||
|
||||
test('should not run if task is disabled', async function() {
|
||||
const service = newService();
|
||||
|
||||
let taskHasRan = false;
|
||||
|
||||
const taskId = 123456;
|
||||
|
||||
const task: Task = {
|
||||
id: taskId,
|
||||
description: '',
|
||||
run: async (_models: Models) => {
|
||||
taskHasRan = true;
|
||||
},
|
||||
schedule: '',
|
||||
};
|
||||
|
||||
await service.registerTask(task);
|
||||
|
||||
await service.runTask(taskId, RunType.Manual);
|
||||
expect(taskHasRan).toBe(true);
|
||||
|
||||
taskHasRan = false;
|
||||
await models().taskState().disable(task.id);
|
||||
await service.runTask(taskId, RunType.Manual);
|
||||
expect(taskHasRan).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -2,25 +2,14 @@ import Logger from '@joplin/lib/Logger';
|
||||
import { Models } from '../models/factory';
|
||||
import { Config, Env } from '../utils/types';
|
||||
import BaseService from './BaseService';
|
||||
import { Event, EventType } from './database/types';
|
||||
import { Event, EventType, TaskId, TaskState } from './database/types';
|
||||
import { Services } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ErrorNotFound } from '../utils/errors';
|
||||
const cron = require('node-cron');
|
||||
|
||||
const logger = Logger.create('TaskService');
|
||||
|
||||
export enum TaskId {
|
||||
DeleteExpiredTokens = 1,
|
||||
UpdateTotalSizes = 2,
|
||||
HandleOversizedAccounts = 3,
|
||||
HandleBetaUserEmails = 4,
|
||||
HandleFailedPaymentSubscriptions = 5,
|
||||
DeleteExpiredSessions = 6,
|
||||
CompressOldChanges = 7,
|
||||
ProcessUserDeletions = 8,
|
||||
AutoAddDisabledAccountsForDeletion = 9,
|
||||
}
|
||||
|
||||
export enum RunType {
|
||||
Scheduled = 1,
|
||||
Manual = 2,
|
||||
@ -60,14 +49,6 @@ export interface Task {
|
||||
|
||||
export type Tasks = Record<number, Task>;
|
||||
|
||||
interface TaskState {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
const defaultTaskState: TaskState = {
|
||||
running: false,
|
||||
};
|
||||
|
||||
interface TaskEvents {
|
||||
taskStarted: Event;
|
||||
taskCompleted: Event;
|
||||
@ -76,7 +57,6 @@ interface TaskEvents {
|
||||
export default class TaskService extends BaseService {
|
||||
|
||||
private tasks_: Tasks = {};
|
||||
private taskStates_: Record<number, TaskState> = {};
|
||||
private services_: Services;
|
||||
|
||||
public constructor(env: Env, models: Models, config: Config, services: Services) {
|
||||
@ -84,23 +64,32 @@ export default class TaskService extends BaseService {
|
||||
this.services_ = services;
|
||||
}
|
||||
|
||||
public registerTask(task: Task) {
|
||||
public async registerTask(task: Task) {
|
||||
if (this.tasks_[task.id]) throw new Error(`Already a task with this ID: ${task.id}`);
|
||||
this.tasks_[task.id] = task;
|
||||
this.taskStates_[task.id] = { ...defaultTaskState };
|
||||
await this.models.taskState().init(task.id);
|
||||
}
|
||||
|
||||
public registerTasks(tasks: Task[]) {
|
||||
for (const task of tasks) this.registerTask(task);
|
||||
public async registerTasks(tasks: Task[]) {
|
||||
for (const task of tasks) await this.registerTask(task);
|
||||
}
|
||||
|
||||
public get tasks(): Tasks {
|
||||
return this.tasks_;
|
||||
}
|
||||
|
||||
public taskState(id: TaskId): TaskState {
|
||||
if (!this.taskStates_[id]) throw new Error(`No such task: ${id}`);
|
||||
return this.taskStates_[id];
|
||||
public get taskIds(): TaskId[] {
|
||||
return Object.keys(this.tasks_).map(s => Number(s));
|
||||
}
|
||||
|
||||
public async taskStates(ids: TaskId[]): Promise<TaskState[]> {
|
||||
return this.models.taskState().loadByTaskIds(ids);
|
||||
}
|
||||
|
||||
public async taskState(id: TaskId): Promise<TaskState> {
|
||||
const r = await this.taskStates([id]);
|
||||
if (!r.length) throw new ErrorNotFound(`No such task: ${id}`);
|
||||
return r[0];
|
||||
}
|
||||
|
||||
public async taskLastEvents(id: TaskId): Promise<TaskEvents> {
|
||||
@ -122,16 +111,16 @@ export default class TaskService extends BaseService {
|
||||
|
||||
public async runTask(id: TaskId, runType: RunType) {
|
||||
const displayString = this.taskDisplayString(id);
|
||||
const state = this.taskState(id);
|
||||
if (state.running) throw new Error(`Already running: ${displayString}`);
|
||||
const taskState = await this.models.taskState().loadByTaskId(id);
|
||||
if (!taskState.enabled) {
|
||||
logger.info(`Not running ${displayString} because the tasks is disabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.models.taskState().start(id);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
this.taskStates_[id] = {
|
||||
...this.taskStates_[id],
|
||||
running: true,
|
||||
};
|
||||
|
||||
await this.models.event().create(EventType.TaskStarted, id.toString());
|
||||
|
||||
try {
|
||||
@ -141,16 +130,16 @@ export default class TaskService extends BaseService {
|
||||
logger.error(`On ${displayString}`, error);
|
||||
}
|
||||
|
||||
this.taskStates_[id] = {
|
||||
...this.taskStates_[id],
|
||||
running: false,
|
||||
};
|
||||
|
||||
await this.models.taskState().stop(id);
|
||||
await this.models.event().create(EventType.TaskCompleted, id.toString());
|
||||
|
||||
logger.info(`Completed ${this.taskDisplayString(id)} in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
|
||||
public async enableTask(taskId: TaskId, enabled: boolean = true) {
|
||||
await this.models.taskState().enable(taskId, enabled);
|
||||
}
|
||||
|
||||
public async runInBackground() {
|
||||
for (const [taskId, task] of Object.entries(this.tasks_)) {
|
||||
if (!task.schedule) continue;
|
||||
|
@ -111,6 +111,20 @@ interface DatabaseTables {
|
||||
[key: string]: DatabaseTable;
|
||||
}
|
||||
|
||||
export enum TaskId {
|
||||
// Don't re-use any of these numbers, always add to it, as the ID is used in
|
||||
// the database
|
||||
DeleteExpiredTokens = 1,
|
||||
UpdateTotalSizes = 2,
|
||||
HandleOversizedAccounts = 3,
|
||||
HandleBetaUserEmails = 4,
|
||||
HandleFailedPaymentSubscriptions = 5,
|
||||
DeleteExpiredSessions = 6,
|
||||
CompressOldChanges = 7,
|
||||
ProcessUserDeletions = 8,
|
||||
AutoAddDisabledAccountsForDeletion = 9,
|
||||
}
|
||||
|
||||
// AUTO-GENERATED-TYPES
|
||||
// Auto-generated using `yarn run generate-types`
|
||||
export interface Session extends WithDates, WithUuid {
|
||||
@ -300,6 +314,13 @@ export interface BackupItem extends WithCreatedDate {
|
||||
content?: Buffer;
|
||||
}
|
||||
|
||||
export interface TaskState extends WithDates {
|
||||
id?: number;
|
||||
task_id?: TaskId;
|
||||
running?: number;
|
||||
enabled?: number;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
sessions: {
|
||||
id: { type: 'string' },
|
||||
@ -504,5 +525,13 @@ export const databaseSchema: DatabaseTables = {
|
||||
content: { type: 'any' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
task_states: {
|
||||
id: { type: 'number' },
|
||||
task_id: { type: 'number' },
|
||||
running: { type: 'number' },
|
||||
enabled: { type: 'number' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
@ -36,6 +36,7 @@ const config = {
|
||||
'main.users': 'WithDates, WithUuid',
|
||||
'main.events': 'WithUuid',
|
||||
'main.user_deletions': 'WithDates',
|
||||
'main.task_states': 'WithDates',
|
||||
'main.backup_items': 'WithCreatedDate',
|
||||
},
|
||||
};
|
||||
@ -65,6 +66,7 @@ const propertyTypes: Record<string, string> = {
|
||||
'user_deletions.scheduled_time': 'number',
|
||||
'users.disabled_time': 'number',
|
||||
'backup_items.content': 'Buffer',
|
||||
'task_states.task_id': 'TaskId',
|
||||
};
|
||||
|
||||
function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string): void {
|
||||
@ -158,6 +160,8 @@ async function main() {
|
||||
content += `export const databaseSchema: DatabaseTables = {\n${tableStrings.join('\n')}\n};`;
|
||||
|
||||
insertContentIntoFile(dbFilePath, fileReplaceWithinMarker, fileReplaceWithinMarker, content);
|
||||
|
||||
console.info(`Types have been updated in ${dbFilePath}`);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
|
@ -20,7 +20,7 @@ async function setupServices(env: Env, models: Models, config: Config): Promise<
|
||||
tasks: null,
|
||||
};
|
||||
|
||||
output.tasks = setupTaskService(env, models, config, output),
|
||||
output.tasks = await setupTaskService(env, models, config, output),
|
||||
|
||||
await output.mustache.loadPartials();
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Models } from '../models/factory';
|
||||
import TaskService, { Task, TaskId, taskIdToLabel } from '../services/TaskService';
|
||||
import { TaskId } from '../services/database/types';
|
||||
import TaskService, { Task, taskIdToLabel } from '../services/TaskService';
|
||||
import { Services } from '../services/types';
|
||||
import { Config, Env } from './types';
|
||||
|
||||
export default function(env: Env, models: Models, config: Config, services: Services): TaskService {
|
||||
export default async function(env: Env, models: Models, config: Config, services: Services): Promise<TaskService> {
|
||||
const taskService = new TaskService(env, models, config, services);
|
||||
|
||||
let tasks: Task[] = [
|
||||
@ -80,7 +81,7 @@ export default function(env: Env, models: Models, config: Config, services: Serv
|
||||
]);
|
||||
}
|
||||
|
||||
taskService.registerTasks(tasks);
|
||||
await taskService.registerTasks(tasks);
|
||||
|
||||
return taskService;
|
||||
}
|
||||
|
@ -7,5 +7,7 @@
|
||||
|
||||
<div class="block">
|
||||
<input class="button is-link" type="submit" value="Start selected tasks" name="startTaskButton"/>
|
||||
<input class="button is-link" type="submit" value="Enabled selected tasks" name="enableTaskButton"/>
|
||||
<input class="button is-link" type="submit" value="Disable selected tasks" name="disableTaskButton"/>
|
||||
</div>
|
||||
</form>
|
Loading…
x
Reference in New Issue
Block a user