1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

improving job ui and job handling

This commit is contained in:
Patrik J. Braun 2019-12-30 15:49:56 +01:00
parent c4ca9e5857
commit 4c507f286d
26 changed files with 312 additions and 413 deletions

View File

@ -50,11 +50,13 @@ export class AdminMWs {
}
}
public static async startJob(req: Request, res: Response, next: NextFunction) {
public static startJob(soloRun: boolean) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const id = req.params.id;
const JobConfig: any = req.body.config;
await ObjectManagers.getInstance().JobManager.run(id, JobConfig);
await ObjectManagers.getInstance().JobManager.run(id, JobConfig, soloRun);
req.resultPipe = 'ok';
return next();
} catch (err) {
@ -63,6 +65,7 @@ export class AdminMWs {
}
return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err));
}
};
}
public static stopJob(req: Request, res: Response, next: NextFunction) {
@ -102,15 +105,4 @@ export class AdminMWs {
return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err));
}
}
public static getJobLastRuns(req: Request, res: Response, next: NextFunction) {
try {
req.resultPipe = ObjectManagers.getInstance().JobManager.getJobLastRuns();
return next();
} catch (err) {
if (err instanceof Error) {
return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + err.toString(), err));
}
return next(new ErrorDTO(ErrorCodes.JOB_ERROR, 'Job error: ' + JSON.stringify(err, null, ' '), err));
}
}
}

View File

@ -3,18 +3,17 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO';
export interface IJobManager {
run(jobId: string, config: any): Promise<void>;
run(jobId: string, config: any, soloRun: boolean): Promise<void>;
stop(jobId: string): void;
getProgresses(): { [key: string]: JobProgressDTO };
getAvailableJobs(): JobDTO[];
stopSchedules(): void;
runSchedules(): void;
getJobLastRuns(): { [key: string]: JobProgressDTO };
}

View File

