remove product key from setup flow (#1750)

* remove product key flow from setup

* feat: backend turned off encryption + new Id + no package id

* implement new encryption scheme in FE

* decode response string

* crypto not working

* update setup wizard closes #1762

* feat: Get the encryption key

* fix: Get to recovery

* remove old code

* fix build

* fix: Install works for now

* fix bug in config for adding new list items

* dismiss action modal on success

* clear button in config

* wip: Currently broken in avahi mdns

* include headers with req/res and refactor patchDB init and usage

* fix: Can now run in the main

* flatline on failed init

* update patch DB

* add last-wifi-region to data model even though not used by FE

* chore: Fix the start.

* wip: Fix wrong order for getting hostname before sql has been
created

* fix edge case where union keys displayed as new when not new

* fix: Can start

* last backup color, markdown links always new tab, fix bug with login

* refactor to remove WithRevision

* resolve circular dep issue

* update submodule

* fix patch-db

* update patchDB

* update patch again

* escape error

* decodeuricomponent

* increase proxy buffer size

* increase proxy buffer size

* fix nginx

Co-authored-by: BluJ <mogulslayer@gmail.com>
Co-authored-by: BluJ <dragondef@gmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2022-09-07 09:25:01 -06:00
committed by GitHub
parent 76682ebef0
commit 50111e37da
175 changed files with 11436 additions and 2906 deletions

View File

@@ -156,7 +156,6 @@ export class HomePage {
console.error(e)
}
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',

View File

@@ -1,46 +1,61 @@
import { Injectable } from '@angular/core'
import { HttpService } from '@start9labs/shared'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { ApiService, GetErrorRes } from './api.service'
import { LogsRes, ServerLogsReq } from '@start9labs/shared'
@Injectable()
export class LiveApiService extends ApiService {
constructor(private readonly http: HttpService) {
super()
}
export class LiveApiService implements ApiService {
constructor(private readonly http: HttpService) {}
getError(): Promise<GetErrorRes> {
return this.http.rpcRequest<GetErrorRes>({
return this.rpcRequest<GetErrorRes>({
method: 'diagnostic.error',
params: {},
})
}
restart(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.restart',
params: {},
})
}
forgetDrive(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.disk.forget',
params: {},
})
}
repairDisk(): Promise<void> {
return this.http.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'diagnostic.disk.repair',
params: {},
})
}
getLogs(params: ServerLogsReq): Promise<LogsRes> {
return this.http.rpcRequest<LogsRes>({
return this.rpcRequest<LogsRes>({
method: 'diagnostic.logs',
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

@@ -4,11 +4,7 @@ import { ApiService, GetErrorRes } from './api.service'
import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared'
@Injectable()
export class MockApiService extends ApiService {
constructor() {
super()
}
export class MockApiService implements ApiService {
async getError(): Promise<GetErrorRes> {
await pauseFor(1000)
return {

View File

@@ -50,7 +50,6 @@ export class AdditionalComponent {
{
text: 'Ok',
handler: (version: string) => this.version.emit(version),
cssClass: 'enter-click',
},
],
})

View File

@@ -1,45 +1,32 @@
import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
const routes: Routes = [
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
{
path: 'product-key',
loadChildren: () =>
import('./pages/product-key/product-key.module').then(
m => m.ProductKeyPageModule,
),
},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
loadChildren: () =>
import('./pages/home/home.module').then(m => m.HomePageModule),
canActivate: [NavGuard],
},
{
path: 'recover',
loadChildren: () =>
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
canActivate: [RecoveryNavGuard],
},
{
path: 'embassy',
loadChildren: () =>
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
canActivate: [NavGuard],
},
{
path: 'loading',
loadChildren: () =>
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
canActivate: [NavGuard],
},
{
path: 'success',
loadChildren: () =>
import('./pages/success/success.module').then(m => m.SuccessPageModule),
canActivate: [NavGuard],
},
]

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from './services/state.service'
@Component({
selector: 'app-root',
@@ -14,21 +13,12 @@ export class AppComponent {
private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService,
private readonly navCtrl: NavController,
private readonly stateService: StateService,
) {}
async ngOnInit() {
try {
const status = await this.apiService.getStatus()
if (status.migrating || status['product-key']) {
this.stateService.hasProductKey = true
this.stateService.isMigrating = status.migrating
await this.navCtrl.navigateForward(`/product-key`)
} else {
this.stateService.hasProductKey = false
this.stateService.isMigrating = false
await this.navCtrl.navigateForward(`/recover`)
}
const { migrating } = await this.apiService.getStatus()
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
} catch (e: any) {
this.errorToastService.present(e)
}

View File

@@ -1,4 +1,4 @@
import { ErrorHandler, NgModule } from '@angular/core'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { RouteReuseStrategy } from '@angular/router'
import { HttpClientModule } from '@angular/common/http'
@@ -15,8 +15,6 @@ 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 { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
import { RecoverPageModule } from './pages/recover/recover.module'
import { WorkspaceConfig } from '@start9labs/shared'
@@ -35,8 +33,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
SuccessPageModule,
HomePageModule,
LoadingPageModule,
ProdKeyModalModule,
ProductKeyPageModule,
RecoverPageModule,
],
providers: [

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router'
import { RPCEncryptedService } from '../services/rpc-encrypted.service'
import { StateService } from '../services/state.service'
@Injectable({
providedIn: 'root',
})
export class NavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}
@Injectable({
providedIn: 'root',
})
export class RecoveryNavGuard implements CanActivate {
constructor(
private readonly router: Router,
private readonly encrypted: RPCEncryptedService,
private readonly stateService: StateService,
) {}
canActivate(): boolean {
if (this.encrypted.productKey || !this.stateService.hasProductKey) {
return true
} else {
this.router.navigateByUrl('product-key')
return false
}
}
}

View File

@@ -1,7 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import {
DiskInfo,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
@@ -15,7 +14,7 @@ import * as argon2 from '@start9labs/argon2'
export class PasswordPage {
@ViewChild('focusInput') elem?: IonInput
@Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() storageDrive?: DiskInfo
@Input() storageDrive = false
pwError = ''
password = ''

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProdKeyModal } from './prod-key-modal.page'
@NgModule({
declarations: [
ProdKeyModal,
],
imports: [
CommonModule,
FormsModule,
IonicModule,
],
exports: [
ProdKeyModal,
],
})
export class ProdKeyModalModule { }

View File

@@ -1,41 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>
Enter Product Key
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form (ngSubmit)="verifyProductKey()">
<div style="padding: 8px 24px;">
<div style="padding-bottom: 16px;">
<p>Enter your 0.2.x Product Key to establish an encrypted connection with your new Embassy.</p>
</div>
<ion-item>
<ion-input
#focusInput
[(ngModel)]="productKey"
placeholder="Enter Product Key"
maxlength="12"
></ion-input>
</ion-item>
<div style="height: 16px;">
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ error }}</p>
</div>
</div>
<input type="submit" style="display: none" />
</form>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
Cancel
</ion-button>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
Submit
</ion-button>
</ion-toolbar>
</ion-footer>

View File

@@ -1,3 +0,0 @@
ion-content {
--ion-text-color: var(--ion-color-dark);
}

View File

@@ -1,54 +0,0 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
@Component({
selector: 'prod-key-modal',
templateUrl: 'prod-key-modal.page.html',
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@ViewChild('focusInput') elem?: IonInput
@Input() target!: DiskBackupTarget
error = ''
productKey = ''
unmasked = false
constructor(
private readonly modalController: ModalController,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ngAfterViewInit() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async verifyProductKey() {
if (!this.productKey || !this.target.logicalname) return
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
await this.apiService.set02XDrive(this.target.logicalname)
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
this.modalController.dismiss({ productKey: this.productKey }, 'success')
} catch (e) {
this.encrypted.productKey = undefined
this.error = 'Invalid Product Key'
} finally {
loader.dismiss()
}
}
cancel() {
this.modalController.dismiss()
}
}

View File

@@ -64,12 +64,7 @@ export class EmbassyPage {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
buttons: ['OK'],
})
await alert.present()
}
@@ -95,40 +90,45 @@ export class EmbassyPage {
text: 'Continue',
handler: () => {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
} else {
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive, this.stateService.recoveryPassword)
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
} else {
this.presentModalPassword(drive)
this.presentModalPassword(drive.logicalname)
}
}
}
private async presentModalPassword(drive: DiskInfo): Promise<void> {
private async presentModalPassword(logicalname: string): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
storageDrive: drive,
storageDrive: true,
},
})
modal.onDidDismiss().then(async ret => {
if (!ret.data || !ret.data.password) return
this.setupEmbassy(drive, ret.data.password)
this.setupEmbassy(logicalname, ret.data.password)
})
await modal.present()
}
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
private async setupEmbassy(
logicalname: string,
password: string,
): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Initializing data drive. This could take a while...',
})
@@ -136,7 +136,7 @@ export class EmbassyPage {
await loader.present()
try {
await this.stateService.setupEmbassy(drive.logicalname, password)
await this.stateService.setupEmbassy(logicalname, password)
if (!!this.stateService.recoverySource) {
await this.navCtrl.navigateForward(`/loading`)
} else {

View File

@@ -4,9 +4,8 @@ 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: [
@@ -15,7 +14,8 @@ import { HomePageRoutingModule } from './home-routing.module'
IonicModule,
HomePageRoutingModule,
PasswordPageModule,
SwiperModule,
],
declarations: [HomePage],
})
export class HomePageModule { }
export class HomePageModule {}

View File

@@ -2,36 +2,78 @@
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
<div style="padding-bottom: 32px">
<img src="assets/img/logo.png" style="max-width: 240px" />
</div>
<ion-card color="dark">
<ion-card-content class="ion-margin">
<!-- fresh -->
<ion-card
routerLink="/embassy"
<ion-card-header>
<ion-button
*ngIf="swiper?.activeIndex === 1"
class="back-button"
fill="clear"
color="light"
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
(click)="previous()"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</ion-card-header>
<!-- recover -->
</ion-card>
<ion-card
routerLink="/recover"
color="light"
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
>
<ion-card-header>
<ion-card-title style="font-size: 40px;">Recover</ion-card-title>
<ion-card-subtitle>Restore from backup or recover an old Embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
</ion-button>
<ion-card-title>
Embassy Setup
<span *ngIf="swiper?.activeIndex === 1"> (recover)</span>
</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin-bottom">
<swiper (swiper)="setSwiperInstance($event)">
<ng-template swiperSlide>
<ion-item
button
[disabled]="error"
detail="true"
routerLink="/embassy"
>
<ion-icon color="dark" slot="start" name="add"></ion-icon>
<ion-label>
<h2><ion-text color="success">Start Fresh</ion-text></h2>
<p>Get started with a brand new Embassy</p>
</ion-label>
</ion-item>
<ion-item
button
[disabled]="error"
detail="true"
lines="none"
(click)="next()"
>
<ion-icon color="dark" slot="start" name="reload"></ion-icon>
<ion-label>
<h2><ion-text color="danger">Recover</ion-text></h2>
<p>
Restore from backup or use an existing Embassy data drive
</p>
</ion-label>
</ion-item>
</ng-template>
<ng-template swiperSlide>
<ion-item button detail="true" routerLink="/recover">
<ion-icon color="dark" slot="start" name="save"></ion-icon>
<ion-label>
<h2>
<ion-text color="warning">Restore From Backup</ion-text>
</h2>
<p>Recover an Embassy from encrypted backup</p>
</ion-label>
</ion-item>
<ion-item button detail="true" lines="none" (click)="import()">
<ion-icon color="dark" slot="start" name="cube"></ion-icon>
<ion-label>
<h2>
<ion-text color="primary">Use Existing Drive</ion-text>
</h2>
<p>Attach and use a valid Embassy data drive</p>
</ion-label>
</ion-item>
</ng-template>
</swiper>
</ion-card-content>
</ion-card>
</ion-col>

View File

@@ -0,0 +1,15 @@
.back-button {
position: absolute;
left: 16px;
top: 24px;
z-index: 1000000;
}
ion-item {
--background: var(--ion-color-medium);
--color: var(--ion-color-dark);
}
p {
color: var(--ion-color-dark);
}

View File

@@ -1,9 +1,112 @@
import { Component } from '@angular/core'
import {
AlertController,
IonicSlides,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { PasswordPage } from 'src/app/modals/password/password.page'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
import SwiperCore, { Swiper } from 'swiper'
import { ErrorToastService } from '@start9labs/shared'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage { }
export class HomePage {
swiper?: Swiper
guid?: string | null
error = false
constructor(
private readonly unencrypted: ApiService,
private readonly encrypted: RPCEncryptedService,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly stateService: StateService,
private readonly navCtrl: NavController,
private readonly errToastService: ErrorToastService,
) {}
async ngOnInit() {
try {
this.encrypted.secret = await this.unencrypted.getSecret()
const { disks } = await this.unencrypted.getDrives()
this.guid = disks.find(d => !!d.guid)?.guid
} catch (e: any) {
this.error = true
this.errToastService.present(e)
}
}
async ionViewDidEnter() {
if (this.swiper) {
this.swiper.allowTouchMove = false
}
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
next() {
this.swiper?.slideNext(500)
}
previous() {
this.swiper?.slidePrev(500)
}
async import() {
if (this.guid) {
const modal = await this.modalCtrl.create({
component: PasswordPage,
componentProps: { storageDrive: true },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(res.data.password)
}
})
await modal.present()
} else {
const alert = await this.alertCtrl.create({
header: 'Drive Not Found',
message:
'Please make sure the drive is a valid Embassy data drive (not a backup) and is firmly connected, then refresh the page.',
})
await alert.present()
}
}
private async importDrive(password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(this.guid!, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errToastService.present(e)
} finally {
loader.dismiss()
}
}
}
function decodeHex(hex: string) {
let str = ''
for (let n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substring(n, 2), 16))
}
return str
}

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/img/logo.png" style="max-width: 240px;" />
</div>
<ion-card color="dark">
<ion-card-header style="padding-bottom: 8px;">
<ion-card-title>Product Key</ion-card-title>
<ion-card-subtitle>Enter your product key to establish an encrypted connection with your Embassy</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin">
<form (submit)="submit()" style="margin-bottom: 12px;">
<ion-item-group class="ion-padding-bottom">
<ion-item color="dark">
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
<ion-input
#focusInput
name="productKey"
[(ngModel)]="productKey"
(ionChange)="error = ''"
maxlength="12"
>
</ion-input>
</ion-item>
<div class="ion-text-left">
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
</div>
</ion-item-group>
<ion-button type="submit" color="light" class="claim-button">
Submit
</ion-button>
</form>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,5 +0,0 @@
ion-item {
--border-style: solid;
--border-width: 1px;
--border-color: var(--ion-color-medium);
}

