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

View File

@@ -111,7 +111,7 @@ export class ServiceComponent implements OnChanges {
readonly connected$ = inject(ConnectionService)
get installed(): boolean {
return this.pkg.stateInfo.state !== 'installed'
return this.pkg.stateInfo.state === 'installed'
}
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 { ActionsService } from 'src/app/services/actions.service'
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'
const STOPPABLE = ['running', 'starting', 'restarting']
@Component({
selector: 'service-actions',
template: `
@if (pkg.status.main.status === 'running') {
@if (canStop) {
<button
tuiButton
appearance="danger-solid"
@@ -25,7 +28,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
>
Stop
</button>
}
@if (canRestart) {
<button
tuiButton
iconStart="@tui.rotate-cw"
@@ -35,17 +40,17 @@ import { getManifest } from 'src/app/utils/get-package-data'
</button>
}
@if (pkg.status.main.status === 'stopped' && isConfigured) {
@if (canStart) {
<button
tuiButton
iconStart="@tui.play"
(click)="actions.start(manifest, hasUnmet(dependencies))"
(click)="actions.start(manifest, hasUnmet(service.dependencies))"
>
Start
</button>
}
@if (!isConfigured) {
@if (canConfigure) {
<button
tuiButton
appearance="secondary-warning"
@@ -73,19 +78,32 @@ import { getManifest } from 'src/app/utils/get-package-data'
})
export class ServiceActionsComponent {
@Input({ required: true })
pkg!: PackageDataEntry
@Input({ required: true })
dependencies: readonly DependencyInfo[] = []
service!: {
pkg: PackageDataEntry
dependencies: readonly DependencyInfo[]
status: PackageStatus
}
readonly actions = inject(ActionsService)
get isConfigured(): boolean {
return this.pkg.status.configured
get manifest(): T.Manifest {
return getManifest(this.service.pkg)
}
get manifest(): T.Manifest {
return getManifest(this.pkg)
get canStop(): boolean {
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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { toSignal } from '@angular/core/rxjs-interop'
import { TuiCheckbox } from '@taiga-ui/kit'
import { CommonModule } from '@angular/common'
import {
@@ -11,6 +12,7 @@ import { ErrorService, LoadingService } from '@start9labs/shared'
import { TUI_TRUE_HANDLER, TUI_FALSE_HANDLER } from '@taiga-ui/cdk'
import { TuiDialogService, TuiIcon, TuiLink, TuiButton } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { from } from 'rxjs'
import { REPORT } from 'src/app/components/report.component'
import { BackupRun } from 'src/app/services/api/api.types'
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>
</td>
<td [style.grid-column]="'span 2'">
<tui-icon [icon]="run.job.target.type | getBackupIcon" />
{{ run.job.target.name }}
@if (targets()?.saved?.[run.job.targetId]; as target) {
<tui-icon [icon]="target.type | getBackupIcon" />
{{ target.name }}
}
</td>
</tr>
} @empty {
@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 {
@for (row of ['', '']; track $index) {
<tr>
<td colspan="6"><div class="tui-skeleton">Loading</div></td>
<td colspan="6">
<div class="tui-skeleton">Loading</div>
</td>
</tr>
}
}
@@ -166,7 +174,8 @@ export class BackupsHistoryModal {
private readonly errorService = inject(ErrorService)
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[] = []
get all(): boolean | null {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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