@ -23,18 +23,14 @@ export class JobManager implements IJobManager, IJobListener {
}
getProgresses(): { [id: string]: JobProgressDTO } {
return this.progressManager.Running;
return this.progressManager.Progresses;
}
getJobLastRuns(): { [key: string]: JobProgressDTO } {
return this.progressManager.Finished;
}
async run<T>(jobName: string, config: T): Promise<void> {
async run<T>(jobName: string, config: T, soloRun: boolean): Promise<void> {
const t = this.findJob(jobName);
if (t) {
t.JobListener = this;
await t.start(config);
await t.start(config, soloRun);
} else {
Logger.warn(LOG_TAG, 'cannot find job to start:' + jobName);
}
@ -54,8 +50,9 @@ export class JobManager implements IJobManager, IJobListener {
};
onJobFinished = async (job: IJob<any>, state: JobProgressStates): Promise<void> => {
if (state !== JobProgressStates.finished) { // if it was not finished peacefully, do not start the next one
onJobFinished = async (job: IJob<any>, state: JobProgressStates, soloRun: boolean): Promise<void> => {
// if it was not finished peacefully or was a soloRun, do not start the next one
if (state !== JobProgressStates.finished || soloRun === true) {
return;
}
const sch = Config.Server.Jobs.scheduled.find(s => s.jobName === job.Name);
@ -64,7 +61,7 @@ export class JobManager implements IJobManager, IJobListener {
(<AfterJobTrigger>s.trigger).afterScheduleName === sch.name);
for (let i = 0; i < children.length; ++i) {
try {
await this.run(children[i].jobName, children[i].config);
await this.run(children[i].jobName, children[i].config, false);
} catch (e) {
NotificationManager.warning('Job running error:' + children[i].name, e.toString());
}
@ -99,7 +96,7 @@ export class JobManager implements IJobManager, IJobListener {
const timer: NodeJS.Timeout = setTimeout(async () => {
this.timers = this.timers.filter(t => t.timer !== timer);
await this.run(schedule.jobName, schedule.config);
await this.run(schedule.jobName, schedule.config, false);
this.runSchedule(schedule);
}, nextDate.getTime() - Date.now());
this.timers.push({schedule: schedule, timer: timer});

View File

@ -5,13 +5,13 @@ import {Config} from '../../../common/config/private/Config';
import {JobProgressDTO, JobProgressStates} from '../../../common/entities/job/JobProgressDTO';
export class JobProgressManager {
private static readonly VERSION = 1;
db: {
private static readonly VERSION = 2;
private db: {
version: number,
db: { [key: string]: { progress: JobProgressDTO, timestamp: number } }
progresses: { [key: string]: { progress: JobProgressDTO, timestamp: number } }
} = {
version: JobProgressManager.VERSION,
db: {}
progresses: {}
};
private readonly dbPath: string;
private timer: NodeJS.Timeout = null;
@ -21,29 +21,20 @@ export class JobProgressManager {
this.loadDB().catch(console.error);
}
get Running(): { [key: string]: JobProgressDTO } {
get Progresses(): { [key: string]: JobProgressDTO } {
const m: { [key: string]: JobProgressDTO } = {};
for (const key of Object.keys(this.db.db)) {
if (this.db.db[key].progress.state === JobProgressStates.running) {
m[key] = this.db.db[key].progress;
for (const key of Object.keys(this.db.progresses)) {
m[key] = this.db.progresses[key].progress;
if (this.db.progresses[key].progress.state === JobProgressStates.running) {
m[key].time.end = Date.now();
}
}
return m;
}
get Finished(): { [key: string]: JobProgressDTO } {
const m: { [key: string]: JobProgressDTO } = {};
for (const key of Object.keys(this.db.db)) {
if (this.db.db[key].progress.state !== JobProgressStates.running) {
m[key] = this.db.db[key].progress;
}
}
return m;
}
onJobProgressUpdate(progress: JobProgressDTO) {
this.db.db[progress.HashName] = {progress: progress, timestamp: Date.now()};
this.db.progresses[progress.HashName] = {progress: progress, timestamp: Date.now()};
this.delayedSave();
}
@ -60,19 +51,20 @@ export class JobProgressManager {
}
this.db = db;
while (Object.keys(this.db.db).length > Config.Server.Jobs.maxSavedProgress) {
while (Object.keys(this.db.progresses).length > Config.Server.Jobs.maxSavedProgress) {
let min: string = null;
for (const key of Object.keys(this.db.db)) {
if (min === null || this.db.db[min].timestamp > this.db.db[key].timestamp) {
for (const key of Object.keys(this.db.progresses)) {
if (min === null || this.db.progresses[min].timestamp > this.db.progresses[key].timestamp) {
min = key;
}
}
delete this.db.db[min];
delete this.db.progresses[min];
}
for (const key of Object.keys(this.db.db)) {
if (this.db.db[key].progress.state === JobProgressStates.running) {
this.db.db[key].progress.state = JobProgressStates.interrupted;
for (const key of Object.keys(this.db.progresses)) {
if (this.db.progresses[key].progress.state === JobProgressStates.running ||
this.db.progresses[key].progress.state === JobProgressStates.cancelling) {
this.db.progresses[key].progress.state = JobProgressStates.interrupted;
}
}
}
@ -88,7 +80,7 @@ export class JobProgressManager {
this.timer = setTimeout(async () => {
this.saveDB().catch(console.error);
this.timer = null;
}, 1000);
}, 5000);
}
}

View File

@ -8,7 +8,7 @@ export interface IJob<T> extends JobDTO {
Progress: JobProgress;
JobListener: IJobListener;
start(config: T): Promise<void>;
start(config: T, soloRun?: boolean): Promise<void>;
cancel(): void;

View File

@ -3,7 +3,7 @@ import {IJob} from './IJob';
import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO';
export interface IJobListener {
onJobFinished(job: IJob<any>, state: JobProgressStates): void;
onJobFinished(job: IJob<any>, state: JobProgressStates, soloRun: boolean): void;
onProgressUpdate(progress: JobProgress): void;
}

View File

@ -16,6 +16,7 @@ export abstract class Job<T = void> implements IJob<T> {
protected prResolve: () => void;
protected IsInstant = false;
private jobListener: IJobListener;
private soloRun: boolean;
public set JobListener(value: IJobListener) {
this.jobListener = value;
@ -32,13 +33,15 @@ export abstract class Job<T = void> implements IJob<T> {
return this.progress;
}
public get CanRun() {
return this.Progress == null && this.Supported;
protected get InProgress(): boolean {
return this.Progress !== null && (this.Progress.State === JobProgressStates.running ||
this.Progress.State === JobProgressStates.cancelling);
}
public start(config: T): Promise<void> {
if (this.CanRun) {
Logger.info(LOG_TAG, 'Running job: ' + this.Name);
public start(config: T, soloRun = false): Promise<void> {
if (this.InProgress === false && this.Supported === true) {
Logger.info(LOG_TAG, 'Running job ' + (soloRun === true ? 'solo' : '') + ': ' + this.Name);
this.soloRun = soloRun;
this.config = config;
this.progress = new JobProgress(JobDTO.getHashName(this.Name, this.config));
this.progress.OnChange = this.jobListener.onProgressUpdate;
@ -52,12 +55,15 @@ export abstract class Job<T = void> implements IJob<T> {
}
return pr;
} else {
Logger.info(LOG_TAG, 'Job already running: ' + this.Name);
return Promise.reject();
Logger.info(LOG_TAG, 'Job already running or not supported: ' + this.Name);
return Promise.reject('Job already running or not supported: ' + this.Name);
}
}
public cancel(): void {
if (this.InProgress === false) {
return;
}
Logger.info(LOG_TAG, 'Stopping job: ' + this.Name);
this.Progress.State = JobProgressStates.cancelling;
}
@ -69,18 +75,20 @@ export abstract class Job<T = void> implements IJob<T> {
};
}
protected abstract async step(): Promise<boolean>;
protected abstract async init(): Promise<void>;
private onFinish(): void {
if (this.InProgress === false) {
return;
}
if (this.Progress.State === JobProgressStates.running) {
this.Progress.State = JobProgressStates.finished;
}
if (this.Progress.State === JobProgressStates.cancelling) {
} else if (this.Progress.State === JobProgressStates.cancelling) {
this.Progress.State = JobProgressStates.canceled;
}
const finishState = this.Progress.State;
this.progress = null;
if (global.gc) {
@ -90,13 +98,14 @@ export abstract class Job<T = void> implements IJob<T> {
if (this.IsInstant) {
this.prResolve();
}
this.jobListener.onJobFinished(this, finishState);
this.jobListener.onJobFinished(this, finishState, this.soloRun);
}
private run() {
process.nextTick(async () => {
try {
if (this.Progress == null || this.Progress.State !== JobProgressStates.running) {
this.onFinish();
return;
}
if (await this.step() === false) { // finished

View File

@ -31,14 +31,14 @@ export class ThumbnailGenerationJob extends FileJob<{ sizes: number[], indexedOn
return true;
}
start(config: { sizes: number[], indexedOnly: boolean }): Promise<void> {
start(config: { sizes: number[], indexedOnly: boolean }, soloRun = false): Promise<void> {
for (let i = 0; i < config.sizes.length; ++i) {
if (Config.Client.Media.Thumbnail.thumbnailSizes.indexOf(config.sizes[i]) === -1) {
throw new Error('unknown thumbnails size: ' + config.sizes[i] + '. Add it to the possible thumbnail sizes.');
}
}
return super.start(config);
return super.start(config, soloRun);
}
protected async filterMediaFiles(files: FileDTO[]): Promise<FileDTO[]> {

View File

@ -43,16 +43,16 @@ export class AdminRouter {
AdminMWs.getJobProgresses,
RenderingMWs.renderResult
);
app.get('/api/admin/jobs/scheduled/lastRun',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.getJobLastRuns,
RenderingMWs.renderResult
);
app.post('/api/admin/jobs/scheduled/:id/start',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.startJob,
AdminMWs.startJob(false),
RenderingMWs.renderResult
);
app.post('/api/admin/jobs/scheduled/:id/soloStart',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.startJob(true),
RenderingMWs.renderResult
);
app.post('/api/admin/jobs/scheduled/:id/stop',

View File

@ -89,6 +89,7 @@ import {JobProgressComponent} from './ui/settings/jobs/progress/job-progress.set
import {JobsSettingsComponent} from './ui/settings/jobs/jobs.settings.component';
import {ScheduledJobsService} from './ui/settings/scheduled-jobs.service';
import {BackendtextService} from './model/backendtext.service';
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
@Injectable()
@ -200,6 +201,8 @@ export function translationsFactory(locale: string) {
IndexingSettingsComponent,
JobProgressComponent,
JobsSettingsComponent,
JobButtonComponent,
// Pipes
StringifyRole,
IconizeSortingMethod,

View File

@ -103,33 +103,26 @@
<ng-container i18n>If you add a new folder to your gallery, the site indexes it automatically.</ng-container>&nbsp;
<ng-container i18n>If you would like to trigger indexing manually, click index button.</ng-container>
<br/>
(<ng-container i18n>Note: search only works among the indexed directories</ng-container>)
(
<ng-container i18n>Note: search only works among the indexed directories</ng-container>
)
</div>
<div *ngIf="Progress != null">
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</div>
<app-settings-job-button #indexingButton
[soloRun]="true"
(error)="error=$event"
[jobName]="indexingJobName"></app-settings-job-button>
<button class="btn btn-success ml-0"
*ngIf="Progress == null"
[disabled]="inProgress"
title="Indexes the folders"
i18n-title
(click)="index()">
<ng-container i18n>Index folders now</ng-container>
<span class="oi oi-media-play ml-2"></span>
</button>
<button class="btn btn-secondary ml-0"
*ngIf="Progress != null"
[disabled]="inProgress || Progress.state !== JobProgressStates.running"
(click)="cancelIndexing()" i18n>Cancel converting
</button>
<button class="btn btn-danger ml-2"
[disabled]="inProgress"
(click)="resetDatabase()" i18n>Reset Indexes
</button>
<app-settings-job-button class="ml-2"
danger="true"
[soloRun]="true"
(error)="error=$event"
[disabled]="indexingButton.Running"
[jobName]="resetJobName"></app-settings-job-button>
<hr/>

View File

@ -25,6 +25,8 @@ export class IndexingSettingsComponent extends SettingsComponent<ServerConfig.In
types: { key: number; value: string }[] = [];
JobProgressStates = JobProgressStates;
readonly indexingJobName = DefaultsJobs[DefaultsJobs.Indexing];
readonly resetJobName = DefaultsJobs[DefaultsJobs['Database Reset']];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,

View File

@ -0,0 +1,3 @@
:host{
display: inline-block;
}

View File

@ -0,0 +1,16 @@
<button class="btn {{danger ? 'btn-danger': 'btn-success'}}"
title="Trigger job run manually"
i18n-title
*ngIf="!Running"
[disabled]="disabled || jobsService.jobStartingStopping[jobName]"
(click)="start();">
<span class="mr-2" *ngIf="!shortName">Run {{jobName}} now</span>
<span class="oi oi-media-play"></span>
</button>
<button class="btn btn-secondary"
*ngIf="Running"
[disabled]="disabled || jobsService.jobStartingStopping[jobName] || Progress.state !== JobProgressStates.running"
(click)="stop();">
<span class="oi oi-media-stop"></span>
<span class="ml-2" *ngIf="!shortName">Cancel {{jobName}}</span>
</button>

View File

@ -0,0 +1,73 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {JobProgressStates} from '../../../../../../common/entities/job/JobProgressDTO';
import {ErrorDTO} from '../../../../../../common/entities/Error';
import {ScheduledJobsService} from '../../scheduled-jobs.service';
import {NotificationService} from '../../../../model/notification.service';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {JobDTO} from '../../../../../../common/entities/job/JobDTO';
@Component({
selector: 'app-settings-job-button',
templateUrl: './job-button.settings.component.html',
styleUrls: ['./job-button.settings.component.css']
})
export class JobButtonComponent {
@Input() jobName: string;
@Input() config: any = {};
@Input() shortName = false;
@Input() disabled = false;
@Input() soloRun = false;
@Input() danger = false;
JobProgressStates = JobProgressStates;
@Output() error = new EventEmitter<string>();
constructor(private notification: NotificationService,
public jobsService: ScheduledJobsService,
private i18n: I18n) {
}
public get Running() {
return this.Progress && (this.Progress.state === JobProgressStates.running || this.Progress.state === JobProgressStates.cancelling);
}
get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.config)];
}
public async start() {
this.error.emit('');
try {
await this.jobsService.start(this.jobName, this.config, this.soloRun);
this.notification.info(this.i18n('Job') + ' ' + this.jobName + ' ' + this.i18n('started'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error.emit((<ErrorDTO>err).message);
}
}
return false;
}
public async stop() {
this.error.emit('');
try {
await this.jobsService.stop(this.jobName);
this.notification.info(this.i18n('Job') + ' ' + this.jobName + ' ' + this.i18n('stopped'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error.emit((<ErrorDTO>err).message);
}
}
return false;
}
}

View File

@ -8,9 +8,11 @@
<div *ngFor="let schedule of sortedSchedules() as schedules; let i= index">
<div class="card bg-light {{shouldIdent(schedule,schedules[i-1])? 'ml-4' : ''}}">
<div class="card-header clickable"
(click)="showDetails[schedule.name]=!showDetails[schedule.name]">
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="clickable"
(click)="showDetails[schedule.name]=!showDetails[schedule.name]">
<span class="oi oi-chevron-{{showDetails[schedule.name] ? 'bottom' : 'right'}}"></span>
{{schedule.name}} @<!--
-->
<ng-container [ngSwitch]="schedule.trigger.type">
@ -26,19 +28,14 @@
{{schedule.trigger.afterScheduleName}}
</ng-container>
</ng-container>
</div>
<div>
<button class="btn btn-danger job-control-button" (click)="remove(i)"><span class="oi oi-trash"></span>
</button>
<button class="btn btn-success job-control-button"
*ngIf="!getProgress(schedule)"
[disabled]="disableButtons"
(click)="start(schedule); $event.stopPropagation();"><span class="oi oi-media-play"></span>
</button>
<button class="btn btn-secondary job-control-button"
*ngIf="getProgress(schedule)"
[disabled]="disableButtons || getProgress(schedule).state !== JobProgressStates.running"
(click)="stop(schedule); $event.stopPropagation();"><span class="oi oi-media-stop"></span>
<button class="btn btn-danger job-control-button" (click)="remove(schedule)"><span class="oi oi-trash"></span>
</button>
<app-settings-job-button class="job-control-button ml-2"
(error)="error=$event"
[jobName]="schedule.jobName" [config]="schedule.config"
[shortName]="true"></app-settings-job-button>
</div>
</div>
</div>
@ -120,18 +117,10 @@
</div>
<div class="col-md-3">
<button class="btn btn-success float-right"
*ngIf="!getProgress(schedule)"
[disabled]="disableButtons"
title="Trigger job run manually"
i18n-title
(click)="start(schedule)" i18n>Start now
</button>
<button class="btn btn-secondary float-right"
*ngIf="getProgress(schedule)"
[disabled]="disableButtons || getProgress(schedule).state !== JobProgressStates.running"
(click)="stop(schedule)" i18n>Stop
</button>
<app-settings-job-button class="float-right"
[jobName]="schedule.jobName"
(error)="error=$event"
[config]="schedule.config"></app-settings-job-button>
</div>
</div>
@ -194,8 +183,8 @@
<app-settings-job-progress
class="card-footer bg-transparent"
*ngIf="getProgress(schedule) || getLastRun(schedule)"
[progress]="getProgress(schedule) || getLastRun(schedule)">
*ngIf="getProgress(schedule)"
[progress]="getProgress(schedule)">
</app-settings-job-progress>
</div>

View File

@ -5,7 +5,6 @@ import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {SettingsComponent} from '../_abstract/abstract.settings.component';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {
AfterJobTrigger,
@ -18,7 +17,6 @@ import {
import {Utils} from '../../../../../common/Utils';
import {ServerConfig} from '../../../../../common/config/private/IPrivateConfig';
import {ConfigTemplateEntry} from '../../../../../common/entities/job/JobDTO';
import {Job} from '../../../../../backend/model/jobs/jobs/Job';
import {ModalDirective} from 'ngx-bootstrap/modal';
import {JobProgressDTO, JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
import {BackendtextService} from '../../../model/backendtext.service';
@ -96,46 +94,8 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
this.jobsService.unsubscribeFromProgress();
}
public async start(schedule: JobScheduleDTO) {
this.error = '';
try {
this.disableButtons = true;
await this.jobsService.start(schedule.jobName, schedule.config);
this.notification.info(this.i18n('Job') + ' ' + schedule.jobName + ' ' + this.i18n('started'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
} finally {
this.disableButtons = false;
}
return false;
}
public async stop(schedule: JobScheduleDTO) {
this.error = '';
try {
this.disableButtons = true;
await this.jobsService.stop(schedule.jobName);
this.notification.info(this.i18n('Job') + ' ' + schedule.jobName + ' ' + this.i18n('stopped'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
} finally {
this.disableButtons = false;
}
return false;
}
remove(index: number) {
this.settings.scheduled.splice(index, 1);
remove(schedule: JobScheduleDTO) {
this.settings.scheduled.splice(this.settings.scheduled.indexOf(schedule), 1);
}
jobTypeChanged(schedule: JobScheduleDTO) {
@ -211,18 +171,11 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
this.jobModal.hide();
}
getConfigHash(schedule: JobScheduleDTO): string {
return JSON.stringify(schedule.config);
}
getProgress(schedule: JobScheduleDTO): JobProgressDTO {
return this.jobsService.getProgress(schedule);
}
getLastRun(schedule: JobScheduleDTO): JobProgressDTO {
return this.jobsService.getLastRun(schedule);
}
private getNextRunningDate(sch: JobScheduleDTO, list: JobScheduleDTO[], depth: number = 0): number {
if (depth > list.length) {
return 0;

View File

@ -1,5 +1,4 @@
<div class="row clickable" *ngIf="!IsRunning"
(click)="openModal(template)">
<div class="row" *ngIf="Stopped">
<div class="col-md-2 col-12" i18n>
Last run:
</div>
@ -7,19 +6,24 @@
<span class="oi oi-clock" aria-hidden="true"></span>
{{progress.time.start | date:'medium'}} - {{progress.time.end | date:'mediumTime'}}
</div>
<div class="col-md-3 col-6" title="processed+skipped/all" i18n-title>
<div class="col-md-2 col-4"
title="processed:{{progress.steps.processed}}+ skipped:{{progress.steps.skipped}} / all:{{progress.steps.all}}">
<span class="oi oi-check" aria-hidden="true"></span>
{{progress.steps.processed}}+{{progress.steps.skipped}}/{{progress.steps.all}}
{{progress.steps.processed + progress.steps.skipped}}/{{progress.steps.all}}
</div>
<div class="col-md-3 col-6" title="Status" i18n-title>
<div class="col-md-2 col-4" title="Status" i18n-title>
<span class="oi oi-pulse" aria-hidden="true"></span>
{{JobProgressStates[progress.state]}}
</div>
<div class="col-md-2 col-4">
<button class="btn btn-secondary float-right" (click)="openModal(template)">
<span class="oi oi-resize-both"></span>
</button>
</div>
</div>
<div *ngIf="IsRunning" class="clickable" (click)="openModal(template)">
<div *ngIf="Running">
<div class="form-group row">
<div class="col-md-12">
<div class="input-group form-group">
<input
*ngIf="progress.state === JobProgressStates.running" type="text" class="form-control" disabled
[ngModel]="progress.logs[progress.logs.length-1].comment" name="details">
@ -27,6 +31,10 @@
*ngIf="progress.state === JobProgressStates.cancelling" type="text" class="form-control" disabled
value="Cancelling: {{progress.logs[progress.logs.length-1].comment}}"
i18n-value name="details">
<div class="input-group-append">
<button class="btn btn-secondary" (click)="openModal(template)">
<span class="oi oi-resize-both"></span>
</button>
</div>
</div>
@ -47,7 +55,7 @@
aria-valuemax="100"
style="min-width: 2em;"
[style.width.%]="((progress.steps.processed+progress.steps.skipped)/progress.steps.all)*100">
{{progress.steps.processed}}+{{progress.steps.skipped}}/{{progress.steps.all}}
{{progress.steps.processed + progress.steps.skipped}}/{{progress.steps.all}}
</div>
<div
*ngIf="progress.steps.all === 0"
@ -69,10 +77,12 @@
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-4">Processed: {{progress.steps.processed}}</div>
<div class="col-sm-4">Skipped: {{progress.steps.skipped}}</div>
<div class="col-sm-4">All: {{progress.steps.all}}</div>
<div class="container">
<div class="row justify-content-between">
<div>Processed: {{progress.steps.processed}}</div>
<div>Skipped: {{progress.steps.skipped}}</div>
<div>Left: {{progress.steps.all - progress.steps.skipped - progress.steps.processed}}</div>
<div>All: {{progress.steps.all}}</div>
</div>
<div class="row mt-3 mb-3">
<div class="progress col-12">
@ -95,6 +105,7 @@
</div>
</div>
</div>
</div>
<div class="card bg-light">
<div class="card-header" i18n>
Logs

View File

@ -20,6 +20,9 @@ export class JobProgressComponent implements OnDestroy, OnChanges {
}
get Name(): string {
if (!this.progress) {
return '';
}
return this.progress.HashName;
}
@ -28,11 +31,15 @@ export class JobProgressComponent implements OnDestroy, OnChanges {
return 0;
}
return (this.progress.time.end - this.progress.time.start) /
(this.progress.steps.processed + this.progress.steps.skipped) * this.progress.steps.all;
(this.progress.steps.processed) * (this.progress.steps.all - this.progress.steps.skipped);
}
get IsRunning() {
return this.progress.state === JobProgressStates.running || this.progress.state === JobProgressStates.canceled;
get Running() {
return this.progress && (this.progress.state === JobProgressStates.running || this.progress.state === JobProgressStates.cancelling);
}
get Stopped() {
return this.progress && (this.progress.state !== JobProgressStates.running && this.progress.state !== JobProgressStates.cancelling);
}
get TimeLeft(): number {

View File

@ -100,21 +100,10 @@
</button>
<div [hidden]="!settings.client.Converting.enabled">
<button class="btn btn-success float-left ml-0"
*ngIf="Progress == null"
[disabled]="inProgress"
title="Indexes the folders"
i18n-title
(click)="convertPhoto()">
<ng-container i18n>Convert photos now</ng-container>
<span class="oi oi-media-play ml-2"></span>
</button>
<button class="btn btn-secondary float-left ml-0"
*ngIf="Progress != null"
[disabled]="inProgress || Progress.state !== JobProgressStates.running"
(click)="cancelPhotoConverting()" i18n>Cancel converting
</button>
<app-settings-job-button class="float-left"
[soloRun]="true"
(error)="error=$event"
[jobName]="jobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>

View File

@ -39,6 +39,7 @@ export class PhotoSettingsComponent extends SettingsComponent<{
}
return v;
});
readonly jobName = DefaultsJobs[DefaultsJobs['Photo Converting']];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
@ -61,44 +62,6 @@ export class PhotoSettingsComponent extends SettingsComponent<{
get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs['Photo Converting']])];
}
async convertPhoto() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.start(DefaultsJobs[DefaultsJobs['Photo Converting']]);
this.notification.info(this.i18n('Photo converting started'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
} finally {
this.inProgress = false;
}
return false;
}
async cancelPhotoConverting() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.stop(DefaultsJobs[DefaultsJobs['Photo Converting']]);
this.notification.info(this.i18n('Photo converting interrupted'));
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
} finally {
this.inProgress = false;
}
return false;
}
}

View File

@ -10,24 +10,19 @@ export class ScheduledJobsService {
public progress: BehaviorSubject<{ [key: string]: JobProgressDTO }>;
public lastRuns: BehaviorSubject<{ [key: string]: JobProgressDTO }>;
public onJobFinish: EventEmitter<string> = new EventEmitter<string>();
timer: number = null;
public jobStartingStopping: { [key: string]: boolean } = {};
private subscribers = 0;
constructor(private _networkService: NetworkService) {
this.progress = new BehaviorSubject({});
this.lastRuns = new BehaviorSubject({});
}
getProgress(schedule: JobScheduleDTO): JobProgressDTO {
return this.progress.value[JobDTO.getHashName(schedule.jobName, schedule.config)];
}
getLastRun(schedule: JobScheduleDTO): JobProgressDTO {
return this.lastRuns.value[JobDTO.getHashName(schedule.jobName, schedule.config)];
}
subscribeToProgress(): void {
this.incSubscribers();
}
@ -40,20 +35,24 @@ export class ScheduledJobsService {
return await this.loadProgress();
}
public async start(id: string, config?: any): Promise<void> {
await this._networkService.postJson('/admin/jobs/scheduled/' + id + '/start', {config: config});
public async start(jobName: string, config?: any, soloStart: boolean = false): Promise<void> {
this.jobStartingStopping[jobName] = true;
await this._networkService.postJson('/admin/jobs/scheduled/' + jobName + '/' + (soloStart === true ? 'soloStart' : 'start'),
{config: config});
delete this.jobStartingStopping[jobName];
this.forceUpdate();
}
public async stop(id: string): Promise<void> {
await this._networkService.postJson('/admin/jobs/scheduled/' + id + '/stop');
public async stop(jobName: string): Promise<void> {
this.jobStartingStopping[jobName] = true;
await this._networkService.postJson('/admin/jobs/scheduled/' + jobName + '/stop');
delete this.jobStartingStopping[jobName];
this.forceUpdate();
}
protected async loadProgress(): Promise<void> {
const prevPrg = this.progress.value;
this.progress.next(await this._networkService.getJson<{ [key: string]: JobProgressDTO }>('/admin/jobs/scheduled/progress'));
this.lastRuns.next(await this._networkService.getJson<{ [key: string]: JobProgressDTO }>('/admin/jobs/scheduled/lastRun'));
for (const prg in prevPrg) {
if (!this.progress.value.hasOwnProperty(prg)) {
this.onJobFinish.emit(prg);

View File

@ -74,20 +74,11 @@
</button>
<button class="btn btn-success float-left ml-0"
*ngIf="Progress == null"
[disabled]="inProgress"
title="Generates all thumbnails now"
i18n-title
(click)="startJob()">
<ng-container i18n>Generate thumbnails now</ng-container>
<span class="oi oi-media-play ml-2"></span>
</button>
<button class="btn btn-secondary float-left ml-0"
*ngIf="Progress != null"
[disabled]="inProgress || Progress.state !== JobProgressStates.running"
(click)="cancelJob()" i18n>Cancel thumbnail generation
</button>
<app-settings-job-button class="float-left"
[soloRun]="true"
(error)="error=$event"
[jobName]="jobName"
[config]="Config"></app-settings-job-button>
<ng-container *ngIf="Progress != null">

View File

@ -8,7 +8,6 @@ import {ThumbnailSettingsService} from './thumbnail.settings.service';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {ServerConfig} from '../../../../../common/config/private/IPrivateConfig';
import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
@ -23,6 +22,7 @@ export class ThumbnailSettingsComponent
extends SettingsComponent<{ server: ServerConfig.ThumbnailConfig, client: ClientConfig.ThumbnailConfig }>
implements OnInit {
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Thumbnail Generation']];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
@ -36,6 +36,10 @@ export class ThumbnailSettingsComponent
}));
}
get Config(): any {
return {sizes: this.original.client.thumbnailSizes[0]};
}
get ThumbnailSizes(): string {
return this.settings.client.thumbnailSizes.join('; ');
}
@ -49,52 +53,12 @@ export class ThumbnailSettingsComponent
}
get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
{sizes: this.original.client.thumbnailSizes[0]})];
return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.Config)];
}
ngOnInit() {
super.ngOnInit();
}
async startJob() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.start(DefaultsJobs[DefaultsJobs['Thumbnail Generation']], {sizes: this.original.client.thumbnailSizes[0]});
this.notification.info(this.i18n('Thumbnail generation started'));
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
}
this.inProgress = false;
return false;
}
async cancelJob() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.stop(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]);
this.notification.info(this.i18n('Thumbnail generation interrupted'));
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
}
this.inProgress = false;
return false;
}
}

View File

@ -115,6 +115,9 @@
</div>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
@ -124,22 +127,10 @@
(click)="reset()" i18n>Reset
</button>
<button class="btn btn-success float-left ml-0"
*ngIf="Progress == null"
[disabled]="inProgress"
title="Indexes the folders"
i18n-title
(click)="transcode()">
<ng-container i18n>Transcode videos now</ng-container>
<span class="oi oi-media-play ml-2"></span>
</button>
<button class="btn btn-secondary float-left ml-0"
*ngIf="Progress != null"
[disabled]="inProgress || Progress.state !== JobProgressStates.running"
(click)="cancelTranscoding()" i18n>Cancel transcoding
</button>
<app-settings-job-button class="float-left"
[soloRun]="true"
(error)="error=$event"
[jobName]="jobName"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>

View File

@ -8,7 +8,6 @@ import {ClientConfig} from '../../../../../common/config/public/ConfigClass';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ServerConfig} from '../../../../../common/config/private/IPrivateConfig';
import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
@ -28,6 +27,7 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
fps = [24, 25, 30, 48, 50, 60];
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Video Converting']];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
@ -92,43 +92,6 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
}
async transcode() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.start(DefaultsJobs[DefaultsJobs['Video Converting']]);
this.notification.info(this.i18n('Video transcoding started'));
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
}
this.inProgress = false;
return false;
}
async cancelTranscoding() {
this.inProgress = true;
this.error = '';
try {
await this.jobsService.stop(DefaultsJobs[DefaultsJobs['Video Converting']]);
this.notification.info(this.i18n('Video transcoding interrupted'));
this.inProgress = false;
return true;
} catch (err) {
console.log(err);
if (err.message) {
this.error = (<ErrorDTO>err).message;
}
}
this.inProgress = false;
return false;
}
}