View File

@@ -1,52 +0,0 @@
import { Component, ViewChild } from '@angular/core'
import { IonInput, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
import { StateService } from 'src/app/services/state.service'
@Component({
selector: 'app-product-key',
templateUrl: 'product-key.page.html',
styleUrls: ['product-key.page.scss'],
})
export class ProductKeyPage {
@ViewChild('focusInput') elem?: IonInput
productKey = ''
error = ''
constructor(
private readonly navCtrl: NavController,
private readonly stateService: StateService,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly encrypted: RPCEncryptedService,
) {}
ionViewDidEnter() {
setTimeout(() => this.elem?.setFocus(), 400)
}
async submit() {
if (!this.productKey) return (this.error = 'Must enter product key')
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
this.encrypted.productKey = this.productKey
await this.apiService.verifyProductKey()
if (this.stateService.isMigrating) {
await this.navCtrl.navigateForward(`/loading`)
} else {
await this.navCtrl.navigateForward(`/home`)
}
} catch (e) {
this.error = 'Invalid Product Key'
this.encrypted.productKey = undefined
} finally {
loader.dismiss()
}
}
}

View File

@@ -5,7 +5,6 @@ import { FormsModule } from '@angular/forms'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { DriveStatusComponent, RecoverPage } from './recover.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
import { RecoverPageRoutingModule } from './recover-routing.module'
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
@@ -17,7 +16,6 @@ import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
IonicModule,
RecoverPageRoutingModule,
PasswordPageModule,
ProdKeyModalModule,
UnitConversionPipesModule,
CifsModalModule,
],

View File

@@ -1,17 +1,10 @@
import { Component, Input } from '@angular/core'
import {
AlertController,
IonicSafeString,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { AlertController, ModalController, NavController } from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@Component({
selector: 'app-recover',
@@ -21,7 +14,6 @@ import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
export class RecoverPage {
loading = true
mappedDrives: MappedDisk[] = []
hasShownGuidAlert = false
constructor(
private readonly apiService: ApiService,
@@ -29,8 +21,7 @@ export class RecoverPage {
private readonly modalCtrl: ModalController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService,
private readonly errToastService: ErrorToastService,
private readonly stateService: StateService,
) {}
@@ -44,10 +35,7 @@ export class RecoverPage {
}
driveClickable(mapped: MappedDisk) {
return (
mapped.drive['embassy-os']?.full &&
(this.stateService.hasProductKey || mapped.is02x)
)
return mapped.drive['embassy-os']?.full
}
async getDrives() {
@@ -89,50 +77,8 @@ export class RecoverPage {
})
await alert.present()
}
const importableDrive = disks.find(d => !!d.guid)
if (
!!importableDrive &&
this.stateService.hasProductKey &&
!this.hasShownGuidAlert
) {
const alert = await this.alertCtrl.create({
header: 'Embassy Data Drive Detected',
message: new IonicSafeString(
`<strong>${importableDrive.vendor || 'Unknown Vendor'} - ${
importableDrive.model || 'Unknown Model'
}</strong> contains Embassy data.
<p>To use this drive and its data, select <strong>"USE DRIVE"</strong>. This will complete the setup process.
<p><strong style="color:red">Important!</strong><br><br>
If you are trying to restore from a backup or update from 0.2.x, <strong>DO NOT</strong> select "USE DRIVE". Instead, select <strong>"CANCEL"</strong> and follow instructions.`,
),
buttons: [
{
role: 'cancel',
text: 'Cancel',
},
{
text: 'Use Drive',
handler: async () => {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { storageDrive: importableDrive },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.importDrive(importableDrive.guid!, res.data.password)
}
})
await modal.present()
},
},
],
})
await alert.present()
this.hasShownGuidAlert = true
}
} catch (e: any) {
this.errorToastService.present(e)
this.errToastService.present(e)
} finally {
this.loading = false
}
@@ -165,65 +111,20 @@ export class RecoverPage {
if (!logicalname) return
if (this.stateService.hasProductKey) {
if (is02x) {
this.selectRecoverySource(logicalname)
} else {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
}
})
await modal.present()
}
// if no product key, it means they are an upgrade kit user
if (is02x) {
this.selectRecoverySource(logicalname)
} else {
if (!is02x) {
const alert = await this.alertCtrl.create({
header: 'Error',
message:
'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.productKey) {
this.selectRecoverySource(logicalname)
}
})
await modal.present()
}
}
}
private async importDrive(guid: string, password: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(guid, password)
await this.navCtrl.navigateForward(`/success`)
} catch (e: any) {
this.errorToastService.present(e)
} finally {
loader.dismiss()
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
}
})
await modal.present()
}
}

