mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
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:
@@ -156,7 +156,6 @@ export class HomePage {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -50,7 +50,6 @@ export class AdditionalComponent {
|
||||
{
|
||||
text: 'Ok',
|
||||
handler: (version: string) => this.version.emit(version),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
ion-item {
|
||||
--border-style: solid;
|
||||
--border-width: 1px;
|
||||
--border-color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService {
|
||||
hasProductKey = false
|
||||
isMigrating = false
|
||||
|
||||
polling = false
|
||||
embassyLoaded = false
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
44
frontend/projects/shared/src/types/http.types.ts
Normal file
44
frontend/projects/shared/src/types/http.types.ts
Normal 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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface RpcErrorDetails<T> {
|
||||
code: number
|
||||
message: string
|
||||
data?:
|
||||
| {
|
||||
details: string
|
||||
revision?: T | null
|
||||
}
|
||||
| string
|
||||
}
|
||||
55
frontend/projects/shared/src/types/rpc.types.ts
Normal file
55
frontend/projects/shared/src/types/rpc.types.ts
Normal 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
|
||||
}
|
||||
11
frontend/projects/shared/src/util/rpc.util.ts
Normal file
11
frontend/projects/shared/src/util/rpc.util.ts
Normal 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
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -18,6 +18,12 @@ ion-alert {
|
||||
}
|
||||
}
|
||||
|
||||
.swiper {
|
||||
.swiper-slide {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
ion-modal::part(content) {
|
||||
position: absolute;
|
||||
height: 90% !important;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<h1>
|
||||
<ion-text color="warning">Warning</ion-text>
|
||||
</h1>
|
||||
<div class="ion-text-left" [innerHTML]="params.message || '' | markdown"></div>
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -1,6 +0,0 @@
|
||||
.underline {
|
||||
margin: 6px 0 8px 16px;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #404040;
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<div style="padding: 32px">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
@@ -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 {}
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.`
|
||||
@@ -1,11 +0,0 @@
|
||||
export type WizardAction =
|
||||
| 'update'
|
||||
| 'downgrade'
|
||||
| 'uninstall'
|
||||
| 'stop'
|
||||
| 'configure'
|
||||
|
||||
export interface BaseSlide {
|
||||
load: () => Promise<void>
|
||||
loading: boolean
|
||||
}
|
||||
@@ -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>,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,7 +6,6 @@ ion-card-title {
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--border-radius: 6px;
|
||||
--border-style: solid;
|
||||
--border-width: 1px;
|
||||
--border-color: var(--ion-color-light);
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user