chore: refactor install and setup wizards (#2561)

* chore: refactor install and setup wizards

* chore: return tui-root
This commit is contained in:
Alex Inkin
2024-02-22 17:58:01 +04:00
committed by GitHub
parent 69d5f521a5
commit 7b41b295b7
109 changed files with 1863 additions and 3538 deletions

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
},
]
@NgModule({
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
preloadingStrategy: PreloadAllModules,
useHash: true,
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -1,6 +1,58 @@
<tui-theme-night></tui-theme-night>
<tui-root tuiMode="onDark">
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
<tui-root>
<main>
<img class="logo" src="assets/img/icon.png" alt="Start9" />
<section tuiCardLarge tuiSurface="elevated" class="card">
<header class="header">
@if (selected) {
<button
tuiIconButton
appearance="flat"
size="m"
class="back"
iconLeft="tuiIconChevronLeft"
[style.border-radius.rem]="10"
(click)="selected = null"
>
Back
</button>
}
<h1>{{ selected ? 'Install Type' : 'Select Disk' }}</h1>
<div [style.color]="'var(--tui-negative)'">{{ error }}</div>
</header>
<div class="pages">
<div class="options" [class.options_selected]="selected">
@for (drive of disks$ | async; track $index) {
<button tuiCell [drive]="drive" (click)="selected = drive"></button>
}
</div>
<div class="options">
@if (guid) {
<button tuiCell (click)="install()">
<tui-icon icon="tuiIconLifeBuoyLarge" />
<span tuiTitle>
<strong [style.color]="'var(--tui-positive)'">
Re-Install StartOS
</strong>
<span tuiSubtitle>Will preserve existing StartOS data</span>
</span>
</button>
}
<button tuiCell [disabled]="!selected" (click)="warn()">
<tui-icon icon="tuiIconDownload" />
<span tuiTitle>
@if (guid) {
<span [style.color]="'var(--tui-negative)'">Factory Reset</span>
} @else {
<span [style.color]="'var(--tui-positive)'">
Install StartOS
</span>
}
<span tuiSubtitle>Will delete existing data on disk</span>
</span>
</button>
</div>
</div>
</section>
</main>
</tui-root>

View File

@@ -1,8 +1,63 @@
:host {
display: block;
height: 100%;
}
@import '@taiga-ui/core/styles/taiga-ui-local';
::ng-deep html,
::ng-deep body,
tui-root {
height: 100%;
margin: 0;
color: var(--tui-text-01);
}
main {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: var(--tui-base-08);
}
.logo {
width: 6rem;
margin-bottom: -2rem;
z-index: 1;
}
.card {
max-width: min(30rem, 90vw);
}
.header {
position: relative;
display: flex;
flex-direction: column;
text-align: center;
padding-top: 0.25rem;
margin-bottom: -2rem;
}
.back {
position: absolute;
top: 1rem;
}
.pages {
display: flex;
align-items: center;
overflow: hidden;
}
.options {
@include transition(margin);
min-width: 100%;
display: flex;
flex-direction: column;
gap: 1.25rem;
padding: 1rem;
box-sizing: border-box;
&_selected {
margin-left: -100%;
}
}

View File

@@ -1,4 +1,10 @@
import { Component } from '@angular/core'
import { Component, inject } from '@angular/core'
import { DiskInfo, LoadingService, toGuid } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { filter, from } from 'rxjs'
import { SUCCESS, toWarning } from 'src/app/app.utils'
import { ApiService } from 'src/app/services/api.service'
@Component({
selector: 'app-root',
@@ -6,5 +12,65 @@ import { Component } from '@angular/core'
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor() {}
private readonly loader = inject(LoadingService)
private readonly api = inject(ApiService)
private readonly dialogs = inject(TuiDialogService)
readonly disks$ = from(this.api.getDisks())
selected: DiskInfo | null = null
error = ''
get guid() {
return toGuid(this.selected)
}
async install(overwrite = false) {
const loader = this.loader.open('Installing StartOS...').subscribe()
const logicalname = this.selected?.logicalname || ''
try {
await this.api.install({ logicalname, overwrite })
this.reboot()
} catch (e: any) {
this.error = e.message
} finally {
loader.unsubscribe()
}
}
warn() {
this.dialogs
.open(TUI_PROMPT, toWarning(this.selected))
.pipe(filter(Boolean))
.subscribe(() => {
this.install(true)
})
}
private async reboot() {
this.dialogs
.open(
'Remove the USB stick and reboot your device to begin using your new Start9 server',
SUCCESS,
)
.subscribe({
complete: async () => {
const loader = this.loader.open('').subscribe()
try {
await this.api.reboot()
this.dialogs
.open(
'Please wait for StartOS to restart, then refresh this page',
{ label: 'Rebooting', size: 's' },
)
.subscribe()
} catch (e: any) {
this.error = e.message
} finally {
loader.unsubscribe()
}
},
})
}
}

View File

@@ -1,24 +1,26 @@
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { RouteReuseStrategy } from '@angular/router'
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
import {
TuiDialogModule,
TuiModeModule,
TuiRootModule,
TuiThemeNightModule,
} from '@taiga-ui/core'
import { AppComponent } from './app.component'
import { AppRoutingModule } from './app-routing.module'
import { HttpClientModule } from '@angular/common/http'
import { ApiService } from './services/api/api.service'
import { MockApiService } from './services/api/mock-api.service'
import { LiveApiService } from './services/api/live-api.service'
import {
DriveComponent,
LoadingModule,
RELATIVE_URL,
UnitConversionPipesModule,
WorkspaceConfig,
} from '@start9labs/shared'
import { TuiDialogModule, TuiRootModule } from '@taiga-ui/core'
import {
TuiButtonModule,
TuiCardModule,
TuiCellModule,
TuiIconModule,
TuiSurfaceModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
import { ApiService } from 'src/app/services/api.service'
import { LiveApiService } from 'src/app/services/live-api.service'
import { MockApiService } from 'src/app/services/mock-api.service'
import { AppComponent } from './app.component'
const {
useMocks,
@@ -30,18 +32,19 @@ const {
imports: [
HttpClientModule,
BrowserAnimationsModule,
IonicModule.forRoot({
mode: 'md',
}),
AppRoutingModule,
TuiRootModule,
TuiDialogModule,
LoadingModule,
TuiModeModule,
TuiThemeNightModule,
DriveComponent,
TuiButtonModule,
TuiCardModule,
TuiCellModule,
TuiIconModule,
TuiSurfaceModule,
TuiTitleModule,
UnitConversionPipesModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService,
useClass: useMocks ? MockApiService : LiveApiService,

View File

@@ -0,0 +1,27 @@
import { DiskInfo } from '@start9labs/shared'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiPromptData } from '@taiga-ui/kit'
export const SUCCESS: Partial<TuiDialogOptions<any>> = {
label: 'Install Success',
closeable: false,
dismissible: false,
size: 's',
data: { button: 'Reboot' },
}
export function toWarning(
disk: DiskInfo | null,
): Partial<TuiDialogOptions<TuiPromptData>> {
return {
label: 'Warning',
size: 's',
data: {
content: `This action will COMPLETELY erase the disk ${
disk?.vendor || 'Unknown Vendor'
} - ${disk?.model || 'Unknown Model'} and install StartOS in its place`,
yes: 'Continue',
no: 'Cancel',
},
}
}

View File

@@ -1,32 +0,0 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { HomePage } from './home.page'
import { SwiperModule } from 'swiper/angular'
import {
UnitConversionPipesModule,
GuidPipePipesModule,
} from '@start9labs/shared'
const routes: Routes = [
{
path: '',
component: HomePage,
},
]
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
SwiperModule,
UnitConversionPipesModule,
GuidPipePipesModule,
],
declarations: [HomePage],
})
export class HomePageModule {}

View File

@@ -1,107 +0,0 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding: 64px 0 32px 0">
<img src="assets/img/icon.png" style="max-width: 100px" />
</div>
<ion-card color="dark">
<ion-card-header>
<ion-button
*ngIf="swiper?.activeIndex === 1"
class="back-button"
fill="clear"
color="light"
(click)="previous()"
>
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
</ion-button>
<ion-card-title>
{{ !swiper || swiper.activeIndex === 0 ? 'Select Disk' : 'Install
Type' }}
</ion-card-title>
<ion-card-subtitle *ngIf="error">
<ion-text color="danger">{{ error }}</ion-text>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin-bottom">
<swiper (swiper)="setSwiperInstance($event)">
<!-- SLIDE 1 -->
<ng-template swiperSlide>
<ion-item
*ngFor="let disk of disks"
button
(click)="next(disk)"
>
<ion-icon
slot="start"
name="save-outline"
size="large"
color="dark"
></ion-icon>
<ion-label class="ion-text-wrap">
<h1>
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model ||
'Unknown Model' }}
</h1>
<h2>
{{ disk.logicalname }} - {{ disk.capacity | convertBytes
}}
</h2>
</ion-label>
</ion-item>
</ng-template>
<!-- SLIDE 2 -->
<ng-template swiperSlide>
<ng-container *ngIf="selectedDisk">
<!-- re-install -->
<ion-item
*ngIf="selectedDisk | guid"
button
(click)="tryInstall(false)"
>
<ion-icon
color="dark"
slot="start"
size="large"
name="medkit-outline"
></ion-icon>
<ion-label>
<h1>
<ion-text color="success">Re-Install StartOS</ion-text>
</h1>
<h2>Will preserve existing StartOS data</h2>
</ion-label>
</ion-item>
<!-- fresh install -->
<ion-item button lines="none" (click)="tryInstall(true)">
<ion-icon
color="dark"
slot="start"
size="large"
name="download-outline"
></ion-icon>
<ion-label>
<h1>
<ion-text
[color]="(selectedDisk | guid) ? 'danger' : 'success'"
>
{{ (selectedDisk | guid) ? 'Factory Reset' : 'Install
StartOS' }}
</ion-text>
</h1>
<h2>Will delete existing data on disk</h2>
</ion-label>
</ion-item>
</ng-container>
</ng-template>
</swiper>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,28 +0,0 @@
/** Ionic CSS Variables overrides **/
:root {
--ion-font-family: 'Benton Sans', sans-serif;
}
ion-content {
--background: var(--ion-color-medium);
}
ion-grid {
padding-top: 32px;
height: 100%;
max-width: 640px;
}
.back-button {
position: absolute;
left: 16px;
top: 24px;
z-index: 1000000;
}
ion-card-title {
margin: 16px 0;
font-family: 'Montserrat';
font-size: x-large;
--color: var(--ion-color-light);
}

View File

@@ -1,137 +0,0 @@
import { Component } from '@angular/core'
import { IonicSlides } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import SwiperCore, { Swiper } from 'swiper'
import { DiskInfo, LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { filter } from 'rxjs'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
swiper?: Swiper
disks: DiskInfo[] = []
selectedDisk?: DiskInfo
error = ''
constructor(
private readonly loader: LoadingService,
private readonly api: ApiService,
private readonly dialogs: TuiDialogService,
) {}
async ngOnInit() {
this.disks = await this.api.getDisks()
}
async ionViewDidEnter() {
if (this.swiper) {
this.swiper.allowTouchMove = false
}
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
next(disk: DiskInfo) {
this.selectedDisk = disk
this.swiper?.slideNext(500)
}
previous() {
this.swiper?.slidePrev(500)
}
async tryInstall(overwrite: boolean) {
if (overwrite) {
return this.presentAlertDanger()
}
this.install(false)
}
private async install(overwrite: boolean) {
const loader = this.loader.open('Installing StartOS...').subscribe()
try {
await this.api.install({
logicalname: this.selectedDisk!.logicalname,
overwrite,
})
this.presentAlertReboot()
} catch (e: any) {
this.error = e.message
} finally {
loader.unsubscribe()
}
}
private presentAlertDanger() {
const { vendor, model } = this.selectedDisk!
this.dialogs
.open(TUI_PROMPT, {
label: 'Warning',
size: 's',
data: {
content: `This action will COMPLETELY erase the disk ${
vendor || 'Unknown Vendor'
} - ${model || 'Unknown Model'} and install StartOS in its place`,
yes: 'Continue',
no: 'Cancel',
},
})
.pipe(filter(Boolean))
.subscribe(() => {
this.install(true)
})
}
private async presentAlertReboot() {
this.dialogs
.open(
'Remove the USB stick and reboot your device to begin using your new Start9 server',
{
label: 'Install Success',
closeable: false,
dismissible: false,
size: 's',
data: { button: 'Reboot' },
},
)
.subscribe({
complete: () => {
this.reboot()
},
})
}
private async reboot() {
const loader = this.loader.open('').subscribe()
try {
await this.api.reboot()
this.presentAlertComplete()
} catch (e: any) {
this.error = e.message
} finally {
loader.unsubscribe()
}
}
private presentAlertComplete() {
this.dialogs
.open('Please wait for StartOS to restart, then refresh this page', {
label: 'Rebooting',
size: 's',
})
.subscribe()
}
}

View File

@@ -1,59 +0,0 @@
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: normal;
src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf');
}
/** Ionic CSS Variables overrides **/
:root {
--ion-font-family: 'Montserrat', sans-serif;
--ion-color-primary: #0075e1;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
--ion-item-background: #2b2b2b;
--ion-toolbar-background: #2b2b2b;
--ion-card-background: #2b2b2b;
--ion-background-color: #282828;
--ion-background-color-rgb: 30,30,30;
--ion-text-color: var(--ion-color-dark);
--ion-text-color-rgb: var(--ion-color-dark-rgb);
}
.loader {
--spinner-color: var(--ion-color-warning) !important;
z-index: 40000 !important;
}
.alert-danger-message {
.alert-title {
color: var(--ion-color-danger);
}
}
.alert-success-message {
.alert-title {
color: var(--ion-color-success);
}
}
ion-alert {
.alert-button {
color: var(--ion-color-dark) !important;
}
}