mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
fix: final fixes
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,8 @@ 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()?.type ? 'success' : 'warning'">
|
||||
{{ target()?.type || 'Select target' }}
|
||||
</tui-badge>
|
||||
</button>
|
||||
<button
|
||||
@@ -111,6 +113,12 @@ export class BackupsEditModal {
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<BackupJob, BackupJobBuilder>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly target = toSignal(
|
||||
from(this.api.getBackupTargets({})).pipe(
|
||||
map(({ saved }) => saved[this.job.targetId]),
|
||||
),
|
||||
)
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ export const mockPatchData: DataModel = {
|
||||
wifi: {
|
||||
enabled: false,
|
||||
lastRegion: null,
|
||||
interface: 'test',
|
||||
ssids: [],
|
||||
selected: null,
|
||||
},
|
||||
wanConfig: {
|
||||
upnp: false,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user