Feat/external-smtp (#1791)

* UI for EOS smtp, missing API layer

* implement api

* fix errors

* switch to external smtp creds

* fix things up

* fix types

* update types for new forms

* feat: add new form to emails and marketplace (#2268)

* import tuilet module

* feat: get rid of old form completely (#2270)

* move to builder spec and delete developer menu

* update sdk

* tiny

* getting better

* working

* done

* feat: add step to number config

* chore: small fixes

* update SDK and step for numbers

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2023-05-16 08:03:29 -06:00
committed by Aiden McClelland
parent 4c465850a2
commit 010be05920
105 changed files with 1237 additions and 4156 deletions

View File

@@ -2,26 +2,44 @@ import { Component } from '@angular/core'
import {
BackupTarget,
BackupTargetType,
DiskBackupTarget,
RR,
UnknownDisk,
} from 'src/app/services/api/api.types'
import {
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared'
import {
CifsSpec,
DropboxSpec,
GoogleDriveSpec,
DiskBackupTargetSpec,
RemoteBackupTargetSpec,
cifsSpec,
diskBackupTargetSpec,
dropboxSpec,
googleDriveSpec,
remoteBackupTargetSpec,
} from '../../types/target-types'
import { BehaviorSubject } from 'rxjs'
import { BehaviorSubject, filter } from 'rxjs'
import { TuiDialogService } from '@taiga-ui/core'
import { ErrorService } from '@start9labs/shared'
import { FormDialogService } from '../../../../services/form-dialog.service'
import { FormPage } from '../../../../modals/form/form.page'
import { LoadingService } from '../../../../modals/loading/loading.service'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import {
InputSpec,
unionSelectKey,
unionValueKey,
} from '@start9labs/start-sdk/lib/config/configTypes'
type BackupConfig =
| {
type: {
[unionSelectKey]: 'dropbox' | 'google-drive'
[unionValueKey]: RR.AddCloudBackupTargetReq
}
}
| {
type: {
[unionSelectKey]: 'cifs'
[unionValueKey]: RR.AddCifsBackupTargetReq
}
}
export type BackupType = 'create' | 'restore'
@@ -41,27 +59,23 @@ export class BackupTargetsPage {
loading$ = new BehaviorSubject(true)
constructor(
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly dialogs: TuiDialogService,
private readonly loader: LoadingService,
private readonly errorService: ErrorService,
private readonly api: ApiService,
private readonly formDialog: FormDialogService,
) {}
ngOnInit() {
this.getTargets()
}
async presentModalAddPhysical(
disk: UnknownDisk,
index: number,
): Promise<void> {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'New Physical Target',
spec: DiskBackupTargetSpec,
initialValue: {
async presentModalAddPhysical(disk: UnknownDisk, index: number) {
this.formDialog.open(FormPage, {
label: 'New Physical Target',
data: {
spec: await configBuilderToSpec(diskBackupTargetSpec),
value: {
name: disk.label || disk.logicalname,
},
buttons: [
@@ -74,60 +88,56 @@ export class BackupTargetsPage {
}).then(disk => {
this.targets['unknown-disks'].splice(index, 1)
this.targets.saved.push(disk)
return true
}),
isSubmit: true,
},
],
},
})
await modal.present()
}
async presentModalAddRemote(): Promise<void> {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'New Remote Target',
spec: RemoteBackupTargetSpec,
async presentModalAddRemote() {
this.formDialog.open(FormPage, {
label: 'New Remote Target',
data: {
spec: await configBuilderToSpec(remoteBackupTargetSpec),
buttons: [
{
text: 'Save',
handler: (
value:
| (RR.AddCifsBackupTargetReq & { type: BackupTargetType })
| (RR.AddCloudBackupTargetReq & { type: BackupTargetType }),
) => this.add(value.type, value),
isSubmit: true,
handler: ({ type }: BackupConfig) =>
this.add(
type[unionSelectKey] === 'cifs' ? 'cifs' : 'cloud',
type[unionValueKey],
),
},
],
},
})
await modal.present()
}
async presentModalUpdate(target: BackupTarget): Promise<void> {
let spec: typeof RemoteBackupTargetSpec = {}
async presentModalUpdate(target: BackupTarget) {
let spec: InputSpec
switch (target.type) {
case 'cifs':
spec = CifsSpec
spec = await configBuilderToSpec(cifsSpec)
break
case 'cloud':
spec = target.provider === 'dropbox' ? DropboxSpec : GoogleDriveSpec
spec = await configBuilderToSpec(
target.provider === 'dropbox' ? dropboxSpec : googleDriveSpec,
)
break
case 'disk':
spec = DiskBackupTargetSpec
spec = await configBuilderToSpec(diskBackupTargetSpec)
break
}
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'Update Remote Target',
this.formDialog.open(FormPage, {
label: 'Update Target',
data: {
spec,
initialValue: target,
value: target,
buttons: [
{
text: 'Save',
@@ -136,49 +146,38 @@ export class BackupTargetsPage {
| RR.UpdateCifsBackupTargetReq
| RR.UpdateCloudBackupTargetReq
| RR.UpdateDiskBackupTargetReq,
) => this.update(target.type, value),
isSubmit: true,
) => this.update(target.type, { ...value, id: target.id }),
},
],
},
})
await modal.present()
}
async presentAlertDelete(id: string, index: number) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Forget backup target? This actions cannot be undone.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
presentAlertDelete(id: string, index: number) {
this.dialogs
.open(TUI_PROMPT, {
label: 'Confirm',
size: 's',
data: {
content: 'Forget backup target? This actions cannot be undone.',
no: 'Cancel',
yes: 'Delete',
},
{
text: 'Delete',
handler: () => {
this.delete(id, index)
},
cssClass: 'enter-click',
},
],
})
await alert.present()
})
.pipe(filter(Boolean))
.subscribe(() => this.delete(id, index))
}
async delete(id: string, index: number): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Removing...',
})
await loader.present()
const loader = this.loader.open('Removing...').subscribe()
try {
await this.api.removeBackupTarget({ id })
this.targets.saved.splice(index, 1)
} catch (e: any) {
this.errToast.present(e)
this.errorService.handleError(e)
} finally {
loader.dismiss()
loader.unsubscribe()
}
}
@@ -202,7 +201,7 @@ export class BackupTargetsPage {
try {
this.targets = await this.api.getBackupTargets({})
} catch (e: any) {
this.errToast.present(e)
this.errorService.handleError(e)
} finally {
this.loading$.next(false)
}
@@ -215,16 +214,12 @@ export class BackupTargetsPage {
| RR.AddCloudBackupTargetReq
| RR.AddDiskBackupTargetReq,
): Promise<BackupTarget> {
const loader = await this.loadingCtrl.create({
message: 'Saving target...',
})
await loader.present()
const loader = this.loader.open('Saving target...').subscribe()
try {
const res = await this.api.addBackupTarget(type, value)
return res
return await this.api.addBackupTarget(type, value)
} finally {
loader.dismiss()
loader.unsubscribe()
}
}
@@ -235,16 +230,12 @@ export class BackupTargetsPage {
| RR.UpdateCloudBackupTargetReq
| RR.UpdateDiskBackupTargetReq,
): Promise<BackupTarget> {
const loader = await this.loadingCtrl.create({
message: 'Saving target...',
})
await loader.present()
const loader = this.loader.open('Saving target...').subscribe()
try {
const res = await this.api.updateBackupTarget(type, value)
return res
return await this.api.updateBackupTarget(type, value)
} finally {
loader.dismiss()
loader.unsubscribe()
}
}
}

