@duplicati/ngclient 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/.github/workflows/npm-publish.yml +59 -0
- package/.prettierignore +13 -0
- package/.prettierrc +12 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +13 -0
- package/.vscode/settings.json +8 -0
- package/.vscode/tasks.json +24 -0
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/angular.json +98 -0
- package/bun.lockb +0 -0
- package/openapi-ts.config.ts +13 -0
- package/package.json +46 -0
- package/scripts/generate-package-json.ts +15 -0
- package/src/app/about/about.component.html +15 -0
- package/src/app/about/about.component.scss +19 -0
- package/src/app/about/about.component.ts +13 -0
- package/src/app/about/changelog/changelog.component.html +3 -0
- package/src/app/about/changelog/changelog.component.scss +7 -0
- package/src/app/about/changelog/changelog.component.ts +18 -0
- package/src/app/about/general/general.component.html +9 -0
- package/src/app/about/general/general.component.scss +23 -0
- package/src/app/about/general/general.component.ts +30 -0
- package/src/app/about/libraries/libraries.component.html +3 -0
- package/src/app/about/libraries/libraries.component.scss +7 -0
- package/src/app/about/libraries/libraries.component.ts +18 -0
- package/src/app/about/logs/logs.component.html +5 -0
- package/src/app/about/logs/logs.component.scss +7 -0
- package/src/app/about/logs/logs.component.ts +18 -0
- package/src/app/about/system-info/system-info.component.html +3 -0
- package/src/app/about/system-info/system-info.component.scss +7 -0
- package/src/app/about/system-info/system-info.component.ts +17 -0
- package/src/app/add-backup/add-backup.component.html +29 -0
- package/src/app/add-backup/add-backup.component.scss +38 -0
- package/src/app/add-backup/add-backup.component.ts +14 -0
- package/src/app/app.component.ts +35 -0
- package/src/app/app.config.ts +23 -0
- package/src/app/app.routes.ts +121 -0
- package/src/app/core/components/file-tree/file-tree.component.html +87 -0
- package/src/app/core/components/file-tree/file-tree.component.scss +80 -0
- package/src/app/core/components/file-tree/file-tree.component.ts +485 -0
- package/src/app/core/components/logo/logo.component.html +22 -0
- package/src/app/core/components/logo/logo.component.scss +12 -0
- package/src/app/core/components/logo/logo.component.ts +11 -0
- package/src/app/core/components/status-bar/status-bar.component.html +38 -0
- package/src/app/core/components/status-bar/status-bar.component.scss +27 -0
- package/src/app/core/components/status-bar/status-bar.component.ts +33 -0
- package/src/app/core/components/status-bar/status-bar.state.ts +182 -0
- package/src/app/core/components/toggle-card/toggle-card.component.html +11 -0
- package/src/app/core/components/toggle-card/toggle-card.component.scss +49 -0
- package/src/app/core/components/toggle-card/toggle-card.component.ts +28 -0
- package/src/app/core/interceptors/http.interceptor.ts +69 -0
- package/src/app/core/interceptors/websocket.interceptor.ts +66 -0
- package/src/app/core/openapi/core/ApiError.ts +21 -0
- package/src/app/core/openapi/core/ApiRequestOptions.ts +21 -0
- package/src/app/core/openapi/core/ApiResult.ts +7 -0
- package/src/app/core/openapi/core/OpenAPI.ts +55 -0
- package/src/app/core/openapi/core/request.ts +337 -0
- package/src/app/core/openapi/index.ts +6 -0
- package/src/app/core/openapi/schemas.gen.ts +3611 -0
- package/src/app/core/openapi/services.gen.ts +1460 -0
- package/src/app/core/openapi/types.gen.ts +1510 -0
- package/src/app/core/pipes/duration.pipe.ts +27 -0
- package/src/app/core/pipes/relative-time.pipe.ts +14 -0
- package/src/app/core/providers/dayjs.ts +16 -0
- package/src/app/core/services/dict.en.ts +0 -0
- package/src/app/core/services/dictionary.service.ts +12 -0
- package/src/app/core/services/localstorage.token.ts +82 -0
- package/src/app/core/services/password-generator.service.ts +39 -0
- package/src/app/core/services/relay-websocket.service.ts +268 -0
- package/src/app/core/services/timespan-literals.service.ts +25 -0
- package/src/app/core/states/app-auth.state.ts +87 -0
- package/src/app/core/states/backups.state.ts +71 -0
- package/src/app/core/states/relayconfig.state.ts +23 -0
- package/src/app/core/states/sysinfo.state.ts +31 -0
- package/src/app/core/states/tasks.state.ts +34 -0
- package/src/app/core/types/subscribed.ts +7 -0
- package/src/app/core/validators/custom.validators.ts +120 -0
- package/src/app/create-edit-backup/create-edit-backup.component.html +28 -0
- package/src/app/create-edit-backup/create-edit-backup.component.scss +15 -0
- package/src/app/create-edit-backup/create-edit-backup.component.ts +33 -0
- package/src/app/create-edit-backup/create-edit-backup.state.ts +582 -0
- package/src/app/create-edit-backup/destination/destination.component.html +367 -0
- package/src/app/create-edit-backup/destination/destination.component.scss +85 -0
- package/src/app/create-edit-backup/destination/destination.component.ts +249 -0
- package/src/app/create-edit-backup/destination/destination.config.ts +103 -0
- package/src/app/create-edit-backup/destination/destination.mapper.ts +177 -0
- package/src/app/create-edit-backup/general/general.component.html +108 -0
- package/src/app/create-edit-backup/general/general.component.scss +34 -0
- package/src/app/create-edit-backup/general/general.component.ts +136 -0
- package/src/app/create-edit-backup/options/options.component.html +95 -0
- package/src/app/create-edit-backup/options/options.component.scss +42 -0
- package/src/app/create-edit-backup/options/options.component.ts +113 -0
- package/src/app/create-edit-backup/schedule/schedule.component.html +102 -0
- package/src/app/create-edit-backup/schedule/schedule.component.scss +38 -0
- package/src/app/create-edit-backup/schedule/schedule.component.ts +137 -0
- package/src/app/create-edit-backup/source-data/source-data.component.html +93 -0
- package/src/app/create-edit-backup/source-data/source-data.component.scss +41 -0
- package/src/app/create-edit-backup/source-data/source-data.component.ts +114 -0
- package/src/app/home/backup.const.ts +167 -0
- package/src/app/home/home.component.html +98 -0
- package/src/app/home/home.component.scss +88 -0
- package/src/app/home/home.component.ts +54 -0
- package/src/app/layout/layout.component.html +64 -0
- package/src/app/layout/layout.component.scss +24 -0
- package/src/app/layout/layout.component.ts +51 -0
- package/src/app/layout/layout.state.ts +70 -0
- package/src/app/login/login.component.html +12 -0
- package/src/app/login/login.component.scss +25 -0
- package/src/app/login/login.component.ts +34 -0
- package/src/app/logout/logout.component.html +1 -0
- package/src/app/logout/logout.component.scss +3 -0
- package/src/app/logout/logout.component.ts +18 -0
- package/src/app/restore/restore.component.html +56 -0
- package/src/app/restore/restore.component.scss +37 -0
- package/src/app/restore/restore.component.ts +55 -0
- package/src/app/restore-flow/options/options.component.html +57 -0
- package/src/app/restore-flow/options/options.component.scss +16 -0
- package/src/app/restore-flow/options/options.component.ts +58 -0
- package/src/app/restore-flow/restore-flow.component.html +14 -0
- package/src/app/restore-flow/restore-flow.component.scss +15 -0
- package/src/app/restore-flow/restore-flow.component.ts +23 -0
- package/src/app/restore-flow/restore-flow.state.ts +120 -0
- package/src/app/restore-flow/select-files/select-files.component.html +42 -0
- package/src/app/restore-flow/select-files/select-files.component.scss +16 -0
- package/src/app/restore-flow/select-files/select-files.component.ts +129 -0
- package/src/app/settings/settings.component.html +256 -0
- package/src/app/settings/settings.component.scss +106 -0
- package/src/app/settings/settings.component.ts +475 -0
- package/src/assets/duplicati-logo.png +0 -0
- package/src/assets/duplicati-logo.svg +10 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/backup.png +0 -0
- package/src/assets/images/dark-theme.png +0 -0
- package/src/assets/images/folder.png +0 -0
- package/src/assets/images/light-theme.png +0 -0
- package/src/assets/images/new-backup.png +0 -0
- package/src/assets/images/restore.png +0 -0
- package/src/assets/images/system-theme.png +0 -0
- package/src/assets/spk.css +33 -0
- package/src/assets/spk.woff2 +0 -0
- package/src/environments/environment-token.ts +4 -0
- package/src/environments/environment.prod.ts +6 -0
- package/src/environments/environment.ts +6 -0
- package/src/index.html +15 -0
- package/src/main.ts +6 -0
- package/src/proxy.conf.mjs +6 -0
- package/src/styles.scss +41 -0
- package/tsconfig.app.json +15 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { FormBuilder } from '@angular/forms';
|
|
2
|
+
import { ArgumentType, ICommandLineArgument } from '../../core/openapi';
|
|
3
|
+
|
|
4
|
+
export type FormView = {
|
|
5
|
+
name: string;
|
|
6
|
+
type: ArgumentType | 'file-tree';
|
|
7
|
+
shortDescription?: string;
|
|
8
|
+
longDescription?: string;
|
|
9
|
+
options?: ICommandLineArgument['ValidValues'];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type CustomFormView = FormView & {
|
|
13
|
+
formElement: any;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DestinationConfig = {
|
|
17
|
+
[key: string]: {
|
|
18
|
+
oauthField?: string;
|
|
19
|
+
customFields?: {
|
|
20
|
+
[key: string]: CustomFormView;
|
|
21
|
+
};
|
|
22
|
+
dynamicFields?: string[];
|
|
23
|
+
advancedFields?: string[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const fb = new FormBuilder();
|
|
28
|
+
|
|
29
|
+
export const DESTINATION_CONFIG: DestinationConfig = {
|
|
30
|
+
file: {
|
|
31
|
+
customFields: {
|
|
32
|
+
path: {
|
|
33
|
+
type: 'file-tree',
|
|
34
|
+
name: 'path',
|
|
35
|
+
shortDescription: 'File path',
|
|
36
|
+
longDescription: 'The path to store the backup',
|
|
37
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
ssh: {
|
|
42
|
+
customFields: {
|
|
43
|
+
server: {
|
|
44
|
+
type: 'String',
|
|
45
|
+
name: 'server',
|
|
46
|
+
shortDescription: 'Server',
|
|
47
|
+
longDescription: 'The server to connect to',
|
|
48
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
49
|
+
},
|
|
50
|
+
port: {
|
|
51
|
+
type: 'Integer',
|
|
52
|
+
name: 'port',
|
|
53
|
+
shortDescription: 'Port',
|
|
54
|
+
longDescription: 'The port to connect to',
|
|
55
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
56
|
+
},
|
|
57
|
+
path: {
|
|
58
|
+
type: 'Path',
|
|
59
|
+
name: 'path',
|
|
60
|
+
shortDescription: 'File path on the server',
|
|
61
|
+
longDescription: 'File path on the server',
|
|
62
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
dynamicFields: ['auth-username'],
|
|
66
|
+
},
|
|
67
|
+
s3: {
|
|
68
|
+
customFields: {
|
|
69
|
+
path: {
|
|
70
|
+
type: 'Path',
|
|
71
|
+
name: 'path',
|
|
72
|
+
shortDescription: 'File path on the server',
|
|
73
|
+
longDescription: 'File path on the server',
|
|
74
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
75
|
+
},
|
|
76
|
+
bucket: {
|
|
77
|
+
type: 'String',
|
|
78
|
+
name: 'bucket',
|
|
79
|
+
shortDescription: 'Bucket name',
|
|
80
|
+
longDescription: 'Bucket name',
|
|
81
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
dynamicFields: ['use-ssl', 's3-server-name', 'auth-username', 'auth-password'],
|
|
85
|
+
},
|
|
86
|
+
gcs: {
|
|
87
|
+
dynamicFields: ['gcs-location', 'gcs-storage-class', 'authid'],
|
|
88
|
+
},
|
|
89
|
+
googledrive: {
|
|
90
|
+
oauthField: 'authid',
|
|
91
|
+
|
|
92
|
+
customFields: {
|
|
93
|
+
path: {
|
|
94
|
+
type: 'Path',
|
|
95
|
+
name: 'path',
|
|
96
|
+
shortDescription: 'File path on the server',
|
|
97
|
+
longDescription: 'File path on the server',
|
|
98
|
+
formElement: (defaultValue?: any) => fb.control<string>(defaultValue ?? ''),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
dynamicFields: ['authid'],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { FormControl, FormGroup } from '@angular/forms';
|
|
2
|
+
import { DestinationFormGroupValue } from './destination.component';
|
|
3
|
+
import { DESTINATION_CONFIG } from './destination.config';
|
|
4
|
+
|
|
5
|
+
type DestinationType = keyof typeof mappings | string;
|
|
6
|
+
|
|
7
|
+
export type DestinationFormGroup = FormGroup<{
|
|
8
|
+
destinationType: FormControl<string>;
|
|
9
|
+
custom: FormGroup<any>;
|
|
10
|
+
dynamic: FormGroup<any>;
|
|
11
|
+
advanced: FormGroup<any>;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type ValueOfDestinationFormGroup = DestinationFormGroup['value'];
|
|
15
|
+
|
|
16
|
+
function handleSearchParams(destinationType: string, urlObj: URL) {
|
|
17
|
+
const advanced: { [key: string]: any } = {};
|
|
18
|
+
const dynamic: { [key: string]: any } = {};
|
|
19
|
+
|
|
20
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
21
|
+
if (DESTINATION_CONFIG[destinationType].dynamicFields?.includes(key)) {
|
|
22
|
+
dynamic[key] = decodeURIComponent(value);
|
|
23
|
+
} else {
|
|
24
|
+
advanced[key] = decodeURIComponent(value);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
advanced,
|
|
30
|
+
dynamic,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toSearchParams(arr: [string, string | number | unknown][]) {
|
|
35
|
+
const searchParamString = arr
|
|
36
|
+
.map(([key, value]) => {
|
|
37
|
+
return `${key}=${value ? encodeURIComponent((value as string | number).toString()) : ''}`;
|
|
38
|
+
})
|
|
39
|
+
.join('&');
|
|
40
|
+
|
|
41
|
+
return searchParamString ? '?' + searchParamString : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mappings = {
|
|
45
|
+
file: {
|
|
46
|
+
to: (fields: ValueOfDestinationFormGroup): string => {
|
|
47
|
+
const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
|
|
48
|
+
|
|
49
|
+
return `${fields.destinationType}://${fields.custom.path}${urlParams}`;
|
|
50
|
+
},
|
|
51
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
52
|
+
const hasLeadingSlash = plainPath.indexOf('file:///') === 0;
|
|
53
|
+
const hostNameCapitalFirstLetter =
|
|
54
|
+
plainPath.split(hasLeadingSlash ? '///' : '//')[1].substring(0, 1) + urlObj.hostname.substring(1);
|
|
55
|
+
|
|
56
|
+
return <ValueOfDestinationFormGroup>{
|
|
57
|
+
destinationType,
|
|
58
|
+
custom: {
|
|
59
|
+
path: `${hasLeadingSlash ? '/' : ''}${hostNameCapitalFirstLetter}${urlObj.pathname}`,
|
|
60
|
+
},
|
|
61
|
+
...handleSearchParams(destinationType, urlObj),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
ssh: {
|
|
66
|
+
to: (fields: any): string => {
|
|
67
|
+
const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
|
|
68
|
+
|
|
69
|
+
return `${fields.destinationType}://${fields.custom.server}${fields.custom.port ? ':' + fields.custom.port : ''}${fields.custom.path.startsWith('/') ? fields.custom.path : '/' + fields.custom.path}${urlParams}`;
|
|
70
|
+
},
|
|
71
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
72
|
+
return <ValueOfDestinationFormGroup>{
|
|
73
|
+
destinationType,
|
|
74
|
+
custom: {
|
|
75
|
+
server: urlObj.hostname,
|
|
76
|
+
port: urlObj.port,
|
|
77
|
+
path: urlObj.pathname,
|
|
78
|
+
},
|
|
79
|
+
...handleSearchParams(destinationType, urlObj),
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
s3: {
|
|
84
|
+
to: (fields: any): string => {
|
|
85
|
+
const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
|
|
86
|
+
|
|
87
|
+
return `${fields.destinationType}://${fields.custom.bucket}${fields.custom.path}${urlParams}`;
|
|
88
|
+
},
|
|
89
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
90
|
+
return <ValueOfDestinationFormGroup>{
|
|
91
|
+
destinationType,
|
|
92
|
+
custom: {
|
|
93
|
+
path: urlObj.pathname,
|
|
94
|
+
bucket: urlObj.hostname,
|
|
95
|
+
},
|
|
96
|
+
...handleSearchParams(destinationType, urlObj),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
gcs: {
|
|
101
|
+
to: (fields: any): string => {
|
|
102
|
+
const bucket = fields.dynamic['gcs-location'] ?? '';
|
|
103
|
+
|
|
104
|
+
delete fields.dynamic['gcs-location'];
|
|
105
|
+
|
|
106
|
+
const urlParams = toSearchParams([...Object.entries(fields.advanced), ...Object.entries(fields.dynamic)]);
|
|
107
|
+
|
|
108
|
+
return `${fields.destinationType}://${bucket}${urlParams}`;
|
|
109
|
+
},
|
|
110
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
111
|
+
const { advanced, dynamic } = handleSearchParams(destinationType, urlObj);
|
|
112
|
+
return <ValueOfDestinationFormGroup>{
|
|
113
|
+
destinationType,
|
|
114
|
+
advanced,
|
|
115
|
+
dynamic: {
|
|
116
|
+
...dynamic,
|
|
117
|
+
'gcs-location': urlObj.hostname + urlObj.pathname,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
googledrive: {
|
|
123
|
+
to: (fields: any): string => {
|
|
124
|
+
const path = fields.custom.path;
|
|
125
|
+
|
|
126
|
+
return `${fields.destinationType}://${path}`;
|
|
127
|
+
},
|
|
128
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
129
|
+
return <ValueOfDestinationFormGroup>{
|
|
130
|
+
destinationType,
|
|
131
|
+
custom: {
|
|
132
|
+
path: plainPath,
|
|
133
|
+
},
|
|
134
|
+
...handleSearchParams(destinationType, urlObj),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
fallbackMapper: {
|
|
140
|
+
to: (fields: any): string => {
|
|
141
|
+
return '';
|
|
142
|
+
},
|
|
143
|
+
from: (destinationType: string, urlObj: URL, plainPath: string) => {
|
|
144
|
+
return <ValueOfDestinationFormGroup>{
|
|
145
|
+
destinationType,
|
|
146
|
+
...handleSearchParams(destinationType, urlObj),
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export function toTargetPath(fields: DestinationFormGroupValue): string {
|
|
153
|
+
const destinationType = fields.destinationType;
|
|
154
|
+
|
|
155
|
+
if (!destinationType || !mappings.hasOwnProperty(destinationType)) {
|
|
156
|
+
return ((mappings as any)['fallbackMapper'].to(fields) as string) ?? '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ((mappings as any)[destinationType].to(fields) as string) ?? '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function fromTargetPath(targetPath: string) {
|
|
163
|
+
const canParse = URL.canParse(targetPath);
|
|
164
|
+
const destinationType = targetPath.split('://')[0];
|
|
165
|
+
const fakeProtocolPrefixed = 'http://' + targetPath.split('://')[1];
|
|
166
|
+
const urlObj = new URL(fakeProtocolPrefixed);
|
|
167
|
+
|
|
168
|
+
if (!mappings.hasOwnProperty(destinationType)) {
|
|
169
|
+
return (mappings as any)['fallbackMapper'].from(destinationType, urlObj);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!canParse) {
|
|
173
|
+
throw new Error('Invalid target path');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (mappings as any)[destinationType].from(destinationType, urlObj, targetPath);
|
|
177
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<form [formGroup]="generalForm" autocomplete="off" #formRef (submit)="next()">
|
|
2
|
+
<h3 class="title-30">General backup settings</h3>
|
|
3
|
+
|
|
4
|
+
<spk-form-field>
|
|
5
|
+
<label for="backupName">Backup name</label>
|
|
6
|
+
<input type="text" tabindex="1" placeholder="Your backup name" formControlName="name" />
|
|
7
|
+
</spk-form-field>
|
|
8
|
+
|
|
9
|
+
<spk-form-field>
|
|
10
|
+
<label for="backupDescription">Backup description</label>
|
|
11
|
+
<textarea
|
|
12
|
+
id="backupDescription"
|
|
13
|
+
tabindex="2"
|
|
14
|
+
placeholder="Your backup description"
|
|
15
|
+
formControlName="description"></textarea>
|
|
16
|
+
</spk-form-field>
|
|
17
|
+
|
|
18
|
+
@if (isNew()) {
|
|
19
|
+
<spk-select [displayValue]="displayFn(generalForm.value.encryption ?? '')">
|
|
20
|
+
<label for="encryption">Encryption</label>
|
|
21
|
+
<input type="text" id="encryption" tabindex="3" placeholder="Encryption" formControlName="encryption" />
|
|
22
|
+
|
|
23
|
+
<ng-container options>
|
|
24
|
+
@for (encryption of encryptionOptions(); track $index) {
|
|
25
|
+
<option [value]="encryption.Key">{{ encryption.DisplayName }}</option>
|
|
26
|
+
}
|
|
27
|
+
</ng-container>
|
|
28
|
+
</spk-select>
|
|
29
|
+
|
|
30
|
+
@if (encryptionFieldSignal() !== 'none') {
|
|
31
|
+
<div class="passwords">
|
|
32
|
+
<spk-form-field>
|
|
33
|
+
<label for="password">Create a password</label>
|
|
34
|
+
<input
|
|
35
|
+
[type]="showPassword() ? 'text' : 'password'"
|
|
36
|
+
id="password"
|
|
37
|
+
tabindex="4"
|
|
38
|
+
placeholder="Password"
|
|
39
|
+
formControlName="password" />
|
|
40
|
+
</spk-form-field>
|
|
41
|
+
|
|
42
|
+
<spk-form-field>
|
|
43
|
+
<input
|
|
44
|
+
[type]="showPassword() ? 'text' : 'password'"
|
|
45
|
+
id="repeatPassword"
|
|
46
|
+
tabindex="5"
|
|
47
|
+
placeholder="Repeat password"
|
|
48
|
+
formControlName="repeatPassword" />
|
|
49
|
+
</spk-form-field>
|
|
50
|
+
|
|
51
|
+
<div class="password-actions">
|
|
52
|
+
<button spk-button type="button" class="small" (click)="showPassword.set(!showPassword())">
|
|
53
|
+
@if (showPassword()) {
|
|
54
|
+
<spk-icon>eye-slash</spk-icon>
|
|
55
|
+
hide
|
|
56
|
+
} @else {
|
|
57
|
+
<spk-icon>eye</spk-icon>
|
|
58
|
+
show
|
|
59
|
+
}
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
<button spk-button type="button" class="small" (click)="generatePassword()">
|
|
63
|
+
<spk-icon>password</spk-icon>
|
|
64
|
+
Generate password
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
@if (showCopyPassword()) {
|
|
68
|
+
<button spk-button type="button" class="small copy" (click)="copyPassword()">
|
|
69
|
+
@if (copiedPassword()) {
|
|
70
|
+
<spk-icon class="spk-success">check</spk-icon>
|
|
71
|
+
} @else {
|
|
72
|
+
<spk-icon>copy</spk-icon>
|
|
73
|
+
}
|
|
74
|
+
{{ copiedPassword() ? 'Copied' : 'Copy' }} password
|
|
75
|
+
</button>
|
|
76
|
+
}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
<div class="form-actions">
|
|
83
|
+
<button spk-button (click)="exit()">
|
|
84
|
+
<spk-icon>arrow-left</spk-icon>
|
|
85
|
+
Exit
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
<div
|
|
89
|
+
[attr.spk-tooltip-primary]="
|
|
90
|
+
(generalForm.valid && encryptionFieldSignal() === 'none') || copiedPassword()
|
|
91
|
+
? undefined
|
|
92
|
+
: 'Remember to copy the password'
|
|
93
|
+
">
|
|
94
|
+
<button
|
|
95
|
+
spk-button
|
|
96
|
+
class="raised primary"
|
|
97
|
+
type="submit"
|
|
98
|
+
[disabled]="
|
|
99
|
+
generalForm.invalid || (encryptionFieldSignal() !== 'none' && showCopyPassword() && !copiedPassword())
|
|
100
|
+
">
|
|
101
|
+
<spk-icon>arrow-right</spk-icon>
|
|
102
|
+
Continue
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
|
|
108
|
+
<!-- <pre>{{ generalForm.value | json }}</pre> -->
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use '@sparkle-ui/core/styles/helpers' as *;
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
form {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: p2r(24);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.passwords {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: p2r(8);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.password-actions {
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: p2r(8);
|
|
22
|
+
align-items: center;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.form-actions {
|
|
26
|
+
display: flex;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.copy {
|
|
31
|
+
spk-icon.spk-success {
|
|
32
|
+
color: var(--success-500);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { JsonPipe } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, ElementRef, inject, signal, viewChild } from '@angular/core';
|
|
3
|
+
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
4
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
5
|
+
import {
|
|
6
|
+
SparkleButtonComponent,
|
|
7
|
+
SparkleFormFieldComponent,
|
|
8
|
+
SparkleIconComponent,
|
|
9
|
+
SparkleSelectComponent,
|
|
10
|
+
} from '@sparkle-ui/core';
|
|
11
|
+
import { IDynamicModule } from '../../core/openapi';
|
|
12
|
+
import { PasswordGeneratorService } from '../../core/services/password-generator.service';
|
|
13
|
+
import { SysinfoState } from '../../core/states/sysinfo.state';
|
|
14
|
+
import { validateWhen, watchField } from '../../core/validators/custom.validators';
|
|
15
|
+
import { CreateEditBackupState } from '../create-edit-backup.state';
|
|
16
|
+
|
|
17
|
+
const fb = new FormBuilder();
|
|
18
|
+
|
|
19
|
+
export const createGeneralForm = (
|
|
20
|
+
defaults = {
|
|
21
|
+
name: '',
|
|
22
|
+
description: '',
|
|
23
|
+
encryption: 'none',
|
|
24
|
+
password: '',
|
|
25
|
+
repeatPassword: '',
|
|
26
|
+
}
|
|
27
|
+
) => {
|
|
28
|
+
return fb.group({
|
|
29
|
+
name: fb.control<string>(defaults.name, [Validators.required]),
|
|
30
|
+
description: fb.control<string>(defaults.description),
|
|
31
|
+
encryption: fb.control<string>(defaults.encryption, [Validators.required, watchField()]),
|
|
32
|
+
password: fb.control<string>(defaults.password, [
|
|
33
|
+
validateWhen((t) => t?.value.encryption !== 'none', [Validators.required]),
|
|
34
|
+
]),
|
|
35
|
+
repeatPassword: fb.control<string>(defaults.repeatPassword, [
|
|
36
|
+
validateWhen((t) => t?.value.encryption !== 'none', [Validators.required]),
|
|
37
|
+
]),
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const NONE_OPTION = {
|
|
42
|
+
Key: 'none',
|
|
43
|
+
DisplayName: 'None',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'app-general',
|
|
48
|
+
standalone: true,
|
|
49
|
+
imports: [
|
|
50
|
+
ReactiveFormsModule,
|
|
51
|
+
SparkleFormFieldComponent,
|
|
52
|
+
SparkleSelectComponent,
|
|
53
|
+
SparkleButtonComponent,
|
|
54
|
+
SparkleIconComponent,
|
|
55
|
+
JsonPipe,
|
|
56
|
+
],
|
|
57
|
+
templateUrl: './general.component.html',
|
|
58
|
+
styleUrl: './general.component.scss',
|
|
59
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
60
|
+
})
|
|
61
|
+
export default class GeneralComponent {
|
|
62
|
+
#router = inject(Router);
|
|
63
|
+
#route = inject(ActivatedRoute);
|
|
64
|
+
#passwordGeneratorService = inject(PasswordGeneratorService);
|
|
65
|
+
#createEditBackupState = inject(CreateEditBackupState);
|
|
66
|
+
#sysinfo = inject(SysinfoState);
|
|
67
|
+
|
|
68
|
+
formRef = viewChild.required<ElementRef<HTMLFormElement>>('formRef');
|
|
69
|
+
generalForm = this.#createEditBackupState.generalForm;
|
|
70
|
+
encryptionFieldSignal = this.#createEditBackupState.encryptionFieldSignal;
|
|
71
|
+
encryptionOptions = this.#createEditBackupState.encryptionOptions;
|
|
72
|
+
isNew = this.#createEditBackupState.isNew;
|
|
73
|
+
|
|
74
|
+
showPassword = signal(false);
|
|
75
|
+
copiedPassword = signal(false);
|
|
76
|
+
showCopyPassword = signal(false);
|
|
77
|
+
|
|
78
|
+
ngOnInit() {
|
|
79
|
+
this.formRef().nativeElement.focus();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
exit() {
|
|
83
|
+
this.#createEditBackupState.exit();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async copyPassword() {
|
|
87
|
+
const pass = this.generalForm.controls.password.value;
|
|
88
|
+
|
|
89
|
+
await this.#copyToClipboard(pass ?? '');
|
|
90
|
+
|
|
91
|
+
this.copiedPassword.set(true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async #copyToClipboard(text: string) {
|
|
95
|
+
try {
|
|
96
|
+
// Attempt to use the Clipboard API
|
|
97
|
+
await navigator.clipboard.writeText(text);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Fallback to execCommand if Clipboard API fails
|
|
100
|
+
try {
|
|
101
|
+
const textArea = document.createElement('textarea');
|
|
102
|
+
textArea.value = text;
|
|
103
|
+
document.body.appendChild(textArea);
|
|
104
|
+
textArea.select();
|
|
105
|
+
document.execCommand('copy');
|
|
106
|
+
document.body.removeChild(textArea);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('Failed to copy text: ', err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
displayFn(val: IDynamicModule['Key']) {
|
|
114
|
+
const encryptionOptions = this.#sysinfo.systemInfo()?.EncryptionModules ?? [];
|
|
115
|
+
const item = [NONE_OPTION, ...encryptionOptions].find((x) => x.Key === val);
|
|
116
|
+
|
|
117
|
+
if (!item) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
return `${item.DisplayName}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
generatePassword() {
|
|
124
|
+
this.copiedPassword.set(false);
|
|
125
|
+
|
|
126
|
+
const newPass = this.#passwordGeneratorService.generate(16);
|
|
127
|
+
this.generalForm.controls.password.setValue(newPass);
|
|
128
|
+
this.generalForm.controls.repeatPassword.setValue(newPass);
|
|
129
|
+
|
|
130
|
+
this.showCopyPassword.set(true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
next() {
|
|
134
|
+
this.#router.navigate(['destination'], { relativeTo: this.#route.parent });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<form [formGroup]="optionsForm" #formRef (submit)="submit()">
|
|
2
|
+
<h3 class="title-30">Options</h3>
|
|
3
|
+
|
|
4
|
+
<div class="remote-volume-wrap">
|
|
5
|
+
<div class="row-inputs" formGroupName="remoteVolumeSize">
|
|
6
|
+
<spk-form-field>
|
|
7
|
+
<label for="size">Remote volume size</label>
|
|
8
|
+
<input type="number" id="size" formControlName="size" />
|
|
9
|
+
</spk-form-field>
|
|
10
|
+
|
|
11
|
+
<spk-select>
|
|
12
|
+
<input type="text" placeholder="Select an option..." formControlName="unit" />
|
|
13
|
+
|
|
14
|
+
<ng-container options>
|
|
15
|
+
@for (option of sizeOptions(); track $index) {
|
|
16
|
+
<option [value]="option">{{ option }}</option>
|
|
17
|
+
}
|
|
18
|
+
</ng-container>
|
|
19
|
+
</spk-select>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<p>
|
|
23
|
+
The backups will be split up into multiple files called volumes. Here you can set the maximum size of the
|
|
24
|
+
individual volume files.
|
|
25
|
+
<a href="#" class="spk-primary">Read more</a>
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<spk-select>
|
|
30
|
+
<label for="size">Backup retention</label>
|
|
31
|
+
<input type="text" placeholder="Local folder or drive" formControlName="backupRetention" />
|
|
32
|
+
|
|
33
|
+
<ng-container options>
|
|
34
|
+
@for (option of rentationOptions(); track $index) {
|
|
35
|
+
<option [value]="option">{{ option }}</option>
|
|
36
|
+
}
|
|
37
|
+
</ng-container>
|
|
38
|
+
</spk-select>
|
|
39
|
+
|
|
40
|
+
<app-toggle-card [disallowToggle]="true">
|
|
41
|
+
<ng-container title>
|
|
42
|
+
Advanced options
|
|
43
|
+
|
|
44
|
+
<spk-menu>
|
|
45
|
+
<button spk-button type="button" class="outlined small">
|
|
46
|
+
Add advanced option
|
|
47
|
+
<spk-icon>plus</spk-icon>
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
<ng-container menu>
|
|
51
|
+
@for (option of nonSelectedAdvancedOptions(); track $index) {
|
|
52
|
+
<button spk-menu-item type="button" (click)="addNewOption(option)">{{ option.Name }}</button>
|
|
53
|
+
}
|
|
54
|
+
</ng-container>
|
|
55
|
+
</spk-menu>
|
|
56
|
+
</ng-container>
|
|
57
|
+
|
|
58
|
+
<ng-container formArrayName="advancedOptions">
|
|
59
|
+
@for (optionCtrl of optionsForm.controls.advancedOptions.controls; track $index) {
|
|
60
|
+
<div class="advanced-option" [formGroup]="optionCtrl">
|
|
61
|
+
<spk-form-field>
|
|
62
|
+
@if ($index === 0) {
|
|
63
|
+
<label for="name">Name</label>
|
|
64
|
+
}
|
|
65
|
+
<input type="text" id="name" formControlName="name" />
|
|
66
|
+
</spk-form-field>
|
|
67
|
+
|
|
68
|
+
<spk-form-field>
|
|
69
|
+
@if ($index === 0) {
|
|
70
|
+
<label for="value">Value</label>
|
|
71
|
+
}
|
|
72
|
+
<input type="text" id="value" formControlName="value" />
|
|
73
|
+
</spk-form-field>
|
|
74
|
+
|
|
75
|
+
<button class="icon" spk-button type="button" (click)="optionsForm.controls.advancedOptions.removeAt($index)">
|
|
76
|
+
<spk-icon>x-circle</spk-icon>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
}
|
|
80
|
+
</ng-container>
|
|
81
|
+
</app-toggle-card>
|
|
82
|
+
|
|
83
|
+
<div class="form-actions">
|
|
84
|
+
<button spk-button type="button" (click)="goBack()">
|
|
85
|
+
<spk-icon>arrow-left</spk-icon>
|
|
86
|
+
Go back
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<button spk-button class="raised primary" type="submit">
|
|
90
|
+
<spk-icon>arrow-right</spk-icon>
|
|
91
|
+
Submit
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<!-- <pre>{{ optionsForm.value | json }}</pre> -->
|
|
95
|
+
</form>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use '@sparkle-ui/core/styles/helpers' as *;
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
form {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: p2r(24);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.remote-volume-wrap {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: p2r(4);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.row-inputs {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: flex-end;
|
|
22
|
+
gap: p2r(8);
|
|
23
|
+
|
|
24
|
+
> * {
|
|
25
|
+
flex: 1 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
p {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.advanced-option {
|
|
33
|
+
display: grid;
|
|
34
|
+
gap: p2r(8);
|
|
35
|
+
grid-template-columns: 1fr 1fr 40px;
|
|
36
|
+
align-items: flex-end;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.form-actions {
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
}
|