From 36d4641e9d3ba9a4bc404d2f9bd7537fea167b9f Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Fri, 30 Dec 2022 15:35:07 +0100 Subject: [PATCH] Adding job schedules to config #569 --- src/common/config/private/PrivateConfig.ts | 85 ++--- src/common/entities/job/JobScheduleDTO.ts | 4 +- src/frontend/app/app.module.ts | 6 + .../app/ui/admin/admin.component.html | 11 +- src/frontend/app/ui/admin/admin.component.ts | 5 +- .../_abstract/abstract.settings.component.ts | 28 +- .../settings-entry.component.html | 13 +- .../settings-entry.component.ts | 7 +- .../app/ui/settings/settings.service.ts | 9 + .../settings/template/template.component.ts | 1 - .../settings/workflow/workflow.component.css | 0 .../settings/workflow/workflow.component.html | 257 +++++++++++++++ .../settings/workflow/workflow.component.ts | 294 ++++++++++++++++++ src/frontend/styles.css | 7 +- 14 files changed, 660 insertions(+), 67 deletions(-) create mode 100644 src/frontend/app/ui/settings/workflow/workflow.component.css create mode 100644 src/frontend/app/ui/settings/workflow/workflow.component.html create mode 100644 src/frontend/app/ui/settings/workflow/workflow.component.ts diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 41b8fc03..fef97ace 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -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 = {}; @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} ), ]; diff --git a/src/common/entities/job/JobScheduleDTO.ts b/src/common/entities/job/JobScheduleDTO.ts index 9ab0ecd0..4ccede90 100644 --- a/src/common/entities/job/JobScheduleDTO.ts +++ b/src/common/entities/job/JobScheduleDTO.ts @@ -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; allowParallelRun: boolean; trigger: | NeverJobTrigger diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 2434aa14..f5fa7c32 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -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}, diff --git a/src/frontend/app/ui/admin/admin.component.html b/src/frontend/app/ui/admin/admin.component.html index 6c6f40eb..917e9c80 100644 --- a/src/frontend/app/ui/admin/admin.component.html +++ b/src/frontend/app/ui/admin/admin.component.html @@ -85,7 +85,14 @@
- +
diff --git a/src/frontend/app/ui/admin/admin.component.ts b/src/frontend/app/ui/admin/admin.component.ts index a32aa294..5ae50750 100644 --- a/src/frontend/app/ui/admin/admin.component.ts +++ b/src/frontend/app/ui/admin/admin.component.ts @@ -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 { diff --git a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts index 5817077f..0d1d8a0f 100644 --- a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts +++ b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts @@ -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 { 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, diff --git a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.html b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.html index 00dd18c5..4c3cd803 100644 --- a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.html +++ b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.html @@ -24,7 +24,6 @@ [disabled]="state.readonly || Disabled" (change)="onChange($event)" placeholder="Search Query"> -
@@ -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"> + + + +
diff --git a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts index fb08969f..5ad080e4 100644 --- a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts +++ b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts @@ -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; } diff --git a/src/frontend/app/ui/settings/settings.service.ts b/src/frontend/app/ui/settings/settings.service.ts index b71209c5..fb2e4ee7 100644 --- a/src/frontend/app/ui/settings/settings.service.ts +++ b/src/frontend/app/ui/settings/settings.service.ts @@ -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; private fetchingSettings = false; + public availableJobs: BehaviorSubject; constructor(private networkService: NetworkService, private cookieService: CookieService) { + this.availableJobs = new BehaviorSubject([]); this.settings = new BehaviorSubject(new WebConfig()); this.getSettings().catch(console.error); @@ -27,6 +30,12 @@ export class SettingsService { } + public async getAvailableJobs(): Promise { + this.availableJobs.next( + await this.networkService.getJson('/admin/jobs/available') + ); + } + public async getSettings(): Promise { if (this.fetchingSettings === true) { return; diff --git a/src/frontend/app/ui/settings/template/template.component.ts b/src/frontend/app/ui/settings/template/template.component.ts index 5a009348..ac56110b 100644 --- a/src/frontend/app/ui/settings/template/template.component.ts +++ b/src/frontend/app/ui/settings/template/template.component.ts @@ -25,7 +25,6 @@ export class TemplateComponent extends SettingsComponentDirective implement globalSettingsService: SettingsService ) { super( - `Template`, authService, navigation, settingsService, diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.css b/src/frontend/app/ui/settings/workflow/workflow.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.html b/src/frontend/app/ui/settings/workflow/workflow.component.html new file mode 100644 index 00000000..886ea4ca --- /dev/null +++ b/src/frontend/app/ui/settings/workflow/workflow.component.html @@ -0,0 +1,257 @@ + +
+
+
+
+
+ + {{schedule.name}} + + + every + {{periods[$any(schedule.trigger).periodicity]}} {{atTimeLocal($any(schedule.trigger).atTime) | date:"HH:mm (z)"}} + + @{{$any(schedule.trigger).time | date:"medium"}} + never + + after + {{$any(schedule.trigger).afterScheduleName}} + + +
+
+ + +
+
+
+ + +
+
+ +
+
+ +
+ {{backendTextService.getJobName(schedule.jobName)}} +
+
+ + +
+
+
+ +
+ + Set the time to run the job. + +
+
+ +
+ +
+ + The job will run after that job finishes. + +
+
+ + +
+ +
+ +
+
+ +
+ +
+ + +
+
+
+ +
+ + + Enables the job to start even if another job is already running. + +
+
+
+ + +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + ';' separated integers. + {{backendTextService.get(configEntry.description)}} + +
+
+ +
+
+
+ + + + +
+
+ + + + + + diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.ts b/src/frontend/app/ui/settings/workflow/workflow.component.ts new file mode 100644 index 00000000..2631068c --- /dev/null +++ b/src/frontend/app/ui/settings/workflow/workflow.component.ts @@ -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; + + 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, 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; + } + +} diff --git a/src/frontend/styles.css b/src/frontend/styles.css index 2ffeae4b..ac1c4c56 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -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; }