mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Merge pull request #2706 from Start9Labs/setup-wizard
fix: fix merge issues for setup-wizard project
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"outputPath": "dist/raw/ui",
|
||||
"index": "projects/ui/src/index.html",
|
||||
"main": "projects/ui/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"polyfills": ["zone.js", "projects/ui/src/polyfills.ts"],
|
||||
"tsConfig": "projects/ui/tsconfig.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
|
||||
@@ -14,7 +14,7 @@ export class AppComponent {
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const inProgress = await this.api.getSetupStatus()
|
||||
const inProgress = await this.api.getStatus()
|
||||
|
||||
let route = 'home'
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { PreloadAllModules, RouterModule } from '@angular/router'
|
||||
import {
|
||||
provideSetupLogsService,
|
||||
provideSetupService,
|
||||
RELATIVE_URL,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
@@ -34,7 +33,6 @@ const {
|
||||
],
|
||||
providers: [
|
||||
NG_EVENT_PLUGINS,
|
||||
provideSetupService(ApiService),
|
||||
provideSetupLogsService(ApiService),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TuiInputModule, TuiInputPasswordModule } from '@taiga-ui/legacy'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, Inject } from '@angular/core'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@@ -9,24 +8,26 @@ import {
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { LoadingService, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiError,
|
||||
TuiButton,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_VALIDATION_ERRORS, TuiFieldErrorPipe } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { PASSWORD } from 'src/app/components/password.component'
|
||||
import { TuiInputModule, TuiInputPasswordModule } from '@taiga-ui/legacy'
|
||||
import {
|
||||
ApiService,
|
||||
CifsBackupTarget,
|
||||
CifsRecoverySource,
|
||||
} from 'src/app/services/api.service'
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { SERVERS, ServersResponse } from 'src/app/components/servers.component'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
|
||||
interface Context {
|
||||
cifs: CifsRecoverySource
|
||||
recoveryPassword: string
|
||||
export interface CifsResponse {
|
||||
cifs: T.Cifs
|
||||
serverId: string
|
||||
password: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -34,10 +35,10 @@ interface Context {
|
||||
template: `
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<tui-input formControlName="hostname">
|
||||
Hostname
|
||||
Hostname *
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
placeholder="'My Computer' OR 'my-computer.local'"
|
||||
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
|
||||
/>
|
||||
</tui-input>
|
||||
<tui-error
|
||||
@@ -46,7 +47,7 @@ interface Context {
|
||||
></tui-error>
|
||||
|
||||
<tui-input formControlName="path" class="input">
|
||||
Path
|
||||
Path *
|
||||
<input tuiTextfieldLegacy placeholder="/Desktop/my-folder'" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
@@ -55,7 +56,7 @@ interface Context {
|
||||
></tui-error>
|
||||
|
||||
<tui-input formControlName="username" class="input">
|
||||
Username
|
||||
Username *
|
||||
<input tuiTextfieldLegacy placeholder="Enter username" />
|
||||
</tui-input>
|
||||
<tui-error
|
||||
@@ -108,7 +109,7 @@ export class CifsComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<Context>>(POLYMORPHEUS_CONTEXT)
|
||||
inject<TuiDialogContext<CifsResponse>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly form = new FormGroup({
|
||||
hostname: new FormControl('', {
|
||||
@@ -141,7 +142,6 @@ export class CifsComponent {
|
||||
try {
|
||||
const diskInfo = await this.api.verifyCifs({
|
||||
...this.form.getRawValue(),
|
||||
type: 'cifs',
|
||||
password: this.form.value.password
|
||||
? await this.api.encrypt(String(this.form.value.password))
|
||||
: null,
|
||||
@@ -149,35 +149,31 @@ export class CifsComponent {
|
||||
|
||||
loader.unsubscribe()
|
||||
|
||||
this.presentModalPassword(diskInfo)
|
||||
this.selectServer(diskInfo)
|
||||
} catch (e) {
|
||||
loader.unsubscribe()
|
||||
this.presentAlertFailed()
|
||||
this.onFail()
|
||||
}
|
||||
}
|
||||
|
||||
private presentModalPassword(diskInfo: StartOSDiskInfo) {
|
||||
const target: CifsBackupTarget = {
|
||||
...this.form.getRawValue(),
|
||||
mountable: true,
|
||||
startOs: diskInfo,
|
||||
}
|
||||
|
||||
private selectServer(servers: Record<string, StartOSDiskInfo>) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { target },
|
||||
.open<ServersResponse>(SERVERS, {
|
||||
label: 'Select Server to Restore',
|
||||
data: {
|
||||
servers: Object.keys(servers).map(id => ({ id, ...servers[id] })),
|
||||
},
|
||||
})
|
||||
.subscribe(recoveryPassword => {
|
||||
.subscribe(({ password, serverId }) => {
|
||||
this.context.completeWith({
|
||||
cifs: { ...this.form.getRawValue(), type: 'cifs' },
|
||||
recoveryPassword,
|
||||
cifs: { ...this.form.getRawValue() },
|
||||
serverId,
|
||||
password,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private presentAlertFailed() {
|
||||
private onFail() {
|
||||
this.dialogs
|
||||
.open(
|
||||
'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.',
|
||||
@@ -189,3 +185,5 @@ export class CifsComponent {
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
export const CIFS = new PolymorpheusComponent(CifsComponent)
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
Component,
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
NgZone,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { Component, ElementRef, inject, NgZone, OnInit } from '@angular/core'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
// a higher fade factor will make the characters fade quicker
|
||||
const FADE_FACTOR = 0.07
|
||||
@@ -19,7 +12,7 @@ const FADE_FACTOR = 0.07
|
||||
})
|
||||
export class MatrixComponent implements OnInit {
|
||||
private readonly ngZone = inject(NgZone)
|
||||
private readonly window = inject(WINDOW)
|
||||
private readonly window = inject(WA_WINDOW)
|
||||
private readonly el: HTMLCanvasElement = inject(ElementRef).nativeElement
|
||||
private readonly ctx = this.el.getContext('2d')!
|
||||
|
||||
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import {
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api.service'
|
||||
|
||||
interface DialogData {
|
||||
target?: CifsBackupTarget | DiskBackupTarget
|
||||
passwordHash?: string
|
||||
storageDrive?: boolean
|
||||
}
|
||||
|
||||
@@ -73,24 +69,14 @@ export class PasswordComponent {
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<string, DialogData>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly target = this.context.data.target
|
||||
readonly storageDrive = this.context.data.storageDrive
|
||||
readonly password = new FormControl('', { nonNullable: true })
|
||||
readonly confirm = new FormControl('', { nonNullable: true })
|
||||
|
||||
get passwordError(): string | null {
|
||||
if (!this.password.touched || this.target) return null
|
||||
|
||||
if (!this.storageDrive && !this.target?.['embassy-os'])
|
||||
return 'No recovery target' // unreachable
|
||||
|
||||
if (this.password.value.length < 12)
|
||||
return 'Must be 12 characters or greater'
|
||||
|
||||
if (this.password.value.length > 64)
|
||||
return 'Must be less than 65 characters'
|
||||
|
||||
return null
|
||||
return this.password.touched && this.password.value.length < 12
|
||||
? 'Must be 12 characters or greater'
|
||||
: null
|
||||
}
|
||||
|
||||
get confirmError(): string | null {
|
||||
@@ -107,9 +93,7 @@ export class PasswordComponent {
|
||||
}
|
||||
|
||||
try {
|
||||
const passwordHash = this.target!.startOs?.passwordHash || ''
|
||||
|
||||
argon2.verify(passwordHash, this.password.value)
|
||||
argon2.verify(this.context.data.passwordHash || '', this.password.value)
|
||||
this.context.completeWith(this.password.value)
|
||||
} catch (e) {
|
||||
this.errorService.handleError('Incorrect password provided')
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { Component, ElementRef, inject, input, Output } from '@angular/core'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { TuiDialogService, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { filter, fromEvent, switchMap } from 'rxjs'
|
||||
import { PASSWORD } from 'src/app/components/password.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'button[server]',
|
||||
template: `
|
||||
<tui-icon icon="@tui.save" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ server().hostname }}.local</strong>
|
||||
<span tuiSubtitle>
|
||||
<b>StartOS Version</b>
|
||||
: {{ server().version }}
|
||||
</span>
|
||||
<span tuiSubtitle>
|
||||
<b>Created</b>
|
||||
: {{ server().timestamp | date: 'medium' }}
|
||||
</span>
|
||||
</span>
|
||||
`,
|
||||
styles: ':host { width: stretch; border-radius: var(--tui-radius-l); }',
|
||||
hostDirectives: [TuiCell],
|
||||
imports: [DatePipe, TuiIcon, TuiTitle],
|
||||
})
|
||||
export class ServerComponent {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
readonly server = input.required<StartOSDiskInfo>()
|
||||
|
||||
@Output()
|
||||
readonly password = fromEvent(inject(ElementRef).nativeElement, 'click').pipe(
|
||||
switchMap(() =>
|
||||
this.dialogs.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { passwordHash: this.server().passwordHash },
|
||||
}),
|
||||
),
|
||||
filter(Boolean),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { ServerComponent } from 'src/app/components/server.component'
|
||||
import { StartOSDiskInfoWithId } from 'src/app/services/api.service'
|
||||
|
||||
interface Data {
|
||||
servers: StartOSDiskInfoWithId[]
|
||||
}
|
||||
|
||||
export interface ServersResponse {
|
||||
password: string
|
||||
serverId: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
@for (server of context.data.servers; track $index) {
|
||||
<button [server]="server" (password)="select($event, server.id)"></button>
|
||||
}
|
||||
`,
|
||||
imports: [ServerComponent],
|
||||
})
|
||||
export class ServersComponent {
|
||||
readonly context =
|
||||
inject<TuiDialogContext<ServersResponse, Data>>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
select(password: string, serverId: string) {
|
||||
this.context.completeWith({ serverId, password })
|
||||
}
|
||||
}
|
||||
|
||||
export const SERVERS = new PolymorpheusComponent(ServersComponent)
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ServerBackupSelectModal } from './server-backup-select.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ServerBackupSelectModal],
|
||||
imports: [CommonModule, FormsModule, IonicModule, PasswordPageModule],
|
||||
exports: [ServerBackupSelectModal],
|
||||
})
|
||||
export class ServerBackupSelectModule {}
|
||||
@@ -1,24 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Select Server to Restore</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item *ngFor="let server of servers" button (click)="select(server)">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<b>Local Hostname</b>
|
||||
: {{ server.hostname }}.local
|
||||
</h2>
|
||||
<h2>
|
||||
<b>StartOS Version</b>
|
||||
: {{ server.version }}
|
||||
</h2>
|
||||
<h2>
|
||||
<b>Created</b>
|
||||
: {{ server.timestamp | date : 'medium' }}
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { StartOSDiskInfoWithId } from 'src/app/services/api/api.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup-select',
|
||||
templateUrl: 'server-backup-select.page.html',
|
||||
styleUrls: ['server-backup-select.page.scss'],
|
||||
})
|
||||
export class ServerBackupSelectModal {
|
||||
@Input() servers: StartOSDiskInfoWithId[] = []
|
||||
|
||||
constructor(private readonly modalController: ModalController) {}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
|
||||
async select(server: StartOSDiskInfoWithId): Promise<void> {
|
||||
this.presentModalPassword(server)
|
||||
}
|
||||
|
||||
private async presentModalPassword(
|
||||
server: StartOSDiskInfoWithId,
|
||||
): Promise<void> {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { passwordHash: server.passwordHash },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalController.dismiss(
|
||||
{
|
||||
serverId: server.id,
|
||||
recoveryPassword: res.data.password,
|
||||
},
|
||||
'success',
|
||||
)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
Back
|
||||
</button>
|
||||
}
|
||||
{{ recover ? 'StartOS Setup' : 'Recover Options' }}
|
||||
{{ recover ? 'Recover Options' : 'StartOS Setup' }}
|
||||
</header>
|
||||
<div class="pages">
|
||||
<div class="options" [class.options_recover]="recover">
|
||||
|
||||
@@ -1,20 +1,105 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { Router } from '@angular/router'
|
||||
import { InitializingComponent } from '@start9labs/shared'
|
||||
import { ErrorService, InitializingComponent } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
filter,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<app-initializing
|
||||
[setupType]="stateService.setupType"
|
||||
(finished)="router.navigate(['success'])"
|
||||
/>
|
||||
`,
|
||||
template: '<app-initializing [setupType]="type" [progress]="progress()" />',
|
||||
styles: ':host { max-width: unset; align-items: stretch; }',
|
||||
imports: [InitializingComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export default class LoadingPage {
|
||||
readonly stateService = inject(StateService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
readonly type = inject(StateService).setupType
|
||||
readonly router = inject(Router)
|
||||
readonly progress = toSignal(
|
||||
from(this.getStatus()).pipe(
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
switchMap(({ guid, progress }) =>
|
||||
this.api.openProgressWebsocket$(guid).pipe(
|
||||
startWith(progress),
|
||||
catchError((_, watch$) =>
|
||||
interval(2000).pipe(
|
||||
switchMap(() =>
|
||||
from(this.api.getStatus()).pipe(catchError(() => EMPTY)),
|
||||
),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
),
|
||||
),
|
||||
tap(({ overall }) => {
|
||||
if (overall === true) {
|
||||
this.getStatus()
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
map(({ phases, overall }) => ({
|
||||
total: getDecimal(overall),
|
||||
message: phases
|
||||
.filter(p => p.progress !== true && p.progress !== null)
|
||||
.map(p => `${p.name}${getPhaseBytes(p.progress)}`)
|
||||
.join(','),
|
||||
})),
|
||||
catchError(e => {
|
||||
this.errorService.handleError(e)
|
||||
return EMPTY
|
||||
}),
|
||||
),
|
||||
{ initialValue: { total: 0, message: '' } },
|
||||
)
|
||||
|
||||
private async getStatus(): Promise<{
|
||||
status: 'running'
|
||||
guid: string
|
||||
progress: T.FullProgress
|
||||
} | null> {
|
||||
const res = await this.api.getStatus()
|
||||
|
||||
if (!res) {
|
||||
this.router.navigate(['home'])
|
||||
return null
|
||||
} else if (res.status === 'complete') {
|
||||
this.router.navigate(['success'])
|
||||
return null
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDecimal(progress: T.Progress): number {
|
||||
if (progress === true) {
|
||||
return 1
|
||||
} else if (!progress || !progress.total) {
|
||||
return 0
|
||||
} else {
|
||||
return progress.total && progress.done / progress.total
|
||||
}
|
||||
}
|
||||
|
||||
function getPhaseBytes(progress: T.Progress): string {
|
||||
return progress === true || !progress
|
||||
? ''
|
||||
: `: (${progress.done}/${progress.total})`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { DriveComponent, ErrorService } from '@start9labs/shared'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogService,
|
||||
@@ -9,15 +10,9 @@ import {
|
||||
TuiTitle,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiCardLarge, TuiCell } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { filter } from 'rxjs'
|
||||
import { CifsComponent } from 'src/app/components/cifs.component'
|
||||
import { PASSWORD } from 'src/app/components/password.component'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api.service'
|
||||
import { CIFS, CifsResponse } from 'src/app/components/cifs.component'
|
||||
import { ServerComponent } from 'src/app/components/server.component'
|
||||
import { ApiService, StartOSDiskInfoFull } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
@@ -38,8 +33,10 @@ import { StateService } from 'src/app/services/state.service'
|
||||
</button>
|
||||
|
||||
<h2>Physical Drive</h2>
|
||||
Restore StartOS data from a physical drive that is plugged directly into
|
||||
your server.
|
||||
<div>
|
||||
Restore StartOS data from a physical drive that is plugged directly
|
||||
into your server.
|
||||
</div>
|
||||
<strong>
|
||||
Warning. Do not use this option if you are using a Raspberry Pi with
|
||||
an external SSD as your main data drive. The Raspberry Pi cannot not
|
||||
@@ -47,18 +44,11 @@ import { StateService } from 'src/app/services/state.service'
|
||||
cause data corruption.
|
||||
</strong>
|
||||
|
||||
@for (d of drives; track d) {
|
||||
<button tuiCell [drive]="d" [disabled]="empty(d)" (click)="select(d)">
|
||||
<span tuiSubtitle>
|
||||
@if (empty(d)) {
|
||||
<tui-icon icon="@tui.cloud-off" class="g-error" />
|
||||
<strong>No StartOS backup</strong>
|
||||
} @else {
|
||||
<tui-icon icon="@tui.cloud" class="g-success" />
|
||||
<strong>StartOS backup detected</strong>
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
@for (server of servers; track $index) {
|
||||
<button
|
||||
[server]="server"
|
||||
(password)="select($event, server)"
|
||||
></button>
|
||||
}
|
||||
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
@@ -74,7 +64,8 @@ import { StateService } from 'src/app/services/state.service'
|
||||
TuiCell,
|
||||
TuiIcon,
|
||||
TuiTitle,
|
||||
DriveComponent,
|
||||
DatePipe,
|
||||
ServerComponent,
|
||||
],
|
||||
})
|
||||
export default class RecoverPage {
|
||||
@@ -85,7 +76,7 @@ export default class RecoverPage {
|
||||
private readonly stateService = inject(StateService)
|
||||
|
||||
loading = true
|
||||
drives: DiskBackupTarget[] = []
|
||||
servers: StartOSDiskInfoFull[] = []
|
||||
|
||||
async ngOnInit() {
|
||||
this.stateService.setupType = 'restore'
|
||||
@@ -97,21 +88,21 @@ export default class RecoverPage {
|
||||
await this.getDrives()
|
||||
}
|
||||
|
||||
empty(drive: DiskBackupTarget) {
|
||||
return !drive.startOs?.full
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
this.drives = []
|
||||
this.servers = []
|
||||
|
||||
try {
|
||||
await this.api.getDrives().then(disks =>
|
||||
disks
|
||||
.filter(d => d.partitions.length)
|
||||
.forEach(d => {
|
||||
d.partitions.forEach(p => {
|
||||
this.drives.push({ ...d, ...p })
|
||||
})
|
||||
}),
|
||||
const drives = await this.api.getDrives()
|
||||
|
||||
this.servers = drives.flatMap(drive =>
|
||||
drive.partitions.flatMap(partition =>
|
||||
Object.entries(partition.startOs).map(([id, val]) => ({
|
||||
id,
|
||||
...val,
|
||||
partition,
|
||||
drive,
|
||||
})),
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
@@ -120,44 +111,35 @@ export default class RecoverPage {
|
||||
}
|
||||
}
|
||||
|
||||
select(target: DiskBackupTarget) {
|
||||
const { logicalname } = target
|
||||
|
||||
if (!logicalname) return
|
||||
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Unlock Drive',
|
||||
size: 's',
|
||||
data: { target },
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(password => {
|
||||
this.onSource(logicalname, password)
|
||||
})
|
||||
select(password: string, server: StartOSDiskInfoFull) {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'backup',
|
||||
target: {
|
||||
type: 'disk',
|
||||
logicalname: server.partition.logicalname,
|
||||
},
|
||||
serverId: server.id,
|
||||
password,
|
||||
}
|
||||
this.router.navigate(['storage'])
|
||||
}
|
||||
|
||||
onCifs() {
|
||||
this.dialogs
|
||||
.open<{
|
||||
cifs: CifsRecoverySource
|
||||
recoveryPassword: string
|
||||
}>(new PolymorpheusComponent(CifsComponent), {
|
||||
.open<CifsResponse>(CIFS, {
|
||||
label: 'Connect Network Folder',
|
||||
})
|
||||
.subscribe(({ cifs, recoveryPassword }) => {
|
||||
this.stateService.recoverySource = { type: 'backup', target: cifs }
|
||||
this.stateService.recoveryPassword = recoveryPassword
|
||||
.subscribe(({ cifs, serverId, password }) => {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'backup',
|
||||
target: {
|
||||
type: 'cifs',
|
||||
...cifs,
|
||||
},
|
||||
serverId,
|
||||
password,
|
||||
}
|
||||
this.router.navigate(['storage'])
|
||||
})
|
||||
}
|
||||
|
||||
private onSource(logicalname: string, password?: string) {
|
||||
this.stateService.recoverySource = {
|
||||
type: 'backup',
|
||||
target: { type: 'disk', logicalname },
|
||||
}
|
||||
this.stateService.recoveryPassword = password
|
||||
this.router.navigate(['storage'])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,7 @@ import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiCell } from '@taiga-ui/layout'
|
||||
import { filter, of, switchMap } from 'rxjs'
|
||||
import { PASSWORD } from 'src/app/components/password.component'
|
||||
import {
|
||||
ApiService,
|
||||
BackupRecoverySource,
|
||||
DiskMigrateSource,
|
||||
DiskRecoverySource,
|
||||
} from 'src/app/services/api.service'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
@@ -44,7 +39,7 @@ import { StateService } from 'src/app/services/state.service'
|
||||
</button>
|
||||
}
|
||||
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="getDrives()">
|
||||
<button tuiButton iconStart="@tui.rotate-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
</section>
|
||||
@@ -81,21 +76,24 @@ export default class StoragePage {
|
||||
const disks = await this.api.getDrives()
|
||||
if (this.stateService.setupType === 'fresh') {
|
||||
this.drives = disks
|
||||
} else if (this.stateService.setupType === 'restore') {
|
||||
this.drives = disks.filter(
|
||||
d =>
|
||||
!d.partitions
|
||||
.map(p => p.logicalname)
|
||||
.includes(
|
||||
(
|
||||
(this.stateService.recoverySource as BackupRecoverySource)
|
||||
?.target as DiskRecoverySource
|
||||
)?.logicalname,
|
||||
),
|
||||
)
|
||||
} else if (this.stateService.setupType === 'transfer') {
|
||||
const guid = (this.stateService.recoverySource as DiskMigrateSource)
|
||||
.guid
|
||||
} else if (
|
||||
this.stateService.setupType === 'restore' &&
|
||||
this.stateService.recoverySource?.type === 'backup'
|
||||
) {
|
||||
if (this.stateService.recoverySource.target.type === 'disk') {
|
||||
const logicalname =
|
||||
this.stateService.recoverySource.target.logicalname
|
||||
this.drives = disks.filter(
|
||||
d => !d.partitions.map(p => p.logicalname).includes(logicalname),
|
||||
)
|
||||
} else {
|
||||
this.drives = disks
|
||||
}
|
||||
} else if (
|
||||
this.stateService.setupType === 'transfer' &&
|
||||
this.stateService.recoverySource?.type === 'migrate'
|
||||
) {
|
||||
const guid = this.stateService.recoverySource.guid
|
||||
this.drives = disks.filter(d => {
|
||||
return (
|
||||
d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid)
|
||||
@@ -130,19 +128,19 @@ export default class StoragePage {
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
// for backup recoveries
|
||||
if (this.stateService.recoveryPassword) {
|
||||
if (this.stateService.recoverySource?.type === 'backup') {
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
this.stateService.recoverySource.password,
|
||||
)
|
||||
} else {
|
||||
// for migrations and fresh setups
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
this.promptPassword(drive.logicalname)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private presentModalPassword(logicalname: string) {
|
||||
private promptPassword(logicalname: string) {
|
||||
this.dialogs
|
||||
.open<string>(PASSWORD, {
|
||||
label: 'Set Password',
|
||||
@@ -162,7 +160,7 @@ export default class StoragePage {
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(logicalname, password)
|
||||
await this.router.navigate([`loading`])
|
||||
await this.router.navigate(['loading'])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -2,6 +2,9 @@ import * as jose from 'node-jose'
|
||||
import {
|
||||
DiskInfo,
|
||||
DiskListResponse,
|
||||
FollowLogsReq,
|
||||
FollowLogsRes,
|
||||
Log,
|
||||
PartitionInfo,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
@@ -22,6 +25,10 @@ export abstract class ApiService {
|
||||
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
abstract openProgressWebsocket$(guid: string): Observable<T.FullProgress>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsReq,
|
||||
FollowLogsRes,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Log,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
SetupStatus,
|
||||
FollowLogsRes,
|
||||
FollowLogsReq,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ApiService } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
import { Observable } from 'rxjs'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { ApiService } from './api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -100,6 +99,14 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||
return this.rpcRequest({ method: 'setup.logs.follow', params })
|
||||
}
|
||||
|
||||
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return webSocket(`http://start.local/ws/${url}`)
|
||||
}
|
||||
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
const res = await this.rpcRequest<T.SetupResult>({
|
||||
method: 'setup.complete',
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsReq,
|
||||
FollowLogsRes,
|
||||
Log,
|
||||
pauseFor,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
Observable,
|
||||
concatMap,
|
||||
delay,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
mergeScan,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
switchScan,
|
||||
takeWhile,
|
||||
} from 'rxjs'
|
||||
import * as jose from 'node-jose'
|
||||
import { interval, map, Observable, of } from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { ApiService } from './api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -279,6 +270,24 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
startCursor: 'fakestartcursor',
|
||||
guid: 'fake-guid',
|
||||
}
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(500).pipe(
|
||||
map(() => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'fake log entry',
|
||||
bootId: 'boot-id',
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { delay, filter } from 'rxjs'
|
||||
import { LogsWindowComponent } from './logs-window.component'
|
||||
import { SetupService } from '../../services/setup.service'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-initializing',
|
||||
template: `
|
||||
<section *tuiLet="progress$ | async as progress">
|
||||
<h1 [style.font-size.rem]="2.5" [style.margin.rem]="1">
|
||||
Initializing StartOS
|
||||
<section>
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
Setting up your server
|
||||
</h1>
|
||||
<div *ngIf="progress" class="center-wrapper">
|
||||
Progress: {{ (progress * 100).toFixed(0) }}%
|
||||
<div *ngIf="progress.total">
|
||||
Progress: {{ (progress.total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
|
||||
<progress
|
||||
tuiProgressBar
|
||||
class="progress"
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress && progress < 1 ? progress : null"
|
||||
[attr.value]="progress.total"
|
||||
></progress>
|
||||
<p>{{ getMessage(progress) }}</p>
|
||||
<p>{{ progress.message }}</p>
|
||||
</section>
|
||||
<logs-window />
|
||||
`,
|
||||
@@ -64,28 +54,9 @@ import { SetupService } from '../../services/setup.service'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InitializingComponent {
|
||||
readonly progress$ = inject(SetupService)
|
||||
@Input()
|
||||
progress: { total: number; message: string } = { total: 0, message: '' }
|
||||
|
||||
@Input()
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
|
||||
@Output()
|
||||
readonly finished = this.progress$.pipe(
|
||||
filter(progress => progress === 1),
|
||||
delay(500),
|
||||
)
|
||||
|
||||
getMessage(progress: number | null): string {
|
||||
if (['fresh', 'attach'].includes(this.setupType || '')) {
|
||||
return 'Setting up your server'
|
||||
}
|
||||
|
||||
if (!progress) {
|
||||
return 'Preparing data. This can take a while'
|
||||
} else if (progress < 1) {
|
||||
return 'Copying data'
|
||||
} else {
|
||||
return 'Finalizing'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||
import { tuiAsDialog } from '@taiga-ui/cdk'
|
||||
import { LoadingComponent } from './loading.component'
|
||||
import { LoadingService } from './loading.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [TuiLoaderModule],
|
||||
declarations: [LoadingComponent],
|
||||
exports: [LoadingComponent],
|
||||
providers: [tuiAsDialog(LoadingService)],
|
||||
})
|
||||
export class LoadingModule {}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { SetupStatus } from '../types/api'
|
||||
import { pauseFor } from '../util/misc.util'
|
||||
|
||||
let tries: number | undefined
|
||||
|
||||
export async function getSetupStatusMock(): Promise<SetupStatus | null> {
|
||||
const restoreOrMigrate = true
|
||||
const total = 4
|
||||
|
||||
await pauseFor(1000)
|
||||
|
||||
if (tries === undefined) {
|
||||
tries = 0
|
||||
return null
|
||||
}
|
||||
|
||||
tries++
|
||||
const progress = tries - 1
|
||||
|
||||
return {
|
||||
bytesTransferred: restoreOrMigrate ? progress : 0,
|
||||
totalBytes: restoreOrMigrate ? total : null,
|
||||
complete: progress === total,
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export * from './components/initializing/logs-window.component'
|
||||
export * from './components/initializing/initializing.component'
|
||||
export * from './components/loading/loading.component'
|
||||
export * from './components/loading/loading.component'
|
||||
export * from './components/loading/loading.module'
|
||||
export * from './components/loading/loading.service'
|
||||
export * from './components/markdown/markdown.component'
|
||||
export * from './components/markdown/markdown.component.module'
|
||||
@@ -20,8 +19,6 @@ export * from './components/drive.component'
|
||||
export * from './directives/drag-scroller.directive'
|
||||
export * from './directives/safe-links.directive'
|
||||
|
||||
export * from './mocks/get-setup-status'
|
||||
|
||||
export * from './pipes/exver/exver.module'
|
||||
export * from './pipes/exver/exver.pipe'
|
||||
export * from './pipes/markdown/markdown.module'
|
||||
@@ -38,7 +35,6 @@ export * from './services/download-html.service'
|
||||
export * from './services/exver.service'
|
||||
export * from './services/error.service'
|
||||
export * from './services/http.service'
|
||||
export * from './services/setup.service'
|
||||
export * from './services/setup-logs.service'
|
||||
|
||||
export * from './types/api'
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { inject, StaticClassProvider } from '@angular/core'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
exhaustMap,
|
||||
filter,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
takeWhile,
|
||||
} from 'rxjs'
|
||||
import { SetupStatus } from '../types/api'
|
||||
import { Constructor } from '../types/constructor'
|
||||
import { ErrorService } from './error.service'
|
||||
|
||||
export function provideSetupService(
|
||||
api: Constructor<ConstructorParameters<typeof SetupService>[0]>,
|
||||
): StaticClassProvider {
|
||||
return {
|
||||
provide: SetupService,
|
||||
deps: [api],
|
||||
useClass: SetupService,
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupService extends Observable<number> {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly progress$ = interval(500).pipe(
|
||||
exhaustMap(() =>
|
||||
from(this.api.getSetupStatus()).pipe(
|
||||
catchError(e => {
|
||||
this.errorService.handleError(e)
|
||||
|
||||
return EMPTY
|
||||
}),
|
||||
),
|
||||
),
|
||||
filter(Boolean),
|
||||
map(progress => {
|
||||
if (progress.complete) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return progress.totalBytes
|
||||
? progress.bytesTransferred / progress.totalBytes
|
||||
: 0
|
||||
}),
|
||||
takeWhile(value => value !== 1, true),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: { getSetupStatus: () => Promise<SetupStatus | null> },
|
||||
) {
|
||||
super(subscriber => this.progress$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,3 @@ export type StartOSDiskInfo = {
|
||||
passwordHash: string | null
|
||||
wrappedKey: string | null
|
||||
}
|
||||
|
||||
export interface SetupStatus {
|
||||
bytesTransferred: number
|
||||
totalBytes: number | null
|
||||
complete: boolean
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { DiagnosticService } from './services/diagnostic.service'
|
||||
import { MockDiagnosticService } from './services/mock-diagnostic.service'
|
||||
import { LiveDiagnosticService } from './services/live-diagnostic.service'
|
||||
|
||||
const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
@@ -22,11 +16,5 @@ const ROUTES: Routes = [
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
providers: [
|
||||
{
|
||||
provide: DiagnosticService,
|
||||
useClass: useMocks ? MockDiagnosticService : LiveDiagnosticService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class DiagnosticModule {}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { DiagnosticService } from '../services/diagnostic.service'
|
||||
========
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
@Component({
|
||||
selector: 'diagnostic-home',
|
||||
@@ -29,14 +22,9 @@ export class HomePage {
|
||||
|
||||
constructor(
|
||||
private readonly loader: LoadingService,
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
private readonly api: DiagnosticService,
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@Inject(WINDOW) private readonly window: Window,
|
||||
========
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -102,11 +90,7 @@ export class HomePage {
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticRestart()
|
||||
@@ -119,11 +103,7 @@ export class HomePage {
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticForgetDrive()
|
||||
@@ -136,31 +116,6 @@ export class HomePage {
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
async presentAlertSystemRebuild() {
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
no: 'Cancel',
|
||||
yes: 'Rebuild',
|
||||
content:
|
||||
'<p>This action will tear down all service containers and rebuild them from scratch. No data will be deleted.</p><p>A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.</p><p>It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.</p>',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
try {
|
||||
this.systemRebuild()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
========
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
async presentAlertRepairDisk() {
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
@@ -187,27 +142,8 @@ export class HomePage {
|
||||
this.window.location.reload()
|
||||
}
|
||||
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
private async systemRebuild(): Promise<void> {
|
||||
const loader = this.loader.open('').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.systemRebuild()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticRepairDisk()
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
||||
import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer'
|
||||
import { convertAnsi, ErrorService } from '@start9labs/shared'
|
||||
import { TuiScrollbar } from '@taiga-ui/core'
|
||||
import { DiagnosticService } from 'src/app/routes/diagnostic/services/diagnostic.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'logs',
|
||||
@@ -28,7 +28,7 @@ import { DiagnosticService } from 'src/app/routes/diagnostic/services/diagnostic
|
||||
export class LogsPage implements OnInit {
|
||||
@ViewChild(TuiScrollbar, { read: ElementRef })
|
||||
private readonly scrollbar?: ElementRef<HTMLElement>
|
||||
private readonly api = inject(DiagnosticService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
startCursor?: string
|
||||
@@ -59,7 +59,7 @@ export class LogsPage implements OnInit {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const response = await this.api.getLogs({
|
||||
const response = await this.api.diagnosticGetLogs({
|
||||
cursor: this.startCursor,
|
||||
before: !!this.startCursor,
|
||||
limit: 200,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
provideSetupLogsService,
|
||||
provideSetupService,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { RR, BackupTargetType, Metrics } from './api.types'
|
||||
import { SetupStatus } from '@start9labs/shared'
|
||||
import { RPCOptions } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
GetPackageRes,
|
||||
GetPackagesRes,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { RPCOptions } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { Observable } from 'rxjs'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
|
||||
export abstract class ApiService {
|
||||
// http
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
|
||||
import {
|
||||
pauseFor,
|
||||
Log,
|
||||
getSetupStatusMock,
|
||||
RPCErrorDetails,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
@@ -1,60 +1,5 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags.ts';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
// @TODO Alex include in angular.json
|
||||
;(window as any).global = window
|
||||
;(window as any).process = { env: { DEBUG: undefined }, browser: true }
|
||||
|
||||
import { Buffer } from 'buffer'
|
||||
;(window as any).Buffer = Buffer
|
||||
|
||||
Reference in New Issue
Block a user