mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
chore: enable strict mode (#1569)
* chore: enable strict mode * refactor: remove sync data access from PatchDbService * launchable even when no LAN url Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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$
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
(['string', 'number'] | includes: data.spec.type) &&
|
||||
!$any(data.spec).nullable
|
||||
"
|
||||
> *</span
|
||||
>
|
||||
*
|
||||
</span>
|
||||
|
||||
<span *ngIf="data.spec.type === 'list' && Range.from(data.spec.range).min"
|
||||
> *</span
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import { Component, Input } from '@angular/core'
|
||||
styleUrls: ['./qr.component.scss'],
|
||||
})
|
||||
export class QRComponent {
|
||||
@Input() text: string
|
||||
@Input() text!: string
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Component, Input, OnChanges } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
background: rgba(53, 56, 62, 0.768);
|
||||
border-radius: 7px;
|
||||
padding: 27px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
export interface AppRecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
'newer-eos': boolean
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toOptions',
|
||||
})
|
||||
export class ToOptionsPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
|
||||
transform(
|
||||
packageData$: Observable<Record<string, PackageDataEntry>>,
|
||||
packageBackups: Record<string, PackageBackupInfo> = {},
|
||||
): Observable<AppRecoverOption[]> {
|
||||
return packageData$.pipe(
|
||||
map(packageData =>
|
||||
Object.keys(packageBackups).map(id => ({
|
||||
...packageBackups[id],
|
||||
id,
|
||||
installed: !!packageData[id],
|
||||
checked: false,
|
||||
'newer-eos': this.compare(packageBackups[id]['os-version']),
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private compare(version: string): boolean {
|
||||
// checks to see if backup was made on a newer version of EOS
|
||||
return this.emver.compare(version, this.config.version) === 1
|
||||
}
|
||||
}
|
||||
@@ -11,20 +11,35 @@
|
||||
|
||||
<ion-content>
|
||||
<ion-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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 }[] = []
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
<h1>{{ action.name }}</h1>
|
||||
<p>{{ action.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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$()
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -26,7 +26,7 @@ export class AppListRecComponent {
|
||||
readonly delete$ = new Subject<RecoveredInfo>()
|
||||
|
||||
@Input()
|
||||
rec: RecoveredInfo
|
||||
rec!: RecoveredInfo
|
||||
|
||||
@Output()
|
||||
readonly deleted = new EventEmitter<void>()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,5 +9,5 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class AppShowHeaderComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
pkg!: PackageDataEntry
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
})
|
||||
export class AppShowHealthChecksComponent {
|
||||
@Input()
|
||||
pkg: PackageDataEntry
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
HealthResult = HealthResult
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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']"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -202,12 +202,6 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
packageMarketplace,
|
||||
currentMarketplace,
|
||||
pkgId,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
],
|
||||
},
|
||||
cssClass: 'medium-modal',
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ import { takeUntil } from 'rxjs/operators'
|
||||
providers: [DestroyService],
|
||||
})
|
||||
export class DeveloperListPage {
|
||||
devData: DevData
|
||||
devData: DevData = {}
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-repo
|
||||
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
|
||||
import { 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 = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
<p>{{ config.gitHash }}</p>
|
||||
<p>{{ gitHash }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' : ''}...`,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user