rename frontend to web

This commit is contained in:
Matt Hill
2023-11-13 15:59:16 -07:00
parent 09303ab2fb
commit 862ca375ee
869 changed files with 0 additions and 66 deletions

View File

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

View File

@@ -0,0 +1,6 @@
<tui-theme-night></tui-theme-night>
<tui-root tuiMode="onDark">
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
</tui-root>

View File

@@ -0,0 +1,8 @@
:host {
display: block;
height: 100%;
}
tui-root {
height: 100%;
}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor() {}
}

View File

@@ -0,0 +1,56 @@
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 {
LoadingModule,
RELATIVE_URL,
WorkspaceConfig,
} from '@start9labs/shared'
const {
useMocks,
ui: { api },
} = require('../../../../config.json') as WorkspaceConfig
@NgModule({
declarations: [AppComponent],
imports: [
HttpClientModule,
BrowserAnimationsModule,
IonicModule.forRoot({
mode: 'md',
}),
AppRoutingModule,
TuiRootModule,
TuiDialogModule,
LoadingModule,
TuiModeModule,
TuiThemeNightModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService,
useClass: useMocks ? MockApiService : LiveApiService,
},
{
provide: RELATIVE_URL,
useValue: `/${api.url}/${api.version}`,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
import { DiskInfo } from '@start9labs/shared'
export abstract class ApiService {
abstract getDisks(): Promise<GetDisksRes> // install.disk.list
abstract install(params: InstallReq): Promise<void> // install.execute
abstract reboot(): Promise<void> // install.reboot
}
export type GetDisksRes = DiskInfo[]
export type InstallReq = {
logicalname: string
overwrite: boolean
}

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { ApiService, GetDisksRes, InstallReq } from './api.service'
@Injectable()
export class LiveApiService implements ApiService {
constructor(private readonly http: HttpService) {}
async getDisks(): Promise<GetDisksRes> {
return this.rpcRequest({
method: 'install.disk.list',
params: {},
})
}
async install(params: InstallReq): Promise<void> {
return this.rpcRequest<void>({
method: 'install.execute',
params,
})
}
async reboot(): Promise<void> {
return this.rpcRequest<void>({
method: 'install.reboot',
params: {},
})
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.http.rpcRequest<T>(opts)
const rpcRes = res.body
if (isRpcError(rpcRes)) {
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
}

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@angular/core'
import { pauseFor } from '@start9labs/shared'
import { ApiService, GetDisksRes, InstallReq } from './api.service'
@Injectable()
export class MockApiService implements ApiService {
async getDisks(): Promise<GetDisksRes> {
await pauseFor(500)
return [
{
logicalname: 'abcd',
vendor: 'Samsung',
model: 'T5',
partitions: [
{
logicalname: 'pabcd',
label: null,
capacity: 73264762332,
used: null,
'embassy-os': {
version: '0.2.17',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': null,
},
guid: null,
},
],
capacity: 123456789123,
guid: 'uuid-uuid-uuid-uuid',
},
{
logicalname: 'dcba',
vendor: 'Crucial',
model: 'MX500',
partitions: [
{
logicalname: 'pbcba',
label: null,
capacity: 73264762332,
used: null,
'embassy-os': {
version: '0.3.3',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': null,
},
guid: null,
},
],
capacity: 124456789123,
guid: null,
},
{
logicalname: 'wxyz',
vendor: 'SanDisk',
model: 'Specialness',
partitions: [
{
logicalname: 'pbcba',
label: null,
capacity: 73264762332,
used: null,
'embassy-os': {
version: '0.3.2',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': null,
},
guid: 'guid-guid-guid-guid',
},
],
capacity: 123459789123,
guid: null,
},
]
}
async install(params: InstallReq): Promise<void> {
await pauseFor(1000)
}
async reboot(): Promise<void> {
await pauseFor(1000)
}
}