setup wizard first draft

This commit is contained in:
Drew Ansbacher
2021-06-29 17:02:11 -06:00
committed by Aiden McClelland
parent 77340ce769
commit 834abdc330
43 changed files with 33533 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'wizard',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule)
},
{
path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule)
},
{
path: 'password',
loadChildren: () => import('./pages/password/password.module').then( m => m.PasswordPageModule)
},
{
path: '',
redirectTo: 'wizard',
pathMatch: 'full'
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@@ -0,0 +1,11 @@
<ion-app>
<ion-content *ngIf="stateService.loading; else loaded">
<h2 color="light">Connecting to Embassy</h2>
</ion-content>
<ng-template #loaded>
<ion-content *ngIf="error">
<h2 color="light">{{ error }}</h2>
</ion-content>
<ion-router-outlet *ngIf="!error"></ion-router-outlet>
</ng-template>
</ion-app>

View File

View File

@@ -0,0 +1,51 @@
import { Component } from '@angular/core';
import { NavController, ToastController } from '@ionic/angular';
import { ApiService } from './services/api/api.service';
import { StateService } from './services/state.sevice';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(
private readonly apiService: ApiService,
private readonly stateService: StateService,
private readonly navCtrl: NavController,
public toastController: ToastController
) {
this.apiService.watchError$.subscribe(error => {
if(error) {
this.presentToast(error)
}
})
}
async ngOnInit() {
await this.stateService.getState()
if(this.stateService.hasPassword) {
//redirect to embassyOS
} else if (this.stateService.recoveryDrive) {
await this.navCtrl.navigateForward(`/recover`)
}
}
async presentToast(error: string) {
const toast = await this.toastController.create({
header: 'Error',
message: error,
position: 'bottom',
buttons: [
{
text: 'X',
role: 'cancel',
}
]
});
await toast.present();
await toast.onDidDismiss();
}
}

View File

@@ -0,0 +1,33 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { ApiService } from './services/api/api.service'
import { MockApiService } from './services/api/mock-api.service'
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import * as config from './config/config'
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
HttpClientModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService ,
useFactory: () => {
return new MockApiService()
},
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,7 @@
export const config: Config = {
"useMocks": true
}
interface Config {
useMocks: boolean
}

View File

@@ -0,0 +1,16 @@
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 {}

View File

@@ -0,0 +1,19 @@
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 { HomePageRoutingModule } from './home-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
HomePageRoutingModule
],
declarations: [HomePage]
})
export class HomePageModule {}

View File

@@ -0,0 +1,52 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Setup Wizard
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<div *ngIf="!stateService.selectedDataDrive">
<h2 color="light">Select Data Drive</h2>
<ion-card
*ngFor="let drive of dataDrives"
(click)="selectDrive(drive['logical-name'])"
button="true"
[class.selected]="selectedDrive === drive['logical-name']"
color="light"
>
<ion-card-header>
<ion-card-title>{{drive["logical-name"]}}</ion-card-title>
<ion-card-subtitle>{{drive.labels}}</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
Currently using {{drive.used}} out of {{drive.capacity}} bytes.
</ion-card-content>
</ion-card>
<ion-button (click)="warn()" color="primary" [disabled]="!selectedDrive">Next</ion-button>
</div>
<div *ngIf="stateService.selectedDataDrive">
<ion-card
[routerLink]="['/password']"
button="true"
color="light"
>
<ion-card-header>
<ion-card-title>Start Fresh</ion-card-title>
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-card
[routerLink]="['/recover']"
button="true"
color="light"
>
<ion-card-header>
<ion-card-title>Recover</ion-card-title>
<ion-card-subtitle>Recover the data from an old embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
</div>
</ion-content>

View File

@@ -0,0 +1,31 @@
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
.selected {
border: 1px solid white;
}

View File

