1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Cleaning up config and performance improvement #569

This commit is contained in:
Patrik J. Braun 2023-01-01 16:01:51 +01:00
parent c191549270
commit c5e80f2d84
22 changed files with 379 additions and 472 deletions

View File

@ -910,7 +910,10 @@ export class ServerMediaConfig extends ClientMediaConfig {
export class ServerServiceConfig extends ClientServiceConfig { export class ServerServiceConfig extends ClientServiceConfig {
@ConfigProperty({ @ConfigProperty({
arrayType: 'string', arrayType: 'string',
tags: {secret: true} tags: {
secret: true,
name: 'sessionSecret'
}
}) })
sessionSecret: string[] = []; sessionSecret: string[] = [];

View File

@ -4,8 +4,8 @@ import {ServerConfig} from './PrivateConfig';
import {WebConfigClass} from 'typeconfig/web'; import {WebConfigClass} from 'typeconfig/web';
import {ConfigState} from 'typeconfig/common'; import {ConfigState} from 'typeconfig/common';
import {WebConfigClassBuilder} from '../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder'; import {WebConfigClassBuilder} from '../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {IWebConfigClass} from '../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass'; import {IWebConfigClassPrivate} from '../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
import { TAGS } from '../public/ClientConfig'; import {TAGS} from '../public/ClientConfig';
@WebConfigClass({softReadonly: true}) @WebConfigClass({softReadonly: true})
@ -13,9 +13,9 @@ export class WebConfig extends ServerConfig {
@ConfigState() @ConfigState()
State: any; State: any;
clone(): IWebConfigClass<TAGS> & WebConfig { clone(): IWebConfigClassPrivate<TAGS> & WebConfig {
const wcg = WebConfigClassBuilder.attachInterface(new WebConfig()); const wcg = WebConfigClassBuilder.attachPrivateInterface(new WebConfig());
wcg.load(WebConfigClassBuilder.attachInterface(this).toJSON()); wcg.load(WebConfigClassBuilder.attachPrivateInterface(this).toJSON());
return wcg; return wcg;
} }
} }

View File

@ -73,7 +73,6 @@ import {ScheduledJobsService} from './ui/settings/scheduled-jobs.service';
import {BackendtextService} from './model/backendtext.service'; import {BackendtextService} from './model/backendtext.service';
import {ErrorInterceptor} from './model/network/helper/error.interceptor'; import {ErrorInterceptor} from './model/network/helper/error.interceptor';
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor'; import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component'; import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
import {StringifySearchQuery} from './pipes/StringifySearchQuery'; import {StringifySearchQuery} from './pipes/StringifySearchQuery';
import {AutoCompleteService} from './ui/gallery/search/autocomplete.service'; import {AutoCompleteService} from './ui/gallery/search/autocomplete.service';
@ -99,11 +98,11 @@ import {GalleryFilterComponent} from './ui/gallery/filter/filter.gallery.compone
import {GallerySortingService} from './ui/gallery/navigator/sorting.service'; 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 {WorkflowComponent} from './ui/settings/workflow/workflow.component'; 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';
import {GalleryStatisticComponent} from './ui/settings/gallery-statistic/gallery-statistic.component'; import {GalleryStatisticComponent} from './ui/settings/gallery-statistic/gallery-statistic.component';
import { JobButtonComponent } from './ui/settings/workflow/button/job-button.settings.component';
import { JobProgressComponent } from './ui/settings/workflow/progress/job-progress.settings.component';
import {SettingsEntryComponent} from './ui/settings/template/settings-entry/settings-entry.component';
@Injectable() @Injectable()
export class MyHammerConfig extends HammerGestureConfig { export class MyHammerConfig extends HammerGestureConfig {
@ -271,8 +270,7 @@ Marker.prototype.options.icon = iconDefault;
VersionService, VersionService,
ScheduledJobsService, ScheduledJobsService,
BackendtextService, BackendtextService,
CookieService, CookieService
AbstractSettingsService
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@ -89,7 +89,6 @@
*ngFor="let cp of configPaths" *ngFor="let cp of configPaths"
#setting #setting
#tmpl #tmpl
icon="list"
[ConfigPath]="cp" [ConfigPath]="cp"
[hidden]="!tmpl.HasAvailableSettings"> [hidden]="!tmpl.HasAvailableSettings">
<ng-container <ng-container
@ -99,62 +98,6 @@
<app-settings-gallery-statistic></app-settings-gallery-statistic> <app-settings-gallery-statistic></app-settings-gallery-statistic>
</ng-container> </ng-container>
</app-settings-template> </app-settings-template>
<!-- <app-settings-template #setting #server
icon="list"
[ConfigPath]="'Server'"
[hidden]="!server.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #users
icon="person"
[ConfigPath]="'Users'"
[hidden]="!users.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #db
icon="list"
[ConfigPath]="'Database'"
[hidden]="!db.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #media
icon="camera-slr"
[ConfigPath]="'Media'"
[hidden]="!media.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #gallery
icon="browser"
[ConfigPath]="'Gallery'"
[hidden]="!gallery.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #preview
icon="image"
[ConfigPath]="'Preview'"
[hidden]="!preview.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #metafile
icon="file"
[ConfigPath]="'MetaFile'"
[hidden]="!metafile.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #map
icon="map-marker"
[ConfigPath]="'Map'"
[hidden]="!map.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #sharing
icon="share"
[ConfigPath]="'Sharing'"
[hidden]="!sharing.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #faces
icon="people"
[ConfigPath]="'Faces'"
[hidden]="!faces.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #album
icon="grid-two-up"
[ConfigPath]="'Album'"
[hidden]="!album.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #randomPhoto
icon="random"
[ConfigPath]="'RandomPhoto'"
[hidden]="!randomPhoto.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #duplicates
icon="layers"
[ConfigPath]="'Duplicates'"
[hidden]="!duplicates.HasAvailableSettings"></app-settings-template>
<app-settings-template #setting #indexing
icon="pie-chart"
[ConfigPath]="'Indexing'"
[hidden]="!indexing.HasAvailableSettings"></app-settings-template>-->
</div> </div>
</div> </div>

View File

@ -4,12 +4,13 @@ import {UserRoles} from '../../../../common/entities/UserDTO';
import {NotificationService} from '../../model/notification.service'; import {NotificationService} from '../../model/notification.service';
import {NotificationType} from '../../../../common/entities/NotificationDTO'; import {NotificationType} from '../../../../common/entities/NotificationDTO';
import {NavigationService} from '../../model/navigation.service'; import {NavigationService} from '../../model/navigation.service';
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 {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'; import {WebConfig} from '../../../../common/config/private/WebConfig';
import {ISettingsComponent} from '../settings/template/ISettingsComponent';
import {WebConfigClassBuilder} from '../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
@Component({ @Component({
selector: 'app-admin', selector: 'app-admin',
@ -32,7 +33,9 @@ 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); const wc = WebConfigClassBuilder.attachPrivateInterface(new WebConfig());
this.configPaths = Object.keys(wc.State)
.filter(s => !wc.__state[s].volatile);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

View File

@ -1,28 +0,0 @@
.title {
margin-left: -5px;
}
.btn {
margin-left: 10px;
}
.switch-wrapper {
display: inline-block;
text-align: right;
padding: 0;
float: right;
margin-top: -4px;
margin-bottom: -4px;
}
.changed-settings input {
border-color: var(--bs-primary);
border-width: 1.5px;
}
.changed-settings label {
color: var(--bs-primary);
font-weight: bold;
}

View File

@ -1,261 +0,0 @@
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';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {NotificationService} from '../../../model/notification.service';
import {NavigationService} from '../../../model/navigation.service';
import {AbstractSettingsService} from './abstract.settings.service';
import {Subscription} from 'rxjs';
import {ISettingsComponent} from './ISettingsComponent';
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 {IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
interface ConfigState<T = unknown> {
value: T;
original: T;
default: T;
readonly: boolean;
tags: TAGS;
onChange: () => unknown;
isEnumType: boolean;
isConfigType: boolean;
isConfigArrayType: boolean;
toJSON: () => T;
}
export interface RecursiveState extends ConfigState {
shouldHide: any;
volatile: any;
tags: any;
isConfigType: any;
isConfigArrayType: any;
onChange: any;
isEnumType: any;
value: any;
original: any;
default: any;
readonly: any;
toJSON: any;
[key: string]: RecursiveState;
}
@Directive()
export abstract class SettingsComponentDirective<
T extends RecursiveState> implements OnInit, OnDestroy, ISettingsComponent {
public icon: string;
@Input() ConfigPath: string;
@ViewChild('settingsForm', {static: true})
form: FormControl;
public inProgress = false;
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 authService: AuthenticationService,
private navigation: NavigationService,
public settingsService: AbstractSettingsService,
protected notification: NotificationService,
public globalSettingsService: SettingsService,
sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => T
) {
this.setSliceFN(sliceFN);
}
setSliceFN(sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => T) {
if (sliceFN) {
this.sliceFN = sliceFN;
this.settingsSubscription = this.settingsService.Settings.subscribe(
this.onNewSettings
);
this.onNewSettings(this.settingsService.settingsService.settings.value);
}
}
get Name(): string {
return this.changed ? this.name + '*' : this.name;
}
get Changed(): boolean {
return this.changed;
}
get HasAvailableSettings(): boolean {
return !this.states?.shouldHide || !this.states?.shouldHide();
}
onNewSettings = (s: WebConfig) => {
this.states = this.sliceFN(s.clone()) as RecursiveState;
const instrument = (st: RecursiveState, parent: RecursiveState) => {
const shouldHide = (state: RecursiveState) => {
return () => {
if (state.volatile) {
return true;
}
if (state.tags &&
((state.tags.relevant && !state.tags.relevant(parent.value))
|| state.tags.secret)) {
return true;
}
// if all sub elements are hidden, hide the parent too.
if (state.isConfigType) {
if (state.value.__state &&
Object.keys(state.value.__state).findIndex(k => !st.value.__state[k].shouldHide()) === -1) {
return true;
}
}
if (state.isConfigArrayType) {
for (let i = 0; i < state.value?.length; ++i) {
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 ||
(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,
['__propPath', '__created', '__prototype', '__rootConfig']));
};
};
st.shouldHide = shouldHide(st);
st.onChange = this.onOptionChange;
st.rootConfig = parent?.value;
if (typeof st.value !== 'undefined') {
st.original = Utils.clone(st.value);
}
if (st.isConfigType) {
for (const k of Object.keys(st.value.__state)) {
instrument(st.value.__state[k], st);
}
}
if (st.isConfigArrayType) {
for (let i = 0; i < st.value?.length; ++i) {
for (const k of Object.keys(st.value[i].__state)) {
instrument(st.value[i].__state[k], st);
}
}
}
};
instrument(this.states, null);
this.icon = this.states.tags?.uiIcon;
};
onOptionChange = () => {
setTimeout(() => {
const settingsSame = (state: RecursiveState): boolean => {
if (typeof state === 'undefined') {
return true;
}
if (typeof state.original === 'object') {
return Utils.equalsFilter(state.value, state.original,
['__propPath', '__created', '__prototype', '__rootConfig', '__state']);
}
if (typeof state.original !== 'undefined') {
return state.value === state.original;
}
const keys = Object.keys(state);
for (const key of keys) {
if (settingsSame(state[key]) === false) {
return false;
}
}
return true;
};
this.changed = !settingsSame(this.states);
}, 0);
};
ngOnInit(): void {
if (
!this.authService.isAuthenticated() ||
this.authService.user.value.role < UserRoles.Admin
) {
this.navigation.toLogin();
return;
}
this.getSettings();
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
this.subscription = this.form.valueChanges.subscribe(() => {
this.onOptionChange();
});
}
ngOnDestroy(): void {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
if (this.settingsSubscription != null) {
this.settingsSubscription.unsubscribe();
}
}
public reset(): void {
this.getSettings();
}
stateToSettings(): T {
return WebConfigClassBuilder.attachInterface(this.states.value).toJSON();
}
public async save(): Promise<boolean> {
this.inProgress = true;
this.error = '';
try {
await this.settingsService.updateSettings(this.stateToSettings(), this.ConfigPath);
await this.getSettings();
this.notification.success(
this.Name + ' ' + $localize`settings saved`,
$localize`Success`
);
this.inProgress = false;
return true;
} catch (err) {
console.error(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
return false;
}
private async getSettings(): Promise<void> {
await this.settingsService.getSettings();
this.changed = false;
}
}

View File

@ -1,24 +0,0 @@
import {BehaviorSubject} from 'rxjs';
import {SettingsService} from '../settings.service';
import {WebConfig} from '../../../../../common/config/private/WebConfig';
import {NetworkService} from '../../../model/network/network.service';
import {Injectable} from '@angular/core';
@Injectable()
export class AbstractSettingsService {
constructor(public settingsService: SettingsService,
private networkService: NetworkService) {
}
get Settings(): BehaviorSubject<WebConfig> {
return this.settingsService.settings;
}
public getSettings(): Promise<void> {
return this.settingsService.getSettings();
}
public updateSettings(settings: Record<string, any>, settingsPath: string): Promise<void> {
return this.networkService.putJson('/settings', {settings, settingsPath});
}
}

View File

@ -4,17 +4,18 @@ import {NetworkService} from '../../model/network/network.service';
import {WebConfig} from '../../../../common/config/private/WebConfig'; import {WebConfig} from '../../../../common/config/private/WebConfig';
import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfigClassBuilder'; import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {ConfigPriority} from '../../../../common/config/public/ClientConfig'; import {ConfigPriority, TAGS} 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 {DefaultsJobs, JobDTO} from '../../../../common/entities/job/JobDTO'; import {DefaultsJobs, JobDTO} from '../../../../common/entities/job/JobDTO';
import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO'; import {StatisticDTO} from '../../../../common/entities/settings/StatisticDTO';
import {ScheduledJobsService} from './scheduled-jobs.service'; import {ScheduledJobsService} from './scheduled-jobs.service';
import {IWebConfigClassPrivate} from '../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
@Injectable() @Injectable()
export class SettingsService { export class SettingsService {
public configPriority = ConfigPriority.basic; public configPriority = ConfigPriority.basic;
public settings: BehaviorSubject<WebConfig>; public settings: BehaviorSubject<IWebConfigClassPrivate<TAGS> & WebConfig>;
private fetchingSettings = false; private fetchingSettings = false;
public availableJobs: BehaviorSubject<JobDTO[]>; public availableJobs: BehaviorSubject<JobDTO[]>;
public statistic: BehaviorSubject<StatisticDTO>; public statistic: BehaviorSubject<StatisticDTO>;
@ -24,7 +25,7 @@ export class SettingsService {
private cookieService: CookieService) { private cookieService: CookieService) {
this.statistic = new BehaviorSubject(null); this.statistic = new BehaviorSubject(null);
this.availableJobs = new BehaviorSubject([]); this.availableJobs = new BehaviorSubject([]);
this.settings = new BehaviorSubject<WebConfig>(new WebConfig()); this.settings = new BehaviorSubject<IWebConfigClassPrivate<TAGS> & WebConfig>(WebConfigClassBuilder.attachPrivateInterface(new WebConfig()));
this.getSettings().catch(console.error); this.getSettings().catch(console.error);
if (this.cookieService.check(CookieNames.configPriority)) { if (this.cookieService.check(CookieNames.configPriority)) {
@ -59,7 +60,7 @@ export class SettingsService {
} }
this.fetchingSettings = true; this.fetchingSettings = true;
try { try {
const wcg = WebConfigClassBuilder.attachInterface(new WebConfig()); const wcg = WebConfigClassBuilder.attachPrivateInterface(new WebConfig());
wcg.load( wcg.load(
await this.networkService.getJson<Promise<WebConfig>>('/settings') await this.networkService.getJson<Promise<WebConfig>>('/settings')
); );
@ -70,6 +71,11 @@ export class SettingsService {
this.fetchingSettings = false; this.fetchingSettings = false;
} }
public updateSettings(settings: Record<string, any>, settingsPath: string): Promise<void> {
return this.networkService.putJson('/settings', {settings, settingsPath});
}
configPriorityChanged(): void { configPriorityChanged(): void {
// save it for some years // save it for some years
this.cookieService.set( this.cookieService.set(

View File

@ -12,9 +12,9 @@
<label class="col-md-2 control-label" [for]="idName">{{name}}</label> <label class="col-md-2 control-label" [for]="idName">{{name}}</label>
<div class="col-md-10"> <div class="col-md-10">
<div class="input-group"> <div class="input-group" [ngSwitch]="uiType">
<app-gallery-search-field <app-gallery-search-field
*ngIf="Type === 'SearchQuery'" *ngSwitchCase="'SearchQuery'"
[(ngModel)]="state.value" [(ngModel)]="state.value"
[id]="idName" [id]="idName"
[name]="idName" [name]="idName"
@ -24,18 +24,11 @@
placeholder="Search Query"> placeholder="Search Query">
</app-gallery-search-field> </app-gallery-search-field>
<div class="input-group"> <div class="input-group"
*ngSwitchCase="'StringInput'">
<input <input
*ngIf="!state.isEnumType && [type]="HTMLInputType" [min]="state.min" [max]="state.max" class="form-control"
!state.isEnumArrayType && [placeholder]="placeholder"
Type !== 'boolean' &&
Type !== 'SearchQuery' &&
ArrayType !== 'MapLayers' &&
ArrayType !== 'NavigationLinkConfig' &&
ArrayType !== 'JobScheduleConfig' &&
ArrayType !== 'UserConfig'"
[type]="type" [min]="state.min" [max]="state.max" class="form-control"
[placeholder]="PlaceHolder"
[title]="title" [title]="title"
[(ngModel)]="StringValue" [(ngModel)]="StringValue"
(ngModelChange)="onChange($event)" (ngModelChange)="onChange($event)"
@ -49,7 +42,7 @@
</div> </div>
<select <select
*ngIf="state.isEnumType" *ngSwitchCase="'EnumType'"
[id]="idName" [id]="idName"
[name]="idName" [name]="idName"
[title]="title" [title]="title"
@ -62,7 +55,7 @@
<bSwitch <bSwitch
*ngIf="Type === 'boolean'" *ngSwitchCase="'Boolean'"
class="switch" class="switch"
[id]="idName" [id]="idName"
[name]="idName" [name]="idName"
@ -83,7 +76,7 @@
<app-settings-workflow <app-settings-workflow
class="w-100" class="w-100"
*ngIf="ArrayType === 'JobScheduleConfig'" *ngSwitchCase="'JobScheduleConfig'"
[(ngModel)]="state.value" [(ngModel)]="state.value"
[id]="idName" [id]="idName"
[name]="idName" [name]="idName"
@ -91,7 +84,7 @@
(ngModelChange)="onChange($event)"> (ngModelChange)="onChange($event)">
</app-settings-workflow> </app-settings-workflow>
<ng-container *ngIf="ArrayType === 'MapLayers'"> <ng-container *ngSwitchCase="'MapLayers'">
<div class="container"> <div class="container">
<table class="table"> <table class="table">
<thead> <thead>
@ -131,7 +124,7 @@
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="ArrayType === 'NavigationLinkConfig'"> <ng-container *ngSwitchCase="'NavigationLinkConfig'">
<div class="container"> <div class="container">
<div class="row mt-1 mb-1 bg-light" *ngFor="let link of state.value; let i = index"> <div class="row mt-1 mb-1 bg-light" *ngFor="let link of state.value; let i = index">
<div class="col ps-0"> <div class="col ps-0">
@ -194,7 +187,7 @@
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="ArrayType === 'UserConfig'"> <ng-container *ngSwitchCase="'UserConfig'">
<div class="container ps-0 pe-0"> <div class="container ps-0 pe-0">
<div class="row ms-0 me-0 mt-1 mb-1 bg-light" *ngFor="let item of state.value; let i = index"> <div class="row ms-0 me-0 mt-1 mb-1 bg-light" *ngFor="let item of state.value; let i = index">
<div class="col ps-0"> <div class="col ps-0">
@ -239,14 +232,14 @@
<div class="row me-0"> <div class="row me-0">
<div class="col pe-0"> <div class="col pe-0">
<button class="btn btn-primary float-end" <button class="btn btn-primary float-end"
(click)="AddNew()" i18n>+ Add Link (click)="AddNew()" i18n>+ Add
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="state.isEnumArrayType"> <ng-container *ngSwitchCase="'EnumArray'">
<ng-container *ngFor="let _ of state.value; let i=index"> <ng-container *ngFor="let _ of state.value; let i=index">
<div class="row col-12 mt-1 m-0 p-0"> <div class="row col-12 mt-1 m-0 p-0">
<div class="col p-0"> <div class="col p-0">
@ -263,7 +256,7 @@
</div> </div>
<ng-container *ngIf="state.type === 'array'"> <ng-container>
<div class="col-auto pe-0"> <div class="col-auto pe-0">
<button class="btn btn-secondary float-end" <button class="btn btn-secondary float-end"
[id]="'list_btn_'+idName+i" [id]="'list_btn_'+idName+i"
@ -274,12 +267,12 @@
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="state.type === 'array'"> <ng-container>
<div class="col-12 p-0"> <div class="col-12 p-0">
<button class="btn btn-primary mt-1 float-end" <button class="btn btn-primary mt-1 float-end"
[id]="'btn_add_'+idName" [id]="'btn_add_'+idName"
[name]="'btn_add_'+idName" [name]="'btn_add_'+idName"
(click)="AddNew()">+Add (click)="AddNew()" i18n>+Add
</button> </button>
</div> </div>
</ng-container> </ng-container>
@ -292,13 +285,14 @@
class="oi oi-warning text-warning warning-icon ms-2" *ngIf="dockerWarning && changed"></span> class="oi oi-warning text-warning warning-icon ms-2" *ngIf="dockerWarning && changed"></span>
</div> </div>
</div> </div>
<small class="form-text text-muted" *ngIf="description">{{description}} <small class="form-text text-muted" *ngIf="description">{{description}}
<span *ngIf="Type==='array' && (state.arrayType === 'string' || isNumberArray)" i18n>';' separated list.</span> <span *ngIf="type==='array' && (state.arrayType === 'string' || isNumberArray)" i18n>';' separated list.</span>
<a *ngIf="state.tags?.githubIssue" <a *ngIf="state.tags?.githubIssue"
[href]="'https://github.com/bpatrik/pigallery2/issues/'+state.tags?.githubIssue">See [href]="'https://github.com/bpatrik/pigallery2/issues/'+state.tags?.githubIssue">
<ng-container i18n>See</ng-container>
#{{state.tags?.githubIssue}}.</a> #{{state.tags?.githubIssue}}.</a>
</small> </small>
<ng-content></ng-content>
</div> </div>
</div> </div>
</ng-container> </ng-container>

View File

@ -1,4 +1,4 @@
import {Component, forwardRef, Input, OnChanges} from '@angular/core'; import {Component, forwardRef, OnChanges} from '@angular/core';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms'; import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
import {Utils} from '../../../../../../common/Utils'; import {Utils} from '../../../../../../common/Utils';
import {propertyTypes} from 'typeconfig/common'; import {propertyTypes} from 'typeconfig/common';
@ -57,11 +57,15 @@ export class SettingsEntryComponent
state: IState; state: IState;
isNumberArray = false; isNumberArray = false;
isNumber = false; isNumber = false;
type = 'text'; HTMLInputType = 'text';
title: string; title: string;
idName: string; idName: string;
private readonly GUID = Utils.GUID(); private readonly GUID = Utils.GUID();
NavigationLinkTypes = NavigationLinkTypes; NavigationLinkTypes = NavigationLinkTypes;
public type: string | object;
public arrayType: string;
public uiType: string;
constructor(private searchQueryParserService: SearchQueryParserService, constructor(private searchQueryParserService: SearchQueryParserService,
public settingsService: SettingsService, public settingsService: SettingsService,
@ -93,12 +97,8 @@ export class SettingsEntryComponent
return this.state.shouldHide && this.state.shouldHide(); return this.state.shouldHide && this.state.shouldHide();
} }
get PlaceHolder(): string {
return this.placeholder || this.state.tags?.hint || this.state.default;
}
get defaultStr(): string { get defaultStr(): string {
if (this.Type === 'SearchQuery') { if (this.type === 'SearchQuery') {
return ( return (
'\'' + this.searchQueryParserService.stringify(this.state.default) + '\'' '\'' + this.searchQueryParserService.stringify(this.state.default) + '\''
); );
@ -111,26 +111,6 @@ export class SettingsEntryComponent
return this.state.default; return this.state.default;
} }
get Type(): string | object {
return this.state.tags?.uiType || this.state.type;
}
get ArrayType(): string {
if (this.state.arrayType === MapLayers) {
return 'MapLayers';
}
if (this.state.arrayType === NavigationLinkConfig) {
return 'NavigationLinkConfig';
}
if (this.state.arrayType === UserConfig) {
return 'UserConfig';
}
if (this.state.arrayType === JobScheduleConfig) {
return 'JobScheduleConfig';
}
this.state.arrayType;
}
get StringValue(): string { get StringValue(): string {
if ( if (
@ -192,6 +172,44 @@ export class SettingsEntryComponent
if (!this.state) { if (!this.state) {
return; return;
} }
// cache type overrides
this.type = this.state.tags?.uiType || this.state.type;
this.arrayType = null;
if (this.state.arrayType === MapLayers) {
this.arrayType = 'MapLayers';
} else if (this.state.arrayType === NavigationLinkConfig) {
this.arrayType = 'NavigationLinkConfig';
} else if (this.state.arrayType === UserConfig) {
this.arrayType = 'UserConfig';
} else if (this.state.arrayType === JobScheduleConfig) {
this.arrayType = 'JobScheduleConfig';
} else {
this.arrayType = this.state.arrayType;
}
this.uiType = this.arrayType;
if (!this.state.isEnumType &&
!this.state.isEnumArrayType &&
this.type !== 'boolean' &&
this.type !== 'SearchQuery' &&
this.arrayType !== 'MapLayers' &&
this.arrayType !== 'NavigationLinkConfig' &&
this.arrayType !== 'JobScheduleConfig' &&
this.arrayType !== 'UserConfig') {
this.uiType = 'StringInput';
}
if (this.type === 'SearchQuery') {
this.uiType = 'SearchQuery';
} else if (this.state.isEnumType) {
this.uiType = 'EnumType';
} else if (this.type === 'boolean') {
this.uiType = 'Boolean';
} else if (this.state.isEnumArrayType) {
this.uiType = 'EnumArray';
}
this.placeholder = this.state.tags?.hint || this.state.default;
if (this.state.tags?.uiOptions) { if (this.state.tags?.uiOptions) {
this.state.isEnumType = true; this.state.isEnumType = true;
} }
@ -218,11 +236,11 @@ export class SettingsEntryComponent
if (this.isNumber) { if (this.isNumber) {
this.type = 'number'; this.HTMLInputType = 'number';
} else if (this.state.type === 'password') { } else if (this.state.type === 'password') {
this.type = 'password'; this.HTMLInputType = 'password';
} else { } else {
this.type = 'text'; this.HTMLInputType = 'text';
} }
this.description = this.description || this.state.description; this.description = this.description || this.state.description;
if (this.state.tags) { if (this.state.tags) {

View File

@ -0,0 +1,28 @@
.title {
margin-left: -5px;
}
.btn {
margin-left: 10px;
}
.switch-wrapper {
display: inline-block;
text-align: right;
padding: 0;
float: right;
margin-top: -4px;
margin-bottom: -4px;
}
.changed-settings input {
border-color: var(--bs-primary);
border-width: 1.5px;
}
.changed-settings label {
color: var(--bs-primary);
font-weight: bold;
}

View File

@ -84,7 +84,7 @@
{{job.description}} {{job.description}}
</div> </div>
<app-settings-job-button <app-settings-job-button
*ngIf="!job.relevant || job.relevant(globalSettingsService.settings | async)" *ngIf="!job.relevant || job.relevant(settingsService.settings | async)"
class="mt-2 mb-1 mb-md-0 mt-md-0 float-left me-2" class="mt-2 mb-1 mb-md-0 mt-md-0 float-left me-2"
[soloRun]="true" [soloRun]="true"
(jobError)="error=$event" (jobError)="error=$event"
@ -95,7 +95,7 @@
<ng-container *ngFor="let job of rStates.tags?.uiJob"> <ng-container *ngFor="let job of rStates.tags?.uiJob">
<ng-container <ng-container
*ngIf="getProgress(job.job) && !job.hideProgress && (!job.relevant || job.relevant(globalSettingsService.settings | async))"> *ngIf="getProgress(job.job) && !job.hideProgress && (!job.relevant || job.relevant(settingsService.settings | async))">
<hr class="mt-1"/> <hr class="mt-1"/>
<app-settings-job-progress <app-settings-job-progress
class="d-block mb-2" class="d-block mb-2"

View File

@ -1,59 +1,284 @@
import {Component, OnInit} from '@angular/core'; import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AuthenticationService} from '../../../model/network/authentication.service'; import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service'; import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service'; import {NotificationService} from '../../../model/notification.service';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
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 {AbstractSettingsService} from '../_abstract/abstract.settings.service';
import {JobProgressDTO} from '../../../../../common/entities/job/JobProgressDTO'; import {JobProgressDTO} from '../../../../../common/entities/job/JobProgressDTO';
import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO'; import {JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service'; import {ScheduledJobsService} from '../scheduled-jobs.service';
import {FormControl} from '../../../../../../node_modules/@angular/forms';
import {Subscription} from 'rxjs';
import {IWebConfigClassPrivate} from '../../../../../../node_modules/typeconfig/src/decorators/class/IWebConfigClass';
import {ConfigPriority, TAGS} from '../../../../../common/config/public/ClientConfig';
import {Utils} from '../../../../../common/Utils';
import {UserRoles} from '../../../../../common/entities/UserDTO';
import {WebConfigClassBuilder} from '../../../../../../node_modules/typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {ErrorDTO} from '../../../../../common/entities/Error';
import {ISettingsComponent} from './ISettingsComponent';
interface ConfigState {
value: {
[key: string]: RecursiveState;
};
default: {
[key: string]: RecursiveState;
};
readonly?: boolean;
tags?: TAGS;
volatile?: boolean;
isEnumType?: boolean;
isConfigType?: boolean;
isConfigArrayType?: boolean;
}
export interface RecursiveState extends ConfigState {
value: any;
default: any;
volatile?: any;
tags?: any;
isConfigType?: any;
isConfigArrayType?: any;
isEnumType?: any;
readonly?: any;
toJSON?: any;
onChange?: any;
original?: any;
shouldHide?: any;
[key: string]: RecursiveState;
}
@Component({ @Component({
selector: 'app-settings-template', selector: 'app-settings-template',
templateUrl: './template.component.html', templateUrl: './template.component.html',
styleUrls: ['./template.component.css', styleUrls: ['./template.component.css']
'../_abstract/abstract.settings.component.css']
}) })
export class TemplateComponent extends SettingsComponentDirective<any> implements OnInit { export class TemplateComponent implements OnInit, OnDestroy, ISettingsComponent {
public icon: string;
@Input() ConfigPath: string;
@ViewChild('settingsForm', {static: true})
form: FormControl;
public inProgress = false;
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: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState;
constructor( constructor(
authService: AuthenticationService, protected authService: AuthenticationService,
navigation: NavigationService, private navigation: NavigationService,
notification: NotificationService, protected notification: NotificationService,
settingsService: AbstractSettingsService, public settingsService: SettingsService,
globalSettingsService: SettingsService,
public jobsService: ScheduledJobsService, public jobsService: ScheduledJobsService,
) { ) {
super(
authService,
navigation,
settingsService,
notification,
globalSettingsService
);
} }
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment if (
// @ts-ignore !this.authService.isAuthenticated() ||
this.authService.user.value.role < UserRoles.Admin
) {
this.navigation.toLogin();
return;
}
this.getSettings();
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
this.subscription = this.form.valueChanges.subscribe(() => {
this.onOptionChange();
});
if (!this.ConfigPath) { if (!this.ConfigPath) {
this.setSliceFN(c => ({value: c, isConfigType: true, type: WebConfig})); this.setSliceFN(c => ({value: c as any, isConfigType: true, type: WebConfig} as any));
} else { } else {
this.setSliceFN(c => c.__state[this.ConfigPath]); this.setSliceFN(c => c.__state[this.ConfigPath]);
} }
this.name = this.states.tags?.name || this.ConfigPath; this.name = this.states.tags?.name || this.ConfigPath;
} }
getKeys(states: any) {
ngOnDestroy(): void {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
if (this.settingsSubscription != null) {
this.settingsSubscription.unsubscribe();
}
}
setSliceFN(sliceFN?: (s: IWebConfigClassPrivate<TAGS> & WebConfig) => ConfigState) {
if (sliceFN) {
this.sliceFN = sliceFN;
this.settingsSubscription = this.settingsService.settings.subscribe(
this.onNewSettings
);
}
}
get Name(): string {
return this.changed ? this.name + '*' : this.name;
}
get Changed(): boolean {
return this.changed;
}
get HasAvailableSettings(): boolean {
return !this.states?.shouldHide || !this.states?.shouldHide();
}
onNewSettings = (s: IWebConfigClassPrivate<TAGS> & WebConfig) => {
this.states = this.sliceFN(s.clone()) as RecursiveState;
const instrument = (st: RecursiveState, parent: RecursiveState) => {
const shouldHide = (state: RecursiveState) => {
return () => {
if (state.volatile) {
return true;
}
if (state.tags &&
((state.tags.relevant && !state.tags.relevant(parent.value))
|| state.tags.secret)) {
return true;
}
// if all sub elements are hidden, hide the parent too.
if (state.isConfigType) {
if (state.value.__state &&
Object.keys(state.value.__state).findIndex(k => !st.value.__state[k].shouldHide()) === -1) {
return true;
}
}
if (state.isConfigArrayType) {
for (let i = 0; i < state.value?.length; ++i) {
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.settingsService.configPriority ||
(this.settingsService.configPriority === ConfigPriority.basic &&
state.tags?.dockerSensitive && this.settingsService.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,
['__propPath', '__created', '__prototype', '__rootConfig']));
};
};
st.shouldHide = shouldHide(st);
st.onChange = this.onOptionChange;
st.rootConfig = parent?.value;
if (typeof st.value !== 'undefined') {
st.original = Utils.clone(st.value);
}
if (st.isConfigType) {
for (const k of Object.keys(st.value.__state)) {
instrument(st.value.__state[k], st);
}
}
if (st.isConfigArrayType) {
for (let i = 0; i < st.value?.length; ++i) {
for (const k of Object.keys(st.value[i].__state)) {
instrument(st.value[i].__state[k], st);
}
}
}
};
instrument(this.states, null);
this.icon = this.states.tags?.uiIcon;
};
onOptionChange = () => {
setTimeout(() => {
const settingsSame = (state: RecursiveState): boolean => {
if (typeof state === 'undefined') {
return true;
}
if (typeof state.original === 'object') {
return Utils.equalsFilter(state.value, state.original,
['__propPath', '__created', '__prototype', '__rootConfig', '__state']);
}
if (typeof state.original !== 'undefined') {
return state.value === state.original;
}
const keys = Object.keys(state);
for (const key of keys) {
if (settingsSame(state[key]) === false) {
return false;
}
}
return true;
};
this.changed = !settingsSame(this.states);
}, 0);
};
public reset(): void {
this.getSettings();
}
public async save(): Promise<boolean> {
this.inProgress = true;
this.error = '';
try {
const state = WebConfigClassBuilder.attachInterface(this.states.value).toJSON();
await this.settingsService.updateSettings(state, this.ConfigPath);
await this.getSettings();
this.notification.success(
this.Name + ' ' + $localize`settings saved`,
$localize`Success`
);
this.inProgress = false;
return true;
} catch (err) {
console.error(err);
if (err.message) {
this.error = (err as ErrorDTO).message;
}
}
this.inProgress = false;
return false;
}
private async getSettings(): Promise<void> {
await this.settingsService.getSettings();
this.changed = false;
}
getKeys(states: any): string[] {
if (states.keys) {
return states.keys;
}
const s = states.value.__state; const s = states.value.__state;
return Object.keys(s).sort((a, b) => { const keys = Object.keys(s).sort((a, b) => {
if ((s[a].isConfigType || s[a].isConfigArrayType) !== (s[b].isConfigType || s[b].isConfigArrayType)) { if ((s[a].isConfigType || s[a].isConfigArrayType) !== (s[b].isConfigType || s[b].isConfigArrayType)) {
if (s[a].isConfigType || s[a].isConfigArrayType) { if (s[a].isConfigType || s[a].isConfigArrayType) {
return 1; return 1;
@ -68,6 +293,8 @@ export class TemplateComponent extends SettingsComponentDirective<any> implement
return (s[a].tags?.name as string || a).localeCompare(s[b].tags?.name || b); return (s[a].tags?.name as string || a).localeCompare(s[b].tags?.name || b);
}); });
states.keys = keys;
return states.keys;
} }
getProgress(jobName: string): JobProgressDTO { getProgress(jobName: string): JobProgressDTO {