Skip to content

Commit

Permalink
Add support for lazy loading of options on custom/overwritten fields …
Browse files Browse the repository at this point in the history
…on the destination, added submit loader on options page, fixes schedule
  • Loading branch information
sp90 committed Nov 15, 2024
1 parent 5af8a46 commit e8976e6
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 33 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0",
"@sparkle-ui/core": "^0.4.21",
"@sparkle-ui/core": "^0.4.23",
"dayjs": "^1.11.13",
"ngxtension": "^4.0.0",
"rxjs": "~7.8.0",
Expand Down
7 changes: 4 additions & 3 deletions src/app/backup/backup.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,16 +518,16 @@ export class BackupState {
})),
},
Schedule: scheduleFormValue.autoRun
? null
: {
? {
Repeat: scheduleRepeat,
Time: scheduleFormValue.nextTime?.date
? new Date(
`${scheduleFormValue.nextTime.date}T${scheduleFormValue.nextTime?.time || '00:00:00'}`
).toISOString()
: null,
AllowedDays: allowedDays,
},
}
: null,
// ExtraOptions: {
// BackupID: this.backupId(),
// Operation: this.backupId() ? 'Update' : 'Create',
Expand Down Expand Up @@ -589,6 +589,7 @@ export class BackupState {
element.type === 'FolderTree' ||
element.type === 'Password' ||
element.type === 'Enumeration' ||
element.type === 'NonValidatedSelectableString' ||
element.type === 'Path'
) {
group.addControl(element.name as string, fb.control(defaultValue));
Expand Down
31 changes: 30 additions & 1 deletion src/app/backup/destination/destination.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ <h3>Google Drive</h3>
formControlName="{{ item.formView.name }}" />

@if (item.oauthField === item.formView.name) {
<span prefix class="link" (click)="oauthStartTokenCreation(item.formViewType)">AuthID</span>
<span prefix class="link" (click)="oauthStartTokenCreation(item.destinationType)">AuthID</span>
} @else {
<spk-icon prefix>password</spk-icon>
}
Expand Down Expand Up @@ -340,6 +340,35 @@ <h3>Google Drive</h3>
}
</ng-container>
</spk-select>
} @else if (item.formView.type === 'NonValidatedSelectableString') {
<spk-select [isFreeText]="true">
<label
for="destination-{{ item.formGroupName }}-{{ item.index }}-{{
item.asHttpOptions ? 'http' : 'other'
}}">
{{ item.formView.shortDescription ?? item.formView.name }}

@if (item.formView.longDescription) {
<spk-tooltip class="primary" [message]="item.formView.longDescription">
<spk-icon>question</spk-icon>
</spk-tooltip>
}
</label>
<input
type="search"
id="destination-{{ item.formGroupName }}-{{ item.index }}-{{
item.asHttpOptions ? 'http' : 'other'
}}"
formControlName="{{ item.formView.name }}" />

