chore: enable strict mode (#1569)

* chore: enable strict mode

* refactor: remove sync data access from PatchDbService

* launchable even when no LAN url

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Alex Inkin
2022-07-22 18:51:08 +03:00
committed by GitHub
parent 9a01a0df8e
commit 7b8a0eadf3
130 changed files with 1130 additions and 1045 deletions

View File

@@ -12,15 +12,13 @@ var convert = new Convert({
styleUrls: ['./logs.page.scss'], styleUrls: ['./logs.page.scss'],
}) })
export class LogsPage { export class LogsPage {
@ViewChild(IonContent) private content: IonContent @ViewChild(IonContent) private content?: IonContent
loading = true loading = true
loadingMore = false loadingMore = false
logs: string
needInfinite = true needInfinite = true
startCursor: string startCursor?: string
endCursor: string endCursor?: string
limit = 200 limit = 200
scrollToBottomButton = false
isOnBottom = true isOnBottom = true
constructor(private readonly api: ApiService) {} constructor(private readonly api: ApiService) {}
@@ -52,7 +50,7 @@ export class LogsPage {
// scroll down // scroll down
scrollBy(0, afterContainerHeight - beforeContainerHeight) scrollBy(0, afterContainerHeight - beforeContainerHeight)
this.content.scrollToPoint( this.content?.scrollToPoint(
0, 0,
afterContainerHeight - beforeContainerHeight, afterContainerHeight - beforeContainerHeight,
) )
@@ -117,7 +115,7 @@ export class LogsPage {
} }
scrollToBottom() { scrollToBottom() {
this.content.scrollToBottom(500) this.content?.scrollToBottom(500)
} }
async loadData(e: any): Promise<void> { async loadData(e: any): Promise<void> {

View File

@@ -9,5 +9,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
}) })
export class ItemComponent { export class ItemComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
} }

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@@ -6,7 +7,7 @@ import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component' import { ItemComponent } from './item.component'
@NgModule({ @NgModule({
imports: [IonicModule, RouterModule, SharedPipesModule], imports: [CommonModule, IonicModule, RouterModule, SharedPipesModule],
declarations: [ItemComponent], declarations: [ItemComponent],
exports: [ItemComponent], exports: [ItemComponent],
}) })

View File

@@ -10,5 +10,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
}) })
export class AboutComponent { export class AboutComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
} }

View File

@@ -18,7 +18,7 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
}) })
export class AdditionalComponent { export class AdditionalComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
@Output() @Output()
version = new EventEmitter<string>() version = new EventEmitter<string>()

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { MarkdownModule } from '@start9labs/shared' import { MarkdownModule } from '@start9labs/shared'
@@ -5,7 +6,7 @@ import { MarkdownModule } from '@start9labs/shared'
import { AdditionalComponent } from './additional.component' import { AdditionalComponent } from './additional.component'
@NgModule({ @NgModule({
imports: [IonicModule, MarkdownModule], imports: [CommonModule, IonicModule, MarkdownModule],
declarations: [AdditionalComponent], declarations: [AdditionalComponent],
exports: [AdditionalComponent], exports: [AdditionalComponent],
}) })

View File

@@ -9,7 +9,7 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
}) })
export class DependenciesComponent { export class DependenciesComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
getImg(key: string): string { getImg(key: string): string {
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon

View File

@@ -10,5 +10,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
}) })
export class PackageComponent { export class PackageComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
} }

View File

@@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared' import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
@@ -5,7 +6,7 @@ import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { PackageComponent } from './package.component' import { PackageComponent } from './package.component'
@NgModule({ @NgModule({
imports: [IonicModule, SharedPipesModule, EmverPipesModule], imports: [CommonModule, IonicModule, SharedPipesModule, EmverPipesModule],
declarations: [PackageComponent], declarations: [PackageComponent],
exports: [PackageComponent], exports: [PackageComponent],
}) })

View File

@@ -1,24 +1,33 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title> <ion-title>
{{ !!storageDrive ? 'Set Password' : 'Unlock Drive' }} {{ storageDrive ? 'Set Password' : 'Unlock Drive' }}
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<div style="padding: 8px 24px;"> <div style="padding: 8px 24px">
<div style="padding-bottom: 16px;"> <div style="padding-bottom: 16px">
<ng-container *ngIf="!!storageDrive"> <ng-template #choose>
<p>Choose a password for your Embassy. <i>Make it good. Write it down.</i></p> <p>
<p style="color: var(--ion-color-warning);">Losing your password can result in total loss of data.</p> Choose a password for your Embassy.
</ng-container> <i>Make it good. Write it down.</i>
<p *ngIf="!storageDrive">Enter the password that was used to encrypt this drive.</p> </p>
<p style="color: var(--ion-color-warning)">
Losing your password can result in total loss of data.
</p>
</ng-template>
<p *ngIf="!storageDrive else choose">
Enter the password that was used to encrypt this drive.
</p>
</div> </div>
<form (ngSubmit)="!!storageDrive ? submitPw() : verifyPw()"> <form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
<p>Password</p> <p>Password</p>
<ion-item [class]="pwError ? 'error-border' : password && !!storageDrive ? 'success-border' : ''"> <ion-item
[class]="pwError ? 'error-border' : password && storageDrive ? 'success-border' : ''"
>
<ion-input <ion-input
#focusInput #focusInput
[(ngModel)]="password" [(ngModel)]="password"
@@ -29,15 +38,23 @@
maxlength="64" maxlength="64"
></ion-input> ></ion-input>
<ion-button fill="clear" color="light" (click)="unmasked1 = !unmasked1"> <ion-button fill="clear" color="light" (click)="unmasked1 = !unmasked1">
<ion-icon slot="icon-only" [name]="unmasked1 ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon> <ion-icon
slot="icon-only"
[name]="unmasked1 ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
<div style="height: 16px;"> <div style="height: 16px">
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ pwError }}</p> <p style="color: var(--ion-color-danger); font-size: x-small">
{{ pwError }}
</p>
</div> </div>
<ng-container *ngIf="!!storageDrive"> <ng-container *ngIf="storageDrive">
<p>Confirm Password</p> <p>Confirm Password</p>
<ion-item [class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''"> <ion-item
[class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''"
>
<ion-input <ion-input
[(ngModel)]="passwordVer" [(ngModel)]="passwordVer"
[ngModelOptions]="{'standalone': true}" [ngModelOptions]="{'standalone': true}"
@@ -46,12 +63,22 @@
maxlength="64" maxlength="64"
placeholder="Retype Password" placeholder="Retype Password"
></ion-input> ></ion-input>
<ion-button fill="clear" color="light" (click)="unmasked2 = !unmasked2"> <ion-button
<ion-icon slot="icon-only" [name]="unmasked2 ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon> fill="clear"
color="light"
(click)="unmasked2 = !unmasked2"
>
<ion-icon
slot="icon-only"
[name]="unmasked2 ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
<div style="height: 16px;"> <div style="height: 16px">
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ verError }}</p> <p style="color: var(--ion-color-danger); font-size: x-small">
{{ verError }}
</p>
</div> </div>
</ng-container> </ng-container>
<input type="submit" style="display: none" /> <input type="submit" style="display: none" />
@@ -61,12 +88,24 @@
<ion-footer> <ion-footer>
<ion-toolbar> <ion-toolbar>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()"> <ion-button
class="ion-padding-end"
slot="end"
color="dark"
fill="clear"
(click)="cancel()"
>
Cancel Cancel
</ion-button> </ion-button>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="!!storageDrive ? submitPw() : verifyPw()"> <ion-button
{{ !!storageDrive ? 'Finish' : 'Unlock' }} class="ion-padding-end"
slot="end"
color="dark"
fill="clear"
strong="true"
(click)="storageDrive ? submitPw() : verifyPw()"
>
{{ storageDrive ? 'Finish' : 'Unlock' }}
</ion-button> </ion-button>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>

View File

@@ -13,9 +13,9 @@ import * as argon2 from '@start9labs/argon2'
styleUrls: ['password.page.scss'], styleUrls: ['password.page.scss'],
}) })
export class PasswordPage { export class PasswordPage {
@ViewChild('focusInput') elem: IonInput @ViewChild('focusInput') elem?: IonInput
@Input() target: CifsBackupTarget | DiskBackupTarget @Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() storageDrive: DiskInfo @Input() storageDrive?: DiskInfo
pwError = '' pwError = ''
password = '' password = ''
@@ -28,7 +28,7 @@ export class PasswordPage {
constructor(private modalController: ModalController) {} constructor(private modalController: ModalController) {}
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => this.elem.setFocus(), 400) setTimeout(() => this.elem?.setFocus(), 400)
} }
async verifyPw() { async verifyPw() {
@@ -36,7 +36,7 @@ export class PasswordPage {
this.pwError = 'No recovery target' // unreachable this.pwError = 'No recovery target' // unreachable
try { try {
const passwordHash = this.target['embassy-os']?.['password-hash'] || '' const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, this.password) argon2.verify(passwordHash, this.password)
this.modalController.dismiss({ password: this.password }, 'success') this.modalController.dismiss({ password: this.password }, 'success')

View File

@@ -9,8 +9,8 @@ import { HttpService } from 'src/app/services/api/http.service'
styleUrls: ['prod-key-modal.page.scss'], styleUrls: ['prod-key-modal.page.scss'],
}) })
export class ProdKeyModal { export class ProdKeyModal {
@ViewChild('focusInput') elem: IonInput @ViewChild('focusInput') elem?: IonInput
@Input() target: DiskBackupTarget @Input() target!: DiskBackupTarget
error = '' error = ''
productKey = '' productKey = ''
@@ -24,7 +24,7 @@ export class ProdKeyModal {
) {} ) {}
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => this.elem.setFocus(), 400) setTimeout(() => this.elem?.setFocus(), 400)
} }
async verifyProductKey() { async verifyProductKey() {

View File

@@ -10,24 +10,24 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['product-key.page.scss'], styleUrls: ['product-key.page.scss'],
}) })
export class ProductKeyPage { export class ProductKeyPage {
@ViewChild('focusInput') elem: IonInput @ViewChild('focusInput') elem?: IonInput
productKey: string productKey = ''
error: string error = ''
constructor ( constructor(
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly stateService: StateService, private readonly stateService: StateService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly httpService: HttpService, private readonly httpService: HttpService,
) { } ) {}
ionViewDidEnter () { ionViewDidEnter() {
setTimeout(() => this.elem.setFocus(), 400) setTimeout(() => this.elem?.setFocus(), 400)
} }
async submit () { async submit() {
if (!this.productKey) return this.error = 'Must enter product key' if (!this.productKey) return (this.error = 'Must enter product key')
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key', message: 'Verifying Product Key',
@@ -50,4 +50,3 @@ export class ProductKeyPage {
} }
} }
} }

View File

