From 7b41b295b767ad0b5cc9b411fee223a0d76a60e7 Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Thu, 22 Feb 2024 17:58:01 +0400 Subject: [PATCH] chore: refactor install and setup wizards (#2561) * chore: refactor install and setup wizards * chore: return tui-root --- web/angular.json | 11 +- web/package-lock.json | 26 +- web/package.json | 2 +- .../src/app/app-routing.module.ts | 22 -- .../install-wizard/src/app/app.component.html | 62 ++- .../install-wizard/src/app/app.component.scss | 63 ++- .../install-wizard/src/app/app.component.ts | 70 +++- .../install-wizard/src/app/app.module.ts | 45 ++- .../install-wizard/src/app/app.utils.ts | 27 ++ .../src/app/pages/home/home.module.ts | 32 -- .../src/app/pages/home/home.page.html | 107 ----- .../src/app/pages/home/home.page.scss | 28 -- .../src/app/pages/home/home.page.ts | 137 ------- .../src/app/services/{api => }/api.service.ts | 0 .../services/{api => }/live-api.service.ts | 0 .../services/{api => }/mock-api.service.ts | 0 web/projects/install-wizard/src/styles.scss | 59 --- .../src/pages/list/search/search.module.ts | 11 +- .../show/additional/additional.module.ts | 16 +- .../show/dependencies/dependencies.module.ts | 10 +- .../src/app/app-routing.module.ts | 56 --- .../setup-wizard/src/app/app.component.html | 6 - .../setup-wizard/src/app/app.component.scss | 8 - .../setup-wizard/src/app/app.component.ts | 29 +- .../setup-wizard/src/app/app.module.ts | 57 +-- .../setup-wizard/src/app/app.routes.ts | 33 ++ .../src/app/components/cifs.component.ts | 195 +++++++++ .../app/components/documentation.component.ts | 145 +++++++ .../src/app/components/matrix.component.ts | 79 ++++ .../src/app/components/password.component.ts | 128 ++++++ .../src/app/components/recover.component.ts | 44 +++ .../modals/cifs-modal/cifs-modal.module.ts | 26 -- .../modals/cifs-modal/cifs-modal.page.html | 39 -- .../modals/cifs-modal/cifs-modal.page.scss | 3 - .../app/modals/cifs-modal/cifs-modal.page.ts | 117 ------ .../app/modals/password/password.module.ts | 20 - .../app/modals/password/password.page.html | 35 -- .../src/app/modals/password/password.page.ts | 77 ---- .../setup-wizard/src/app/pages/attach.page.ts | 108 +++++ .../app/pages/attach/attach-routing.module.ts | 16 - .../src/app/pages/attach/attach.module.ts | 21 - .../src/app/pages/attach/attach.page.html | 67 ---- .../src/app/pages/attach/attach.page.scss | 0 .../src/app/pages/attach/attach.page.ts | 71 ---- .../pages/embassy/embassy-routing.module.ts | 16 - .../src/app/pages/embassy/embassy.module.ts | 25 -- .../src/app/pages/embassy/embassy.page.html | 87 ---- .../src/app/pages/embassy/embassy.page.scss | 0 .../setup-wizard/src/app/pages/home.page.ts | 144 +++++++ .../src/app/pages/home/home-routing.module.ts | 16 - .../src/app/pages/home/home.module.ts | 21 - .../src/app/pages/home/home.page.html | 129 ------ .../src/app/pages/home/home.page.scss | 13 - .../src/app/pages/home/home.page.ts | 53 --- .../src/app/pages/loading.page.ts | 20 + .../src/app/pages/loading/loading.module.ts | 17 - .../src/app/pages/loading/loading.page.html | 5 - .../src/app/pages/loading/loading.page.ts | 13 - .../src/app/pages/recover.page.ts | 163 ++++++++ .../pages/recover/drive-status.component.html | 14 - .../pages/recover/recover-routing.module.ts | 16 - .../src/app/pages/recover/recover.module.ts | 23 -- .../src/app/pages/recover/recover.page.html | 97 ----- .../src/app/pages/recover/recover.page.scss | 5 - .../src/app/pages/recover/recover.page.ts | 137 ------- .../embassy.page.ts => storage.page.ts} | 104 +++-- .../src/app/pages/success.page.ts | 175 ++++++++ .../download-doc/download-doc.component.html | 129 ------ .../download-doc/download-doc.component.ts | 14 - .../pages/success/success-routing.module.ts | 16 - .../src/app/pages/success/success.module.ts | 24 -- .../src/app/pages/success/success.page.html | 102 ----- .../src/app/pages/success/success.page.scss | 183 --------- .../src/app/pages/success/success.page.ts | 143 ------- .../src/app/pages/transfer.page.ts | 105 +++++ .../pages/transfer/transfer-routing.module.ts | 16 - .../src/app/pages/transfer/transfer.module.ts | 21 - .../src/app/pages/transfer/transfer.page.html | 61 --- .../src/app/pages/transfer/transfer.page.scss | 0 .../src/app/pages/transfer/transfer.page.ts | 68 ---- .../src/app/services/{api => }/api.service.ts | 0 .../services/{api => }/live-api.service.ts | 6 +- .../services/{api => }/mock-api.service.ts | 2 +- .../src/app/services/state.service.ts | 9 +- web/projects/setup-wizard/src/styles.scss | 372 +++--------------- web/projects/shared/package.json | 2 +- .../shared/src/components/drive.component.ts | 29 ++ .../initializing/initializing.component.html | 15 - .../initializing/initializing.component.scss | 33 -- .../initializing/initializing.component.ts | 62 ++- .../initializing/initializing.module.ts | 14 - .../text-spinner/text-spinner.component.html | 8 - .../text-spinner.component.module.ts | 11 - .../text-spinner/text-spinner.component.scss | 3 - .../text-spinner/text-spinner.component.ts | 10 - .../directives/responsive-col.directive.ts | 121 ------ .../shared/src/pipes/guid/guid.module.ts | 8 - .../shared/src/pipes/guid/guid.pipe.ts | 11 - web/projects/shared/src/public-api.ts | 8 +- web/projects/shared/src/util/to-guid.ts | 5 + web/projects/ui/src/app/app.component.html | 3 - web/projects/ui/src/app/app.module.ts | 2 - .../ui/src/app/apps/loading/loading.page.ts | 4 +- .../components/header/snek.component.ts | 6 +- .../src/app/common/logs/logs.component.html | 94 ----- .../app/common/logs/logs.component.module.ts | 13 - .../src/app/common/logs/logs.component.scss | 5 - .../ui/src/app/common/logs/logs.component.ts | 259 ------------ .../widget-list.component.module.ts | 3 +- 109 files changed, 1863 insertions(+), 3538 deletions(-) delete mode 100644 web/projects/install-wizard/src/app/app-routing.module.ts create mode 100644 web/projects/install-wizard/src/app/app.utils.ts delete mode 100644 web/projects/install-wizard/src/app/pages/home/home.module.ts delete mode 100644 web/projects/install-wizard/src/app/pages/home/home.page.html delete mode 100644 web/projects/install-wizard/src/app/pages/home/home.page.scss delete mode 100644 web/projects/install-wizard/src/app/pages/home/home.page.ts rename web/projects/install-wizard/src/app/services/{api => }/api.service.ts (100%) rename web/projects/install-wizard/src/app/services/{api => }/live-api.service.ts (100%) rename web/projects/install-wizard/src/app/services/{api => }/mock-api.service.ts (100%) delete mode 100644 web/projects/install-wizard/src/styles.scss delete mode 100644 web/projects/setup-wizard/src/app/app-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/app.component.html delete mode 100644 web/projects/setup-wizard/src/app/app.component.scss create mode 100644 web/projects/setup-wizard/src/app/app.routes.ts create mode 100644 web/projects/setup-wizard/src/app/components/cifs.component.ts create mode 100644 web/projects/setup-wizard/src/app/components/documentation.component.ts create mode 100644 web/projects/setup-wizard/src/app/components/matrix.component.ts create mode 100644 web/projects/setup-wizard/src/app/components/password.component.ts create mode 100644 web/projects/setup-wizard/src/app/components/recover.component.ts delete mode 100644 web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts delete mode 100644 web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html delete mode 100644 web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss delete mode 100644 web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts delete mode 100644 web/projects/setup-wizard/src/app/modals/password/password.module.ts delete mode 100644 web/projects/setup-wizard/src/app/modals/password/password.page.html delete mode 100644 web/projects/setup-wizard/src/app/modals/password/password.page.ts create mode 100644 web/projects/setup-wizard/src/app/pages/attach.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/attach/attach-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/attach/attach.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/attach/attach.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/attach/attach.page.scss delete mode 100644 web/projects/setup-wizard/src/app/pages/attach/attach.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/embassy/embassy-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/embassy/embassy.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/embassy/embassy.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/embassy/embassy.page.scss create mode 100644 web/projects/setup-wizard/src/app/pages/home.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/home/home-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/home/home.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/home/home.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/home/home.page.scss delete mode 100644 web/projects/setup-wizard/src/app/pages/home/home.page.ts create mode 100644 web/projects/setup-wizard/src/app/pages/loading.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/loading/loading.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/loading/loading.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/loading/loading.page.ts create mode 100644 web/projects/setup-wizard/src/app/pages/recover.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/recover-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/recover.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/recover.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/recover.page.scss delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/recover.page.ts rename web/projects/setup-wizard/src/app/pages/{embassy/embassy.page.ts => storage.page.ts} (59%) create mode 100644 web/projects/setup-wizard/src/app/pages/success.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html delete mode 100644 web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/success/success-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/success/success.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/success/success.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/success/success.page.scss delete mode 100644 web/projects/setup-wizard/src/app/pages/success/success.page.ts create mode 100644 web/projects/setup-wizard/src/app/pages/transfer.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/transfer/transfer.page.html delete mode 100644 web/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss delete mode 100644 web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts rename web/projects/setup-wizard/src/app/services/{api => }/api.service.ts (100%) rename web/projects/setup-wizard/src/app/services/{api => }/live-api.service.ts (96%) rename web/projects/setup-wizard/src/app/services/{api => }/mock-api.service.ts (99%) create mode 100644 web/projects/shared/src/components/drive.component.ts delete mode 100644 web/projects/shared/src/components/initializing/initializing.component.html delete mode 100644 web/projects/shared/src/components/initializing/initializing.component.scss delete mode 100644 web/projects/shared/src/components/initializing/initializing.module.ts delete mode 100644 web/projects/shared/src/components/text-spinner/text-spinner.component.html delete mode 100644 web/projects/shared/src/components/text-spinner/text-spinner.component.module.ts delete mode 100644 web/projects/shared/src/components/text-spinner/text-spinner.component.scss delete mode 100644 web/projects/shared/src/components/text-spinner/text-spinner.component.ts delete mode 100644 web/projects/shared/src/directives/responsive-col.directive.ts delete mode 100644 web/projects/shared/src/pipes/guid/guid.module.ts delete mode 100644 web/projects/shared/src/pipes/guid/guid.pipe.ts create mode 100644 web/projects/shared/src/util/to-guid.ts delete mode 100644 web/projects/ui/src/app/common/logs/logs.component.html delete mode 100644 web/projects/ui/src/app/common/logs/logs.component.module.ts delete mode 100644 web/projects/ui/src/app/common/logs/logs.component.scss delete mode 100644 web/projects/ui/src/app/common/logs/logs.component.ts diff --git a/web/angular.json b/web/angular.json index 963d90bb7..9e99210cd 100644 --- a/web/angular.json +++ b/web/angular.json @@ -184,12 +184,7 @@ } ], "styles": [ - "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", - "projects/shared/styles/taiga.scss", - "projects/shared/styles/variables.scss", - "projects/shared/styles/global.scss", - "projects/shared/styles/shared.scss", - "projects/install-wizard/src/styles.scss" + "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less" ], "scripts": [] }, @@ -322,10 +317,6 @@ ], "styles": [ "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", - "projects/shared/styles/taiga.scss", - "projects/shared/styles/variables.scss", - "projects/shared/styles/global.scss", - "projects/shared/styles/shared.scss", "projects/setup-wizard/src/styles.scss" ], "scripts": [] diff --git a/web/package-lock.json b/web/package-lock.json index 0a28b4f8f..9537c894e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -24,15 +24,15 @@ "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2", - "@taiga-ui/addon-charts": "^3.65.0", - "@taiga-ui/addon-commerce": "^3.65.0", - "@taiga-ui/addon-mobile": "^3.65.0", - "@taiga-ui/cdk": "^3.65.0", - "@taiga-ui/core": "^3.65.0", - "@taiga-ui/experimental": "^3.65.0", - "@taiga-ui/icons": "^3.65.0", - "@taiga-ui/kit": "^3.65.0", - "@taiga-ui/styles": "^3.65.0", + "@taiga-ui/addon-charts": "3.65.0", + "@taiga-ui/addon-commerce": "3.65.0", + "@taiga-ui/addon-mobile": "3.65.0", + "@taiga-ui/cdk": "3.65.0", + "@taiga-ui/core": "3.65.0", + "@taiga-ui/experimental": "3.65.0", + "@taiga-ui/icons": "3.65.0", + "@taiga-ui/kit": "3.65.0", + "@taiga-ui/styles": "3.65.0", "@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-event-plugins": "3.1.0", "ansi-to-html": "^0.7.2", @@ -83,7 +83,7 @@ "ng-packagr": "^17.0.2", "node-html-parser": "^5.3.3", "postcss": "^8.4.21", - "prettier": "^3.1.1", + "prettier": "^3.2.5", "raw-loader": "^4.0.2", "ts-node": "^10.7.0", "tslint": "^6.1.3", @@ -14107,9 +14107,9 @@ } }, "node_modules/prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/web/package.json b/web/package.json index a6498057f..e80feeea9 100644 --- a/web/package.json +++ b/web/package.json @@ -105,7 +105,7 @@ "ng-packagr": "^17.0.2", "node-html-parser": "^5.3.3", "postcss": "^8.4.21", - "prettier": "^3.1.1", + "prettier": "^3.2.5", "raw-loader": "^4.0.2", "ts-node": "^10.7.0", "tslint": "^6.1.3", diff --git a/web/projects/install-wizard/src/app/app-routing.module.ts b/web/projects/install-wizard/src/app/app-routing.module.ts deleted file mode 100644 index 80901192f..000000000 --- a/web/projects/install-wizard/src/app/app-routing.module.ts +++ /dev/null @@ -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 {} diff --git a/web/projects/install-wizard/src/app/app.component.html b/web/projects/install-wizard/src/app/app.component.html index 2d86be205..be4da9ab2 100644 --- a/web/projects/install-wizard/src/app/app.component.html +++ b/web/projects/install-wizard/src/app/app.component.html @@ -1,6 +1,58 @@ - - - - - + +
+ +
+
+ @if (selected) { + + } +

{{ selected ? 'Install Type' : 'Select Disk' }}

+
{{ error }}
+
+
+
+ @for (drive of disks$ | async; track $index) { + + } +
+
+ @if (guid) { + + } + + +
+
+
+
diff --git a/web/projects/install-wizard/src/app/app.component.scss b/web/projects/install-wizard/src/app/app.component.scss index b528fd9bd..d7d44fd08 100644 --- a/web/projects/install-wizard/src/app/app.component.scss +++ b/web/projects/install-wizard/src/app/app.component.scss @@ -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%; + } } diff --git a/web/projects/install-wizard/src/app/app.component.ts b/web/projects/install-wizard/src/app/app.component.ts index 5ac82a652..a42a75f31 100644 --- a/web/projects/install-wizard/src/app/app.component.ts +++ b/web/projects/install-wizard/src/app/app.component.ts @@ -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() + } + }, + }) + } } diff --git a/web/projects/install-wizard/src/app/app.module.ts b/web/projects/install-wizard/src/app/app.module.ts index 9cc91ba3f..3164d5d99 100644 --- a/web/projects/install-wizard/src/app/app.module.ts +++ b/web/projects/install-wizard/src/app/app.module.ts @@ -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, diff --git a/web/projects/install-wizard/src/app/app.utils.ts b/web/projects/install-wizard/src/app/app.utils.ts new file mode 100644 index 000000000..cf6735c2d --- /dev/null +++ b/web/projects/install-wizard/src/app/app.utils.ts @@ -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> = { + label: 'Install Success', + closeable: false, + dismissible: false, + size: 's', + data: { button: 'Reboot' }, +} + +export function toWarning( + disk: DiskInfo | null, +): Partial> { + 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', + }, + } +} diff --git a/web/projects/install-wizard/src/app/pages/home/home.module.ts b/web/projects/install-wizard/src/app/pages/home/home.module.ts deleted file mode 100644 index e7cd274f6..000000000 --- a/web/projects/install-wizard/src/app/pages/home/home.module.ts +++ /dev/null @@ -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 {} diff --git a/web/projects/install-wizard/src/app/pages/home/home.page.html b/web/projects/install-wizard/src/app/pages/home/home.page.html deleted file mode 100644 index 5bdaee493..000000000 --- a/web/projects/install-wizard/src/app/pages/home/home.page.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - -
- -
- - - - - - - - {{ !swiper || swiper.activeIndex === 0 ? 'Select Disk' : 'Install - Type' }} - - - {{ error }} - - - - - - - - - -

- {{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || - 'Unknown Model' }} -

-

- {{ disk.logicalname }} - {{ disk.capacity | convertBytes - }} -

-
-
-
- - - - - - - - -

- Re-Install StartOS -

-

Will preserve existing StartOS data

-
-
- - - - - -

- - {{ (selectedDisk | guid) ? 'Factory Reset' : 'Install - StartOS' }} - -

-

Will delete existing data on disk

-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/projects/install-wizard/src/app/pages/home/home.page.scss b/web/projects/install-wizard/src/app/pages/home/home.page.scss deleted file mode 100644 index bc9c9b4a0..000000000 --- a/web/projects/install-wizard/src/app/pages/home/home.page.scss +++ /dev/null @@ -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); -} diff --git a/web/projects/install-wizard/src/app/pages/home/home.page.ts b/web/projects/install-wizard/src/app/pages/home/home.page.ts deleted file mode 100644 index b2bf633ee..000000000 --- a/web/projects/install-wizard/src/app/pages/home/home.page.ts +++ /dev/null @@ -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() - } -} diff --git a/web/projects/install-wizard/src/app/services/api/api.service.ts b/web/projects/install-wizard/src/app/services/api.service.ts similarity index 100% rename from web/projects/install-wizard/src/app/services/api/api.service.ts rename to web/projects/install-wizard/src/app/services/api.service.ts diff --git a/web/projects/install-wizard/src/app/services/api/live-api.service.ts b/web/projects/install-wizard/src/app/services/live-api.service.ts similarity index 100% rename from web/projects/install-wizard/src/app/services/api/live-api.service.ts rename to web/projects/install-wizard/src/app/services/live-api.service.ts diff --git a/web/projects/install-wizard/src/app/services/api/mock-api.service.ts b/web/projects/install-wizard/src/app/services/mock-api.service.ts similarity index 100% rename from web/projects/install-wizard/src/app/services/api/mock-api.service.ts rename to web/projects/install-wizard/src/app/services/mock-api.service.ts diff --git a/web/projects/install-wizard/src/styles.scss b/web/projects/install-wizard/src/styles.scss deleted file mode 100644 index 540205695..000000000 --- a/web/projects/install-wizard/src/styles.scss +++ /dev/null @@ -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; - } -} diff --git a/web/projects/marketplace/src/pages/list/search/search.module.ts b/web/projects/marketplace/src/pages/list/search/search.module.ts index 2da2a8e50..ea60b4fb2 100644 --- a/web/projects/marketplace/src/pages/list/search/search.module.ts +++ b/web/projects/marketplace/src/pages/list/search/search.module.ts @@ -1,11 +1,10 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { ResponsiveColDirective } from "@start9labs/shared"; -import { SearchComponent } from "./search.component"; +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { SearchComponent } from './search.component' @NgModule({ - imports: [FormsModule, CommonModule, ResponsiveColDirective], + imports: [FormsModule, CommonModule], declarations: [SearchComponent], exports: [SearchComponent], }) diff --git a/web/projects/marketplace/src/pages/show/additional/additional.module.ts b/web/projects/marketplace/src/pages/show/additional/additional.module.ts index 13e353291..e84ac26b7 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.module.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional.module.ts @@ -1,19 +1,17 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; -import { AdditionalComponent } from "./additional.component"; +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { AdditionalComponent } from './additional.component' import { TuiRadioListModule, TuiStringifyContentPipeModule, -} from "@taiga-ui/kit"; -import { FormsModule } from "@angular/forms"; -import { TuiButtonModule, TuiLabelModule } from "@taiga-ui/core"; -import { AdditionalLinkModule } from "./additional-link/additional-link.component.module"; -import { ResponsiveColDirective } from "@start9labs/shared"; +} from '@taiga-ui/kit' +import { FormsModule } from '@angular/forms' +import { TuiButtonModule, TuiLabelModule } from '@taiga-ui/core' +import { AdditionalLinkModule } from './additional-link/additional-link.component.module' @NgModule({ imports: [ CommonModule, - ResponsiveColDirective, TuiRadioListModule, FormsModule, TuiStringifyContentPipeModule, diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts index 168fe663f..01cadddf3 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts @@ -1,17 +1,11 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' -import { EmverPipesModule, ResponsiveColDirective } from '@start9labs/shared' +import { EmverPipesModule } from '@start9labs/shared' import { DependenciesComponent } from './dependencies.component' import { TuiAvatarModule } from '@taiga-ui/experimental' @NgModule({ - imports: [ - CommonModule, - RouterModule, - ResponsiveColDirective, - TuiAvatarModule, - EmverPipesModule, - ], + imports: [CommonModule, RouterModule, TuiAvatarModule, EmverPipesModule], declarations: [DependenciesComponent], exports: [DependenciesComponent], }) diff --git a/web/projects/setup-wizard/src/app/app-routing.module.ts b/web/projects/setup-wizard/src/app/app-routing.module.ts deleted file mode 100644 index aa56c382d..000000000 --- a/web/projects/setup-wizard/src/app/app-routing.module.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NgModule } from '@angular/core' -import { PreloadAllModules, RouterModule, Routes } from '@angular/router' - -const routes: Routes = [ - { path: '', redirectTo: '/home', pathMatch: 'full' }, - { - path: 'home', - loadChildren: () => - import('./pages/home/home.module').then(m => m.HomePageModule), - }, - { - path: 'attach', - loadChildren: () => - import('./pages/attach/attach.module').then(m => m.AttachPageModule), - }, - { - path: 'recover', - loadChildren: () => - import('./pages/recover/recover.module').then(m => m.RecoverPageModule), - }, - { - path: 'transfer', - loadChildren: () => - import('./pages/transfer/transfer.module').then( - m => m.TransferPageModule, - ), - }, - { - path: 'storage', - loadChildren: () => - import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule), - }, - { - path: 'loading', - loadChildren: () => - import('./pages/loading/loading.module').then(m => m.LoadingPageModule), - }, - { - path: 'success', - loadChildren: () => - import('./pages/success/success.module').then(m => m.SuccessPageModule), - }, -] - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - scrollPositionRestoration: 'enabled', - preloadingStrategy: PreloadAllModules, - useHash: true, - initialNavigation: 'disabled', - }), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/web/projects/setup-wizard/src/app/app.component.html b/web/projects/setup-wizard/src/app/app.component.html deleted file mode 100644 index 2d86be205..000000000 --- a/web/projects/setup-wizard/src/app/app.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/web/projects/setup-wizard/src/app/app.component.scss b/web/projects/setup-wizard/src/app/app.component.scss deleted file mode 100644 index b528fd9bd..000000000 --- a/web/projects/setup-wizard/src/app/app.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: block; - height: 100%; -} - -tui-root { - height: 100%; -} diff --git a/web/projects/setup-wizard/src/app/app.component.ts b/web/projects/setup-wizard/src/app/app.component.ts index aee925f41..3f88d6f89 100644 --- a/web/projects/setup-wizard/src/app/app.component.ts +++ b/web/projects/setup-wizard/src/app/app.component.ts @@ -1,30 +1,31 @@ -import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' -import { ApiService } from './services/api/api.service' +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' import { ErrorService } from '@start9labs/shared' +import { ApiService } from 'src/app/services/api.service' @Component({ selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], + template: ` + + + `, }) export class AppComponent { - constructor( - private readonly apiService: ApiService, - private readonly errorService: ErrorService, - private readonly navCtrl: NavController, - ) {} + private readonly api = inject(ApiService) + private readonly errorService = inject(ErrorService) + private readonly router = inject(Router) async ngOnInit() { try { - const inProgress = await this.apiService.getSetupStatus() + const inProgress = await this.api.getSetupStatus() + + let route = 'home' - let route = '/home' if (inProgress) { - route = inProgress.complete ? '/success' : '/loading' + route = inProgress.complete ? 'success' : 'loading' } - await this.navCtrl.navigateForward(route) + await this.router.navigate([route]) } catch (e: any) { this.errorService.handleError(e) } diff --git a/web/projects/setup-wizard/src/app/app.module.ts b/web/projects/setup-wizard/src/app/app.module.ts index b346a135c..fbe56a356 100644 --- a/web/projects/setup-wizard/src/app/app.module.ts +++ b/web/projects/setup-wizard/src/app/app.module.ts @@ -1,30 +1,7 @@ +import { HttpClientModule } from '@angular/common/http' import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RouteReuseStrategy } from '@angular/router' -import { HttpClientModule } from '@angular/common/http' -import { - TuiAlertModule, - tuiButtonOptionsProvider, - TuiDialogModule, - TuiModeModule, - TuiRootModule, - TuiThemeNightModule, -} from '@taiga-ui/core' -import { ApiService } from './services/api/api.service' -import { MockApiService } from './services/api/mock-api.service' -import { LiveApiService } from './services/api/live-api.service' -import { - IonicModule, - IonicRouteStrategy, - iosTransitionAnimation, -} from '@ionic/angular' -import { AppComponent } from './app.component' -import { AppRoutingModule } from './app-routing.module' -import { SuccessPageModule } from './pages/success/success.module' -import { HomePageModule } from './pages/home/home.module' -import { LoadingPageModule } from './pages/loading/loading.module' -import { RecoverPageModule } from './pages/recover/recover.module' -import { TransferPageModule } from './pages/transfer/transfer.module' +import { PreloadAllModules, RouterModule } from '@angular/router' import { LoadingModule, provideSetupLogsService, @@ -32,6 +9,19 @@ import { RELATIVE_URL, WorkspaceConfig, } from '@start9labs/shared' +import { + TuiAlertModule, + TuiDialogModule, + TuiModeModule, + TuiRootModule, + TuiThemeNightModule, +} from '@taiga-ui/core' +import { tuiButtonOptionsProvider } 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' +import { ROUTES } from './app.routes' const { useMocks, @@ -42,21 +32,15 @@ const { declarations: [AppComponent], imports: [ BrowserAnimationsModule, - IonicModule.forRoot({ - mode: 'md', - navAnimation: iosTransitionAnimation, - }), - AppRoutingModule, HttpClientModule, - SuccessPageModule, - HomePageModule, - LoadingPageModule, - RecoverPageModule, - TransferPageModule, + RouterModule.forRoot(ROUTES, { + preloadingStrategy: PreloadAllModules, + initialNavigation: 'disabled', + }), + LoadingModule, TuiRootModule, TuiDialogModule, TuiAlertModule, - LoadingModule, TuiModeModule, TuiThemeNightModule, ], @@ -64,7 +48,6 @@ const { provideSetupService(ApiService), provideSetupLogsService(ApiService), tuiButtonOptionsProvider({ size: 'm' }), - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: ApiService, useClass: useMocks ? MockApiService : LiveApiService, diff --git a/web/projects/setup-wizard/src/app/app.routes.ts b/web/projects/setup-wizard/src/app/app.routes.ts new file mode 100644 index 000000000..388ebc740 --- /dev/null +++ b/web/projects/setup-wizard/src/app/app.routes.ts @@ -0,0 +1,33 @@ +import { Routes } from '@angular/router' + +export const ROUTES: Routes = [ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { + path: 'home', + loadComponent: () => import('src/app/pages/home.page'), + }, + { + path: 'attach', + loadComponent: () => import('src/app/pages/attach.page'), + }, + { + path: 'recover', + loadComponent: () => import('src/app/pages/recover.page'), + }, + { + path: 'transfer', + loadComponent: () => import('src/app/pages/transfer.page'), + }, + { + path: 'storage', + loadComponent: () => import('src/app/pages/storage.page'), + }, + { + path: 'loading', + loadComponent: () => import('src/app/pages/loading.page'), + }, + { + path: 'success', + loadComponent: () => import('src/app/pages/success.page'), + }, +] diff --git a/web/projects/setup-wizard/src/app/components/cifs.component.ts b/web/projects/setup-wizard/src/app/components/cifs.component.ts new file mode 100644 index 000000000..00fa27a5b --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/cifs.component.ts @@ -0,0 +1,195 @@ +import { CommonModule } from '@angular/common' +import { Component, inject, Inject } from '@angular/core' +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms' +import { LoadingService, StartOSDiskInfo } from '@start9labs/shared' +import { + TuiButtonModule, + TuiDialogContext, + TuiDialogService, + TuiErrorModule, +} from '@taiga-ui/core' +import { + TUI_VALIDATION_ERRORS, + TuiFieldErrorPipeModule, + TuiInputModule, + TuiInputPasswordModule, +} from '@taiga-ui/kit' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { PASSWORD } from 'src/app/components/password.component' +import { + ApiService, + CifsBackupTarget, + CifsRecoverySource, +} from 'src/app/services/api.service' + +interface Context { + cifs: CifsRecoverySource + recoveryPassword: string +} + +@Component({ + standalone: true, + template: ` +
+ + Hostname + + + + + + Path + + + + + + Username + + + + + + Password + + +
+ + +
+
+ `, + styles: [ + '.input { margin-top: 1rem }', + 'footer { display: flex; gap: 1rem; margin-top: 1rem }', + ], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TuiButtonModule, + TuiInputModule, + TuiInputPasswordModule, + TuiErrorModule, + TuiFieldErrorPipeModule, + ], + providers: [ + { + provide: TUI_VALIDATION_ERRORS, + useValue: { + required: 'This field is required', + }, + }, + ], +}) +export class CifsComponent { + private readonly dialogs = inject(TuiDialogService) + private readonly api = inject(ApiService) + private readonly loader = inject(LoadingService) + private readonly context = + inject>(POLYMORPHEUS_CONTEXT) + + readonly form = new FormGroup({ + hostname: new FormControl('', { + validators: [ + Validators.required, + Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/), + ], + nonNullable: true, + }), + path: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + username: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + password: new FormControl(), + }) + + cancel() { + this.context.$implicit.complete() + } + + async submit(): Promise { + const loader = this.loader + .open('Connecting to shared folder...') + .subscribe() + + 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, + }) + + loader.unsubscribe() + + this.presentModalPassword(diskInfo) + } catch (e) { + loader.unsubscribe() + this.presentAlertFailed() + } + } + + private presentModalPassword(diskInfo: StartOSDiskInfo) { + const target: CifsBackupTarget = { + ...this.form.getRawValue(), + mountable: true, + 'embassy-os': diskInfo, + } + + this.dialogs + .open(PASSWORD, { + label: 'Unlock Drive', + size: 's', + data: { target }, + }) + .subscribe(recoveryPassword => { + this.context.completeWith({ + cifs: { ...this.form.getRawValue(), type: 'cifs' }, + recoveryPassword, + }) + }) + } + + private presentAlertFailed() { + 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.', + { + label: 'Connection Failed', + size: 's', + }, + ) + .subscribe() + } +} diff --git a/web/projects/setup-wizard/src/app/components/documentation.component.ts b/web/projects/setup-wizard/src/app/components/documentation.component.ts new file mode 100644 index 000000000..43473c1b9 --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/documentation.component.ts @@ -0,0 +1,145 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-documentation', + template: ` + + + + StartOS Address Info + + +
+

+ StartOS Address Info +

+ +
+
+

Important!

+

+ Download your server's Root CA and + + follow the instructions + + to establish a secure connection with your server. +

+
+ +
+
+

+ Access from home (LAN) +

+

+ Visit the address below when you are connected to the same WiFi or + Local Area Network (LAN) as your server. +

+

+ +

+ +

+ Access on the go (Tor) +

+

Visit the address below when you are away from home.

+

+ Note: + This address will only work from a Tor-enabled browser. + + Follow the instructions + + to get setup. +

+

+ +

+
+
+ + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DocumentationComponent { + @Input({ required: true }) lanAddress!: string + + get crtName(): string { + return `${new URL(this.lanAddress).hostname}.crt` + } +} diff --git a/web/projects/setup-wizard/src/app/components/matrix.component.ts b/web/projects/setup-wizard/src/app/components/matrix.component.ts new file mode 100644 index 000000000..e2b04a6b3 --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/matrix.component.ts @@ -0,0 +1,79 @@ +import { + Component, + Directive, + ElementRef, + inject, + NgZone, + OnInit, +} from '@angular/core' +import { WINDOW } from '@ng-web-apis/common' + +// a higher fade factor will make the characters fade quicker +const FADE_FACTOR = 0.07 + +@Component({ + standalone: true, + selector: 'canvas[matrix]', + template: 'Your browser does not support the canvas element.', + styles: ':host { position: fixed; }', +}) +export class MatrixComponent implements OnInit { + private readonly ngZone = inject(NgZone) + private readonly window = inject(WINDOW) + private readonly el: HTMLCanvasElement = inject(ElementRef).nativeElement + private readonly ctx = this.el.getContext('2d')! + + private tileSize = 16 + private columns: any[] = [] + private maxStackHeight = 0 + + ngOnInit() { + this.ngZone.runOutsideAngular(() => { + this.setupMatrixGrid() + this.tick() + }) + } + + private setupMatrixGrid() { + this.el.width = Math.max(this.window.innerWidth, 1920) + this.el.height = Math.max(this.window.innerHeight, 1080) + this.maxStackHeight = Math.ceil(this.ctx.canvas.height / this.tileSize) + // divide the canvas into columns + for (let i = 0; i < this.ctx.canvas.width / this.tileSize; ++i) { + const column = {} as any + // save the x position of the column + column.x = i * this.tileSize + // create a random stack height for the column + column.stackHeight = 10 + Math.random() * this.maxStackHeight + // add a counter to count the stack height + column.stackCounter = 0 + // add the column to the list + this.columns.push(column) + } + } + + private draw() { + // draw a semi transparent black rectangle on top of the scene to slowly fade older characters + this.ctx.fillStyle = `rgba(0, 0, 0, ${FADE_FACTOR})` + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height) + // pick a font slightly smaller than the tile size + this.ctx.font = `${this.tileSize - 2}px monospace` + this.ctx.fillStyle = '#ff4961' + for (let i = 0; i < this.columns.length; ++i) { + // pick a random ascii character (change the 94 to a higher number to include more characters) + const { x, stackCounter } = this.columns[i] + const char = String.fromCharCode(33 + Math.floor(Math.random() * 94)) + this.ctx.fillText(char, x, stackCounter * this.tileSize + this.tileSize) + // if the stack is at its height limit, pick a new random height and reset the counter + if (++this.columns[i].stackCounter >= this.columns[i].stackHeight) { + this.columns[i].stackHeight = 10 + Math.random() * this.maxStackHeight + this.columns[i].stackCounter = 0 + } + } + } + + private tick() { + this.draw() + setTimeout(this.tick.bind(this), 50) + } +} diff --git a/web/projects/setup-wizard/src/app/components/password.component.ts b/web/projects/setup-wizard/src/app/components/password.component.ts new file mode 100644 index 000000000..2b41a4c0f --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/password.component.ts @@ -0,0 +1,128 @@ +import { Component, inject } from '@angular/core' +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms' +import * as argon2 from '@start9labs/argon2' +import { ErrorService } from '@start9labs/shared' +import { + TuiButtonModule, + TuiDialogContext, + TuiErrorModule, +} from '@taiga-ui/core' +import { TuiInputPasswordModule } from '@taiga-ui/kit' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusComponent, +} from '@tinkoff/ng-polymorpheus' +import { + CifsBackupTarget, + DiskBackupTarget, +} from 'src/app/services/api.service' + +interface DialogData { + target?: CifsBackupTarget | DiskBackupTarget + storageDrive?: boolean +} + +@Component({ + standalone: true, + template: ` + @if (storageDrive) { + Choose a password for your server. + Make it good. Write it down. + } @else { + Enter the password that was used to encrypt this drive. + } + +
+ + Enter Password + + + + @if (storageDrive) { + + Retype Password + + + + } +
+ + +
+
+ `, + styles: ['footer { display: flex; gap: 1rem; margin-top: 1rem }'], + imports: [ + FormsModule, + ReactiveFormsModule, + TuiButtonModule, + TuiInputPasswordModule, + TuiErrorModule, + ], +}) +export class PasswordComponent { + private readonly errorService = inject(ErrorService) + private readonly context = + inject>(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 + } + + get confirmError(): string | null { + return this.confirm.touched && this.password.value !== this.confirm.value + ? 'Passwords do not match' + : null + } + + submit() { + if (this.storageDrive) { + this.context.completeWith(this.password.value) + + return + } + + try { + const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' + + argon2.verify(passwordHash, this.password.value) + this.context.completeWith(this.password.value) + } catch (e) { + this.errorService.handleError('Incorrect password provided') + } + } + + cancel() { + this.context.$implicit.complete() + } +} + +export const PASSWORD = new PolymorpheusComponent(PasswordComponent) diff --git a/web/projects/setup-wizard/src/app/components/recover.component.ts b/web/projects/setup-wizard/src/app/components/recover.component.ts new file mode 100644 index 000000000..4303d709d --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/recover.component.ts @@ -0,0 +1,44 @@ +import { Component, Input } from '@angular/core' +import { RouterLink } from '@angular/router' +import { + TuiCellModule, + TuiIconModule, + TuiTitleModule, +} from '@taiga-ui/experimental' + +@Component({ + standalone: true, + selector: 'app-recover', + template: ` + + + + Use Existing Drive + + Attach an existing StartOS data drive (not a backup) + + + + + + + Transfer + + Transfer data from an existing StartOS data drive (not a backup) to a + new, preferred drive + + + + + + + Restore From Backup (Disaster Recovery) + Restore StartOS data from an encrypted backup + + + `, + imports: [RouterLink, TuiIconModule, TuiCellModule, TuiTitleModule], +}) +export class RecoverComponent { + @Input() disabled = false +} diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts deleted file mode 100644 index 3e26600bc..000000000 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core' -import { - TuiFieldErrorPipeModule, - TuiInputModule, - TuiInputPasswordModule, -} from '@taiga-ui/kit' -import { CifsModal } from './cifs-modal.page' - -@NgModule({ - declarations: [CifsModal], - imports: [ - CommonModule, - FormsModule, - TuiButtonModule, - TuiInputModule, - TuiErrorModule, - ReactiveFormsModule, - TuiFieldErrorPipeModule, - TuiInputPasswordModule, - ], - exports: [CifsModal], -}) -export class CifsModalModule {} diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html deleted file mode 100644 index 6250ad636..000000000 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html +++ /dev/null @@ -1,39 +0,0 @@ -
- - Hostname - - - - - - Path - - - - - - Username - - - - - - Password - - -
- - -
-
diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss deleted file mode 100644 index 5638f9537..000000000 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -.input { - margin-top: 16px; -} diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts deleted file mode 100644 index 4335f8a0e..000000000 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Component, Inject } from '@angular/core' -import { FormControl, FormGroup, Validators } from '@angular/forms' -import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' -import { LoadingService, StartOSDiskInfo } from '@start9labs/shared' -import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core' -import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' -import { - ApiService, - CifsBackupTarget, - CifsRecoverySource, -} from 'src/app/services/api/api.service' -import { PASSWORD } from '../password/password.page' - -@Component({ - selector: 'cifs-modal', - templateUrl: 'cifs-modal.page.html', - styleUrls: ['cifs-modal.page.scss'], - providers: [ - { - provide: TUI_VALIDATION_ERRORS, - useValue: { - required: 'This field is required', - }, - }, - ], -}) -export class CifsModal { - readonly form = new FormGroup({ - hostname: new FormControl('', { - validators: [ - Validators.required, - Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/), - ], - nonNullable: true, - }), - path: new FormControl('', { - validators: [Validators.required], - nonNullable: true, - }), - username: new FormControl('', { - validators: [Validators.required], - nonNullable: true, - }), - password: new FormControl(), - }) - - constructor( - @Inject(POLYMORPHEUS_CONTEXT) - private readonly context: TuiDialogContext<{ - cifs: CifsRecoverySource - recoveryPassword: string - }>, - private readonly dialogs: TuiDialogService, - private readonly api: ApiService, - private readonly loader: LoadingService, - ) {} - - cancel() { - this.context.$implicit.complete() - } - - async submit(): Promise { - const loader = this.loader - .open('Connecting to shared folder...') - .subscribe() - - 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, - }) - - loader.unsubscribe() - - this.presentModalPassword(diskInfo) - } catch (e) { - loader.unsubscribe() - this.presentAlertFailed() - } - } - - private presentModalPassword(diskInfo: StartOSDiskInfo) { - const target: CifsBackupTarget = { - ...this.form.getRawValue(), - mountable: true, - 'embassy-os': diskInfo, - } - - this.dialogs - .open(PASSWORD, { - label: 'Unlock Drive', - size: 's', - data: { target }, - }) - .subscribe(recoveryPassword => { - this.context.completeWith({ - cifs: { ...this.form.getRawValue(), type: 'cifs' }, - recoveryPassword, - }) - }) - } - - private presentAlertFailed() { - 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.', - { - label: 'Connection Failed', - size: 's', - }, - ) - .subscribe() - } -} diff --git a/web/projects/setup-wizard/src/app/modals/password/password.module.ts b/web/projects/setup-wizard/src/app/modals/password/password.module.ts deleted file mode 100644 index ce89d0709..000000000 --- a/web/projects/setup-wizard/src/app/modals/password/password.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core' -import { TuiInputPasswordModule } from '@taiga-ui/kit' -import { PasswordPage } from './password.page' - -@NgModule({ - declarations: [PasswordPage], - imports: [ - CommonModule, - FormsModule, - TuiButtonModule, - TuiInputPasswordModule, - TuiErrorModule, - ReactiveFormsModule, - ], - exports: [PasswordPage], -}) -export class PasswordPageModule {} diff --git a/web/projects/setup-wizard/src/app/modals/password/password.page.html b/web/projects/setup-wizard/src/app/modals/password/password.page.html deleted file mode 100644 index 10dae39cd..000000000 --- a/web/projects/setup-wizard/src/app/modals/password/password.page.html +++ /dev/null @@ -1,35 +0,0 @@ -

- Enter the password that was used to encrypt this drive. -

- -

- Choose a password for your server. - Make it good. Write it down. -

-
- -
- - Enter Password - - - - - - Retype Password - - - - -
- - -
-
diff --git a/web/projects/setup-wizard/src/app/modals/password/password.page.ts b/web/projects/setup-wizard/src/app/modals/password/password.page.ts deleted file mode 100644 index ec500c4ad..000000000 --- a/web/projects/setup-wizard/src/app/modals/password/password.page.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component, Inject } from '@angular/core' -import { FormControl } from '@angular/forms' -import * as argon2 from '@start9labs/argon2' -import { ErrorService } from '@start9labs/shared' -import { TuiDialogContext } from '@taiga-ui/core' -import { - PolymorpheusComponent, - POLYMORPHEUS_CONTEXT, -} from '@tinkoff/ng-polymorpheus' -import { - CifsBackupTarget, - DiskBackupTarget, -} from 'src/app/services/api/api.service' - -interface DialogData { - target?: CifsBackupTarget | DiskBackupTarget - storageDrive?: boolean -} - -@Component({ - selector: 'app-password', - templateUrl: 'password.page.html', -}) -export class PasswordPage { - readonly target = this.context.data.target - readonly storageDrive = this.context.data.storageDrive - readonly password = new FormControl('', { nonNullable: true }) - readonly confirm = new FormControl('', { nonNullable: true }) - - constructor( - @Inject(POLYMORPHEUS_CONTEXT) - private readonly context: TuiDialogContext, - private readonly errorService: ErrorService, - ) {} - - 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 - } - - get confirmError(): string | null { - return this.confirm.touched && this.password.value !== this.confirm.value - ? 'Passwords do not match' - : null - } - - verifyPw() { - try { - const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' - - argon2.verify(passwordHash, this.password.value) - this.context.completeWith(this.password.value) - } catch (e) { - this.errorService.handleError('Incorrect password provided') - } - } - - submitPw() { - this.context.completeWith(this.password.value) - } - - cancel() { - this.context.$implicit.complete() - } -} - -export const PASSWORD = new PolymorpheusComponent(PasswordPage) diff --git a/web/projects/setup-wizard/src/app/pages/attach.page.ts b/web/projects/setup-wizard/src/app/pages/attach.page.ts new file mode 100644 index 000000000..005bbc7c2 --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/attach.page.ts @@ -0,0 +1,108 @@ +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' +import { + DiskInfo, + DriveComponent, + ErrorService, + LoadingService, + toGuid, +} from '@start9labs/shared' +import { TuiDialogService, TuiLoaderModule } from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCardModule, + TuiCellModule, +} from '@taiga-ui/experimental' +import { PASSWORD } from 'src/app/components/password.component' +import { ApiService } from 'src/app/services/api.service' +import { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` +
+
Use existing drive
+
Select the physical drive containing your StartOS data
+ + @if (loading) { + + } @else { + @for (drive of drives; track drive) { + + } @empty { + No valid StartOS data drives found. Please make sure the drive is a + valid StartOS data drive (not a backup) and is firmly connected, then + refresh the page. + } + + } +
+ `, + imports: [ + TuiButtonModule, + TuiCardModule, + TuiCellModule, + TuiLoaderModule, + DriveComponent, + ], +}) +export default class AttachPage { + private readonly apiService = inject(ApiService) + private readonly router = inject(Router) + private readonly errorService = inject(ErrorService) + private readonly stateService = inject(StateService) + private readonly dialogs = inject(TuiDialogService) + private readonly loader = inject(LoadingService) + + loading = true + drives: DiskInfo[] = [] + + async ngOnInit() { + this.stateService.setupType = 'attach' + await this.getDrives() + } + + async refresh() { + this.loading = true + await this.getDrives() + } + + async getDrives() { + try { + this.drives = await this.apiService + .getDrives() + .then(drives => drives.filter(toGuid)) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + this.loading = false + } + } + + select(disk: DiskInfo) { + this.dialogs + .open(PASSWORD, { + label: 'Set Password', + size: 's', + data: { storageDrive: true }, + }) + .subscribe(password => { + this.attachDrive(toGuid(disk) || '', password) + }) + } + + private async attachDrive(guid: string, password: string) { + const loader = this.loader.open('Connecting to drive...').subscribe() + + try { + await this.stateService.importDrive(guid, password) + await this.router.navigate([`loading`]) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } +} diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach-routing.module.ts b/web/projects/setup-wizard/src/app/pages/attach/attach-routing.module.ts deleted file mode 100644 index 8ba45b29c..000000000 --- a/web/projects/setup-wizard/src/app/pages/attach/attach-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { AttachPage } from './attach.page' - -const routes: Routes = [ - { - path: '', - component: AttachPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class AttachPageRoutingModule {} diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.module.ts b/web/projects/setup-wizard/src/app/pages/attach/attach.module.ts deleted file mode 100644 index 486884878..000000000 --- a/web/projects/setup-wizard/src/app/pages/attach/attach.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { - GuidPipePipesModule, - UnitConversionPipesModule, -} from '@start9labs/shared' -import { AttachPage } from './attach.page' -import { AttachPageRoutingModule } from './attach-routing.module' - -@NgModule({ - declarations: [AttachPage], - imports: [ - CommonModule, - IonicModule, - AttachPageRoutingModule, - UnitConversionPipesModule, - GuidPipePipesModule, - ], -}) -export class AttachPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.page.html b/web/projects/setup-wizard/src/app/pages/attach/attach.page.html deleted file mode 100644 index 2c10d9545..000000000 --- a/web/projects/setup-wizard/src/app/pages/attach/attach.page.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - Use existing drive -
- - Select the physical drive containing your StartOS data - -
-
- - - - - - - -

- No valid StartOS data drives found. Please make sure the drive - is a valid StartOS data drive (not a backup) and is firmly - connected, then refresh the page. -

- - - - - -

{{ drive.logicalname }}

-

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

Capacity: {{ drive.capacity | convertBytes }}

-
-
-
- - - Refresh - -
-
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.page.scss b/web/projects/setup-wizard/src/app/pages/attach/attach.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts b/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts deleted file mode 100644 index 2662abc9d..000000000 --- a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' -import { DiskInfo, ErrorService, LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { ApiService } from 'src/app/services/api/api.service' -import { StateService } from 'src/app/services/state.service' -import { PASSWORD, PasswordPage } from 'src/app/modals/password/password.page' - -@Component({ - selector: 'app-attach', - templateUrl: 'attach.page.html', - styleUrls: ['attach.page.scss'], -}) -export class AttachPage { - loading = true - drives: DiskInfo[] = [] - - constructor( - private readonly apiService: ApiService, - private readonly navCtrl: NavController, - private readonly errorService: ErrorService, - private readonly stateService: StateService, - private readonly dialogs: TuiDialogService, - private readonly loader: LoadingService, - ) {} - - async ngOnInit() { - this.stateService.setupType = 'attach' - await this.getDrives() - } - - async refresh() { - this.loading = true - await this.getDrives() - } - - async getDrives() { - try { - this.drives = await this.apiService.getDrives() - } catch (e: any) { - this.errorService.handleError(e) - } finally { - this.loading = false - } - } - - select(guid: string) { - this.dialogs - .open(PASSWORD, { - label: 'Set Password', - size: 's', - data: { storageDrive: true }, - }) - .subscribe(password => { - this.attachDrive(guid, password) - }) - } - - private async attachDrive(guid: string, password: string) { - const loader = this.loader.open('Connecting to drive...').subscribe() - - try { - await this.stateService.importDrive(guid, password) - await this.navCtrl.navigateForward(`/loading`) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } -} diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy-routing.module.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy-routing.module.ts deleted file mode 100644 index 9265ba721..000000000 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { EmbassyPage } from './embassy.page' - -const routes: Routes = [ - { - path: '', - component: EmbassyPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class EmbassyPageRoutingModule { } diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.module.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy.module.ts deleted file mode 100644 index 7a7594b41..000000000 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { - GuidPipePipesModule, - UnitConversionPipesModule, -} from '@start9labs/shared' -import { EmbassyPage } from './embassy.page' -import { PasswordPageModule } from '../../modals/password/password.module' -import { EmbassyPageRoutingModule } from './embassy-routing.module' - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - EmbassyPageRoutingModule, - PasswordPageModule, - UnitConversionPipesModule, - GuidPipePipesModule, - ], - declarations: [EmbassyPage], -}) -export class EmbassyPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.html b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.html deleted file mode 100644 index bc7e82036..000000000 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - Select storage drive -
- - This is the drive where your StartOS data will be stored. - -
-
- - - No drives found -
- - Please connect a storage drive to your server. Then click - "Refresh". - -
-
-
- - - - - - - - - - - -

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

- {{ drive.logicalname }} - {{ drive.capacity | convertBytes - }} -

-

- - Drive capacity too small. - -

-
-
-
- - - Refresh - -
-
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.scss b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/setup-wizard/src/app/pages/home.page.ts b/web/projects/setup-wizard/src/app/pages/home.page.ts new file mode 100644 index 000000000..1480543c7 --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/home.page.ts @@ -0,0 +1,144 @@ +import { CommonModule } from '@angular/common' +import { Component, inject, OnInit } from '@angular/core' +import { RouterLink } from '@angular/router' +import { ErrorService } from '@start9labs/shared' +import { + TuiButtonModule, + TuiCardModule, + TuiCellModule, + TuiIconModule, + TuiIconsModule, + TuiTitleModule, +} from '@taiga-ui/experimental' +import { RecoverComponent } from 'src/app/components/recover.component' +import { ApiService } from 'src/app/services/api.service' +import { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` + + @if (!loading) { +
+
+ @if (recover) { + + } + {{ recover ? 'StartOS Setup' : 'Recover Options' }} +
+
+
+ + + + Start Fresh + + Get started with a brand new Start9 server + + + + +
+ +
+
+ } + `, + styles: ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + .logo { + width: 6rem; + margin: auto auto -2rem; + z-index: 1; + + &:only-child { + margin: auto; + } + + + * { + margin-top: 0; + } + } + + .back { + position: absolute; + top: 1rem; + border-radius: 10rem; + } + + .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; + + &_recover { + margin-left: -100%; + } + } + `, + imports: [ + CommonModule, + RouterLink, + TuiCardModule, + TuiButtonModule, + TuiIconsModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, + RecoverComponent, + ], +}) +export default class HomePage implements OnInit { + private readonly api = inject(ApiService) + private readonly errorService = inject(ErrorService) + private readonly stateService = inject(StateService) + + error = false + loading = true + recover = false + + async ngOnInit() { + this.stateService.setupType = 'fresh' + + try { + await this.api.getPubKey() + } catch (e: any) { + this.error = true + this.errorService.handleError(e) + } finally { + this.loading = false + } + } +} diff --git a/web/projects/setup-wizard/src/app/pages/home/home-routing.module.ts b/web/projects/setup-wizard/src/app/pages/home/home-routing.module.ts deleted file mode 100644 index fcc236c16..000000000 --- a/web/projects/setup-wizard/src/app/pages/home/home-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { HomePage } from './home.page' - -const routes: Routes = [ - { - path: '', - component: HomePage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class HomePageRoutingModule { } diff --git a/web/projects/setup-wizard/src/app/pages/home/home.module.ts b/web/projects/setup-wizard/src/app/pages/home/home.module.ts deleted file mode 100644 index 306796314..000000000 --- a/web/projects/setup-wizard/src/app/pages/home/home.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { HomePage } from './home.page' -import { PasswordPageModule } from '../../modals/password/password.module' -import { HomePageRoutingModule } from './home-routing.module' -import { SwiperModule } from 'swiper/angular' - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - HomePageRoutingModule, - PasswordPageModule, - SwiperModule, - ], - declarations: [HomePage], -}) -export class HomePageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/home/home.page.html b/web/projects/setup-wizard/src/app/pages/home/home.page.html deleted file mode 100644 index ab517cbcb..000000000 --- a/web/projects/setup-wizard/src/app/pages/home/home.page.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - -
- -
- - - - - - - {{ swiper?.activeIndex === 0 ? 'StartOS Setup' : 'Recover Options' - }} - - - - - - - - - - -

Start Fresh

-

Get started with a brand new Start9 server

-
-
- - - - - -

Recover

-

Recover, restore, or transfer StartOS data

-
-
-
- - - - - - - -

- Use Existing Drive -

-

Attach an existing StartOS data drive (not a backup)

-
-
- - - - - -

- Transfer -

-

- Transfer data from an existing StartOS data drive (not a - backup) to a new, preferred drive -
-

-
-
- - - - - -

- - Restore From Backup (Disaster Recovery) - -

-

Restore StartOS data from an encrypted backup

-
-
-
-
-
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/home/home.page.scss b/web/projects/setup-wizard/src/app/pages/home/home.page.scss deleted file mode 100644 index 508690bc7..000000000 --- a/web/projects/setup-wizard/src/app/pages/home/home.page.scss +++ /dev/null @@ -1,13 +0,0 @@ -.back-button { - position: absolute; - left: 16px; - top: 24px; - z-index: 1000000; -} - -.inline { - display: flex; - align-items: center; - text-align: center; - justify-content: center; -} diff --git a/web/projects/setup-wizard/src/app/pages/home/home.page.ts b/web/projects/setup-wizard/src/app/pages/home/home.page.ts deleted file mode 100644 index 88ab04160..000000000 --- a/web/projects/setup-wizard/src/app/pages/home/home.page.ts +++ /dev/null @@ -1,53 +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 { ErrorService } from '@start9labs/shared' -import { StateService } from 'src/app/services/state.service' - -SwiperCore.use([IonicSlides]) - -@Component({ - selector: 'app-home', - templateUrl: 'home.page.html', - styleUrls: ['home.page.scss'], -}) -export class HomePage { - swiper?: Swiper - error = false - loading = true - - constructor( - private readonly api: ApiService, - private readonly errorService: ErrorService, - private readonly stateService: StateService, - ) {} - - async ionViewDidEnter() { - this.stateService.setupType = 'fresh' - if (this.swiper) { - this.swiper.allowTouchMove = false - } - - try { - await this.api.getPubKey() - } catch (e: any) { - this.error = true - this.errorService.handleError(e) - } finally { - this.loading = false - } - } - - setSwiperInstance(swiper: any) { - this.swiper = swiper - } - - next() { - this.swiper?.slideNext(500) - } - - previous() { - this.swiper?.slidePrev(500) - } -} diff --git a/web/projects/setup-wizard/src/app/pages/loading.page.ts b/web/projects/setup-wizard/src/app/pages/loading.page.ts new file mode 100644 index 000000000..e65e791e7 --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/loading.page.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { Router } from '@angular/router' +import { InitializingComponent } from '@start9labs/shared' +import { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` + + `, + imports: [InitializingComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class LoadingPage { + readonly stateService = inject(StateService) + readonly router = inject(Router) +} diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts deleted file mode 100644 index 3de110846..000000000 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { InitializingModule } from '@start9labs/shared' -import { LoadingPage } from './loading.page' - -const routes: Routes = [ - { - path: '', - component: LoadingPage, - }, -] - -@NgModule({ - imports: [InitializingModule, RouterModule.forChild(routes)], - declarations: [LoadingPage], -}) -export class LoadingPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html deleted file mode 100644 index 54609eb9a..000000000 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts deleted file mode 100644 index f88c23e86..000000000 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' -import { StateService } from 'src/app/services/state.service' - -@Component({ - templateUrl: 'loading.page.html', -}) -export class LoadingPage { - constructor( - readonly stateService: StateService, - readonly navCtrl: NavController, - ) {} -} diff --git a/web/projects/setup-wizard/src/app/pages/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover.page.ts new file mode 100644 index 000000000..a7c10e708 --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/recover.page.ts @@ -0,0 +1,163 @@ +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' +import { DriveComponent, ErrorService } from '@start9labs/shared' +import { TuiDialogService, TuiLoaderModule } from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCardModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, +} from '@taiga-ui/experimental' +import { PolymorpheusComponent } from '@tinkoff/ng-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 { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` +
+
Restore from Backup
+ @if (loading) { + + } @else { +

Network Folder

+ Restore StartOS data from a folder on another computer that is connected + to the same network as your server. + + + +

Physical Drive

+ Restore StartOS data from a physical drive that is plugged directly into + your server. + + 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 + support more than one external drive without additional power and can + cause data corruption. + + + @for (d of drives; track d) { + + } + + + } +
+ `, + imports: [ + TuiCardModule, + TuiLoaderModule, + TuiButtonModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, + DriveComponent, + ], +}) +export default class RecoverPage { + private readonly api = inject(ApiService) + private readonly router = inject(Router) + private readonly dialogs = inject(TuiDialogService) + private readonly errorService = inject(ErrorService) + private readonly stateService = inject(StateService) + + loading = true + drives: DiskBackupTarget[] = [] + + async ngOnInit() { + this.stateService.setupType = 'restore' + await this.getDrives() + } + + async refresh() { + this.loading = true + await this.getDrives() + } + + empty(drive: DiskBackupTarget) { + return !drive['embassy-os']?.full + } + + async getDrives() { + this.drives = [] + try { + await this.api.getDrives().then(disks => + disks + .filter(d => d.partitions.length) + .forEach(d => { + d.partitions.forEach(p => { + this.drives.push({ ...d, ...p }) + }) + }), + ) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + this.loading = false + } + } + + select(target: DiskBackupTarget) { + const { logicalname } = target + + if (!logicalname) return + + this.dialogs + .open(PASSWORD, { + label: 'Unlock Drive', + size: 's', + data: { target }, + }) + .pipe(filter(Boolean)) + .subscribe(password => { + this.onSource(logicalname, password) + }) + } + + onCifs() { + this.dialogs + .open<{ + cifs: CifsRecoverySource + recoveryPassword: string + }>(new PolymorpheusComponent(CifsComponent), { + label: 'Connect Network Folder', + }) + .subscribe(({ cifs, recoveryPassword }) => { + this.stateService.recoverySource = { type: 'backup', target: cifs } + this.stateService.recoveryPassword = recoveryPassword + 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']) + } +} diff --git a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html b/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html deleted file mode 100644 index 7f4a4e5bd..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
- -

- - StartOS backup detected -

- - -

- - No StartOS backup -

-
-
diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover-routing.module.ts b/web/projects/setup-wizard/src/app/pages/recover/recover-routing.module.ts deleted file mode 100644 index 524458fa8..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/recover-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { RecoverPage } from './recover.page' - -const routes: Routes = [ - { - path: '', - component: RecoverPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class RecoverPageRoutingModule { } diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts deleted file mode 100644 index eaf04f506..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { UnitConversionPipesModule } from '@start9labs/shared' -import { DriveStatusComponent, RecoverPage } from './recover.page' -import { PasswordPageModule } from '../../modals/password/password.module' -import { RecoverPageRoutingModule } from './recover-routing.module' -import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module' - -@NgModule({ - declarations: [RecoverPage, DriveStatusComponent], - imports: [ - CommonModule, - FormsModule, - IonicModule, - RecoverPageRoutingModule, - PasswordPageModule, - UnitConversionPipesModule, - CifsModalModule, - ], -}) -export class RecoverPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html b/web/projects/setup-wizard/src/app/pages/recover/recover.page.html deleted file mode 100644 index 32f71a3ad..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - Restore from Backup - - - - - - - - -

Network Folder

-

- Restore StartOS data from a folder on another computer that is - connected to the same network as your server. -

- - - - - - Open - - - -
-
- - -

Physical Drive

-
-

- Restore StartOS data from a physical drive that is plugged - directly into your server. -

-
- - 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 support more than one external drive without - additional power and can cause data corruption. - -
- - - - - -

{{ drive.label || drive.logicalname }}

- -

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

Capacity: {{ drive.capacity | convertBytes }}

-
-
-
- - - Refresh - -
-
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.scss b/web/projects/setup-wizard/src/app/pages/recover/recover.page.scss deleted file mode 100644 index 4acada7bf..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.scss +++ /dev/null @@ -1,5 +0,0 @@ -.target-label { - font-weight: 500; - padding-bottom: 6px; - font-variant-caps: all-small-caps; -} \ No newline at end of file diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts deleted file mode 100644 index c0042cd95..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Component, Input } from '@angular/core' -import { NavController } from '@ionic/angular' -import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' -import { - ApiService, - CifsRecoverySource, - DiskBackupTarget, -} from 'src/app/services/api/api.service' -import { ErrorService } from '@start9labs/shared' -import { StateService } from 'src/app/services/state.service' -import { PASSWORD } from '../../modals/password/password.page' -import { TuiDialogService } from '@taiga-ui/core' -import { filter } from 'rxjs' -import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' - -@Component({ - selector: 'app-recover', - templateUrl: 'recover.page.html', - styleUrls: ['recover.page.scss'], -}) -export class RecoverPage { - loading = true - mappedDrives: MappedDisk[] = [] - - constructor( - private readonly apiService: ApiService, - private readonly navCtrl: NavController, - private readonly dialogs: TuiDialogService, - private readonly errorService: ErrorService, - private readonly stateService: StateService, - ) {} - - async ngOnInit() { - this.stateService.setupType = 'restore' - await this.getDrives() - } - - async refresh() { - this.loading = true - await this.getDrives() - } - - driveClickable(mapped: MappedDisk) { - return mapped.drive['embassy-os']?.full - } - - async getDrives() { - this.mappedDrives = [] - try { - const disks = await this.apiService.getDrives() - disks - .filter(d => d.partitions.length) - .forEach(d => { - d.partitions.forEach(p => { - const drive: DiskBackupTarget = { - vendor: d.vendor, - model: d.model, - logicalname: p.logicalname, - label: p.label, - capacity: p.capacity, - used: p.used, - 'embassy-os': p['embassy-os'], - } - this.mappedDrives.push({ - hasValidBackup: !!p['embassy-os']?.full, - drive, - }) - }) - }) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - this.loading = false - } - } - - presentModalCifs() { - this.dialogs - .open<{ cifs: CifsRecoverySource; recoveryPassword: string }>( - new PolymorpheusComponent(CifsModal), - { - label: 'Connect Network Folder', - }, - ) - .subscribe(({ cifs, recoveryPassword }) => { - this.stateService.recoverySource = { - type: 'backup', - target: cifs, - } - this.stateService.recoveryPassword = recoveryPassword - this.navCtrl.navigateForward('/storage') - }) - } - - async select(target: DiskBackupTarget) { - const { logicalname } = target - - if (!logicalname) return - - this.dialogs - .open(PASSWORD, { - label: 'Unlock Drive', - size: 's', - data: { target }, - }) - .pipe(filter(Boolean)) - .subscribe(password => { - this.selectRecoverySource(logicalname, password) - }) - } - - private async selectRecoverySource(logicalname: string, password?: string) { - this.stateService.recoverySource = { - type: 'backup', - target: { - type: 'disk', - logicalname, - }, - } - this.stateService.recoveryPassword = password - this.navCtrl.navigateForward(`/storage`) - } -} - -@Component({ - selector: 'drive-status', - templateUrl: './drive-status.component.html', - styleUrls: ['./recover.page.scss'], -}) -export class DriveStatusComponent { - @Input({ required: true }) hasValidBackup!: boolean -} - -interface MappedDisk { - hasValidBackup: boolean - drive: DiskBackupTarget -} diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/web/projects/setup-wizard/src/app/pages/storage.page.ts similarity index 59% rename from web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts rename to web/projects/setup-wizard/src/app/pages/storage.page.ts index 33c2298d0..6622d4083 100644 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/web/projects/setup-wizard/src/app/pages/storage.page.ts @@ -1,49 +1,83 @@ -import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' import { DiskInfo, + DriveComponent, ErrorService, - GuidPipe, LoadingService, + toGuid, } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' +import { TuiDialogService, TuiLoaderModule } from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCardModule, + TuiCellModule, +} from '@taiga-ui/experimental' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter, of, switchMap } from 'rxjs' +import { PASSWORD } from 'src/app/components/password.component' import { ApiService, BackupRecoverySource, - DiskRecoverySource, DiskMigrateSource, -} from 'src/app/services/api/api.service' + DiskRecoverySource, +} from 'src/app/services/api.service' import { StateService } from 'src/app/services/state.service' -import { PASSWORD, PasswordPage } from '../../modals/password/password.page' -import { TUI_PROMPT } from '@taiga-ui/kit' -import { filter, of, switchMap } from 'rxjs' @Component({ - selector: 'app-embassy', - templateUrl: 'embassy.page.html', - styleUrls: ['embassy.page.scss'], - providers: [GuidPipe], -}) -export class EmbassyPage { - storageDrives: DiskInfo[] = [] - loading = true + standalone: true, + template: ` +
+ @if (loading || drives.length) { +
Select storage drive
+ This is the drive where your StartOS data will be stored. + } @else { +
No drives found
+ Please connect a storage drive to your server. Then click "Refresh". + } - constructor( - private readonly apiService: ApiService, - private readonly navCtrl: NavController, - private readonly dialogs: TuiDialogService, - private readonly stateService: StateService, - private readonly loader: LoadingService, - private readonly errorService: ErrorService, - private readonly guidPipe: GuidPipe, - ) {} + @if (loading) { + + } + + @for (d of drives; track d) { + + } + + +
+ `, + imports: [ + TuiCardModule, + TuiLoaderModule, + TuiCellModule, + TuiButtonModule, + DriveComponent, + ], +}) +export default class StoragePage { + private readonly api = inject(ApiService) + private readonly router = inject(Router) + private readonly dialogs = inject(TuiDialogService) + private readonly stateService = inject(StateService) + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + + drives: DiskInfo[] = [] + loading = true async ngOnInit() { await this.getDrives() } - tooSmall(drive: DiskInfo) { - return drive.capacity < 34359738368 + isSmall({ capacity }: DiskInfo) { + return capacity < 34359738368 } async refresh() { @@ -54,11 +88,11 @@ export class EmbassyPage { async getDrives() { this.loading = true try { - const disks = await this.apiService.getDrives() + const disks = await this.api.getDrives() if (this.stateService.setupType === 'fresh') { - this.storageDrives = disks + this.drives = disks } else if (this.stateService.setupType === 'restore') { - this.storageDrives = disks.filter( + this.drives = disks.filter( d => !d.partitions .map(p => p.logicalname) @@ -72,7 +106,7 @@ export class EmbassyPage { } else if (this.stateService.setupType === 'transfer') { const guid = (this.stateService.recoverySource as DiskMigrateSource) .guid - this.storageDrives = disks.filter(d => { + this.drives = disks.filter(d => { return ( d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid) ) @@ -85,8 +119,8 @@ export class EmbassyPage { } } - chooseDrive(drive: DiskInfo) { - of(!this.guidPipe.transform(drive) && !drive.partitions.some(p => p.used)) + select(drive: DiskInfo) { + of(!toGuid(drive) && !drive.partitions.some(p => p.used)) .pipe( switchMap(unused => unused @@ -138,7 +172,7 @@ export class EmbassyPage { try { await this.stateService.setupEmbassy(logicalname, password) - await this.navCtrl.navigateForward(`/loading`) + await this.router.navigate([`loading`]) } catch (e: any) { this.errorService.handleError(e) } finally { diff --git a/web/projects/setup-wizard/src/app/pages/success.page.ts b/web/projects/setup-wizard/src/app/pages/success.page.ts new file mode 100644 index 000000000..fab7825df --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/success.page.ts @@ -0,0 +1,175 @@ +import { DOCUMENT } from '@angular/common' +import { + AfterViewInit, + Component, + ElementRef, + inject, + ViewChild, +} from '@angular/core' +import { DownloadHTMLService, ErrorService } from '@start9labs/shared' +import { + TuiButtonModule, + TuiCardModule, + TuiIconModule, + TuiSurfaceModule, +} from '@taiga-ui/experimental' +import { DocumentationComponent } from 'src/app/components/documentation.component' +import { MatrixComponent } from 'src/app/components/matrix.component' +import { ApiService } from 'src/app/services/api.service' +import { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` + + @if (isKiosk) { +
+

+ + Setup Complete! +

+ +
+ } @else if (lanAddress) { +
+

+ + Setup Complete! +

+ @if (stateService.setupType === 'restore') { +

You can now safely unplug your backup drive

+ } @else if (stateService.setupType === 'transfer') { +

You can now safely unplug your old StartOS data drive

+ } + + + + + Trust your Root CA + + In the new tab, follow instructions to trust your server's Root CA + and log in. + + + Open + + + +
+ } + `, + styles: ` + .heading { + display: flex; + gap: 1rem; + align-items: center; + margin: 0; + font: var(--tui-font-heading-4); + } + + .caps { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + text-transform: uppercase; + } + + [tuiCardLarge] { + color: var(--tui-text-01); + text-decoration: none; + text-align: center; + } + + a[tuiCardLarge]:not([href]) { + opacity: var(--tui-disabled-opacity); + pointer-events: none; + } + `, + imports: [ + TuiCardModule, + TuiIconModule, + TuiButtonModule, + TuiSurfaceModule, + MatrixComponent, + DocumentationComponent, + ], +}) +export default class SuccessPage implements AfterViewInit { + @ViewChild(DocumentationComponent, { read: ElementRef }) + private readonly documentation?: ElementRef + + private readonly document = inject(DOCUMENT) + private readonly errorService = inject(ErrorService) + private readonly api = inject(ApiService) + private readonly downloadHtml = inject(DownloadHTMLService) + + readonly stateService = inject(StateService) + readonly isKiosk = ['localhost', '127.0.0.1'].includes( + this.document.location.hostname, + ) + + torAddress?: string + lanAddress?: string + cert?: string + disableLogin = this.stateService.setupType === 'fresh' + + ngAfterViewInit() { + setTimeout(() => this.complete(), 1000) + } + + download() { + const torAddress = this.document.getElementById('tor-addr') + const lanAddress = this.document.getElementById('lan-addr') + const html = this.documentation?.nativeElement.innerHTML || '' + + if (torAddress) torAddress.innerHTML = this.torAddress! + if (lanAddress) lanAddress.innerHTML = this.lanAddress! + + this.document + .getElementById('cert') + ?.setAttribute( + 'href', + `data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`, + ) + this.downloadHtml.download('StartOS-info.html', html).then(_ => { + this.disableLogin = false + }) + } + + exitKiosk() { + this.api.exit() + } + + private async complete() { + try { + const ret = await this.api.complete() + if (!this.isKiosk) { + this.torAddress = ret['tor-address'].replace(/^https:/, 'http:') + this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:') + this.cert = ret['root-ca'] + + await this.api.exit() + } + } catch (e: any) { + this.errorService.handleError(e) + } + } +} diff --git a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html b/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html deleted file mode 100644 index 51e2c8483..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - StartOS Address Info - - -
-

- StartOS Address Info -

- -
-
-

Important!

-

- Download your server's Root CA and - - follow the instructions - - to establish a secure connection with your server. -

-
- -
-
-

- Access from home (LAN) -

-

- Visit the address below when you are connected to the same WiFi or - Local Area Network (LAN) as your server. -

-

- -

- -

- Access on the go (Tor) -

-

Visit the address below when you are away from home.

-

- Note: - This address will only work from a Tor-enabled browser. - - Follow the instructions - - to get setup. -

-

- -

-
-
- - diff --git a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts b/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts deleted file mode 100644 index 6d8fb54d1..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, Input } from '@angular/core' - -@Component({ - selector: 'download-doc', - templateUrl: 'download-doc.component.html', -}) -export class DownloadDocComponent { - @Input({ required: true }) lanAddress!: string - - get crtName(): string { - const hostname = new URL(this.lanAddress).hostname - return `${hostname}.crt` - } -} diff --git a/web/projects/setup-wizard/src/app/pages/success/success-routing.module.ts b/web/projects/setup-wizard/src/app/pages/success/success-routing.module.ts deleted file mode 100644 index 33c6de9be..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/success-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { SuccessPage } from './success.page' - -const routes: Routes = [ - { - path: '', - component: SuccessPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class SuccessPageRoutingModule {} diff --git a/web/projects/setup-wizard/src/app/pages/success/success.module.ts b/web/projects/setup-wizard/src/app/pages/success/success.module.ts deleted file mode 100644 index ef937ded7..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/success.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { ResponsiveColDirective } from '@start9labs/shared' - -import { SuccessPage } from './success.page' -import { PasswordPageModule } from '../../modals/password/password.module' -import { SuccessPageRoutingModule } from './success-routing.module' -import { DownloadDocComponent } from './download-doc/download-doc.component' - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - PasswordPageModule, - SuccessPageRoutingModule, - ResponsiveColDirective, - ], - declarations: [SuccessPage, DownloadDocComponent], - exports: [SuccessPage], -}) -export class SuccessPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.html b/web/projects/setup-wizard/src/app/pages/success/success.page.html deleted file mode 100644 index dbce29073..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.html +++ /dev/null @@ -1,102 +0,0 @@ -Your browser does not support the canvas element. - - - - - - - - - -
- -

Setup Complete!

-
- -
-
-
-
- - - - - -
-
- -

Setup Complete!

-
-

- You can now safely unplug your backup drive -

-

- You can now safely unplug your old StartOS data drive -

-
-
- - - Download address info -

- start.local was for setup purposes only. It will no - longer work. -

-
- -
-

Download

- -
-
-
- - - Trust your Root CA -

- In the new tab, follow instructions to trust your - server's Root CA and log in. -

-
- -
-
-

Open

- -
-
-
-
-
-
-
- - -
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.scss b/web/projects/setup-wizard/src/app/pages/success/success.page.scss deleted file mode 100644 index 910849788..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.scss +++ /dev/null @@ -1,183 +0,0 @@ -canvas { - position: fixed; - left: 0; - top: 0; - z-index: -1; -} - -h1 { - font-variant: all-small-caps; - margin: unset; -} - -ion-content { - position: absolute; - z-index: 0; - --background: transparent; -} - -ion-grid { - max-width: 760px; -} - -.inline-container { - display: flex; - justify-content: center; - align-items: center; -} - -.card-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 1rem; -} - -ion-card { - padding: 2.4rem; - - h1 { - color: var(--ion-color-success); - padding-left: 0.5rem; - } - ion-icon { - font-size: 40px; - } - - li { - margin-bottom: 2rem; - } - - // download info card - ion-card { - min-height: 260px; - width: 80%; - background: #615F5F; - color: var(--ion-text-color); - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); - border-radius: 44px; - text-align: left; - cursor: pointer; - position: relative; - padding: 1rem 2rem; - transition: all 350ms ease; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - transition-property: transform; - transform: scale(1.05); - transition-delay: 40ms; - } - - ion-card-title { - color: var(--ion-text-color); - font-size: 1.3rem; - } - - ion-footer { - position: absolute; - bottom: 10px; - left: 0; - color: var(--ion-text-color); - - p { - font-size: 1.1rem; - font-weight: bold; - margin: unset; - } - - ion-icon { - font-size: 1.6rem; - } - } - - .footer-md::before { - background-image: none; - } - } - - .login-button { - --background: var(--color-accent); - --padding-bottom: 2.5rem; - --padding-top: 2.5rem; - --padding-start: 2.5rem; - --padding-end: 2.5rem; - --border-radius: 44px; - font-size: 1.4rem !important; - font-weight: bold; - text-transform: none; - letter-spacing: normal; - transition: all 350ms ease; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - transition-property: transform; - transform: scale(1.05); - transition-delay: 40ms; - } - - ion-icon { - font-size: 1.7rem; - } - } - - .launch-button { - --background: var(--alt-blue); - } - - #information:after, #launch:after { - content: ''; - position: absolute; - left: 0; - top: 79%; - width: 100%; - height: 100%; - background: var(--color-accent); - } - - #launch:after { - background: var(--alt-blue); - } - -} - -.mb-12 { - margin-bottom: 3rem; -} - -.pb-2 { - padding-bottom: 0.5rem; -} - -.pt-1 { - padding-top: 0.25rem; -} - - -.action-text { - font-variant-caps: all-small-caps; - padding-right: 0.5rem; - font-size: 1.5rem !important; - letter-spacing: 0.03rem; - padding-bottom: 0.1rem; -} - -@media (max-width: 700px) { - .setup { - flex-direction: column; - } - - ion-card { - ion-card { - width: 100%; - padding-bottom: unset; - } - #information:after { - top: 84%; - } - #launch:after { - top: 85%; - } - } -} \ No newline at end of file diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.ts b/web/projects/setup-wizard/src/app/pages/success/success.page.ts deleted file mode 100644 index 9847c0e67..000000000 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { DOCUMENT } from '@angular/common' -import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core' -import { DownloadHTMLService, ErrorService } from '@start9labs/shared' -import { ApiService } from 'src/app/services/api/api.service' -import { StateService } from 'src/app/services/state.service' - -@Component({ - selector: 'success', - templateUrl: 'success.page.html', - styleUrls: ['success.page.scss'], -}) -export class SuccessPage { - @ViewChild('canvas', { static: true }) - private canvas: ElementRef = - {} as ElementRef - private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D - - torAddress?: string - lanAddress?: string - cert?: string - - tileSize = 16 - // a higher fade factor will make the characters fade quicker - fadeFactor = 0.07 - columns: any[] = [] - maxStackHeight: any - disableLogin = this.stateService.setupType === 'fresh' - - constructor( - @Inject(DOCUMENT) private readonly document: Document, - private readonly errorService: ErrorService, - private readonly stateService: StateService, - private readonly api: ApiService, - private readonly downloadHtml: DownloadHTMLService, - private readonly ngZone: NgZone, - ) {} - - get setupType() { - return this.stateService.setupType - } - - get isKiosk() { - return ['localhost', '127.0.0.1'].includes(this.document.location.hostname) - } - - async ngAfterViewInit() { - this.ngZone.runOutsideAngular(() => this.initMatrix()) - setTimeout(() => this.complete(), 1000) - } - - download() { - const torAddress = this.document.getElementById('tor-addr') - const lanAddress = this.document.getElementById('lan-addr') - - if (torAddress) torAddress.innerHTML = this.torAddress! - if (lanAddress) lanAddress.innerHTML = this.lanAddress! - - this.document - .getElementById('cert') - ?.setAttribute( - 'href', - 'data:application/x-x509-ca-cert;base64,' + - encodeURIComponent(this.cert!), - ) - let html = this.document.getElementById('downloadable')?.innerHTML || '' - this.downloadHtml.download('StartOS-info.html', html).then(_ => { - this.disableLogin = false - }) - } - - exitKiosk() { - this.api.exit() - } - - private async complete() { - try { - const ret = await this.api.complete() - if (!this.isKiosk) { - this.torAddress = ret['tor-address'].replace(/^https:/, 'http:') - this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:') - this.cert = ret['root-ca'] - - await this.api.exit() - } - } catch (e: any) { - this.errorService.handleError(e) - } - } - - private initMatrix() { - this.ctx = this.canvas.nativeElement.getContext('2d')! - this.canvas.nativeElement.width = window.innerWidth - this.canvas.nativeElement.height = window.innerHeight - this.setupMatrixGrid() - this.tick() - } - - private setupMatrixGrid() { - this.maxStackHeight = Math.ceil(this.ctx.canvas.height / this.tileSize) - // divide the canvas into columns - for (let i = 0; i < this.ctx.canvas.width / this.tileSize; ++i) { - const column = {} as any - // save the x position of the column - column.x = i * this.tileSize - // create a random stack height for the column - column.stackHeight = 10 + Math.random() * this.maxStackHeight - // add a counter to count the stack height - column.stackCounter = 0 - // add the column to the list - this.columns.push(column) - } - } - - private draw() { - // draw a semi transparent black rectangle on top of the scene to slowly fade older characters - this.ctx.fillStyle = 'rgba( 0 , 0 , 0 , ' + this.fadeFactor + ' )' - this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height) - // pick a font slightly smaller than the tile size - this.ctx.font = this.tileSize - 2 + 'px monospace' - this.ctx.fillStyle = '#ff4961' - for (let i = 0; i < this.columns.length; ++i) { - // pick a random ascii character (change the 94 to a higher number to include more characters) - const randomCharacter = String.fromCharCode( - 33 + Math.floor(Math.random() * 94), - ) - this.ctx.fillText( - randomCharacter, - this.columns[i].x, - this.columns[i].stackCounter * this.tileSize + this.tileSize, - ) - // if the stack is at its height limit, pick a new random height and reset the counter - if (++this.columns[i].stackCounter >= this.columns[i].stackHeight) { - this.columns[i].stackHeight = 10 + Math.random() * this.maxStackHeight - this.columns[i].stackCounter = 0 - } - } - } - - private tick() { - this.draw() - setTimeout(this.tick.bind(this), 50) - } -} diff --git a/web/projects/setup-wizard/src/app/pages/transfer.page.ts b/web/projects/setup-wizard/src/app/pages/transfer.page.ts new file mode 100644 index 000000000..eca9b8820 --- /dev/null +++ b/web/projects/setup-wizard/src/app/pages/transfer.page.ts @@ -0,0 +1,105 @@ +import { Component, inject } from '@angular/core' +import { Router } from '@angular/router' +import { + DiskInfo, + DriveComponent, + ErrorService, + toGuid, +} from '@start9labs/shared' +import { + TuiDialogOptions, + TuiDialogService, + TuiLoaderModule, +} from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCardModule, + TuiCellModule, +} from '@taiga-ui/experimental' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { filter } from 'rxjs' +import { ApiService } from 'src/app/services/api.service' +import { StateService } from 'src/app/services/state.service' + +@Component({ + standalone: true, + template: ` +
+
Transfer
+ Select the physical drive containing your StartOS data + @if (loading) { + + } + @for (drive of drives; track drive) { + + } + +
+ `, + imports: [ + TuiCardModule, + TuiCellModule, + TuiButtonModule, + TuiLoaderModule, + DriveComponent, + ], +}) +export default class TransferPage { + private readonly apiService = inject(ApiService) + private readonly router = inject(Router) + private readonly dialogs = inject(TuiDialogService) + private readonly errorService = inject(ErrorService) + private readonly stateService = inject(StateService) + + loading = true + drives: DiskInfo[] = [] + + async ngOnInit() { + this.stateService.setupType = 'transfer' + await this.getDrives() + } + + async refresh() { + await this.getDrives() + } + + async getDrives() { + this.loading = true + + try { + this.drives = await this.apiService + .getDrives() + .then(drives => drives.filter(toGuid)) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + this.loading = false + } + } + + select(drive: DiskInfo) { + this.dialogs + .open(TUI_PROMPT, OPTIONS) + .pipe(filter(Boolean)) + .subscribe(() => { + this.stateService.recoverySource = { + type: 'migrate', + guid: toGuid(drive) || '', + } + this.router.navigate([`storage`]) + }) + } +} + +const OPTIONS: Partial> = { + label: 'Warning', + size: 's', + data: { + content: + 'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.', + yes: 'Continue', + no: 'Cancel', + }, +} diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts b/web/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts deleted file mode 100644 index acee2e7f3..000000000 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { TransferPage } from './transfer.page' - -const routes: Routes = [ - { - path: '', - component: TransferPage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class TransferPageRoutingModule {} diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts b/web/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts deleted file mode 100644 index 2dfd57b20..000000000 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { - GuidPipePipesModule, - UnitConversionPipesModule, -} from '@start9labs/shared' -import { TransferPage } from './transfer.page' -import { TransferPageRoutingModule } from './transfer-routing.module' - -@NgModule({ - declarations: [TransferPage], - imports: [ - CommonModule, - IonicModule, - TransferPageRoutingModule, - UnitConversionPipesModule, - GuidPipePipesModule, - ], -}) -export class TransferPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.html b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.html deleted file mode 100644 index 958a1e6d8..000000000 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Transfer -
- - Select the physical drive containing your StartOS data - -
-
- - - - - - - - - - - -

{{ drive.logicalname }}

-

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

Capacity: {{ drive.capacity | convertBytes }}

-
-
-
- - - Refresh - -
-
-
-
-
-
-
diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts deleted file mode 100644 index 8cf58d7fa..000000000 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorService } from '@start9labs/shared' -import { StateService } from 'src/app/services/state.service' -import { TuiDialogService } from '@taiga-ui/core' -import { TUI_PROMPT } from '@taiga-ui/kit' -import { filter } from 'rxjs' - -@Component({ - selector: 'app-transfer', - templateUrl: 'transfer.page.html', - styleUrls: ['transfer.page.scss'], -}) -export class TransferPage { - loading = true - drives: DiskInfo[] = [] - - constructor( - private readonly apiService: ApiService, - private readonly navCtrl: NavController, - private readonly dialogs: TuiDialogService, - private readonly errorService: ErrorService, - private readonly stateService: StateService, - ) {} - - async ngOnInit() { - this.stateService.setupType = 'transfer' - await this.getDrives() - } - - async refresh() { - this.loading = true - await this.getDrives() - } - - async getDrives() { - try { - this.drives = await this.apiService.getDrives() - } catch (e: any) { - this.errorService.handleError(e) - } finally { - this.loading = false - } - } - - select(guid: string) { - this.dialogs - .open(TUI_PROMPT, { - label: 'Warning', - size: 's', - data: { - content: - 'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.', - yes: 'Continue', - no: 'Cancel', - }, - }) - .pipe(filter(Boolean)) - .subscribe(() => { - this.stateService.recoverySource = { - type: 'migrate', - guid, - } - this.navCtrl.navigateForward(`/storage`) - }) - } -} diff --git a/web/projects/setup-wizard/src/app/services/api/api.service.ts b/web/projects/setup-wizard/src/app/services/api.service.ts similarity index 100% rename from web/projects/setup-wizard/src/app/services/api/api.service.ts rename to web/projects/setup-wizard/src/app/services/api.service.ts diff --git a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts b/web/projects/setup-wizard/src/app/services/live-api.service.ts similarity index 96% rename from web/projects/setup-wizard/src/app/services/api/live-api.service.ts rename to web/projects/setup-wizard/src/app/services/live-api.service.ts index b13967f91..666d2beb9 100644 --- a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/live-api.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core' +import { inject, Injectable } from '@angular/core' import { DiskListResponse, StartOSDiskInfo, @@ -28,9 +28,7 @@ import { Observable } from 'rxjs' providedIn: 'root', }) export class LiveApiService extends ApiService { - constructor(private readonly http: HttpService) { - super() - } + private readonly http = inject(HttpService) async getSetupStatus() { return this.rpcRequest({ diff --git a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/mock-api.service.ts similarity index 99% rename from web/projects/setup-wizard/src/app/services/api/mock-api.service.ts rename to web/projects/setup-wizard/src/app/services/mock-api.service.ts index e170683eb..00032ba9f 100644 --- a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/mock-api.service.ts @@ -111,7 +111,7 @@ export class MockApiService extends ApiService { guid: 'guid-guid-guid-guid', }, ], - capacity: 1000190509056, + capacity: 1000190509, guid: null, }, ] diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index e70478559..b4b5a1264 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -1,17 +1,16 @@ -import { Injectable } from '@angular/core' -import { ApiService, RecoverySource } from './api/api.service' +import { inject, Injectable } from '@angular/core' +import { ApiService, RecoverySource } from './api.service' @Injectable({ providedIn: 'root', }) export class StateService { - setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' + private readonly api = inject(ApiService) + setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' recoverySource?: RecoverySource recoveryPassword?: string - constructor(private readonly api: ApiService) {} - async importDrive(guid: string, password: string): Promise { await this.api.attach({ guid, diff --git a/web/projects/setup-wizard/src/styles.scss b/web/projects/setup-wizard/src/styles.scss index b9f09b845..5ff5bd3c4 100644 --- a/web/projects/setup-wizard/src/styles.scss +++ b/web/projects/setup-wizard/src/styles.scss @@ -1,345 +1,69 @@ -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf'); -} - -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: bold; - src: url('/assets/fonts/Montserrat/Montserrat-Bold.ttf'); -} - -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: 600; - src: url('/assets/fonts/Montserrat/Montserrat-SemiBold.ttf'); -} - -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: bold; - src: url('/assets/fonts/Montserrat/Montserrat-Bold.ttf'); -} - -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: 500; - src: url('/assets/fonts/Montserrat/Montserrat-Medium.ttf'); -} - -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: thin; - src: url('/assets/fonts/Montserrat/Montserrat-Light.ttf'); -} - -@font-face { - font-family: 'Benton Sans'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Benton_Sans/BentonSans-Regular.otf'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Open_Sans/OpenSans-Regular.ttf'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: bold; - src: url('/assets/fonts/Open_Sans/OpenSans-Bold.ttf'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 700; - src: url('/assets/fonts/Open_Sans/OpenSans-Bold.ttf'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: url('/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: thin; - src: url('/assets/fonts/Open_Sans/OpenSans-Light.ttf'); -} - -/** Ionic CSS Variables overrides **/ -:root { - --ion-font-family: 'Montserrat', sans-serif; - - --ion-background-color: #333333; - --ion-background-color-rgb: 51, 51, 51; - - --ion-text-color: #F4F4F5; - --ion-text-color-rgb: 244, 244, 245; - - --ion-color-step-50: #3d3d3d; - --ion-color-step-100: #464646; - --ion-color-step-150: #505050; - --ion-color-step-200: #5a5a5a; - --ion-color-step-250: #636364; - --ion-color-step-300: #6d6d6d; - --ion-color-step-350: #777777; - --ion-color-step-400: #808081; - --ion-color-step-450: #8a8a8a; - --ion-color-step-500: #949494; - --ion-color-step-550: #9d9d9e; - --ion-color-step-600: #a7a7a7; - --ion-color-step-650: #b0b0b1; - --ion-color-step-700: #bababb; - --ion-color-step-750: #c4c4c5; - --ion-color-step-800: #cdcdce; - --ion-color-step-850: #d7d7d8; - --ion-color-step-900: #e1e1e2; - --ion-color-step-950: #eaeaeb; - - - --ion-color-dark: var(--ion-color-step-50) !important; - // --ion-color-base-rgb: - --ion-color-dark-contrast: var(--ion-color-step-950) !important; - // --ion-color-dark-contrast-rgb: - --ion-color-dark-shade: var(--ion-color-step-100) !important; - --ion-color-dark-tint: var(--ion-color-step-250) !important; - - --color-accent: #6866cc; - --color-dark-black: #121212; - - --alt-red: #FF4961; - --alt-orange: #F89248; - --alt-yellow: #E5D53E; - --alt-green: #3DCF6F; - --alt-blue: #00A8A8; - --alt-purple: #9747FF; -} - -h1, -h2, -h3, -h4 { - font-weight: 400; -} - -h1 { - font-size: 42px; -} - -ion-card-title { - margin: 16px 0; - font-family: 'Montserrat', sans-serif; - font-size: x-large; - --color: var(--ion-color-light); -} - -ion-card-subtitle { - font-size: 20px; - font-weight: 200; - max-width: 400px; - padding: 0.7rem; - color: var(--ion-color-step-900) !important; -} - -ion-label ion-text { - font-size: 1.2rem; - font-weight: 500; -} - -p { - color: var(--ion-color-dark-contrast) !important; - font-size: 1.12rem !important; - font-family: 'Open Sans', sans-serif; - font-weight: normal; -} - -ion-icon { - color: var(--ion-color-dark-contrast) !important; -} - -.small-caps { - font-variant-caps: all-small-caps; -} - -ion-grid { - padding-top: 32px; +html, +body { height: 100%; - max-width: 695px; } -ion-row { - height: 90%; +app-root { + display: block; + height: 100%; } -ion-card { - border-radius: 31px; +tui-root { + height: 100%; } -ion-item { - --highlight-color-valid: transparent; - --highlight-color-invalid: transparent; -} - -ion-avatar { - width: 27px; - height: 27px; -} - -ion-toast { - --background: var(--ion-color-light); - --button-color: var(--ion-color-dark); - --border-style: solid; - --border-width: 1px; - --color: white; -} - -.center-spinner { - height: 6vh; - width: 100%; -} - -.inline { - * { - display: inline-block; - vertical-align: middle; - padding-left: 0px 0.3rem; - } -} - -.claim-button { - margin-inline-start: 0; - margin-inline-end: 0; - margin-top: 24px; - min-width: 140px; -} - -.error-toast { - --border-color: var(--ion-color-danger); - width: 40%; - min-width: 400px; - --end: 8px; - right: 8px; - left: unset; - top: 64px; -} - -.error-border { - border: 2px solid var(--ion-color-danger); - border-radius: 4px; -} - -.success-border { - border: 2px solid var(--ion-color-success); - border-radius: 4px; -} - -.sc-ion-label-md-s p { - line-height: 23px; -} - -ion-button { - --padding-top: 1.3rem; - --padding-bottom: 1.3rem; -} - -ion-item { - border: var(--ion-color-step-750) 1px solid; - margin: 2rem; - --background: transparent; - --border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13)))); - transition: all 350ms ease; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - transition-property: transform; - transform: scale(1.05); - transition-delay: 40ms; - } - - ion-button { - --padding-top: unset; - --padding-bottom: unset; - } -} - -.item.sc-ion-label-md-h, -.item .sc-ion-label-md-h { - white-space: normal; -} - -.center-wrapper { +router-outlet + * { + height: 100%; + max-width: min(35rem, 100vw); display: flex; - justify-content: center; + flex-direction: column; align-items: center; - height: 100%; -} + box-sizing: border-box; + padding: 2rem; + margin: 0 auto; - -.loader { - --spinner-color: var(--ion-color-tertiary) !important; -} - -.toolbar-background { - background: #2a2a2a !important; -} - -.toolbar-container { - padding-right: 2rem !important; -} - -ion-header { - ion-toolbar { - --border-color: var(--ion-color-step-950); - --border-width: 0 0 1px 0; - - --min-height: 80px; - --padding-top: 20px; - --padding-bottom: 20px; - --padding-end: 2rem; + [tuiCardLarge] { + width: 100%; + background: var(--tui-base-02); + margin: auto; } } -ion-footer { - ion-toolbar { - --border-width: 0; - --padding-end: 2.3rem; - --padding-bottom: 2rem; +button:disabled { + opacity: var(--tui-disabled-opacity); + pointer-events: none; +} + +header { + position: relative; + display: flex; + flex-direction: column; + text-align: center; + font: var(--tui-font-heading-4); + + p { + font: var(--tui-font-text-m); + color: var(--tui-text-02); } } -.footer-md::before { - content: none; +h2 { + margin: 0; + font: var(--tui-font-heading-6); } -@media (max-width: 500px) { - h1 { - font-size: 36px; - } - - ion-item { - margin: 0 0.5rem 2rem 0.5rem; - } +.g-success { + color: var(--tui-success-fill); } -p a { - color: var(--ion-text-color); - // text-decoration: none; - font-weight: 600; - text-underline-offset: 0.4rem; +.g-warning { + color: var(--tui-warning-fill); +} + +.g-error { + color: var(--tui-error-fill); +} + +.g-info { + color: var(--tui-info-fill); } diff --git a/web/projects/shared/package.json b/web/projects/shared/package.json index b0063e302..77f70e0b7 100644 --- a/web/projects/shared/package.json +++ b/web/projects/shared/package.json @@ -5,12 +5,12 @@ "@angular/common": "^17.0.6", "@angular/core": "^17.0.6", "@angular/router": "^17.0.6", - "@ionic/angular": ">=6.0.0", "@ng-web-apis/mutation-observer": ">=2.0.0", "@ng-web-apis/resize-observer": ">=2.0.0", "@start9labs/emver": "^0.1.5", "@taiga-ui/cdk": ">=3.0.0", "@taiga-ui/core": ">=3.0.0", + "@taiga-ui/experimental": ">=3.0.0", "@tinkoff/ng-dompurify": ">=4.0.0", "ansi-to-html": "^0.7.2" }, diff --git a/web/projects/shared/src/components/drive.component.ts b/web/projects/shared/src/components/drive.component.ts new file mode 100644 index 000000000..59ff90727 --- /dev/null +++ b/web/projects/shared/src/components/drive.component.ts @@ -0,0 +1,29 @@ +import { Component, Input } from '@angular/core' +import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental' +import { UnitConversionPipesModule } from '../pipes/unit-conversion/unit-conversion.module' + +@Component({ + standalone: true, + selector: 'button[drive]', + template: ` + + + {{ drive.logicalname }} + + {{ drive.vendor || 'Unknown Vendor' }} - + {{ drive.model || 'Unknown Model' }} + + Capacity: {{ drive.capacity | convertBytes }} + + + `, + imports: [TuiIconModule, TuiTitleModule, UnitConversionPipesModule], +}) +export class DriveComponent { + @Input() drive!: { + logicalname: string | null + vendor: string | null + model: string | null + capacity: number + } +} diff --git a/web/projects/shared/src/components/initializing/initializing.component.html b/web/projects/shared/src/components/initializing/initializing.component.html deleted file mode 100644 index 836caf38c..000000000 --- a/web/projects/shared/src/components/initializing/initializing.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
-

Initializing StartOS

-
- Progress: {{ (progress * 100).toFixed(0) }}% -
- - -

{{ getMessage(progress) }}

-
- - diff --git a/web/projects/shared/src/components/initializing/initializing.component.scss b/web/projects/shared/src/components/initializing/initializing.component.scss deleted file mode 100644 index 0675add7d..000000000 --- a/web/projects/shared/src/components/initializing/initializing.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -.card { - border-radius: 0.25rem; - padding: 1rem; - margin: 1.5rem; - text-align: center; - // TODO: Theme - background: #e0e0e0; - color: #333; - --tui-clear-inverse: rgba(0, 0, 0, 0.1); -} - -.title { - font-size: 2.5rem; - margin: 1rem; -} - -.progress { - max-width: 40rem; - margin: 1rem auto; -} - -.logs { - display: flex; - flex-direction: column; - height: 18rem; - padding: 1rem; - margin: 0 1.5rem auto; - text-align: left; - overflow: hidden; - border-radius: 2rem; - // TODO: Theme - background: #181818; -} diff --git a/web/projects/shared/src/components/initializing/initializing.component.ts b/web/projects/shared/src/components/initializing/initializing.component.ts index e72cecb9e..5fc86f85d 100644 --- a/web/projects/shared/src/components/initializing/initializing.component.ts +++ b/web/projects/shared/src/components/initializing/initializing.component.ts @@ -1,11 +1,67 @@ -import { Component, inject, Input, Output } from '@angular/core' +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + Output, +} from '@angular/core' +import { TuiLetModule } from '@taiga-ui/cdk' +import { TuiProgressModule } 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', - templateUrl: 'initializing.component.html', - styleUrls: ['initializing.component.scss'], + template: ` +
+

+ Initializing StartOS +

+
+ Progress: {{ (progress * 100).toFixed(0) }}% +
+ + +

{{ getMessage(progress) }}

+
+ + `, + styles: ` + section { + border-radius: 0.25rem; + padding: 1rem; + margin: 1.5rem; + text-align: center; + // TODO: Theme + background: #e0e0e0; + color: #333; + --tui-clear-inverse: rgba(0, 0, 0, 0.1); + } + + logs-window { + display: flex; + flex-direction: column; + height: 18rem; + padding: 1rem; + margin: 0 1.5rem auto; + text-align: left; + overflow: hidden; + border-radius: 2rem; + // TODO: Theme + background: #181818; + } + `, + imports: [CommonModule, LogsWindowComponent, TuiLetModule, TuiProgressModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class InitializingComponent { readonly progress$ = inject(SetupService) diff --git a/web/projects/shared/src/components/initializing/initializing.module.ts b/web/projects/shared/src/components/initializing/initializing.module.ts deleted file mode 100644 index 93c5c99d4..000000000 --- a/web/projects/shared/src/components/initializing/initializing.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { TuiLetModule } from '@taiga-ui/cdk' -import { TuiProgressModule } from '@taiga-ui/kit' - -import { LogsWindowComponent } from './logs-window.component' -import { InitializingComponent } from './initializing.component' - -@NgModule({ - imports: [CommonModule, TuiLetModule, LogsWindowComponent, TuiProgressModule], - declarations: [InitializingComponent], - exports: [InitializingComponent], -}) -export class InitializingModule {} diff --git a/web/projects/shared/src/components/text-spinner/text-spinner.component.html b/web/projects/shared/src/components/text-spinner/text-spinner.component.html deleted file mode 100644 index 789f4a120..000000000 --- a/web/projects/shared/src/components/text-spinner/text-spinner.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - -

{{ text }}

-
-
-
diff --git a/web/projects/shared/src/components/text-spinner/text-spinner.component.module.ts b/web/projects/shared/src/components/text-spinner/text-spinner.component.module.ts deleted file mode 100644 index dacc85ffa..000000000 --- a/web/projects/shared/src/components/text-spinner/text-spinner.component.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { TextSpinnerComponent } from './text-spinner.component' - -@NgModule({ - declarations: [TextSpinnerComponent], - imports: [CommonModule, IonicModule], - exports: [TextSpinnerComponent], -}) -export class TextSpinnerComponentModule {} diff --git a/web/projects/shared/src/components/text-spinner/text-spinner.component.scss b/web/projects/shared/src/components/text-spinner/text-spinner.component.scss deleted file mode 100644 index 56fec7993..000000000 --- a/web/projects/shared/src/components/text-spinner/text-spinner.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.full-height { - height: 100%; -} diff --git a/web/projects/shared/src/components/text-spinner/text-spinner.component.ts b/web/projects/shared/src/components/text-spinner/text-spinner.component.ts deleted file mode 100644 index f43b451ad..000000000 --- a/web/projects/shared/src/components/text-spinner/text-spinner.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component, Input } from '@angular/core' - -@Component({ - selector: 'text-spinner', - templateUrl: './text-spinner.component.html', - styleUrls: ['./text-spinner.component.scss'], -}) -export class TextSpinnerComponent { - @Input() text = '' -} diff --git a/web/projects/shared/src/directives/responsive-col.directive.ts b/web/projects/shared/src/directives/responsive-col.directive.ts deleted file mode 100644 index 56e92627d..000000000 --- a/web/projects/shared/src/directives/responsive-col.directive.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - Directive, - OnInit, - Optional, - ElementRef, - Inject, - InjectionToken, - Input, - NgZone, -} from '@angular/core' -import { ResizeObserverService } from '@ng-web-apis/resize-observer' -import { distinctUntilChanged, map, Observable } from 'rxjs' -import { tuiZonefree, TuiDestroyService } from '@taiga-ui/cdk' -import { IonCol } from '@ionic/angular' -import { takeUntil } from 'rxjs' - -export type Step = 'xs' | 'sm' | 'md' | 'lg' | 'xl' - -const SIZE: readonly Step[] = ['xl', 'lg', 'md', 'sm', 'xs'] - -/** - * Not exported: - * https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/media.ts - * - * export const SIZE_TO_MEDIA: any = { - * xs: '(min-width: 0px)', - * sm: '(min-width: 576px)', - * md: '(min-width: 768px)', - * lg: '(min-width: 992px)', - * xl: '(min-width: 1200px)', - * }; - */ -export const BREAKPOINTS = new InjectionToken( - 'BREAKPOINTS', - { - factory: () => [ - [1200, 'xl'], - [992, 'lg'], - [768, 'md'], - [576, 'sm'], - [0, 'xs'], - ], - }, -) - -@Directive({ - selector: '[responsiveColViewport]', - exportAs: 'viewport', - providers: [ResizeObserverService], - standalone: true, -}) -export class ResponsiveColViewportDirective extends Observable { - @Input() - responsiveColViewport: Observable | '' = '' - - constructor( - @Inject(BREAKPOINTS) - private readonly breakpoints: readonly [number, Step][], - private readonly resize$: ResizeObserverService, - private readonly elementRef: ElementRef, - private readonly zone: NgZone, - ) { - super(subscriber => - (this.responsiveColViewport || this.stream$).subscribe(subscriber), - ) - } - - private readonly stream$ = this.resize$.pipe( - map(() => this.elementRef.nativeElement.clientWidth), - map(width => this.breakpoints.find(([step]) => width >= step)?.[1] || 'xs'), - distinctUntilChanged(), - tuiZonefree(this.zone), - ) -} - -@Directive({ - selector: 'ion-col[responsiveCol]', - providers: [TuiDestroyService], - standalone: true, -}) -export class ResponsiveColDirective implements OnInit { - readonly size: Record = { - xs: '12', - sm: '6', - md: '4', - lg: '3', - xl: '2', - } - - constructor( - @Optional() - viewport$: ResponsiveColViewportDirective | null, - destroy$: TuiDestroyService, - private readonly col: IonCol, - ) { - viewport$?.pipe(takeUntil(destroy$)).subscribe(size => { - const max = this.size[size] || this.findMax(size) - - this.col.sizeLg = max - this.col.sizeMd = max - this.col.sizeSm = max - this.col.sizeXl = max - this.col.sizeXs = max - }) - } - - ngOnInit() { - this.size.lg = this.col.sizeLg - this.size.md = this.col.sizeMd - this.size.sm = this.col.sizeSm - this.size.xl = this.col.sizeXl - this.size.xs = this.col.sizeXs - } - - private findMax(current: Step): string | undefined { - const start = SIZE.indexOf(current) - 1 - const max = SIZE.find((size, i) => i > start && this.size[size]) || current - - return this.size[max] - } -} diff --git a/web/projects/shared/src/pipes/guid/guid.module.ts b/web/projects/shared/src/pipes/guid/guid.module.ts deleted file mode 100644 index bd4e22f5f..000000000 --- a/web/projects/shared/src/pipes/guid/guid.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NgModule } from '@angular/core' -import { GuidPipe } from './guid.pipe' - -@NgModule({ - declarations: [GuidPipe], - exports: [GuidPipe], -}) -export class GuidPipePipesModule {} diff --git a/web/projects/shared/src/pipes/guid/guid.pipe.ts b/web/projects/shared/src/pipes/guid/guid.pipe.ts deleted file mode 100644 index a095bd929..000000000 --- a/web/projects/shared/src/pipes/guid/guid.pipe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { DiskInfo } from '../../types/api' - -@Pipe({ - name: 'guid', -}) -export class GuidPipe implements PipeTransform { - transform(disk: DiskInfo): string | null { - return disk.guid || disk.partitions.find(p => p.guid)?.guid || null - } -} diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index f926e0da9..61d4e76fd 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -6,20 +6,17 @@ export * from './classes/http-error' export * from './classes/rpc-error' export * from './components/initializing/logs-window.component' -export * from './components/initializing/initializing.module' export * from './components/initializing/initializing.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' -export * from './components/text-spinner/text-spinner.component' -export * from './components/text-spinner/text-spinner.component.module' export * from './components/ticker/ticker.component' export * from './components/ticker/ticker.module' +export * from './components/drive.component' export * from './directives/drag-scroller.directive' -export * from './directives/responsive-col.directive' export * from './directives/safe-links.directive' export * from './directives/enter/enter.directive' export * from './directives/enter/enter.module' @@ -28,8 +25,6 @@ export * from './mocks/get-setup-status' export * from './pipes/emver/emver.module' export * from './pipes/emver/emver.pipe' -export * from './pipes/guid/guid.module' -export * from './pipes/guid/guid.pipe' export * from './pipes/markdown/markdown.module' export * from './pipes/markdown/markdown.pipe' export * from './pipes/shared/shared.module' @@ -70,5 +65,6 @@ export * from './util/get-pkg-id' export * from './util/invert' export * from './util/misc.util' export * from './util/rpc.util' +export * from './util/to-guid' export * from './util/to-local-iso-string' export * from './util/unused' diff --git a/web/projects/shared/src/util/to-guid.ts b/web/projects/shared/src/util/to-guid.ts new file mode 100644 index 000000000..a58c02ca4 --- /dev/null +++ b/web/projects/shared/src/util/to-guid.ts @@ -0,0 +1,5 @@ +import { DiskInfo } from '../types/api' + +export function toGuid(disk: DiskInfo | null): string | null { + return disk?.guid || disk?.partitions.find(p => p.guid)?.guid || null +} diff --git a/web/projects/ui/src/app/app.component.html b/web/projects/ui/src/app/app.component.html index f26e0cee2..c588d56ac 100644 --- a/web/projects/ui/src/app/app.component.html +++ b/web/projects/ui/src/app/app.component.html @@ -56,14 +56,11 @@ diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index 4ebc57315..9a42a28e4 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -10,7 +10,6 @@ import { LightThemeModule, LoadingModule, MarkdownModule, - ResponsiveColViewportDirective, SharedPipesModule, } from '@start9labs/shared' import { @@ -57,7 +56,6 @@ import { RoutingModule } from './routing.module' TuiAlertModule, TuiModeModule, TuiThemeNightModule, - ResponsiveColViewportDirective, DarkThemeModule, LightThemeModule, ServiceWorkerModule.register('ngsw-worker.js', { diff --git a/web/projects/ui/src/app/apps/loading/loading.page.ts b/web/projects/ui/src/app/apps/loading/loading.page.ts index da6970de8..028c5f1a4 100644 --- a/web/projects/ui/src/app/apps/loading/loading.page.ts +++ b/web/projects/ui/src/app/apps/loading/loading.page.ts @@ -1,7 +1,7 @@ import { Component, inject } from '@angular/core' import { NavController } from '@ionic/angular' import { - InitializingModule, + InitializingComponent, provideSetupLogsService, provideSetupService, } from '@start9labs/shared' @@ -20,7 +20,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' provideSetupService(ApiService), provideSetupLogsService(ApiService), ], - imports: [InitializingModule], + imports: [InitializingComponent], }) export class LoadingPage { readonly navCtrl = inject(NavController) diff --git a/web/projects/ui/src/app/apps/portal/components/header/snek.component.ts b/web/projects/ui/src/app/apps/portal/components/header/snek.component.ts index 17caa60d5..1f6ba9771 100644 --- a/web/projects/ui/src/app/apps/portal/components/header/snek.component.ts +++ b/web/projects/ui/src/app/apps/portal/components/header/snek.component.ts @@ -45,10 +45,8 @@ import { TuiDialogContext } from '@taiga-ui/core' }) export class HeaderSnekComponent implements AfterViewInit, OnDestroy { private readonly document = inject(DOCUMENT) - private readonly dialog = inject(POLYMORPHEUS_CONTEXT) as TuiDialogContext< - number, - number - > + private readonly dialog = + inject>(POLYMORPHEUS_CONTEXT) highScore: number = this.dialog.data score = 0 diff --git a/web/projects/ui/src/app/common/logs/logs.component.html b/web/projects/ui/src/app/common/logs/logs.component.html deleted file mode 100644 index f786ef79e..000000000 --- a/web/projects/ui/src/app/common/logs/logs.component.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - {{ pageTitle }} - - - - - - - - - - -
-
-
- - -
-

- Reconnecting -

-

- Waiting for network connectivity -

-
- - - -
-
-
- - - -
- -

Autoscroll

-
- - Download - - -
-
diff --git a/web/projects/ui/src/app/common/logs/logs.component.module.ts b/web/projects/ui/src/app/common/logs/logs.component.module.ts deleted file mode 100644 index f00ff7660..000000000 --- a/web/projects/ui/src/app/common/logs/logs.component.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { LogsComponent } from './logs.component' -import { FormsModule } from '@angular/forms' -import { TextSpinnerComponentModule } from '@start9labs/shared' - -@NgModule({ - declarations: [LogsComponent], - imports: [CommonModule, IonicModule, TextSpinnerComponentModule, FormsModule], - exports: [LogsComponent], -}) -export class LogsComponentModule {} diff --git a/web/projects/ui/src/app/common/logs/logs.component.scss b/web/projects/ui/src/app/common/logs/logs.component.scss deleted file mode 100644 index 1e73b9928..000000000 --- a/web/projects/ui/src/app/common/logs/logs.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -#container { - padding-bottom: 16px; - font-family: monospace; - white-space: pre-line; -} diff --git a/web/projects/ui/src/app/common/logs/logs.component.ts b/web/projects/ui/src/app/common/logs/logs.component.ts deleted file mode 100644 index 7cc250173..000000000 --- a/web/projects/ui/src/app/common/logs/logs.component.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core' -import { IonContent } from '@ionic/angular' -import { - bufferTime, - catchError, - filter, - finalize, - from, - Observable, - switchMap, - takeUntil, - tap, -} from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' -import { - FetchLogsReq, - FetchLogsRes, - toLocalIsoString, - Log, - DownloadHTMLService, - LoadingService, - ErrorService, -} from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { RR } from 'src/app/services/api/api.types' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConnectionService } from 'src/app/services/connection.service' - -var Convert = require('ansi-to-html') -var convert = new Convert({ - newline: true, - bg: 'transparent', - colors: { - 4: 'Cyan', - }, - escapeXML: true, -}) - -@Component({ - selector: 'logs', - templateUrl: './logs.component.html', - styleUrls: ['./logs.component.scss'], - providers: [TuiDestroyService], -}) -export class LogsComponent { - @ViewChild(IonContent) - private content?: IonContent - - @Input({ required: true }) followLogs!: ( - params: RR.FollowServerLogsReq, - ) => Promise - @Input({ required: true }) fetchLogs!: ( - params: FetchLogsReq, - ) => Promise - @Input({ required: true }) context!: string - @Input() defaultBack = '' - @Input() pageTitle = '' - - loading = true - infiniteStatus: 0 | 1 | 2 = 0 - startCursor?: string - isOnBottom = true - autoScroll = true - websocketStatus: - | 'connecting' - | 'connected' - | 'reconnecting' - | 'disconnected' = 'connecting' - limit = 400 - count = 0 - - constructor( - private readonly errorService: ErrorService, - private readonly destroy$: TuiDestroyService, - private readonly api: ApiService, - private readonly loader: LoadingService, - private readonly downloadHtml: DownloadHTMLService, - private readonly connectionService: ConnectionService, - ) {} - - async ngOnInit() { - from(this.followLogs({ limit: this.limit })) - .pipe( - switchMap(({ 'start-cursor': startCursor, guid }) => { - this.startCursor = startCursor - return this.connect$(guid) - }), - takeUntil(this.destroy$), - finalize(() => console.log('CLOSING')), - ) - .subscribe() - } - - async doInfinite(e: any): Promise { - try { - const res = await this.fetchLogs({ - cursor: this.startCursor, - before: true, - limit: this.limit, - }) - - this.processRes(res) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - e.target.complete() - } - } - - handleScroll(e: any) { - if (e.detail.deltaY < -50) this.autoScroll = false - } - - handleScrollEnd() { - const bottomDiv = document.getElementById('bottom-div') - this.isOnBottom = - !!bottomDiv && - bottomDiv.getBoundingClientRect().top - 420 < window.innerHeight - } - - scrollToBottom() { - this.content?.scrollToBottom(200) - } - - async download() { - const loader = this.loader.open('Processing 10,000 logs...').subscribe() - - try { - const { entries } = await this.fetchLogs({ - before: true, - limit: 10000, - }) - - const styles = { - 'background-color': '#222428', - color: '#e0e0e0', - 'font-family': 'monospace', - } - const html = this.convertToAnsi(entries) - - this.downloadHtml.download(`${this.context}-logs.html`, html, styles) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } - - private reconnect$(): Observable { - return from(this.followLogs({})).pipe( - tap(_ => this.recordConnectionChange()), - switchMap(({ guid }) => this.connect$(guid, true)), - ) - } - - private connect$(guid: string, reconnect = false) { - const config: WebSocketSubjectConfig = { - url: `/rpc/${guid}`, - openObserver: { - next: () => { - this.websocketStatus = 'connected' - }, - }, - } - - return this.api.openLogsWebsocket$(config).pipe( - tap(_ => this.count++), - bufferTime(1000), - tap(msgs => { - this.loading = false - this.processRes({ entries: msgs }) - if (this.infiniteStatus === 0 && this.count >= this.limit) - this.infiniteStatus = 1 - }), - catchError(() => { - this.recordConnectionChange(false) - return this.connectionService.connected$.pipe( - tap( - connected => - (this.websocketStatus = connected - ? 'reconnecting' - : 'disconnected'), - ), - filter(Boolean), - switchMap(() => this.reconnect$()), - ) - }), - ) - } - - private recordConnectionChange(success = true) { - const container = document.getElementById('container') - const elem = document.getElementById('template')?.cloneNode() - if (!(elem instanceof HTMLElement)) return - elem.innerHTML = `
${ - success ? 'Reconnected' : 'Disconnected' - } at ${toLocalIsoString(new Date())}
` - container?.append(elem) - if (this.isOnBottom) { - setTimeout(() => { - this.scrollToBottom() - }, 25) - } - } - - private processRes(res: FetchLogsRes) { - const { entries, 'start-cursor': startCursor } = res - - if (!entries.length) return - - const container = document.getElementById('container') - const newLogs = document.getElementById('template')?.cloneNode() - - if (!(newLogs instanceof HTMLElement)) return - - newLogs.innerHTML = this.convertToAnsi(entries) - - // if response contains a startCursor, it means we are scrolling backwards - if (startCursor) { - this.startCursor = startCursor - - const beforeContainerHeight = container?.scrollHeight || 0 - container?.prepend(newLogs) - const afterContainerHeight = container?.scrollHeight || 0 - - // maintain scroll height - setTimeout(() => { - this.content?.scrollToPoint( - 0, - afterContainerHeight - beforeContainerHeight, - ) - }, 25) - - if (entries.length < this.limit) { - this.infiniteStatus = 2 - } - } else { - container?.append(newLogs) - if (this.autoScroll) { - setTimeout(() => { - this.scrollToBottom() - }, 25) - } - } - } - - private convertToAnsi(entries: Log[]) { - return entries - .map( - entry => - `${toLocalIsoString( - new Date(entry.timestamp), - )}  ${convert.toHtml(entry.message)}`, - ) - .join('
') - } -} diff --git a/web/projects/ui/src/app/common/widget-list/widget-list.component.module.ts b/web/projects/ui/src/app/common/widget-list/widget-list.component.module.ts index 0fdec7d01..b75bad20b 100644 --- a/web/projects/ui/src/app/common/widget-list/widget-list.component.module.ts +++ b/web/projects/ui/src/app/common/widget-list/widget-list.component.module.ts @@ -2,14 +2,13 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule } from '@angular/router' -import { ResponsiveColDirective } from '@start9labs/shared' import { AnyLinkComponent } from './any-link/any-link.component' import { WidgetListComponent } from './widget-list.component' import { WidgetCardComponent } from './widget-card/widget-card.component' @NgModule({ declarations: [WidgetListComponent, WidgetCardComponent, AnyLinkComponent], - imports: [CommonModule, IonicModule, RouterModule, ResponsiveColDirective], + imports: [CommonModule, IonicModule, RouterModule], exports: [WidgetListComponent], }) export class WidgetListComponentModule {}