chore: enable strict mode (#1569)

* chore: enable strict mode

* refactor: remove sync data access from PatchDbService

* launchable even when no LAN url

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

View File

@@ -4,4 +4,4 @@
<h1>{{ action.name }}</h1>
<p>{{ action.description }}</p>
</ion-label>
</ion-item>
</ion-item>

View File

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

View File

@@ -1,9 +1,8 @@
import { Component, Input, ViewChild } from '@angular/core'
import { Component, Input } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
AlertController,
IonContent,
LoadingController,
ModalController,
NavController,
@@ -14,7 +13,6 @@ import {
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
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'],
})
export class AppActionsPage {
@ViewChild(IonContent) content: IonContent
readonly pkgId = getPkgId(this.route)
pkg: PackageDataEntry
subs: Subscription[]
readonly pkg$ = this.patch.watch$('package-data', this.pkgId)
constructor(
private readonly route: ActivatedRoute,
@@ -42,24 +38,11 @@ export class AppActionsPage {
private readonly patch: PatchDbService,
) {}
ngOnInit() {
this.subs = [
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
this.pkg = pkg
}),
]
}
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
async handleAction(
pkg: PackageDataEntry,
action: { key: string; value: Action },
) {
const status = pkg.installed?.status
if (
status &&
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
@@ -134,14 +117,14 @@ export class AppActionsPage {
}
}
async tryUninstall(): Promise<void> {
const { title, alerts } = this.pkg.manifest
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
const { title, alerts } = pkg.manifest
let message =
alerts.uninstall ||
`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`
}
@@ -233,5 +216,5 @@ interface LocalAction {
styleUrls: ['./app-actions.page.scss'],
})
export class AppActionsItemComponent {
@Input() action: LocalAction
@Input() action!: LocalAction
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { map } from 'rxjs/operators'
import { ConnectionService } from 'src/app/services/connection.service'
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 {
@Input()
pkg: PkgInfo
pkg!: PkgInfo
disconnected$ = this.connectionService.watchDisconnected$()

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,21 @@
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
</ion-buttons>
<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-header>
<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 *ngFor="let metric of metrics | keyvalue : asIsOrder">
<ion-label>{{ metric.key }}</ion-label>
<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-item>
</ion-item-group>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import {
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import { Emver, ErrorToastService, isEmptyObject } from '@start9labs/shared'
import {
PackageDataEntry,
PackageState,
@@ -16,12 +17,10 @@ import {
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
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 { isEmptyObject } from '../../../../../../../shared/src/util/misc.util'
import { Breakages } from 'src/app/services/api/api.types'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { getAllPackages } from 'src/app/util/get-package-data'
@Component({
selector: 'marketplace-show-controls',
@@ -31,16 +30,18 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
})
export class MarketplaceShowControlsComponent {
@Input()
pkg: MarketplacePkg
pkg!: MarketplacePkg
@Input()
localPkg: PackageDataEntry | null = null
localPkg!: PackageDataEntry | null
readonly showDevTools$ = this.localStorageService.showDevTools$
readonly PackageState = PackageState
constructor(
private readonly alertCtrl: AlertController,
public readonly localStorageService: LocalStorageService,
private readonly localStorageService: LocalStorageService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly loadingCtrl: LoadingController,
@@ -151,7 +152,7 @@ export class MarketplaceShowControlsComponent {
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
let message: string =
'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 title = localPkgs[id].manifest.title
return `<li><b>${title}</b></li>`

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,11 @@
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<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-header>

View File

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

View File

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

View File

@@ -11,7 +11,10 @@
<ng-container *ngIf="ui$ | async as ui">
<ion-item-group *ngIf="server$ | async as server">
<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-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
</ion-item>

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core'
import { Component } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import {
LoadingController,
@@ -34,7 +34,10 @@ export class PreferencesPage {
readonly serverConfig: ServerConfigService,
) {}
async presentModalName(placeholder: string): Promise<void> {
async presentModalName(
placeholder: string,
initialValue: string,
): Promise<void> {
const options: GenericInputOptions = {
title: 'Edit Device Name',
message: 'This is for your reference only.',
@@ -42,7 +45,7 @@ export class PreferencesPage {
useMask: false,
placeholder,
nullable: true,
initialValue: this.patch.getData().ui.name,
initialValue,
buttonText: 'Save',
submitFn: (value: string) =>
this.setDbValue('name', value || placeholder),

View File

@@ -7,7 +7,6 @@ import {
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { take } from 'rxjs/operators'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { EOSService } from 'src/app/services/eos.service'
import { Observable } from 'rxjs'
@Component({
@@ -25,10 +24,7 @@ export class BackingUpComponent {
PackageMainStatus = PackageMainStatus
constructor(
public readonly eosService: EOSService,
public readonly patch: PatchDbService,
) {}
constructor(private readonly patch: PatchDbService) {}
}
@Pipe({

View File

@@ -20,6 +20,7 @@ import {
import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.page'
import { EOSService } from 'src/app/services/eos.service'
import { DestroyService } from '@start9labs/shared'
import { getServerInfo } from 'src/app/util/get-server-info'
@Component({
selector: 'server-backup',
@@ -28,7 +29,6 @@ import { DestroyService } from '@start9labs/shared'
providers: [DestroyService],
})
export class ServerBackupPage {
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
serviceIds: string[] = []
readonly backingUp$ = this.eosService.backingUp$
@@ -56,8 +56,6 @@ export class ServerBackupPage {
async presentModalSelect(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
) {
this.target = target
const modal = await this.modalCtrl.create({
presentingElement: await this.modalCtrl.getTop(),
component: BackupSelectPage,
@@ -66,14 +64,16 @@ export class ServerBackupPage {
modal.onWillDismiss().then(res => {
if (res.data) {
this.serviceIds = res.data
this.presentModalPassword()
this.presentModalPassword(target)
}
})
await modal.present()
}
async presentModalPassword(): Promise<void> {
private async presentModalPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
): Promise<void> {
const options: GenericInputOptions = {
title: 'Master Password Needed',
message: 'Enter your master password to encrypt this backup.',
@@ -83,25 +83,29 @@ export class ServerBackupPage {
buttonText: 'Create Backup',
submitFn: async (password: string) => {
// confirm password matches current master password
const passwordHash =
this.patch.getData()['server-info']['password-hash']
const { 'password-hash': passwordHash } = await getServerInfo(
this.patch,
)
argon2.verify(passwordHash, password)
// first time backup
if (!this.target.hasValidBackup) {
await this.createBackup(password)
if (!target.hasValidBackup) {
await this.createBackup(target, password)
// existing backup
} else {
try {
const passwordHash =
this.target.entry['embassy-os']?.['password-hash'] || ''
target.entry['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, password)
} catch {
setTimeout(() => this.presentModalOldPassword(password), 500)
setTimeout(
() => this.presentModalOldPassword(target, password),
500,
)
return
}
await this.createBackup(password)
await this.createBackup(target, password)
}
},
}
@@ -115,7 +119,10 @@ export class ServerBackupPage {
await m.present()
}
private async presentModalOldPassword(password: string): Promise<void> {
private async presentModalOldPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
): Promise<void> {
const options: GenericInputOptions = {
title: 'Original Password Needed',
message:
@@ -125,11 +132,10 @@ export class ServerBackupPage {
useMask: true,
buttonText: 'Create Backup',
submitFn: async (oldPassword: string) => {
const passwordHash =
this.target.entry['embassy-os']?.['password-hash'] || ''
const passwordHash = target.entry['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, oldPassword)
await this.createBackup(password, oldPassword)
await this.createBackup(target, password, oldPassword)
},
}
@@ -143,6 +149,7 @@ export class ServerBackupPage {
}
private async createBackup(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
oldPassword?: string,
): Promise<void> {
@@ -153,7 +160,7 @@ export class ServerBackupPage {
try {
await this.embassyApi.createBackup({
'target-id': this.target.id,
'target-id': target.id,
'package-ids': this.serviceIds,
'old-password': oldPassword || null,
password,

View File

@@ -4,9 +4,11 @@
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<ion-title>OS Logs</ion-title>
<ion-button slot="end" fill="clear" size="small" (click)="copy()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
<ion-buttons slot="end">
<ion-button (click)="copy()">
<ion-icon name="copy-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -9,11 +9,6 @@ import { copyToClipboard, strip } from 'src/app/util/web.util'
styleUrls: ['./server-logs.page.scss'],
})
export class ServerLogsPage {
pkgId: string
loading = true
needInfinite = true
before: string
constructor(
private readonly embassyApi: ApiService,
private readonly toastCtrl: ToastController,

View File

@@ -4,25 +4,32 @@
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<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-header>
<ion-content class="ion-padding">
<skeleton-list *ngIf="loading" groups="2"></skeleton-list>
<skeleton-list *ngIf="loading" [groups]="2"></skeleton-list>
<div id="metricSection">
<ng-container *ngIf="!loading">
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
<ion-item-group
*ngFor="let metricGroup of metrics | keyvalue : asIsOrder"
>
<ion-item-divider>{{ metricGroup.key }}</ion-item-divider>
<ion-item *ngFor="let metric of metricGroup.value | keyvalue : asIsOrder">
<ion-item
*ngFor="let metric of metricGroup.value | keyvalue : asIsOrder"
>
<ion-label>{{ metric.key }}</ion-label>
<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-item>
</ion-item-group>
</ng-container>
</div>
</ion-content>
</ion-content>

View File

@@ -37,7 +37,7 @@
<ng-container *ngFor="let button of cat.value">
<ion-item
button
[style.display]="(button.title === 'Repair Disk' && !(localStorageService.showDiskRepair$ | async)) ? 'none' : 'block'"
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
[detail]="button.detail"
[disabled]="button.disabled | async"
(click)="button.action()"
@@ -55,7 +55,7 @@
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
>
Last Backup: {{ server['last-backup'] ?
(server['last-backup'] | date: 'short') : 'never' }}
(server['last-backup'] | date: 'medium') : 'never' }}
</ion-text>
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
<ion-spinner
@@ -76,9 +76,7 @@
Update Complete. Restart to apply changes
</ion-text>
<ng-template #notUpdated>
<ng-container
*ngIf="eosService.showUpdate$ | async; else check"
>
<ng-container *ngIf="showUpdate$ | async; else check">
<ion-text class="inline" color="success">
<ion-icon name="rocket-outline"></ion-icon>
Update Available

View File

@@ -15,6 +15,7 @@ import { EOSService } from 'src/app/services/eos.service'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { RecoveredPackageDataEntry } from 'src/app/services/patch-db/data-model'
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
import { getAllPackages } from '../../../util/get-package-data'
@Component({
selector: 'server-show',
@@ -22,12 +23,14 @@ import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
styleUrls: ['server-show.page.scss'],
})
export class ServerShowPage {
hasRecoveredPackage: boolean
hasRecoveredPackage = false
clicks = 0
readonly server$ = this.patch.watch$('server-info')
readonly ui$ = this.patch.watch$('ui')
readonly connected$ = this.patch.connected$
readonly showUpdate$ = this.eosService.showUpdate$
readonly showDiskRepair$ = this.localStorageService.showDiskRepair$
constructor(
private readonly alertCtrl: AlertController,
@@ -38,8 +41,8 @@ export class ServerShowPage {
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
private readonly patch: PatchDbService,
public readonly eosService: EOSService,
public readonly localStorageService: LocalStorageService,
private readonly eosService: EOSService,
private readonly localStorageService: LocalStorageService,
) {}
ngOnInit() {
@@ -63,7 +66,7 @@ export class ServerShowPage {
} else {
const modal = await this.modalCtrl.create({
componentProps: {
releaseNotes: this.eosService.eos['release-notes'],
releaseNotes: this.eosService.eos?.['release-notes'],
},
component: OSUpdatePage,
})
@@ -117,7 +120,8 @@ export class ServerShowPage {
}
async presentAlertSystemRebuild() {
const minutes = Object.keys(this.patch.getData()['package-data']).length * 2
const localPkgs = await getAllPackages(this.patch)
const minutes = Object.keys(localPkgs).length * 2
const alert = await this.alertCtrl.create({
header: 'Warning',
message: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`,

View File

@@ -21,7 +21,7 @@
<ion-item>
<ion-label>
<h2>Git Hash</h2>
<p>{{ config.gitHash }}</p>
<p>{{ gitHash }}</p>
</ion-label>
</ion-item>

View File

@@ -1,5 +1,5 @@
import { Component, ViewChild } from '@angular/core'
import { IonContent, ToastController } from '@ionic/angular'
import { Component } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { copyToClipboard } from 'src/app/util/web.util'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ConfigService } from 'src/app/services/config.service'
@@ -10,18 +10,16 @@ import { ConfigService } from 'src/app/services/config.service'
styleUrls: ['./server-specs.page.scss'],
})
export class ServerSpecsPage {
@ViewChild(IonContent) content: IonContent
readonly server$ = this.patch.watch$('server-info')
constructor(
private readonly toastCtrl: ToastController,
private readonly patch: PatchDbService,
public readonly config: ConfigService,
private readonly config: ConfigService,
) {}
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
get gitHash(): string {
return this.config.gitHash
}
async copy(address: string) {

View File

@@ -9,7 +9,7 @@
<ion-content class="ion-padding-top">
<!-- loading -->
<ion-item-group *ngIf="loading">
<ion-item-group *ngIf="loading; else notLoading">
<div *ngFor="let entry of ['This Session', 'Other Sessions']">
<ion-item-divider>{{ entry }}</ion-item-divider>
<ion-item style="padding-bottom: 6px">
@@ -41,60 +41,64 @@
</ion-item-group>
<!-- not loading -->
<ion-item-group *ngIf="!loading">
<ion-item-divider>Current Session</ion-item-divider>
<ion-item>
<ion-icon
slot="start"
size="large"
[name]="getPlatformIcon(currentSession.metadata.platforms)"
></ion-icon>
<ion-label>
<h1>{{ getPlatformName(currentSession.metadata.platforms) }}</h1>
<h2>
Last Active: {{ currentSession['last-active'] | date : 'medium' }}
</h2>
<p>{{ currentSession['user-agent'] }}</p>
</ion-label>
</ion-item>
<ion-item-divider>
Other Sessions
<ion-button
*ngIf="otherSessions.length"
slot="end"
fill="clear"
strong
(click)="presentAlertKillAll()"
>
Terminate all
</ion-button>
</ion-item-divider>
<div *ngFor="let session of otherSessions">
<ng-template #notLoading>
<ion-item-group *ngIf="currentSession">
<ion-item-divider>Current Session</ion-item-divider>
<ion-item>
<ion-icon
slot="start"
size="large"
[name]="getPlatformIcon(session.metadata.platforms)"
[name]="getPlatformIcon(currentSession.metadata.platforms)"
></ion-icon>
<ion-label>
<h1>{{ getPlatformName(session.metadata.platforms) }}</h1>
<h2>Last Active: {{ session['last-active'] | date : 'medium' }}</h2>
<p>{{ session['user-agent'] }}</p>
<h1>{{ getPlatformName(currentSession.metadata.platforms) }}</h1>
<h2>
Last Active: {{ currentSession['last-active'] | date : 'medium' }}
</h2>
<p>{{ currentSession['user-agent'] }}</p>
</ion-label>
</ion-item>
<ion-item-divider>
Other Sessions
<ion-button
*ngIf="otherSessions.length"
slot="end"
fill="clear"
(click)="presentAlertKill(session.id)"
strong
(click)="presentAlertKillAll()"
>
<ion-icon slot="icon-only" name="log-out-outline"></ion-icon>
Terminate all
</ion-button>
</ion-item-divider>
<div *ngFor="let session of otherSessions">
<ion-item>
<ion-icon
slot="start"
size="large"
[name]="getPlatformIcon(session.metadata.platforms)"
></ion-icon>
<ion-label>
<h1>{{ getPlatformName(session.metadata.platforms) }}</h1>
<h2>Last Active: {{ session['last-active'] | date : 'medium' }}</h2>
<p>{{ session['user-agent'] }}</p>
</ion-label>
<ion-button
slot="end"
fill="clear"
color="danger"
(click)="kill([session.id])"
>
Logout
<ion-icon slot="start" name="log-out-outline"></ion-icon>
</ion-button>
</ion-item>
</div>
<ion-item *ngIf="!otherSessions.length">
<ion-label>
<p>You are not logged in anywhere else</p>
</ion-label>
</ion-item>
</div>
<ion-item *ngIf="!otherSessions.length">
<ion-label>
<p>You are not logged in anywhere else</p>
</ion-label>
</ion-item>
</ion-item-group>
</ion-item-group>
</ng-template>
</ion-content>

View File

@@ -11,7 +11,7 @@ import { PlatformType, Session } from 'src/app/services/api/api.types'
})
export class SessionsPage {
loading = true
currentSession: Session
currentSession?: Session
otherSessions: SessionWithId[] = []
constructor(
@@ -67,27 +67,6 @@ export class SessionsPage {
await alert.present()
}
async presentAlertKill(id: string) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Terminate other web session?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Terminate',
handler: () => {
this.kill([id])
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
async kill(ids: string[]): Promise<void> {
const loader = await this.loadingCtrl.create({
message: `Terminating session${ids.length > 1 ? 's' : ''}...`,

View File

@@ -21,14 +21,13 @@
color="dark"
style="font-size: 42px"
></ion-icon>
<h4>Manually upload a service package</h4>
<h4>Upload .s9pk package file</h4>
<p *ngIf="onTor">
<ion-text color="success"
>Tip: switch to LAN for faster uploads.</ion-text
>
</p>
<br />
<ion-button color="primary" type="file">
<ion-button color="primary" type="file" class="ion-margin-top">
<label for="upload-photo">Browse</label>
<input
type="file"

View File

@@ -58,7 +58,6 @@
min-width: 200px;
max-width: 200px;
height: auto;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
@@ -89,4 +88,4 @@
p {
text-align: center;
}
}
}

View File

@@ -30,7 +30,7 @@ export class SideloadPage {
file: null,
}
onTor = this.config.isTor()
uploadState: {
uploadState?: {
invalid: boolean
message: string
}
@@ -52,6 +52,7 @@ export class SideloadPage {
const files = e.target.files
this.setFile(files)
}
async setFile(files?: File[]) {
if (!files || !files.length) return
const file = files[0]

View File

@@ -66,7 +66,7 @@
<ion-icon slot="start" name="key-outline" size="large"></ion-icon>
<ion-label>
<h1>{{ ssh.hostname }}</h1>
<h2>{{ ssh['created-at'] | date: 'short' }}</h2>
<h2>{{ ssh['created-at'] | date: 'medium' }}</h2>
<p>{{ ssh.alg }} {{ ssh.fingerprint }}</p>
</ion-label>
<ion-button