mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
Adding job schedules to config #569
This commit is contained in:
parent
9ac67ead63
commit
36d4641e9d
@ -1,6 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
||||
import 'reflect-metadata';
|
||||
import {JobScheduleDTO, JobTrigger, JobTriggerType,} from '../../entities/job/JobScheduleDTO';
|
||||
import {
|
||||
AfterJobTrigger,
|
||||
JobScheduleDTO,
|
||||
JobTrigger,
|
||||
JobTriggerType,
|
||||
NeverJobTrigger,
|
||||
PeriodicJobTrigger,
|
||||
ScheduledJobTrigger,
|
||||
} from '../../entities/job/JobScheduleDTO';
|
||||
import {
|
||||
ClientConfig,
|
||||
ClientGPXCompressingConfig,
|
||||
@ -334,8 +342,9 @@ export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
|
||||
{
|
||||
name: $localize`Min time delta`,
|
||||
priority: ConfigPriority.underTheHood,
|
||||
unit: 'ms',
|
||||
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx
|
||||
},
|
||||
} as TAGS,
|
||||
description: $localize`Filters out entry that are closer than this in time in milliseconds.`
|
||||
})
|
||||
minTimeDistance: number = 5000;
|
||||
@ -485,13 +494,13 @@ export class ServerLogConfig {
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class NeverJobTrigger implements JobTrigger {
|
||||
export class NeverJobTriggerConfig implements NeverJobTrigger {
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.never;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ScheduledJobTrigger implements JobTrigger {
|
||||
export class ScheduledJobTriggerConfig implements ScheduledJobTrigger {
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.scheduled;
|
||||
|
||||
@ -500,17 +509,17 @@ export class ScheduledJobTrigger implements JobTrigger {
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class PeriodicJobTrigger implements JobTrigger {
|
||||
export class PeriodicJobTriggerConfig implements PeriodicJobTrigger {
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.periodic;
|
||||
@ConfigProperty({type: 'unsignedInt', max: 7})
|
||||
periodicity: number | undefined; // 0-6: week days 7 every day
|
||||
periodicity: number | undefined = 7; // 0-6: week days 7 every day
|
||||
@ConfigProperty({type: 'unsignedInt', max: 23 * 60 + 59})
|
||||
atTime: number | undefined; // day time
|
||||
atTime: number | undefined = 0; // day time
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class AfterJobTrigger implements JobTrigger {
|
||||
export class AfterJobTriggerConfig implements AfterJobTrigger {
|
||||
@ConfigProperty({type: JobTriggerType})
|
||||
readonly type = JobTriggerType.after;
|
||||
@ConfigProperty()
|
||||
@ -528,42 +537,42 @@ export class JobScheduleConfig implements JobScheduleDTO {
|
||||
@ConfigProperty()
|
||||
jobName: string;
|
||||
@ConfigProperty()
|
||||
config: any = {};
|
||||
config: Record<string, string | number | string[] | number[]> = {};
|
||||
@ConfigProperty()
|
||||
allowParallelRun: boolean;
|
||||
allowParallelRun: boolean = false;
|
||||
@ConfigProperty({
|
||||
type: NeverJobTrigger,
|
||||
type: NeverJobTriggerConfig,
|
||||
typeBuilder: (v: JobTrigger) => {
|
||||
const type = typeof v.type === 'number' ? v.type : JobTriggerType[v.type];
|
||||
switch (type) {
|
||||
case JobTriggerType.after:
|
||||
return AfterJobTrigger;
|
||||
return AfterJobTriggerConfig;
|
||||
case JobTriggerType.never:
|
||||
return NeverJobTrigger;
|
||||
return NeverJobTriggerConfig;
|
||||
case JobTriggerType.scheduled:
|
||||
return ScheduledJobTrigger;
|
||||
return ScheduledJobTriggerConfig;
|
||||
case JobTriggerType.periodic:
|
||||
return PeriodicJobTrigger;
|
||||
return PeriodicJobTriggerConfig;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
})
|
||||
trigger:
|
||||
| AfterJobTrigger
|
||||
| NeverJobTrigger
|
||||
| PeriodicJobTrigger
|
||||
| ScheduledJobTrigger;
|
||||
| AfterJobTriggerConfig
|
||||
| NeverJobTriggerConfig
|
||||
| PeriodicJobTriggerConfig
|
||||
| ScheduledJobTriggerConfig;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
jobName: string,
|
||||
allowParallelRun: boolean,
|
||||
trigger:
|
||||
| AfterJobTrigger
|
||||
| NeverJobTrigger
|
||||
| PeriodicJobTrigger
|
||||
| ScheduledJobTrigger,
|
||||
config: any
|
||||
| AfterJobTriggerConfig
|
||||
| NeverJobTriggerConfig
|
||||
| PeriodicJobTriggerConfig
|
||||
| ScheduledJobTriggerConfig,
|
||||
config: any = {},
|
||||
allowParallelRun: boolean = false
|
||||
) {
|
||||
this.name = name;
|
||||
this.jobName = jobName;
|
||||
@ -600,43 +609,43 @@ export class ServerJobConfig {
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs.Indexing],
|
||||
DefaultsJobs[DefaultsJobs.Indexing],
|
||||
false,
|
||||
new NeverJobTrigger(),
|
||||
{indexChangesOnly: true}
|
||||
new NeverJobTriggerConfig(),
|
||||
{indexChangesOnly: true} // set config explicitly so it not undefined on the UI
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Preview Filling']],
|
||||
DefaultsJobs[DefaultsJobs['Preview Filling']],
|
||||
false,
|
||||
new NeverJobTrigger(),
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Indexing']]),
|
||||
{}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
|
||||
DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]),
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Preview Filling']]),
|
||||
{sizes: [240], indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||
DefaultsJobs[DefaultsJobs['Photo Converting']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
|
||||
{indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Video Converting']],
|
||||
DefaultsJobs[DefaultsJobs['Video Converting']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]),
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Photo Converting']]),
|
||||
{indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['GPX Compression']],
|
||||
DefaultsJobs[DefaultsJobs['GPX Compression']],
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Video Converting']]),
|
||||
{indexedOnly: true}
|
||||
),
|
||||
new JobScheduleConfig(
|
||||
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
|
||||
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
|
||||
false,
|
||||
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]),
|
||||
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['GPX Compression']]),
|
||||
{indexedOnly: true}
|
||||
),
|
||||
];
|
||||
|
@ -10,7 +10,7 @@ export interface JobTrigger {
|
||||
type: JobTriggerType;
|
||||
}
|
||||
|
||||
export interface NeverJobTrigger {
|
||||
export interface NeverJobTrigger extends JobTrigger {
|
||||
type: JobTriggerType.never;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ export interface AfterJobTrigger extends JobTrigger {
|
||||
export interface JobScheduleDTO {
|
||||
name: string;
|
||||
jobName: string;
|
||||
config: any;
|
||||
config: Record<string, string | number | string[] | number[]>;
|
||||
allowParallelRun: boolean;
|
||||
trigger:
|
||||
| NeverJobTrigger
|
||||
|
@ -100,6 +100,9 @@ import {GallerySortingService} from './ui/gallery/navigator/sorting.service';
|
||||
import {FilterService} from './ui/gallery/filter/filter.service';
|
||||
import {TemplateComponent} from './ui/settings/template/template.component';
|
||||
import {AbstractSettingsService} from './ui/settings/_abstract/abstract.settings.service';
|
||||
import { WorkflowComponent } from './ui/settings/workflow/workflow.component';
|
||||
import {JobProgressComponent} from './ui/settings/jobs/progress/job-progress.settings.component';
|
||||
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
|
||||
|
||||
@Injectable()
|
||||
export class MyHammerConfig extends HammerGestureConfig {
|
||||
@ -217,6 +220,8 @@ Marker.prototype.options.icon = iconDefault;
|
||||
// Settings
|
||||
SettingsEntryComponent,
|
||||
TemplateComponent,
|
||||
JobProgressComponent,
|
||||
JobButtonComponent,
|
||||
/* UserMangerSettingsComponent,
|
||||
DatabaseSettingsComponent,
|
||||
MapSettingsComponent,
|
||||
@ -247,6 +252,7 @@ Marker.prototype.options.icon = iconDefault;
|
||||
StringifySearchQuery,
|
||||
FileDTOToPathPipe,
|
||||
PhotoFilterPipe,
|
||||
WorkflowComponent,
|
||||
],
|
||||
providers: [
|
||||
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},
|
||||
|
@ -85,7 +85,14 @@
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<app-settings-template #setting #server
|
||||
<app-settings-template
|
||||
*ngFor="let cp of configPaths"
|
||||
#setting
|
||||
#tmpl
|
||||
icon="list"
|
||||
[ConfigPath]="cp"
|
||||
[hidden]="!tmpl.HasAvailableSettings"></app-settings-template>
|
||||
<!-- <app-settings-template #setting #server
|
||||
icon="list"
|
||||
[ConfigPath]="'Server'"
|
||||
[hidden]="!server.HasAvailableSettings"></app-settings-template>
|
||||
@ -140,7 +147,7 @@
|
||||
<app-settings-template #setting #indexing
|
||||
icon="pie-chart"
|
||||
[ConfigPath]="'Indexing'"
|
||||
[hidden]="!indexing.HasAvailableSettings"></app-settings-template>
|
||||
[hidden]="!indexing.HasAvailableSettings"></app-settings-template>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -7,10 +7,9 @@ import {NavigationService} from '../../model/navigation.service';
|
||||
import {ISettingsComponent} from '../settings/_abstract/ISettingsComponent';
|
||||
import {PageHelper} from '../../model/page.helper';
|
||||
import {SettingsService} from '../settings/settings.service';
|
||||
import {CookieNames} from '../../../../common/CookieNames';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {WebConfig} from '../../../../common/config/private/WebConfig';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
@ -24,6 +23,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
|
||||
contents: ISettingsComponent[] = [];
|
||||
configPriorities: { key: number; value: string; }[];
|
||||
public readonly ConfigPriority = ConfigPriority;
|
||||
public readonly configPaths: string[] = [];
|
||||
|
||||
constructor(
|
||||
private authService: AuthenticationService,
|
||||
@ -32,6 +32,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
|
||||
public settingsService: SettingsService,
|
||||
) {
|
||||
this.configPriorities = Utils.enumToArray(ConfigPriority);
|
||||
this.configPaths = Object.keys((new WebConfig()).State);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
|
@ -1,12 +1,4 @@
|
||||
import {
|
||||
Directive,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {Directive, Input, OnDestroy, OnInit, ViewChild,} from '@angular/core';
|
||||
import {AuthenticationService} from '../../../model/network/authentication.service';
|
||||
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
@ -20,12 +12,8 @@ import {WebConfig} from '../../../../../common/config/private/WebConfig';
|
||||
import {FormControl} from '@angular/forms';
|
||||
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {IConfigClass} from 'typeconfig/common';
|
||||
import {IConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IConfigClass';
|
||||
import {IPropertyMetadata} from '../../../../../../node_modules/typeconfig/src/decorators/property/IPropertyState';
|
||||
import {IWebConfigClass, IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
|
||||
import {IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
|
||||
import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
|
||||
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
|
||||
|
||||
interface ConfigState<T = unknown> {
|
||||
value: T;
|
||||
@ -73,13 +61,13 @@ export abstract class SettingsComponentDirective<
|
||||
public error: string = null;
|
||||
public changed = false;
|
||||
public states: RecursiveState = {} as RecursiveState;
|
||||
protected name: string;
|
||||
|
||||
private subscription: Subscription = null;
|
||||
private settingsSubscription: Subscription = null;
|
||||
protected sliceFN?: (s: WebConfig) => T;
|
||||
|
||||
protected constructor(
|
||||
protected name: string,
|
||||
protected authService: AuthenticationService,
|
||||
private navigation: NavigationService,
|
||||
public settingsService: AbstractSettingsService,
|
||||
@ -128,7 +116,8 @@ export abstract class SettingsComponentDirective<
|
||||
|
||||
// if all sub elements are hidden, hide the parent too.
|
||||
if (state.isConfigType) {
|
||||
if (Object.keys(state.value.__state).findIndex(k => !st.value.__state[k].shouldHide()) === -1) {
|
||||
if (state.value.__state &&
|
||||
Object.keys(state.value.__state).findIndex(k => !st.value.__state[k].shouldHide()) === -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -136,14 +125,17 @@ export abstract class SettingsComponentDirective<
|
||||
|
||||
if (state.isConfigArrayType) {
|
||||
for (let i = 0; i < state.value?.length; ++i) {
|
||||
if (Object.keys(state.value[i].__state).findIndex(k => !(st.value[i].__state[k].shouldHide && st.value[i].__state[k].shouldHide())) === -1) {
|
||||
if (state.value[i].__state &&
|
||||
Object.keys(state.value[i].__state).findIndex(k => !(st.value[i].__state[k].shouldHide && st.value[i].__state[k].shouldHide())) === -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
state.tags?.priority > this.globalSettingsService.configPriority &&
|
||||
(state.tags?.priority > this.globalSettingsService.configPriority ||
|
||||
(this.globalSettingsService.configPriority === ConfigPriority.basic &&
|
||||
state.tags?.dockerSensitive && this.globalSettingsService.settings.value.Environment.isDocker)) && //if this value should not change in Docker, lets hide it
|
||||
Utils.equalsFilter(state.value, state.default,
|
||||
['__propPath', '__created', '__prototype', '__rootConfig']) &&
|
||||
Utils.equalsFilter(state.original, state.default,
|
||||
|
@ -24,7 +24,6 @@
|
||||
[disabled]="state.readonly || Disabled"
|
||||
(change)="onChange($event)"
|
||||
placeholder="Search Query">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
<div class="input-group">
|
||||
@ -35,6 +34,7 @@
|
||||
Type !== 'SearchQuery' &&
|
||||
ArrayType !== 'MapLayers' &&
|
||||
ArrayType !== 'NavigationLinkConfig' &&
|
||||
ArrayType !== 'JobScheduleConfig' &&
|
||||
ArrayType !== 'UserConfig'"
|
||||
[type]="type" [min]="state.min" [max]="state.max" class="form-control"
|
||||
[placeholder]="PlaceHolder"
|
||||
@ -82,6 +82,17 @@
|
||||
[(ngModel)]="state.value">
|
||||
</bSwitch>
|
||||
|
||||
|
||||
<app-settings-workflow
|
||||
class="w-100"
|
||||
*ngIf="ArrayType === 'JobScheduleConfig'"
|
||||
[(ngModel)]="state.value"
|
||||
[id]="idName"
|
||||
[name]="idName"
|
||||
[title]="title"
|
||||
(ngModelChange)="onChange($event)">
|
||||
</app-settings-workflow>
|
||||
|
||||
<ng-container *ngIf="ArrayType === 'MapLayers'">
|
||||
<div class="container">
|
||||
<table class="table">
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
} from '../../../../../../common/config/public/ClientConfig';
|
||||
import {SettingsService} from '../../settings.service';
|
||||
import {WebConfig} from '../../../../../../common/config/private/WebConfig';
|
||||
import {UserConfig} from '../../../../../../common/config/private/PrivateConfig';
|
||||
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
|
||||
|
||||
interface IState {
|
||||
shouldHide(): boolean;
|
||||
@ -78,7 +78,6 @@ export class SettingsEntryComponent
|
||||
title: string;
|
||||
idName: string;
|
||||
private readonly GUID = Utils.GUID();
|
||||
public NavigationLinkTypesEnum = Utils.enumToArray(NavigationLinkTypes);
|
||||
NavigationLinkTypes = NavigationLinkTypes;
|
||||
|
||||
constructor(private searchQueryParserService: SearchQueryParserService,
|
||||
@ -142,6 +141,10 @@ export class SettingsEntryComponent
|
||||
if (this.state.arrayType === UserConfig) {
|
||||
return 'UserConfig';
|
||||
}
|
||||
if (this.state.arrayType === JobScheduleConfig) {
|
||||
return 'JobScheduleConfig';
|
||||
}
|
||||
|
||||
this.state.arrayType;
|
||||
}
|
||||
|
||||
|
@ -7,15 +7,18 @@ import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfi
|
||||
import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
|
||||
import {CookieNames} from '../../../../common/CookieNames';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {JobDTO} from '../../../../common/entities/job/JobDTO';
|
||||
|
||||
@Injectable()
|
||||
export class SettingsService {
|
||||
public configPriority = ConfigPriority.basic;
|
||||
public settings: BehaviorSubject<WebConfig>;
|
||||
private fetchingSettings = false;
|
||||
public availableJobs: BehaviorSubject<JobDTO[]>;
|
||||
|
||||
constructor(private networkService: NetworkService,
|
||||
private cookieService: CookieService) {
|
||||
this.availableJobs = new BehaviorSubject([]);
|
||||
this.settings = new BehaviorSubject<WebConfig>(new WebConfig());
|
||||
this.getSettings().catch(console.error);
|
||||
|
||||
@ -27,6 +30,12 @@ export class SettingsService {
|
||||
|
||||
}
|
||||
|
||||
public async getAvailableJobs(): Promise<void> {
|
||||
this.availableJobs.next(
|
||||
await this.networkService.getJson<JobDTO[]>('/admin/jobs/available')
|
||||
);
|
||||
}
|
||||
|
||||
public async getSettings(): Promise<void> {
|
||||
if (this.fetchingSettings === true) {
|
||||
return;
|
||||
|
@ -25,7 +25,6 @@ export class TemplateComponent extends SettingsComponentDirective<any> implement
|
||||
globalSettingsService: SettingsService
|
||||
) {
|
||||
super(
|
||||
`Template`,
|
||||
authService,
|
||||
navigation,
|
||||
settingsService,
|
||||
|
257
src/frontend/app/ui/settings/workflow/workflow.component.html
Normal file
257
src/frontend/app/ui/settings/workflow/workflow.component.html
Normal file
@ -0,0 +1,257 @@
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
<div *ngFor="let schedule of sortedSchedules() as sortedSchedules; let i= index">
|
||||
<div class="card bg-light mt-2 mb-2 no-changed-settings {{shouldIdent(schedule,sortedSchedules[i-1])? 'ms-4' : ''}}">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div (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">
|
||||
<ng-container *ngSwitchCase="JobTriggerType.periodic">
|
||||
<span class="badge bg-primary" i18n>every</span>
|
||||
{{periods[$any(schedule.trigger).periodicity]}} {{atTimeLocal($any(schedule.trigger).atTime) | date:"HH:mm (z)"}}
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngSwitchCase="JobTriggerType.scheduled">@{{$any(schedule.trigger).time | date:"medium"}}</ng-container>
|
||||
<span class="badge bg-secondary" *ngSwitchCase="JobTriggerType.never" i18n>never</span>
|
||||
<ng-container *ngSwitchCase="JobTriggerType.after">
|
||||
<span class="badge bg-primary" i18n>after</span>
|
||||
{{$any(schedule.trigger).afterScheduleName}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger ms-0" (click)="remove(schedule)"><span
|
||||
class="oi oi-trash"></span>
|
||||
</button>
|
||||
<app-settings-job-button class="ms-md-2 mt-2 mt-md-0"
|
||||
(jobError)="error=$event"
|
||||
[allowParallelRun]="schedule.allowParallelRun"
|
||||
[jobName]="schedule.jobName" [config]="schedule.config"
|
||||
[shortName]="true"></app-settings-job-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body" [hidden]="!showDetails[schedule.name]">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="mb-1 row">
|
||||
<label class="col-md-2 control-label" [for]="'jobName'+i" i18n>Job:</label>
|
||||
<div class="col-md-4">
|
||||
{{backendTextService.getJobName(schedule.jobName)}}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<app-settings-job-button class="float-end"
|
||||
[jobName]="schedule.jobName"
|
||||
[allowParallelRun]="schedule.allowParallelRun"
|
||||
(jobError)="error=$event"
|
||||
[config]="schedule.config"></app-settings-job-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 row">
|
||||
<label class="col-md-2 control-label" [for]="'repeatType'+i" i18n>Periodicity:</label>
|
||||
<div class="col-md-10">
|
||||
<select class="form-select" [(ngModel)]="schedule.trigger.type"
|
||||
(ngModelChange)="jobTriggerTypeChanged($event,schedule); onChange($event);"
|
||||
[name]="'repeatType'+i" required>
|
||||
<option *ngFor="let jobTrigger of JobTriggerTypeMap"
|
||||
[ngValue]="jobTrigger.key">{{jobTrigger.value}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted"
|
||||
i18n>Set the time to run the job.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.after">
|
||||
<label class="col-md-2 control-label" [for]="'triggerAfter'+i" i18n>After:</label>
|
||||
<div class="col-md-10">
|
||||
<select class="form-select"
|
||||
[(ngModel)]="schedule.trigger.afterScheduleName"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[name]="'triggerAfter'+i" required>
|
||||
<ng-container *ngFor="let sch of sortedSchedules">
|
||||
<option *ngIf="sch.name !== schedule.name"
|
||||
[ngValue]="sch.name">{{sch.name}}
|
||||
</option>
|
||||
</ng-container>
|
||||
</select>
|
||||
<small class="form-text text-muted"
|
||||
i18n>The job will run after that job finishes.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.scheduled">
|
||||
<label class="col-md-2 control-label" [for]="'triggerTime'+i" i18n>At:</label>
|
||||
<div class="col-md-10">
|
||||
<app-timestamp-datepicker
|
||||
[name]="'triggerTime'+i"
|
||||
(timestampChange)="onChange($event)"
|
||||
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row" *ngIf="schedule.trigger.type == JobTriggerType.periodic">
|
||||
<label class="col-md-2 control-label" [for]="'periodicity'+i" i18n>At:</label>
|
||||
<div class="col-md-10">
|
||||
<select
|
||||
class="form-select"
|
||||
[(ngModel)]="schedule.trigger.periodicity"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[name]="'periodicity' + i"
|
||||
required>
|
||||
<option *ngFor="let period of periods; let i = index"
|
||||
[ngValue]="i">
|
||||
<ng-container i18n>every</ng-container>
|
||||
{{period}}
|
||||
</option>
|
||||
</select>
|
||||
<app-timestamp-timepicker
|
||||
[name]="'atTime'+i"
|
||||
(timestampChange)="onChange($event)"
|
||||
[(timestamp)]="schedule.trigger.atTime"></app-timestamp-timepicker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 control-label" [for]="'allowParallelRun'+'_'+i" i18n>Allow parallel run</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
[name]="'allowParallelRun'+'_'+i"
|
||||
[id]="'allowParallelRun'+'_'+i"
|
||||
switch-on-color="primary"
|
||||
[switch-inverse]="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text i18n-switch-on-text
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="schedule.allowParallelRun">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted ms-2"
|
||||
i18n>Enables the job to start even if another job is already running.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="getConfigTemplate(schedule.jobName) ">
|
||||
<hr/>
|
||||
<div *ngFor="let configEntry of getConfigTemplate(schedule.jobName)">
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-md-2 control-label"
|
||||
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
|
||||
<div class="col-md-10">
|
||||
<ng-container [ngSwitch]="configEntry.type">
|
||||
<ng-container *ngSwitchCase="'boolean'">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
[name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
switch-on-color="primary"
|
||||
[switch-inverse]="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text i18n-switch-on-text
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="schedule.config[configEntry.id]">
|
||||
</bSwitch>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'string'">
|
||||
<input type="text" class="form-control" [name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="schedule.config[configEntry.id]" required>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'number'">
|
||||
<input type="number" class="form-control" [name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="schedule.config[configEntry.id]" required>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'number-array'">
|
||||
<input type="text" class="form-control"
|
||||
[name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
(ngModelChange)="setNumberArray(schedule.config,configEntry.id,$event); onChange($event);"
|
||||
[ngModel]="getNumberArray($any(schedule.config),configEntry.id)" required>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<small class="form-text text-muted ms-2">
|
||||
<ng-container *ngIf="configEntry.type == 'number-array'" i18n>';' separated integers.
|
||||
</ng-container>{{backendTextService.get(configEntry.description)}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<app-settings-job-progress
|
||||
class="card-footer bg-transparent"
|
||||
*ngIf="getProgress(schedule)"
|
||||
[progress]="getProgress(schedule)">
|
||||
</app-settings-job-progress>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary float-end mt-2"
|
||||
(click)="prepareNewJob()" i18n>+ Add Job
|
||||
</button>
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div bsModal #jobModal="bs-modal" class="modal fade" id="jobModal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="jobModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="jobModalLabel" i18n>Add new job</h5>
|
||||
<button type="button" class="btn-close" (click)="jobModal.hide()" data-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<form #jobModalForm="ngForm">
|
||||
<div class="modal-body">
|
||||
<select class="form-select"
|
||||
(change)="jobTypeChanged(newSchedule)"
|
||||
[(ngModel)]="newSchedule.jobName"
|
||||
name="newJobName" required>
|
||||
<option *ngFor="let availableJob of settingsService.availableJobs | async"
|
||||
[ngValue]="availableJob.Name">{{backendTextService.getJobName(availableJob.Name)}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted"
|
||||
i18n>Select a job to schedule.
|
||||
</small>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="jobModal.hide()" i18n>Close</button>
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal"
|
||||
(click)="addNewJob()"
|
||||
[disabled]="!jobModalForm.form.valid" i18n>Add Job
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
294
src/frontend/app/ui/settings/workflow/workflow.component.ts
Normal file
294
src/frontend/app/ui/settings/workflow/workflow.component.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import {Component, OnDestroy, OnInit, QueryList, ViewChildren} from '@angular/core';
|
||||
import {forwardRef} from '../../../../../../node_modules/@angular/core';
|
||||
import {ModalDirective} from 'ngx-bootstrap/modal';
|
||||
import {
|
||||
AfterJobTrigger,
|
||||
JobScheduleDTO,
|
||||
JobScheduleDTOUtils,
|
||||
JobTriggerType,
|
||||
PeriodicJobTrigger,
|
||||
ScheduledJobTrigger
|
||||
} from '../../../../../common/entities/job/JobScheduleDTO';
|
||||
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
||||
import {BackendtextService} from '../../../model/backendtext.service';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ConfigTemplateEntry} from '../../../../../common/entities/job/JobDTO';
|
||||
import {JobProgressDTO, JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
|
||||
import {
|
||||
AfterJobTriggerConfig,
|
||||
JobScheduleConfig,
|
||||
NeverJobTriggerConfig,
|
||||
PeriodicJobTriggerConfig,
|
||||
ScheduledJobTriggerConfig
|
||||
} from '../../../../../common/config/private/PrivateConfig';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator
|
||||
} from '../../../../../../node_modules/@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-workflow',
|
||||
templateUrl: './workflow.component.html',
|
||||
styleUrls: ['./workflow.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => WorkflowComponent),
|
||||
multi: true,
|
||||
},
|
||||
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => WorkflowComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class WorkflowComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
|
||||
|
||||
|
||||
public schedules: JobScheduleConfig[] = [];
|
||||
|
||||
@ViewChildren('jobModal') public jobModalQL: QueryList<ModalDirective>;
|
||||
|
||||
public disableButtons = false;
|
||||
public JobTriggerTypeMap: { key: number; value: string }[];
|
||||
public JobTriggerType = JobTriggerType;
|
||||
public periods: string[] = [];
|
||||
public showDetails: { [key: string]: boolean } = {};
|
||||
public JobProgressStates = JobProgressStates;
|
||||
public newSchedule: JobScheduleDTO = {
|
||||
name: '',
|
||||
config: null,
|
||||
jobName: '',
|
||||
trigger: {
|
||||
type: JobTriggerType.never,
|
||||
},
|
||||
allowParallelRun: false,
|
||||
};
|
||||
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
public settingsService: SettingsService,
|
||||
public jobsService: ScheduledJobsService,
|
||||
public backendTextService: BackendtextService,
|
||||
) {
|
||||
this.JobTriggerTypeMap = [
|
||||
{key: JobTriggerType.after, value: $localize`after`},
|
||||
{key: JobTriggerType.never, value: $localize`never`},
|
||||
{key: JobTriggerType.periodic, value: $localize`periodic`},
|
||||
{key: JobTriggerType.scheduled, value: $localize`scheduled`},
|
||||
];
|
||||
this.periods = [
|
||||
$localize`Monday`, // 0
|
||||
$localize`Tuesday`, // 1
|
||||
$localize`Wednesday`, // 2
|
||||
$localize`Thursday`,
|
||||
$localize`Friday`,
|
||||
$localize`Saturday`,
|
||||
$localize`Sunday`,
|
||||
$localize`day`,
|
||||
]; // 7
|
||||
}
|
||||
|
||||
atTimeLocal(atTime: number): Date {
|
||||
const d = new Date();
|
||||
d.setUTCHours(Math.floor(atTime / 60));
|
||||
d.setUTCMinutes(Math.floor(atTime % 60));
|
||||
return d;
|
||||
}
|
||||
|
||||
getConfigTemplate(JobName: string): ConfigTemplateEntry[] {
|
||||
const job = this.settingsService.availableJobs.value.find(
|
||||
(t) => t.Name === JobName
|
||||
);
|
||||
if (job && job.ConfigTemplate && job.ConfigTemplate.length > 0) {
|
||||
return job.ConfigTemplate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.jobsService.subscribeToProgress();
|
||||
this.settingsService.getAvailableJobs().catch(console.error);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.jobsService.unsubscribeFromProgress();
|
||||
}
|
||||
|
||||
remove(schedule: JobScheduleDTO): void {
|
||||
this.schedules.splice(
|
||||
this.schedules.indexOf(schedule),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
jobTypeChanged(schedule: JobScheduleDTO): void {
|
||||
const job = this.settingsService.availableJobs.value.find(
|
||||
(t) => t.Name === schedule.jobName
|
||||
);
|
||||
schedule.config = schedule.config || {};
|
||||
if (job.ConfigTemplate) {
|
||||
job.ConfigTemplate.forEach(
|
||||
(ct) => (schedule.config[ct.id] = ct.defaultValue)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
jobTriggerTypeChanged(
|
||||
triggerType: JobTriggerType,
|
||||
schedule: JobScheduleDTO
|
||||
): void {
|
||||
switch (triggerType) {
|
||||
case JobTriggerType.never:
|
||||
schedule.trigger = new NeverJobTriggerConfig();
|
||||
break;
|
||||
case JobTriggerType.scheduled:
|
||||
schedule.trigger = new ScheduledJobTriggerConfig();
|
||||
(schedule.trigger as unknown as ScheduledJobTrigger).time = Date.now();
|
||||
break;
|
||||
|
||||
case JobTriggerType.periodic:
|
||||
schedule.trigger = new PeriodicJobTriggerConfig();
|
||||
break;
|
||||
|
||||
case JobTriggerType.after:
|
||||
schedule.trigger = new AfterJobTriggerConfig();
|
||||
if (!(schedule.trigger as unknown as AfterJobTrigger).afterScheduleName && this.schedules.length > 1) {
|
||||
(schedule.trigger as unknown as AfterJobTrigger).afterScheduleName = this.schedules.find(s => s.name !== schedule.name).name;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setNumberArray(configElement: any, id: string, value: string): void {
|
||||
value = value.replace(new RegExp(',', 'g'), ';');
|
||||
value = value.replace(new RegExp(' ', 'g'), ';');
|
||||
configElement[id] = value
|
||||
.split(';')
|
||||
.map((s: string) => parseInt(s, 10))
|
||||
.filter((i: number) => !isNaN(i) && i > 0);
|
||||
}
|
||||
|
||||
getNumberArray(configElement: Record<string, number[]>, id: string): string {
|
||||
return configElement[id] ? configElement[id].join('; ') : '';
|
||||
}
|
||||
|
||||
public shouldIdent(curr: JobScheduleDTO, prev: JobScheduleDTO): boolean {
|
||||
return (
|
||||
curr &&
|
||||
curr.trigger.type === JobTriggerType.after &&
|
||||
prev &&
|
||||
prev.name === curr.trigger.afterScheduleName
|
||||
);
|
||||
}
|
||||
|
||||
public sortedSchedules(): JobScheduleDTO[] {
|
||||
return (this.schedules || [])
|
||||
.slice()
|
||||
.sort((a: JobScheduleDTO, b: JobScheduleDTO) => {
|
||||
return (
|
||||
this.getNextRunningDate(a, this.schedules) -
|
||||
this.getNextRunningDate(b, this.schedules)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
prepareNewJob(): void {
|
||||
const jobName = this.settingsService.availableJobs.value[0].Name;
|
||||
this.newSchedule = new JobScheduleConfig('new job',
|
||||
jobName,
|
||||
new NeverJobTriggerConfig());
|
||||
|
||||
// setup job specific config
|
||||
const job = this.settingsService.availableJobs.value.find(
|
||||
(t) => t.Name === jobName
|
||||
);
|
||||
this.newSchedule.config = this.newSchedule.config || {};
|
||||
if (job.ConfigTemplate) {
|
||||
job.ConfigTemplate.forEach(
|
||||
(ct) => (this.newSchedule.config[ct.id] = ct.defaultValue)
|
||||
);
|
||||
}
|
||||
this.jobModalQL.first.show();
|
||||
}
|
||||
|
||||
addNewJob(): void {
|
||||
// make unique job name
|
||||
const jobName = this.newSchedule.jobName;
|
||||
const count = this.schedules.filter(
|
||||
(s: JobScheduleDTO) => s.jobName === jobName
|
||||
).length;
|
||||
this.newSchedule.name =
|
||||
count === 0
|
||||
? jobName
|
||||
: this.backendTextService.getJobName(jobName) + ' ' + (count + 1);
|
||||
this.schedules.push(this.newSchedule);
|
||||
|
||||
this.jobModalQL.first.hide();
|
||||
this.onChange(null); // trigger change detection after adding new job
|
||||
}
|
||||
|
||||
getProgress(schedule: JobScheduleDTO): JobProgressDTO {
|
||||
return this.jobsService.getProgress(schedule);
|
||||
}
|
||||
|
||||
private getNextRunningDate(
|
||||
sch: JobScheduleDTO,
|
||||
list: JobScheduleDTO[],
|
||||
depth = 0
|
||||
): number {
|
||||
if (depth > list.length) {
|
||||
return 0;
|
||||
}
|
||||
if (sch.trigger.type === JobTriggerType.never) {
|
||||
return (
|
||||
list
|
||||
.map((s) => s.name)
|
||||
.sort()
|
||||
.indexOf(sch.name) * -1
|
||||
);
|
||||
}
|
||||
if (sch.trigger.type === JobTriggerType.after) {
|
||||
const parent = list.find(
|
||||
(s) => s.name === (sch.trigger as AfterJobTrigger).afterScheduleName
|
||||
);
|
||||
if (parent) {
|
||||
return this.getNextRunningDate(parent, list, depth + 1) + 0.001;
|
||||
}
|
||||
}
|
||||
const d = JobScheduleDTOUtils.getNextRunningDate(new Date(), sch);
|
||||
return d !== null ? d.getTime() : 0;
|
||||
}
|
||||
|
||||
validate(): ValidationErrors {
|
||||
return null;
|
||||
}
|
||||
|
||||
public onChange = (value: unknown): void => {
|
||||
// empty
|
||||
};
|
||||
|
||||
public onTouched = (): void => {
|
||||
// empty
|
||||
};
|
||||
|
||||
public writeValue(obj: JobScheduleConfig[]): void {
|
||||
this.schedules = obj;
|
||||
}
|
||||
|
||||
public registerOnChange(fn: (v: JobScheduleConfig[]) => void): void {
|
||||
this.onChange = () => fn(this.schedules);
|
||||
}
|
||||
|
||||
public registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
}
|
@ -13,11 +13,16 @@ a {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.changed-settings .bootstrap-switch {
|
||||
|
||||
.changed-settings.bootstrap-switch {
|
||||
border-color: var(--bs-primary);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.changed-settings .no-changed-settings .bootstrap-switch {
|
||||
border-color: var(--bs-gray-400) !important;
|
||||
}
|
||||
|
||||
#toast-container > div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user