Merge pull request #2727 from Start9Labs/final-fixes

fix: final fixes
This commit is contained in:
Matt Hill
2024-08-28 15:38:44 -06:00
committed by GitHub
23 changed files with 235 additions and 157 deletions

View File

@@ -13,13 +13,16 @@ import { UILaunchComponent } from 'src/app/routes/portal/routes/dashboard/ui.com
import { ActionsService } from 'src/app/services/actions.service' import { ActionsService } from 'src/app/services/actions.service'
import { DepErrorService } from 'src/app/services/dep-error.service' import { DepErrorService } from 'src/app/services/dep-error.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
const RUNNING = ['running', 'starting', 'restarting']
@Component({ @Component({
standalone: true, standalone: true,
selector: 'fieldset[appControls]', selector: 'fieldset[appControls]',
template: ` template: `
@if (pkg().status.main.status === 'running') { @if (running()) {
<button <button
tuiIconButton tuiIconButton
iconStart="@tui.square" iconStart="@tui.square"
@@ -31,6 +34,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
<button <button
tuiIconButton tuiIconButton
iconStart="@tui.rotate-cw" iconStart="@tui.rotate-cw"
[disabled]="status().primary !== 'running'"
(click)="actions.restart(manifest())" (click)="actions.restart(manifest())"
> >
Restart Restart
@@ -40,7 +44,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
*tuiLet="hasUnmet() | async as hasUnmet" *tuiLet="hasUnmet() | async as hasUnmet"
tuiIconButton tuiIconButton
iconStart="@tui.play" iconStart="@tui.play"
[disabled]="!pkg().status.configured" [disabled]="status().primary !== 'stopped' || !pkg().status.configured"
(click)="actions.start(manifest(), !!hasUnmet)" (click)="actions.start(manifest(), !!hasUnmet)"
> >
Start Start
@@ -78,14 +82,16 @@ export class ControlsComponent {
private readonly errors = inject(DepErrorService) private readonly errors = inject(DepErrorService)
readonly actions = inject(ActionsService) readonly actions = inject(ActionsService)
pkg = input.required<PackageDataEntry>() readonly pkg = input.required<PackageDataEntry>()
readonly status = computed(() => renderPkgStatus(this.pkg()))
readonly running = computed(() => RUNNING.includes(this.status().primary))
readonly manifest = computed(() => getManifest(this.pkg())) readonly manifest = computed(() => getManifest(this.pkg()))
readonly hasUnmet = computed(() => readonly hasUnmet = computed(() =>
this.errors.getPkgDepErrors$(this.manifest().id).pipe( this.errors.getPkgDepErrors$(this.manifest().id).pipe(
map(errors => map(errors =>
Object.keys(this.pkg().currentDependencies) Object.keys(this.pkg().currentDependencies)
.map(id => !!(errors[id] as any)?.[id]) // @TODO fix type .map(id => errors[id])
.some(Boolean), .some(Boolean),
), ),
), ),

View File

@@ -111,7 +111,7 @@ export class ServiceComponent implements OnChanges {
readonly connected$ = inject(ConnectionService) readonly connected$ = inject(ConnectionService)
get installed(): boolean { get installed(): boolean {
return this.pkg.stateInfo.state !== 'installed' return this.pkg.stateInfo.state === 'installed'
} }
get manifest() { get manifest() {

View File

@@ -11,12 +11,15 @@ import { tuiButtonOptionsProvider } from '@taiga-ui/core'
import { DependencyInfo } from 'src/app/routes/portal/routes/service/types/dependency-info' import { DependencyInfo } from 'src/app/routes/portal/routes/service/types/dependency-info'
import { ActionsService } from 'src/app/services/actions.service' import { ActionsService } from 'src/app/services/actions.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
const STOPPABLE = ['running', 'starting', 'restarting']
@Component({ @Component({
selector: 'service-actions', selector: 'service-actions',
template: ` template: `
@if (pkg.status.main.status === 'running') { @if (canStop) {
<button <button
tuiButton tuiButton
appearance="danger-solid" appearance="danger-solid"
@@ -25,7 +28,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
> >
Stop Stop
</button> </button>
}
@if (canRestart) {
<button <button
tuiButton tuiButton
iconStart="@tui.rotate-cw" iconStart="@tui.rotate-cw"
@@ -35,17 +40,17 @@ import { getManifest } from 'src/app/utils/get-package-data'
</button> </button>
} }
@if (pkg.status.main.status === 'stopped' && isConfigured) { @if (canStart) {
<button <button
tuiButton tuiButton
iconStart="@tui.play" iconStart="@tui.play"
(click)="actions.start(manifest, hasUnmet(dependencies))" (click)="actions.start(manifest, hasUnmet(service.dependencies))"
> >
Start Start
</button> </button>
} }
@if (!isConfigured) { @if (canConfigure) {
<button <button
tuiButton tuiButton
appearance="secondary-warning" appearance="secondary-warning"
@@ -73,19 +78,32 @@ import { getManifest } from 'src/app/utils/get-package-data'
}) })
export class ServiceActionsComponent { export class ServiceActionsComponent {
@Input({ required: true }) @Input({ required: true })
pkg!: PackageDataEntry service!: {
pkg: PackageDataEntry
@Input({ required: true }) dependencies: readonly DependencyInfo[]
dependencies: readonly DependencyInfo[] = [] status: PackageStatus
}
readonly actions = inject(ActionsService) readonly actions = inject(ActionsService)
get isConfigured(): boolean { get manifest(): T.Manifest {
return this.pkg.status.configured return getManifest(this.service.pkg)
} }
get manifest(): T.Manifest { get canStop(): boolean {
return getManifest(this.pkg) return STOPPABLE.includes(this.service.status.primary)
}
get canStart(): boolean {
return this.service.status.primary === 'stopped' && !this.canConfigure
}
get canRestart(): boolean {
return this.service.status.primary === 'running'
}
get canConfigure(): boolean {
return !this.service.pkg.status.configured
} }
@tuiPure @tuiPure

View File

@@ -50,10 +50,7 @@ import { DependencyInfo } from '../types/dependency-info'
/> />
@if (isInstalled(service) && (connected$ | async)) { @if (isInstalled(service) && (connected$ | async)) {
<service-actions <service-actions [service]="service" />
[pkg]="service.pkg"
[dependencies]="service.dependencies"
/>
} }
</section> </section>

View File

@@ -1,3 +1,4 @@
import { KeyValuePipe } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
@@ -30,27 +31,27 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (target of backupsTargets; track $index) { @for (target of backupsTargets || {} | keyvalue; track $index) {
<tr> <tr>
<td class="title">{{ target.name }}</td> <td class="title">{{ target.value.name }}</td>
<td class="type"> <td class="type">
<tui-icon [icon]="target.type | getBackupIcon" /> <tui-icon [icon]="target.value.type | getBackupIcon" />
{{ target.type }} {{ target.value.type }}
</td> </td>
<td class="available"> <td class="available">
<tui-icon <tui-icon
[icon]="target.mountable ? '@tui.check' : '@tui.x'" [icon]="target.value.mountable ? '@tui.check' : '@tui.x'"
[class]="target.mountable ? 'g-success' : 'g-error'" [class]="target.value.mountable ? 'g-success' : 'g-error'"
/> />
</td> </td>
<td class="path">{{ target.path }}</td> <td class="path">{{ target.value.path }}</td>
<td class="actions"> <td class="actions">
<button <button
tuiIconButton tuiIconButton
size="xs" size="xs"
appearance="icon" appearance="icon"
iconStart="@tui.pencil" iconStart="@tui.pencil"
(click)="update.emit(target)" (click)="update.emit(target.key)"
> >
Update Update
</button> </button>
@@ -59,7 +60,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
size="xs" size="xs"
appearance="icon" appearance="icon"
iconStart="@tui.trash-2" iconStart="@tui.trash-2"
(click)="delete$.next(target.id)" (click)="delete$.next(target.key)"
> >
Delete Delete
</button> </button>
@@ -132,7 +133,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [TuiButton, GetBackupIconPipe, TuiIcon], imports: [TuiButton, GetBackupIconPipe, TuiIcon, KeyValuePipe],
}) })
export class BackupsTargetsComponent { export class BackupsTargetsComponent {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
@@ -140,10 +141,10 @@ export class BackupsTargetsComponent {
readonly delete$ = new Subject<string>() readonly delete$ = new Subject<string>()
@Input() @Input()
backupsTargets: readonly BackupTarget[] | null = null backupsTargets: Record<string, BackupTarget> | null = null
@Output() @Output()
readonly update = new EventEmitter<BackupTarget>() readonly update = new EventEmitter<string>()
@Output() @Output()
readonly delete = this.delete$.pipe( readonly delete = this.delete$.pipe(

View File

@@ -33,8 +33,10 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
</td> </td>
<td class="name">{{ job.name }}</td> <td class="name">{{ job.name }}</td>
<td> <td>
<tui-icon [icon]="job.target.type | getBackupIcon" /> @if (targets()?.saved?.[job.targetId]; as target) {
{{ job.target.name }} <tui-icon [icon]="target.type | getBackupIcon" />
{{ target.name }}
}
</td> </td>
<td class="packages">Packages: {{ job.packageIds.length }}</td> <td class="packages">Packages: {{ job.packageIds.length }}</td>
</tr> </tr>
@@ -94,6 +96,9 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
imports: [GetBackupIconPipe, DatePipe, TuiIcon], imports: [GetBackupIconPipe, DatePipe, TuiIcon],
}) })
export class BackupsUpcomingComponent { export class BackupsUpcomingComponent {
private readonly api = inject(ApiService)
readonly targets = toSignal(from(this.api.getBackupTargets({})))
readonly current = toSignal( readonly current = toSignal(
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job') .watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
@@ -101,7 +106,7 @@ export class BackupsUpcomingComponent {
) )
readonly upcoming = toSignal( readonly upcoming = toSignal(
from(inject(ApiService).getBackupJobs({})).pipe( from(this.api.getBackupJobs({})).pipe(
map(jobs => map(jobs =>
jobs jobs
.map(job => { .map(job => {

View File

@@ -1,3 +1,4 @@
import { toSignal } from '@angular/core/rxjs-interop'
import { import {
TuiWrapperModule, TuiWrapperModule,
TuiInputModule, TuiInputModule,
@@ -13,6 +14,7 @@ import {
POLYMORPHEUS_CONTEXT, POLYMORPHEUS_CONTEXT,
PolymorpheusComponent, PolymorpheusComponent,
} from '@taiga-ui/polymorpheus' } from '@taiga-ui/polymorpheus'
import { from, map } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { BackupJob, BackupTarget } from 'src/app/services/api/api.types' import { BackupJob, BackupTarget } from 'src/app/services/api/api.types'
import { TARGET, TARGET_CREATE } from './target.component' import { TARGET, TARGET_CREATE } from './target.component'
@@ -36,8 +38,10 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
(click)="selectTarget()" (click)="selectTarget()"
> >
Target Target
<tui-badge [appearance]="job.target.type ? 'success' : 'warning'"> <tui-badge
{{ job.target.type || 'Select target' }} [appearance]="target()?.[job.targetId]?.type ? 'success' : 'warning'"
>
{{ target()?.[job.targetId]?.type || 'Select target' }}
</tui-badge> </tui-badge>
</button> </button>
<button <button
@@ -111,6 +115,10 @@ export class BackupsEditModal {
private readonly context = private readonly context =
inject<TuiDialogContext<BackupJob, BackupJobBuilder>>(POLYMORPHEUS_CONTEXT) inject<TuiDialogContext<BackupJob, BackupJobBuilder>>(POLYMORPHEUS_CONTEXT)
readonly target = toSignal(
from(this.api.getBackupTargets({})).pipe(map(({ saved }) => saved)),
)
get job() { get job() {
return this.context.data return this.context.data
} }
@@ -132,9 +140,11 @@ export class BackupsEditModal {
} }
selectTarget() { selectTarget() {
this.dialogs.open<BackupTarget>(TARGET, TARGET_CREATE).subscribe(target => { this.dialogs
this.job.target = target .open<BackupTarget & { id: string }>(TARGET, TARGET_CREATE)
}) .subscribe(({ id }) => {
this.job.targetId = id
})
} }
selectPackages() { selectPackages() {

View File

@@ -1,3 +1,4 @@
import { toSignal } from '@angular/core/rxjs-interop'
import { TuiCheckbox } from '@taiga-ui/kit' import { TuiCheckbox } from '@taiga-ui/kit'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { import {
@@ -11,6 +12,7 @@ import { ErrorService, LoadingService } from '@start9labs/shared'
import { TUI_TRUE_HANDLER, TUI_FALSE_HANDLER } from '@taiga-ui/cdk' import { TUI_TRUE_HANDLER, TUI_FALSE_HANDLER } from '@taiga-ui/cdk'
import { TuiDialogService, TuiIcon, TuiLink, TuiButton } from '@taiga-ui/core' import { TuiDialogService, TuiIcon, TuiLink, TuiButton } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { from } from 'rxjs'
import { REPORT } from 'src/app/components/report.component' import { REPORT } from 'src/app/components/report.component'
import { BackupRun } from 'src/app/services/api/api.types' import { BackupRun } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -76,17 +78,23 @@ import { HasErrorPipe } from '../pipes/has-error.pipe'
<button tuiLink (click)="showReport(run)">Report</button> <button tuiLink (click)="showReport(run)">Report</button>
</td> </td>
<td [style.grid-column]="'span 2'"> <td [style.grid-column]="'span 2'">
<tui-icon [icon]="run.job.target.type | getBackupIcon" /> @if (targets()?.saved?.[run.job.targetId]; as target) {
{{ run.job.target.name }} <tui-icon [icon]="target.type | getBackupIcon" />
{{ target.name }}
}
</td> </td>
</tr> </tr>
} @empty { } @empty {
@if (runs()) { @if (runs()) {
<tr><td colspan="6">No backups have been run yet.</td></tr> <tr>
<td colspan="6">No backups have been run yet.</td>
</tr>
} @else { } @else {
@for (row of ['', '']; track $index) { @for (row of ['', '']; track $index) {
<tr> <tr>
<td colspan="6"><div class="tui-skeleton">Loading</div></td> <td colspan="6">
<div class="tui-skeleton">Loading</div>
</td>
</tr> </tr>
} }
} }
@@ -166,7 +174,8 @@ export class BackupsHistoryModal {
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
runs = signal<BackupRun[] | null>(null) readonly targets = toSignal(from(this.api.getBackupTargets({})))
readonly runs = signal<BackupRun[] | null>(null)
selected: boolean[] = [] selected: boolean[] = []
get all(): boolean | null { get all(): boolean | null {

View File

@@ -1,4 +1,5 @@
import { Component, inject, OnInit } from '@angular/core' import { Component, inject, OnInit } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { import {
TuiDialogOptions, TuiDialogOptions,
@@ -9,7 +10,7 @@ import {
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit' import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { BehaviorSubject, filter } from 'rxjs' import { BehaviorSubject, filter, from } from 'rxjs'
import { BackupJob } from 'src/app/services/api/api.types' import { BackupJob } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe' import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
@@ -52,8 +53,10 @@ import { EDIT } from './edit.component'
<tr> <tr>
<td class="title">{{ job.name }}</td> <td class="title">{{ job.name }}</td>
<td class="target"> <td class="target">
<tui-icon [icon]="job.target.type | getBackupIcon" /> @if (targets()?.saved?.[job.targetId]; as target) {
{{ job.target.name }} <tui-icon [icon]="target.type | getBackupIcon" />
{{ target.name }}
}
</td> </td>
<td class="packages">Packages: {{ job.packageIds.length }}</td> <td class="packages">Packages: {{ job.packageIds.length }}</td>
<td class="schedule">{{ (job.cron | toHumanCron).message }}</td> <td class="schedule">{{ (job.cron | toHumanCron).message }}</td>
@@ -76,11 +79,15 @@ import { EDIT } from './edit.component'
</tr> </tr>
} @empty { } @empty {
@if (jobs) { @if (jobs) {
<tr><td colspan="5">No jobs found.</td></tr> <tr>
<td colspan="5">No jobs found.</td>
</tr>
} @else { } @else {
@for (i of ['', '']; track $index) { @for (i of ['', '']; track $index) {
<tr> <tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td> <td colspan="5">
<div class="tui-skeleton">Loading</div>
</td>
</tr> </tr>
} }
} }
@@ -147,6 +154,7 @@ export class BackupsJobsModal implements OnInit {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
readonly loading$ = new BehaviorSubject(true) readonly loading$ = new BehaviorSubject(true)
readonly targets = toSignal(from(this.api.getBackupTargets({})))
jobs?: BackupJob[] jobs?: BackupJob[]
@@ -179,11 +187,11 @@ export class BackupsJobsModal implements OnInit {
label: 'Edit Job', label: 'Edit Job',
data: new BackupJobBuilder(data), data: new BackupJobBuilder(data),
}) })
.subscribe(job => { .subscribe(({ name, targetId, cron, packageIds }) => {
data.name = job.name data.name = name
data.target = job.target data.targetId = targetId
data.cron = job.cron data.cron = cron
data.packageIds = job.packageIds data.packageIds = packageIds
}) })
} }

View File

@@ -85,17 +85,17 @@ export class BackupsRecoverModal {
.watch$('packageData') .watch$('packageData')
.pipe(take(1)) .pipe(take(1))
readonly toMessage = (option: RecoverOption) => { readonly toMessage = ({ newerStartOs, installed, title }: RecoverOption) => {
if (option.newerStartOs) { if (newerStartOs) {
return { return {
text: `Unavailable. Backup was made on a newer version of StartOS.`, text: `Unavailable. Backup was made on a newer version of StartOS.`,
color: 'var(--tui-status-negative)', color: 'var(--tui-status-negative)',
} }
} }
if (option.installed) { if (installed) {
return { return {
text: `Unavailable. ${option.title} is already installed.`, text: `Unavailable. ${title} is already installed.`,
color: 'var(--tui-status-warning)', color: 'var(--tui-status-warning)',
} }
} }

View File

@@ -1,10 +1,11 @@
import { KeyValuePipe } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject, inject,
signal, signal,
} from '@angular/core' } from '@angular/core'
import { ErrorService, Exver, isEmptyObject } from '@start9labs/shared' import { ErrorService, Exver } from '@start9labs/shared'
import { import {
TuiButton, TuiButton,
TuiDialogContext, TuiDialogContext,
@@ -17,15 +18,15 @@ import {
POLYMORPHEUS_CONTEXT, POLYMORPHEUS_CONTEXT,
PolymorpheusComponent, PolymorpheusComponent,
} from '@taiga-ui/polymorpheus' } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { BackupTarget } from 'src/app/services/api/api.types' import { BackupTarget } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { BackupsStatusComponent } from '../components/status.component' import { BackupsStatusComponent } from '../components/status.component'
import { GetDisplayInfoPipe } from '../pipes/get-display-info.pipe' import { GetDisplayInfoPipe } from '../pipes/get-display-info.pipe'
import { BackupType } from '../types/backup-type' import { BackupType } from '../types/backup-type'
import { TARGETS } from './targets.component' import { TARGETS } from './targets.component'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
template: ` template: `
@@ -33,20 +34,20 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
<tui-loader size="l" [textContent]="text" /> <tui-loader size="l" [textContent]="text" />
} @else { } @else {
<h3 class="g-title">Saved Targets</h3> <h3 class="g-title">Saved Targets</h3>
@for (target of targets; track $index) { @for (target of targets | keyvalue; track $index) {
<button <button
class="g-action" class="g-action"
[disabled]="isDisabled(target)" [disabled]="isDisabled(target.value)"
(click)="context.completeWith(target)" (click)="select(target.value, target.key)"
> >
@if (target | getDisplayInfo; as displayInfo) { @if (target.value | getDisplayInfo; as displayInfo) {
<tui-icon [icon]="displayInfo.icon" /> <tui-icon [icon]="displayInfo.icon" />
<div> <div>
<strong>{{ displayInfo.name }}</strong> <strong>{{ displayInfo.name }}</strong>
<backups-status <backups-status
[type]="context.data.type" [type]="context.data.type"
[mountable]="target.mountable" [mountable]="target.value.mountable"
[hasBackup]="hasBackup(target)" [hasBackup]="hasBackup(target.value)"
/> />
<div [style.color]="'var(--tui-text-secondary'"> <div [style.color]="'var(--tui-text-secondary'">
{{ displayInfo.description }} {{ displayInfo.description }}
@@ -70,6 +71,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
TuiIcon, TuiIcon,
BackupsStatusComponent, BackupsStatusComponent,
GetDisplayInfoPipe, GetDisplayInfoPipe,
KeyValuePipe,
], ],
}) })
export class BackupsTargetModal { export class BackupsTargetModal {
@@ -80,9 +82,9 @@ export class BackupsTargetModal {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly context = readonly context =
inject<TuiDialogContext<BackupTarget, { type: BackupType }>>( inject<
POLYMORPHEUS_CONTEXT, TuiDialogContext<BackupTarget & { id: string }, { type: BackupType }>
) >(POLYMORPHEUS_CONTEXT)
readonly loading = signal(true) readonly loading = signal(true)
readonly text = readonly text =
@@ -91,7 +93,7 @@ export class BackupsTargetModal {
: 'Loading Backup Sources' : 'Loading Backup Sources'
serverId = '' serverId = ''
targets: BackupTarget[] = [] targets: Record<string, BackupTarget> = {}
async ngOnInit() { async ngOnInit() {
try { try {
@@ -127,6 +129,10 @@ export class BackupsTargetModal {
.open(TARGETS, { label: 'Backup Targets', size: 'l' }) .open(TARGETS, { label: 'Backup Targets', size: 'l' })
.subscribe() .subscribe()
} }
select(target: BackupTarget, id: string) {
this.context.completeWith({ ...target, id })
}
} }
export const TARGET = new PolymorpheusComponent(BackupsTargetModal) export const TARGET = new PolymorpheusComponent(BackupsTargetModal)

View File

@@ -96,7 +96,7 @@ export class BackupsTargetsModal implements OnInit {
this.targets.set(await this.api.getBackupTargets({})) this.targets.set(await this.api.getBackupTargets({}))
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
this.targets.set({ unknownDisks: [], saved: [] }) this.targets.set({ unknownDisks: [], saved: {} })
} }
} }
@@ -105,7 +105,12 @@ export class BackupsTargetsModal implements OnInit {
try { try {
await this.api.removeBackupTarget({ id }) await this.api.removeBackupTarget({ id })
this.setTargets(this.targets()?.saved.filter(a => a.id !== id))
const saved = this.targets()?.saved || {}
delete saved[id]
this.setTargets(saved)
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {
@@ -113,7 +118,11 @@ export class BackupsTargetsModal implements OnInit {
} }
} }
async onUpdate(value: BackupTarget) { async onUpdate(id: string) {
const value = this.targets()?.saved[id]
if (!value) return
this.formDialog.open(FormComponent, { this.formDialog.open(FormComponent, {
label: 'Update Target', label: 'Update Target',
data: { data: {
@@ -127,7 +136,7 @@ export class BackupsTargetsModal implements OnInit {
| RR.UpdateCifsBackupTargetReq | RR.UpdateCifsBackupTargetReq
| RR.UpdateCloudBackupTargetReq | RR.UpdateCloudBackupTargetReq
| RR.UpdateDiskBackupTargetReq, | RR.UpdateDiskBackupTargetReq,
) => this.update(value.type, { ...response, id: value.id }), ) => this.update(value.type, { ...response, id }),
}, },
], ],
}, },
@@ -148,8 +157,13 @@ export class BackupsTargetsModal implements OnInit {
logicalname: disk.logicalname, logicalname: disk.logicalname,
...value, ...value,
}).then(response => { }).then(response => {
const [id, entry] = Object.entries(response)[0]
const saved = this.targets()?.saved || {}
saved[id] = entry
this.setTargets( this.setTargets(
this.targets()?.saved.concat(response), saved,
this.targets()?.unknownDisks.filter(a => a !== disk), this.targets()?.unknownDisks.filter(a => a !== disk),
) )
return true return true
@@ -185,7 +199,7 @@ export class BackupsTargetsModal implements OnInit {
| RR.AddCifsBackupTargetReq | RR.AddCifsBackupTargetReq
| RR.AddCloudBackupTargetReq | RR.AddCloudBackupTargetReq
| RR.AddDiskBackupTargetReq, | RR.AddDiskBackupTargetReq,
): Promise<BackupTarget> { ): Promise<RR.AddBackupTargetRes> {
const loader = this.loader.open('Saving target...').subscribe() const loader = this.loader.open('Saving target...').subscribe()
try { try {
@@ -201,7 +215,7 @@ export class BackupsTargetsModal implements OnInit {
| RR.UpdateCifsBackupTargetReq | RR.UpdateCifsBackupTargetReq
| RR.UpdateCloudBackupTargetReq | RR.UpdateCloudBackupTargetReq
| RR.UpdateDiskBackupTargetReq, | RR.UpdateDiskBackupTargetReq,
): Promise<BackupTarget> { ): Promise<RR.UpdateBackupTargetRes> {
const loader = this.loader.open('Saving target...').subscribe() const loader = this.loader.open('Saving target...').subscribe()
try { try {
@@ -212,7 +226,7 @@ export class BackupsTargetsModal implements OnInit {
} }
private setTargets( private setTargets(
saved: BackupTarget[] = this.targets()?.saved || [], saved: Record<string, BackupTarget> = this.targets()?.saved || {},
unknownDisks: UnknownDisk[] = this.targets()?.unknownDisks || [], unknownDisks: UnknownDisk[] = this.targets()?.unknownDisks || [],
) { ) {
this.targets.set({ unknownDisks, saved }) this.targets.set({ unknownDisks, saved })

View File

@@ -6,7 +6,7 @@ import { BackupTargetType } from 'src/app/services/api/api.types'
standalone: true, standalone: true,
}) })
export class GetBackupIconPipe implements PipeTransform { export class GetBackupIconPipe implements PipeTransform {
transform(type: BackupTargetType) { transform(type: BackupTargetType = 'disk') {
switch (type) { switch (type) {
case 'cifs': case 'cifs':
return '@tui.folder' return '@tui.folder'

View File

@@ -17,7 +17,7 @@ export class BackupsCreateService {
readonly handle = () => { readonly handle = () => {
this.dialogs this.dialogs
.open<BackupTarget>(TARGET, TARGET_CREATE) .open<BackupTarget & { id: string }>(TARGET, TARGET_CREATE)
.pipe( .pipe(
switchMap(({ id }) => switchMap(({ id }) =>
this.dialogs this.dialogs

View File

@@ -41,7 +41,7 @@ export class BackupsRestoreService {
readonly handle = () => { readonly handle = () => {
this.dialogs this.dialogs
.open<BackupTarget>(TARGET, TARGET_RESTORE) .open<BackupTarget & { id: string }>(TARGET, TARGET_RESTORE)
.pipe( .pipe(
switchMap(target => switchMap(target =>
this.dialogs this.dialogs

View File

@@ -2,40 +2,46 @@ import { BackupJob, BackupTarget, RR } from 'src/app/services/api/api.types'
export class BackupJobBuilder { export class BackupJobBuilder {
name: string name: string
target: BackupTarget targetId: string
cron: string cron: string
packageIds: string[] packageIds: string[]
now = false now = false
constructor(readonly job: Partial<BackupJob>) { constructor(readonly job: Partial<BackupJob>) {
const { name, target, cron } = job const {
this.name = name || '' name = '',
this.target = target || ({} as BackupTarget) targetId = '',
this.cron = cron || '0 2 * * *' cron = '0 2 * * *',
this.packageIds = job.packageIds || [] packageIds = [],
} = job
this.name = name
this.targetId = targetId
this.cron = cron
this.packageIds = packageIds
} }
buildCreate(): RR.CreateBackupJobReq { buildCreate(): RR.CreateBackupJobReq {
const { name, target, cron, now } = this const { name, targetId, cron, now, packageIds } = this
return { return {
name, name,
targetId: target.id, targetId,
cron, cron,
packageIds: this.packageIds, packageIds,
now, now,
} }
} }
buildUpdate(id: string): RR.UpdateBackupJobReq { buildUpdate(id: string): RR.UpdateBackupJobReq {
const { name, target, cron } = this const { name, targetId, cron, packageIds } = this
return { return {
id, id,
name, name,
targetId: target.id, targetId,
cron, cron,
packageIds: this.packageIds, packageIds,
} }
} }
} }

View File

@@ -1,20 +1,15 @@
import { CommonModule } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
inject, inject,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared'
ErrorService,
LoadingService,
pauseFor,
SharedPipesModule,
} from '@start9labs/shared'
import { TuiLet } from '@taiga-ui/cdk'
import { import {
TuiAlertService, TuiAlertService,
TuiAppearance,
TuiButton, TuiButton,
TuiDialogOptions, TuiDialogOptions,
TuiLoader, TuiLoader,
@@ -38,53 +33,58 @@ import { wifiSpec } from './wifi.const'
@Component({ @Component({
template: ` template: `
<wifi-info /> <wifi-info />
<ng-container *tuiLet="enabled$ | async as enabled"> @if (status()?.interface) {
<h3 class="g-title"> <h3 class="g-title">
Wi-Fi Wi-Fi
<input <input
type="checkbox" type="checkbox"
tuiSwitch tuiSwitch
[ngModel]="enabled" [ngModel]="status()?.enabled"
(ngModelChange)="onToggle($event)" (ngModelChange)="onToggle($event)"
/> />
</h3> </h3>
<ng-container *ngIf="enabled"> @if (status()?.enabled) {
<ng-container *ngIf="wifi$ | async as wifi; else loading"> @if (wifi(); as data) {
<ng-container *ngIf="wifi.known.length"> @if (data.known.length) {
<h3 class="g-title">Known Networks</h3> <h3 class="g-title">Known Networks</h3>
<div tuiCard="l" [wifi]="wifi.known"></div> <div tuiCardLarge tuiAppearance="neutral" [wifi]="data.known"></div>
</ng-container> }
<ng-container *ngIf="wifi.available.length"> @if (data.available.length) {
<h3 class="g-title">Other Networks</h3> <h3 class="g-title">Other Networks</h3>
<div tuiCard="l" [wifi]="wifi.available"></div> <div
</ng-container> tuiCardLarge
tuiAppearance="neutral"
[wifi]="data.available"
></div>
}
<p> <p>
<button <button
tuiButton tuiButton
size="s" size="s"
appearance="opposite" appearance="opposite"
(click)="other(wifi)" (click)="other(data)"
> >
Other... Other...
</button> </button>
</p> </p>
</ng-container> } @else {
<ng-template #loading><tui-loader></tui-loader></ng-template> <tui-loader />
</ng-container> }
</ng-container> }
} @else {
<p>No wireless interface detected.</p>
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [ imports: [
CommonModule,
FormsModule, FormsModule,
TuiButton, TuiButton,
TuiSwitch, TuiSwitch,
TuiLet,
TuiCardLarge, TuiCardLarge,
TuiLoader, TuiLoader,
SharedPipesModule, TuiAppearance,
WifiInfoComponent, WifiInfoComponent,
WifiTableComponent, WifiTableComponent,
], ],
@@ -97,14 +97,10 @@ export class SettingsWifiComponent {
private readonly update$ = new Subject<WifiData>() private readonly update$ = new Subject<WifiData>()
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)
private readonly cdr = inject(ChangeDetectorRef) private readonly cdr = inject(ChangeDetectorRef)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly wifi$ = merge(this.getWifi$(), this.update$) readonly status = toSignal(this.patch.watch$('serverInfo', 'network', 'wifi'))
readonly enabled$ = inject<PatchDB<DataModel>>(PatchDB).watch$( readonly wifi = toSignal(merge(this.getWifi$(), this.update$))
'serverInfo',
'network',
'wifi',
'enabled',
)
async onToggle(enable: boolean) { async onToggle(enable: boolean) {
const loader = this.loader const loader = this.loader

View File

@@ -1006,9 +1006,8 @@ export module Mock {
startOs: {}, startOs: {},
}, },
], ],
saved: [ saved: {
{ hsbdjhasbasda: {
id: 'hsbdjhasbasda',
type: 'cifs', type: 'cifs',
name: 'Embassy Backups', name: 'Embassy Backups',
hostname: 'smb://192.169.10.0', hostname: 'smb://192.169.10.0',
@@ -1026,8 +1025,7 @@ export module Mock {
}, },
}, },
}, },
{ ftcvewdnkemfksdm: {
id: 'ftcvewdnkemfksdm',
type: 'cloud', type: 'cloud',
name: 'Dropbox 1', name: 'Dropbox 1',
provider: 'dropbox', provider: 'dropbox',
@@ -1035,8 +1033,7 @@ export module Mock {
mountable: false, mountable: false,
startOs: {}, startOs: {},
}, },
{ csgashbdjkasnd: {
id: 'csgashbdjkasnd',
type: 'cifs', type: 'cifs',
name: 'Network Folder 2', name: 'Network Folder 2',
hostname: 'smb://192.169.10.0', hostname: 'smb://192.169.10.0',
@@ -1045,8 +1042,7 @@ export module Mock {
mountable: true, mountable: true,
startOs: {}, startOs: {},
}, },
{ powjefhjbnwhdva: {
id: 'powjefhjbnwhdva',
type: 'disk', type: 'disk',
name: 'Physical Drive 1', name: 'Physical Drive 1',
logicalname: 'sdba1', logicalname: 'sdba1',
@@ -1068,21 +1064,21 @@ export module Mock {
}, },
}, },
}, },
], },
} }
export const BackupJobs: RR.GetBackupJobsRes = [ export const BackupJobs: RR.GetBackupJobsRes = [
{ {
id: 'lalalalalala-babababababa', id: 'lalalalalala-babababababa',
name: 'My Backup Job', name: 'My Backup Job',
target: BackupTargets.saved[0], targetId: Object.keys(BackupTargets.saved)[0],
cron: '0 3 * * *', cron: '0 3 * * *',
packageIds: ['bitcoind', 'lnd'], packageIds: ['bitcoind', 'lnd'],
}, },
{ {
id: 'hahahahaha-mwmwmwmwmwmw', id: 'hahahahaha-mwmwmwmwmwmw',
name: 'Another Backup Job', name: 'Another Backup Job',
target: BackupTargets.saved[1], targetId: Object.keys(BackupTargets.saved)[1],
cron: '0 * * * *', cron: '0 * * * *',
packageIds: ['lnd'], packageIds: ['lnd'],
}, },

View File

@@ -256,7 +256,7 @@ export module RR {
export type GetBackupTargetsReq = {} // backup.target.list export type GetBackupTargetsReq = {} // backup.target.list
export type GetBackupTargetsRes = { export type GetBackupTargetsRes = {
unknownDisks: UnknownDisk[] unknownDisks: UnknownDisk[]
saved: BackupTarget[] saved: Record<string, BackupTarget>
} }
export type AddCifsBackupTargetReq = { export type AddCifsBackupTargetReq = {
@@ -277,7 +277,7 @@ export module RR {
name: string name: string
path: string path: string
} // backup.target.disk.add } // backup.target.disk.add
export type AddBackupTargetRes = BackupTarget export type AddBackupTargetRes = Record<string, BackupTarget>
export type UpdateCifsBackupTargetReq = AddCifsBackupTargetReq & { export type UpdateCifsBackupTargetReq = AddCifsBackupTargetReq & {
id: string id: string
@@ -539,7 +539,6 @@ export interface UnknownDisk {
} }
export interface BaseBackupTarget { export interface BaseBackupTarget {
id: string
type: BackupTargetType type: BackupTargetType
name: string name: string
mountable: boolean mountable: boolean
@@ -574,7 +573,7 @@ export type BackupRun = {
export type BackupJob = { export type BackupJob = {
id: string id: string
name: string name: string
target: BackupTarget targetId: string
cron: string // '* * * * * *' https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules cron: string // '* * * * * *' https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules
packageIds: string[] packageIds: string[]
} }

View File

@@ -822,14 +822,15 @@ export class MockApiService extends ApiService {
await pauseFor(2000) await pauseFor(2000)
const { path, name } = params const { path, name } = params
return { return {
id: 'latfgvwdbhjsndmk', latfgvwdbhjsndmk: {
name, name,
type: 'cifs', type: 'cifs',
hostname: 'mockhotname', hostname: 'mockhotname',
path: path.replace(/\\/g, '/'), path: path.replace(/\\/g, '/'),
username: 'mockusername', username: 'mockusername',
mountable: true, mountable: true,
startOs: {}, startOs: {},
},
} }
} }
@@ -838,7 +839,7 @@ export class MockApiService extends ApiService {
params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq, params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq,
): Promise<RR.UpdateBackupTargetRes> { ): Promise<RR.UpdateBackupTargetRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.BackupTargets.saved.find(b => b.id === params.id)! return { [params.id]: Mock.BackupTargets.saved[params.id] }
} }
async removeBackupTarget( async removeBackupTarget(
@@ -862,7 +863,7 @@ export class MockApiService extends ApiService {
return { return {
id: 'hjdfbjsahdbn', id: 'hjdfbjsahdbn',
name: params.name, name: params.name,
target: Mock.BackupTargets.saved[0], targetId: Object.keys(Mock.BackupTargets.saved)[0],
cron: params.cron, cron: params.cron,
packageIds: params.packageIds, packageIds: params.packageIds,
} }

View File

@@ -79,6 +79,9 @@ export const mockPatchData: DataModel = {
wifi: { wifi: {
enabled: false, enabled: false,
lastRegion: null, lastRegion: null,
interface: 'test',
ssids: [],
selected: null,
}, },
wanConfig: { wanConfig: {
upnp: false, upnp: false,

View File

@@ -86,6 +86,9 @@ export type PortForward = {
export type WiFiInfo = { export type WiFiInfo = {
enabled: boolean enabled: boolean
lastRegion: string | null lastRegion: string | null
interface: string | null
ssids: Array<string>
selected: string | null
} }
export type Domain = { export type Domain = {

View File

@@ -10,7 +10,7 @@ export interface PackageStatus {
export function renderPkgStatus( export function renderPkgStatus(
pkg: PackageDataEntry, pkg: PackageDataEntry,
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors = {},
): PackageStatus { ): PackageStatus {
let primary: PrimaryStatus let primary: PrimaryStatus
let dependency: DependencyStatus | null = null let dependency: DependencyStatus | null = null