mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
cosmetics plus a slew of little frontend rendering bugs
This commit is contained in:
committed by
Aiden McClelland
parent
c18a119c70
commit
7dc53a4e85
@@ -75,7 +75,6 @@ export class AppComponent {
|
|||||||
async init () {
|
async init () {
|
||||||
await this.storage.create()
|
await this.storage.create()
|
||||||
await this.authService.init()
|
await this.authService.init()
|
||||||
await this.emver.init()
|
|
||||||
await this.patch.init()
|
await this.patch.init()
|
||||||
|
|
||||||
this.router.initialNavigation()
|
this.router.initialNavigation()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
<ion-item *ngIf="spec.description">
|
<ion-item *ngIf="spec.description">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p>
|
<p>
|
||||||
<ion-text color="dark">Description</ion-text>
|
<ion-text color="dark">Description</ion-text>
|
||||||
</p>
|
</p>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- warning -->
|
<!-- warning -->
|
||||||
<ion-item *ngIf="spec['change-warning']">
|
<ion-item *ngIf="spec['change-warning']">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p>
|
<p>
|
||||||
<ion-text color="warning">Warning!</ion-text>
|
<ion-text color="warning">Warning!</ion-text>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
<!-- primitive -->
|
<!-- primitive -->
|
||||||
<ng-container *ngIf="['string', 'number', 'boolean', 'enum'] | includes : spec.type">
|
<ng-container *ngIf="['string', 'number', 'boolean', 'enum'] | includes : spec.type">
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<p class="input-label">
|
<h4 class="input-label">
|
||||||
<form-label [data]="{
|
<form-label [data]="{
|
||||||
spec: spec,
|
spec: spec,
|
||||||
isNew: current && current[entry.key] === undefined,
|
isNew: current && current[entry.key] === undefined,
|
||||||
isEdited: entry.value.dirty
|
isEdited: entry.value.dirty
|
||||||
}"></form-label>
|
}"></form-label>
|
||||||
</p>
|
</h4>
|
||||||
<!-- string -->
|
<!-- string -->
|
||||||
<ion-item color="dark" *ngIf="spec.type === 'string'">
|
<ion-item color="dark" *ngIf="spec.type === 'string'">
|
||||||
<ion-input [type]="spec.masked && !unmasked[entry.key] ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
|
<ion-input [type]="spec.masked && !unmasked[entry.key] ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionChange)="presentAlertChangeWarning(entry.key, spec)"></ion-input>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
.help-icon {
|
.help-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 1px;
|
||||||
|
padding-right: 6px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--ion-color-dark);
|
color: var(--ion-color-dark);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -18,17 +19,6 @@ ion-item-divider {
|
|||||||
border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-label {
|
|
||||||
// padding-top: 10px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: medium;
|
|
||||||
font-weight: bold;
|
|
||||||
* {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nested-wrapper {
|
.nested-wrapper {
|
||||||
padding: 0 0 30px 30px;
|
padding: 0 0 30px 30px;
|
||||||
// border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
// border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))))
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<ion-item button (click)="handleClick()">
|
|
||||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Added'" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
|
|
||||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'NoChange'" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
|
|
||||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Edited'" style="margin-right: 15px" color="primary" name="ellipse"></ion-icon>
|
|
||||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Invalid'" style="margin-right: 15px" color="danger" name="warning-outline"></ion-icon>
|
|
||||||
|
|
||||||
<div class="organizer">
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
{{ spec.name }}
|
|
||||||
<ion-text class="new-tag" *ngIf="anno | annotationStatus: 'Added'">(new)</ion-text>
|
|
||||||
</ion-label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ion-note *ngIf="displayValue" style="font-size: small;" [class.bold]="anno | annotationStatus: 'Edited'" slot="end">{{ displayValue }}</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<ion-item-group>
|
|
||||||
<object-config-item
|
|
||||||
*ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder"
|
|
||||||
[key]="keyval.key"
|
|
||||||
[spec]="keyval.value"
|
|
||||||
[value]="value[keyval.key]"
|
|
||||||
[anno]="annotations.members[keyval.key]"
|
|
||||||
(onClick)="handleClick(keyval.key)"
|
|
||||||
[class.add-margin]="keyval.key === 'advanced'"
|
|
||||||
></object-config-item>
|
|
||||||
</ion-item-group>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { ObjectConfigComponent, ObjectConfigItemComponent } from './object-config.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
ObjectConfigComponent,
|
|
||||||
ObjectConfigItemComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
IonicModule,
|
|
||||||
SharingModule,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
ObjectConfigComponent,
|
|
||||||
ObjectConfigItemComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ObjectConfigComponentModule { }
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.add-margin {
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-tag {
|
|
||||||
padding: 0px 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: smaller;
|
|
||||||
color: #cecece;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organizer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
|
||||||
import { Annotation, Annotations } from '../../pkg-config/config-utilities'
|
|
||||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
|
||||||
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
|
|
||||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
|
||||||
import { IonNav } from '@ionic/angular'
|
|
||||||
import { SubNavService } from 'src/app/services/sub-nav.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'object-config',
|
|
||||||
templateUrl: './object-config.component.html',
|
|
||||||
styleUrls: ['./object-config.component.scss'],
|
|
||||||
})
|
|
||||||
export class ObjectConfigComponent {
|
|
||||||
@Input() cursor: ConfigCursor<'object' | 'union'>
|
|
||||||
@Output() onEdit = new EventEmitter<boolean>()
|
|
||||||
spec: ValueSpecOf<'object' | 'union'>
|
|
||||||
value: object
|
|
||||||
annotations: Annotations<'object' | 'union'>
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private readonly subNav: SubNavService,
|
|
||||||
private readonly nav: IonNav,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.spec = this.cursor.spec()
|
|
||||||
this.value = this.cursor.config()
|
|
||||||
this.annotations = this.cursor.getAnnotations()
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleClick (key: string) {
|
|
||||||
const nextCursor = this.cursor.seekNext(key)
|
|
||||||
nextCursor.createFirstEntryForList()
|
|
||||||
this.subNav.push(key, nextCursor, this.nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
asIsOrder () {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'object-config-item',
|
|
||||||
templateUrl: './object-config-item.component.html',
|
|
||||||
styleUrls: ['./object-config.component.scss'],
|
|
||||||
})
|
|
||||||
export class ObjectConfigItemComponent {
|
|
||||||
@Input() key: string
|
|
||||||
@Input() spec: ValueSpec
|
|
||||||
@Input() value: string | number
|
|
||||||
@Input() anno: Annotation
|
|
||||||
@Output() onClick = new EventEmitter<boolean>()
|
|
||||||
maskPipe: MaskPipe = new MaskPipe()
|
|
||||||
|
|
||||||
displayValue?: string | number | boolean
|
|
||||||
|
|
||||||
ngOnChanges () {
|
|
||||||
switch (this.spec.type) {
|
|
||||||
case 'string':
|
|
||||||
if (this.value) {
|
|
||||||
if (this.spec.masked) {
|
|
||||||
this.displayValue = this.maskPipe.transform(this.value as string, 4)
|
|
||||||
} else {
|
|
||||||
this.displayValue = this.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.displayValue = '-'
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'boolean':
|
|
||||||
this.displayValue = String(this.value)
|
|
||||||
break
|
|
||||||
case 'number':
|
|
||||||
this.displayValue = this.value || '-'
|
|
||||||
if (this.displayValue && this.spec.units) {
|
|
||||||
this.displayValue = `${this.displayValue} ${this.spec.units}`
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'enum':
|
|
||||||
this.displayValue = this.spec['value-names'][this.value]
|
|
||||||
break
|
|
||||||
case 'pointer':
|
|
||||||
this.displayValue = 'System Defined'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleClick (): Promise<void> {
|
|
||||||
if (this.spec.type === 'pointer') return
|
|
||||||
this.onClick.emit(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ng-container *ngFor="let g of groupsArr">
|
<ng-container *ngFor="let g of groupsArr">
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<ion-skeleton-text style="width: 15%"></ion-skeleton-text>
|
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item *ngFor="let r of rowsArr">
|
<ion-item *ngFor="let r of rowsArr">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
|
<ion-skeleton-text animated style="width: 200px; height: 14px;"></ion-skeleton-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-note slot="end">
|
<ion-note slot="end">
|
||||||
<ion-skeleton-text animated style="width: 100%"></ion-skeleton-text>
|
<ion-skeleton-text animated style="width: 80px; height: 14px;"></ion-skeleton-text>
|
||||||
</ion-note>
|
</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -17,12 +17,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!groups">
|
<ng-container *ngIf="!groups">
|
||||||
<ion-item *ngFor="let r of rowsArr">
|
<ion-item-group>
|
||||||
<ion-label>
|
<ion-item *ngFor="let r of rowsArr">
|
||||||
<ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
|
<ion-label>
|
||||||
</ion-label>
|
<ion-skeleton-text animated style="width: 200px; height: 14px;"></ion-skeleton-text>
|
||||||
<ion-note slot="end">
|
</ion-label>
|
||||||
<ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
|
<ion-note slot="end">
|
||||||
</ion-note>
|
<ion-skeleton-text animated style="width: 80px; height: 14px;"></ion-skeleton-text>
|
||||||
</ion-item>
|
</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<ion-header>
|
<ion-header style="min-height: unset;">
|
||||||
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
|
<ion-toolbar *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||||
<ion-title>Config</ion-title>
|
<ion-title>Config</ion-title>
|
||||||
<ion-buttons slot="end" class="ion-padding-end">
|
<ion-buttons slot="end" class="ion-padding-end">
|
||||||
<ion-button fill="clear" [disabled]="loadingText" (click)="resetDefaults()">
|
<ion-button fill="clear" [disabled]="loadingText" (click)="resetDefaults()">
|
||||||
|
<ion-icon slot="start" name="refresh-outline"></ion-icon>
|
||||||
Reset Defaults
|
Reset Defaults
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
<ng-container *ngIf="patch.data['package-data'][pkgId] as pkg">
|
<ng-container *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||||
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited">
|
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.status.configured && !edited">
|
||||||
<ion-item class="notifier-item">
|
<ion-item class="notifier-item">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
||||||
<ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon>
|
<ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon>
|
||||||
<ion-text style="font-size: smaller;">Initial Config</ion-text>
|
<ion-text style="font-size: smaller;">Initial Config</ion-text>
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="rec && showRec">
|
<ng-container *ngIf="rec && showRec">
|
||||||
<ion-item class="rec-item">
|
<ion-item class="rec-item">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2 style="display: flex; align-items: center;">
|
<h2 style="display: flex; align-items: center;">
|
||||||
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
||||||
<ion-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
<ion-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
|
|
||||||
<!-- no config -->
|
<!-- no config -->
|
||||||
<ion-item *ngIf="!hasConfig">
|
<ion-item *ngIf="!hasConfig">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -18,43 +18,37 @@
|
|||||||
|
|
||||||
<ion-content *ngIf="!loading && !submitting">
|
<ion-content *ngIf="!loading && !submitting">
|
||||||
<ion-item class="ion-margin-bottom">
|
<ion-item class="ion-margin-bottom">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p class="ion-padding-bottom"><ion-text color="warning">Warning</ion-text></p>
|
|
||||||
<h2>
|
<h2>
|
||||||
Restoring from backup will overwrite all current data for {{ patch.data['package-data'][pkgId].manifest.title }} .
|
Select the drive containing the backup you would like to restore.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<ion-text color="warning">
|
||||||
|
Warning! All current data for {{ patch.data['package-data'][pkgId].manifest.title }} will be overwritten by the backup.
|
||||||
|
</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Select Backup Drive</ion-item-divider>
|
|
||||||
|
|
||||||
<ion-item *ngIf="allPartitionsMounted">
|
<ion-item *ngIf="allPartitionsMounted">
|
||||||
<ion-text class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you intend to restore.</ion-text>
|
<ion-text class="ion-text-wrap" color="warning">No drives found containing a valid backup for {{ patch.data['package-data'][pkgId].manifest.title }}. Insert the storage device containing the backup you intend to restore.</ion-text>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-card *ngFor="let disk of disks | keyvalue">
|
<ion-item-group>
|
||||||
<ion-card-header>
|
<div *ngFor="let disk of disks | keyvalue">
|
||||||
<ion-card-title>
|
<ion-item-divider>{{ disk.key }} - {{ disk.value.size }}</ion-item-divider>
|
||||||
{{ disk.value.size }}
|
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
|
||||||
</ion-card-title>
|
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||||
<ion-card-subtitle>
|
<ion-label>
|
||||||
{{ disk.key }}
|
<h1>{{ partition.value.label || partition.key }}</h1>
|
||||||
</ion-card-subtitle>
|
<h2>{{ partition.value.size || 'unknown size' }}</h2>
|
||||||
</ion-card-header>
|
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
||||||
<ion-card-content>
|
<ng-template #unavailable>
|
||||||
<ion-item-group>
|
<p><ion-text color="danger">Unavailable</ion-text></p>
|
||||||
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
|
</ng-template>
|
||||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
</ion-label>
|
||||||
<ion-label>
|
</ion-item>
|
||||||
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
|
</div>
|
||||||
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
</ion-item-group>
|
||||||
<ng-template #unavailable>
|
|
||||||
<p><ion-text color="danger">Unavailable</ion-text></p>
|
|
||||||
</ng-template>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-card>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { LoadingController, ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
||||||
import { DiskInfo } from 'src/app/services/api/api.types'
|
import { DiskInfo } from 'src/app/services/api/api.types'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { Subscription } from 'rxjs'
|
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -15,29 +14,22 @@ import { ErrorToastService } from 'src/app/services/error-toast.service'
|
|||||||
export class AppRestoreComponent {
|
export class AppRestoreComponent {
|
||||||
@Input() pkgId: string
|
@Input() pkgId: string
|
||||||
disks: DiskInfo
|
disks: DiskInfo
|
||||||
title: string
|
|
||||||
loading = true
|
loading = true
|
||||||
submitting = false
|
|
||||||
allPartitionsMounted: boolean
|
allPartitionsMounted: boolean
|
||||||
|
modal: HTMLIonModalElement
|
||||||
subs: Subscription[] = []
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly loadingCtrl: LoadingController,
|
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
public readonly patch: PatchDbService,
|
public readonly patch: PatchDbService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
async ngOnInit () {
|
||||||
this.getExternalDisks()
|
this.getExternalDisks()
|
||||||
|
this.modal = await this.modalCtrl.getTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ngAfterViewInit () {
|
|
||||||
// this.content.scrollToPoint(undefined, 1)
|
|
||||||
// }
|
|
||||||
|
|
||||||
async refresh () {
|
async refresh () {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await this.getExternalDisks()
|
await this.getExternalDisks()
|
||||||
@@ -55,42 +47,36 @@ export class AppRestoreComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentModal (logicalname: string): Promise<void> {
|
async presentModal (logicalname: string): Promise<void> {
|
||||||
const m = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
type: 'restore',
|
title: 'Enter Password',
|
||||||
|
message: 'Backup encrypted. Enter the password that was originally used to encrypt this backup.',
|
||||||
|
label: 'Password',
|
||||||
|
useMask: true,
|
||||||
|
buttonText: 'Restore',
|
||||||
|
submitFn: async (value: string) => await this.restore(logicalname, value),
|
||||||
},
|
},
|
||||||
cssClass: 'alertlike-modal',
|
cssClass: 'alertlike-modal',
|
||||||
|
presentingElement: await this.modalCtrl.getTop(),
|
||||||
component: BackupConfirmationComponent,
|
component: BackupConfirmationComponent,
|
||||||
backdropDismiss: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
m.onWillDismiss().then(res => {
|
modal.onWillDismiss().then(res => {
|
||||||
const data = res.data
|
if (res.role === 'success') this.modal.dismiss(undefined, 'success')
|
||||||
if (data.cancel) return
|
|
||||||
this.restore(logicalname, data.password)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await m.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss () {
|
dismiss () {
|
||||||
this.modalCtrl.dismiss({ })
|
this.modalCtrl.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async restore (logicalname: string, password: string): Promise<void> {
|
private async restore (logicalname: string, password: string): Promise<void> {
|
||||||
this.submitting = true
|
await this.embassyApi.restorePackage({
|
||||||
// await loader.present()
|
id: this.pkgId,
|
||||||
|
logicalname,
|
||||||
try {
|
password,
|
||||||
await this.embassyApi.restorePackage({
|
})
|
||||||
id: this.pkgId,
|
|
||||||
logicalname,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
this.modalCtrl.dismiss({ error: e })
|
|
||||||
} finally {
|
|
||||||
this.modalCtrl.dismiss({ })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,37 @@
|
|||||||
<ion-content>
|
<ion-content>
|
||||||
<div style="height: 85%; margin: 24px; display: flex; flex-direction: column; justify-content: space-between;">
|
<div style="margin: 24px 24px 12px 24px; display: flex; flex-direction: column;">
|
||||||
<div *ngIf="type === 'backup'">
|
|
||||||
<h4><ion-text color="dark">Encrypt Backup</ion-text></h4>
|
|
||||||
<p><ion-text>Enter your master password to create an encrypted backup.</ion-text></p>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="type === 'restore'">
|
|
||||||
<h4><ion-text color="dark">Decrypt Backup</ion-text></h4>
|
|
||||||
<p><ion-text>Enter the password that was originally used to encrypt this backup.</ion-text></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<ion-item style="padding-bottom: 8px;">
|
||||||
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
|
<ion-label>
|
||||||
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
|
<h1>{{ title }}</h1>
|
||||||
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="error = ''"></ion-input>
|
<br />
|
||||||
</ion-item>
|
<p>{{ message }}</p>
|
||||||
<ion-item *ngIf="error" lines="none" style="--background: var(--ion-background-color);">
|
</ion-label>
|
||||||
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{ error }}</ion-label>
|
</ion-item>
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: flex-end; align-items: center;">
|
<form (ngSubmit)="submit()">
|
||||||
<ion-button fill="clear" (click)="cancel()">
|
<div style="margin-left: 16px;">
|
||||||
Cancel
|
<p class="input-label">{{ label }}</p>
|
||||||
</ion-button>
|
<ion-item lines="none" color="dark">
|
||||||
<ion-button fill="clear" (click)="submit()" [disabled]="!password.length">
|
<ion-input [type]="useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" (ionChange)="error = ''"></ion-input>
|
||||||
{{ type === 'backup' ? 'Create Backup' : 'Restore Backup' }}
|
<ion-button slot="end" *ngIf="useMask" fill="clear" color="light" (click)="toggleMask()">
|
||||||
</ion-button>
|
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||||
</div>
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
<!-- error -->
|
||||||
|
<p>
|
||||||
|
<ion-text [color]="error ? 'danger' : 'medium'">{{ error || 'placeholder' }}</ion-text>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ion-text-right">
|
||||||
|
<ion-button fill="clear" (click)="cancel()">
|
||||||
|
Cancel
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" type="submit" [disabled]="!value">
|
||||||
|
{{ buttonText }}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { IonicSafeString, LoadingController, ModalController } from '@ionic/angular'
|
||||||
|
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'backup-confirmation',
|
selector: 'backup-confirmation',
|
||||||
@@ -7,13 +8,19 @@ import { ModalController } from '@ionic/angular'
|
|||||||
styleUrls: ['./backup-confirmation.component.scss'],
|
styleUrls: ['./backup-confirmation.component.scss'],
|
||||||
})
|
})
|
||||||
export class BackupConfirmationComponent {
|
export class BackupConfirmationComponent {
|
||||||
@Input() type: 'backup' | 'restore'
|
@Input() title: string
|
||||||
|
@Input() message: string
|
||||||
|
@Input() label = 'Enter value'
|
||||||
|
@Input() buttonText = 'Submit'
|
||||||
|
@Input() useMask = false
|
||||||
|
@Input() value = ''
|
||||||
|
@Input() submitFn: (value: string) => Promise<any>
|
||||||
unmasked = false
|
unmasked = false
|
||||||
password = ''
|
error: string | IonicSafeString
|
||||||
error = ''
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
|
private readonly loadingCtrl: LoadingController,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
toggleMask () {
|
toggleMask () {
|
||||||
@@ -21,15 +28,23 @@ export class BackupConfirmationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel () {
|
cancel () {
|
||||||
this.modalCtrl.dismiss({ cancel: true })
|
this.modalCtrl.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
submit () {
|
async submit () {
|
||||||
if (!this.password || this.password.length < 12) {
|
const loader = await this.loadingCtrl.create({
|
||||||
this.error = 'Password must be at least 12 characters in length.'
|
spinner: 'lines',
|
||||||
return
|
cssClass: 'loader',
|
||||||
|
})
|
||||||
|
loader.present()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.submitFn(this.value)
|
||||||
|
this.modalCtrl.dismiss(undefined, 'success')
|
||||||
|
} catch (e) {
|
||||||
|
this.error = getErrorMessage(e)
|
||||||
|
} finally {
|
||||||
|
loader.dismiss()
|
||||||
}
|
}
|
||||||
const { password } = this
|
|
||||||
this.modalCtrl.dismiss({ password })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ion-item button>
|
<ion-item button>
|
||||||
<ion-icon slot="start" [name]="action.icon"></ion-icon>
|
<ion-icon slot="start" [name]="action.icon" size="large"></ion-icon>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h1>{{ action.name }}</h1>
|
<h1>{{ action.name }}</h1>
|
||||||
<h2>{{ action.description }}</h2>
|
<p>{{ action.description }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -110,15 +110,13 @@ export class AppActionsPage {
|
|||||||
pkgId: this.pkgId,
|
pkgId: this.pkgId,
|
||||||
},
|
},
|
||||||
component: AppRestoreComponent,
|
component: AppRestoreComponent,
|
||||||
backdropDismiss: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
modal.onWillDismiss().then(res => {
|
modal.onWillDismiss().then(res => {
|
||||||
const data = res.data
|
if (res.role === 'success') this.navCtrl.back()
|
||||||
if (data.error) this.errToast.present(data.error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall (manifest: Manifest) {
|
async uninstall (manifest: Manifest) {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-icon slot="start" [name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"></ion-icon>
|
<ion-icon slot="start" size="large" [name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"></ion-icon>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h1>{{ interface.def.name }}</h1>
|
<h1>{{ interface.def.name }}</h1>
|
||||||
<h2>{{ interface.def.description }}</h2>
|
<h2>{{ interface.def.description }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<div style="padding-left: 54px;">
|
<div style="padding-left: 64px;">
|
||||||
<!-- has tor -->
|
<!-- has tor -->
|
||||||
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
|
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>Tor Address</h2>
|
<h2>Tor Address</h2>
|
||||||
<p>{{ tor }}</p>
|
<p>{{ tor }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- no tor -->
|
<!-- no tor -->
|
||||||
<ion-item *ngIf="!interface.addresses['tor-address']">
|
<ion-item *ngIf="!interface.addresses['tor-address']">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>Tor Address</h2>
|
<h2>Tor Address</h2>
|
||||||
<p>Service does not use a Tor Address</p>
|
<p>Service does not use a Tor Address</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<!-- lan -->
|
<!-- lan -->
|
||||||
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
|
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>LAN Address</h2>
|
<h2>LAN Address</h2>
|
||||||
<p>{{ lan }}</p>
|
<p>{{ lan }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- no lan -->
|
<!-- no lan -->
|
||||||
<ion-item *ngIf="!interface.addresses['lan-address']">
|
<ion-item *ngIf="!interface.addresses['lan-address']">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>LAN Address</h2>
|
<h2>LAN Address</h2>
|
||||||
<p>Service does not use a LAN Address</p>
|
<p>Service does not use a LAN Address</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
.vertical-align {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2">
|
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2">
|
||||||
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
|
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
|
||||||
<div class="launch-container" *ngIf="pkg.value.entry | hasUi">
|
<div class="launch-container" *ngIf="pkg.value.entry.manifest.interfaces | hasUi">
|
||||||
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry | isLaunchable)">
|
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry.state | isLaunchable : pkg.value.entry.installed.status.main.status : pkg.value.entry.manifest.interfaces)">
|
||||||
<ion-icon name="open-outline"></ion-icon>
|
<ion-icon name="open-outline"></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<ion-title>Properties</ion-title>
|
<ion-title>Properties</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<ion-button (click)="refresh()">
|
<ion-button (click)="refresh()">
|
||||||
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
|
<ion-icon slot="start" name="refresh-outline"></ion-icon>
|
||||||
|
Refresh
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
@@ -18,14 +19,14 @@
|
|||||||
<ng-template #loaded>
|
<ng-template #loaded>
|
||||||
<!-- not running -->
|
<!-- not running -->
|
||||||
<ion-item *ngIf="!running" class="ion-margin-bottom">
|
<ion-item *ngIf="!running" class="ion-margin-bottom">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p><ion-text color="warning">Service not running. Information on this page could be inaccurate.</ion-text></p>
|
<p><ion-text color="warning">Service not running. Information on this page could be inaccurate.</ion-text></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<!-- no properties -->
|
<!-- no properties -->
|
||||||
<ion-item *ngIf="properties | empty">
|
<ion-item *ngIf="properties | empty">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p>No properties.</p>
|
<p>No properties.</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
<ion-button *ngIf="prop.value.description" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
<ion-button *ngIf="prop.value.description" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
||||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>{{ prop.key }}</h2>
|
<h2>{{ prop.key }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
<ion-button *ngIf="prop.value.description" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
<ion-button *ngIf="prop.value.description" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
||||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>{{ prop.key }}</h2>
|
<h2>{{ prop.key }}</h2>
|
||||||
<p>{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | mask ) : (prop.value.value | truncateEnd : 100) }}</p>
|
<p>{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | mask ) : (prop.value.value | truncateEnd : 100) }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|||||||
@@ -18,35 +18,35 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ng-container *ngIf="pkg">
|
<ion-item-group>
|
||||||
<ion-item-group>
|
<!-- ** always ** -->
|
||||||
<!-- ** always ** -->
|
<ion-item-divider>Status</ion-item-divider>
|
||||||
<ion-item-divider>Status</ion-item-divider>
|
<ion-item>
|
||||||
<ion-item>
|
<ion-label style="overflow: visible;">
|
||||||
<ion-label style="overflow: visible;">
|
<status size="x-large" weight="500" [rendering]="rendering"></status>
|
||||||
<status size="x-large" weight="500" [rendering]="rendering"></status>
|
</ion-label>
|
||||||
</ion-label>
|
<ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg.manifest.interfaces | hasUi)" [disabled]="!(pkg.state | isLaunchable : pkg.installed.status.main.status : pkg.manifest.interfaces)" (click)="launchUiTab()">
|
||||||
<ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" (click)="launchUiTab()">
|
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
Open UI
|
||||||
Open UI
|
</ion-button>
|
||||||
</ion-button>
|
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']">
|
||||||
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']">
|
Configure
|
||||||
Configure
|
</ion-button>
|
||||||
</ion-button>
|
<ion-button slot="end" class="action-button" *ngIf="[FeStatus.Running, FeStatus.StartingUp, FeStatus.NeedsAttention] | includes : rendering.feStatus" color="danger" (click)="stop()">
|
||||||
<ion-button slot="end" class="action-button" *ngIf="[FeStatus.Running, FeStatus.StartingUp, FeStatus.NeedsAttention] | includes : rendering.feStatus" color="danger" (click)="stop()">
|
Stop
|
||||||
Stop
|
</ion-button>
|
||||||
</ion-button>
|
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.DependencyIssue" (click)="scrollToRequirements()">
|
||||||
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.DependencyIssue" (click)="scrollToRequirements()">
|
Fix
|
||||||
Fix
|
</ion-button>
|
||||||
</ion-button>
|
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.Stopped" color="success" (click)="tryStart()">
|
||||||
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.Stopped" color="success" (click)="tryStart()">
|
Start
|
||||||
Start
|
</ion-button>
|
||||||
</ion-button>
|
</ion-item>
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- ** iff installed ** -->
|
|
||||||
<ng-container *ngIf="pkg.state === PackageState.Installed">
|
|
||||||
|
|
||||||
|
<!-- ** iff && !restoring/backing-up ** -->
|
||||||
|
<ng-container *ngIf="pkg.state === PackageState.Installed">
|
||||||
|
<!-- ** iff !restoring/backing-up ** -->
|
||||||
|
<ng-container *ngIf="!([PackageMainStatus.BackingUp, PackageMainStatus.Restoring] | includes : mainStatus.status); else maintenance">
|
||||||
<!-- ** iff health checks ** -->
|
<!-- ** iff health checks ** -->
|
||||||
<ng-container *ngIf="!(mainStatus.health | empty)">
|
<ng-container *ngIf="!(mainStatus.health | empty)">
|
||||||
<ion-item-divider>Health Checks</ion-item-divider>
|
<ion-item-divider>Health Checks</ion-item-divider>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<ion-thumbnail slot="start">
|
<ion-thumbnail slot="start">
|
||||||
<img [src]="pkg.installed['dependency-info'][dep.key].icon" />
|
<img [src]="pkg.installed['dependency-info'][dep.key].icon" />
|
||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2 style="font-family: 'Montserrat'">{{ pkg.installed['dependency-info'][dep.key].manifest.title }}</h2>
|
<h2 style="font-family: 'Montserrat'">{{ pkg.installed['dependency-info'][dep.key].manifest.title }}</h2>
|
||||||
<p>{{ pkg.manifest.dependencies[dep.key].version | displayEmver }}</p>
|
<p>{{ pkg.manifest.dependencies[dep.key].version | displayEmver }}</p>
|
||||||
<p><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
|
<p><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
|
||||||
@@ -112,27 +112,30 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
<ng-template #maintenance>
|
||||||
|
App is undergoing maintenance.
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</ion-item-group>
|
||||||
|
|
||||||
<!-- ** installed or updating ** -->
|
<!-- ** installing or updating ** -->
|
||||||
<div *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.state" style="padding: 16px;">
|
<div *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.state" style="padding: 16px;">
|
||||||
<p>Downloading: {{ (pkg['install-progress'] | installState).downloadProgress }}%</p>
|
<p>Downloading: {{ (pkg['install-progress'] | installState).downloadProgress }}%</p>
|
||||||
<ion-progress-bar
|
<ion-progress-bar
|
||||||
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
||||||
[value]="(pkg['install-progress'] | installState).downloadProgress / 100"
|
[value]="(pkg['install-progress'] | installState).downloadProgress / 100"
|
||||||
></ion-progress-bar>
|
></ion-progress-bar>
|
||||||
|
|
||||||
<p>Validating: {{ (pkg['install-progress'] | installState).validateProgress }}%</p>
|
<p>Validating: {{ (pkg['install-progress'] | installState).validateProgress }}%</p>
|
||||||
<ion-progress-bar
|
<ion-progress-bar
|
||||||
[color]="pkg['install-progress']['validation-complete'] ? 'success' : 'secondary'"
|
[color]="pkg['install-progress']['validation-complete'] ? 'success' : 'secondary'"
|
||||||
[value]="(pkg['install-progress'] | installState).validateProgress / 100"
|
[value]="(pkg['install-progress'] | installState).validateProgress / 100"
|
||||||
></ion-progress-bar>
|
></ion-progress-bar>
|
||||||
|
|
||||||
<p>Installing: {{ (pkg['install-progress'] | installState).unpackProgress }}%</p>
|
<p>Installing: {{ (pkg['install-progress'] | installState).unpackProgress }}%</p>
|
||||||
<ion-progress-bar
|
<ion-progress-bar
|
||||||
[color]="pkg['install-progress']['unpack-complete'] ? 'success' : 'secondary'"
|
[color]="pkg['install-progress']['unpack-complete'] ? 'success' : 'secondary'"
|
||||||
[value]="(pkg['install-progress'] | installState).unpackProgress / 100"
|
[value]="(pkg['install-progress'] | installState).unpackProgress / 100"
|
||||||
></ion-progress-bar>
|
></ion-progress-bar>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { wizardModal } from 'src/app/components/install-wizard/install-wizard.co
|
|||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, MainStatus, PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, MainStatus, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
@@ -31,6 +31,7 @@ export class AppShowPage {
|
|||||||
rendering: PkgStatusRendering
|
rendering: PkgStatusRendering
|
||||||
Math = Math
|
Math = Math
|
||||||
mainStatus: MainStatus
|
mainStatus: MainStatus
|
||||||
|
PackageMainStatus = PackageMainStatus
|
||||||
|
|
||||||
@ViewChild(IonContent) content: IonContent
|
@ViewChild(IonContent) content: IonContent
|
||||||
subs: Subscription[] = []
|
subs: Subscription[] = []
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar *ngIf="!pageLoading">
|
<ion-toolbar>
|
||||||
<ion-searchbar color="dark" (ionChange)="search($event)" debounce="400"></ion-searchbar>
|
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<badge-menu-button></badge-menu-button>
|
<badge-menu-button></badge-menu-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
@@ -8,11 +7,23 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<text-spinner *ngIf="pageLoading; else pageLoaded" text="Loading Marketplace"></text-spinner>
|
<h1 style="font-family: 'Montserrat'; font-weight: 100px; margin: 32px 0;" class="ion-text-center">Embassy Marketplace</h1>
|
||||||
|
|
||||||
|
<ion-searchbar color="dark" (ionChange)="search($event)" debounce="400" style="padding-bottom: 32px;"></ion-searchbar>
|
||||||
|
|
||||||
|
<!-- page loading -->
|
||||||
|
<ng-container *ngIf="pageLoading; else pageLoaded">
|
||||||
|
<div class="scrollable ion-text-center">
|
||||||
|
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
|
||||||
|
<ion-skeleton-text animated style="width: 80px; border-radius: 0;"></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider" style="margin: 24px 0;"></div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- page loaded -->
|
||||||
<ng-template #pageLoaded>
|
<ng-template #pageLoaded>
|
||||||
<h1 style="font-family: 'Montserrat'; font-weight: 100px; margin: 0 0 32px 0;" class="ion-text-center">Embassy Marketplace</h1>
|
|
||||||
|
|
||||||
<div class="scrollable ion-text-center">
|
<div class="scrollable ion-text-center">
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngFor="let cat of data.categories"
|
*ngFor="let cat of data.categories"
|
||||||
@@ -25,57 +36,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider" style="margin: 24px;"></div>
|
<div class="divider" style="margin: 24px;"></div>
|
||||||
|
|
||||||
<div *ngIf="pkgsLoading; else loaded" style="margin-top: 64px;" class="ion-text-center">
|
|
||||||
<text-spinner text="Loading Packages"></text-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #loaded>
|
|
||||||
<ion-grid>
|
|
||||||
<ion-row>
|
|
||||||
<ion-col *ngIf="eos && category === 'featured'" sizeXs="12" sizeSm="12" sizeMd="6">
|
|
||||||
<ion-item button class="eos-item" (click)="updateEos()">
|
|
||||||
<ion-thumbnail slot="start">
|
|
||||||
<img src="assets/img/icon.png" />
|
|
||||||
</ion-thumbnail>
|
|
||||||
<ion-label>
|
|
||||||
<h3>Now Available...</h3>
|
|
||||||
<h2>Embassy OS {{ eos.version }}</h2>
|
|
||||||
<p>{{ eos.headline }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-col>
|
|
||||||
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeSm="12" sizeMd="6">
|
|
||||||
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
|
|
||||||
<ion-thumbnail slot="start">
|
|
||||||
<img [src]="pkg.icon" />
|
|
||||||
</ion-thumbnail>
|
|
||||||
<ion-label>
|
|
||||||
<h2 style="font-family: 'Montserrat'; font-weight: bold;">{{ pkg.manifest.title }}</h2>
|
|
||||||
<h3>{{ pkg.manifest.description.short }}</h3>
|
|
||||||
<ng-container *ngIf="localPkgs[pkg.manifest.id] as localPkg; else none">
|
|
||||||
<p *ngIf="localPkg.state === PackageState.Installed">
|
|
||||||
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0" color="success">Installed</ion-text>
|
|
||||||
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
|
|
||||||
</p>
|
|
||||||
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
|
||||||
<ion-text color="primary">{{ localPkg.state | titlecase }}...{{ (localPkg['install-progress'] | installState).totalProgress }}%</ion-text>
|
|
||||||
</p>
|
|
||||||
<p *ngIf="localPkg.state === PackageState.Removing">
|
|
||||||
<ion-text color="warning">{{ localPkg.state | Removing }}</ion-text>
|
|
||||||
<ion-spinner name="dots" color="warning"></ion-spinner>
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #none>
|
|
||||||
<p>Not Installed</p>
|
|
||||||
</ng-template>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-grid>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- packages loading -->
|
||||||
|
<ng-container *ngIf="pkgsLoading; else pkgsLoaded">
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col *ngFor="let pkg of ['', '', '', '']" sizeXs="12" sizeSm="12" sizeMd="6">
|
||||||
|
<ion-item>
|
||||||
|
<ion-thumbnail slot="start">
|
||||||
|
<ion-skeleton-text style="border-radius: 100%;" animated></ion-skeleton-text>
|
||||||
|
</ion-thumbnail>
|
||||||
|
<ion-label>
|
||||||
|
<ion-skeleton-text animated style="width: 150px; height: 18px; margin-bottom: 8px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 400px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 400px;"></ion-skeleton-text>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- packages loaded -->
|
||||||
|
<ng-template #pkgsLoaded>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col *ngIf="eos && category === 'featured'" sizeXs="12" sizeSm="12" sizeMd="6">
|
||||||
|
<ion-item button class="eos-item" (click)="updateEos()">
|
||||||
|
<ion-thumbnail slot="start">
|
||||||
|
<img src="assets/img/icon.png" />
|
||||||
|
</ion-thumbnail>
|
||||||
|
<ion-label>
|
||||||
|
<h3>Now Available...</h3>
|
||||||
|
<h2>Embassy OS {{ eos.version }}</h2>
|
||||||
|
<p>{{ eos.headline }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeSm="12" sizeMd="6">
|
||||||
|
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||||
|
<ion-thumbnail slot="start">
|
||||||
|
<img [src]="pkg.icon" />
|
||||||
|
</ion-thumbnail>
|
||||||
|
<ion-label>
|
||||||
|
<h2 style="font-family: 'Montserrat'; font-weight: bold;">{{ pkg.manifest.title }}</h2>
|
||||||
|
<h3>{{ pkg.manifest.description.short }}</h3>
|
||||||
|
<ng-container *ngIf="localPkgs[pkg.manifest.id] as localPkg; else none">
|
||||||
|
<p *ngIf="localPkg.state === PackageState.Installed">
|
||||||
|
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0" color="success">Installed</ion-text>
|
||||||
|
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
||||||
|
<ion-text color="primary">{{ localPkg.state | titlecase }}...{{ (localPkg['install-progress'] | installState).totalProgress }}%</ion-text>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="localPkg.state === PackageState.Removing">
|
||||||
|
<ion-text color="warning">{{ localPkg.state | Removing }}</ion-text>
|
||||||
|
<ion-spinner name="dots" color="warning"></ion-spinner>
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #none>
|
||||||
|
<p>Not Installed</p>
|
||||||
|
</ng-template>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)">
|
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)">
|
||||||
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
||||||
</ion-infinite-scroll>
|
</ion-infinite-scroll>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
<!-- recommendation -->
|
<!-- recommendation -->
|
||||||
<ion-item *ngIf="rec && showRec" class="rec-item">
|
<ion-item *ngIf="rec && showRec" class="rec-item">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2 style="display: flex; align-items: center;">
|
<h2 style="display: flex; align-items: center;">
|
||||||
<ion-thumbnail style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
<ion-thumbnail style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
||||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||||
@@ -98,14 +98,14 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item lines="none" color="transparent">
|
<ion-item lines="none" color="transparent">
|
||||||
<ion-label class="ion-text-wrap" >
|
<ion-label>
|
||||||
<div id='release-notes' [innerHTML]="pkg.manifest['release-notes'] | markdown"></div>
|
<div id='release-notes' [innerHTML]="pkg.manifest['release-notes'] | markdown"></div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
<ion-item-divider>Description</ion-item-divider>
|
<ion-item-divider>Description</ion-item-divider>
|
||||||
<ion-item lines="none" color="transparent">
|
<ion-item lines="none" color="transparent">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<div id="release-notes" class="release-notes">{{ pkg.manifest.description.long }}</div>
|
<div id="release-notes" class="release-notes">{{ pkg.manifest.description.long }}</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<ion-thumbnail slot="start">
|
<ion-thumbnail slot="start">
|
||||||
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
|
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
|
||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>
|
<h2>
|
||||||
{{ pkg['dependency-metadata'][dep.key].title }}
|
{{ pkg['dependency-metadata'][dep.key].title }}
|
||||||
<span *ngIf="dep.value.recommended"> (recommended)</span>
|
<span *ngIf="dep.value.recommended"> (recommended)</span>
|
||||||
|
|||||||
@@ -11,14 +11,30 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
|
|
||||||
<text-spinner *ngIf="loading" text="Loading Notifications"></text-spinner>
|
<!-- loading -->
|
||||||
|
<ion-item-group *ngIf="loading">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-button slot="end" fill="clear">
|
||||||
|
<ion-skeleton-text style="width: 90px; height: 14px; border-radius: 0;" animated></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item *ngFor="let entry of ['', '', '', '']">
|
||||||
|
<ion-label>
|
||||||
|
<ion-skeleton-text animated style="width: 15%; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 50%; margin-bottom: 18px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 20%;"></ion-skeleton-text>
|
||||||
|
</ion-label>
|
||||||
|
<ion-button slot="end" fill="clear">
|
||||||
|
<ion-skeleton-text animated style="width: 20px; height: 20px; border-radius: 0"></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
</ion-item-group>
|
||||||
|
|
||||||
<!-- no notifications -->
|
<!-- not loading -->
|
||||||
<ng-container *ngIf="!loading">
|
<ng-container *ngIf="!loading">
|
||||||
|
|
||||||
|
<!-- no notifications -->
|
||||||
<ion-item-group *ngIf="!notifications.length">
|
<ion-item-group *ngIf="!notifications.length">
|
||||||
<div
|
<div
|
||||||
style="
|
style="
|
||||||
@@ -28,8 +44,8 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);"
|
transform: translate(-50%, -50%);"
|
||||||
>
|
>
|
||||||
<ion-icon style="font-size: 84px; color: #2c3038" name="mail-outline"></ion-icon>
|
<ion-icon style="font-size: 84px; color: #595959" name="mail-outline"></ion-icon>
|
||||||
<h4 style="color: #2c3038; margin-top: 0px">Inbox Empty</h4>
|
<h4 style="color: #595959; margin-top: 0px">Inbox Empty</h4>
|
||||||
</div>
|
</div>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
@@ -42,7 +58,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item *ngFor="let not of notifications; let i = index">
|
<ion-item *ngFor="let not of notifications; let i = index">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h2>
|
<h2>
|
||||||
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.notification-message {
|
.notification-message {
|
||||||
margin: 10px 0 12px 0;
|
margin: 6px 0 8px 0;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export class NotificationsPage {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh (e: any) {
|
async refresh (e: any) {
|
||||||
this.page = 1
|
this.page = 1
|
||||||
this.notifications = await this.getNotifications(),
|
this.notifications = await this.getNotifications(),
|
||||||
e.target.complete()
|
e.target.complete()
|
||||||
|
|||||||
@@ -11,25 +11,27 @@
|
|||||||
|
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<!-- about -->
|
<!-- about -->
|
||||||
<ion-item>
|
<ion-item class="ion-padding-bottom">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p class="ion-padding-bottom">About</p>
|
<h2>
|
||||||
<h2>You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.</h2>
|
Connecting to your Embassy over the Local Area Network (LAN) is great for achieving a faster experience, as well as a fallback in case Tor is experiencing issues.
|
||||||
|
<a [href]="docsUrl" target="_blank">View instructions</a>
|
||||||
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item [href]="docsUrl" target="_blank" detail="false">
|
|
||||||
<ion-icon slot="start" name="list-outline"></ion-icon>
|
|
||||||
<ion-label>View Instructions</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item button (click)="installCert()" [disabled]="lanDisabled">
|
<ion-item button (click)="installCert()" [disabled]="lanDisabled">
|
||||||
<ion-icon slot="start" name="download-outline"></ion-icon>
|
<ion-icon slot="start" name="download-outline" size="large"></ion-icon>
|
||||||
<ion-label>Download Root Certificate Authority</ion-label>
|
<ion-label>
|
||||||
|
<h1>Download Root CA</h1>
|
||||||
|
<p>Download and trust your Embassy's Root Certificate Authority to achieve a secure connection on the LAN.</p>
|
||||||
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="lanDisabled">
|
<ng-container *ngIf="lanDisabled">
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p class="ion-padding-bottom">Setup</p>
|
<p class="ion-padding-bottom">Setup</p>
|
||||||
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
|
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
@@ -37,16 +39,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Refresh Network -->
|
<!-- Refresh Network -->
|
||||||
<ion-item-divider></ion-item-divider>
|
|
||||||
<ion-item>
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
<p style="padding-bottom: 6px;">Troubleshooting</p>
|
|
||||||
<h2>If you are having issues connecting to your Embassy over LAN, try refreshing your LAN services by clicking the button below.</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item button (click)="refreshLAN()" detail="false">
|
<ion-item button (click)="refreshLAN()" detail="false">
|
||||||
<ion-icon slot="start" name="refresh-outline"></ion-icon>
|
<ion-icon slot="start" name="refresh-outline" size="large"></ion-icon>
|
||||||
<ion-label>Refresh LAN</ion-label>
|
<ion-label>
|
||||||
|
<h1>Refresh LAN</h1>
|
||||||
|
<p>If you are having issues connecting to your Embassy over LAN, try refreshing your LAN services by clicking the button below.</p>
|
||||||
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,37 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<text-spinner *ngIf="loading" text="Loading Sessions"></text-spinner>
|
|
||||||
|
|
||||||
|
<!-- loading -->
|
||||||
|
<ion-item-group *ngIf="loading">
|
||||||
|
<div *ngFor="let entry of ['first', 'second']">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-skeleton-text animated style="width: 120px; height: 20px;"></ion-skeleton-text>
|
||||||
|
</ion-item-divider>
|
||||||
|
|
||||||
|
<ion-item style="padding-bottom: 6px;">
|
||||||
|
<ion-avatar slot="start" style="margin-right: 30px;">
|
||||||
|
<ion-skeleton-text animated style="width: 40px; height: 40px; border-radius: 0;"></ion-skeleton-text>
|
||||||
|
</ion-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<ion-skeleton-text animated style="width: 150px; height: 20px; margin-bottom: 10px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 250px; height: 14px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 350px;"></ion-skeleton-text>
|
||||||
|
</ion-label>
|
||||||
|
<ion-button *ngIf="entry === 'second'" slot="end" fill="clear">
|
||||||
|
<ion-skeleton-text animated style="width: 60px; border-radius: 0"></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
</ion-item-group>
|
||||||
|
|
||||||
|
<!-- not loading -->
|
||||||
<ion-item-group *ngIf="!loading">
|
<ion-item-group *ngIf="!loading">
|
||||||
|
|
||||||
<ion-item-divider>Current Session</ion-item-divider>
|
<ion-item-divider>This Session</ion-item-divider>
|
||||||
<ion-item *ngIf="sessionInfo.sessions[sessionInfo.current] as current">
|
<ion-item *ngIf="sessionInfo.sessions[sessionInfo.current] as current">
|
||||||
<ion-icon slot="start" [name]="getPlatformIcon(current.metadata.platforms)"></ion-icon>
|
<ion-icon slot="start" size="large" [name]="getPlatformIcon(current.metadata.platforms)"></ion-icon>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h1>{{ getPlatformName(current.metadata.platforms) }}</h1>
|
<h1>{{ getPlatformName(current.metadata.platforms) }}</h1>
|
||||||
<h2>Last Active: {{ current['last-active'] | date : 'medium' }}</h2>
|
<h2>Last Active: {{ current['last-active'] | date : 'medium' }}</h2>
|
||||||
<p>{{ current['user-agent'] }}</p>
|
<p>{{ current['user-agent'] }}</p>
|
||||||
@@ -28,8 +51,8 @@
|
|||||||
[id]="session.key"
|
[id]="session.key"
|
||||||
*ngIf="session.key !== sessionInfo.current"
|
*ngIf="session.key !== sessionInfo.current"
|
||||||
>
|
>
|
||||||
<ion-icon slot="start" [name]="getPlatformIcon(session.value.metadata.platforms)"></ion-icon>
|
<ion-icon slot="start" size="large" [name]="getPlatformIcon(session.value.metadata.platforms)"></ion-icon>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<h1>{{ getPlatformName(session.value.metadata.platforms) }}</h1>
|
<h1>{{ getPlatformName(session.value.metadata.platforms) }}</h1>
|
||||||
<h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2>
|
<h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2>
|
||||||
<p>{{ session.value['user-agent'] }}</p>
|
<p>{{ session.value['user-agent'] }}</p>
|
||||||
|
|||||||
@@ -4,38 +4,62 @@
|
|||||||
<pwa-back-button></pwa-back-button>
|
<pwa-back-button></pwa-back-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>SSH Keys</ion-title>
|
<ion-title>SSH Keys</ion-title>
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button (click)="serverConfig.presentAlert('ssh')">
|
|
||||||
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<text-spinner *ngIf="loading" text="Loading Keys"></text-spinner>
|
|
||||||
|
|
||||||
<ion-item-group *ngIf="!loading">
|
<!-- always -->
|
||||||
<!-- about -->
|
<ion-item-group>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p class="ion-padding-bottom">About</p>
|
<h2>
|
||||||
<h2>Adding an SSH key to your Embassy can be useful for advanced usage from the command line, as well as for debugging purposes.</h2>
|
Adding SSH keys to your Embassy is useful for command line access, as well as for debugging purposes.
|
||||||
|
<a [href]="docsUrl" target="_blank">View instructions</a>
|
||||||
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item [href]="docsUrl" target="_blank" detail="false">
|
|
||||||
<ion-icon slot="start" name="list-outline"></ion-icon>
|
|
||||||
<ion-label>View Instructions</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item-divider>Saved Keys</ion-item-divider>
|
<ion-item-divider>Saved Keys</ion-item-divider>
|
||||||
<ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
|
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-item button detail="false" (click)="serverConfig.presentInputModal('ssh')">
|
||||||
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
|
<ion-icon slot="start" name="add" size="large"></ion-icon>
|
||||||
</ion-label>
|
<ion-label>Add new key</ion-label>
|
||||||
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(ssh.key)">
|
|
||||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- loading -->
|
||||||
|
<ng-container *ngIf="loading">
|
||||||
|
<ion-item *ngFor="let entry of ['', '']">
|
||||||
|
<ion-button slot="start" fill="clear">
|
||||||
|
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
<ion-label>
|
||||||
|
<ion-skeleton-text animated style="width: 15%; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 50%; margin-bottom: 18px;"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text animated style="width: 20%;"></ion-skeleton-text>
|
||||||
|
</ion-label>
|
||||||
|
<ion-button slot="end" fill="clear">
|
||||||
|
<ion-skeleton-text animated style="width: 80px; border-radius: 0"></ion-skeleton-text>
|
||||||
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- not loading -->
|
||||||
|
<ng-container *ngIf="!loading">
|
||||||
|
<ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
|
||||||
|
<ion-icon slot="start" name="key-outline" size="large"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h1>{{ ssh.value.hostname }}</h1>
|
||||||
|
<h2>{{ ssh.value['created-at'] | date: 'short' }}</h2>
|
||||||
|
<p>{{ ssh.value.alg }} {{ ssh.key }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-button slot="end" fill="clear" color="danger" (click)="presentAlertDelete(ssh.key)">
|
||||||
|
<ion-icon slot="start" name="close"></ion-icon>
|
||||||
|
Remove
|
||||||
|
</ion-button>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
@@ -4,11 +4,6 @@
|
|||||||
<pwa-back-button></pwa-back-button>
|
<pwa-back-button></pwa-back-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>Create Backup</ion-title>
|
<ion-title>Create Backup</ion-title>
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button (click)="doRefresh()">
|
|
||||||
<ion-icon slot="icon-only" name="refresh-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
@@ -18,45 +13,33 @@
|
|||||||
<ng-template #loaded>
|
<ng-template #loaded>
|
||||||
|
|
||||||
<ion-item class="ion-margin-bottom">
|
<ion-item class="ion-margin-bottom">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p class="ion-padding-bottom">About</p>
|
|
||||||
<h2>
|
<h2>
|
||||||
Create frequent backups of your Embassy to avoid loss of data.
|
Select the drive where you want to create a backup of your Embassy, including all your installed services.
|
||||||
</h2>
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Select Backup Drive</ion-item-divider>
|
|
||||||
|
|
||||||
<ion-item *ngIf="allPartitionsMounted">
|
<ion-item *ngIf="allPartitionsMounted">
|
||||||
<ion-text *ngIf="type === 'create'" class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
|
<ion-text class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
|
||||||
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-card *ngFor="let disk of disks | keyvalue">
|
<ion-item-group>
|
||||||
<ion-card-header>
|
<div *ngFor="let disk of disks | keyvalue">
|
||||||
<ion-card-title>
|
<ion-item-divider>{{ disk.key }} - {{ disk.value.size }}</ion-item-divider>
|
||||||
{{ disk.value.size }}
|
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
|
||||||
</ion-card-title>
|
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||||
<ion-card-subtitle>
|
<ion-label>
|
||||||
{{ disk.key }}
|
<h1>{{ partition.value.label || partition.key }}</h1>
|
||||||
</ion-card-subtitle>
|
<h2>{{ partition.value.size || 'unknown size' }}</h2>
|
||||||
</ion-card-header>
|
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
||||||
<ion-card-content>
|
<ng-template #unavailable>
|
||||||
<ion-item-group>
|
<p><ion-text color="danger">Unavailable</ion-text></p>
|
||||||
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
|
</ng-template>
|
||||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
</ion-label>
|
||||||
<ion-label>
|
</ion-item>
|
||||||
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
|
</div>
|
||||||
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
</ion-item-group>
|
||||||
<ng-template #unavailable>
|
|
||||||
<p><ion-text color="danger">Unavailable</ion-text></p>
|
|
||||||
</ng-template>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-card>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { LoadingController, ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
||||||
import { DiskInfo } from 'src/app/services/api/api.types'
|
import { DiskInfo } from 'src/app/services/api/api.types'
|
||||||
@@ -19,7 +19,6 @@ export class ServerBackupPage {
|
|||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly loadingCtrl: LoadingController,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
@@ -45,36 +44,21 @@ export class ServerBackupPage {
|
|||||||
async presentModal (logicalname: string): Promise<void> {
|
async presentModal (logicalname: string): Promise<void> {
|
||||||
const m = await this.modalCtrl.create({
|
const m = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
type: 'backup',
|
title: 'Create Backup',
|
||||||
|
message: `Enter your master password to create an encrypted backup of your Embassy and all its installed services.`,
|
||||||
|
label: 'Password',
|
||||||
|
useMask: true,
|
||||||
|
buttonText: 'Create Backup',
|
||||||
|
submitFn: async (value: string) => await this.create(logicalname, value),
|
||||||
},
|
},
|
||||||
cssClass: 'alertlike-modal',
|
cssClass: 'alertlike-modal',
|
||||||
component: BackupConfirmationComponent,
|
component: BackupConfirmationComponent,
|
||||||
backdropDismiss: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
m.onWillDismiss().then(res => {
|
|
||||||
const data = res.data
|
|
||||||
if (data.cancel) return
|
|
||||||
this.create(logicalname, data.password)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return await m.present()
|
return await m.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async create (logicalname: string, password: string): Promise<void> {
|
private async create (logicalname: string, password: string): Promise<void> {
|
||||||
const loader = await this.loadingCtrl.create({
|
await this.embassyApi.createBackup({ logicalname, password })
|
||||||
spinner: 'lines',
|
|
||||||
message: 'Starting backup...',
|
|
||||||
cssClass: 'loader',
|
|
||||||
})
|
|
||||||
await loader.present()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.embassyApi.createBackup({ logicalname, password })
|
|
||||||
} catch (e) {
|
|
||||||
this.errToast.present(e)
|
|
||||||
} finally {
|
|
||||||
loader.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<skeleton-list *ngIf="loading; else loaded" groups="3"></skeleton-list>
|
<skeleton-list *ngIf="loading; else loaded" groups="2"></skeleton-list>
|
||||||
|
|
||||||
<ng-template #loaded>
|
<ng-template #loaded>
|
||||||
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
|
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Specs</ion-item-divider>
|
<ion-item-divider>Hardware Specs</ion-item-divider>
|
||||||
|
|
||||||
<ion-item *ngFor="let spec of server.specs | keyvalue : asIsOrder">
|
<ion-item *ngFor="let spec of server.specs | keyvalue : asIsOrder">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label>
|
||||||
<p style="padding-bottom: 6px;">About</p>
|
<p style="padding-bottom: 6px;">About</p>
|
||||||
<h2>Embassy will automatically connect to saved WiFi networks when they are available, allowing you to remove the Ethernet cable.</h2>
|
<h2>Embassy will automatically connect to saved WiFi networks when they are available, allowing you to remove the Ethernet cable.</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { PackageDataEntry } from '../services/patch-db/data-model'
|
import { InterfaceDef, PackageMainStatus, PackageState } from '../services/patch-db/data-model'
|
||||||
import { ConfigService, hasUi } from '../services/config.service'
|
import { ConfigService, hasUi } from '../services/config.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
@@ -7,8 +7,7 @@ import { ConfigService, hasUi } from '../services/config.service'
|
|||||||
})
|
})
|
||||||
export class HasUiPipe implements PipeTransform {
|
export class HasUiPipe implements PipeTransform {
|
||||||
|
|
||||||
transform (pkg: PackageDataEntry): boolean {
|
transform (interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||||
const interfaces = pkg.manifest.interfaces
|
|
||||||
return hasUi(interfaces)
|
return hasUi(interfaces)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +19,7 @@ export class LaunchablePipe implements PipeTransform {
|
|||||||
|
|
||||||
constructor (private configService: ConfigService) { }
|
constructor (private configService: ConfigService) { }
|
||||||
|
|
||||||
transform (pkg: PackageDataEntry): boolean {
|
transform (state: PackageState, status: PackageMainStatus, interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||||
return this.configService.isLaunchable(pkg)
|
return this.configService.isLaunchable(state, status, interfaces)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,11 +832,13 @@ export module Mock {
|
|||||||
|
|
||||||
export const SshKeys: RR.GetSSHKeysRes = {
|
export const SshKeys: RR.GetSSHKeysRes = {
|
||||||
'28:d2:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53': {
|
'28:d2:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53': {
|
||||||
|
'created-at': new Date().toISOString(),
|
||||||
alg: 'ed25519',
|
alg: 'ed25519',
|
||||||
hostname: 'Matt Key',
|
hostname: 'Matt Key',
|
||||||
hash: 'VeryLongHashOfSSHKey1',
|
hash: 'VeryLongHashOfSSHKey1',
|
||||||
},
|
},
|
||||||
'12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53': {
|
'12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53': {
|
||||||
|
'created-at': new Date().toISOString(),
|
||||||
alg: 'ed25519',
|
alg: 'ed25519',
|
||||||
hostname: 'Aiden Key',
|
hostname: 'Aiden Key',
|
||||||
hash: 'VeryLongHashOfSSHKey2',
|
hash: 'VeryLongHashOfSSHKey2',
|
||||||
@@ -845,6 +847,7 @@ export module Mock {
|
|||||||
|
|
||||||
export const SshKey: RR.AddSSHKeyRes = {
|
export const SshKey: RR.AddSSHKeyRes = {
|
||||||
'44:44:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53': {
|
'44:44:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53': {
|
||||||
|
'created-at': new Date().toISOString(),
|
||||||
alg: 'ed25519',
|
alg: 'ed25519',
|
||||||
hostname: 'Lucy Key',
|
hostname: 'Lucy Key',
|
||||||
hash: 'VeryLongHashOfSSHKey3',
|
hash: 'VeryLongHashOfSSHKey3',
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ export interface SSHKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHKeyEntry {
|
export interface SSHKeyEntry {
|
||||||
|
'created-at': string
|
||||||
alg: string
|
alg: string
|
||||||
hostname: string
|
hostname: string
|
||||||
hash: string
|
hash: string
|
||||||
|
|||||||
@@ -49,15 +49,15 @@ export class ConfigService {
|
|||||||
return this.isConsulate || (mocks.enabled && mocks.connection === 'poll')
|
return this.isConsulate || (mocks.enabled && mocks.connection === 'poll')
|
||||||
}
|
}
|
||||||
|
|
||||||
isLaunchable (pkg: PackageDataEntry): boolean {
|
isLaunchable (state: PackageState, status: PackageMainStatus, interfaces: { [id: string]: InterfaceDef }): boolean {
|
||||||
if (this.isConsulate || pkg.state !== PackageState.Installed) {
|
if (this.isConsulate || state !== PackageState.Installed) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkg.installed.status.main.status === PackageMainStatus.Running &&
|
return status === PackageMainStatus.Running &&
|
||||||
(
|
(
|
||||||
(hasTorUi(pkg.manifest.interfaces) && this.isTor()) ||
|
(hasTorUi(interfaces) && this.isTor()) ||
|
||||||
(hasLanUi(pkg.manifest.interfaces) && !this.isTor())
|
(hasLanUi(interfaces) && !this.isTor())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
|
import * as emver from '@start9labs/emver'
|
||||||
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class Emver {
|
export class Emver {
|
||||||
private e: typeof import('@start9labs/emver')
|
|
||||||
constructor () { }
|
constructor () { }
|
||||||
|
|
||||||
async init () {
|
|
||||||
this.e = await import('@start9labs/emver')
|
|
||||||
}
|
|
||||||
|
|
||||||
compare (lhs: string, rhs: string): number {
|
compare (lhs: string, rhs: string): number {
|
||||||
console.log('EMVER', this.e)
|
console.log('EMVER', emver)
|
||||||
const compare = this.e.compare(lhs, rhs)
|
const compare = emver.compare(lhs, rhs)
|
||||||
console.log('COMPARE', compare)
|
console.log('COMPARE', compare)
|
||||||
return compare
|
return compare
|
||||||
}
|
}
|
||||||
|
|
||||||
satisfies (version: string, range: string): boolean {
|
satisfies (version: string, range: string): boolean {
|
||||||
return this.e.satisfies(version, range)
|
return emver.satisfies(version, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,24 +17,9 @@ export class ErrorToastService {
|
|||||||
|
|
||||||
if (this.toast) return
|
if (this.toast) return
|
||||||
|
|
||||||
let message: string | IonicSafeString
|
|
||||||
|
|
||||||
if (e.code) message = String(e.code)
|
|
||||||
if (e.message) message = `${message ? message + ' ' : ''}${e.message}`
|
|
||||||
if (e.details) message = `${message ? message + ': ' : ''}${e.details}`
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
message = 'Unknown Error.'
|
|
||||||
link = 'https://docs.start9.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
message = new IonicSafeString(`${message}<br /><br /><a href=${link} target="_blank" style="color: white;">Get Help</a>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toast = await this.toastCtrl.create({
|
this.toast = await this.toastCtrl.create({
|
||||||
header: 'Error',
|
header: 'Error',
|
||||||
message,
|
message: getErrorMessage(e, link),
|
||||||
duration: 0,
|
duration: 0,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
cssClass: 'error-toast',
|
cssClass: 'error-toast',
|
||||||
@@ -58,3 +43,22 @@ export class ErrorToastService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getErrorMessage (e: RequestError, link?: string): string | IonicSafeString {
|
||||||
|
let message: string | IonicSafeString
|
||||||
|
|
||||||
|
if (e.code) message = String(e.code)
|
||||||
|
if (e.message) message = `${message ? message + ' ' : ''}${e.message}`
|
||||||
|
if (e.details) message = `${message ? message + ': ' : ''}${e.details}`
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
message = 'Unknown Error.'
|
||||||
|
link = 'https://docs.start9.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
message = new IonicSafeString(`${message}<br /><br /><a href=${link} target="_blank" style="color: white;">Get Help</a>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { AlertInput, AlertButton } from '@ionic/core'
|
import { AlertInput, AlertButton } from '@ionic/core'
|
||||||
// import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page'
|
|
||||||
import { ApiService } from './api/embassy-api.service'
|
import { ApiService } from './api/embassy-api.service'
|
||||||
import { ConfigSpec } from '../pkg-config/config-types'
|
import { ConfigSpec, ValueSpecString } from '../pkg-config/config-types'
|
||||||
import { SSHService } from '../pages/server-routes/security-routes/ssh-keys/ssh.service'
|
import { SSHService } from '../pages/server-routes/security-routes/ssh-keys/ssh.service'
|
||||||
import { AlertController, LoadingController } from '@ionic/angular'
|
import { AlertController, LoadingController } from '@ionic/angular'
|
||||||
import { ErrorToastService } from './error-toast.service'
|
import { ErrorToastService } from './error-toast.service'
|
||||||
// import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
|
import { BackupConfirmationComponent } from '../modals/backup-confirmation/backup-confirmation.component'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -14,7 +14,7 @@ import { ErrorToastService } from './error-toast.service'
|
|||||||
export class ServerConfigService {
|
export class ServerConfigService {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
// private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
@@ -71,16 +71,8 @@ export class ServerConfigService {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
break
|
break
|
||||||
case 'string':
|
default:
|
||||||
inputs = [
|
return
|
||||||
{
|
|
||||||
name: key,
|
|
||||||
type: 'textarea',
|
|
||||||
placeholder: 'Enter SSH public key',
|
|
||||||
value: current,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
@@ -92,6 +84,24 @@ export class ServerConfigService {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async presentInputModal (key: string, current?: string) {
|
||||||
|
const { name, description, masked } = serverConfig[key] as ValueSpecString
|
||||||
|
|
||||||
|
const modal = await this.modalCtrl.create({
|
||||||
|
component: BackupConfirmationComponent,
|
||||||
|
componentProps: {
|
||||||
|
title: name,
|
||||||
|
message: description,
|
||||||
|
label: name,
|
||||||
|
useMask: masked,
|
||||||
|
value: current,
|
||||||
|
submitFn: this.saveFns[key],
|
||||||
|
},
|
||||||
|
cssClass: 'alertlike-modal',
|
||||||
|
})
|
||||||
|
await modal.present()
|
||||||
|
}
|
||||||
|
|
||||||
// async presentModalForm (key: string, current?: string) {
|
// async presentModalForm (key: string, current?: string) {
|
||||||
// const modal = await this.modalCtrl.create({
|
// const modal = await this.modalCtrl.create({
|
||||||
// component: AppConfigValuePage,
|
// component: AppConfigValuePage,
|
||||||
@@ -105,7 +115,6 @@ export class ServerConfigService {
|
|||||||
|
|
||||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||||
'auto-check-updates': async (enabled: boolean) => {
|
'auto-check-updates': async (enabled: boolean) => {
|
||||||
console.log('SAVING auto check', enabled)
|
|
||||||
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
|
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
|
||||||
},
|
},
|
||||||
ssh: async (pubkey: string) => {
|
ssh: async (pubkey: string) => {
|
||||||
@@ -136,7 +145,7 @@ export const serverConfig: ConfigSpec = {
|
|||||||
ssh: {
|
ssh: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'SSH Key',
|
name: 'SSH Key',
|
||||||
description: 'Enter an SSH public key to authorize root access from the command line.',
|
description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
// @TODO regex for SSH Key
|
// @TODO regex for SSH Key
|
||||||
// pattern: '',
|
// pattern: '',
|
||||||
|
|||||||
@@ -43,11 +43,18 @@ $subheader-height: 48px;
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.input-label {
|
||||||
--spinner-color: var(--ion-color-warning) !important;
|
// padding-top: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: 500;
|
||||||
|
* {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader-ontop-of-all {
|
.loader {
|
||||||
--spinner-color: var(--ion-color-warning) !important;
|
--spinner-color: var(--ion-color-warning) !important;
|
||||||
z-index: 40000 !important;
|
z-index: 40000 !important;
|
||||||
}
|
}
|
||||||
@@ -142,13 +149,6 @@ ion-button {
|
|||||||
--color: var(--ion-color-dark) !important;
|
--color: var(--ion-color-dark) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
|
||||||
-webkit-user-select: text;
|
|
||||||
-moz-user-select: text;
|
|
||||||
-ms-user-select: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-ellipses {
|
.text-ellipses {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -183,7 +183,7 @@ ion-button {
|
|||||||
|
|
||||||
.alertlike-modal {
|
.alertlike-modal {
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
height: 50% !important;
|
max-height: 380px !important;
|
||||||
top: 25% !important;
|
top: 25% !important;
|
||||||
width: 90% !important;
|
width: 90% !important;
|
||||||
left: 5% !important;
|
left: 5% !important;
|
||||||
@@ -194,8 +194,8 @@ ion-button {
|
|||||||
@media (min-width:1000px) {
|
@media (min-width:1000px) {
|
||||||
.alertlike-modal {
|
.alertlike-modal {
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
width: 50% !important;
|
width: 40% !important;
|
||||||
left: 25% !important;
|
left: 30% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +241,10 @@ ion-item {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-label {
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
ion-loading {
|
ion-loading {
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user