mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user