@@ -0,0 +1,66 @@
import { Component } from '@angular/core'
import { AlertController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.sevice'
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
dataDrives = []
selectedDrive = null
constructor(
private readonly apiService: ApiService,
private readonly stateService: StateService,
public alertController: AlertController,
private readonly navCtrl: NavController,
) {}
async ngOnInit() {
if(!this.stateService.selectedDataDrive) {
this.dataDrives = await this.apiService.getStorageDisks()
}
}
selectDrive(name: string) {
if (name === this.selectedDrive) {
this.selectedDrive = null
} else {
this.selectedDrive = name
}
}
async warn() {
const alert = await this.alertController.create({
cssClass: 'my-custom-class',
header: 'Warning!',
message: 'This disk will be entirely wiped of all memory.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary',
handler: () => {
this.selectedDrive = null
}
}, {
text: 'Okay',
handler: async () => {
await this.chooseDisk()
}
}
]
});
await alert.present();
}
async chooseDisk() {
await this.apiService.selectStorageDisk({logicalName: this.selectedDrive})
this.stateService.selectedDataDrive = this.selectedDrive
}
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PasswordPage } from './password.page';
const routes: Routes = [
{
path: '',
component: PasswordPage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PasswordPageRoutingModule {}

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { PasswordPage } from './password.page'
import { PasswordPageRoutingModule } from './password-routing.module'
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
PasswordPageRoutingModule
],
declarations: [PasswordPage]
})
export class PasswordPageModule {}

View File

@@ -0,0 +1,20 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Setup Wizard
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true" color="light">
<div *ngIf="stateService.recoveryDrive">
<h2>Enter Password</h2>
<ion-input type="password" [(ngModel)]="password">Password:</ion-input>
</div>
<div *ngIf="!stateService.recoveryDrive">
<h2>Create Password</h2>
<ion-input type="password" [(ngModel)]="password">Password:</ion-input>
<ion-input type="password" [(ngModel)]="passwordVer">Verify Password:</ion-input>
</div>
<p>{{error}}</p>
<ion-button (click)="submitPassword()" [disabled]="!password">Submit</ion-button>
</ion-content>

View File

@@ -0,0 +1,31 @@
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
.selected {
border: 1px solid white;
}

View File

@@ -0,0 +1,33 @@
import { Component } from '@angular/core'
import { AlertController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.sevice'
@Component({
selector: 'password-page',
templateUrl: 'password.page.html',
styleUrls: ['password.page.scss'],
})
export class PasswordPage {
error = ''
password = ''
passwordVer = ''
constructor(
private readonly apiService: ApiService,
private readonly stateService: StateService,
public alertController: AlertController,
) {}
async submitPassword () {
if (!this.stateService.recoveryDrive && this.password !== this.passwordVer) {
console.log('here')
this.error="*passwords dont match"
} else {
console.log('submitting')
await this.apiService.submitPassword(this.password)
// @Todo navigate to embassy os
}
}
}

View File

@@ -0,0 +1,16 @@
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 {}

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RecoverPage } from './recover.page';
import { RecoverPageRoutingModule } from './recover-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RecoverPageRoutingModule
],
declarations: [RecoverPage]
})
export class RecoverPageModule {}

View File

@@ -0,0 +1,40 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Setup Wizard
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<div *ngIf="!stateService.recoveryDrive">
<h2 color="light">Select Data Drive</h2>
<ion-card
*ngFor="let drive of dataDrives"
(click)="selectDrive(drive['logical-name'])"
button="true"
[class.selected]="selectedDrive === drive['logical-name']"
color="light"
>
<ion-card-header>
<ion-card-title>{{drive["logical-name"]}}</ion-card-title>
<ion-card-subtitle>{{drive.name}}</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
Currently running {{drive.version}}
</ion-card-content>
</ion-card>
<ion-button (click)="warn()" color="primary" [disabled]="!selectedDrive">Next</ion-button>
</div>
<div *ngIf="stateService.recoveryDrive">
<h2>Progress: {{ 100 * stateService.dataProgress }}% <ion-spinner *ngIf="stateService.dataProgress != 1"></ion-spinner> </h2>
<ion-progress-bar value="{{stateService.dataProgress}}"></ion-progress-bar>
<ion-button
[routerLink]="['/password']"
color="primary"
[disabled]="stateService.dataProgress != 1"
>
Next
</ion-button>
</div>
</ion-content>

View File

@@ -0,0 +1,31 @@
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
.selected {
border: 1px solid white;
}

View File

