mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
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:
@@ -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> {
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
|||||||
})
|
})
|
||||||
export class ItemComponent {
|
export class ItemComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg: MarketplacePkg
|
pkg!: MarketplacePkg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
|||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg: MarketplacePkg
|
pkg!: MarketplacePkg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
|||||||
})
|
})
|
||||||
export class PackageComponent {
|
export class PackageComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg: MarketplacePkg
|
pkg!: MarketplacePkg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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$
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
(['string', 'number'] | includes: data.spec.type) &&
|
(['string', 'number'] | includes: data.spec.type) &&
|
||||||
!$any(data.spec).nullable
|
!$any(data.spec).nullable
|
||||||
"
|
"
|
||||||
> *</span
|
|
||||||
>
|
>
|
||||||
|
*
|
||||||
|
</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"
|
||||||
> *</span
|
> *</span
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 }[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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$()
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class AppShowHealthChecksComponent {
|
export class AppShowHealthChecksComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
HealthResult = HealthResult
|
HealthResult = HealthResult
|
||||||
|
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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']"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user