View File

@@ -1,221 +1,121 @@
import { InputSpec } from 'start-sdk/lib/config/configTypes'
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
export const DropboxSpec: InputSpec = {
name: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
export const dropboxSpec = Config.of({
name: Value.text({
name: 'Name',
description: 'A friendly name for this Dropbox target',
placeholder: 'My Dropbox',
required: true,
masked: false,
warning: null,
default: null,
},
token: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
required: { default: null },
}),
token: Value.text({
name: 'Access Token',
description: 'The secret access token for your custom Dropbox app',
warning: null,
placeholder: null,
required: true,
required: { default: null },
masked: true,
default: null,
},
path: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
}),
path: Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
warning: null,
placeholder: 'e.g. /Desktop/my-folder',
required: true,
masked: false,
default: null,
},
}
required: { default: null },
}),
})
export const GoogleDriveSpec: InputSpec = {
name: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
export const googleDriveSpec = Config.of({
name: Value.text({
name: 'Name',
description: 'A friendly name for this Google Drive target',
warning: null,
placeholder: 'My Google Drive',
required: true,
masked: false,
default: null,
},
key: {
type: 'file',
required: { default: null },
}),
path: Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder',
required: { default: null },
}),
key: Value.file({
name: 'Private Key File',
description:
'Your Google Drive service account private key file (.json file)',
warning: null,
required: true,
extensions: ['json'],
},
path: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
name: 'Path',
description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Desktop/my-folder',
required: true,
masked: false,
warning: null,
default: null,
},
}
}),
})
export const CifsSpec: InputSpec = {
name: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
export const cifsSpec = Config.of({
name: Value.text({
name: 'Name',
description: 'A friendly name for this Network Folder',
warning: null,
placeholder: 'My Network Folder',
required: true,
masked: false,
default: null,
},
hostname: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
description: `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
},
],
required: { default: null },
}),
hostname: Value.text({
name: 'Hostname',
description:
'The hostname of your target device on the Local Area Network.',
warning: null,
required: true,
masked: false,
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
default: null,
},
path: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
required: { default: null },
patterns: [],
}),
path: Value.text({
name: 'Path',
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
required: true,
masked: false,
warning: null,
default: null,
},
username: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
required: { default: null },
}),
username: Value.text({
name: 'Username',
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
required: true,
masked: false,
warning: null,
required: { default: null },
placeholder: 'My Network Folder',
default: null,
},
password: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
}),
password: Value.text({
name: 'Password',
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
required: false,
masked: true,
warning: null,
placeholder: 'My Network Folder',
default: null,
},
}
}),
})
export const RemoteBackupTargetSpec: InputSpec = {
type: {
type: 'union',
name: 'Target Type',
description: null,
warning: null,
required: true,
variants: {
export const remoteBackupTargetSpec = Config.of({
type: Value.union(
{
name: 'Target Type',
required: { default: 'dropbox' },
},
Variants.of({
dropbox: {
name: 'Dropbox',
spec: DropboxSpec,
spec: dropboxSpec,
},
'google-drive': {
name: 'Google Drive',
spec: GoogleDriveSpec,
spec: googleDriveSpec,
},
cifs: {
name: 'Network Folder',
spec: CifsSpec,
spec: cifsSpec,
},
},
default: 'dropbox',
},
}
}),
),
})
export const DiskBackupTargetSpec: InputSpec = {
name: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
export const diskBackupTargetSpec = Config.of({
name: Value.text({
name: 'Name',
description: 'A friendly name for this physical target',
placeholder: 'My Physical Target',
required: true,
masked: false,
warning: null,
default: null,
},
path: {
type: 'text',
inputmode: 'text',
minLength: null,
maxLength: null,
patterns: [],
required: { default: null },
}),
path: Value.text({
name: 'Path',
description: 'The fully qualified path to the backup directory',
placeholder: 'e.g. /Backups/my-folder',
required: true,
masked: false,
warning: null,
default: null,
},
}
required: { default: null },
}),
})