@@ -0,0 +1,68 @@
import { Component } from '@angular/core'
import { AlertController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.sevice'
@Component({
selector: 'recover-page',
templateUrl: 'recover.page.html',
styleUrls: ['recover.page.scss'],
})
export class RecoverPage {
dataDrives = []
selectedDrive = null
constructor(
private readonly apiService: ApiService,
private readonly stateService: StateService,
public alertController: AlertController,
) {}
async ngOnInit() {
if(!this.stateService.recoveryDrive) {
this.dataDrives = await this.apiService.getEmbassyDrives()
} else {
this.stateService.pollDataTransferProgress()
}
}
selectDrive(name: string) {
if (name === this.selectedDrive) {
this.selectedDrive = null
} else {
this.selectedDrive = name
}
}
async warn() {
const alert = await this.alertController.create({
cssClass: 'my-custom-class',
header: 'Warning!',
message: 'Once you select this the recovery process will begin.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary',
handler: () => {
this.selectedDrive = null
}
}, {
text: 'Okay',
handler: async () => {
await this.chooseDisk()
}
}
]
});
await alert.present();
}
async chooseDisk() {
await this.apiService.selectEmbassyDrive({logicalName: this.selectedDrive})
this.stateService.recoveryDrive = this.selectedDrive
this.stateService.pollDataTransferProgress()
}
}

View File

@@ -0,0 +1,37 @@
import { Subject } from "rxjs";
export abstract class ApiService {
protected error$: Subject<string> = new Subject()
watchError$ = this.error$.asObservable()
abstract getState (): Promise<State>
abstract getStorageDisks (): Promise<StorageDisk[]>
abstract selectStorageDisk (disk: { logicalName: string }): Promise<void>
abstract getEmbassyDrives (): Promise<EmbassyDrive[]>
abstract selectEmbassyDrive (disk: { logicalName: string }): Promise<void>
abstract getDataTransferProgress () : Promise<TransferProgress>
abstract submitPassword (password: string) : Promise<void>
}
export interface State {
'selected-data-drive': string | null,
'recovery-drive': string | null,
'has-password': boolean,
}
export interface TransferProgress {
'bytes-transfered': number
'total-bytes': number
}
export interface StorageDisk {
"logical-name": string
labels: string[]
capacity: number
used: number
}
export interface EmbassyDrive {
"logical-name": string
version: string
name: string
}

View File

@@ -0,0 +1,77 @@
import { Injectable } from "@angular/core";
import { pauseFor } from "../state.sevice";
import { ApiService } from "./api.service"
@Injectable({
providedIn: 'root'
})
export class MockApiService extends ApiService {
constructor () {
super()
}
async getState () {
await pauseFor(2000)
return {
'selected-data-drive': 'name1',
'recovery-drive': null,
'has-password': false,
'data-transfer-progress': null
}
}
async getDataTransferProgress () {
tries = Math.min(tries + 1, 4)
return {
'bytes-transfered': tries,
'total-bytes': 4
}
}
async getStorageDisks () {
await pauseFor(2000)
return [
{
"logical-name": 'name1',
labels: ['label 1', 'label 2'],
capacity: 1600,
used: 200,
},
{
"logical-name": 'name2',
labels: [],
capacity: 1600,
used: 0,
}
]
}
async selectStorageDisk(disk) {
await pauseFor(200)
return
}
async getEmbassyDrives () {
await pauseFor(2000)
return [
{
"logical-name": 'name1',
version: '0.2.3',
name: 'Embassy 0.2.3'
}
]
}
async selectEmbassyDrive(disk) {
await pauseFor(200)
return
}
async submitPassword(password) {
await pauseFor(200)
return
}
}
let tries = 4

View File

@@ -0,0 +1,51 @@
import { Injectable } from "@angular/core";
import { ApiService } from "./api/api.service"
@Injectable({
providedIn: 'root'
})
export class StateService {
loading = true
selectedDataDrive: string
recoveryDrive: string
hasPassword: Boolean
dataTransferProgress: { bytesTransfered: number, totalBytes: number } | null
dataProgress = 0
constructor (
private readonly apiService: ApiService
) {}
async getState () {
const state = await this.apiService.getState()
if(state) {
this.selectedDataDrive = state['selected-data-drive']
this.recoveryDrive = state['recovery-drive']
this.hasPassword = state['has-password']
this.loading = false
}
}
async pollDataTransferProgress () {
if (
this.dataTransferProgress?.totalBytes &&
this.dataTransferProgress.bytesTransfered === this.dataTransferProgress.totalBytes
) {return }
const progress = await this.apiService.getDataTransferProgress()
this.dataTransferProgress = {
bytesTransfered: progress['bytes-transfered'],
totalBytes: progress['total-bytes']
}
if (this.dataTransferProgress.totalBytes) {
this.dataProgress = this.dataTransferProgress.bytesTransfered / this.dataTransferProgress.totalBytes
}
await pauseFor(7000)
this.pollDataTransferProgress()
}
}
export function pauseFor (ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}