remove product key from setup flow (#1750)

* remove product key flow from setup

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

* implement new encryption scheme in FE

* decode response string

* crypto not working

* update setup wizard closes #1762

* feat: Get the encryption key

* fix: Get to recovery

* remove old code

* fix build

* fix: Install works for now

* fix bug in config for adding new list items

* dismiss action modal on success

* clear button in config

* wip: Currently broken in avahi mdns

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

* fix: Can now run in the main

* flatline on failed init

* update patch DB

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

* chore: Fix the start.

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

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

* fix: Can start

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

* refactor to remove WithRevision

* resolve circular dep issue

* update submodule

* fix patch-db

* update patchDB

* update patch again

* escape error

* decodeuricomponent

* increase proxy buffer size

* increase proxy buffer size

* fix nginx

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,8 @@ import {
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from '@start9labs/shared'
import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page'
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: 'notifications',
@@ -36,7 +37,7 @@ export class NotificationsPage {
private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService,
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async ngOnInit() {

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ConfigService } from 'src/app/services/config.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: 'lan',
@@ -14,7 +15,7 @@ export class LANPage {
constructor(
private readonly config: ConfigService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
installCert(): void {

View File

@@ -11,9 +11,12 @@ import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { v4 } from 'uuid'
import { UIMarketplaceData } from '../../../services/patch-db/data-model'
import {
DataModel,
UIMarketplaceData,
} from '../../../services/patch-db/data-model'
import { ConfigService } from '../../../services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import {
@@ -50,7 +53,7 @@ export class MarketplacesPage {
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly config: ConfigService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly destroy$: DestroyService,
private readonly alertCtrl: AlertController,
) {}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import {
LoadingController,
ModalController,
@@ -16,6 +16,7 @@ import {
ServerNameInfo,
ServerNameService,
} from 'src/app/services/server-name.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'preferences',
@@ -36,7 +37,7 @@ export class PreferencesPage {
private readonly api: ApiService,
private readonly toastCtrl: ToastController,
private readonly localStorageService: LocalStorageService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly serverNameService: ServerNameService,
readonly serverConfig: ServerConfigService,
) {}

View File

@@ -4,9 +4,12 @@ import {
Pipe,
PipeTransform,
} from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { filter, take } from 'rxjs/operators'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
import {
DataModel,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs'
@Component({
@@ -26,7 +29,7 @@ export class BackingUpComponent {
PackageMainStatus = PackageMainStatus
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
}
@Pipe({
@@ -44,5 +47,5 @@ export class PkgMainStatusPipe implements PipeTransform {
)
}
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
}

View File

@@ -9,7 +9,7 @@ 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 { skip, takeUntil } from 'rxjs/operators'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import * as argon2 from '@start9labs/argon2'
@@ -21,6 +21,7 @@ import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.pag
import { EOSService } from 'src/app/services/eos.service'
import { DestroyService } from '@start9labs/shared'
import { getServerInfo } from 'src/app/util/get-server-info'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'server-backup',
@@ -40,7 +41,7 @@ export class ServerBackupPage {
private readonly navCtrl: NavController,
private readonly destroy$: DestroyService,
private readonly eosService: EOSService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
ngOnInit() {

View File

@@ -7,6 +7,7 @@ import { FormsModule } from '@angular/forms'
import { TextSpinnerComponentModule } from '@start9labs/shared'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { OSUpdatePageModule } from 'src/app/modals/os-update/os-update.page.module'
import { BackupColorPipeModule } from 'src/app/pipes/backup-color/backup-color.module'
const routes: Routes = [
{
@@ -24,6 +25,7 @@ const routes: Routes = [
TextSpinnerComponentModule,
BadgeMenuComponentModule,
OSUpdatePageModule,
BackupColorPipeModule,
],
declarations: [ServerShowPage],
})

View File

@@ -48,7 +48,7 @@
<p *ngIf="button.title === 'Create Backup'">
<ng-container *ngIf="server['status-info'] as statusInfo">
<ion-text
color="warning"
[color]="server['last-backup'] | backupColor"
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
>
Last Backup: {{ server['last-backup'] ? (server['last-backup']

View File

@@ -7,7 +7,7 @@ import {
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ServerNameService } from 'src/app/services/server-name.service'
import { Observable, of } from 'rxjs'
import { filter, take, tap } from 'rxjs/operators'
@@ -17,6 +17,7 @@ import { LocalStorageService } from 'src/app/services/local-storage.service'
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
import { getAllPackages } from '../../../util/get-package-data'
import { AuthService } from 'src/app/services/auth.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'server-show',
@@ -40,7 +41,7 @@ export class ServerShowPage {
private readonly embassyApi: ApiService,
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly eosService: EOSService,
private readonly localStorageService: LocalStorageService,
private readonly serverNameService: ServerNameService,

View File

@@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { ConfigService } from 'src/app/services/config.service'
import { copyToClipboard } from '@start9labs/shared'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'server-specs',
@@ -15,7 +16,7 @@ export class ServerSpecsPage {
constructor(
private readonly toastCtrl: ToastController,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
) {}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { BackupColorPipe } from './backup-color.pipe'
@NgModule({
declarations: [BackupColorPipe],
exports: [BackupColorPipe],
})
export class BackupColorPipeModule {}

View File

@@ -0,0 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({
name: 'backupColor',
})
export class BackupColorPipe implements PipeTransform {
transform(lastBackup: string | null): 'success' | 'warning' | 'danger' {
if (!lastBackup) return 'danger'
const currentDate = new Date().valueOf()
const backupDate = new Date(lastBackup).valueOf()
const diff = currentDate - backupDate
const week = 604800000
if (diff <= week) {
return 'success'
} else if (diff > week && diff <= week * 2) {
return 'warning'
} else {
return 'danger'
}
}
}

View File

@@ -679,7 +679,7 @@ export module Mock {
manifest: {
...Mock.MockManifestBitcoind,
'release-notes':
'For a complete list of changes, please visit <a href="https://bitcoincore.org/en/releases/0.21.0/">https://bitcoincore.org/en/releases/0.21.0/</a><br /><ul><li>Taproot!</li><li>New RPCs</li><li>Experimental Descriptor Wallets</li></ul>',
'For a complete list of changes, please visit <a href="https://bitcoincore.org/en/releases/0.21.0/" target="_blank">https://bitcoincore.org/en/releases/0.21.0/</a><br />Or in [markdown](https://bitcoincore.org/en/releases/0.21.0/)<ul><li>Taproot!</li><li>New RPCs</li><li>Experimental Descriptor Wallets</li></ul>',
},
categories: ['bitcoin', 'cryptocurrency'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
@@ -1469,6 +1469,14 @@ export module Mock {
masked: false,
copyable: true,
},
'private-domain': {
name: 'Private Domain',
type: 'string',
description: 'the private address of the node',
nullable: false,
masked: true,
copyable: true,
},
},
},
},
@@ -1726,7 +1734,10 @@ export module Mock {
rpcuser: '123',
rulemakers: [],
},
'bitcoin-node': undefined,
'bitcoin-node': {
type: 'external',
'public-domain': 'hello.com',
},
port: 20,
rpcallowip: undefined,
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],

View File

@@ -16,8 +16,8 @@ export module RR {
export type GetDumpRes = Dump<DataModel>
export type SetDBValueReq = WithExpire<{ pointer: string; value: any }> // db.put.ui
export type SetDBValueRes = WithRevision<null>
export type SetDBValueReq = { pointer: string; value: any } // db.put.ui
export type SetDBValueRes = null
// auth
@@ -44,8 +44,8 @@ export module RR {
export type GetServerMetricsReq = {} // server.metrics
export type GetServerMetricsRes = Metrics
export type UpdateServerReq = WithExpire<{ 'marketplace-url': string }> // server.update
export type UpdateServerRes = WithRevision<'updating' | 'no-updates'>
export type UpdateServerReq = { 'marketplace-url': string } // server.update
export type UpdateServerRes = 'updating' | 'no-updates'
export type RestartServerReq = {} // server.restart
export type RestartServerRes = null
@@ -64,8 +64,8 @@ export module RR {
sessions: { [hash: string]: Session }
}
export type KillSessionsReq = WithExpire<{ ids: string[] }> // sessions.kill
export type KillSessionsRes = WithRevision<null>
export type KillSessionsReq = { ids: string[] } // sessions.kill
export type KillSessionsRes = null
// password
@@ -74,11 +74,11 @@ export module RR {
// notification
export type GetNotificationsReq = WithExpire<{
export type GetNotificationsReq = {
before?: number
limit?: number
}> // notification.list
export type GetNotificationsRes = WithRevision<ServerNotification<number>[]>
} // notification.list
export type GetNotificationsRes = ServerNotification<number>[]
export type DeleteNotificationReq = { id: number } // notification.delete
export type DeleteNotificationRes = null
@@ -96,8 +96,8 @@ export module RR {
ssids: {
[ssid: string]: number
}
connected?: string
country: string
connected: string | null
country: string | null
ethernet: boolean
'available-wifi': AvailableWifi[]
}
@@ -151,14 +151,14 @@ export module RR {
export type GetBackupInfoReq = { 'target-id': string; password: string } // backup.target.info
export type GetBackupInfoRes = BackupInfo
export type CreateBackupReq = WithExpire<{
export type CreateBackupReq = {
// backup.create
'target-id': string
'package-ids': string[]
'old-password': string | null
password: string
}>
export type CreateBackupRes = WithRevision<null>
}
export type CreateBackupRes = null
// package
@@ -175,13 +175,13 @@ export module RR {
export type GetPackageMetricsReq = { id: string } // package.metrics
export type GetPackageMetricsRes = Metric
export type InstallPackageReq = WithExpire<{
export type InstallPackageReq = {
id: string
'version-spec'?: string
'version-priority'?: 'min' | 'max'
'marketplace-url': string
}> // package.install
export type InstallPackageRes = WithRevision<null>
} // package.install
export type InstallPackageRes = null
export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry
export type DryUpdatePackageRes = Breakages
@@ -192,17 +192,17 @@ export module RR {
export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry
export type DrySetPackageConfigRes = Breakages
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
export type SetPackageConfigRes = WithRevision<null>
export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set
export type SetPackageConfigRes = null
export type RestorePackagesReq = WithExpire<{
export type RestorePackagesReq = {
// package.backup.restore
ids: string[]
'target-id': string
'old-password': string | null
password: string
}>
export type RestorePackagesRes = WithRevision<null>
}
export type RestorePackagesRes = null
export type ExecutePackageActionReq = {
id: string
@@ -211,20 +211,20 @@ export module RR {
} // package.action
export type ExecutePackageActionRes = ActionResponse
export type StartPackageReq = WithExpire<{ id: string }> // package.start
export type StartPackageRes = WithRevision<null>
export type StartPackageReq = { id: string } // package.start
export type StartPackageRes = null
export type RestartPackageReq = WithExpire<{ id: string }> // package.restart
export type RestartPackageRes = WithRevision<null>
export type RestartPackageReq = { id: string } // package.restart
export type RestartPackageRes = null
export type StopPackageReq = WithExpire<{ id: string }> // package.stop
export type StopPackageRes = WithRevision<null>
export type StopPackageReq = { id: string } // package.stop
export type StopPackageRes = null
export type UninstallPackageReq = WithExpire<{ id: string }> // package.uninstall
export type UninstallPackageRes = WithRevision<null>
export type UninstallPackageReq = { id: string } // package.uninstall
export type UninstallPackageRes = null
export type DeleteRecoveredPackageReq = { id: string } // package.delete-recovered
export type DeleteRecoveredPackageRes = WithRevision<null>
export type DeleteRecoveredPackageRes = null
export type DryConfigureDependencyReq = {
'dependency-id': string
@@ -268,9 +268,6 @@ export module RR {
export type GetReleaseNotesRes = { [version: string]: string }
}
export type WithExpire<T> = { 'expire-id'?: string } & T
export type WithRevision<T> = { response: T | null; revision?: Revision }
export interface MarketplaceEOS {
version: string
headline: string

View File

@@ -1,12 +1,12 @@
import { Subject, Observable } from 'rxjs'
import { Update, Operation, Revision } from 'patch-db-client'
import { Observable, ReplaySubject } from 'rxjs'
import { Update } from 'patch-db-client'
import { RR } from './api.types'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { Log, RequestError } from '@start9labs/shared'
import { Log } from '@start9labs/shared'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
export abstract class ApiService {
readonly sync$ = new Subject<Update<DataModel>>()
readonly patchStream$ = new ReplaySubject<Update<DataModel>[]>(1)
// http
@@ -18,15 +18,7 @@ export abstract class ApiService {
// db
abstract getRevisions(since: number): Promise<RR.GetRevisionsRes>
abstract getDump(): Promise<RR.GetDumpRes>
protected abstract setDbValueRaw(
params: RR.SetDBValueReq,
): Promise<RR.SetDBValueRes>
setDbValue = (params: RR.SetDBValueReq) =>
this.syncResponse(() => this.setDbValueRaw(params))()
abstract setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes>
// auth
@@ -72,18 +64,7 @@ export abstract class ApiService {
params: RR.GetPackageMetricsReq,
): Promise<RR.GetPackageMetricsRes>
protected abstract updateServerRaw(
params: RR.UpdateServerReq,
): Promise<RR.UpdateServerRes>
updateServer = (params: RR.UpdateServerReq) =>
this.syncResponse(() => this.updateServerWrapper(params))()
async updateServerWrapper(params: RR.UpdateServerReq) {
const res = await this.updateServerRaw(params)
if (res.response === 'no-updates') {
throw new Error('Could not find a newer version of EmbassyOS')
}
return res
}
abstract updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
abstract restartServer(
params: RR.RestartServerReq,
@@ -116,13 +97,9 @@ export abstract class ApiService {
// notification
abstract getNotificationsRaw(
abstract getNotifications(
params: RR.GetNotificationsReq,
): Promise<RR.GetNotificationsRes>
getNotifications = (params: RR.GetNotificationsReq) =>
this.syncResponse<RR.GetNotificationsRes['response'], any>(() =>
this.getNotificationsRaw(params),
)()
abstract deleteNotification(
params: RR.DeleteNotificationReq,
@@ -179,11 +156,7 @@ export abstract class ApiService {
params: RR.GetBackupInfoReq,
): Promise<RR.GetBackupInfoRes>
protected abstract createBackupRaw(
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes>
createBackup = (params: RR.CreateBackupReq) =>
this.syncResponse(() => this.createBackupRaw(params))()
abstract createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes>
// package
@@ -199,11 +172,9 @@ export abstract class ApiService {
params: RR.FollowPackageLogsReq,
): Promise<RR.FollowPackageLogsRes>
protected abstract installPackageRaw(
abstract installPackage(
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes>
installPackage = (params: RR.InstallPackageReq) =>
this.syncResponse(() => this.installPackageRaw(params))()
abstract dryUpdatePackage(
params: RR.DryUpdatePackageReq,
@@ -217,85 +188,39 @@ export abstract class ApiService {
params: RR.DrySetPackageConfigReq,
): Promise<RR.DrySetPackageConfigRes>
protected abstract setPackageConfigRaw(
abstract setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes>
setPackageConfig = (params: RR.SetPackageConfigReq) =>
this.syncResponse(() => this.setPackageConfigRaw(params))()
protected abstract restorePackagesRaw(
abstract restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes>
restorePackages = (params: RR.RestorePackagesReq) =>
this.syncResponse(() => this.restorePackagesRaw(params))()
abstract executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes>
protected abstract startPackageRaw(
params: RR.StartPackageReq,
): Promise<RR.StartPackageRes>
startPackage = (params: RR.StartPackageReq) =>
this.syncResponse(() => this.startPackageRaw(params))()
abstract startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes>
protected abstract restartPackageRaw(
abstract restartPackage(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes>
restartPackage = (params: RR.RestartPackageReq) =>
this.syncResponse(() => this.restartPackageRaw(params))()
protected abstract stopPackageRaw(
params: RR.StopPackageReq,
): Promise<RR.StopPackageRes>
stopPackage = (params: RR.StopPackageReq) =>
this.syncResponse(() => this.stopPackageRaw(params))()
abstract stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes>
protected abstract uninstallPackageRaw(
abstract uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes>
uninstallPackage = (params: RR.UninstallPackageReq) =>
this.syncResponse(() => this.uninstallPackageRaw(params))()
abstract dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes>
protected abstract deleteRecoveredPackageRaw(
abstract deleteRecoveredPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes>
deleteRecoveredPackage = (params: RR.UninstallPackageReq) =>
this.syncResponse(() => this.deleteRecoveredPackageRaw(params))()
abstract sideloadPackage(
params: RR.SideloadPackageReq,
): Promise<RR.SideloadPacakgeRes>
// Helper allowing quick decoration to sync the response patch and return the response contents.
// Pass in a tempUpdate function which returns a UpdateTemp corresponding to a temporary
// state change you'd like to enact prior to request and expired when request terminates.
private syncResponse<
T,
F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>,
>(f: F, temp?: Operation<unknown>): (...args: Parameters<F>) => Promise<T> {
return (...a) => {
// let expireId = undefined
// if (temp) {
// expireId = uuid.v4()
// this.sync.next({ patch: [temp], expiredBy: expireId })
// }
return f(a)
.catch((e: UIRequestError) => {
if (e.revision) this.sync$.next(e.revision)
throw e
})
.then(({ response, revision }) => {
if (revision) this.sync$.next(revision)
return response
})
}
}
}
type UIRequestError = RequestError & { revision: Revision }

View File

@@ -1,9 +1,11 @@
import { Inject, Injectable } from '@angular/core'
import {
HttpOptions,
HttpService,
isRpcError,
Log,
Method,
RPCError,
RpcError,
RPCOptions,
} from '@start9labs/shared'
import { ApiService } from './embassy-api.service'
@@ -11,11 +13,11 @@ import { RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { ConfigService } from '../config.service'
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
import { Observable, timeout } from 'rxjs'
import { Observable } from 'rxjs'
import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
import { DataModel } from '../patch-db/data-model'
import { Update } from 'patch-db-client'
import { PatchDB, Update } from 'patch-db-client'
@Injectable()
export class LiveApiService extends ApiService {
@@ -24,13 +26,14 @@ export class LiveApiService extends ApiService {
private readonly http: HttpService,
private readonly config: ConfigService,
private readonly auth: AuthService,
private readonly patch: PatchDB<DataModel>,
) {
super()
; (window as any).rpcClient = this
}
async getStatic(url: string): Promise<string> {
return this.http.httpRequest({
return this.httpRequest({
method: Method.GET,
url,
responseType: 'text',
@@ -38,7 +41,7 @@ export class LiveApiService extends ApiService {
}
async uploadPackage(guid: string, body: ArrayBuffer): Promise<string> {
return this.http.httpRequest({
return this.httpRequest({
method: Method.POST,
body,
url: `/rest/rpc/${guid}`,
@@ -48,22 +51,14 @@ export class LiveApiService extends ApiService {
// db
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
return this.rpcRequest({ method: 'db.revisions', params: { since } })
}
async getDump(): Promise<RR.GetDumpRes> {
return this.rpcRequest({ method: 'db.dump', params: {} })
}
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
async setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
return this.rpcRequest({ method: 'db.put.ui', params })
}
// auth
async login(params: RR.LoginReq): Promise<RR.loginRes> {
return this.rpcRequest({ method: 'auth.login', params })
return this.rpcRequest({ method: 'auth.login', params }, false)
}
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
@@ -81,7 +76,7 @@ export class LiveApiService extends ApiService {
// server
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
return this.rpcRequest({ method: 'echo', params })
return this.rpcRequest({ method: 'echo', params }, false)
}
openPatchWebsocket$(): Observable<Update<DataModel>> {
@@ -94,7 +89,7 @@ export class LiveApiService extends ApiService {
},
}
return this.openWebsocket(config).pipe(timeout({ first: 21000 }))
return this.openWebsocket(config)
}
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
@@ -131,10 +126,13 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.metrics', params })
}
async updateServerRaw(
params: RR.UpdateServerReq,
): Promise<RR.UpdateServerRes> {
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
return this.rpcRequest({ method: 'server.update', params })
// const res = await this.updateServer(params)
// if (res.response === 'no-updates') {
// throw new Error('Could not find a newer version of EmbassyOS')
// }
// return res
}
async restartServer(
@@ -182,7 +180,7 @@ export class LiveApiService extends ApiService {
// notification
async getNotificationsRaw(
async getNotifications(
params: RR.GetNotificationsReq,
): Promise<RR.GetNotificationsRes> {
return this.rpcRequest({ method: 'notification.list', params })
@@ -277,9 +275,7 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'backup.target.info', params })
}
async createBackupRaw(
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes> {
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
return this.rpcRequest({ method: 'backup.create', params })
}
@@ -288,9 +284,9 @@ export class LiveApiService extends ApiService {
async getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
return this.http
.rpcRequest({ method: 'package.properties', params })
.then(parsePropertiesPermissive)
return this.rpcRequest({ method: 'package.properties', params }).then(
parsePropertiesPermissive,
)
}
async getPackageLogs(
@@ -311,7 +307,7 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.metrics', params })
}
async installPackageRaw(
async installPackage(
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes> {
return this.rpcRequest({ method: 'package.install', params })
@@ -335,13 +331,13 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.config.set.dry', params })
}
async setPackageConfigRaw(
async setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
return this.rpcRequest({ method: 'package.config.set', params })
}
async restorePackagesRaw(
async restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {
return this.rpcRequest({ method: 'package.backup.restore', params })
@@ -353,29 +349,27 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.action', params })
}
async startPackageRaw(
params: RR.StartPackageReq,
): Promise<RR.StartPackageRes> {
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
return this.rpcRequest({ method: 'package.start', params })
}
async restartPackageRaw(
async restartPackage(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
return this.rpcRequest({ method: 'package.restart', params })
}
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
return this.rpcRequest({ method: 'package.stop', params })
}
async deleteRecoveredPackageRaw(
async deleteRecoveredPackage(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
return this.rpcRequest({ method: 'package.delete-recovered', params })
}
async uninstallPackageRaw(
async uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
return this.rpcRequest({ method: 'package.uninstall', params })
@@ -409,13 +403,42 @@ export class LiveApiService extends ApiService {
return webSocket(config)
}
private async rpcRequest<T>(options: RPCOptions): Promise<T> {
return this.http.rpcRequest<T>(options).catch(e => {
if ((e as RPCError).error.code === 34) {
private async rpcRequest<T>(
options: RPCOptions,
addHeader = true,
): Promise<T> {
if (addHeader) {
options.headers = {
'x-patch-sequence': String(this.patch.cache$.value.sequence),
...(options.headers || {}),
}
}
const res = await this.http.rpcRequest<T>(options)
const encoded = res.headers.get('x-patch-updates')
if (encoded) {
const updates: Update<DataModel>[] = JSON.parse(
decodeURIComponent(encoded),
)
this.patchStream$.next(updates)
}
const rpcRes = res.body
if (isRpcError(rpcRes)) {
if (rpcRes.error.code === 34) {
console.error('Unauthenticated, logging out')
this.auth.setUnverified()
}
throw e
})
throw new RpcError(rpcRes.error)
}
return rpcRes.result
}
private async httpRequest<T>(opts: HttpOptions): Promise<T> {
const res = await this.http.httpRequest<T>(opts)
return res.body
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { pauseFor, Log, LogsRes } from '@start9labs/shared'
import { pauseFor, Log } from '@start9labs/shared'
import { ApiService } from './embassy-api.service'
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
import {
@@ -11,11 +11,11 @@ import {
PackageState,
ServerStatus,
} from 'src/app/services/patch-db/data-model'
import { CifsBackupTarget, RR, WithRevision } from './api.types'
import { CifsBackupTarget, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from './api.fixures'
import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md'
import { BehaviorSubject, interval, map, Observable, tap } from 'rxjs'
import { BehaviorSubject, interval, map, Observable } from 'rxjs'
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
import { mockPatchData } from './mock-patch'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
@@ -32,10 +32,9 @@ const PROGRESS: InstallProgress = {
@Injectable()
export class MockApiService extends ApiService {
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>({
readonly mockWsSource$ = new BehaviorSubject<Update<DataModel>>({
id: 1,
value: mockPatchData,
expireId: null,
})
private readonly revertTime = 2000
sequence = 0
@@ -56,20 +55,7 @@ export class MockApiService extends ApiService {
// db
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
return this.getDump()
}
async getDump(): Promise<RR.GetDumpRes> {
const cache = await this.bootstrapper.init()
return {
id: cache.sequence,
value: cache.data,
expireId: null,
}
}
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
async setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
await pauseFor(2000)
const patch = [
{
@@ -87,7 +73,7 @@ export class MockApiService extends ApiService {
await pauseFor(2000)
setTimeout(() => {
this.mockPatch$.next({ id: 1, value: mockPatchData, expireId: null })
this.mockWsSource$.next({ id: 1, value: mockPatchData })
}, 2000)
return null
@@ -105,7 +91,7 @@ export class MockApiService extends ApiService {
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
await pauseFor(2000)
return { response: null }
return null
}
// server
@@ -116,7 +102,7 @@ export class MockApiService extends ApiService {
}
openPatchWebsocket$(): Observable<Update<DataModel>> {
return this.mockPatch$
return this.mockWsSource$
}
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
@@ -198,9 +184,7 @@ export class MockApiService extends ApiService {
return Mock.getAppMetrics()
}
async updateServerRaw(
params: RR.UpdateServerReq,
): Promise<RR.UpdateServerRes> {
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
await pauseFor(2000)
const initialProgress = {
size: 10000,
@@ -289,7 +273,7 @@ export class MockApiService extends ApiService {
// notification
async getNotificationsRaw(
async getNotifications(
params: RR.GetNotificationsReq,
): Promise<RR.GetNotificationsRes> {
await pauseFor(2000)
@@ -418,9 +402,7 @@ export class MockApiService extends ApiService {
return Mock.BackupInfo
}
async createBackupRaw(
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes> {
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
await pauseFor(2000)
const path = '/server-info/status-info/backup-progress'
const ids = params['package-ids']
@@ -436,17 +418,17 @@ export class MockApiService extends ApiService {
value: PackageMainStatus.BackingUp,
},
]
this.updateMock(appPatch)
this.mockRevision(appPatch)
await pauseFor(8000)
this.updateMock([
this.mockRevision([
{
...appPatch[0],
value: PackageMainStatus.Stopped,
},
])
this.updateMock([
this.mockRevision([
{
op: PatchOp.REPLACE,
path: `${path}/${id}/complete`,
@@ -465,7 +447,7 @@ export class MockApiService extends ApiService {
value: null,
},
]
this.updateMock(lastPatch)
this.mockRevision(lastPatch)
}, 500)
const originalPatch = [
@@ -525,7 +507,7 @@ export class MockApiService extends ApiService {
}
}
async installPackageRaw(
async installPackage(
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes> {
await pauseFor(2000)
@@ -582,7 +564,7 @@ export class MockApiService extends ApiService {
return {}
}
async setPackageConfigRaw(
async setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
await pauseFor(2000)
@@ -596,7 +578,7 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async restorePackagesRaw(
async restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {
await pauseFor(2000)
@@ -627,9 +609,7 @@ export class MockApiService extends ApiService {
return Mock.ActionResponse
}
async startPackageRaw(
params: RR.StartPackageReq,
): Promise<RR.StartPackageRes> {
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
const path = `/package-data/${params.id}/installed/status/main`
await pauseFor(2000)
@@ -647,7 +627,7 @@ export class MockApiService extends ApiService {
value: new Date().toISOString(),
},
]
this.updateMock(patch2)
this.mockRevision(patch2)
const patch3 = [
{
@@ -663,7 +643,7 @@ export class MockApiService extends ApiService {
},
},
]
this.updateMock(patch3)
this.mockRevision(patch3)
await pauseFor(2000)
@@ -692,7 +672,7 @@ export class MockApiService extends ApiService {
},
},
]
this.updateMock(patch4)
this.mockRevision(patch4)
}, 2000)
const originalPatch = [
@@ -706,7 +686,7 @@ export class MockApiService extends ApiService {
return this.withRevision(originalPatch)
}
async restartPackageRaw(
async restartPackage(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
// first enact stop
@@ -744,7 +724,7 @@ export class MockApiService extends ApiService {
},
} as any,
]
this.updateMock(patch2)
this.mockRevision(patch2)
}, this.revertTime)
const patch = [
@@ -763,7 +743,7 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main`
@@ -775,7 +755,7 @@ export class MockApiService extends ApiService {
value: PackageMainStatus.Stopped,
},
]
this.updateMock(patch2)
this.mockRevision(patch2)
}, this.revertTime)
const patch = [
@@ -794,7 +774,7 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async uninstallPackageRaw(
async uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
await pauseFor(2000)
@@ -806,7 +786,7 @@ export class MockApiService extends ApiService {
path: `/package-data/${params.id}`,
},
]
this.updateMock(patch2)
this.mockRevision(patch2)
}, this.revertTime)
const patch = [
@@ -820,7 +800,7 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async deleteRecoveredPackageRaw(
async deleteRecoveredPackage(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
await pauseFor(2000)
@@ -878,7 +858,7 @@ export class MockApiService extends ApiService {
value: { ...progress },
},
]
this.updateMock(patch)
this.mockRevision(patch)
}
}
@@ -898,7 +878,7 @@ export class MockApiService extends ApiService {
path: `/recovered-packages/${id}`,
},
]
this.updateMock(patch2)
this.mockRevision(patch2)
}, 1000)
}
@@ -914,7 +894,7 @@ export class MockApiService extends ApiService {
value: downloaded,
},
]
this.updateMock(patch)
this.mockRevision(patch)
}
const patch2 = [
@@ -924,7 +904,7 @@ export class MockApiService extends ApiService {
value: size,
},
]
this.updateMock(patch2)
this.mockRevision(patch2)
setTimeout(async () => {
const patch3: Operation<ServerStatus>[] = [
@@ -938,7 +918,7 @@ export class MockApiService extends ApiService {
path: '/server-info/status-info/update-progress',
},
]
this.updateMock(patch3)
this.mockRevision(patch3)
// quickly revert server to "running" for continued testing
await pauseFor(100)
const patch4 = [
@@ -948,7 +928,7 @@ export class MockApiService extends ApiService {
value: ServerStatus.Running,
},
]
this.updateMock(patch4)
this.mockRevision(patch4)
// set patch indicating update is complete
await pauseFor(100)
const patch6 = [
@@ -958,11 +938,11 @@ export class MockApiService extends ApiService {
value: Mock.ServerUpdated,
},
]
this.updateMock(patch6)
this.mockRevision(patch6)
}, 1000)
}
private async updateMock<T>(patch: Operation<T>[]): Promise<void> {
private async mockRevision<T>(patch: Operation<T>[]): Promise<void> {
if (!this.sequence) {
const { sequence } = await this.bootstrapper.init()
this.sequence = sequence
@@ -970,26 +950,26 @@ export class MockApiService extends ApiService {
const revision = {
id: ++this.sequence,
patch,
expireId: null,
}
this.mockPatch$.next(revision)
this.mockWsSource$.next(revision)
}
private async withRevision<T>(
patch: Operation<unknown>[],
response: T | null = null,
): Promise<WithRevision<T>> {
): Promise<T> {
if (!this.sequence) {
const { sequence } = await this.bootstrapper.init()
this.sequence = sequence
}
const revision = {
id: ++this.sequence,
patch,
expireId: null,
}
this.patchStream$.next([
{
id: ++this.sequence,
patch,
},
])
return { response, revision }
return response as T
}
}

View File

@@ -28,9 +28,10 @@ export const mockPatchData: DataModel = {
'server-info': {
id: 'abcdefgh',
version: '0.3.1.1',
'last-backup': null,
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
'lan-address': 'https://embassy-abcdefgh.local',
'tor-address': 'http://myveryownspecialtoraddress.onion',
'last-wifi-region': null,
'unread-notification-count': 4,
// password is asdfasdf
'password-hash':

View File

@@ -1,6 +1,6 @@
import { Injectable, NgZone } from '@angular/core'
import { ReplaySubject } from 'rxjs'
import { map } from 'rxjs/operators'
import { distinctUntilChanged, map } from 'rxjs/operators'
import { Storage } from '@ionic/storage-angular'
import { Router } from '@angular/router'
@@ -17,6 +17,7 @@ export class AuthService {
readonly isVerified$ = this.authState$.pipe(
map(state => state === AuthState.VERIFIED),
distinctUntilChanged(),
)
constructor(

View File

@@ -17,6 +17,9 @@ export class ConnectionService {
readonly websocketConnected$ = new ReplaySubject<boolean>(1)
readonly connected$ = combineLatest([
this.networkConnected$,
this.websocketConnected$,
]).pipe(map(([network, websocket]) => network && websocket))
this.websocketConnected$.pipe(distinctUntilChanged()),
]).pipe(
map(([network, websocket]) => network && websocket),
distinctUntilChanged(),
)
}

View File

@@ -5,8 +5,9 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators'
import { MarketplaceEOS } 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 { getServerInfo } from 'src/app/util/get-server-info'
import { DataModel } from './patch-db/data-model'
@Injectable({
providedIn: 'root',
@@ -49,7 +50,7 @@ export class EOSService {
constructor(
private readonly api: ApiService,
private readonly emver: Emver,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
) {}
async getEOS(): Promise<boolean> {

View File

@@ -12,10 +12,11 @@ import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import {
DataModel,
ServerInfo,
UIMarketplaceData,
} 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 {
catchError,
distinctUntilChanged,
@@ -119,7 +120,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
constructor(
private readonly api: ApiService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
private readonly errToast: ErrorToastService,
private readonly emver: Emver,

View File

@@ -3,7 +3,7 @@ import { ModalController } from '@ionic/angular'
import { Observable } from 'rxjs'
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
import { exists, isEmptyObject } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
import { EOSService } from 'src/app/services/eos.service'
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
@@ -33,7 +33,7 @@ export class PatchDataService extends Observable<DataModel> {
)
constructor(
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly eosService: EOSService,
private readonly config: ConfigService,
private readonly modalCtrl: ModalController,

View File

@@ -52,6 +52,7 @@ export interface ServerInfo {
'last-backup': string | null
'lan-address': Url
'tor-address': Url
'last-wifi-region': string | null
'unread-notification-count': number
'status-info': ServerStatusInfo
'eos-version-compat': string

View File

@@ -1,41 +1,45 @@
import { InjectionToken } from '@angular/core'
import { catchError, switchMap, take, tap } from 'rxjs/operators'
import { Bootstrapper, DBCache, Update } from 'patch-db-client'
import { InjectionToken, Injector } from '@angular/core'
import { bufferTime, catchError, switchMap, take, tap } from 'rxjs/operators'
import { Update } from 'patch-db-client'
import { DataModel } from './data-model'
import { EMPTY, from, interval, merge, Observable } from 'rxjs'
import { defer, EMPTY, from, interval, merge, Observable } from 'rxjs'
import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service'
import { ApiService } from '../api/embassy-api.service'
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>>>(
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
'',
)
export const PATCH_CACHE = new InjectionToken<DBCache<DataModel>>('', {
factory: () => ({} as any),
})
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
export function sourceFactory(
api: ApiService,
authService: AuthService,
connectionService: ConnectionService,
): Observable<Update<DataModel>> {
const websocket$ = api.openPatchWebsocket$().pipe(
catchError((_, watch$) => {
connectionService.websocketConnected$.next(false)
injector: Injector,
): Observable<Update<DataModel>[]> {
// defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there
return defer(() => {
const api = injector.get(ApiService)
const authService = injector.get(AuthService)
const connectionService = injector.get(ConnectionService)
return interval(4000).pipe(
switchMap(() =>
from(api.echo({ message: 'ping' })).pipe(catchError(() => EMPTY)),
),
take(1),
switchMap(() => watch$),
)
}),
tap(() => connectionService.websocketConnected$.next(true)),
)
const websocket$ = api.openPatchWebsocket$().pipe(
bufferTime(250),
catchError((_, watch$) => {
connectionService.websocketConnected$.next(false)
return authService.isVerified$.pipe(
switchMap(verified => (verified ? merge(websocket$, api.sync$) : EMPTY)),
)
return interval(4000).pipe(
switchMap(() =>
from(api.echo({ message: 'ping' })).pipe(catchError(() => EMPTY)),
),
take(1),
switchMap(() => watch$),
)
}),
tap(() => connectionService.websocketConnected$.next(true)),
)
return authService.isVerified$.pipe(
switchMap(verified =>
verified ? merge(websocket$, api.patchStream$) : EMPTY,
),
)
})
}

View File

@@ -1,31 +1,18 @@
import { PatchDB } from 'patch-db-client'
import { NgModule } from '@angular/core'
import {
BOOTSTRAPPER,
PATCH_CACHE,
PATCH_SOURCE,
sourceFactory,
} from './patch-db.factory'
import { LocalStorageBootstrap } from './local-storage-bootstrap'
import { ApiService } from '../api/embassy-api.service'
import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service'
import { Injector, NgModule } from '@angular/core'
import { PATCH_SOURCE, sourceFactory } from './patch-db.factory'
// This module is purely for providers organization purposes
@NgModule({
providers: [
{
provide: BOOTSTRAPPER,
useExisting: LocalStorageBootstrap,
},
{
provide: PATCH_SOURCE,
deps: [ApiService, AuthService, ConnectionService],
deps: [Injector],
useFactory: sourceFactory,
},
{
provide: PatchDB,
deps: [PATCH_SOURCE, PATCH_CACHE],
deps: [PATCH_SOURCE],
useClass: PatchDB,
},
],

View File

@@ -1,64 +0,0 @@
import { Inject, Injectable } from '@angular/core'
import { Bootstrapper, PatchDB, Store } from 'patch-db-client'
import { Observable, of, Subscription } from 'rxjs'
import { catchError, debounceTime, finalize, tap } from 'rxjs/operators'
import { DataModel } from './data-model'
import { BOOTSTRAPPER } from './patch-db.factory'
@Injectable({
providedIn: 'root',
})
export class PatchDbService {
private sub?: Subscription
constructor(
@Inject(BOOTSTRAPPER)
private readonly bootstrapper: Bootstrapper<DataModel>,
private readonly patchDb: PatchDB<DataModel>,
) {}
start(): void {
// Early return if already started
if (this.sub) {
return
}
console.log('patchDB: STARTING')
this.sub = this.patchDb.cache$
.pipe(
debounceTime(420),
tap(cache => {
this.bootstrapper.update(cache)
}),
)
.subscribe()
}
stop(): void {
// Early return if already stopped
if (!this.sub) {
return
}
console.log('patchDB: STOPPING')
this.patchDb.store.reset()
this.sub.unsubscribe()
this.sub = undefined
}
// prettier-ignore
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
const argsString = '/' + args.join('/')
console.log('patchDB: WATCHING ', argsString)
return this.patchDb.store.watch$(...(args as [])).pipe(
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
catchError(e => {
console.error('patchDB: WATCH ERROR', e)
return of(e.message)
}),
finalize(() => console.log('patchDB: UNSUBSCRIBING', argsString)),
)
}
}

View File

@@ -1,28 +1,31 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { tap } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import { AuthService } from 'src/app/services/auth.service'
import { DataModel } from './patch-db/data-model'
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
// Start and stop PatchDb upon verification
@Injectable({
providedIn: 'root',
})
export class PatchMonitorService extends Observable<boolean> {
export class PatchMonitorService extends Observable<any> {
// @TODO not happy with Observable<void>
private readonly stream$ = this.authService.isVerified$.pipe(
map(verified => {
tap(verified => {
if (verified) {
this.patch.start()
return true
this.patch.start(this.bootstrapper)
} else {
this.patch.stop()
}
this.patch.stop()
return false
}),
)
constructor(
private readonly authService: AuthService,
private readonly patch: PatchDbService,
private readonly patch: PatchDB<DataModel>,
private readonly bootstrapper: LocalStorageBootstrap,
) {
super(subscriber => this.stream$.subscribe(subscriber))
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'
import { PatchDbService } from './patch-db/patch-db.service'
import { PatchDB } from 'patch-db-client'
import { combineLatest, filter, map, Observable } from 'rxjs'
import { DataModel } from './patch-db/data-model'
export interface ServerNameInfo {
current: string
@@ -26,5 +27,5 @@ export class ServerNameService {
}),
)
constructor(private readonly patch: PatchDbService) {}
constructor(private readonly patch: PatchDB<DataModel>) {}
}

View File

@@ -1,9 +1,12 @@
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { UIMarketplaceData } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
UIMarketplaceData,
} from 'src/app/services/patch-db/data-model'
import { filter, firstValueFrom } from 'rxjs'
export function getMarketplace(
patch: PatchDbService,
patch: PatchDB<DataModel>,
): Promise<UIMarketplaceData> {
return firstValueFrom(patch.watch$('ui', 'marketplace').pipe(filter(Boolean)))
}

View File

@@ -1,16 +1,19 @@
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 { filter, firstValueFrom } from 'rxjs'
export function getPackage(
patch: PatchDbService,
patch: PatchDB<DataModel>,
id: string,
): Promise<PackageDataEntry> {
return firstValueFrom(patch.watch$('package-data', id))
}
export function getAllPackages(
patch: PatchDbService,
patch: PatchDB<DataModel>,
): Promise<Record<string, PackageDataEntry>> {
return firstValueFrom(patch.watch$('package-data').pipe(filter(Boolean)))
}

View File

@@ -1,7 +1,7 @@
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ServerInfo } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model'
import { filter, firstValueFrom } from 'rxjs'
export function getServerInfo(patch: PatchDbService): Promise<ServerInfo> {
export function getServerInfo(patch: PatchDB<DataModel>): Promise<ServerInfo> {
return firstValueFrom(patch.watch$('server-info').pipe(filter(Boolean)))
}