View File

@@ -9,174 +9,143 @@
name="checkmark-circle-outline"
></ion-icon>
<ion-card-title>Setup Complete</ion-card-title>
<ion-card-subtitle
><b
>You have successully claimed your Embassy!</b
></ion-card-subtitle
>
<br />
</ion-card-header>
<ion-card-content>
<br />
<ng-template
[ngIf]="recoverySource && recoverySource.type === 'disk'"
<br />
<h2
*ngIf="recoverySource && recoverySource.type === 'disk'"
class="ion-padding-bottom"
>
<h2>You can now safely unplug your backup drive.</h2>
</ng-template>
<h2>
You have successully claimed your Embassy! You can now access your
device using the methods below.
You can now safely unplug your backup drive.
</h2>
<h2 style="font-weight: bold">
Access your Embassy using the methods below. You should
<a (click)="download()" class="inline">
download this page <ion-icon name="download-outline"></ion-icon>
</a>
for your records.
</h2>
<br />
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will no
longer work.
</p>
<div class="line"></div>
<!-- LAN Instructions -->
<div (click)="toggleLan()" class="toggle-label">
<h2>From Home (LAN)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<h1><b>From Home (LAN)</b></h1>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': lanOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are conncted to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<div class="ion-padding ion-text-start">
<p>
Visit the address below when you are conncted to the same WiFi
or Local Area Network (LAN) as your Embassy:
</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ lanAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(lanAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
Your browser will warn you that the website is untrusted. You
can bypass this warning on most browsers. The warning will go
away after you
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
>
<b>download and trust</b>
</a>
your Embassy's Root Certificate Authority.
</p>
<ion-button
style="margin-top: 24px; margin-bottom: 24px"
color="light"
(click)="installCert()"
>
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<!-- Tor Instructions -->
<div (click)="toggleTor()" class="toggle-label">
<h2>On The Go (Tor)</h2>
<ion-icon
name="chevron-down-outline"
[ngStyle]="{
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
'transition': 'transform 0.4s ease-out'
}"
></ion-icon>
</div>
<p>
<b>Note:</b> embassy.local was for setup purposes only, it will
no longer work.
</p>
<div
[ngStyle]="{
'overflow' : 'hidden',
'max-height': torOpen ? '500px' : '0px',
'transition': 'max-height 0.4s ease-out'
}"
>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ torAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
This address will only work from a
<a
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
>
<b>Tor-enabled browser</b> </a
>.
</p>
</div>
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
<br />
</div>
<div class="ion-text-center ion-padding-top">
<ion-button
color="light"
fill="clear"
color="primary"
strong
(click)="download()"
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
Download this page
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ lanAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
[href]="lanAddress"
target="_blank"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button
color="light"
fill="clear"
(click)="copy(lanAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
Your browser will warn you that the website is untrusted. You
can bypass this warning on most browsers. The warning will go
away after you
<a
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank"
rel="noreferrer"
class="inline"
>
follow the instructions
<ion-icon name="open-outline"></ion-icon>
</a>
to downlaod and trust your Embassy's Root Certificate Authority.
</p>
<ion-button style="margin-top: 24px" (click)="installCert()">
Download Root CA
<ion-icon slot="end" name="download-outline"></ion-icon>
</ion-button>
</div>
<br />
<div class="line"></div>
<!-- Tor Instructions -->
<h1><b>On The Go (Tor)</b></h1>
<div class="ion-padding ion-text-start">
<p>Visit the address below when you are away from home:</p>
<ion-item
lines="none"
color="dark"
class="ion-padding-top ion-padding-bottom"
>
<ion-label class="ion-text-wrap">
<code
><ion-text color="light"
><b>{{ torAddress }}</b></ion-text
></code
>
</ion-label>
<ion-button
color="light"
fill="clear"
(click)="copy(torAddress)"
>
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
<p>
<b>Important!</b>
This address will only work from a
<a
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
class="inline"
>
Tor-enabled browser
<ion-icon name="open-outline"></ion-icon> </a
>.
</p>
</div>
</ion-card-content>
</ion-card>

View File