<ng-container options>
@for (option of item.formView.loadOptions(injector)(); track $index) {
<option [value]="option.value">
{{ option.key }} {{ option.value ? '(' + option.value + ')' : '' }}
</option>
}
</ng-container>
</spk-select>
} @else if (item.formView.type === 'Size') {
<div class="form-column" [formGroupName]="item.formView.name">
<label
Expand Down
16 changes: 11 additions & 5 deletions src/app/backup/destination/destination.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { JsonPipe, NgTemplateOutlet } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, signal, viewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
inject,
Injector,
signal,
viewChild,
} from '@angular/core';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
Expand Down Expand Up @@ -99,6 +108,7 @@ export default class DestinationComponent {
#dupServer = inject(DuplicatiServerService);
#backupState = inject(BackupState);
#dialog = inject(SparkleDialogService);
injector = inject(Injector);

formRef = viewChild.required<ElementRef<HTMLFormElement>>('formRef');

Expand Down Expand Up @@ -142,10 +152,6 @@ export default class DestinationComponent {
}

targetUrl = computed(() => {
const destinationFormSignal = this.destinationFormSignal();

// console.log(destinationFormSignal);

const targetUrls = this.#backupState.getCurrentTargetUrl();
const targetUrl = targetUrls[0];

Expand Down
37 changes: 35 additions & 2 deletions src/app/backup/destination/destination.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { inject, Injector, runInInjectionContext, Signal } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { toLazySignal } from 'ngxtension/to-lazy-signal';
import { ArgumentType, ICommandLineArgument } from '../../core/openapi';
import { WebModuleOption, WebModulesService } from '../../core/services/webmodules.service';

type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };

export type FormView = {
name: string;
type: ArgumentType | 'FileTree' | 'FolderTree';
type: ArgumentType | 'FileTree' | 'FolderTree' | 'NonValidatedSelectableString';
accepts?: string;
shortDescription?: string;
longDescription?: string;
options?: ICommandLineArgument['ValidValues'];
loadOptions?: (destinationFormValue: any) => Signal<WebModuleOption[] | undefined>;
// loadOptions?: (hi: any) => Observable<WebModuleOption[]>;
defaultValue?: ICommandLineArgument['DefaultValue'];
order?: number;
};
Expand Down Expand Up @@ -102,7 +107,28 @@ export const DESTINATION_CONFIG: DestinationConfig = {
},
gcs: {
oauthField: 'authid',
dynamicFields: ['gcs-location', 'gcs-storage-class', 'authid'],
customFields: {
path: {
type: 'Path',
name: 'path',
shortDescription: 'Bucket name',
longDescription: 'Bucket name',
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
},
},
dynamicFields: [
{
name: 'gcs-location',
type: 'NonValidatedSelectableString', // Convert to string before submitting
loadOptions: (injector: Injector) => injector.get(WebModulesService).gcsConfigLocations,
},
{
name: 'gcs-storage-class',
type: 'NonValidatedSelectableString', // Convert to string before submitting
loadOptions: (injector: Injector) => injector.get(WebModulesService).gcsConfigStorageClasses,
},
'authid',
],
},
googledrive: {
oauthField: 'authid',
Expand All @@ -118,3 +144,10 @@ export const DESTINATION_CONFIG: DestinationConfig = {
dynamicFields: ['authid'],
},
};

function hello(injector: Injector) {
let signal: Signal<any> | null = null;
runInInjectionContext(injector, () => {
signal = toLazySignal(inject(WebModulesService).getGcsConfig('Locations'));
});
}
14 changes: 5 additions & 9 deletions src/app/backup/destination/destination.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,19 @@ const mappings = {
},
gcs: {
to: (fields: any): string => {
const bucket = fields.dynamic['gcs-location'] ?? '';

delete fields.dynamic['gcs-location'];

const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);

return `${fields.destinationType}://${bucket}${urlParams}`;
return `${fields.destinationType}://${fields.custom.path}${urlParams}`;
},
from: (destinationType: string, urlObj: URL, plainPath: string) => {
const { advanced, dynamic } = handleSearchParams(destinationType, urlObj);
return <ValueOfDestinationFormGroup>{
destinationType,
advanced,
dynamic: {
...dynamic,
'gcs-location': urlObj.hostname + urlObj.pathname,
custom: {
path: urlObj.hostname + urlObj.pathname,
},
dynamic,
advanced,
};
},
},
Expand Down
8 changes: 4 additions & 4 deletions src/app/backup/options/options.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ <h3 class="title-30">Options</h3>
id="destination-{{ item.formGroupName }}-{{ item.index }}"
formControlName="{{ item.formView.name }}" />

<span error>
<!-- {{ destinationForm.controls.destinations.controls[index]?.hasError('pattern') }} -->
</span>
<!-- <span error>
{{ destinationForm.controls.destinations.controls[index]?.hasError('pattern') }}
</span> -->
</spk-form-field>
}
</ng-container>
Expand All @@ -309,7 +309,7 @@ <h3 class="title-30">Options</h3>
Go back
</button>

<button spk-button class="raised primary" type="submit">
<button spk-button class="raised primary" [class.loader]="isSubmitting()" type="submit">
<spk-icon>arrow-right</spk-icon>
Submit
</button>
Expand Down
5 changes: 1 addition & 4 deletions src/app/backup/options/options.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import FileTreeComponent from '../../core/components/file-tree/file-tree.component';
import ToggleCardComponent from '../../core/components/toggle-card/toggle-card.component';
import { SettingInputDto } from '../../core/openapi';
import { SysinfoState } from '../../core/states/sysinfo.state';
import { BackupState } from '../backup.state';
import { FormView } from '../destination/destination.config';

Expand Down Expand Up @@ -99,19 +98,17 @@ export const createAdvancedOption = (name: string | null | undefined, defaultVal
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class OptionsComponent {
#sysinfo = inject(SysinfoState);
#backupState = inject(BackupState);
#router = inject(Router);
#route = inject(ActivatedRoute);
formRef = viewChild.required<ElementRef<HTMLFormElement>>('formRef');

optionsForm = this.#backupState.optionsForm;
finishedLoading = this.#backupState.finishedLoading;
selectedOptions = this.#backupState.selectedOptions;
nonSelectedOptions = this.#backupState.nonSelectedOptions;
isSubmitting = this.#backupState.isSubmitting;
sizeOptions = signal(SIZE_OPTIONS);
rentationOptions = signal(RETENTION_OPTIONS);
sysinfoLoaded = this.#sysinfo.isLoaded;

oauthStartTokenCreation(_: any) {}
getFormFieldValue(
Expand Down
110 changes: 110 additions & 0 deletions src/app/core/services/webmodules.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// '/webmodule/s3-getconfig', {'s3-config': 'Providers'}
// '/webmodule/s3-getconfig', {'s3-config': 'Regions'}
// '/webmodule/s3-getconfig', {'s3-config': 'StorageClasses'}

// '/webmodule/s3-iamconfig'
// 's3-operation': 'CreateIAMUser'
// 's3-operation': 'GetPolicyDoc'

// '/webmodule/storj-getconfig', {'storj-config': 'Satellites'}
// '/webmodule/storj-getconfig', {'storj-config': 'AuthenticationMethods'}

// '/webmodule/openstack-getconfig', {'openstack-config': 'Providers'}
// '/webmodule/openstack-getconfig', {'openstack-config': 'Versions'}

// '/webmodule/gcs-getconfig', {'gcs-config': 'Locations'}
// '/webmodule/gcs-getconfig', {'gcs-config': 'StorageClasses'}

import { inject, Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map, Observable } from 'rxjs';
import { DuplicatiServerService, WebModuleOutputDto } from '../openapi';

export type WebModuleOption = { key: string; value: any };

@Injectable({
providedIn: 'root',
})
export class WebModulesService {
#dupServer = inject(DuplicatiServerService);

#defaultMapResultObjToArray(x: WebModuleOutputDto) {
return (
(x.Result &&
typeof x.Result === 'object' &&
Object.entries(x.Result).map(([key, value]) => ({
key,
value,
}))) ??
[]
);
}

getS3Config(config: 'Providers' | 'Regions' | 'RegionHosts' | 'StorageClasses') {
return this.#dupServer
.postApiV1WebmoduleByModulekey({
modulekey: 's3-getconfig',
requestBody: {
's3-config': config,
},
})
.pipe(map((x) => this.#defaultMapResultObjToArray(x)));
}

createS3IamUser(username: string, password: string) {
return this.#dupServer.postApiV1WebmoduleByModulekey({
modulekey: 's3-iamconfig',
requestBody: {
's3-operation': 'CanCreateUser',
's3-username': username,
's3-password': password,
},
});
}

createS3PolicyIAM(path: string) {
return this.#dupServer.postApiV1WebmoduleByModulekey({
modulekey: 's3-iamconfig',
requestBody: {
's3-operation': 'GetPolicyDoc',
path, // "${bucketname}/{path on server}"
},
});
}

getStorjConfig(config: 'Satellites' | 'AuthenticationMethods') {
return this.#dupServer
.postApiV1WebmoduleByModulekey({
modulekey: 'storj-getconfig',
requestBody: {
'storj-config': config,
},
})
.pipe(map((x) => this.#defaultMapResultObjToArray(x)));
}

getOpenstackConfig(config: 'Providers' | 'Versions') {
return this.#dupServer
.postApiV1WebmoduleByModulekey({
modulekey: 'openstack-getconfig',
requestBody: {
'openstack-config': config,
},
})
.pipe(map((x) => this.#defaultMapResultObjToArray(x)));
}

gcsConfigLocations = toSignal(this.getGcsConfig('Locations'));
gcsConfigStorageClasses = toSignal(this.getGcsConfig('StorageClasses'));

getGcsConfig(config: 'Locations' | 'StorageClasses') {
return this.#dupServer
.postApiV1WebmoduleByModulekey({
modulekey: 'gcs-getconfig',
requestBody: {
'gcs-config': config,
},
})
.pipe(map((x) => this.#defaultMapResultObjToArray(x))) as Observable<WebModuleOption[]>;
}
}
Loading

0 comments on commit e8976e6

Please sign in to comment.