1
0
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:
Patrik J. Braun 2022-12-30 15:35:07 +01:00
parent 9ac67ead63
commit 36d4641e9d
14 changed files with 660 additions and 67 deletions

View File

@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-inferrable-types */
import 'reflect-metadata'; import 'reflect-metadata';
import {JobScheduleDTO, JobTrigger, JobTriggerType,} from '../../entities/job/JobScheduleDTO'; import {
AfterJobTrigger,
JobScheduleDTO,
JobTrigger,
JobTriggerType,
NeverJobTrigger,
PeriodicJobTrigger,
ScheduledJobTrigger,
} from '../../entities/job/JobScheduleDTO';
import { import {
ClientConfig, ClientConfig,
ClientGPXCompressingConfig, ClientGPXCompressingConfig,
@ -334,8 +342,9 @@ export class ServerGPXCompressingConfig extends ClientGPXCompressingConfig {
{ {
name: $localize`Min time delta`, name: $localize`Min time delta`,
priority: ConfigPriority.underTheHood, priority: ConfigPriority.underTheHood,
unit: 'ms',
uiDisabled: (sc: ServerGPXCompressingConfig, c: ServerConfig) => !c.Map.enabled || !sc.enabled || !c.MetaFile.gpx 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.` description: $localize`Filters out entry that are closer than this in time in milliseconds.`
}) })
minTimeDistance: number = 5000; minTimeDistance: number = 5000;
@ -485,13 +494,13 @@ export class ServerLogConfig {
} }
@SubConfigClass() @SubConfigClass()
export class NeverJobTrigger implements JobTrigger { export class NeverJobTriggerConfig implements NeverJobTrigger {
@ConfigProperty({type: JobTriggerType}) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.never; readonly type = JobTriggerType.never;
} }
@SubConfigClass() @SubConfigClass()
export class ScheduledJobTrigger implements JobTrigger { export class ScheduledJobTriggerConfig implements ScheduledJobTrigger {
@ConfigProperty({type: JobTriggerType}) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.scheduled; readonly type = JobTriggerType.scheduled;
@ -500,17 +509,17 @@ export class ScheduledJobTrigger implements JobTrigger {
} }
@SubConfigClass() @SubConfigClass()
export class PeriodicJobTrigger implements JobTrigger { export class PeriodicJobTriggerConfig implements PeriodicJobTrigger {
@ConfigProperty({type: JobTriggerType}) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.periodic; readonly type = JobTriggerType.periodic;
@ConfigProperty({type: 'unsignedInt', max: 7}) @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}) @ConfigProperty({type: 'unsignedInt', max: 23 * 60 + 59})
atTime: number | undefined; // day time atTime: number | undefined = 0; // day time
} }
@SubConfigClass() @SubConfigClass()
export class AfterJobTrigger implements JobTrigger { export class AfterJobTriggerConfig implements AfterJobTrigger {
@ConfigProperty({type: JobTriggerType}) @ConfigProperty({type: JobTriggerType})
readonly type = JobTriggerType.after; readonly type = JobTriggerType.after;
@ConfigProperty() @ConfigProperty()
@ -528,42 +537,42 @@ export class JobScheduleConfig implements JobScheduleDTO {
@ConfigProperty() @ConfigProperty()
jobName: string; jobName: string;
@ConfigProperty() @ConfigProperty()
config: any = {}; config: Record<string, string | number | string[] | number[]> = {};
@ConfigProperty() @ConfigProperty()
allowParallelRun: boolean; allowParallelRun: boolean = false;
@ConfigProperty({ @ConfigProperty({
type: NeverJobTrigger, type: NeverJobTriggerConfig,
typeBuilder: (v: JobTrigger) => { typeBuilder: (v: JobTrigger) => {
const type = typeof v.type === 'number' ? v.type : JobTriggerType[v.type]; const type = typeof v.type === 'number' ? v.type : JobTriggerType[v.type];
switch (type) { switch (type) {
case JobTriggerType.after: case JobTriggerType.after:
return AfterJobTrigger; return AfterJobTriggerConfig;
case JobTriggerType.never: case JobTriggerType.never:
return NeverJobTrigger; return NeverJobTriggerConfig;
case JobTriggerType.scheduled: case JobTriggerType.scheduled:
return ScheduledJobTrigger; return ScheduledJobTriggerConfig;
case JobTriggerType.periodic: case JobTriggerType.periodic:
return PeriodicJobTrigger; return PeriodicJobTriggerConfig;
} }
return null; return null;
}, },
}) })
trigger: trigger:
| AfterJobTrigger | AfterJobTriggerConfig
| NeverJobTrigger | NeverJobTriggerConfig
| PeriodicJobTrigger | PeriodicJobTriggerConfig
| ScheduledJobTrigger; | ScheduledJobTriggerConfig;
constructor( constructor(
name: string, name: string,
jobName: string, jobName: string,
allowParallelRun: boolean,
trigger: trigger:
| AfterJobTrigger | AfterJobTriggerConfig
| NeverJobTrigger | NeverJobTriggerConfig
| PeriodicJobTrigger | PeriodicJobTriggerConfig
| ScheduledJobTrigger, | ScheduledJobTriggerConfig,
config: any config: any = {},
allowParallelRun: boolean = false
) { ) {
this.name = name; this.name = name;
this.jobName = jobName; this.jobName = jobName;
@ -600,43 +609,43 @@ export class ServerJobConfig {
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs.Indexing], DefaultsJobs[DefaultsJobs.Indexing],
DefaultsJobs[DefaultsJobs.Indexing], DefaultsJobs[DefaultsJobs.Indexing],
false, new NeverJobTriggerConfig(),
new NeverJobTrigger(), {indexChangesOnly: true} // set config explicitly so it not undefined on the UI
{indexChangesOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Preview Filling']], DefaultsJobs[DefaultsJobs['Preview Filling']],
DefaultsJobs[DefaultsJobs['Preview Filling']], DefaultsJobs[DefaultsJobs['Preview Filling']],
false, new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Indexing']]),
new NeverJobTrigger(),
{} {}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Thumbnail Generation']], DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
DefaultsJobs[DefaultsJobs['Thumbnail Generation']], DefaultsJobs[DefaultsJobs['Thumbnail Generation']],
false, new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Preview Filling']]),
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Preview Filling']]),
{sizes: [240], indexedOnly: true} {sizes: [240], indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Photo Converting']], DefaultsJobs[DefaultsJobs['Photo Converting']],
DefaultsJobs[DefaultsJobs['Photo Converting']], DefaultsJobs[DefaultsJobs['Photo Converting']],
false, new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Thumbnail Generation']]),
{indexedOnly: true} {indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Video Converting']], DefaultsJobs[DefaultsJobs['Video Converting']],
DefaultsJobs[DefaultsJobs['Video Converting']], DefaultsJobs[DefaultsJobs['Video Converting']],
false, new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Photo Converting']]),
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Photo Converting']]), {indexedOnly: true}
),
new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['GPX Compression']],
DefaultsJobs[DefaultsJobs['GPX Compression']],
new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['Video Converting']]),
{indexedOnly: true} {indexedOnly: true}
), ),
new JobScheduleConfig( new JobScheduleConfig(
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']], DefaultsJobs[DefaultsJobs['Temp Folder Cleaning']],
false, new AfterJobTriggerConfig(DefaultsJobs[DefaultsJobs['GPX Compression']]),
new AfterJobTrigger(DefaultsJobs[DefaultsJobs['Video Converting']]),
{indexedOnly: true} {indexedOnly: true}
), ),
]; ];

View File

@ -10,7 +10,7 @@ export interface JobTrigger {
type: JobTriggerType; type: JobTriggerType;
} }
export interface NeverJobTrigger { export interface NeverJobTrigger extends JobTrigger {
type: JobTriggerType.never; type: JobTriggerType.never;
} }
@ -33,7 +33,7 @@ export interface AfterJobTrigger extends JobTrigger {
export interface JobScheduleDTO { export interface JobScheduleDTO {
name: string; name: string;
jobName: string; jobName: string;
config: any; config: Record<string, string | number | string[] | number[]>;
allowParallelRun: boolean; allowParallelRun: boolean;
trigger: trigger:
| NeverJobTrigger | NeverJobTrigger

View File

@ -100,6 +100,9 @@ import {GallerySortingService} from './ui/gallery/navigator/sorting.service';
import {FilterService} from './ui/gallery/filter/filter.service'; import {FilterService} from './ui/gallery/filter/filter.service';
import {TemplateComponent} from './ui/settings/template/template.component'; import {TemplateComponent} from './ui/settings/template/template.component';
import {AbstractSettingsService} from './ui/settings/_abstract/abstract.settings.service'; 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() @Injectable()
export class MyHammerConfig extends HammerGestureConfig { export class MyHammerConfig extends HammerGestureConfig {
@ -217,6 +220,8 @@ Marker.prototype.options.icon = iconDefault;
// Settings // Settings
SettingsEntryComponent, SettingsEntryComponent,
TemplateComponent, TemplateComponent,
JobProgressComponent,
JobButtonComponent,
/* UserMangerSettingsComponent, /* UserMangerSettingsComponent,
DatabaseSettingsComponent, DatabaseSettingsComponent,
MapSettingsComponent, MapSettingsComponent,
@ -247,6 +252,7 @@ Marker.prototype.options.icon = iconDefault;
StringifySearchQuery, StringifySearchQuery,
FileDTOToPathPipe, FileDTOToPathPipe,
PhotoFilterPipe, PhotoFilterPipe,
WorkflowComponent,
], ],
providers: [ providers: [
{provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: CSRFInterceptor, multi: true},

View File

@ -85,7 +85,14 @@
</nav> </nav>
</div> </div>
<div class="col-md-10"> <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" icon="list"
[ConfigPath]="'Server'" [ConfigPath]="'Server'"
[hidden]="!server.HasAvailableSettings"></app-settings-template> [hidden]="!server.HasAvailableSettings"></app-settings-template>
@ -140,7 +147,7 @@
<app-settings-template #setting #indexing <app-settings-template #setting #indexing
icon="pie-chart" icon="pie-chart"
[ConfigPath]="'Indexing'" [ConfigPath]="'Indexing'"
[hidden]="!indexing.HasAvailableSettings"></app-settings-template> [hidden]="!indexing.HasAvailableSettings"></app-settings-template>-->
</div> </div>
</div> </div>

View File

@ -7,10 +7,9 @@ import {NavigationService} from '../../model/navigation.service';
import {ISettingsComponent} from '../settings/_abstract/ISettingsComponent'; import {ISettingsComponent} from '../settings/_abstract/ISettingsComponent';
import {PageHelper} from '../../model/page.helper'; import {PageHelper} from '../../model/page.helper';
import {SettingsService} from '../settings/settings.service'; 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 {ConfigPriority} from '../../../../common/config/public/ClientConfig';
import {Utils} from '../../../../common/Utils'; import {Utils} from '../../../../common/Utils';
import {WebConfig} from '../../../../common/config/private/WebConfig';
@Component({ @Component({
selector: 'app-admin', selector: 'app-admin',
@ -24,6 +23,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
contents: ISettingsComponent[] = []; contents: ISettingsComponent[] = [];
configPriorities: { key: number; value: string; }[]; configPriorities: { key: number; value: string; }[];
public readonly ConfigPriority = ConfigPriority; public readonly ConfigPriority = ConfigPriority;
public readonly configPaths: string[] = [];
constructor( constructor(
private authService: AuthenticationService, private authService: AuthenticationService,
@ -32,6 +32,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
public settingsService: SettingsService, public settingsService: SettingsService,
) { ) {
this.configPriorities = Utils.enumToArray(ConfigPriority); this.configPriorities = Utils.enumToArray(ConfigPriority);
this.configPaths = Object.keys((new WebConfig()).State);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

View File

@ -1,12 +1,4 @@
import { import {Directive, Input, OnDestroy, OnInit, ViewChild,} from '@angular/core';
Directive,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import {AuthenticationService} from '../../../model/network/authentication.service'; import {AuthenticationService} from '../../../model/network/authentication.service';
import {UserRoles} from '../../../../../common/entities/UserDTO'; import {UserRoles} from '../../../../../common/entities/UserDTO';
import {Utils} from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
@ -20,12 +12,8 @@ import {WebConfig} from '../../../../../common/config/private/WebConfig';
import {FormControl} from '@angular/forms'; import {FormControl} from '@angular/forms';
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig'; import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../settings.service'; import {SettingsService} from '../settings.service';
import {IConfigClass} from 'typeconfig/common'; import {IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
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 {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder'; import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
interface ConfigState<T = unknown> { interface ConfigState<T = unknown> {
value: T; value: T;
@ -73,13 +61,13 @@ export abstract class SettingsComponentDirective<
public error: string = null; public error: string = null;
public changed = false; public changed = false;
public states: RecursiveState = {} as RecursiveState; public states: RecursiveState = {} as RecursiveState;
protected name: string;
private subscription: Subscription = null; private subscription: Subscription = null;
private settingsSubscription: Subscription = null; private settingsSubscription: Subscription = null;
protected sliceFN?: (s: WebConfig) => T; protected sliceFN?: (s: WebConfig) => T;
protected constructor( protected constructor(
protected name: string,
protected authService: AuthenticationService, protected authService: AuthenticationService,
private navigation: NavigationService, private navigation: NavigationService,
public settingsService: AbstractSettingsService, public settingsService: AbstractSettingsService,
@ -128,7 +116,8 @@ export abstract class SettingsComponentDirective<
// if all sub elements are hidden, hide the parent too. // if all sub elements are hidden, hide the parent too.
if (state.isConfigType) { 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; return true;
} }
} }
@ -136,14 +125,17 @@ export abstract class SettingsComponentDirective<
if (state.isConfigArrayType) { if (state.isConfigArrayType) {
for (let i = 0; i < state.value?.length; ++i) { 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 true;
} }
} }
return false; return false;
} }
return ( 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, Utils.equalsFilter(state.value, state.default,
['__propPath', '__created', '__prototype', '__rootConfig']) && ['__propPath', '__created', '__prototype', '__rootConfig']) &&
Utils.equalsFilter(state.original, state.default, Utils.equalsFilter(state.original, state.default,

View File

@ -24,7 +24,6 @@
[disabled]="state.readonly || Disabled" [disabled]="state.readonly || Disabled"
(change)="onChange($event)" (change)="onChange($event)"
placeholder="Search Query"> placeholder="Search Query">
</app-gallery-search-field> </app-gallery-search-field>
<div class="input-group"> <div class="input-group">
@ -35,6 +34,7 @@
Type !== 'SearchQuery' && Type !== 'SearchQuery' &&
ArrayType !== 'MapLayers' && ArrayType !== 'MapLayers' &&
ArrayType !== 'NavigationLinkConfig' && ArrayType !== 'NavigationLinkConfig' &&
ArrayType !== 'JobScheduleConfig' &&
ArrayType !== 'UserConfig'" ArrayType !== 'UserConfig'"
[type]="type" [min]="state.min" [max]="state.max" class="form-control" [type]="type" [min]="state.min" [max]="state.max" class="form-control"
[placeholder]="PlaceHolder" [placeholder]="PlaceHolder"
@ -82,6 +82,17 @@
[(ngModel)]="state.value"> [(ngModel)]="state.value">
</bSwitch> </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'"> <ng-container *ngIf="ArrayType === 'MapLayers'">
<div class="container"> <div class="container">
<table class="table"> <table class="table">

View File

@ -19,7 +19,7 @@ import {
} from '../../../../../../common/config/public/ClientConfig'; } from '../../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../../settings.service'; import {SettingsService} from '../../settings.service';
import {WebConfig} from '../../../../../../common/config/private/WebConfig'; import {WebConfig} from '../../../../../../common/config/private/WebConfig';
import {UserConfig} from '../../../../../../common/config/private/PrivateConfig'; import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
interface IState { interface IState {
shouldHide(): boolean; shouldHide(): boolean;
@ -78,7 +78,6 @@ export class SettingsEntryComponent
title: string; title: string;
idName: string; idName: string;
private readonly GUID = Utils.GUID(); private readonly GUID = Utils.GUID();
public NavigationLinkTypesEnum = Utils.enumToArray(NavigationLinkTypes);
NavigationLinkTypes = NavigationLinkTypes; NavigationLinkTypes = NavigationLinkTypes;
constructor(private searchQueryParserService: SearchQueryParserService, constructor(private searchQueryParserService: SearchQueryParserService,
@ -142,6 +141,10 @@ export class SettingsEntryComponent
if (this.state.arrayType === UserConfig) { if (this.state.arrayType === UserConfig) {
return 'UserConfig'; return 'UserConfig';
} }
if (this.state.arrayType === JobScheduleConfig) {
return 'JobScheduleConfig';
}
this.state.arrayType; this.state.arrayType;
} }

View File

@ -7,15 +7,18 @@ import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfi
import {ConfigPriority} from '../../../../common/config/public/ClientConfig'; import {ConfigPriority} from '../../../../common/config/public/ClientConfig';
import {CookieNames} from '../../../../common/CookieNames'; import {CookieNames} from '../../../../common/CookieNames';
import {CookieService} from 'ngx-cookie-service'; import {CookieService} from 'ngx-cookie-service';
import {JobDTO} from '../../../../common/entities/job/JobDTO';
@Injectable() @Injectable()
export class SettingsService { export class SettingsService {
public configPriority = ConfigPriority.basic; public configPriority = ConfigPriority.basic;
public settings: BehaviorSubject<WebConfig>; public settings: BehaviorSubject<WebConfig>;
private fetchingSettings = false; private fetchingSettings = false;
public availableJobs: BehaviorSubject<JobDTO[]>;
constructor(private networkService: NetworkService, constructor(private networkService: NetworkService,
private cookieService: CookieService) { private cookieService: CookieService) {
this.availableJobs = new BehaviorSubject([]);
this.settings = new BehaviorSubject<WebConfig>(new WebConfig()); this.settings = new BehaviorSubject<WebConfig>(new WebConfig());
this.getSettings().catch(console.error); 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> { public async getSettings(): Promise<void> {
if (this.fetchingSettings === true) { if (this.fetchingSettings === true) {
return; return;

View File

@ -25,7 +25,6 @@ export class TemplateComponent extends SettingsComponentDirective<any> implement
globalSettingsService: SettingsService globalSettingsService: SettingsService
) { ) {
super( super(
`Template`,
authService, authService,
navigation, navigation,
settingsService, settingsService,

View 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>

View 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;
}
}

View File

@ -13,11 +13,16 @@ a {
margin-left: -4px; margin-left: -4px;
} }
.changed-settings .bootstrap-switch {
.changed-settings.bootstrap-switch {
border-color: var(--bs-primary); border-color: var(--bs-primary);
border-width: 1px; border-width: 1px;
} }
.changed-settings .no-changed-settings .bootstrap-switch {
border-color: var(--bs-gray-400) !important;
}
#toast-container > div { #toast-container > div {
opacity: 1; opacity: 1;
} }