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

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

View File

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

View File

@@ -49,7 +49,13 @@
</ion-item>
</ion-menu-toggle>
</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="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false">

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,8 @@ SwiperCore.use([IonicSlides])
styleUrls: ['./app-wizard.component.scss'],
})
export class AppWizardComponent {
@Input() params: {
@Input()
params!: {
action: WizardAction
title: string
slides: SlideDefinition[]
@@ -31,16 +32,17 @@ export class AppWizardComponent {
}
// 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
@ViewChildren('components')
slideComponentsQL: QueryList<BaseSlide>
slideComponentsQL?: QueryList<BaseSlide>
get slideComponents(): BaseSlide[] {
return this.slideComponentsQL.toArray()
return this.slideComponentsQL?.toArray() || []
}
get currentSlide(): BaseSlide {
@@ -48,7 +50,7 @@ export class AppWizardComponent {
}
get currentIndex(): number {
return this.swiper.activeIndex
return this.swiper?.activeIndex || NaN
}
initializing = true
@@ -58,7 +60,7 @@ export class AppWizardComponent {
ionViewDidEnter() {
this.initializing = false
this.swiper.allowTouchMove = false
if (this.swiper) this.swiper.allowTouchMove = false
this.loadSlide()
}
@@ -71,8 +73,8 @@ export class AppWizardComponent {
}
async next() {
await this.content.scrollToTop()
this.swiper.slideNext(500)
await this.content?.scrollToTop()
this.swiper?.slideNext(500)
}
setError(e: any) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
*ngIf="!loading && needInfinite"
position="top"
threshold="0"
(ionInfinite)="loadData($event)"
(ionInfinite)="doInfinite($event)"
>
<ion-infinite-scroll-content
loadingSpinner="lines"
@@ -25,11 +25,11 @@
></div>
</div>
<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
<ion-icon slot="end" name="refresh"></ion-icon>
</ion-button>
<ion-spinner *ngIf="loadingMore" name="lines" color="warning"></ion-spinner>
<ion-spinner *ngIf="loadingNext" name="lines" color="warning"></ion-spinner>
</div>
<div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,15 +16,24 @@
</ion-label>
</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">
<qr-code [value]="actionRes.value" size="240"></qr-code>
</div>
<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>
<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>
</div>
</ion-content>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,11 @@ import {
ModalController,
IonicSafeString,
} 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 { 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 { AppRecoverOption } from './to-options.pipe'
@Component({
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'],
})
export class AppRecoverSelectPage {
@Input() id: string
@Input() backupInfo: BackupInfo
@Input() password: string
@Input() oldPassword: string
options: (PackageBackupInfo & {
id: string
checked: boolean
installed: boolean
'newer-eos': boolean
})[]
@Input() id!: string
@Input() backupInfo!: BackupInfo
@Input() password!: string
@Input() oldPassword?: string
readonly packageData$ = this.patch.watch$('package-data')
hasSelection = false
error: string | IonicSafeString
error: string | IonicSafeString = ''
constructor(
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
private readonly emver: Emver,
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() {
this.modalCtrl.dismiss()
}
handleChange() {
this.hasSelection = this.options.some(o => o.checked)
handleChange(options: AppRecoverOption[]) {
this.hasSelection = options.some(o => o.checked)
}
async restore(): Promise<void> {
const ids = this.options
.filter(option => !!option.checked)
.map(option => option.id)
async restore(options: AppRecoverOption[]): Promise<void> {
const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
const loader = await this.loadingCtrl.create({
message: 'Initializing...',
})
@@ -76,7 +52,7 @@ export class AppRecoverSelectPage {
await this.embassyApi.restorePackages({
ids,
'target-id': this.id,
'old-password': this.oldPassword,
'old-password': this.oldPassword || null,
password: this.password,
})
this.modalCtrl.dismiss(undefined, 'success')

View File

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

View File

@@ -11,20 +11,35 @@
<ion-content>
<ion-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-label>
<h2>System data</h2>
<p><ion-text [color]="system.color">{{ system.result }}</ion-text></p>
</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 *ngFor="let pkg of report.packages | keyvalue">
<ion-item *ngFor="let pkg of report?.packages | keyvalue">
<ion-label>
<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-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-group>
</ion-content>

View File

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

View File

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

View File

@@ -10,11 +10,12 @@
</ion-header>
<ion-content class="ion-padding">
<form [formGroup]="formGroup" (ngSubmit)="handleClick(submitBtn.handler)" novalidate>
<form-object
[objectSpec]="spec"
[formGroup]="formGroup"
></form-object>
<form
[formGroup]="formGroup"
(ngSubmit)="handleClick(submitBtn.handler)"
novalidate
>
<form-object [objectSpec]="spec" [formGroup]="formGroup"></form-object>
<button hidden type="submit"></button>
</form>
</ion-content>
@@ -22,7 +23,11 @@
<ion-footer>
<ion-toolbar class="footer">
<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 }}
</ion-button>
</ion-buttons>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -3,8 +3,8 @@ import { ValueSpec, DefaultString } from './config-types'
export class Range {
min?: number
max?: number
minInclusive: boolean
maxInclusive: boolean
minInclusive!: boolean
maxInclusive!: boolean
static from(s: string): Range {
const r = new Range()

View File

@@ -37,7 +37,7 @@ export class MockApiService extends ApiService {
expireId: null,
})
private readonly revertTime = 2000
sequence: number
sequence = 0
constructor(private readonly bootstrapper: LocalStorageBootstrap) {
super()

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