@@ -31,7 +31,7 @@ export class RecoverPage {
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService, private readonly errorToastService: ErrorToastService,
public readonly stateService: StateService, private readonly stateService: StateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -243,8 +243,8 @@ export class RecoverPage {
styleUrls: ['./recover.page.scss'], styleUrls: ['./recover.page.scss'],
}) })
export class DriveStatusComponent { export class DriveStatusComponent {
@Input() hasValidBackup: boolean @Input() hasValidBackup!: boolean
@Input() is02x: boolean @Input() is02x!: boolean
} }
interface MappedDisk { interface MappedDisk {

View File

@@ -13,7 +13,7 @@
<ion-card-content> <ion-card-content>
<br /> <br />
<ng-template <ng-template
[ngIf]="stateService.recoverySource && stateService.recoverySource.type === 'disk'" [ngIf]="recoverySource && recoverySource.type === 'disk'"
> >
<h2>You can now safely unplug your backup drive.</h2> <h2>You can now safely unplug your backup drive.</h2>
</ng-template> </ng-template>
@@ -53,15 +53,13 @@
<ion-item lines="none" color="dark"> <ion-item lines="none" color="dark">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<code <code
><ion-text color="light" ><ion-text color="light">{{ torAddress }}</ion-text></code
>{{ stateService.torAddress }}</ion-text
></code
> >
</ion-label> </ion-label>
<ion-button <ion-button
color="light" color="light"
fill="clear" fill="clear"
(click)="copy(stateService.torAddress)" (click)="copy(torAddress)"
> >
<ion-icon slot="icon-only" name="copy-outline"></ion-icon> <ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button> </ion-button>
@@ -133,15 +131,13 @@
<ion-item lines="none" color="dark"> <ion-item lines="none" color="dark">
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<code <code
><ion-text color="light" ><ion-text color="light">{{ lanAddress }}</ion-text></code
>{{ stateService.lanAddress }}</ion-text
></code
> >
</ion-label> </ion-label>
<ion-button <ion-button
color="light" color="light"
fill="clear" fill="clear"
(click)="copy(stateService.lanAddress)" (click)="copy(lanAddress)"
> >
<ion-icon slot="icon-only" name="copy-outline"></ion-icon> <ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button> </ion-button>

View File

@@ -16,9 +16,21 @@ export class SuccessPage {
constructor( constructor(
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
private readonly errCtrl: ErrorToastService, private readonly errCtrl: ErrorToastService,
public readonly stateService: StateService, private readonly stateService: StateService,
) {} ) {}
get recoverySource() {
return this.stateService.recoverySource
}
get torAddress() {
return this.stateService.torAddress
}
get lanAddress() {
return this.stateService.lanAddress
}
async ngAfterViewInit() { async ngAfterViewInit() {
try { try {
await this.stateService.completeEmbassy() await this.stateService.completeEmbassy()

View File

@@ -11,26 +11,26 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
providedIn: 'root', providedIn: 'root',
}) })
export class StateService { export class StateService {
hasProductKey: boolean hasProductKey = false
isMigrating: boolean isMigrating = false
polling = false polling = false
embassyLoaded = false embassyLoaded = false
recoverySource: CifsRecoverySource | DiskRecoverySource recoverySource?: CifsRecoverySource | DiskRecoverySource
recoveryPassword?: string recoveryPassword?: string
dataTransferProgress: { dataTransferProgress?: {
bytesTransferred: number bytesTransferred: number
totalBytes: number totalBytes: number
complete: boolean complete: boolean
} | null }
dataProgress = 0 dataProgress = 0
dataCompletionSubject = new BehaviorSubject(false) dataCompletionSubject = new BehaviorSubject(false)
torAddress: string torAddress = ''
lanAddress: string lanAddress = ''
cert: string cert = ''
constructor( constructor(
private readonly apiService: ApiService, private readonly apiService: ApiService,

View File

@@ -11,8 +11,8 @@ import { getErrorMessage } from '../../services/error-toast.service'
styleUrls: ['./markdown.component.scss'], styleUrls: ['./markdown.component.scss'],
}) })
export class MarkdownComponent { export class MarkdownComponent {
@Input() content?: string | Observable<string> @Input() content!: string | Observable<string>
@Input() title = '' @Input() title!: string
private readonly data$ = defer(() => private readonly data$ = defer(() =>
isObservable(this.content) ? this.content : of(this.content), isObservable(this.content) ? this.content : of(this.content),

View File

@@ -5,7 +5,7 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
name: 'trustUrl', name: 'trustUrl',
}) })
export class TrustUrlPipe implements PipeTransform { export class TrustUrlPipe implements PipeTransform {
constructor(public readonly sanitizer: DomSanitizer) {} constructor(private readonly sanitizer: DomSanitizer) {}
transform(base64Icon: string): SafeResourceUrl { transform(base64Icon: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(base64Icon) return this.sanitizer.bypassSecurityTrustResourceUrl(base64Icon)

View File

@@ -10,7 +10,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
// Watch unread notification count to display toast // Watch unread notification count to display toast
@Injectable() @Injectable()
export class UnreadToastService extends Observable<unknown> { export class UnreadToastService extends Observable<unknown> {
private unreadToast: HTMLIonToastElement private unreadToast?: HTMLIonToastElement
private readonly stream$ = this.patchData.pipe( private readonly stream$ = this.patchData.pipe(
switchMap<DataModel | null, ObservableInput<number>>(data => { switchMap<DataModel | null, ObservableInput<number>>(data => {

View File

@@ -16,7 +16,7 @@ import { PatchDataService } from './patch-data.service'
// Watch status to present toast for updated state // Watch status to present toast for updated state
@Injectable() @Injectable()
export class UpdateToastService extends Observable<unknown> { export class UpdateToastService extends Observable<unknown> {
private updateToast: HTMLIonToastElement private updateToast?: HTMLIonToastElement
private readonly stream$ = this.patchData.pipe( private readonly stream$ = this.patchData.pipe(
switchMap(data => { switchMap(data => {

View File

@@ -49,7 +49,13 @@
</ion-item> </ion-item>
</ion-menu-toggle> </ion-menu-toggle>
</ion-item-group> </ion-item-group>
<img appSnek class="snek" alt="Play Snek" src="assets/img/icons/snek.png" /> <img
appSnek
class="snek"
alt="Play Snek"
src="assets/img/icons/snek.png"
[appSnekHighScore]="snekScore$ | async"
/>
<div class="bottom"> <div class="bottom">
<div class="divider" style="margin-bottom: 10px"></div> <div class="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false"> <ion-menu-toggle auto-hide="false">

View File

@@ -51,6 +51,8 @@ export class MenuComponent {
'unread-notification-count', 'unread-notification-count',
) )
readonly snekScore$ = this.patch.watch$('ui', 'gaming', 'snake', 'high-score')
readonly showEOSUpdate$ = this.eosService.showUpdate$ readonly showEOSUpdate$ = this.eosService.showUpdate$
readonly showDevTools$ = this.localStorageService.showDevTools$ readonly showDevTools$ = this.localStorageService.showDevTools$

View File

@@ -1,4 +1,4 @@
import { Directive, HostListener } from '@angular/core' import { Directive, HostListener, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular' import { LoadingController, ModalController } from '@ionic/angular'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
@@ -10,12 +10,14 @@ import { ApiService } from '../../services/api/embassy-api.service'
selector: 'img[appSnek]', selector: 'img[appSnek]',
}) })
export class SnekDirective { export class SnekDirective {
@Input()
appSnekHighScore: number | null = null
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
) {} ) {}
@HostListener('click') @HostListener('click')
@@ -24,30 +26,28 @@ export class SnekDirective {
component: SnakePage, component: SnakePage,
cssClass: 'snake-modal', cssClass: 'snake-modal',
backdropDismiss: false, backdropDismiss: false,
componentProps: { highScore: this.appSnekHighScore || 0 },
}) })
modal.onDidDismiss().then(async ({ data }) => { modal.onDidDismiss().then(async ({ data }) => {
const highScore = if (data?.highScore <= (this.appSnekHighScore || 0)) return
this.patch.getData().ui.gaming?.snake?.['high-score'] || 0
if (data?.highScore > highScore) { const loader = await this.loadingCtrl.create({
const loader = await this.loadingCtrl.create({ message: 'Saving high score...',
message: 'Saving high score...', backdropDismiss: true,
backdropDismiss: true, })
await loader.present()
try {
await this.embassyApi.setDbValue({
pointer: '/gaming',
value: { snake: { 'high-score': data.highScore } },
}) })
} catch (e: any) {
await loader.present() this.errToast.present(e)
} finally {
try { this.loadingCtrl.dismiss()
await this.embassyApi.setDbValue({
pointer: '/gaming',
value: { snake: { 'high-score': data.highScore } },
})
} catch (e: any) {
this.errToast.present(e)
} finally {
this.loadingCtrl.dismiss()
}
} }
}) })

View File

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

View File

@@ -7,9 +7,8 @@ import { BaseSlide } from '../wizard-types'
styleUrls: ['../app-wizard.component.scss'], styleUrls: ['../app-wizard.component.scss'],
}) })
export class AlertComponent implements BaseSlide { export class AlertComponent implements BaseSlide {
@Input() params: { @Input()
message: string params!: { message: string }
}
async load() {} async load() {}

View File

@@ -22,7 +22,8 @@ SwiperCore.use([IonicSlides])
styleUrls: ['./app-wizard.component.scss'], styleUrls: ['./app-wizard.component.scss'],
}) })
export class AppWizardComponent { export class AppWizardComponent {
@Input() params: { @Input()
params!: {
action: WizardAction action: WizardAction
title: string title: string
slides: SlideDefinition[] slides: SlideDefinition[]
@@ -31,16 +32,17 @@ export class AppWizardComponent {
} }
// content container so we can scroll to top between slide transitions // content container so we can scroll to top between slide transitions
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent)
content?: IonContent
swiper: Swiper swiper?: Swiper
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view //a slide component gives us hook into a slide. Allows us to call load when slide comes into view
@ViewChildren('components') @ViewChildren('components')
slideComponentsQL: QueryList<BaseSlide> slideComponentsQL?: QueryList<BaseSlide>
get slideComponents(): BaseSlide[] { get slideComponents(): BaseSlide[] {
return this.slideComponentsQL.toArray() return this.slideComponentsQL?.toArray() || []
} }
get currentSlide(): BaseSlide { get currentSlide(): BaseSlide {
@@ -48,7 +50,7 @@ export class AppWizardComponent {
} }
get currentIndex(): number { get currentIndex(): number {
return this.swiper.activeIndex return this.swiper?.activeIndex || NaN
} }
initializing = true initializing = true
@@ -58,7 +60,7 @@ export class AppWizardComponent {
ionViewDidEnter() { ionViewDidEnter() {
this.initializing = false this.initializing = false
this.swiper.allowTouchMove = false if (this.swiper) this.swiper.allowTouchMove = false
this.loadSlide() this.loadSlide()
} }
@@ -71,8 +73,8 @@ export class AppWizardComponent {
} }
async next() { async next() {
await this.content.scrollToTop() await this.content?.scrollToTop()
this.swiper.slideNext(500) this.swiper?.slideNext(500)
} }
setError(e: any) { setError(e: any) {

View File

@@ -8,7 +8,8 @@ import { BaseSlide } from '../wizard-types'
styleUrls: ['../app-wizard.component.scss'], styleUrls: ['../app-wizard.component.scss'],
}) })
export class CompleteComponent implements BaseSlide { export class CompleteComponent implements BaseSlide {
@Input() params: { @Input()
params!: {
verb: string // loader verb: '*stopping* ...' verb: string // loader verb: '*stopping* ...'
title: string title: string
Fn: () => Promise<any> Fn: () => Promise<any>
@@ -17,13 +18,13 @@ export class CompleteComponent implements BaseSlide {
@Output() onSuccess: EventEmitter<void> = new EventEmitter() @Output() onSuccess: EventEmitter<void> = new EventEmitter()
@Output() onError: EventEmitter<string> = new EventEmitter() @Output() onError: EventEmitter<string> = new EventEmitter()
message: string message = ''
loading = true loading = true
async load() { async load() {
this.message = this.message =
capitalizeFirstLetter(this.params.verb) + ' ' + this.params.title capitalizeFirstLetter(this.params.verb || '') + ' ' + this.params.title
try { try {
await this.params.Fn() await this.params.Fn()
this.onSuccess.emit() this.onSuccess.emit()

View File

@@ -10,7 +10,8 @@ import { BaseSlide } from '../wizard-types'
styleUrls: ['./dependents.component.scss', '../app-wizard.component.scss'], styleUrls: ['./dependents.component.scss', '../app-wizard.component.scss'],
}) })
export class DependentsComponent implements BaseSlide { export class DependentsComponent implements BaseSlide {
@Input() params: { @Input()
params!: {
title: string title: string
verb: string // *Uninstalling* will cause problems... verb: string // *Uninstalling* will cause problems...
Fn: () => Promise<Breakages> Fn: () => Promise<Breakages>
@@ -19,21 +20,21 @@ export class DependentsComponent implements BaseSlide {
@Output() onSuccess: EventEmitter<void> = new EventEmitter() @Output() onSuccess: EventEmitter<void> = new EventEmitter()
@Output() onError: EventEmitter<string> = new EventEmitter() @Output() onError: EventEmitter<string> = new EventEmitter()
breakages: Breakages breakages?: Breakages
warningMessage: string | undefined warningMessage = ''
loading = true loading = true
readonly pkgs$ = this.patch.watch$('package-data') readonly pkgs$ = this.patch.watch$('package-data')
constructor(public readonly patch: PatchDbService) {} constructor(private readonly patch: PatchDbService) {}
async load() { async load() {
try { try {
this.breakages = await this.params.Fn() this.breakages = await this.params.Fn()
if (this.breakages && !isEmptyObject(this.breakages)) { if (this.breakages && !isEmptyObject(this.breakages)) {
this.warningMessage = this.warningMessage =
capitalizeFirstLetter(this.params.verb) + capitalizeFirstLetter(this.params.verb || '') +
' ' + ' ' +
this.params.title + this.params.title +
' will prohibit the following services from functioning properly.' ' will prohibit the following services from functioning properly.'

View File

@@ -7,7 +7,7 @@
type === 'create' ? 'Create Backup' : 'Restore From Backup' type === 'create' ? 'Create Backup' : 'Restore From Backup'
}}</ion-title> }}</ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button [disabled]="backupService.loading" (click)="refresh()"> <ion-button [disabled]="loading" (click)="refresh()">
Refresh Refresh
<ion-icon slot="end" name="refresh"></ion-icon> <ion-icon slot="end" name="refresh"></ion-icon>
</ion-button> </ion-button>

View File

@@ -3,17 +3,17 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<!-- loading --> <!-- loading -->
<text-spinner <text-spinner
*ngIf="backupService.loading; else loaded" *ngIf="loading; else loaded"
[text]="loadingText" [text]="loadingText"
></text-spinner> ></text-spinner>
<!-- loaded --> <!-- loaded -->
<ng-template #loaded> <ng-template #loaded>
<!-- error --> <!-- error -->
<ion-item *ngIf="backupService.loadingError; else noError"> <ion-item *ngIf="loadingError; else noError">
<ion-label> <ion-label>
<ion-text color="danger"> <ion-text color="danger">
{{ backupService.loadingError }} {{ loadingError }}
</ion-text> </ion-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
@@ -49,7 +49,7 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- cifs list --> <!-- cifs list -->
<ng-container *ngFor="let target of backupService.cifs; let i = index"> <ng-container *ngFor="let target of cifs; let i = index">
<ion-item <ion-item
button button
*ngIf="target.entry as cifs" *ngIf="target.entry as cifs"
@@ -91,7 +91,7 @@
<ion-item-divider>Physical Drives</ion-item-divider> <ion-item-divider>Physical Drives</ion-item-divider>
<!-- no drives --> <!-- no drives -->
<ion-item <ion-item
*ngIf="!backupService.drives.length; else hasDrives" *ngIf="!drives.length; else hasDrives"
class="ion-padding-bottom" class="ion-padding-bottom"
> >
<ion-label> <ion-label>
@@ -119,7 +119,7 @@
<ng-template #hasDrives> <ng-template #hasDrives>
<ion-item <ion-item
button button
*ngFor="let target of backupService.drives" *ngFor="let target of drives"
(click)="select(target)" (click)="select(target)"
> >
<ion-icon slot="start" name="save-outline" size="large"></ion-icon> <ion-icon slot="start" name="save-outline" size="large"></ion-icon>

View File

@@ -25,11 +25,11 @@ type BackupType = 'create' | 'restore'
styleUrls: ['./backup-drives.component.scss'], styleUrls: ['./backup-drives.component.scss'],
}) })
export class BackupDrivesComponent { export class BackupDrivesComponent {
@Input() type: BackupType @Input() type!: BackupType
@Output() onSelect: EventEmitter< @Output() onSelect: EventEmitter<
MappedBackupTarget<CifsBackupTarget | DiskBackupTarget> MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
> = new EventEmitter() > = new EventEmitter()
loadingText: string loadingText = ''
constructor( constructor(
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
@@ -38,9 +38,25 @@ export class BackupDrivesComponent {
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
public readonly backupService: BackupService, private readonly backupService: BackupService,
) {} ) {}
get loading() {
return this.backupService.loading
}
get loadingError() {
return this.backupService.loadingError
}
get drives() {
return this.backupService.drives
}
get cifs() {
return this.backupService.cifs
}
ngOnInit() { ngOnInit() {
this.loadingText = this.loadingText =
this.type === 'create' this.type === 'create'
@@ -234,10 +250,14 @@ export class BackupDrivesComponent {
styleUrls: ['./backup-drives.component.scss'], styleUrls: ['./backup-drives.component.scss'],
}) })
export class BackupDrivesHeaderComponent { export class BackupDrivesHeaderComponent {
@Input() type: BackupType @Input() type!: BackupType
@Output() onClose: EventEmitter<void> = new EventEmitter() @Output() onClose: EventEmitter<void> = new EventEmitter()
constructor(public readonly backupService: BackupService) {} constructor(private readonly backupService: BackupService) {}
get loading() {
return this.backupService.loading
}
refresh() { refresh() {
this.backupService.getBackupTargets() this.backupService.getBackupTargets()
@@ -250,8 +270,8 @@ export class BackupDrivesHeaderComponent {
styleUrls: ['./backup-drives.component.scss'], styleUrls: ['./backup-drives.component.scss'],
}) })
export class BackupDrivesStatusComponent { export class BackupDrivesStatusComponent {
@Input() type: string @Input() type!: BackupType
@Input() hasValidBackup: boolean @Input() hasValidBackup!: boolean
} }
const CifsSpec: ConfigSpec = { const CifsSpec: ConfigSpec = {

View File

@@ -13,10 +13,10 @@ import { getErrorMessage, Emver } from '@start9labs/shared'
providedIn: 'root', providedIn: 'root',
}) })
export class BackupService { export class BackupService {
cifs: MappedBackupTarget<CifsBackupTarget>[] cifs: MappedBackupTarget<CifsBackupTarget>[] = []
drives: MappedBackupTarget<DiskBackupTarget>[] drives: MappedBackupTarget<DiskBackupTarget>[] = []
loading = true loading = true
loadingError: string | IonicSafeString loadingError: string | IonicSafeString = ''
constructor( constructor(
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,

View File

@@ -8,32 +8,30 @@ import { combineLatest, Subscription } from 'rxjs'
templateUrl: './badge-menu.component.html', templateUrl: './badge-menu.component.html',
styleUrls: ['./badge-menu.component.scss'], styleUrls: ['./badge-menu.component.scss'],
}) })
export class BadgeMenuComponent { export class BadgeMenuComponent {
unreadCount: number unreadCount = 0
sidebarOpen: boolean sidebarOpen = false
subs: Subscription[] = [] subs: Subscription[] = []
constructor ( constructor(
private readonly splitPane: SplitPaneTracker, private readonly splitPane: SplitPaneTracker,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
) { } ) {}
ngOnInit () { ngOnInit() {
this.subs = [ this.subs = [
combineLatest([ combineLatest([
this.patch.watch$('server-info', 'unread-notification-count'), this.patch.watch$('server-info', 'unread-notification-count'),
this.splitPane.sidebarOpen$, this.splitPane.sidebarOpen$,
]) ]).subscribe(([unread, menu]) => {
.subscribe(([unread, menu]) => {
this.unreadCount = unread this.unreadCount = unread
this.sidebarOpen = menu this.sidebarOpen = menu
}), }),
] ]
} }
ngOnDestroy () { ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe()) this.subs.forEach(sub => sub.unsubscribe())
} }
} }

View File

@@ -18,8 +18,9 @@
(['string', 'number'] | includes: data.spec.type) && (['string', 'number'] | includes: data.spec.type) &&
!$any(data.spec).nullable !$any(data.spec).nullable
" "
>&nbsp;*</span
> >
&nbsp;*
</span>
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min" <span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min"
>&nbsp;*</span >&nbsp;*</span

View File

@@ -35,8 +35,8 @@ interface Config {
styleUrls: ['./form-object.component.scss'], styleUrls: ['./form-object.component.scss'],
}) })
export class FormObjectComponent { export class FormObjectComponent {
@Input() objectSpec: ConfigSpec @Input() objectSpec!: ConfigSpec
@Input() formGroup: FormGroup @Input() formGroup!: FormGroup
@Input() unionSpec?: ValueSpecUnion @Input() unionSpec?: ValueSpecUnion
@Input() current?: Config @Input() current?: Config
@Input() original?: Config @Input() original?: Config
@@ -396,7 +396,7 @@ interface HeaderData {
}) })
export class FormLabelComponent { export class FormLabelComponent {
Range = Range Range = Range
@Input() data: HeaderData @Input() data!: HeaderData
constructor(private readonly alertCtrl: AlertController) {} constructor(private readonly alertCtrl: AlertController) {}
@@ -424,6 +424,6 @@ export class FormLabelComponent {
styleUrls: ['./form-object.component.scss'], styleUrls: ['./form-object.component.scss'],
}) })
export class FormErrorComponent { export class FormErrorComponent {
@Input() control: AbstractFormGroupDirective @Input() control!: AbstractFormGroupDirective
@Input() spec: ValueSpec @Input() spec!: ValueSpec
} }

View File

@@ -9,7 +9,7 @@
*ngIf="!loading && needInfinite" *ngIf="!loading && needInfinite"
position="top" position="top"
threshold="0" threshold="0"
(ionInfinite)="loadData($event)" (ionInfinite)="doInfinite($event)"
> >
<ion-infinite-scroll-content <ion-infinite-scroll-content
loadingSpinner="lines" loadingSpinner="lines"
@@ -25,11 +25,11 @@
></div> ></div>
</div> </div>
<div id="button-div" *ngIf="!loading" style="width: 100%; text-align: center"> <div id="button-div" *ngIf="!loading" style="width: 100%; text-align: center">
<ion-button *ngIf="!loadingMore" (click)="loadMore()" strong color="dark"> <ion-button *ngIf="!loadingNext" (click)="getNext()" strong color="dark">
Load More Load More
<ion-icon slot="end" name="refresh"></ion-icon> <ion-icon slot="end" name="refresh"></ion-icon>
</ion-button> </ion-button>
<ion-spinner *ngIf="loadingMore" name="lines" color="warning"></ion-spinner> <ion-spinner *ngIf="loadingNext" name="lines" color="warning"></ion-spinner>
</div> </div>
<div <div

View File

@@ -17,29 +17,91 @@ var convert = new Convert({
styleUrls: ['./logs.page.scss'], styleUrls: ['./logs.page.scss'],
}) })
export class LogsPage { export class LogsPage {
@ViewChild(IonContent) private content: IonContent @ViewChild(IonContent)
@Input() fetchLogs: (params: { private content?: IonContent
@Input()
fetchLogs!: (params: {
before_flag?: boolean before_flag?: boolean
limit?: number limit?: number
cursor?: string cursor?: string
}) => Promise<RR.LogsRes> }) => Promise<RR.LogsRes>
loading = true loading = true
loadingMore = false loadingNext = false
logs: string
needInfinite = true needInfinite = true
startCursor: string startCursor?: string
endCursor: string endCursor?: string
limit = 200 limit = 400
scrollToBottomButton = false
isOnBottom = true isOnBottom = true
constructor(private readonly errToast: ErrorToastService) {} constructor(private readonly errToast: ErrorToastService) {}
ngOnInit() { async ngOnInit() {
this.getLogs() await this.getPrior()
this.loading = false
} }
async fetch(isBefore: boolean = true) { async getNext() {
this.loadingNext = true
const logs = await this.fetch(false)
if (!logs?.length) return (this.loadingNext = false)
const container = document.getElementById('container')
const newLogs = document.getElementById('template')?.cloneNode(true)
if (!(newLogs instanceof HTMLElement)) return
newLogs.innerHTML =
logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') +
(logs.length ? '\n' : '')
container?.append(newLogs)
this.loadingNext = false
this.scrollEvent()
}
async doInfinite(e: any): Promise<void> {
await this.getPrior()
e.target.complete()
}
scrollEvent() {
const buttonDiv = document.getElementById('button-div')
this.isOnBottom =
!!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
}
scrollToBottom() {
this.content?.scrollToBottom(500)
}
private async getPrior() {
// get logs
const logs = await this.fetch()
if (!logs?.length) return
const container = document.getElementById('container')
const beforeContainerHeight = container?.scrollHeight || 0
const newLogs = document.getElementById('template')?.cloneNode(true)
if (!(newLogs instanceof HTMLElement)) return
newLogs.innerHTML =
logs.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`).join('\n') +
(logs.length ? '\n' : '')
container?.prepend(newLogs)
const afterContainerHeight = container?.scrollHeight || 0
// scroll down
scrollBy(0, afterContainerHeight - beforeContainerHeight)
this.content?.scrollToPoint(0, afterContainerHeight - beforeContainerHeight)
if (logs.length < this.limit) {
this.needInfinite = false
}
}
private async fetch(isBefore: boolean = true) {
try { try {
const cursor = isBefore ? this.startCursor : this.endCursor const cursor = isBefore ? this.startCursor : this.endCursor
const logsRes = await this.fetchLogs({ const logsRes = await this.fetchLogs({
@@ -55,79 +117,10 @@ export class LogsPage {
if ((!isBefore || !this.endCursor) && logsRes['end-cursor']) { if ((!isBefore || !this.endCursor) && logsRes['end-cursor']) {
this.endCursor = logsRes['end-cursor'] this.endCursor = logsRes['end-cursor']
} }
this.loading = false
return logsRes.entries return logsRes.entries
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} }
} }
async getLogs() {
try {
// get logs
const logs = await this.fetch()
if (!logs?.length) return
const container = document.getElementById('container')
const beforeContainerHeight = container?.scrollHeight || 0
const newLogs = document.getElementById('template')?.cloneNode(true)
if (!(newLogs instanceof HTMLElement)) return
newLogs.innerHTML =
logs
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
.join('\n') + (logs.length ? '\n' : '')
container?.prepend(newLogs)
const afterContainerHeight = container?.scrollHeight || 0
// scroll down
scrollBy(0, afterContainerHeight - beforeContainerHeight)
this.content.scrollToPoint(
0,
afterContainerHeight - beforeContainerHeight,
)
if (logs.length < this.limit) {
this.needInfinite = false
}
} catch (e) {}
}
async loadMore() {
try {
this.loadingMore = true
const logs = await this.fetch(false)
if (!logs?.length) return (this.loadingMore = false)
const container = document.getElementById('container')
const newLogs = document.getElementById('template')?.cloneNode(true)
if (!(newLogs instanceof HTMLElement)) return
newLogs.innerHTML =
logs
.map(l => `${l.timestamp} ${convert.toHtml(l.message)}`)
.join('\n') + (logs.length ? '\n' : '')
container?.append(newLogs)
this.loadingMore = false
this.scrollEvent()
} catch (e) {}
}
scrollEvent() {
const buttonDiv = document.getElementById('button-div')
this.isOnBottom =
!!buttonDiv && buttonDiv.getBoundingClientRect().top < window.innerHeight
}
scrollToBottom() {
this.content.scrollToBottom(500)
}
async loadData(e: any): Promise<void> {
await this.getLogs()
e.target.complete()
}
} }

View File

@@ -6,5 +6,5 @@ import { Component, Input } from '@angular/core'
styleUrls: ['./qr.component.scss'], styleUrls: ['./qr.component.scss'],
}) })
export class QRComponent { export class QRComponent {
@Input() text: string @Input() text!: string
} }

View File

@@ -1,20 +1,18 @@
import { Component, Input } from '@angular/core' import { Component, Input, OnChanges } from '@angular/core'
@Component({ @Component({
selector: 'skeleton-list', selector: 'skeleton-list',
templateUrl: './skeleton-list.component.html', templateUrl: './skeleton-list.component.html',
styleUrls: ['./skeleton-list.component.scss'], styleUrls: ['./skeleton-list.component.scss'],
}) })
export class SkeletonListComponent { export class SkeletonListComponent implements OnChanges {
@Input() groups: string @Input() groups = 0
@Input() rows: string = '3' @Input() rows = 3
groupsArr: number[] = [] groupsArr: number[] = []
rowsArr: number[] = [] rowsArr: number[] = []
ngOnInit () { ngOnChanges() {
if (this.groups) { this.groupsArr = Array(this.groups).fill(0)
this.groupsArr = Array(Number(this.groups)).fill(0).map((_, i) => i) this.rowsArr = Array(this.rows).fill(0)
}
this.rowsArr = Array(Number(this.rows)).fill(0).map((_, i) => i)
} }
} }

View File

@@ -16,7 +16,7 @@ export class StatusComponent {
PS = PrimaryStatus PS = PrimaryStatus
PR = PrimaryRendering PR = PrimaryRendering
@Input() rendering: StatusRendering @Input() rendering!: StatusRendering
@Input() size?: string @Input() size?: string
@Input() style?: string = 'regular' @Input() style?: string = 'regular'
@Input() weight?: string = 'normal' @Input() weight?: string = 'normal'

View File

@@ -1,11 +1,11 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start"> <ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()"> <ion-button (click)="dismiss()">
<ion-icon name="close"></ion-icon> <ion-icon name="close"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ title }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
@@ -17,16 +17,15 @@
<h4>But you are currently connected to:</h4> <h4>But you are currently connected to:</h4>
<p class="courier-new color-primary-shade">{{ currentMarketplace }}</p> <p class="courier-new color-primary-shade">{{ currentMarketplace }}</p>
</div> </div>
<br />
<div> <div>
<p>To switch marketplaces visit your</p> <p>Switch to {{ packageMarketplace }} in</p>
<ion-button <ion-button
color="success" color="success"
routerLink="embassy/marketplaces" routerLink="embassy/marketplaces"
(click)="dismiss()" (click)="dismiss()"
>Marketplace Settings</ion-button >Marketplace Settings</ion-button
> >
<p>or you can</p> <p>Or you can</p>
<ion-button <ion-button
[routerLink]="['marketplace/', pkgId]" [routerLink]="['marketplace/', pkgId]"
click="dismiss()" click="dismiss()"

View File

@@ -13,4 +13,5 @@
background: rgba(53, 56, 62, 0.768); background: rgba(53, 56, 62, 0.768);
border-radius: 7px; border-radius: 7px;
padding: 27px; padding: 27px;
margin-bottom: 24px;
} }

View File

@@ -7,10 +7,10 @@ import { ModalController } from '@ionic/angular'
styleUrls: ['./action-marketplace.component.scss'], styleUrls: ['./action-marketplace.component.scss'],
}) })
export class ActionMarketplaceComponent { export class ActionMarketplaceComponent {
@Input() title: string @Input() title!: string
@Input() packageMarketplace: string @Input() packageMarketplace!: string
@Input() currentMarketplace: string @Input() currentMarketplace!: string
@Input() pkgId: string @Input() pkgId!: string
constructor(private readonly modalCtrl: ModalController) {} constructor(private readonly modalCtrl: ModalController) {}

View File

@@ -16,15 +16,24 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<div *ngIf="actionRes.value" class="ion-text-center" style="padding: 64px 0;"> <div *ngIf="actionRes.value" class="ion-text-center" style="padding: 64px 0">
<div *ngIf="actionRes.qr" class="ion-padding-bottom"> <div *ngIf="actionRes.qr" class="ion-padding-bottom">
<qr-code [value]="actionRes.value" size="240"></qr-code> <qr-code [value]="actionRes.value" size="240"></qr-code>
</div> </div>
<p *ngIf="!actionRes.copyable">{{ actionRes.value }}</p> <p *ngIf="!actionRes.copyable">{{ actionRes.value }}</p>
<a *ngIf="actionRes.copyable" style="cursor: copy;" (click)="copy(actionRes.value)"> <a
*ngIf="actionRes.copyable"
style="cursor: copy"
(click)="copy(actionRes.value)"
>
<b>{{ actionRes.value }}</b> <b>{{ actionRes.value }}</b>
<sup><ion-icon name="copy-outline" style="padding-left: 6px; font-size: small;"></ion-icon></sup> <sup
><ion-icon
name="copy-outline"
style="padding-left: 6px; font-size: small"
></ion-icon
></sup>
</a> </a>
</div> </div>
</ion-content> </ion-content>

View File

@@ -9,7 +9,8 @@ import { copyToClipboard } from 'src/app/util/web.util'
styleUrls: ['./action-success.page.scss'], styleUrls: ['./action-success.page.scss'],
}) })
export class ActionSuccessPage { export class ActionSuccessPage {
@Input() actionRes: ActionResponse @Input()
actionRes!: ActionResponse
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,

View File

@@ -11,10 +11,13 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<!-- loading --> <!-- loading -->
<text-spinner *ngIf="loadingText" [text]="loadingText"></text-spinner> <text-spinner
*ngIf="loading; else notLoading"
[text]="loadingText"
></text-spinner>
<!-- not loading --> <!-- not loading -->
<ng-container *ngIf="!loadingText && pkg"> <ng-template #notLoading>
<ion-item *ngIf="loadingError; else noError"> <ion-item *ngIf="loadingError; else noError">
<ion-label> <ion-label>
<ion-text color="danger"> {{ loadingError }} </ion-text> <ion-text color="danger"> {{ loadingError }} </ion-text>
@@ -22,21 +25,28 @@
</ion-item> </ion-item>
<ng-template #noError> <ng-template #noError>
<h2 <ng-container *ngIf="hasConfig && !pkg.installed?.status?.configured">
*ngIf="hasConfig && !pkg.installed?.status?.configured && !configForm.dirty" <ng-container *ngIf="!original; else hasOriginal">
class="ion-padding-bottom" <h2
> *ngIf="!configForm.dirty"
<ion-text class="header-details" color="success"> class="ion-padding-bottom header-details"
<span *ngIf="!original; else hasOriginal"> >
{{ pkg.manifest.title }} has been automatically configured with <ion-text color="success">
recommended defaults. Make whatever changes you want, then click {{ pkg.manifest.title }} has been automatically configured with
"Save". recommended defaults. Make whatever changes you want, then click
</span> "Save".
<ng-template #hasOriginal> </ion-text>
<span *ngIf="hasNewOptions"> New config options! </span> </h2>
</ng-template> </ng-container>
</ion-text> <ng-template #hasOriginal>
</h2> <h2 *ngIf="hasNewOptions" class="ion-padding-bottom header-details">
<ion-text color="success">
New config options! To accept the default values, click "Save".
You may also customize these new options below.
</ion-text>
</h2>
</ng-template>
</ng-container>
<!-- auto-config --> <!-- auto-config -->
<ion-item <ion-item
@@ -91,46 +101,40 @@
></form-object> ></form-object>
</form> </form>
</ng-template> </ng-template>
</ng-container> </ng-template>
</ion-content> </ion-content>
<ion-footer> <ion-footer>
<ion-toolbar> <ion-toolbar>
<ion-buttons <ng-container *ngIf="!loading && !loadingError">
*ngIf="!loadingText && !loadingError && hasConfig" <ion-buttons *ngIf="hasConfig" slot="start" class="ion-padding-start">
slot="start" <ion-button fill="clear" (click)="resetDefaults()">
class="ion-padding-start" <ion-icon slot="start" name="refresh"></ion-icon>
> Reset Defaults
<ion-button fill="clear" (click)="resetDefaults()"> </ion-button>
<ion-icon slot="start" name="refresh"></ion-icon> </ion-buttons>
Reset Defaults <ion-buttons slot="end" class="ion-padding-end">
</ion-button> <ion-button
</ion-buttons> *ngIf="hasConfig"
<ion-buttons fill="solid"
*ngIf="!loadingText && !loadingError" color="primary"
slot="end" [disabled]="saving"
class="ion-padding-end" (click)="tryConfigure()"
> class="enter-click btn-128"
<ion-button [class.no-click]="saving"
*ngIf="hasConfig" >
fill="solid" Save
color="primary" </ion-button>
[disabled]="saving" <ion-button
(click)="tryConfigure()" *ngIf="!hasConfig"
class="enter-click btn-128" fill="solid"
[class.no-click]="saving" color="dark"
> (click)="dismiss()"
Save class="enter-click btn-128"
</ion-button> >
<ion-button Close
*ngIf="!hasConfig" </ion-button>
fill="solid" </ion-buttons>
color="dark" </ng-container>
(click)="dismiss()"
class="enter-click btn-128"
>
Close
</ion-button>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>

View File

@@ -1,8 +1,7 @@
import { Component, Input, ViewChild } from '@angular/core' import { Component, Input } from '@angular/core'
import { import {
AlertController, AlertController,
ModalController, ModalController,
IonContent,
LoadingController, LoadingController,
IonicSafeString, IonicSafeString,
} from '@ionic/angular' } from '@ionic/angular'
@@ -24,6 +23,7 @@ import {
} from 'src/app/services/form.service' } from 'src/app/services/form.service'
import { compare, Operation, getValueByPointer } from 'fast-json-patch' import { compare, Operation, getValueByPointer } from 'fast-json-patch'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages, getPackage } from 'src/app/util/get-package-data'
import { Breakages } from 'src/app/services/api/api.types' import { Breakages } from 'src/app/services/api/api.types'
@Component({ @Component({
@@ -32,19 +32,24 @@ import { Breakages } from 'src/app/services/api/api.types'
styleUrls: ['./app-config.page.scss'], styleUrls: ['./app-config.page.scss'],
}) })
export class AppConfigPage { export class AppConfigPage {
@ViewChild(IonContent) content: IonContent @Input() pkgId!: string
@Input() pkgId: string
@Input() dependentInfo?: DependentInfo @Input()
diff: string[] // only if dependent info dependentInfo?: DependentInfo
pkg: PackageDataEntry
loadingText: string | undefined pkg!: PackageDataEntry
configSpec: ConfigSpec loadingText!: string
configForm: FormGroup configSpec!: ConfigSpec
original: object configForm!: FormGroup
original?: object // only if existing config
diff?: string[] // only if dependent info
loading = true
hasConfig = false hasConfig = false
hasNewOptions = false hasNewOptions = false
saving = false saving = false
loadingError: string | IonicSafeString loadingError: string | IonicSafeString = ''
constructor( constructor(
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
@@ -57,17 +62,15 @@ export class AppConfigPage {
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.pkg = this.patch.getData()['package-data'][this.pkgId]
this.hasConfig = !!this.pkg?.manifest.config
if (!this.hasConfig) return
let oldConfig: object | null
let newConfig: object | undefined
let spec: ConfigSpec
let patch: Operation[] | undefined
try { try {
this.pkg = await getPackage(this.patch, this.pkgId)
this.hasConfig = !!this.pkg.manifest.config
if (!this.hasConfig) return
let newConfig: object | undefined
let patch: Operation[] | undefined
if (this.dependentInfo) { if (this.dependentInfo) {
this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}` this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}`
const { const {
@@ -78,24 +81,22 @@ export class AppConfigPage {
'dependency-id': this.pkgId, 'dependency-id': this.pkgId,
'dependent-id': this.dependentInfo.id, 'dependent-id': this.dependentInfo.id,
}) })
oldConfig = oc this.original = oc
newConfig = nc newConfig = nc
spec = s this.configSpec = s
patch = compare(oldConfig, newConfig) patch = compare(this.original, newConfig)
} else { } else {
this.loadingText = 'Loading Config' this.loadingText = 'Loading Config'
const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ const { config: c, spec: s } = await this.embassyApi.getPackageConfig({
id: this.pkgId, id: this.pkgId,
}) })
oldConfig = c this.original = c
spec = s this.configSpec = s
} }
this.original = oldConfig
this.configSpec = spec
this.configForm = this.formService.createForm( this.configForm = this.formService.createForm(
spec, this.configSpec,
newConfig || oldConfig, newConfig || this.original,
) )
this.configForm.markAllAsTouched() this.configForm.markAllAsTouched()
@@ -106,22 +107,18 @@ export class AppConfigPage {
} catch (e: any) { } catch (e: any) {
this.loadingError = getErrorMessage(e) this.loadingError = getErrorMessage(e)
} finally { } finally {
this.loadingText = undefined this.loading = false
} }
} }
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
resetDefaults() { resetDefaults() {
this.configForm = this.formService.createForm(this.configSpec) this.configForm = this.formService.createForm(this.configSpec!)
const patch = compare(this.original, this.configForm.value) const patch = compare(this.original || {}, this.configForm.value)
this.markDirty(patch) this.markDirty(patch)
} }
async dismiss() { async dismiss() {
if (this.configForm?.dirty) { if (this.configForm.dirty) {
this.presentAlertUnsaved() this.presentAlertUnsaved()
} else { } else {
this.modalCtrl.dismiss() this.modalCtrl.dismiss()
@@ -202,7 +199,7 @@ export class AppConfigPage {
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> { private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
let message: string = let message: string =
'As a result of this change, the following services will no longer work properly and may crash:<ul>' 'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const localPkgs = this.patch.getData()['package-data'] const localPkgs = await getAllPackages(this.patch)
const bullets = Object.keys(breakages).map(id => { const bullets = Object.keys(breakages).map(id => {
const title = localPkgs[id].manifest.title const title = localPkgs[id].manifest.title
return `<li><b>${title}</b></li>` return `<li><b>${title}</b></li>`

View File

@@ -1,16 +1,14 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppRecoverSelectPage } from './app-recover-select.page'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { AppRecoverSelectPage } from './app-recover-select.page'
import { ToOptionsPipe } from './to-options.pipe'
@NgModule({ @NgModule({
declarations: [AppRecoverSelectPage], declarations: [AppRecoverSelectPage, ToOptionsPipe],
imports: [ imports: [CommonModule, IonicModule, FormsModule],
CommonModule,
IonicModule,
FormsModule,
],
exports: [AppRecoverSelectPage], exports: [AppRecoverSelectPage],
}) })
export class AppRecoverSelectPageModule { } export class AppRecoverSelectPageModule {}

View File

@@ -1,62 +1,65 @@
<ion-header> <ng-container
<ion-toolbar> *ngIf="packageData$ | toOptions : backupInfo['package-backups'] | async as options"
<ion-title>Select Services to Restore</ion-title> >
<ion-buttons slot="end"> <ion-header>
<ion-button (click)="dismiss()"> <ion-toolbar>
<ion-icon slot="icon-only" name="close"></ion-icon> <ion-title>Select Services to Restore</ion-title>
</ion-button> <ion-buttons slot="end">
</ion-buttons> <ion-button (click)="dismiss()">
</ion-toolbar> <ion-icon slot="icon-only" name="close"></ion-icon>
</ion-header> </ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content> <ion-content>
<h3 class="padding warning"> <h3 class="padding warning">
Warning! Restoring a service will <i>permanently overwrite</i> its current Warning! Restoring a service will <i>permanently overwrite</i> its current
data with data from its backup. Please make selections carefully. data with data from its backup. Please make selections carefully.
</h3> </h3>
<ion-item-group> <ion-item-group>
<ion-item *ngFor="let option of options"> <ion-item *ngFor="let option of options">
<ion-label> <ion-label>
<h2>{{ option.title }}</h2> <h2>{{ option.title }}</h2>
<p>Version {{ option.version }}</p> <p>Version {{ option.version }}</p>
<p>Backup made: {{ option.timestamp | date : 'short' }}</p> <p>Backup made: {{ option.timestamp | date : 'medium' }}</p>
<p *ngIf="!option.installed && !option['newer-eos']"> <p *ngIf="!option.installed && !option['newer-eos']">
<ion-text color="success">Ready to restore</ion-text> <ion-text color="success">Ready to restore</ion-text>
</p> </p>
<p *ngIf="option.installed"> <p *ngIf="option.installed">
<ion-text color="warning" <ion-text color="warning">
>Unavailable. {{ option.title }} is already installed.</ion-text Unavailable. {{ option.title }} is already installed.
> </ion-text>
</p> </p>
<p *ngIf="option['newer-eos']"> <p *ngIf="option['newer-eos']">
<ion-text color="danger" <ion-text color="danger">
>Unavailable. Backup was made on a newer version of Unavailable. Backup was made on a newer version of EmbassyOS.
EmbassyOS.</ion-text </ion-text>
> </p>
</p> </ion-label>
</ion-label> <ion-checkbox
<ion-checkbox slot="end"
slot="end" [(ngModel)]="option.checked"
[(ngModel)]="option.checked" [disabled]="option.installed || option['newer-eos']"
[disabled]="option.installed || option['newer-eos']" (ionChange)="handleChange(options)"
(ionChange)="handleChange()" ></ion-checkbox>
></ion-checkbox> </ion-item>
</ion-item> </ion-item-group>
</ion-item-group> </ion-content>
</ion-content>
<ion-footer> <ion-footer>
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="end" class="ion-padding-end"> <ion-buttons slot="end" class="ion-padding-end">
<ion-button <ion-button
[disabled]="!hasSelection" fill="solid"
fill="solid" color="primary"
color="primary" class="enter-click btn-128"
(click)="restore()" [disabled]="!hasSelection"
class="enter-click btn-128" (click)="restore(options)"
> >
Restore Selected Restore Selected
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>
</ng-container>

View File

@@ -4,11 +4,11 @@ import {
ModalController, ModalController,
IonicSafeString, IonicSafeString,
} from '@ionic/angular' } from '@ionic/angular'
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types' 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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { getErrorMessage, Emver } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { AppRecoverOption } from './to-options.pipe'
@Component({ @Component({
selector: 'app-recover-select', selector: 'app-recover-select',
@@ -16,57 +16,33 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
styleUrls: ['./app-recover-select.page.scss'], styleUrls: ['./app-recover-select.page.scss'],
}) })
export class AppRecoverSelectPage { export class AppRecoverSelectPage {
@Input() id: string @Input() id!: string
@Input() backupInfo: BackupInfo @Input() backupInfo!: BackupInfo
@Input() password: string @Input() password!: string
@Input() oldPassword: string @Input() oldPassword?: string
options: (PackageBackupInfo & {
id: string readonly packageData$ = this.patch.watch$('package-data')
checked: boolean
installed: boolean
'newer-eos': boolean
})[]
hasSelection = false hasSelection = false
error: string | IonicSafeString error: string | IonicSafeString = ''
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly config: ConfigService,
private readonly emver: Emver,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
) {} ) {}
ngOnInit() {
this.options = Object.keys(this.backupInfo['package-backups']).map(id => {
return {
...this.backupInfo['package-backups'][id],
id,
checked: false,
installed: !!this.patch.getData()['package-data'][id],
'newer-eos':
this.emver.compare(
this.backupInfo['package-backups'][id]['os-version'],
this.config.version,
) === 1,
}
})
}
dismiss() { dismiss() {
this.modalCtrl.dismiss() this.modalCtrl.dismiss()
} }
handleChange() { handleChange(options: AppRecoverOption[]) {
this.hasSelection = this.options.some(o => o.checked) this.hasSelection = options.some(o => o.checked)
} }
async restore(): Promise<void> { async restore(options: AppRecoverOption[]): Promise<void> {
const ids = this.options const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
.filter(option => !!option.checked)
.map(option => option.id)
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Initializing...', message: 'Initializing...',
}) })
@@ -76,7 +52,7 @@ export class AppRecoverSelectPage {
await this.embassyApi.restorePackages({ await this.embassyApi.restorePackages({
ids, ids,
'target-id': this.id, 'target-id': this.id,
'old-password': this.oldPassword, 'old-password': this.oldPassword || null,
password: this.password, password: this.password,
}) })
this.modalCtrl.dismiss(undefined, 'success') this.modalCtrl.dismiss(undefined, 'success')

View File

@@ -0,0 +1,46 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { PackageBackupInfo } from 'src/app/services/api/api.types'
import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
export interface AppRecoverOption extends PackageBackupInfo {
id: string
checked: boolean
installed: boolean
'newer-eos': boolean
}
@Pipe({
name: 'toOptions',
})
export class ToOptionsPipe implements PipeTransform {
constructor(
private readonly config: ConfigService,
private readonly emver: Emver,
) {}
transform(
packageData$: Observable<Record<string, PackageDataEntry>>,
packageBackups: Record<string, PackageBackupInfo> = {},
): Observable<AppRecoverOption[]> {
return packageData$.pipe(
map(packageData =>
Object.keys(packageBackups).map(id => ({
...packageBackups[id],
id,
installed: !!packageData[id],
checked: false,
'newer-eos': this.compare(packageBackups[id]['os-version']),
})),
),
)
}
private compare(version: string): boolean {
// checks to see if backup was made on a newer version of EOS
return this.emver.compare(version, this.config.version) === 1
}
}

View File

@@ -11,20 +11,35 @@
<ion-content> <ion-content>
<ion-item-group> <ion-item-group>
<ion-item-divider>Completed: {{ timestamp | date : 'short' }}</ion-item-divider> <ion-item-divider
>Completed: {{ timestamp | date : 'medium' }}</ion-item-divider
>
<ion-item> <ion-item>
<ion-label> <ion-label>
<h2>System data</h2> <h2>System data</h2>
<p><ion-text [color]="system.color">{{ system.result }}</ion-text></p> <p><ion-text [color]="system.color">{{ system.result }}</ion-text></p>
</ion-label> </ion-label>
<ion-icon slot="end" [name]="system.icon" [color]="system.color"></ion-icon> <ion-icon
slot="end"
[name]="system.icon"
[color]="system.color"
></ion-icon>
</ion-item> </ion-item>
<ion-item *ngFor="let pkg of report.packages | keyvalue"> <ion-item *ngFor="let pkg of report?.packages | keyvalue">
<ion-label> <ion-label>
<h2>{{ pkg.key }}</h2> <h2>{{ pkg.key }}</h2>
<p><ion-text [color]="pkg.value.error ? 'danger' : 'success'">{{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }}</ion-text></p> <p>
<ion-text [color]="pkg.value.error ? 'danger' : 'success'"
>{{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded'
}}</ion-text
>
</p>
</ion-label> </ion-label>
<ion-icon slot="end" [name]="pkg.value.error ? 'remove-circle-outline' : 'checkmark'" [color]="pkg.value.error ? 'danger' : 'success'"></ion-icon> <ion-icon
slot="end"
[name]="pkg.value.error ? 'remove-circle-outline' : 'checkmark'"
[color]="pkg.value.error ? 'danger' : 'success'"
></ion-icon>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
</ion-content> </ion-content>

View File

@@ -8,9 +8,10 @@ import { BackupReport } from 'src/app/services/api/api.types'
styleUrls: ['./backup-report.page.scss'], styleUrls: ['./backup-report.page.scss'],
}) })
export class BackupReportPage { export class BackupReportPage {
@Input() report: BackupReport @Input() report!: BackupReport
@Input() timestamp: string @Input() timestamp!: string
system: {
system!: {
result: string result: string
icon: 'remove' | 'remove-circle-outline' | 'checkmark' icon: 'remove' | 'remove-circle-outline' | 'checkmark'
color: 'dark' | 'danger' | 'success' color: 'dark' | 'danger' | 'success'

View File

@@ -8,16 +8,17 @@ import { ValueSpecListOf } from 'src/app/pkg-config/config-types'
styleUrls: ['./enum-list.page.scss'], styleUrls: ['./enum-list.page.scss'],
}) })
export class EnumListPage { export class EnumListPage {
@Input() key: string @Input() key!: string
@Input() spec: ValueSpecListOf<'enum'> @Input() spec!: ValueSpecListOf<'enum'>
@Input() current: string[] @Input() current: string[] = []
options: { [option: string]: boolean } = {} options: { [option: string]: boolean } = {}
selectAll = false selectAll = false
constructor(private readonly modalCtrl: ModalController) {} constructor(private readonly modalCtrl: ModalController) {}
ngOnInit() { ngOnInit() {
for (let val of this.spec.spec.values) { for (let val of this.spec.spec.values || []) {
this.options[val] = this.current.includes(val) this.options[val] = this.current.includes(val)
} }
// if none are selected, set selectAll to true // if none are selected, set selectAll to true

View File

@@ -10,11 +10,12 @@
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<form [formGroup]="formGroup" (ngSubmit)="handleClick(submitBtn.handler)" novalidate> <form
<form-object [formGroup]="formGroup"
[objectSpec]="spec" (ngSubmit)="handleClick(submitBtn.handler)"
[formGroup]="formGroup" novalidate
></form-object> >
<form-object [objectSpec]="spec" [formGroup]="formGroup"></form-object>
<button hidden type="submit"></button> <button hidden type="submit"></button>
</form> </form>
</ion-content> </ion-content>
@@ -22,7 +23,11 @@
<ion-footer> <ion-footer>
<ion-toolbar class="footer"> <ion-toolbar class="footer">
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button class="ion-padding-end" *ngFor="let button of buttons" (click)="handleClick(button.handler)"> <ion-button
class="ion-padding-end"
*ngFor="let button of buttons"
(click)="handleClick(button.handler)"
>
{{ button.text }} {{ button.text }}
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>

View File

@@ -19,12 +19,13 @@ export interface ActionButton {
styleUrls: ['./generic-form.page.scss'], styleUrls: ['./generic-form.page.scss'],
}) })
export class GenericFormPage { export class GenericFormPage {
@Input() title: string @Input() title!: string
@Input() spec: ConfigSpec @Input() spec!: ConfigSpec
@Input() buttons: ActionButton[] @Input() buttons!: ActionButton[]
@Input() initialValue: object = {} @Input() initialValue: object = {}
submitBtn: ActionButton
formGroup: FormGroup submitBtn!: ActionButton
formGroup!: FormGroup
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,

View File

@@ -10,12 +10,16 @@ import { MaskPipe } from 'src/app/pipes/mask/mask.pipe'
providers: [MaskPipe], providers: [MaskPipe],
}) })
export class GenericInputComponent { export class GenericInputComponent {
@ViewChild('mainInput') elem: IonInput @ViewChild('mainInput') elem?: IonInput
@Input() options: GenericInputOptions
value: string @Input() options!: GenericInputOptions
maskedValue: string
masked: boolean value!: string
error: string | IonicSafeString masked!: boolean
maskedValue?: string
error: string | IonicSafeString = ''
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
@@ -40,7 +44,7 @@ export class GenericInputComponent {
} }
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => this.elem.setFocus(), 400) setTimeout(() => this.elem?.setFocus(), 400)
} }
toggleMask() { toggleMask() {

View File

@@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular' import { LoadingController, ModalController } from '@ionic/angular'
import { ConfigService } from '../../services/config.service' import { ConfigService } from '../../services/config.service'
import { ApiService } from '../../services/api/embassy-api.service' import { ApiService } from '../../services/api/embassy-api.service'
import { ErrorToastService } from '../../../../../shared/src/services/error-toast.service' import { ErrorToastService } from '@start9labs/shared'
@Component({ @Component({
selector: 'os-update', selector: 'os-update',
@@ -10,7 +10,7 @@ import { ErrorToastService } from '../../../../../shared/src/services/error-toas
styleUrls: ['./os-update.page.scss'], styleUrls: ['./os-update.page.scss'],
}) })
export class OSUpdatePage { export class OSUpdatePage {
@Input() releaseNotes: { [version: string]: string } @Input() releaseNotes!: { [version: string]: string }
versions: { version: string; notes: string }[] = [] versions: { version: string; notes: string }[] = []

View File

@@ -7,13 +7,11 @@ import { ModalController } from '@ionic/angular'
styleUrls: ['./os-welcome.page.scss'], styleUrls: ['./os-welcome.page.scss'],
}) })
export class OSWelcomePage { export class OSWelcomePage {
@Input() version: string @Input() version!: string
constructor ( constructor(private readonly modalCtrl: ModalController) {}
private readonly modalCtrl: ModalController,
) { }
async dismiss () { async dismiss() {
return this.modalCtrl.dismiss() return this.modalCtrl.dismiss()
} }
} }

View File

@@ -7,7 +7,7 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<div class="canvas-center" style="width: 100%; height: 100%"> <div class="canvas-center" style="width: 100%; height: 100%">
<canvas id="game"> </canvas> <canvas id="game"></canvas>
</div> </div>
</ion-content> </ion-content>

View File

@@ -1,7 +1,6 @@
import { Component, HostListener } from '@angular/core' import { Component, HostListener, Input } from '@angular/core'
import { ModalController } from '@ionic/angular' import { ModalController } from '@ionic/angular'
import { pauseFor } from '@start9labs/shared' import { pauseFor } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({ @Component({
selector: 'snake', selector: 'snake',
@@ -9,38 +8,30 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
styleUrls: ['./snake.page.scss'], styleUrls: ['./snake.page.scss'],
}) })
export class SnakePage { export class SnakePage {
speed = 45 @Input()
width = 40
height = 26
grid = NaN
startingLength = 4
score = 0
highScore = 0 highScore = 0
xDown?: number score = 0
yDown?: number
canvas: HTMLCanvasElement
image: HTMLImageElement
context: CanvasRenderingContext2D
snake: any private readonly speed = 45
bitcoin: { x: number; y: number } = { x: NaN, y: NaN } private readonly width = 40
private readonly height = 26
private grid = NaN
moveQueue: String[] = [] private readonly startingLength = 4
constructor( private xDown?: number
private readonly modalCtrl: ModalController, private yDown?: number
private readonly patch: PatchDbService, private canvas!: HTMLCanvasElement
) {} private image!: HTMLImageElement
private context!: CanvasRenderingContext2D
ngOnInit() { private snake: any
if (this.patch.getData().ui.gaming?.snake?.['high-score']) { private bitcoin: { x: number; y: number } = { x: NaN, y: NaN }
this.highScore =
this.patch.getData().ui.gaming?.snake?.['high-score'] || 0 private moveQueue: String[] = []
}
} constructor(private readonly modalCtrl: ModalController) {}
async dismiss() { async dismiss() {
return this.modalCtrl.dismiss({ highScore: this.highScore }) return this.modalCtrl.dismiss({ highScore: this.highScore })
@@ -77,7 +68,7 @@ export class SnakePage {
} }
init() { init() {
this.canvas = document.getElementById('game') as HTMLCanvasElement this.canvas = document.querySelector('canvas#game')!
this.canvas.style.border = '1px solid #e0e0e0' this.canvas.style.border = '1px solid #e0e0e0'
this.context = this.canvas.getContext('2d')! this.context = this.canvas.getContext('2d')!
const container = document.getElementsByClassName('canvas-center')[0] const container = document.getElementsByClassName('canvas-center')[0]
@@ -224,7 +215,7 @@ export class SnakePage {
// snake ate bitcoin // snake ate bitcoin
if (cell.x === this.bitcoin.x && cell.y === this.bitcoin.y) { if (cell.x === this.bitcoin.x && cell.y === this.bitcoin.y) {
this.score++ this.score++
if (this.score > this.highScore) this.highScore = this.score this.highScore = Math.max(this.score, this.highScore)
this.snake.maxCells++ this.snake.maxCells++
this.bitcoin.x = this.getRandomInt(0, this.width) * this.grid this.bitcoin.x = this.getRandomInt(0, this.width) * this.grid

View File

@@ -8,7 +8,7 @@
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<ion-item-group *ngIf="pkg"> <ion-item-group *ngIf="pkg$ | async as pkg">
<!-- ** standard actions ** --> <!-- ** standard actions ** -->
<ion-item-divider>Standard Actions</ion-item-divider> <ion-item-divider>Standard Actions</ion-item-divider>
<app-actions-item <app-actions-item
@@ -17,7 +17,7 @@
description: 'This will uninstall the service from your Embassy and delete all data permanently.', description: 'This will uninstall the service from your Embassy and delete all data permanently.',
icon: 'trash-outline' icon: 'trash-outline'
}" }"
(click)="tryUninstall()" (click)="tryUninstall(pkg)"
> >
</app-actions-item> </app-actions-item>
@@ -32,7 +32,7 @@
description: action.value.description, description: action.value.description,
icon: 'play-circle-outline' icon: 'play-circle-outline'
}" }"
(click)="handleAction(action)" (click)="handleAction(pkg, action)"
> >
</app-actions-item> </app-actions-item>
</ion-item-group> </ion-item-group>

View File

@@ -1,9 +1,8 @@
import { Component, Input, ViewChild } from '@angular/core' import { Component, Input } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { import {
AlertController, AlertController,
IonContent,
LoadingController, LoadingController,
ModalController, ModalController,
NavController, NavController,
@@ -14,7 +13,6 @@ import {
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
@@ -26,10 +24,8 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
styleUrls: ['./app-actions.page.scss'], styleUrls: ['./app-actions.page.scss'],
}) })
export class AppActionsPage { export class AppActionsPage {
@ViewChild(IonContent) content: IonContent
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
pkg: PackageDataEntry readonly pkg$ = this.patch.watch$('package-data', this.pkgId)
subs: Subscription[]
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
@@ -42,24 +38,11 @@ export class AppActionsPage {
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
) {} ) {}
ngOnInit() { async handleAction(
this.subs = [ pkg: PackageDataEntry,
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => { action: { key: string; value: Action },
this.pkg = pkg ) {
}), const status = pkg.installed?.status
]
}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
}
async handleAction(action: { key: string; value: Action }) {
const status = this.pkg.installed?.status
if ( if (
status && status &&
(action.value['allowed-statuses'] as PackageMainStatus[]).includes( (action.value['allowed-statuses'] as PackageMainStatus[]).includes(
@@ -134,14 +117,14 @@ export class AppActionsPage {
} }
} }
async tryUninstall(): Promise<void> { async tryUninstall(pkg: PackageDataEntry): Promise<void> {
const { title, alerts } = this.pkg.manifest const { title, alerts } = pkg.manifest
let message = let message =
alerts.uninstall || alerts.uninstall ||
`Uninstalling ${title} will permanently delete its data` `Uninstalling ${title} will permanently delete its data`
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(pkg)) {
message = `${message}. Services that depend on ${title} will no longer work properly and may crash` message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
} }
@@ -233,5 +216,5 @@ interface LocalAction {
styleUrls: ['./app-actions.page.scss'], styleUrls: ['./app-actions.page.scss'],
}) })
export class AppActionsItemComponent { export class AppActionsItemComponent {
@Input() action: LocalAction @Input() action!: LocalAction
} }

View File

@@ -1,4 +1,4 @@
<ion-item> <ion-item *ngIf="interface">
<ion-icon <ion-icon
slot="start" slot="start"
size="large" size="large"
@@ -9,7 +9,7 @@
<h2>{{ interface.def.description }}</h2> <h2>{{ interface.def.description }}</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<div style="padding-left: 64px"> <div *ngIf="interface" style="padding-left: 64px">
<!-- has tor --> <!-- has tor -->
<ion-item *ngIf="interface.addresses['tor-address'] as tor"> <ion-item *ngIf="interface.addresses['tor-address'] as tor">
<ion-label> <ion-label>

View File

@@ -2,11 +2,12 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { SharedPipesModule } from '@start9labs/shared'
import { import {
AppInterfacesItemComponent, AppInterfacesItemComponent,
AppInterfacesPage, AppInterfacesPage,
} from './app-interfaces.page' } from './app-interfaces.page'
import { SharedPipesModule } from '@start9labs/shared'
const routes: Routes = [ const routes: Routes = [
{ {

View File

@@ -1,6 +1,6 @@
import { Component, Input, ViewChild } from '@angular/core' import { Component, Input } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { IonContent, ModalController, ToastController } from '@ionic/angular' import { ModalController, ToastController } from '@ionic/angular'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
import { getUiInterfaceKey } from 'src/app/services/config.service' import { getUiInterfaceKey } from 'src/app/services/config.service'
import { import {
@@ -10,6 +10,7 @@ import {
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { copyToClipboard } from 'src/app/util/web.util' import { copyToClipboard } from 'src/app/util/web.util'
import { QRComponent } from 'src/app/components/qr/qr.component' import { QRComponent } from 'src/app/components/qr/qr.component'
import { getPackage } from '../../../util/get-package-data'
interface LocalInterface { interface LocalInterface {
def: InterfaceDef def: InterfaceDef
@@ -22,22 +23,21 @@ interface LocalInterface {
styleUrls: ['./app-interfaces.page.scss'], styleUrls: ['./app-interfaces.page.scss'],
}) })
export class AppInterfacesPage { export class AppInterfacesPage {
@ViewChild(IonContent) content: IonContent ui?: LocalInterface
ui: LocalInterface | null
other: LocalInterface[] = [] other: LocalInterface[] = []
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
public readonly patch: PatchDbService, private readonly patch: PatchDbService,
) {} ) {}
ngOnInit() { async ngOnInit() {
const pkg = this.patch.getData()['package-data'][this.pkgId] const pkg = await getPackage(this.patch, this.pkgId)
const interfaces = pkg.manifest.interfaces const interfaces = pkg.manifest.interfaces
const uiKey = getUiInterfaceKey(interfaces) const uiKey = getUiInterfaceKey(interfaces)
if (!pkg?.installed) return if (!pkg.installed) return
const addressesMap = pkg.installed['interface-addresses'] const addressesMap = pkg.installed['interface-addresses']
@@ -73,14 +73,6 @@ export class AppInterfacesPage {
} }
}) })
} }
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
asIsOrder() {
return 0
}
} }
@Component({ @Component({
@@ -89,7 +81,8 @@ export class AppInterfacesPage {
styleUrls: ['./app-interfaces.page.scss'], styleUrls: ['./app-interfaces.page.scss'],
}) })
export class AppInterfacesItemComponent { export class AppInterfacesItemComponent {
@Input() interface: LocalInterface @Input()
interface!: LocalInterface
constructor( constructor(
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,

View File

@@ -1,25 +1,31 @@
<ion-icon <div
*ngIf="pkg.error; else noError" *ngIf="disconnected$ | async; else connected"
class="warning-icon" class="bulb"
name="warning-outline" [style.background-color]="'var(--ion-color-dark)'"
size="small" ></div>
color="warning" <ng-template #connected>
></ion-icon> <ion-icon
<ng-template #noError> *ngIf="pkg.error; else noError"
<ion-spinner class="warning-icon"
*ngIf="pkg.transitioning; else bulb" name="warning-outline"
class="spinner"
size="small" size="small"
color="primary" color="warning"
></ion-spinner> ></ion-icon>
<ng-template #bulb> <ng-template #noError>
<div <ion-spinner
class="bulb" *ngIf="pkg.transitioning; else bulb"
[style.background-color]=" class="spinner"
(disconnected$ | async) size="small"
? 'var(--ion-color-dark)' color="primary"
: 'var(--ion-color-' + this.pkg.primaryRendering.color + ')' ></ion-spinner>
" <ng-template #bulb>
></div> <div
class="bulb"
[style.background-color]="
'var(--ion-color-' + pkg.primaryRendering.color + ')'
"
[style.color]="'var(--ion-color-' + pkg.primaryRendering.color + ')'"
></div>
</ng-template>
</ng-template> </ng-template>
</ng-template> </ng-template>

View File

@@ -1,20 +1,18 @@
.bulb { .bulb {
position: absolute !important; position: absolute !important;
top: 6px !important; top: 9px !important;
height: 14px; height: 14px;
width: 14px; width: 14px;
border-radius: 100%; border-radius: 100%;
box-shadow: 0 0 6px 6px rgba(255, 213, 52, 0.1);
} }
.warning-icon { .warning-icon {
position: absolute !important; position: absolute !important;
top: 6px !important; top: 8px !important;
left: 11px !important;
font-size: 12px; font-size: 12px;
border-radius: 100%; border-radius: 100%;
padding: 1px; padding: 1px;
background-color: rgba(255, 213, 52, 0.1);
box-shadow: 0 0 4px 4px rgba(255, 213, 52, 0.1);
} }
.spinner { .spinner {

View File

@@ -1,5 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { map } from 'rxjs/operators'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { PkgInfo } from 'src/app/util/get-package-info' import { PkgInfo } from 'src/app/util/get-package-info'
@@ -11,7 +10,7 @@ import { PkgInfo } from 'src/app/util/get-package-info'
}) })
export class AppListIconComponent { export class AppListIconComponent {
@Input() @Input()
pkg: PkgInfo pkg!: PkgInfo
disconnected$ = this.connectionService.watchDisconnected$() disconnected$ = this.connectionService.watchDisconnected$()

View File

@@ -1,4 +1,9 @@
<ion-item button detail="false" [routerLink]="['/services', manifest.id]"> <ion-item
button
*ngIf="pkg.entry.manifest as manifest"
detail="false"
[routerLink]="['/services', manifest.id]"
>
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon> <app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
<ion-thumbnail slot="start"> <ion-thumbnail slot="start">
<img alt="" [src]="pkg.entry['static-files'].icon" /> <img alt="" [src]="pkg.entry['static-files'].icon" />

View File

@@ -1,8 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
PackageMainStatus,
Manifest,
} from 'src/app/services/patch-db/data-model'
import { PkgInfo } from 'src/app/util/get-package-info' import { PkgInfo } from 'src/app/util/get-package-info'
import { UiLauncherService } from 'src/app/services/ui-launcher.service' import { UiLauncherService } from 'src/app/services/ui-launcher.service'
@@ -13,7 +10,7 @@ import { UiLauncherService } from 'src/app/services/ui-launcher.service'
}) })
export class AppListPkgComponent { export class AppListPkgComponent {
@Input() @Input()
pkg: PkgInfo pkg!: PkgInfo
constructor(private readonly launcherService: UiLauncherService) {} constructor(private readonly launcherService: UiLauncherService) {}
@@ -23,10 +20,6 @@ export class AppListPkgComponent {
) )
} }
get manifest(): Manifest {
return this.pkg.entry.manifest
}
launchUi(e: Event): void { launchUi(e: Event): void {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()

View File

@@ -26,7 +26,7 @@ export class AppListRecComponent {
readonly delete$ = new Subject<RecoveredInfo>() readonly delete$ = new Subject<RecoveredInfo>()
@Input() @Input()
rec: RecoveredInfo rec!: RecoveredInfo
@Output() @Output()
readonly deleted = new EventEmitter<void>() readonly deleted = new EventEmitter<void>()

View File

@@ -12,9 +12,6 @@ import { copyToClipboard, strip } from 'src/app/util/web.util'
}) })
export class AppLogsPage { export class AppLogsPage {
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
loading = true
needInfinite = true
before: string
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,

View File

@@ -4,18 +4,21 @@
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button> <ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>Monitor</ion-title> <ion-title>Monitor</ion-title>
<ion-title slot="end"><ion-spinner name="dots" class="fader"></ion-spinner></ion-title> <ion-title slot="end"
><ion-spinner name="dots" class="fader"></ion-spinner
></ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<skeleton-list *ngIf="loading" [rows]="3"></skeleton-list>
<skeleton-list *ngIf="loading" rows="3"></skeleton-list>
<ion-item-group *ngIf="!loading"> <ion-item-group *ngIf="!loading">
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder"> <ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
<ion-label>{{ metric.key }}</ion-label> <ion-label>{{ metric.key }}</ion-label>
<ion-note *ngIf="metric.value" slot="end" class="metric-note"> <ion-note *ngIf="metric.value" slot="end" class="metric-note">
<ion-text style="color: white;">{{ metric.value.value }} {{ metric.value.unit }}</ion-text> <ion-text style="color: white"
>{{ metric.value.value }} {{ metric.value.unit }}</ion-text
>
</ion-note> </ion-note>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>

View File

@@ -1,10 +1,7 @@
import { Component, ViewChild } from '@angular/core' import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { IonContent } from '@ionic/angular'
import { Subscription } from 'rxjs'
import { Metric } from 'src/app/services/api/api.types' import { Metric } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MainStatus } from 'src/app/services/patch-db/data-model'
import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared' import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared'
@Component({ @Component({
@@ -15,12 +12,8 @@ import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared'
export class AppMetricsPage { export class AppMetricsPage {
loading = true loading = true
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
mainStatus: MainStatus
going = false going = false
metrics: Metric metrics?: Metric
subs: Subscription[] = []
@ViewChild(IonContent) content: IonContent
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
@@ -32,10 +25,6 @@ export class AppMetricsPage {
this.startDaemon() this.startDaemon()
} }
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy() { ngOnDestroy() {
this.stopDaemon() this.stopDaemon()
} }

View File

@@ -21,7 +21,7 @@
<ng-template #loaded> <ng-template #loaded>
<!-- not running --> <!-- not running -->
<ion-item *ngIf="!running" class="ion-margin-bottom"> <ion-item *ngIf="notRunning$ | async" class="ion-margin-bottom">
<ion-label> <ion-label>
<p> <p>
<ion-text color="warning" <ion-text color="warning"

View File

@@ -1,12 +1,10 @@
import { Component, ViewChild } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Subscription } from 'rxjs'
import { copyToClipboard } from 'src/app/util/web.util' import { copyToClipboard } from 'src/app/util/web.util'
import { import {
AlertController, AlertController,
IonBackButtonDelegate, IonBackButtonDelegate,
IonContent,
ModalController, ModalController,
NavController, NavController,
ToastController, ToastController,
@@ -15,27 +13,32 @@ import { PackageProperties } from 'src/app/util/properties.util'
import { QRComponent } from 'src/app/components/qr/qr.component' import { QRComponent } from 'src/app/components/qr/qr.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model' import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { ErrorToastService, getPkgId } from '@start9labs/shared' import { DestroyService, ErrorToastService, getPkgId } from '@start9labs/shared'
import { getValueByPointer } from 'fast-json-patch' import { getValueByPointer } from 'fast-json-patch'
import { map, takeUntil } from 'rxjs/operators'
@Component({ @Component({
selector: 'app-properties', selector: 'app-properties',
templateUrl: './app-properties.page.html', templateUrl: './app-properties.page.html',
styleUrls: ['./app-properties.page.scss'], styleUrls: ['./app-properties.page.scss'],
providers: [DestroyService],
}) })
export class AppPropertiesPage { export class AppPropertiesPage {
loading = true loading = true
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
pointer: string
properties: PackageProperties pointer = ''
node: PackageProperties node: PackageProperties = {}
properties: PackageProperties = {}
unmasked: { [key: string]: boolean } = {} unmasked: { [key: string]: boolean } = {}
running = true
notRunning$ = this.patch
.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
.pipe(map(status => status !== PackageMainStatus.Running))
@ViewChild(IonBackButtonDelegate, { static: false }) @ViewChild(IonBackButtonDelegate, { static: false })
backButton: IonBackButtonDelegate backButton?: IonBackButtonDelegate
@ViewChild(IonContent) content: IonContent
subs: Subscription[] = []
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
@@ -46,9 +49,11 @@ export class AppPropertiesPage {
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
private readonly destroy$: DestroyService,
) {} ) {}
ionViewDidEnter() { ionViewDidEnter() {
if (!this.backButton) return
this.backButton.onClick = () => { this.backButton.onClick = () => {
history.back() history.back()
} }
@@ -57,33 +62,13 @@ export class AppPropertiesPage {
async ngOnInit() { async ngOnInit() {
await this.getProperties() await this.getProperties()
this.subs = [ this.route.queryParams
this.route.queryParams.subscribe(queryParams => { .pipe(takeUntil(this.destroy$))
.subscribe(queryParams => {
if (queryParams['pointer'] === this.pointer) return if (queryParams['pointer'] === this.pointer) return
this.pointer = queryParams['pointer'] this.pointer = queryParams['pointer'] || ''
this.node = getValueByPointer(this.properties, this.pointer || '') this.node = getValueByPointer(this.properties, this.pointer)
}), })
this.patch
.watch$(
'package-data',
this.pkgId,
'installed',
'status',
'main',
'status',
)
.subscribe(status => {
this.running = status === PackageMainStatus.Running
}),
]
}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
} }
async refresh() { async refresh() {
@@ -106,7 +91,7 @@ export class AppPropertiesPage {
async goToNested(key: string): Promise<any> { async goToNested(key: string): Promise<any> {
this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, { this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, {
queryParams: { queryParams: {
pointer: `${this.pointer || ''}/${key}/value`, pointer: `${this.pointer}/${key}/value`,
}, },
}) })
} }
@@ -148,7 +133,7 @@ export class AppPropertiesPage {
this.properties = await this.embassyApi.getPackageProperties({ this.properties = await this.embassyApi.getPackageProperties({
id: this.pkgId, id: this.pkgId,
}) })
this.node = getValueByPointer(this.properties, this.pointer || '') this.node = getValueByPointer(this.properties, this.pointer)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {

View File

@@ -11,7 +11,7 @@ import {
PackageStatus, PackageStatus,
PrimaryStatus, PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
import { map, startWith, filter } from 'rxjs/operators' import { filter, tap } from 'rxjs/operators'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
@@ -36,19 +36,16 @@ export class AppShowPage {
private readonly pkgId = getPkgId(this.route) private readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe( readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
map(pkg => { tap(pkg => {
// if package disappears, navigate to list page // if package disappears, navigate to list page
if (!pkg) { if (!pkg) {
this.navCtrl.navigateRoot('/services') this.navCtrl.navigateRoot('/services')
} }
return { ...pkg }
}), }),
startWith(this.patch.getData()['package-data'][this.pkgId]),
filter( filter(
(p: PackageDataEntry | undefined) => (p?: PackageDataEntry) =>
// will be undefined when sideloading // will be undefined when sideloading
p !== undefined && !!p &&
!( !(
p.installed?.status.main.status === PackageMainStatus.Starting && p.installed?.status.main.status === PackageMainStatus.Starting &&
p.installed?.status.main.restarting p.installed?.status.main.restarting

View File

@@ -9,5 +9,5 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
}) })
export class AppShowHeaderComponent { export class AppShowHeaderComponent {
@Input() @Input()
pkg: PackageDataEntry pkg!: PackageDataEntry
} }

View File

@@ -13,7 +13,7 @@ import {
}) })
export class AppShowHealthChecksComponent { export class AppShowHealthChecksComponent {
@Input() @Input()
pkg: PackageDataEntry pkg!: PackageDataEntry
HealthResult = HealthResult HealthResult = HealthResult

View File

@@ -13,10 +13,10 @@ import { ProgressData } from 'src/app/types/progress-data'
}) })
export class AppShowProgressComponent { export class AppShowProgressComponent {
@Input() @Input()
pkg: PackageDataEntry pkg!: PackageDataEntry
@Input() @Input()
progressData: ProgressData progressData!: ProgressData
get unpackingBuffer(): number { get unpackingBuffer(): number {
return this.progressData.validateProgress === 100 && return this.progressData.validateProgress === 100 &&

View File

@@ -3,7 +3,7 @@
<ion-label class="label"> <ion-label class="label">
<status <status
size="x-large" size="x-large"
weight="500" weight="600"
[installProgress]="pkg['install-progress']" [installProgress]="pkg['install-progress']"
[rendering]="PR[status.primary]" [rendering]="PR[status.primary]"
[sigtermTimeout]="pkg.manifest.main['sigterm-timeout']" [sigtermTimeout]="pkg.manifest.main['sigterm-timeout']"

View File

@@ -27,10 +27,10 @@ import { ConnectionService } from 'src/app/services/connection.service'
}) })
export class AppShowStatusComponent { export class AppShowStatusComponent {
@Input() @Input()
pkg: PackageDataEntry pkg!: PackageDataEntry
@Input() @Input()
status: PackageStatus status!: PackageStatus
@Input() @Input()
dependencies: DependencyInfo[] = [] dependencies: DependencyInfo[] = []
@@ -50,7 +50,7 @@ export class AppShowStatusComponent {
) {} ) {}
get interfaces(): Record<string, InterfaceDef> { get interfaces(): Record<string, InterfaceDef> {
return this.pkg.manifest.interfaces return this.pkg.manifest.interfaces || {}
} }
get pkgStatus(): Status | null { get pkgStatus(): Status | null {
@@ -74,7 +74,9 @@ export class AppShowStatusComponent {
} }
async presentModalConfig(): Promise<void> { async presentModalConfig(): Promise<void> {
return this.modalService.presentModalConfig({ pkgId: this.pkg.manifest.id }) return this.modalService.presentModalConfig({
pkgId: this.id,
})
} }
async tryStart(): Promise<void> { async tryStart(): Promise<void> {
@@ -87,7 +89,7 @@ export class AppShowStatusComponent {
const alertMsg = this.pkg.manifest.alerts.start const alertMsg = this.pkg.manifest.alerts.start
if (!!alertMsg) { if (alertMsg) {
const proceed = await this.presentAlertStart(alertMsg) const proceed = await this.presentAlertStart(alertMsg)
if (!proceed) return if (!proceed) return
@@ -180,6 +182,10 @@ export class AppShowStatusComponent {
await alert.present() await alert.present()
} }
private get id(): string {
return this.pkg.manifest.id
}
private async start(): Promise<void> { private async start(): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: `Starting...`, message: `Starting...`,
@@ -187,7 +193,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.startPackage({ id: this.pkg.manifest.id }) await this.embassyApi.startPackage({ id: this.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -202,7 +208,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.stopPackage({ id: this.pkg.manifest.id }) await this.embassyApi.stopPackage({ id: this.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -217,7 +223,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.restartPackage({ id: this.pkg.manifest.id }) await this.embassyApi.restartPackage({ id: this.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {

View File

@@ -202,12 +202,6 @@ export class ToButtonsPipe implements PipeTransform {
packageMarketplace, packageMarketplace,
currentMarketplace, currentMarketplace,
pkgId, pkgId,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
],
}, },
cssClass: 'medium-modal', cssClass: 'medium-modal',
}) })

View File

@@ -28,7 +28,7 @@ import { takeUntil } from 'rxjs/operators'
providers: [DestroyService], providers: [DestroyService],
}) })
export class DeveloperListPage { export class DeveloperListPage {
devData: DevData devData: DevData = {}
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,

View File

@@ -8,7 +8,7 @@ import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-repo
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { SharedPipesModule } from '../../../../../../shared/src/pipes/shared/shared.module' import { SharedPipesModule } from '@start9labs/shared'
const routes: Routes = [ const routes: Routes = [
{ {

View File

@@ -3,7 +3,7 @@
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button defaultHref="/developer"></ion-back-button> <ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ name }}</ion-title> <ion-title>{{ (projectData$ | async)?.name || '' }}</ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button routerLink="manifest">View Manifest</ion-button> <ion-button routerLink="manifest">View Manifest</ion-button>
</ion-buttons> </ion-buttons>

View File

@@ -5,7 +5,7 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { BasicInfo, getBasicInfoSpec } from './form-info' import { BasicInfo, getBasicInfoSpec } from './form-info'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService, DestroyService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { getProjectId } from 'src/app/util/get-project-id' import { getProjectId } from 'src/app/util/get-project-id'
import { DevProjectData } from 'src/app/services/patch-db/data-model' import { DevProjectData } from 'src/app/services/patch-db/data-model'
@@ -13,11 +13,10 @@ import { DevProjectData } from 'src/app/services/patch-db/data-model'
selector: 'developer-menu', selector: 'developer-menu',
templateUrl: 'developer-menu.page.html', templateUrl: 'developer-menu.page.html',
styleUrls: ['developer-menu.page.scss'], styleUrls: ['developer-menu.page.scss'],
providers: [DestroyService],
}) })
export class DeveloperMenuPage { export class DeveloperMenuPage {
readonly projectId = getProjectId(this.route) readonly projectId = getProjectId(this.route)
projectData$ = this.patch.watch$('ui', 'dev', this.projectId) readonly projectData$ = this.patch.watch$('ui', 'dev', this.projectId)
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
@@ -26,11 +25,7 @@ export class DeveloperMenuPage {
private readonly api: ApiService, private readonly api: ApiService,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService, private readonly patch: PatchDbService,
) { } ) {}
get name(): string {
return this.patch.getData().ui?.dev?.[this.projectId]?.name || ''
}
async openBasicInfoModal(data: DevProjectData) { async openBasicInfoModal(data: DevProjectData) {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
@@ -41,13 +36,7 @@ export class DeveloperMenuPage {
buttons: [ buttons: [
{ {
text: 'Save', text: 'Save',
handler: (basicInfo: any) => { handler: (basicInfo: BasicInfo) => {
basicInfo.description = {
short: basicInfo.short,
long: basicInfo.long,
}
delete basicInfo.short
delete basicInfo.long
this.saveBasicInfo(basicInfo) this.saveBasicInfo(basicInfo)
}, },
isSubmit: true, isSubmit: true,

View File

@@ -27,19 +27,19 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. bitcoind', placeholder: 'e.g. bitcoind',
nullable: false, nullable: false,
masked: false, masked: false,
copyable: true, copyable: false,
pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
'pattern-description': 'Must be kebab case', 'pattern-description': 'Must be kebab case',
default: basicInfo?.id, default: basicInfo?.id,
}, },
title: { title: {
type: 'string', type: 'string',
name: 'Title', name: 'Service Name',
description: 'A human readable service title', description: 'A human readable service title',
placeholder: 'e.g. Bitcoin Core', placeholder: 'e.g. Bitcoin Core',
nullable: false, nullable: false,
masked: false, masked: false,
copyable: true, copyable: false,
default: basicInfo ? basicInfo.title : devData.name, default: basicInfo ? basicInfo.title : devData.name,
}, },
'service-version-number': { 'service-version-number': {
@@ -50,19 +50,51 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. 0.1.2.3', placeholder: 'e.g. 0.1.2.3',
nullable: false, nullable: false,
masked: false, masked: false,
copyable: true, copyable: false,
pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$', pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
'pattern-description': 'Must be valid Emver version', 'pattern-description': 'Must be valid Emver version',
default: basicInfo?.['service-version-number'], default: basicInfo?.['service-version-number'],
}, },
description: {
type: 'object',
name: 'Marketplace Descriptions',
spec: {
short: {
type: 'string',
name: 'Short Description',
description:
'This is the first description visible to the user in the marketplace',
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.short,
pattern: '^.{1,320}$',
'pattern-description': 'Must be shorter than 320 characters',
},
long: {
type: 'string',
name: 'Long Description',
description: `This description will display with additional details in the service's individual marketplace page`,
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.long,
pattern: '^.{1,5000}$',
'pattern-description': 'Must be shorter than 5000 characters',
},
},
},
'release-notes': { 'release-notes': {
type: 'string', type: 'string',
name: 'Release Notes', name: 'Release Notes',
description: 'A human readable service title', description:
placeholder: 'e.g. Bitcoin Core', 'Markdown supported release notes for this version of this service.',
placeholder: 'e.g. Markdown _release notes_ for **Bitcoin Core**',
nullable: false, nullable: false,
masked: false, masked: false,
copyable: true, copyable: false,
textarea: true, textarea: true,
default: basicInfo?.['release-notes'], default: basicInfo?.['release-notes'],
}, },
@@ -102,7 +134,7 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. www.github.com/example', placeholder: 'e.g. www.github.com/example',
nullable: false, nullable: false,
masked: false, masked: false,
copyable: true, copyable: false,
default: basicInfo?.['wrapper-repo'], default: basicInfo?.['wrapper-repo'],
}, },
'upstream-repo': { 'upstream-repo': {
@@ -112,7 +144,7 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. www.github.com/example', placeholder: 'e.g. www.github.com/example',
nullable: true, nullable: true,
masked: false, masked: false,
copyable: true, copyable: false,
default: basicInfo?.['upstream-repo'], default: basicInfo?.['upstream-repo'],
}, },
'support-site': { 'support-site': {
@@ -122,7 +154,7 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. www.start9labs.com', placeholder: 'e.g. www.start9labs.com',
nullable: true, nullable: true,
masked: false, masked: false,
copyable: true, copyable: false,
default: basicInfo?.['support-site'], default: basicInfo?.['support-site'],
}, },
'marketing-site': { 'marketing-site': {
@@ -132,33 +164,8 @@ export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec {
placeholder: 'e.g. www.start9labs.com', placeholder: 'e.g. www.start9labs.com',
nullable: true, nullable: true,
masked: false, masked: false,
copyable: true, copyable: false,
default: basicInfo?.['marketing-site'], default: basicInfo?.['marketing-site'],
}, },
short: {
type: 'string',
name: 'Short Description',
description:
'This is the first description visible to the user in the marketplace',
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.short,
pattern: '^.{1,320}$',
'pattern-description': 'Must be shorter than 320 characters',
},
long: {
type: 'string',
name: 'Long Description',
description: `This description will display with additional details in the service's individual marketplace page`,
nullable: false,
masked: false,
copyable: false,
textarea: true,
default: basicInfo?.description?.long,
pattern: '^.{1,5000}$',
'pattern-description': 'Must be shorter than 5000 characters',
},
} }
} }

View File

@@ -25,7 +25,7 @@
> >
Downgrade Downgrade
</ion-button> </ion-button>
<ng-container *ngIf="localStorageService.showDevTools$ | async"> <ng-container *ngIf="showDevTools$ | async">
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0" *ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0"
expand="block" expand="block"

View File

@@ -9,6 +9,7 @@ import {
AbstractMarketplaceService, AbstractMarketplaceService,
MarketplacePkg, MarketplacePkg,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { Emver, ErrorToastService, isEmptyObject } from '@start9labs/shared'
import { import {
PackageDataEntry, PackageDataEntry,
PackageState, PackageState,
@@ -16,12 +17,10 @@ import {
import { LocalStorageService } from 'src/app/services/local-storage.service' import { LocalStorageService } from 'src/app/services/local-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { Emver } from '../../../../../../../shared/src/services/emver.service'
import { ErrorToastService } from '../../../../../../../shared/src/services/error-toast.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { isEmptyObject } from '../../../../../../../shared/src/util/misc.util'
import { Breakages } from 'src/app/services/api/api.types' import { Breakages } from 'src/app/services/api/api.types'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { getAllPackages } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'marketplace-show-controls', selector: 'marketplace-show-controls',
@@ -31,16 +30,18 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
}) })
export class MarketplaceShowControlsComponent { export class MarketplaceShowControlsComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
@Input() @Input()
localPkg: PackageDataEntry | null = null localPkg!: PackageDataEntry | null
readonly showDevTools$ = this.localStorageService.showDevTools$
readonly PackageState = PackageState readonly PackageState = PackageState
constructor( constructor(
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
public readonly localStorageService: LocalStorageService, private readonly localStorageService: LocalStorageService,
@Inject(AbstractMarketplaceService) @Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService, private readonly marketplaceService: MarketplaceService,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
@@ -151,7 +152,7 @@ export class MarketplaceShowControlsComponent {
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> { private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
let message: string = let message: string =
'As a result of this update, the following services will no longer work properly and may crash:<ul>' 'As a result of this update, the following services will no longer work properly and may crash:<ul>'
const localPkgs = this.patch.getData()['package-data'] const localPkgs = await getAllPackages(this.patch)
const bullets = Object.keys(breakages).map(id => { const bullets = Object.keys(breakages).map(id => {
const title = localPkgs[id].manifest.title const title = localPkgs[id].manifest.title
return `<li><b>${title}</b></li>` return `<li><b>${title}</b></li>`

View File

@@ -16,7 +16,7 @@ import { DependentInfo } from 'src/app/types/dependent-info'
}) })
export class MarketplaceShowDependentComponent { export class MarketplaceShowDependentComponent {
@Input() @Input()
pkg: MarketplacePkg pkg!: MarketplacePkg
readonly dependentInfo?: DependentInfo = readonly dependentInfo?: DependentInfo =
this.document.defaultView?.history.state?.dependentInfo this.document.defaultView?.history.state?.dependentInfo
@@ -24,10 +24,10 @@ export class MarketplaceShowDependentComponent {
constructor(@Inject(DOCUMENT) private readonly document: Document) {} constructor(@Inject(DOCUMENT) private readonly document: Document) {}
get title(): string { get title(): string {
return this.pkg?.manifest.title || '' return this.pkg.manifest.title
} }
get version(): string { get version(): string {
return this.pkg?.manifest.version || '' return this.pkg.manifest.version
} }
} }

View File

@@ -10,10 +10,9 @@ import {
styleUrls: ['marketplace-status.component.scss'], styleUrls: ['marketplace-status.component.scss'],
}) })
export class MarketplaceStatusComponent { export class MarketplaceStatusComponent {
@Input() @Input() version!: string
version: string
@Input() @Input() localPkg?: PackageDataEntry
localPkg?: PackageDataEntry
PackageState = PackageState PackageState = PackageState

View File

@@ -12,7 +12,7 @@
<ion-content> <ion-content>
<!-- loading --> <!-- loading -->
<ion-item-group *ngIf="loading"> <ion-item-group *ngIf="loading; else loaded">
<ion-item-divider> <ion-item-divider>
<ion-button slot="end" fill="clear"> <ion-button slot="end" fill="clear">
<ion-skeleton-text <ion-skeleton-text
@@ -43,9 +43,9 @@
</ion-item-group> </ion-item-group>
<!-- not loading --> <!-- not loading -->
<ng-container *ngIf="!loading"> <ng-template #loaded>
<!-- no notifications --> <!-- no notifications -->
<ion-item-group *ngIf="!notifications.length"> <ion-item-group *ngIf="!notifications.length; else hasNotifications">
<div <div
style=" style="
text-align: center; text-align: center;
@@ -64,8 +64,11 @@
</ion-item-group> </ion-item-group>
<!-- has notifications --> <!-- has notifications -->
<ng-container *ngIf="notifications.length"> <ng-template #hasNotifications>
<ion-item-group style="margin-bottom: 16px"> <ion-item-group
*ngIf="packageData$ | async as packageData"
style="margin-bottom: 16px"
>
<ion-item-divider> <ion-item-divider>
<ion-button <ion-button
slot="end" slot="end"
@@ -80,12 +83,8 @@
<ion-label> <ion-label>
<h2> <h2>
<b> <b>
<span <span *ngIf="not['package-id'] as pkgId">
*ngIf="not['package-id'] && patch.getData()['package-data']" {{ packageData[pkgId]?.manifest!.title || pkgId }} -
>
{{ patch.getData()['package-data'][not['package-id']] ?
patch.getData()['package-data'][not['package-id']].manifest.title
: not['package-id'] }} -
</span> </span>
<ion-text [color]="getColor(not)"> {{ not.title }} </ion-text> <ion-text [color]="getColor(not)"> {{ not.title }} </ion-text>
</b> </b>
@@ -101,7 +100,7 @@
View Full Message View Full Message
</a> </a>
</p> </p>
<p>{{ not['created-at'] | date: 'short' }}</p> <p>{{ not['created-at'] | date: 'medium' }}</p>
</ion-label> </ion-label>
<ion-button <ion-button
*ngIf="not.code === 1" *ngIf="not.code === 1"
@@ -135,6 +134,6 @@
loadingSpinner="lines" loadingSpinner="lines"
></ion-infinite-scroll-content> ></ion-infinite-scroll-content>
</ion-infinite-scroll> </ion-infinite-scroll>
</ng-container> </ng-template>
</ng-container> </ng-template>
</ion-content> </ion-content>

View File

@@ -27,6 +27,7 @@ export class NotificationsPage {
needInfinite = false needInfinite = false
fromToast = false fromToast = false
readonly perPage = 40 readonly perPage = 40
readonly packageData$ = this.patch.watch$('package-data')
constructor( constructor(
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
@@ -35,7 +36,7 @@ export class NotificationsPage {
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
public readonly patch: PatchDbService, private readonly patch: PatchDbService,
) {} ) {}
async ngOnInit() { async ngOnInit() {

View File

@@ -4,6 +4,11 @@
<ion-back-button defaultHref="embassy"></ion-back-button> <ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>Kernel Logs</ion-title> <ion-title>Kernel Logs</ion-title>
<ion-buttons slot="end">
<ion-button (click)="copy()">
<ion-icon name="copy-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>

View File

@@ -1,5 +1,7 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { copyToClipboard, strip } from 'src/app/util/web.util'
@Component({ @Component({
selector: 'kernel-logs', selector: 'kernel-logs',
@@ -7,12 +9,10 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
styleUrls: ['./kernel-logs.page.scss'], styleUrls: ['./kernel-logs.page.scss'],
}) })
export class KernelLogsPage { export class KernelLogsPage {
pkgId: string constructor(
loading = true private readonly embassyApi: ApiService,
needInfinite = true private readonly toastCtrl: ToastController,
before: string ) {}
constructor(private readonly embassyApi: ApiService) {}
fetchFetchLogs() { fetchFetchLogs() {
return async (params: { return async (params: {
@@ -27,4 +27,22 @@ export class KernelLogsPage {
}) })
} }
} }
async copy(): Promise<void> {
const logs = document
.getElementById('template')
?.cloneNode(true) as HTMLElement
const formatted = '```' + strip(logs.innerHTML) + '```'
const success = await copyToClipboard(formatted)
const message = success
? 'Copied to clipboard!'
: 'Failed to copy to clipboard.'
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
})
await toast.present()
}
} }

View File

@@ -21,9 +21,11 @@ import {
first, first,
takeUntil, takeUntil,
} from 'rxjs/operators' } from 'rxjs/operators'
import { getServerInfo } from '../../../util/get-server-info'
import { getMarketplace } from '../../../util/get-marketplace'
type Marketplaces = { type Marketplaces = {
id: string | undefined id: string | null
name: string name: string
url: string url: string
}[] }[]
@@ -35,7 +37,7 @@ type Marketplaces = {
providers: [DestroyService], providers: [DestroyService],
}) })
export class MarketplacesPage { export class MarketplacesPage {
selectedId: string | undefined selectedId: string | null = null
marketplaces: Marketplaces = [] marketplaces: Marketplaces = []
constructor( constructor(
@@ -47,7 +49,7 @@ export class MarketplacesPage {
@Inject(AbstractMarketplaceService) @Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService, private readonly marketplaceService: MarketplaceService,
private readonly config: ConfigService, private readonly config: ConfigService,
public readonly patch: PatchDbService, private readonly patch: PatchDbService,
private readonly destroy$: DestroyService, private readonly destroy$: DestroyService,
) {} ) {}
@@ -58,13 +60,13 @@ export class MarketplacesPage {
.subscribe((mp: UIMarketplaceData | undefined) => { .subscribe((mp: UIMarketplaceData | undefined) => {
let marketplaces: Marketplaces = [ let marketplaces: Marketplaces = [
{ {
id: undefined, id: null,
name: this.config.marketplace.name, name: this.config.marketplace.name,
url: this.config.marketplace.url, url: this.config.marketplace.url,
}, },
] ]
if (mp) { if (mp) {
this.selectedId = mp['selected-id'] || undefined this.selectedId = mp['selected-id']
const alts = Object.entries(mp['known-hosts']).map(([k, v]) => { const alts = Object.entries(mp['known-hosts']).map(([k, v]) => {
return { return {
id: k, id: k,
@@ -107,34 +109,33 @@ export class MarketplacesPage {
await modal.present() await modal.present()
} }
async presentAction(id: string = '') { async presentAction(id: string | null) {
// no need to view actions if is selected marketplace // no need to view actions if is selected marketplace
if (id === this.patch.getData().ui.marketplace?.['selected-id']) return const marketplace = await getMarketplace(this.patch)
if (id === marketplace['selected-id']) return
const buttons: ActionSheetButton[] = [ const buttons: ActionSheetButton[] = [
{ {
text: 'Forget', text: 'Connect',
icon: 'trash',
role: 'destructive',
handler: () => {
this.delete(id)
},
},
{
text: 'Connect to marketplace',
handler: () => { handler: () => {
this.connect(id) this.connect(id)
}, },
}, },
] ]
if (!id) { if (id) {
buttons.shift() buttons.unshift({
text: 'Delete',
role: 'destructive',
handler: () => {
this.delete(id)
},
})
} }
const action = await this.actionCtrl.create({ const action = await this.actionCtrl.create({
header: id, header: this.marketplaces.find(mp => mp.id === id)?.name,
subHeader: 'Manage marketplaces',
mode: 'ios', mode: 'ios',
buttons, buttons,
}) })
@@ -142,10 +143,8 @@ export class MarketplacesPage {
await action.present() await action.present()
} }
private async connect(id: string): Promise<void> { private async connect(id: string | null): Promise<void> {
const marketplace: UIMarketplaceData = JSON.parse( const marketplace = await getMarketplace(this.patch)
JSON.stringify(this.patch.getData().ui.marketplace),
)
const url = id const url = id
? marketplace['known-hosts'][id].url ? marketplace['known-hosts'][id].url
@@ -157,10 +156,8 @@ export class MarketplacesPage {
await loader.present() await loader.present()
try { try {
await this.marketplaceService.getMarketplaceData( const { id } = await getServerInfo(this.patch)
{ 'server-id': this.patch.getData()['server-info'].id }, await this.marketplaceService.getMarketplaceData({ 'server-id': id }, url)
url,
)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
loader.dismiss() loader.dismiss()
@@ -169,9 +166,13 @@ export class MarketplacesPage {
loader.message = 'Changing Marketplace...' loader.message = 'Changing Marketplace...'
const value: UIMarketplaceData = {
...marketplace,
'selected-id': id,
}
try { try {
marketplace['selected-id'] = id await this.api.setDbValue({ pointer: `/marketplace`, value })
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
loader.dismiss() loader.dismiss()
@@ -189,10 +190,8 @@ export class MarketplacesPage {
} }
private async delete(id: string): Promise<void> { private async delete(id: string): Promise<void> {
if (!id) return const data = await getMarketplace(this.patch)
const marketplace: UIMarketplaceData = JSON.parse( const marketplace: UIMarketplaceData = JSON.parse(JSON.stringify(data))
JSON.stringify(this.patch.getData().ui.marketplace),
)
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Deleting...', message: 'Deleting...',
@@ -210,13 +209,12 @@ export class MarketplacesPage {
} }
private async save(url: string): Promise<void> { private async save(url: string): Promise<void> {
const marketplace = this.patch.getData().ui.marketplace const data = await getMarketplace(this.patch)
? (JSON.parse( const marketplace: UIMarketplaceData = data
JSON.stringify(this.patch.getData().ui.marketplace), ? JSON.parse(JSON.stringify(data))
) as UIMarketplaceData)
: { : {
'selected-id': undefined, 'selected-id': null,
'known-hosts': {} as Record<string, unknown>, 'known-hosts': {},
} }
// no-op on duplicates // no-op on duplicates
@@ -231,8 +229,9 @@ export class MarketplacesPage {
try { try {
const id = v4() const id = v4()
const { id: serverId } = await getServerInfo(this.patch)
const { name } = await this.marketplaceService.getMarketplaceData( const { name } = await this.marketplaceService.getMarketplaceData(
{ 'server-id': this.patch.getData()['server-info'].id }, { 'server-id': serverId },
url, url,
) )
marketplace['known-hosts'][id] = { name, url } marketplace['known-hosts'][id] = { name, url }
@@ -254,13 +253,12 @@ export class MarketplacesPage {
} }
private async saveAndConnect(url: string): Promise<void> { private async saveAndConnect(url: string): Promise<void> {
const marketplace = this.patch.getData().ui.marketplace const data = await getMarketplace(this.patch)
? (JSON.parse( const marketplace: UIMarketplaceData = data
JSON.stringify(this.patch.getData().ui.marketplace), ? JSON.parse(JSON.stringify(data))
) as UIMarketplaceData)
: { : {
'selected-id': undefined, 'selected-id': null,
'known-hosts': {} as Record<string, unknown>, 'known-hosts': {},
} }
// no-op on duplicates // no-op on duplicates
@@ -274,8 +272,9 @@ export class MarketplacesPage {
try { try {
const id = v4() const id = v4()
const { id: serverId } = await getServerInfo(this.patch)
const { name } = await this.marketplaceService.getMarketplaceData( const { name } = await this.marketplaceService.getMarketplaceData(
{ 'server-id': this.patch.getData()['server-info'].id }, { 'server-id': serverId },
url, url,
) )
marketplace['known-hosts'][id] = { name, url } marketplace['known-hosts'][id] = { name, url }

View File

@@ -11,7 +11,10 @@
<ng-container *ngIf="ui$ | async as ui"> <ng-container *ngIf="ui$ | async as ui">
<ion-item-group *ngIf="server$ | async as server"> <ion-item-group *ngIf="server$ | async as server">
<ion-item-divider>General</ion-item-divider> <ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName('Embassy-' + server.id)"> <ion-item
button
(click)="presentModalName('Embassy-' + server.id, ui.name)"
>
<ion-label>Device Name</ion-label> <ion-label>Device Name</ion-label>
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note> <ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
</ion-item> </ion-item>

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