@@ -4,26 +4,12 @@ p {
a {
text-decoration: none;
}
.toggle-label {
padding: 24px 0 8px 0;
font-weight: bold;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
* {
display: inline-block;
vertical-align: middle;
}
h2 {
font-weight: bold;
}
ion-icon {
text-align: right;
font-size: 24px;
}
}
.line {
margin-bottom: 48px;
padding-bottom: 48px;
border-bottom: solid 1px;
}

View File

@@ -16,8 +16,6 @@ import { StateService } from 'src/app/services/state.service'
})
export class SuccessPage {
@Output() onDownload = new EventEmitter()
torOpen = false
lanOpen = false
constructor(
@Inject(DOCUMENT) private readonly document: Document,
@@ -69,14 +67,6 @@ export class SuccessPage {
await toast.present()
}
toggleTor() {
this.torOpen = !this.torOpen
}
toggleLan() {
this.lanOpen = !this.lanOpen
}
installCert() {
this.document.getElementById('install-cert')?.click()
}

View File

@@ -1,20 +1,19 @@
export abstract class ApiService {
// unencrypted
abstract getStatus(): Promise<GetStatusRes> // setup.status
abstract getSecret(): Promise<string> // setup.get-secret
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
// encrypted
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
}
export type GetStatusRes = {
'product-key': boolean
migrating: boolean
}

View File

@@ -1,5 +1,10 @@
import { Injectable } from '@angular/core'
import { HttpService } from '@start9labs/shared'
import {
HttpService,
isRpcError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
@@ -13,43 +18,71 @@ import {
SetupEmbassyRes,
} from './api.service'
import { RPCEncryptedService } from '../rpc-encrypted.service'
import * as jose from 'node-jose'
@Injectable({
providedIn: 'root',
})
export class LiveApiService extends ApiService {
export class LiveApiService implements ApiService {
constructor(
private readonly unencrypted: HttpService,
private readonly encrypted: RPCEncryptedService,
) {
super()
}
) {}
// ** UNENCRYPTED **
async getStatus() {
return this.unencrypted.rpcRequest<GetStatusRes>({
return this.rpcRequest<GetStatusRes>({
method: 'setup.status',
params: {},
})
}
/**
* We want to update the secret, which means that we will call in clearnet the
* getSecret, and all the information is never in the clear, and only public
* information is sent across the network. We don't want to expose that we do
* this wil all public/private key, which means that there is no information loss
* through the network.
*/
async getSecret() {
const keystore = jose.JWK.createKeyStore()
const key = await keystore.generate('EC', 'P-256')
// const { privateKey, publicKey } =
// jose.generateKeyPair('ECDH-ES', {
// extractable: true,
// })
console.log({ publicKey: key.toJSON() })
const response: string = await this.rpcRequest({
method: 'setup.get-secret',
params: { pubkey: key.toJSON() },
})
// const { plaintext } = await jose.compactDecrypt(response, privateKey)
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
const decoded = new TextDecoder().decode(decrypted.plaintext)
console.log({ decoded })
return decoded
}
async getDrives() {
return this.unencrypted.rpcRequest<DiskListResponse>({
return this.rpcRequest<DiskListResponse>({
method: 'setup.disk.list',
params: {},
})
}
async set02XDrive(logicalname: string) {
return this.unencrypted.rpcRequest<void>({
return this.rpcRequest<void>({
method: 'setup.recovery.v2.set',
params: { logicalname },
})
}
async getRecoveryStatus() {
return this.unencrypted.rpcRequest<RecoveryStatusRes>({
return this.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: {},
})
@@ -65,13 +98,6 @@ export class LiveApiService extends ApiService {
})
}
async verifyProductKey() {
return this.encrypted.rpcRequest<void>({
method: 'echo',
params: { message: 'hello' },
})
}
async importDrive(params: ImportDriveReq) {
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
method: 'setup.attach',
@@ -113,6 +139,18 @@ export class LiveApiService extends ApiService {
'root-ca': btoa(res['root-ca']),
}
}
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const res = await this.unencrypted.rpcRequest<T>(opts)
const rpcRes = res.body
if (isRpcError(rpcRes)) {
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
}
function isCifsSource(

View File

@@ -12,21 +12,29 @@ let tries = 0
@Injectable({
providedIn: 'root',
})
export class MockApiService extends ApiService {
constructor() {
super()
}
export class MockApiService implements ApiService {
// ** UNENCRYPTED **
async getStatus() {
await pauseFor(1000)
return {
'product-key': true,
migrating: false,
}
}
async getSecret() {
await pauseFor(1000)
const ascii = 'thisisasecret'
const arr1 = []
for (let n = 0, l = ascii.length; n < l; n++) {
var hex = Number(ascii.charCodeAt(n)).toString(16)
arr1.push(hex)
}
return arr1.join('')
}
async getDrives() {
await pauseFor(1000)
return {
@@ -84,11 +92,6 @@ export class MockApiService extends ApiService {
}
}
async verifyProductKey() {
await pauseFor(1000)
return
}
async importDrive(params: ImportDriveReq) {
await pauseFor(3000)
return setupRes

View File

@@ -15,13 +15,13 @@ import {
providedIn: 'root',
})
export class RPCEncryptedService {
productKey?: string
secret?: string
constructor(private readonly http: HttpService) {}
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
const encryptedBody = await AES_CTR.encryptPbkdf2(
this.productKey || '',
this.secret || '',
encodeUtf8(JSON.stringify(opts)),
)
@@ -36,7 +36,11 @@ export class RPCEncryptedService {
'Content-Type': 'application/json',
},
})
.then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body))
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
.then(x => {
console.log(`Network: ${x}`)
return x
})
.then(res => JSON.parse(res))
.catch(e => {
if (!e.status && !e.statusText) {

View File

@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
providedIn: 'root',
})
export class StateService {
hasProductKey = false
isMigrating = false
polling = false
embassyLoaded = false

View File

@@ -38,7 +38,7 @@ ion-content {
ion-grid {
padding-top: 32px;
height: 100%;
max-width: 600px;
max-width: 640px;
}
ion-row {
@@ -47,6 +47,8 @@ ion-row {
ion-item {
--color: var(--ion-color-light);
--highlight-color-valid: transparent;
--highlight-color-invalid: transparent;
}
ion-toolbar {
@@ -61,13 +63,6 @@ ion-avatar {
height: 27px;
}
ion-item {
--highlight-color-valid: transparent;
--highlight-color-invalid: transparent;
--border-radius: 4px;
}
ion-card-title {
margin: 16px 0;
font-family: 'Montserrat';

View File

@@ -1,11 +1,10 @@
import { RpcErrorDetails } from '../types/rpc-error-details'
import { RPCErrorDetails } from '../types/rpc.types'
export class RpcError<T> {
export class RpcError {
readonly code = this.error.code
readonly message = this.getMessage()
readonly revision = this.getRevision()
constructor(private readonly error: RpcErrorDetails<T>) {}
constructor(private readonly error: RPCErrorDetails) {}
private getMessage(): string {
if (typeof this.error.data === 'string') {
@@ -16,10 +15,4 @@ export class RpcError<T> {
? `${this.error.message}\n\n${this.error.data.details}`
: this.error.message
}
private getRevision(): T | null {
return typeof this.error.data === 'string'
? null
: this.error.data?.revision || null
}
}

View File

@@ -14,17 +14,15 @@ export class MarkdownComponent {
@Input() content!: string | Observable<string>
@Input() title!: string
private readonly data$ = defer(() =>
readonly content$ = defer(() =>
isObservable(this.content) ? this.content : of(this.content),
).pipe(share())
readonly error$ = this.data$.pipe(
readonly error$ = this.content$.pipe(
ignoreElements(),
catchError(e => of(getErrorMessage(e))),
)
readonly content$ = this.data$.pipe(catchError(() => of([])))
constructor(private readonly modalCtrl: ModalController) {}
async dismiss() {

View File

@@ -6,11 +6,22 @@ import * as DOMPurify from 'dompurify'
name: 'markdown',
})
export class MarkdownPipe implements PipeTransform {
transform(value: any): any {
transform(value: string): string {
if (value && value.length > 0) {
// convert markdown to html
const html = marked(value)
// sanitize html
const sanitized = DOMPurify.sanitize(html)
return sanitized
// parse html to find all links
let parser = new DOMParser()
const doc = parser.parseFromString(sanitized, 'text/html')
const links = Array.from(doc.getElementsByTagName('a'))
// add target="_blank" to every link
links.forEach(link => {
link.setAttribute('target', '_blank')
})
// return new html string
return doc.documentElement.innerHTML
}
return value
}

View File

@@ -40,12 +40,14 @@ export * from './services/error-toast.service'
export * from './services/http.service'
export * from './types/api'
export * from './types/rpc-error-details'
export * from './types/http.types'
export * from './types/rpc.types'
export * from './types/url'
export * from './types/workspace-config'
export * from './util/copy-to-clipboard'
export * from './util/get-pkg-id'
export * from './util/misc.util'
export * from './util/rpc.util'
export * from './util/to-local-iso-string'
export * from './util/unused'

View File

@@ -1,6 +1,14 @@
import { Inject, Injectable } from '@angular/core'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { HttpError, RpcError, WorkspaceConfig } from '@start9labs/shared'
import { HttpClient } from '@angular/common/http'
import { HttpError } from '../classes/http-error'
import {
HttpAngularOptions,
HttpOptions,
LocalHttpResponse,
Method,
} from '../types/http.types'
import { RPCResponse, RPCOptions } from '../types/rpc.types'
import { WorkspaceConfig } from '../types/workspace-config'
import {
firstValueFrom,
from,
@@ -32,20 +40,21 @@ export class HttpService {
this.fullUrl = `${protocol}//${hostname}:${port}`
}
async rpcRequest<T>(opts: RPCOptions): Promise<T> {
const { method, params, timeout } = opts
async rpcRequest<T>(
opts: RPCOptions,
): Promise<LocalHttpResponse<RPCResponse<T>>> {
const { method, headers, params, timeout } = opts
const res = await this.httpRequest<RPCResponse<T>>({
return this.httpRequest<RPCResponse<T>>({
method: Method.POST,
url: this.relativeUrl,
headers,
body: { method, params },
timeout,
})
if (isRpcError(res)) throw new RpcError(res.error)
return res.result
}
async httpRequest<T>(opts: HttpOptions): Promise<T> {
async httpRequest<T>(opts: HttpOptions): Promise<LocalHttpResponse<T>> {
let { method, url, headers, body, responseType, timeout } = opts
url = opts.url.startsWith('/') ? this.fullUrl + url : url
@@ -67,113 +76,21 @@ export class HttpService {
responseType: responseType || 'json',
}
let req: Observable<{ body: T }>
let req: Observable<LocalHttpResponse<T>>
if (method === Method.GET) {
req = this.http.get(url, options as any) as any
} else {
req = this.http.post(url, body, options as any) as any
}
return firstValueFrom(timeout ? withTimeout(req, timeout) : req)
.then(res => res.body)
.catch(e => {
return firstValueFrom(timeout ? withTimeout(req, timeout) : req).catch(
e => {
throw new HttpError(e)
})
},
)
}
}
// ** RPC types **
interface RPCBase {
jsonrpc: '2.0'
id: string
}
export interface RPCRequest<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccess<T> extends RPCBase {
result: T
}
export interface RPCError extends RPCBase {
error: {
code: number
message: string
data?:
| {
details: string
}
| string
}
}
export type RPCResponse<T> = RPCSuccess<T> | RPCError
export interface RPCOptions {
method: string
params: {
[param: string]:
| string
| number
| boolean
| object
| string[]
| number[]
| null
}
timeout?: number
}
export function isRpcError<Error, Result>(
arg: { error: Error } | { result: Result },
): arg is { error: Error } {
return (arg as any).error !== undefined
}
// ** HTTP types **
export enum Method {
GET = 'GET',
POST = 'POST',
}
export interface HttpOptions {
method: Method
url: string
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?:
| HttpParams
| {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
body?: any
timeout?: number
}
interface HttpAngularOptions {
observe: 'response'
withCredentials: true
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?:
| HttpParams
| {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
}
function hasParams(
params?: HttpOptions['params'],
): params is Record<string, string | string[]> {
@@ -191,9 +108,3 @@ function withTimeout<U>(req: Observable<U>, timeout: number): Observable<U> {
),
)
}
export interface RequestError {
code: number
message: string
details: string
}

View File

@@ -0,0 +1,44 @@
import { HttpHeaders, HttpResponse } from '@angular/common/http'
export enum Method {
GET = 'GET',
POST = 'POST',
}
export interface HttpOptions {
method: Method
url: string
headers?: {
[header: string]: string | string[]
}
params?: {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
body?: any
timeout?: number
}
export interface HttpAngularOptions {
observe: 'response'
withCredentials: true
headers?:
| HttpHeaders
| {
[header: string]: string | string[]
}
params?: {
[param: string]: string | string[]
}
responseType?: 'json' | 'text' | 'arrayBuffer'
}
export interface LocalHttpResponse<T> extends HttpResponse<T> {
body: T
}
export interface RequestError {
code: number
message: string
details: string
}

View File

@@ -1,10 +0,0 @@
export interface RpcErrorDetails<T> {
code: number
message: string
data?:
| {
details: string
revision?: T | null
}
| string
}

View File

@@ -0,0 +1,55 @@
// ** RPC types **
interface RPCBase {
jsonrpc: '2.0'
id: string
}
export interface RPCRequest<T> extends RPCBase {
method: string
params?: T
}
export interface RPCSuccessRes<T> extends RPCBase {
result: T
}
export interface RPCErrorRes extends RPCBase {
error: RPCErrorDetails
}
export interface RPCErrorDetails {
code: number
message: string
data?:
| {
details: string
}
| string
}
export type RPCResponse<T> = RPCSuccessRes<T> | RPCErrorRes
export interface RPCOptions {
method: string
headers?: {
[header: string]: string | string[]
}
params: {
[param: string]:
| string
| number
| boolean
| object
| string[]
| number[]
| null
}
timeout?: number
}
export function isRpcError<Error, Result>(
arg: { error: Error } | { result: Result },
): arg is { error: Error } {
return (arg as any).error !== undefined
}

View File

@@ -0,0 +1,11 @@
import { RPCErrorDetails } from '../types/rpc.types'
export function getRpcErrorMessage(error: RPCErrorDetails): string {
if (typeof error.data === 'string') {
return `${error.message}\n\n${error.data}`
}
return error.data?.details
? `${error.message}\n\n${error.data.details}`
: error.message
}

View File

@@ -24,3 +24,6 @@
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
/* Import swiper styles for slides */
@import '~swiper/scss';

View File

@@ -18,6 +18,12 @@ ion-alert {
}
}
.swiper {
.swiper-slide {
display: unset;
}
}
ion-modal::part(content) {
position: absolute;
height: 90% !important;

View File

@@ -34,7 +34,7 @@ import { ConnectionBarComponentModule } from './components/connection-bar/connec
storeName: '_embassykv',
dbKey: '_embassykey',
name: '_embassystorage',
driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB],
driverOrder: [Drivers.LocalStorage],
}),
MenuModule,
PreloaderModule,

View File

@@ -1,5 +1,4 @@
import { Bootstrapper, DBCache } from 'patch-db-client'
import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core'
import { APP_INITIALIZER, Provider } from '@angular/core'
import { UntypedFormBuilder } from '@angular/forms'
import { Router, RouteReuseStrategy } from '@angular/router'
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
@@ -8,10 +7,8 @@ import { WorkspaceConfig } from '@start9labs/shared'
import { ApiService } from './services/api/embassy-api.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
import { AuthService } from './services/auth.service'
import { LocalStorageService } from './services/local-storage.service'
import { DataModel } from './services/patch-db/data-model'
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -30,14 +27,7 @@ export const APP_PROVIDERS: Provider[] = [
},
{
provide: APP_INITIALIZER,
deps: [
Storage,
AuthService,
LocalStorageService,
Router,
BOOTSTRAPPER,
PATCH_CACHE,
],
deps: [Storage, AuthService, LocalStorageService, Router],
useFactory: appInitializer,
multi: true,
},
@@ -48,19 +38,12 @@ export function appInitializer(
auth: AuthService,
localStorage: LocalStorageService,
router: Router,
bootstrapper: Bootstrapper<DataModel>,
cache: DBCache<DataModel>,
): () => Promise<void> {
return async () => {
await storage.create()
await auth.init()
await localStorage.init()
const localCache = await bootstrapper.init()
cache.sequence = localCache.sequence
cache.data = localCache.data
router.initialNavigation()
}
}

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { heightCollapse } from '../../util/animations'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs/operators'
import { ServerInfo } from '../../services/patch-db/data-model'
import { DataModel, ServerInfo } from '../../services/patch-db/data-model'
@Component({
selector: 'footer[appFooter]',
@@ -24,7 +24,7 @@ export class FooterComponent {
},
}
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
getProgress({
downloaded,

View File

@@ -57,6 +57,6 @@
src="assets/img/icons/snek.png"
[appSnekHighScore]="snekScore$ | async"
/>
<ion-footer class="bottom">
<ion-footer *ngIf="sidebarOpen$ | async" class="bottom">
<connection-bar></connection-bar>
</ion-footer>

View File

@@ -1,11 +1,13 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { LocalStorageService } from '../../services/local-storage.service'
import { EOSService } from '../../services/eos.service'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
@Component({
selector: 'app-menu',
@@ -57,11 +59,14 @@ export class MenuComponent {
.getUpdates()
.pipe(map(pkgs => pkgs.length))
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly localStorageService: LocalStorageService,
private readonly eosService: EOSService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly splitPane: SplitPaneTracker,
) {}
}

View File

@@ -4,9 +4,6 @@
<!-- 3rd party components -->
<qr-code value="hello"></qr-code>
<swiper>
<ng-template swiperSlide>Slide 1</ng-template>
</swiper>
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>

View File

@@ -2,11 +2,10 @@ import { CommonModule } from '@angular/common'
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { QrCodeModule } from 'ng-qrcode'
import { SwiperModule } from 'swiper/angular'
import { PreloaderComponent } from './preloader.component'
@NgModule({
imports: [CommonModule, IonicModule, QrCodeModule, SwiperModule],
imports: [CommonModule, IonicModule, QrCodeModule],
declarations: [PreloaderComponent],
exports: [PreloaderComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],

View File

@@ -1,9 +1,7 @@
import { Directive, HostListener, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ErrorToastService } from '@start9labs/shared'
import { SnakePage } from '../../modals/snake/snake.page'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { ApiService } from '../../services/api/embassy-api.service'
@Directive({

View File

@@ -1,4 +0,0 @@
<h1>
<ion-text color="warning">Warning</ion-text>
</h1>
<div class="ion-text-left" [innerHTML]="params.message || '' | markdown"></div>

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AlertComponent } from './alert.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { MarkdownPipeModule } from '@start9labs/shared'
@NgModule({
declarations: [AlertComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
MarkdownPipeModule,
],
exports: [AlertComponent],
})
export class AlertComponentModule {}

View File

@@ -1,16 +0,0 @@
import { Component, Input } from '@angular/core'
import { BaseSlide } from '../wizard-types'
@Component({
selector: 'alert',
templateUrl: './alert.component.html',
styleUrls: ['../app-wizard.component.scss'],
})
export class AlertComponent implements BaseSlide {
@Input()
params!: { message: string }
async load() {}
loading = false
}

View File

@@ -1,93 +0,0 @@
<ion-header>
<ion-toolbar>
<div style="padding: 10px 0">
<ion-title style="font-size: 32px">{{ params.title }}</ion-title>
<div class="underline"></div>
<ion-title>
<i
>{{ params.action | titlecase
}}<span *ngIf="params.version"
>: {{ params.version | displayEmver }}</span
></i
>
</ion-title>
</div>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div style="padding: 36px; height: 100%">
<swiper
*ngIf="!error; else hasError"
(swiper)="setSwiperInstance($event)"
(slideNextTransitionStart)="loadSlide()"
>
<ng-template swiperSlide *ngFor="let slide of params.slides">
<alert
#components
*ngIf="slide.selector === 'alert'"
[params]="slide.params"
></alert>
<dependents
#components
*ngIf="slide.selector === 'dependents'"
[params]="slide.params"
(onSuccess)="next()"
(onError)="setError($event)"
></dependents>
<complete
#components
*ngIf="slide.selector === 'complete'"
[params]="slide.params"
(onSuccess)="dismiss('success')"
(onError)="setError($event)"
></complete>
</ng-template>
</swiper>
<ng-template #hasError>
<p>
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ng-template>
</div>
</ion-content>
<ion-footer>
<ion-toolbar>
<ng-container *ngIf="!initializing && swiper">
<ion-buttons slot="end" class="ion-padding-end">
<ion-button
*ngIf="error; else noError"
fill="solid"
color="dark"
(click)="dismiss()"
class="enter-click btn-128"
>
Dismiss
</ion-button>
<ng-template #noError>
<ion-button
*ngIf="!currentSlide.loading && !swiper.isEnd"
fill="solid"
color="primary"
(click)="next()"
class="enter-click btn-128"
[class.no-click]="currentSlide.loading"
>
{{
currentIndex < swiper.slides.length - 2
? 'Continue'
: params.submitBtn
}}
</ion-button>
</ng-template>
</ion-buttons>
</ng-container>
</ion-toolbar>
</ion-footer>

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AppWizardComponent } from './app-wizard.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { EmverPipesModule } from '@start9labs/shared'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { AlertComponentModule } from './alert/alert.component.module'
import { SwiperModule } from 'swiper/angular'
@NgModule({
declarations: [AppWizardComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
EmverPipesModule,
DependentsComponentModule,
CompleteComponentModule,
AlertComponentModule,
SwiperModule,
],
exports: [AppWizardComponent],
})
export class AppWizardComponentModule {}

View File

@@ -1,6 +0,0 @@
.underline {
margin: 6px 0 8px 16px;
border-style: solid;
border-width: 0px 0px 1px 0px;
border-color: #404040;
}

View File

@@ -1,107 +0,0 @@
import {
Component,
Input,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { IonContent, ModalController } from '@ionic/angular'
import { CompleteComponent } from './complete/complete.component'
import { DependentsComponent } from './dependents/dependents.component'
import { AlertComponent } from './alert/alert.component'
import { WizardAction } from './wizard-types'
import SwiperCore, { Swiper } from 'swiper'
import { IonicSlides } from '@ionic/angular'
import { BaseSlide } from './wizard-types'
SwiperCore.use([IonicSlides])
@Component({
selector: 'app-wizard',
templateUrl: './app-wizard.component.html',
styleUrls: ['./app-wizard.component.scss'],
})
export class AppWizardComponent {
@Input()
params!: {
action: WizardAction
title: string
slides: SlideDefinition[]
submitBtn: string
version?: string
}
// content container so we can scroll to top between slide transitions
@ViewChild(IonContent)
content?: IonContent
swiper?: Swiper
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
@ViewChildren('components')
slideComponentsQL?: QueryList<BaseSlide>
get slideComponents(): BaseSlide[] {
return this.slideComponentsQL?.toArray() || []
}
get currentSlide(): BaseSlide {
return this.slideComponents[this.currentIndex]
}
get currentIndex(): number {
return this.swiper?.activeIndex || NaN
}
initializing = true
error = ''
constructor(private readonly modalController: ModalController) {}
ionViewDidEnter() {
this.initializing = false
if (this.swiper) this.swiper.allowTouchMove = false
this.loadSlide()
}
setSwiperInstance(swiper: any) {
this.swiper = swiper
}
dismiss(role = 'cancelled') {
this.modalController.dismiss(null, role)
}
async next() {
await this.content?.scrollToTop()
this.swiper?.slideNext(500)
}
setError(e: any) {
this.error = e
}
async loadSlide() {
this.currentSlide.load()
}
}
export type SlideDefinition =
| { selector: 'alert'; params: AlertComponent['params'] }
| { selector: 'dependents'; params: DependentsComponent['params'] }
| { selector: 'complete'; params: CompleteComponent['params'] }
export async function wizardModal(
modalController: ModalController,
params: AppWizardComponent['params'],
): Promise<boolean> {
const modal = await modalController.create({
backdropDismiss: false,
cssClass: 'wizard-modal',
component: AppWizardComponent,
componentProps: { params },
})
await modal.present()
return modal.onDidDismiss().then(({ role }) => role === 'success')
}

View File

@@ -1,4 +0,0 @@
<div style="padding: 32px">
<ion-spinner color="warning" name="lines"></ion-spinner>
<p>{{ message }}</p>
</div>

View File

@@ -1,12 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { CompleteComponent } from './complete.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
@NgModule({
declarations: [CompleteComponent],
imports: [CommonModule, IonicModule, RouterModule.forChild([])],
exports: [CompleteComponent],
})
export class CompleteComponentModule {}

View File

@@ -1,35 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { capitalizeFirstLetter } from '@start9labs/shared'
import { BaseSlide } from '../wizard-types'
@Component({
selector: 'complete',
templateUrl: './complete.component.html',
styleUrls: ['../app-wizard.component.scss'],
})
export class CompleteComponent implements BaseSlide {
@Input()
params!: {
verb: string // loader verb: '*stopping* ...'
title: string
Fn: () => Promise<any>
}
@Output() onSuccess: EventEmitter<void> = new EventEmitter()
@Output() onError: EventEmitter<string> = new EventEmitter()
message = ''
loading = true
async load() {
this.message =
capitalizeFirstLetter(this.params.verb || '') + ' ' + this.params.title
try {
await this.params.Fn()
this.onSuccess.emit()
} catch (e: any) {
this.onError.emit(`Error: ${e.message || e}`)
}
}
}

View File

@@ -1,25 +0,0 @@
<div *ngIf="loading; else loaded" style="padding: 32px">
<ion-spinner color="warning" name="lines"></ion-spinner>
<p>Checking for installed services which depend on {{ params.title }}...</p>
</div>
<ng-template #loaded>
<h1><ion-text color="warning">Warning</ion-text></h1>
<p>{{ warningMessage }}</p>
<ng-container *ngIf="pkgs$ | async as pkgs">
<ion-item-group>
<ion-item-divider class="ion-padding-bottom">
Affected Services
</ion-item-divider>
<ion-item *ngFor="let dep of breakages | keyvalue">
<ion-thumbnail slot="start">
<img alt="" [src]="pkgs[dep.key]['static-files'].icon" />
</ion-thumbnail>
<ion-label>
{{ pkgs[dep.key].manifest.title }}
</ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ng-template>

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DependentsComponent } from './dependents.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
@NgModule({
declarations: [DependentsComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharedPipesModule,
],
exports: [DependentsComponent],
})
export class DependentsComponentModule {}

View File

@@ -1,52 +0,0 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { Breakages } from 'src/app/services/api/api.types'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { capitalizeFirstLetter, isEmptyObject } from '@start9labs/shared'
import { BaseSlide } from '../wizard-types'
@Component({
selector: 'dependents',
templateUrl: './dependents.component.html',
styleUrls: ['./dependents.component.scss', '../app-wizard.component.scss'],
})
export class DependentsComponent implements BaseSlide {
@Input()
params!: {
title: string
verb: string // *Uninstalling* will cause problems...
Fn: () => Promise<Breakages>
}
@Output() onSuccess: EventEmitter<void> = new EventEmitter()
@Output() onError: EventEmitter<string> = new EventEmitter()
breakages?: Breakages
warningMessage = ''
loading = true
readonly pkgs$ = this.patch.watch$('package-data')
constructor(private readonly patch: PatchDbService) {}
async load() {
try {
this.breakages = await this.params.Fn()
if (this.breakages && !isEmptyObject(this.breakages)) {
this.warningMessage =
capitalizeFirstLetter(this.params.verb || '') +
' ' +
this.params.title +
' will prohibit the following services from functioning properly.'
} else {
this.onSuccess.emit()
}
} catch (e: any) {
this.onError.emit(
`Error fetching dependent service information: ${e.message || e}`,
)
} finally {
this.loading = false
}
}
}

View File

@@ -1,193 +0,0 @@
import { Inject, Injectable } from '@angular/core'
import { exists } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { Manifest } from 'src/app/services/patch-db/data-model'
import { ApiService } from '../../services/api/embassy-api.service'
import { AppWizardComponent, SlideDefinition } from './app-wizard.component'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { firstValueFrom } from 'rxjs'
@Injectable({ providedIn: 'root' })
export class WizardDefs {
constructor(
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}
update(values: {
id: string
title: string
version: string
installAlert?: string
}): AppWizardComponent['params'] {
const { id, title, version, installAlert } = values
const slides: Array<SlideDefinition | undefined> = [
installAlert
? {
selector: 'alert',
params: {
message: installAlert,
},
}
: undefined,
{
selector: 'complete',
params: {
verb: 'beginning update for',
title,
Fn: () =>
firstValueFrom(
this.marketplaceService.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
}),
),
},
},
]
return {
action: 'update',
title,
version,
slides: slides.filter(exists),
submitBtn: 'Begin Update',
}
}
downgrade(values: {
id: string
title: string
version: string
installAlert?: string
}): AppWizardComponent['params'] {
const { id, title, version, installAlert } = values
const slides: Array<SlideDefinition | undefined> = [
installAlert
? {
selector: 'alert',
params: {
message: installAlert,
},
}
: undefined,
{
selector: 'complete',
params: {
verb: 'beginning downgrade for',
title,
Fn: () =>
firstValueFrom(
this.marketplaceService.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
}),
),
},
},
]
return {
action: 'downgrade',
title,
version,
slides: slides.filter(exists),
submitBtn: 'Begin Downgrade',
}
}
uninstall(values: {
id: string
title: string
uninstallAlert?: string
}): AppWizardComponent['params'] {
const { id, title, uninstallAlert } = values
const slides: SlideDefinition[] = [
{
selector: 'alert',
params: {
message: uninstallAlert || defaultUninstallWarning(title),
},
},
{
selector: 'complete',
params: {
verb: 'uninstalling',
title,
Fn: () => this.embassyApi.uninstallPackage({ id }),
},
},
]
return {
action: 'uninstall',
title,
slides: slides.filter(exists),
submitBtn: 'Uninstall Anyway',
}
}
stop(values: { id: string; title: string }): AppWizardComponent['params'] {
const { title, id } = values
const slides: SlideDefinition[] = [
{
selector: 'complete',
params: {
verb: 'stopping',
title,
Fn: () => this.embassyApi.stopPackage({ id }),
},
},
]
return {
action: 'stop',
title,
slides: slides.filter(exists),
submitBtn: 'Stop Anyway',
}
}
configure(values: {
manifest: Manifest
config: object
}): AppWizardComponent['params'] {
const { manifest, config } = values
const { id, title } = manifest
const slides: SlideDefinition[] = [
{
selector: 'dependents',
params: {
verb: 'saving config for',
title,
Fn: () => this.embassyApi.drySetPackageConfig({ id, config }),
},
},
{
selector: 'complete',
params: {
verb: 'configuring',
title,
Fn: () => this.embassyApi.setPackageConfig({ id, config }),
},
},
]
return {
action: 'configure',
title,
slides: slides.filter(exists),
submitBtn: 'Configure Anyway',
}
}
}
const defaultUninstallWarning = (serviceName: string) =>
`Uninstalling ${serviceName} will result in the deletion of its data.`

View File

@@ -1,11 +0,0 @@
export type WizardAction =
| 'update'
| 'downgrade'
| 'uninstall'
| 'stop'
| 'configure'
export interface BaseSlide {
load: () => Promise<void>
loading: boolean
}

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'badge-menu-button',
@@ -14,6 +15,6 @@ export class BadgeMenuComponent {
constructor(
private readonly splitPane: SplitPaneTracker,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
}

View File

@@ -8,7 +8,7 @@
slot="end"
[name]="connection.icon"
class="icon"
color="light"
[color]="connection.iconColor"
></ion-icon>
<p style="margin: 8px 0; font-weight: 600">{{ connection.message }}</p>
<ion-spinner

View File

@@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { combineLatest, map, Observable, startWith, tap } from 'rxjs'
import { Component } from '@angular/core'
import { combineLatest, map, Observable } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'connection-bar',
@@ -14,8 +13,9 @@ export class ConnectionBarComponent {
readonly connection$: Observable<{
message: string
icon: string
color: string
icon: string
iconColor: string
dots: boolean
}> = combineLatest([
this.connectionService.networkConnected$,
@@ -25,29 +25,29 @@ export class ConnectionBarComponent {
if (!network)
return {
message: 'No Internet',
color: 'danger',
icon: 'cloud-offline-outline',
color: 'dark',
iconColor: 'dark',
dots: false,
}
if (!websocket)
return {
message: 'Connecting',
icon: 'cloud-offline-outline',
color: 'warning',
icon: 'cloud-offline-outline',
iconColor: 'light',
dots: true,
}
return {
message: 'Connected',
icon: 'cloud-done',
color: 'success',
icon: 'cloud-done',
iconColor: 'light',
dots: false,
}
}),
)
constructor(
private readonly connectionService: ConnectionService,
private readonly patch: PatchDbService,
) {}
constructor(private readonly connectionService: ConnectionService) {}
}

View File

@@ -214,15 +214,23 @@
>
<div class="nested-wrapper">
<form-object
[objectSpec]="
spec.type === 'union'
? spec.variants[$any(entry.value).controls[spec.tag.id].value]
: spec.spec
"
*ngIf="spec.type === 'object'"
[objectSpec]="spec.spec"
[formGroup]="$any(entry.value)"
[current]="current?.[entry.key]"
[original]="original?.[entry.key]"
[unionSpec]="spec.type === 'union' ? spec : undefined"
(onExpand)="resize(entry.key)"
(hasNewOptions)="setHasNew(entry.key)"
></form-object>
<form-object
*ngIf="spec.type === 'union'"
[objectSpec]="
spec.variants[$any(entry.value).controls[spec.tag.id].value]
"
[formGroup]="$any(entry.value)"
[current]="current?.[entry.key]"
[original]="original?.[entry.key][spec.tag.id] === current?.[entry.key][spec.tag.id] ? original?.[entry.key] : undefined"
[unionSpec]="spec"
(onExpand)="resize(entry.key)"
(hasNewOptions)="setHasNew(entry.key)"
></form-object>
@@ -366,6 +374,7 @@
</ion-input>
<ion-button
strong
fill="clear"
slot="end"
color="danger"
(click)="presentAlertDelete(entry.key, i)"

View File

@@ -1,6 +1,7 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import {
AbstractFormGroupDirective,
FormArray,
UntypedFormArray,
UntypedFormGroup,
} from '@angular/forms'
@@ -105,10 +106,10 @@ export class FormObjectComponent {
}
updateUnion(e: any): void {
const primary = this.unionSpec?.tag.id
const id = this.unionSpec?.tag.id
Object.keys(this.formGroup.controls).forEach(control => {
if (control === primary) return
if (control === id) return
this.formGroup.removeControl(control)
})
@@ -118,7 +119,7 @@ export class FormObjectComponent {
)
Object.keys(unionGroup.controls).forEach(control => {
if (control === primary) return
if (control === id) return
this.formGroup.addControl(control, unionGroup.controls[control])
})
@@ -152,35 +153,6 @@ export class FormObjectComponent {
this.presentAlertChangeWarning(key, spec, () => this.addListItem(key))
}
addListItem(key: string, markDirty = true, val?: string): void {
const arr = this.formGroup.get(key) as UntypedFormArray
if (markDirty) arr.markAsDirty()
const listSpec = this.objectSpec[key] as ValueSpecList
const newItem = this.formService.getListItem(listSpec, val)
if (!newItem) return
const index = arr.length
newItem.markAllAsTouched()
arr.insert(index, newItem)
if (['object', 'union'].includes(listSpec.subtype)) {
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
this.objectListDisplay[key].push({
height: '0px',
expanded: false,
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',
})
}
pauseFor(400).then(() => {
const element = document.getElementById(this.getElementId(key, index))
element?.parentElement?.scrollIntoView({ behavior: 'smooth' })
})
}
toggleExpandObject(key: string) {
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
this.objectDisplay[key].height = this.objectDisplay[key].expanded
@@ -327,6 +299,36 @@ export class FormObjectComponent {
await alert.present()
}
private addListItem(key: string): void {
const arr = this.formGroup.get(key) as UntypedFormArray
const listSpec = this.objectSpec[key] as ValueSpecList
const newItem = this.formService.getListItem(listSpec, undefined)!
const index = arr.length
arr.insert(index, newItem)
if (['object', 'union'].includes(listSpec.subtype)) {
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
this.objectListDisplay[key].push({
height: '0px',
expanded: false,
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',
})
}
this.onExpand.emit()
pauseFor(400).then(() => {
const element = document.getElementById(this.getElementId(key, index))
element?.parentElement?.scrollIntoView({ behavior: 'smooth' })
})
arr.markAsDirty()
newItem.markAllAsTouched()
}
private deleteListItem(key: string, index: number, markDirty = true): void {
if (this.objectListDisplay[key])
this.objectListDisplay[key][index].height = '0px'
@@ -340,19 +342,25 @@ export class FormObjectComponent {
}
private updateEnumList(key: string, current: string[], updated: string[]) {
this.formGroup.get(key)?.markAsDirty()
const arr = this.formGroup.get(key) as FormArray
for (let i = current.length - 1; i >= 0; i--) {
if (!updated.includes(current[i])) {
this.deleteListItem(key, i, false)
arr.removeAt(i)
}
}
const listSpec = this.objectSpec[key] as ValueSpecList
updated.forEach(val => {
if (!current.includes(val)) {
this.addListItem(key, false, val)
const newItem = this.formService.getListItem(listSpec, val)!
arr.insert(arr.length, newItem)
}
})
arr.markAsDirty()
arr.markAllAsTouched()
}
private getDocSize(key: string, index = 0): string {

View File

@@ -2,7 +2,8 @@ import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs'
import { filter, map, pairwise } from 'rxjs/operators'
import { exists } from '@start9labs/shared'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Injectable({ providedIn: 'root' })
export class NotificationsToastService extends Observable<boolean> {
@@ -15,7 +16,7 @@ export class NotificationsToastService extends Observable<boolean> {
endWith(false),
)
constructor(private readonly patch: PatchDbService) {
constructor(private readonly patch: PatchDB<DataModel>) {
super(subscriber => this.stream$.subscribe(subscriber))
}
}

View File

@@ -2,9 +2,9 @@ import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Emver } from '@start9labs/shared'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ConfigService } from '../../../services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
// Watch for connection status
@Injectable({ providedIn: 'root' })
@@ -15,7 +15,7 @@ export class RefreshAlertService extends Observable<boolean> {
)
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly emver: Emver,
private readonly config: ConfigService,
) {

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs'
import { distinctUntilChanged, filter } from 'rxjs/operators'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Injectable({ providedIn: 'root' })
export class UpdateToastService extends Observable<boolean> {
@@ -9,7 +10,7 @@ export class UpdateToastService extends Observable<boolean> {
.watch$('server-info', 'status-info', 'updated')
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false))
constructor(private readonly patch: PatchDbService) {
constructor(private readonly patch: PatchDB<DataModel>) {
super(subscriber => this.stream$.subscribe(subscriber))
}
}

View File

@@ -14,8 +14,11 @@ import {
} from '@start9labs/shared'
import { DependentInfo } from 'src/app/types/dependent-info'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import { UntypedFormGroup } from '@angular/forms'
import {
convertValuesRecursive,
@@ -57,7 +60,7 @@ export class AppConfigPage {
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async ngOnInit() {

View File

@@ -7,8 +7,9 @@ import {
import { getErrorMessage } from '@start9labs/shared'
import { BackupInfo } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { AppRecoverOption } from './to-options.pipe'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'app-recover-select',
@@ -30,7 +31,7 @@ export class AppRecoverSelectPage {
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
dismiss() {

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { filter, map, take } from 'rxjs/operators'
import { PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
@Component({
selector: 'backup-select',
@@ -22,7 +22,7 @@ export class BackupSelectPage {
constructor(
private readonly modalCtrl: ModalController,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
ngOnInit() {

View File

@@ -7,9 +7,10 @@ import {
ModalController,
NavController,
} from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import {
Action,
DataModel,
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
@@ -36,7 +37,7 @@ export class AppActionsPage {
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private readonly navCtrl: NavController,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async handleAction(
@@ -197,10 +198,10 @@ export class AppActionsPage {
})
setTimeout(() => successModal.present(), 500)
return false
return true // needed to dismiss original modal/alert
} catch (e: any) {
this.errToast.present(e)
return false
return false // don't dismiss original modal/alert
} finally {
loader.dismiss()
}

View File

@@ -4,10 +4,11 @@ import { ModalController, ToastController } from '@ionic/angular'
import { getPkgId, copyToClipboard } from '@start9labs/shared'
import { getUiInterfaceKey } from 'src/app/services/config.service'
import {
DataModel,
InstalledPackageDataEntry,
InterfaceDef,
} from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { getPackage } from '../../../util/get-package-data'
@@ -28,7 +29,7 @@ export class AppInterfacesPage {
constructor(
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async ngOnInit() {

View File

@@ -1,6 +1,9 @@
import { Component } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs'
import { filter, map, switchMap, take, takeUntil, tap } from 'rxjs/operators'
import { isEmptyObject, exists, DestroyService } from '@start9labs/shared'
@@ -22,7 +25,7 @@ export class AppListPage {
constructor(
private readonly api: ApiService,
private readonly destroy$: DestroyService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
get empty(): boolean {

View File

@@ -1,15 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Observable } from 'rxjs'
import { filter, map, startWith } from 'rxjs/operators'
import { PackageDataEntry } from '../../../services/patch-db/data-model'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
@Pipe({
name: 'packageInfo',
})
export class PackageInfoPipe implements PipeTransform {
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
return this.patch

View File

@@ -10,8 +10,11 @@ import {
} from '@ionic/angular'
import { PackageProperties } from 'src/app/util/properties.util'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import {
DestroyService,
ErrorToastService,
@@ -52,7 +55,7 @@ export class AppPropertiesPage {
private readonly toastCtrl: ToastController,
private readonly modalCtrl: ModalController,
private readonly navCtrl: NavController,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly destroy$: DestroyService,
) {}

View File

@@ -4,7 +4,6 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppShowPage } from './app-show.page'
import { EmverPipesModule } from '@start9labs/shared'
import { AppWizardComponentModule } from 'src/app/components/app-wizard/app-wizard.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
@@ -51,7 +50,6 @@ const routes: Routes = [
StatusComponentModule,
IonicModule,
RouterModule.forChild(routes),
AppWizardComponentModule,
AppConfigPageModule,
EmverPipesModule,
LaunchablePipeModule,

View File

@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { NavController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
PackageDataEntry,
PackageMainStatus,
PackageState,
@@ -62,7 +63,7 @@ export class AppShowPage {
constructor(
private readonly route: ActivatedRoute,
private readonly navCtrl: NavController,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}

View File

@@ -8,6 +8,7 @@ import {
removeTrailingSlash,
} from '@start9labs/shared'
import {
DataModel,
PackageDataEntry,
UIMarketplaceData,
} from 'src/app/services/patch-db/data-model'
@@ -16,7 +17,8 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, map, Observable } from 'rxjs'
import { Marketplace } from '@start9labs/marketplace'
import { ActionMarketplaceComponent } from 'src/app/modals/action-marketplace/action-marketplace.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
export interface Button {
title: string
description: string
@@ -38,7 +40,7 @@ export class ToButtonsPipe implements PipeTransform {
private readonly modalCtrl: ModalController,
private readonly modalService: ModalService,
private readonly apiService: ApiService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
transform(

View File

@@ -4,12 +4,13 @@ import { NavController } from '@ionic/angular'
import { combineLatest, Observable } from 'rxjs'
import { filter, map, startWith } from 'rxjs/operators'
import {
DataModel,
DependencyError,
DependencyErrorType,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { DependentInfo } from 'src/app/types/dependent-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ModalService } from 'src/app/services/modal.service'
export interface DependencyInfo {
@@ -27,7 +28,7 @@ export interface DependencyInfo {
})
export class ToDependenciesPipe implements PipeTransform {
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly navCtrl: NavController,
private readonly modalService: ModalService,
) {}

View File

@@ -1,19 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core'
import {
DataModel,
HealthCheckResult,
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { exists, isEmptyObject } from '@start9labs/shared'
import { filter, map, startWith } from 'rxjs/operators'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs'
@Pipe({
name: 'toHealthChecks',
})
export class ToHealthChecksPipe implements PipeTransform {
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
transform(
pkg: PackageDataEntry,

View File

@@ -5,9 +5,10 @@ import { debounce, ErrorToastService } from '@start9labs/shared'
import * as yaml from 'js-yaml'
import { filter, take } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { getProjectId } from 'src/app/util/get-project-id'
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'dev-config',
@@ -24,12 +25,12 @@ export class DevConfigPage {
private readonly route: ActivatedRoute,
private readonly errToast: ErrorToastService,
private readonly modalCtrl: ModalController,
private readonly patchDb: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly api: ApiService,
) {}
ngOnInit() {
this.patchDb
this.patch
.watch$('ui', 'dev', this.projectId, 'config')
.pipe(filter(Boolean), take(1))
.subscribe(config => {

View File

@@ -8,8 +8,9 @@ import {
ErrorToastService,
MarkdownComponent,
} from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { getProjectId } from 'src/app/util/get-project-id'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'dev-instructions',
@@ -26,12 +27,12 @@ export class DevInstructionsPage {
private readonly route: ActivatedRoute,
private readonly errToast: ErrorToastService,
private readonly modalCtrl: ModalController,
private readonly patchDb: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly api: ApiService,
) {}
ngOnInit() {
this.patchDb
this.patch
.watch$('ui', 'dev', this.projectId, 'instructions')
.pipe(filter(Boolean), take(1))
.subscribe(config => {

View File

@@ -2,8 +2,9 @@ import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import * as yaml from 'js-yaml'
import { take } from 'rxjs/operators'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { getProjectId } from 'src/app/util/get-project-id'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'dev-manifest',
@@ -17,11 +18,11 @@ export class DevManifestPage {
constructor(
private readonly route: ActivatedRoute,
private readonly patchDb: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
ngOnInit() {
this.patchDb
this.patch
.watch$('ui', 'dev', this.projectId)
.pipe(take(1))
.subscribe(devData => {

View File

@@ -10,12 +10,12 @@ import {
GenericInputComponent,
GenericInputOptions,
} from 'src/app/modals/generic-input/generic-input.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import * as yaml from 'js-yaml'
import { v4 } from 'uuid'
import { DevData } from 'src/app/services/patch-db/data-model'
import { DataModel, DevData } from 'src/app/services/patch-db/data-model'
import { DestroyService, ErrorToastService } from '@start9labs/shared'
import { takeUntil } from 'rxjs/operators'
@@ -35,7 +35,7 @@ export class DeveloperListPage {
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
private readonly destroy$: DestroyService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly actionCtrl: ActionSheetController,
) {}

View File

@@ -3,11 +3,11 @@ import { ActivatedRoute } from '@angular/router'
import { LoadingController, ModalController } from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { BasicInfo, getBasicInfoSpec } from './form-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared'
import { getProjectId } from 'src/app/util/get-project-id'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
import { DataModel, DevProjectData } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'developer-menu',
@@ -25,7 +25,7 @@ export class DeveloperMenuPage {
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async openBasicInfoModal(data: DevProjectData) {

View File

@@ -6,7 +6,6 @@ ion-card-title {
}
ion-item {
--border-radius: 6px;
--border-style: solid;
--border-width: 1px;
--border-color: var(--ion-color-light);

View File

@@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { map } from 'rxjs/operators'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ConnectionService } from 'src/app/services/connection.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-list',
@@ -23,7 +24,7 @@ export class MarketplaceListPage {
.pipe(map(({ name }) => name))
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly connectionService: ConnectionService,
) {}

View File

@@ -11,6 +11,7 @@ import {
} from '@start9labs/marketplace'
import { Emver, ErrorToastService, isEmptyObject } from '@start9labs/shared'
import {
DataModel,
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
@@ -19,7 +20,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Breakages } from 'src/app/services/api/api.types'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { getAllPackages } from 'src/app/util/get-package-data'
import { firstValueFrom } from 'rxjs'
@@ -49,7 +50,7 @@ export class MarketplaceShowControlsComponent {
private readonly emver: Emver,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
get localVersion(): string {

View File

@@ -14,8 +14,6 @@ import {
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import { AppWizardComponentModule } from 'src/app/components/app-wizard/app-wizard.component.module'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
@@ -39,7 +37,6 @@ const routes: Routes = [
EmverPipesModule,
MarkdownPipeModule,
MarketplaceStatusModule,
AppWizardComponentModule,
PackageModule,
AboutModule,
DependenciesModule,

View File

@@ -5,10 +5,10 @@ import {
MarketplacePkg,
AbstractMarketplaceService,
} from '@start9labs/marketplace'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import { BehaviorSubject, Observable, of } from 'rxjs'
import { catchError, filter, shareReplay, switchMap } from 'rxjs/operators'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'marketplace-show',
@@ -40,7 +40,7 @@ export class MarketplaceShowPage {
constructor(
private readonly route: ActivatedRoute,
private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly marketplaceService: AbstractMarketplaceService,
) {}

Some files were not shown because too many files have changed in this diff Show More