start9 marketplace config

shift not unshift

move eos updates to embassy tab

selected id sub

roughly working

keep name in sync in case of change

delete commented code

64 img
This commit is contained in:
Drew Ansbacher
2022-02-01 13:21:50 -07:00
committed by Aiden McClelland
parent 2d4ecd3096
commit 0c0cd9d0a0
28 changed files with 1787 additions and 1619 deletions

View File

@@ -18,7 +18,7 @@
"index": "projects/ui/src/index.html",
"main": "projects/ui/src/main.ts",
"polyfills": "projects/ui/src/polyfills.ts",
"tsConfig": "projects/ui/tsconfig.app.json",
"tsConfig": "projects/ui/tsconfig.json",
"inlineStyleLanguage": "scss",
"assets": [
{
@@ -142,7 +142,7 @@
"index": "projects/setup-wizard/src/index.html",
"main": "projects/setup-wizard/src/main.ts",
"polyfills": "projects/setup-wizard/src/polyfills.ts",
"tsConfig": "projects/setup-wizard/tsconfig.app.json",
"tsConfig": "projects/setup-wizard/tsconfig.json",
"inlineStyleLanguage": "scss",
"assets": [
{
@@ -261,7 +261,7 @@
"index": "projects/diagnostic-ui/src/index.html",
"main": "projects/diagnostic-ui/src/main.ts",
"polyfills": "projects/diagnostic-ui/src/polyfills.ts",
"tsConfig": "projects/diagnostic-ui/tsconfig.app.json",
"tsConfig": "projects/diagnostic-ui/tsconfig.json",
"inlineStyleLanguage": "scss",
"assets": [
{

View File

@@ -14,6 +14,7 @@
"mocks": {
"maskAs": "tor",
"skipStartupAlerts": true
}
},
"eosMarketplaceURL": "https://beta-registry-0-3.start9labs.com "
}
}

View File

@@ -7,9 +7,9 @@
"ng": "ng",
"check": "npm run check:shared && npm run check:ui && npm run check:setup-wizard && npm run check:diagnostic-ui",
"check:shared": "tsc --project projects/shared/tsconfig.lib.json --noEmit --skipLibCheck",
"check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.app.json --noEmit --skipLibCheck",
"check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.app.json --noEmit --skipLibCheck",
"check:ui": "tsc --project projects/ui/tsconfig.app.json --noEmit --skipLibCheck",
"check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck",
"check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
"build:deps": "cd ../patch-db/client && npm install && npm run build",
"build:diagnostic-ui": "ng run diagnostic-ui:build",
"build:setup-wizard": "ng run setup-wizard:build",

View File

@@ -16,5 +16,6 @@ export type WorkspaceConfig = {
maskAs: 'tor' | 'lan'
skipStartupAlerts: boolean
}
eosMarketplaceURL: string
}
}

View File

@@ -45,11 +45,17 @@
>
{{ page.title }}
</ion-label>
<ng-container *ngIf="page.url === '/embassy'">
<ion-icon
*ngIf="eosService.updateAvailable$ | async"
color="success"
name="repeat"
></ion-icon>
</ng-container>
<ion-badge
*ngIf="page.url === '/notifications' && unreadCount"
color="danger"
style="margin-right: 3%"
[class.selected-badge]="selectedIndex == i"
>{{ unreadCount }}</ion-badge
>
</ion-item>
@@ -117,6 +123,7 @@
<ion-icon name="cloud-offline-outline"></ion-icon>
<ion-icon name="cloud-upload-outline"></ion-icon>
<ion-icon name="code-outline"></ion-icon>
<ion-icon name="cog-outline"></ion-icon>
<ion-icon name="color-wand-outline"></ion-icon>
<ion-icon name="construct-outline"></ion-icon>
<ion-icon name="copy-outline"></ion-icon>
@@ -155,6 +162,7 @@
<ion-icon name="reload"></ion-icon>
<ion-icon name="remove"></ion-icon>
<ion-icon name="remove-circle-outline"></ion-icon>
<ion-icon name="repeat"></ion-icon>
<ion-icon name="save-outline"></ion-icon>
<ion-icon name="shield-checkmark-outline"></ion-icon>
<ion-icon name="storefront-outline"></ion-icon>

View File

@@ -19,7 +19,11 @@ import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { ToastButton } from '@ionic/core'
import { PatchDbService } from './services/patch-db/patch-db.service'
import { ServerStatus } from './services/patch-db/data-model'
import {
ServerStatus,
UIData,
UIMarketplaceData,
} from './services/patch-db/data-model'
import {
ConnectionFailure,
ConnectionService,
@@ -30,6 +34,8 @@ import { debounce, isEmptyObject } from './util/misc.util'
import { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs'
import { LocalStorageService } from './services/local-storage.service'
import { EOSService } from './services/eos.service'
import { v4 } from 'uuid'
@Component({
selector: 'app-root',
@@ -39,7 +45,7 @@ import { LocalStorageService } from './services/local-storage.service'
export class AppComponent {
@HostListener('document:keydown.enter', ['$event'])
@debounce()
handleKeyboardEvent() {
handleKeyboardEvent () {
const elems = document.getElementsByClassName('enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement
if (!elem || elem.classList.contains('no-click') || elem.disabled) return
@@ -84,7 +90,7 @@ export class AppComponent {
},
]
constructor(
constructor (
private readonly storage: Storage,
private readonly authService: AuthService,
private readonly router: Router,
@@ -101,11 +107,12 @@ export class AppComponent {
public readonly splitPane: SplitPaneTracker,
public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
public readonly eosService: EOSService,
) {
this.init()
}
async init() {
async init () {
await this.storage.create()
await this.authService.init()
await this.localStorageService.init()
@@ -140,7 +147,12 @@ export class AppComponent {
filter(obj => !isEmptyObject(obj)),
take(1),
)
.subscribe(_ => {
.subscribe(data => {
// check for updates to EOS
this.checkForEosUpdate(data.ui)
// seed EOS marketplace as default for services too
this.seedMarketplace(data.ui.marketplace)
this.subscriptions = this.subscriptions.concat([
// watch status to present toast for updated state
this.watchStatus(),
@@ -172,7 +184,7 @@ export class AppComponent {
})
}
async goToWebsite(): Promise<void> {
async goToWebsite (): Promise<void> {
let url: string
if (this.config.isTor()) {
url =
@@ -183,7 +195,7 @@ export class AppComponent {
window.open(url, '_blank', 'noreferrer')
}
async presentAlertLogout() {
async presentAlertLogout () {
const alert = await this.alertCtrl.create({
header: 'Caution',
message:
@@ -206,13 +218,39 @@ export class AppComponent {
await alert.present()
}
private async checkForEosUpdate (ui: UIData): Promise<void> {
if (ui['auto-check-updates']) {
await this.eosService.getEOS()
}
}
private async seedMarketplace(marketplace: UIMarketplaceData): Promise<void> {
if (
!marketplace ||
!marketplace['known-hosts'] ||
!marketplace['selected-id']
) {
const uuid = v4()
const value: UIMarketplaceData = {
'selected-id': uuid,
'known-hosts': {
[uuid]: {
url: this.config.eosMarketplaceUrl,
name: 'Start9 Embassy Marketplace',
},
},
}
await this.embassyApi.setDbValue({ pointer: '/marketplace', value })
}
}
// should wipe cache independant of actual BE logout
private async logout() {
private async logout () {
this.embassyApi.logout({})
this.authService.setUnverified()
}
private watchConnection(): Subscription {
private watchConnection (): Subscription {
return this.connectionService
.watchFailure$()
.pipe(distinctUntilChanged(), debounceTime(500))
@@ -245,7 +283,7 @@ export class AppComponent {
})
}
private watchRouter(): Subscription {
private watchRouter (): Subscription {
return this.router.events
.pipe(filter((e: RoutesRecognized) => !!e.urlAfterRedirects))
.subscribe(e => {
@@ -256,7 +294,7 @@ export class AppComponent {
})
}
private watchStatus(): Subscription {
private watchStatus (): Subscription {
return this.patch.watch$('server-info', 'status').subscribe(status => {
if (status === ServerStatus.Updated && !this.updateToast) {
this.presentToastUpdated()
@@ -264,7 +302,7 @@ export class AppComponent {
})
}
private watchUpdateProgress(): Subscription {
private watchUpdateProgress (): Subscription {
return this.patch
.watch$('server-info', 'update-progress')
.subscribe(progress => {
@@ -272,7 +310,7 @@ export class AppComponent {
})
}
private watchVersion(): Subscription {
private watchVersion (): Subscription {
return this.patch.watch$('server-info', 'version').subscribe(version => {
if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded()
@@ -280,7 +318,7 @@ export class AppComponent {
})
}
private watchNotifications(): Subscription {
private watchNotifications (): Subscription {
let previous: number
return this.patch
.watch$('server-info', 'unread-notification-count')
@@ -292,7 +330,7 @@ export class AppComponent {
})
}
private async presentAlertRefreshNeeded() {
private async presentAlertRefreshNeeded () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Refresh Needed',
@@ -311,7 +349,7 @@ export class AppComponent {
await alert.present()
}
private async presentToastUpdated() {
private async presentToastUpdated () {
if (this.updateToast) return
this.updateToast = await this.toastCtrl.create({
@@ -341,7 +379,7 @@ export class AppComponent {
await this.updateToast.present()
}
private async presentToastNotifications() {
private async presentToastNotifications () {
if (this.notificationToast) return
this.notificationToast = await this.toastCtrl.create({
@@ -371,7 +409,7 @@ export class AppComponent {
await this.notificationToast.present()
}
private async presentToastOffline(
private async presentToastOffline (
message: string | IonicSafeString,
link?: string,
) {
@@ -412,7 +450,7 @@ export class AppComponent {
await this.offlineToast.present()
}
private async restart(): Promise<void> {
private async restart (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Restarting...',
@@ -429,7 +467,7 @@ export class AppComponent {
}
}
splitPaneVisible(e: any) {
splitPaneVisible (e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}

View File

@@ -15,9 +15,14 @@
<!-- not loading -->
<ng-template #data>
<h1 style="font-family: 'Montserrat'; font-size: 42px; margin: 32px 0;" class="ion-text-center">Embassy Marketplace</h1>
<h1
style="font-family: 'Montserrat'; font-size: 42px; margin: 32px 0"
class="ion-text-center"
>
Embassy Marketplace
</h1>
<ion-grid style="padding-bottom: 32px;">
<ion-grid style="padding-bottom: 32px">
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
@@ -36,12 +41,18 @@
<!-- loading -->
<ng-container *ngIf="loading; 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
*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>
<div class="divider" style="margin: 24px 0"></div>
</ng-container>
<!-- loaded -->
@@ -57,22 +68,39 @@
</ion-button>
</div>
<div class="divider" style="margin: 24px;"></div>
<div class="divider" style="margin: 24px"></div>
</ng-template>
<!-- loading -->
<ng-container *ngIf="loading; else pkgsLoaded">
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of ['', '', '', '']" sizeXs="12" sizeSm="12" sizeMd="6">
<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-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: 100px;"></ion-skeleton-text>
<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: 100px"
></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
@@ -85,39 +113,46 @@
<div
class="ion-padding"
*ngIf="!pkgs.length && category ==='updates'"
style="text-align: center;"
style="text-align: center"
>
<h1>All services are up to date!</h1>
</div>
<ion-grid>
<ion-row>
<ion-col *ngIf="marketplaceService.eosUpdateAvailable && 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 {{ marketplaceService.eos.version }}</h2>
<p>{{ marketplaceService.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]="'/marketplace' + pkg.icon" />
<img
[src]="sanitizer.bypassSecurityTrustResourceUrl('data:image/png;base64,' + pkg.icon)"
/>
</ion-thumbnail>
<ion-label>
<h2 style="font-family: 'Montserrat'; font-weight: bold;">{{ pkg.manifest.title }}</h2>
<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">
<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>
<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" *ngIf="(localPkg['install-progress'] | installProgress) as progress">
<p
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
>
<ion-text
color="primary"
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
>
Installing
<span class="loading-dots"></span>{{ progress }}
</ion-text>

View File

@@ -1,17 +1,18 @@
import { Component, ViewChild } from '@angular/core'
import { MarketplacePkg } from 'src/app/services/api/api.types'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { AlertController, IonContent, ModalController } from '@ionic/angular'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
import { IonContent } from '@ionic/angular'
import {
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MarketplaceService } from '../marketplace.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import Fuse from 'fuse.js/dist/fuse.min.js'
import { exists, isEmptyObject } from 'src/app/util/misc.util'
import { Router } from '@angular/router'
import { filter, first } from 'rxjs/operators'
import { DomSanitizer } from '@angular/platform-browser'
const defaultOps = {
isCaseSensitive: false,
@@ -45,145 +46,106 @@ export class MarketplaceListPage {
@ViewChild(IonContent) content: IonContent
pkgs: MarketplacePkg[] = []
hasRecoveredPackage: boolean
categories: string[]
localPkgs: { [id: string]: PackageDataEntry } = { }
localPkgs: { [id: string]: PackageDataEntry } = {}
category = 'featured'
query: string
loading = true
subs: Subscription[] = []
constructor (
private readonly modalCtrl: ModalController,
constructor(
private readonly errToast: ErrorToastService,
private readonly wizardBaker: WizardBaker,
private readonly alertCtrl: AlertController,
private readonly router: Router,
public readonly patch: PatchDbService,
public readonly marketplaceService: MarketplaceService,
) { }
public readonly sanitizer: DomSanitizer,
) {}
async ngOnInit () {
async ngOnInit() {
this.subs = [
this.patch.watch$('package-data')
.pipe(
filter((data) => exists(data) && !isEmptyObject(data)),
).subscribe(pkgs => {
this.localPkgs = pkgs
Object.values(this.localPkgs).forEach(pkg => {
pkg['install-progress'] = { ...pkg['install-progress'] }
})
}),
this.patch.watch$('recovered-packages').subscribe(rps => {
this.hasRecoveredPackage = !isEmptyObject(rps)
}),
this.patch
.watch$('package-data')
.pipe(filter(data => exists(data) && !isEmptyObject(data)))
.subscribe(pkgs => {
this.localPkgs = pkgs
Object.values(this.localPkgs).forEach(pkg => {
pkg['install-progress'] = { ...pkg['install-progress'] }
})
}),
]
this.patch.watch$('server-info')
.pipe(
filter((data) => exists(data) && !isEmptyObject(data)),
first(),
).subscribe(async _ => {
try {
if (!this.marketplaceService.pkgs.length) {
await this.marketplaceService.load()
this.patch
.watch$('server-info')
.pipe(
filter(data => exists(data) && !isEmptyObject(data)),
first(),
)
.subscribe(async _ => {
try {
if (!this.marketplaceService.pkgs.length) {
await this.marketplaceService.load()
}
// category should start as first item in array
// remove here then add at beginning
const filterdCategories =
this.marketplaceService.data.categories.filter(
cat => this.category !== cat,
)
this.categories = [this.category, 'updates']
.concat(filterdCategories)
.concat(['all'])
this.filterPkgs()
} catch (e) {
this.errToast.present(e)
} finally {
this.loading = false
}
// category should start as first item in array
// remove here then add at beginning
const filterdCategories = this.marketplaceService.data.categories.filter(cat => this.category !== cat)
this.categories = [this.category, 'updates'].concat(filterdCategories).concat(['all'])
this.filterPkgs()
} catch (e) {
this.errToast.present(e)
} finally {
this.loading = false
}
})
})
}
ngAfterViewInit () {
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
}
async search (): Promise<void> {
search(): void {
if (this.query) {
this.category = undefined
}
await this.filterPkgs()
this.filterPkgs()
}
async switchCategory (category: string): Promise<void> {
switchCategory(category: string): void {
this.category = category
this.query = undefined
this.filterPkgs()
}
async updateEos (): Promise<void> {
if (this.hasRecoveredPackage) {
const alert = await this.alertCtrl.create({
header: 'Cannot Update',
message: 'You cannot update EmbassyOS when you have unresolved recovered services.',
buttons: [
{
text: 'OK',
role: 'cancel',
},
{
text: 'Resolve',
handler: () => {
this.router.navigate(['/services/list'], { replaceUrl: true })
},
cssClass: 'enter-click',
},
],
})
await alert.present()
return
}
const { version, headline, 'release-notes': releaseNotes } = this.marketplaceService.eos
await wizardModal(
this.modalCtrl,
this.wizardBaker.updateOS({
version,
headline,
releaseNotes,
}),
)
}
private async filterPkgs (): Promise<void> {
private filterPkgs(): void {
if (this.category === 'updates') {
this.pkgs = this.marketplaceService.pkgs.filter(pkg => {
const { id, version } = pkg.manifest
return this.localPkgs[id] && version !== this.localPkgs[id].manifest.version
return (
this.localPkgs[id] && version !== this.localPkgs[id].manifest.version
)
})
} else if (this.query) {
const fuse = new Fuse(this.marketplaceService.pkgs, defaultOps)
this.pkgs = fuse.search(this.query).map(p => p.item)
} else {
const pkgsToSort = this.marketplaceService.pkgs.filter(p => {
return this.category === 'all' || p.categories.includes(this.category)
})
const opts = {
...defaultOps,
threshold: 1,
}
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
this.pkgs = fuse.search(this.category !== 'all' ? this.category || '' : 'bit').map(p => p.item)
this.pkgs = fuse
.search(this.category !== 'all' ? this.category || '' : 'bit')
.map(p => p.item)
}
}
}

View File

@@ -8,18 +8,24 @@
</ion-header>
<ion-content class="ion-padding">
<text-spinner *ngIf="loading; else loaded" text="Loading Package"></text-spinner>
<text-spinner
*ngIf="loading; else loaded"
text="Loading Package"
></text-spinner>
<ng-template #loaded>
<ion-grid>
<ion-row>
<ion-col sizeXs="12" sizeSm="12" sizeMd="9" sizeLg="9" sizeXl="9">
<div class="header">
<img [src]="'/marketplace' + pkg.icon" />
<img
[src]="sanitizer.bypassSecurityTrustResourceUrl('data:image/png;base64,' + pkg.icon)"
/>
<div class="header-text">
<h1 class="header-title">{{ pkg.manifest.title }}</h1>
<p class="header-version">{{ pkg.manifest.version | displayEmver }}</p>
<p class="header-version">
{{ pkg.manifest.version | displayEmver }}
</p>
<div class="header-status">
<!-- no localPkg -->
<p *ngIf="!localPkg; else local">Not Installed</p>
@@ -27,12 +33,25 @@
<ng-template #local>
<!-- installed -->
<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>
<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>
<!-- installing, updating -->
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
<ion-text color="primary" *ngIf="(localPkg['install-progress'] | installProgress) as progress">
<p
*ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state"
>
<ion-text
color="primary"
*ngIf="(localPkg['install-progress'] | installProgress) as progress"
>
Installing
<span class="loading-dots"></span>{{ progress }}
</ion-text>
@@ -49,7 +68,14 @@
</div>
</div>
</ion-col>
<ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center">
<ion-col
sizeXl="3"
sizeLg="3"
sizeMd="3"
sizeSm="12"
sizeXs="12"
class="ion-align-self-center"
>
<!-- no localPkg -->
<ion-button *ngIf="!localPkg" expand="block" (click)="tryInstall()">
Install
@@ -58,10 +84,19 @@
<ng-container *ngIf="localPkg">
<!-- not installing, updating, or removing -->
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button *ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === -1" expand="block" (click)="presentModal('update')">
<ion-button
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === -1"
expand="block"
(click)="presentModal('update')"
>
Update
</ion-button>
<ion-button *ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 1" expand="block" color="warning" (click)="presentModal('downgrade')">
<ion-button
*ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 1"
expand="block"
color="warning"
(click)="presentModal('downgrade')"
>
Downgrade
</ion-button>
</ng-container>
@@ -69,8 +104,20 @@
</ion-col>
</ion-row>
<ion-row *ngIf="localPkg">
<ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center">
<ion-button expand="block" fill="outline" color="primary" [routerLink]="['/services', pkg.manifest.id]">
<ion-col
sizeXl="3"
sizeLg="3"
sizeMd="3"
sizeSm="12"
sizeXs="12"
class="ion-align-self-center"
>
<ion-button
expand="block"
fill="outline"
color="primary"
[routerLink]="['/services', pkg.manifest.id]"
>
View Service
</ion-button>
</ion-col>
@@ -80,16 +127,30 @@
<!-- auto-config -->
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item">
<ion-label>
<h2 style="display: flex; align-items: center;">
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
<h2 style="display: flex; align-items: center">
<ion-text
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
>{{ pkg.manifest.title }}</ion-text
>
</h2>
<p>
<ion-text color="dark">
{{ dependentInfo.title }} requires an install of {{ pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
{{ dependentInfo.title }} requires an install of {{
pkg.manifest.title }} satisfying {{ dependentInfo.version }}.
<br />
<br />
<span *ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</span>
<span *ngIf="!(pkg.manifest.version | satisfiesEmver: dependentInfo.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</span>
<span
*ngIf="pkg.manifest.version | satisfiesEmver: dependentInfo.version"
class="recommendation-text"
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
displayEmver }} is compatible.</span
>
<span
*ngIf="!(pkg.manifest.version | satisfiesEmver: dependentInfo.version)"
class="recommendation-text recommendation-error"
>{{ pkg.manifest.title }} version {{ pkg.manifest.version |
displayEmver }} is NOT compatible.</span
>
</ion-text>
</p>
</ion-label>
@@ -99,47 +160,71 @@
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
<ion-button [routerLink]="['notes']" style="position: absolute; right: 10px;" fill="clear" color="dark">
<ion-button
[routerLink]="['notes']"
style="position: absolute; right: 10px"
fill="clear"
color="dark"
>
All Release Notes
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item lines="none" color="transparent">
<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-item>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<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-item>
<!-- dependencies -->
<ng-container *ngIf="!(pkg.manifest.dependencies | empty)">
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col *ngFor="let dep of pkg.manifest.dependencies | keyvalue" sizeSm="12" sizeMd="6">
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img [src]="'/marketplace' + pkg['dependency-metadata'][dep.key].icon" />
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<span *ngIf="dep.value.requirement.type === 'required'"> (required)</span>
<span *ngIf="dep.value.requirement.type === 'opt-out'"> (required by default)</span>
<span *ngIf="dep.value.requirement.type === 'opt-in'"> (optional)</span>
</h2>
<p style="font-size: small">{{ dep.value.version | displayEmver }}</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img
[src]="'/marketplace' + pkg['dependency-metadata'][dep.key].icon"
/>
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<span *ngIf="dep.value.requirement.type === 'required'">
(required)</span
>
<span *ngIf="dep.value.requirement.type === 'opt-out'">
(required by default)</span
>
<span *ngIf="dep.value.requirement.type === 'opt-in'">
(optional)</span
>
</h2>
<p style="font-size: small">
{{ dep.value.version | displayEmver }}
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
</ng-container>
</ion-item-group>
@@ -156,14 +241,22 @@
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('license')">
<ion-item
button
detail="false"
(click)="presentModalMd('license')"
>
<ion-label>
<h2>License</h2>
<p>{{ pkg.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('instructions')">
<ion-item
button
detail="false"
(click)="presentModalMd('instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
@@ -174,21 +267,36 @@
</ion-col>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item [href]="pkg.manifest['upstream-repo']" target="_blank" rel="noreferrer" detail="false">
<ion-item
[href]="pkg.manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ pkg.manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item [href]="pkg.manifest['wrapper-repo']" target="_blank" rel="noreferrer" detail="false">
<ion-item
[href]="pkg.manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item [href]="pkg.manifest['support-site']" target="_blank" rel="noreferrer" detail="false">
<ion-item
[href]="pkg.manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ pkg.manifest['support-site'] }}</p>

View File

@@ -1,6 +1,12 @@
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
import {
AlertController,
IonContent,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Emver } from 'src/app/services/emver.service'
@@ -8,12 +14,16 @@ import { displayEmver } from 'src/app/pipes/emver.pipe'
import { DependentInfo, pauseFor } from 'src/app/util/misc.util'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
import {
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from '../marketplace.service'
import { Subscription } from 'rxjs'
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MarketplacePkg } from 'src/app/services/api/api.types'
import { DomSanitizer } from '@angular/platform-browser'
@Component({
selector: 'marketplace-show',
@@ -30,7 +40,7 @@ export class MarketplaceShowPage {
dependentInfo: DependentInfo
subs: Subscription[] = []
constructor (
constructor(
private readonly route: ActivatedRoute,
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
@@ -42,18 +52,21 @@ export class MarketplaceShowPage {
private readonly patch: PatchDbService,
private readonly embassyApi: ApiService,
private readonly marketplaceService: MarketplaceService,
) { }
public readonly sanitizer: DomSanitizer,
) {}
async ngOnInit () {
async ngOnInit() {
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
this.dependentInfo = history.state && history.state.dependentInfo as DependentInfo
this.dependentInfo =
history.state && (history.state.dependentInfo as DependentInfo)
this.subs = [
this.patch.watch$('package-data', this.pkgId)
.subscribe(pkg => {
this.patch.watch$('package-data', this.pkgId).subscribe(pkg => {
if (!pkg) return
this.localPkg = pkg
this.localPkg['install-progress'] = { ...this.localPkg['install-progress'] }
this.localPkg['install-progress'] = {
...this.localPkg['install-progress'],
}
}),
]
@@ -61,7 +74,9 @@ export class MarketplaceShowPage {
if (!this.marketplaceService.pkgs.length) {
await this.marketplaceService.load()
}
this.pkg = this.marketplaceService.pkgs.find(pkg => pkg.manifest.id === this.pkgId)
this.pkg = this.marketplaceService.pkgs.find(
pkg => pkg.manifest.id === this.pkgId,
)
if (!this.pkg) {
throw new Error(`Service with ID "${this.pkgId}" not found.`)
}
@@ -72,31 +87,34 @@ export class MarketplaceShowPage {
}
}
ngAfterViewInit () {
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
ngOnDestroy () {
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe())
}
async presentAlertVersions () {
async presentAlertVersions() {
const alert = await this.alertCtrl.create({
header: 'Versions',
inputs: this.pkg.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => {
return {
name: v, // for CSS
type: 'radio',
label: displayEmver(v), // appearance on screen
value: v, // literal SEM version value
checked: this.pkg.manifest.version === v,
}
}),
inputs: this.pkg.versions
.sort((a, b) => -1 * this.emver.compare(a, b))
.map(v => {
return {
name: v, // for CSS
type: 'radio',
label: displayEmver(v), // appearance on screen
value: v, // literal SEM version value
checked: this.pkg.manifest.version === v,
}
}),
buttons: [
{
text: 'Cancel',
role: 'cancel',
}, {
},
{
text: 'Ok',
handler: (version: string) => {
this.getPkg(version)
@@ -109,7 +127,7 @@ export class MarketplaceShowPage {
await alert.present()
}
async presentModalMd (title: string) {
async presentModalMd(title: string) {
const modal = await this.modalCtrl.create({
componentProps: {
title,
@@ -121,7 +139,7 @@ export class MarketplaceShowPage {
await modal.present()
}
async tryInstall () {
async tryInstall() {
const { id, title, version, alerts } = this.pkg.manifest
if (!alerts.install) {
@@ -148,7 +166,7 @@ export class MarketplaceShowPage {
}
}
async presentModal (action: 'update' | 'downgrade') {
async presentModal(action: 'update' | 'downgrade') {
const { id, title, version, dependencies, alerts } = this.pkg.manifest
const value = {
id,
@@ -160,9 +178,9 @@ export class MarketplaceShowPage {
const { cancelled } = await wizardModal(
this.modalCtrl,
action === 'update' ?
this.wizardBaker.update(value) :
this.wizardBaker.downgrade(value),
action === 'update'
? this.wizardBaker.update(value)
: this.wizardBaker.downgrade(value),
)
if (cancelled) return
@@ -170,7 +188,7 @@ export class MarketplaceShowPage {
this.navCtrl.back()
}
private async getPkg (version?: string): Promise<void> {
private async getPkg(version?: string): Promise<void> {
this.loading = true
try {
this.pkg = await this.marketplaceService.getPkg(this.pkgId, version)
@@ -182,7 +200,7 @@ export class MarketplaceShowPage {
}
}
private async install (id: string, version?: string): Promise<void> {
private async install(id: string, version?: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Beginning Installation',
@@ -191,7 +209,10 @@ export class MarketplaceShowPage {
loader.present()
try {
await this.embassyApi.installPackage({ id, 'version-spec': version ? `=${version}` : undefined })
await this.embassyApi.installPackage({
id,
'version-spec': version ? `=${version}` : undefined,
})
} catch (e) {
this.errToast.present(e)
} finally {

View File

@@ -1,9 +1,5 @@
import { Injectable } from '@angular/core'
import {
MarketplaceData,
MarketplaceEOS,
MarketplacePkg,
} from 'src/app/services/api/api.types'
import { MarketplaceData, MarketplacePkg } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Emver } from 'src/app/services/emver.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -14,7 +10,6 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
})
export class MarketplaceService {
data: MarketplaceData
eos: MarketplaceEOS
pkgs: MarketplacePkg[] = []
releaseNotes: {
[id: string]: {
@@ -28,31 +23,24 @@ export class MarketplaceService {
private readonly patch: PatchDbService,
) {}
get eosUpdateAvailable() {
return (
this.emver.compare(
this.eos.version,
this.patch.data['server-info'].version,
) === 1
)
}
async load(): Promise<void> {
try {
const [data, eos, pkgs] = await Promise.all([
const [data, pkgs] = await Promise.all([
this.api.getMarketplaceData({}),
this.api.getEos({
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
}),
this.getPkgs(1, 100),
])
this.data = data
this.eos = eos
this.pkgs = pkgs
const { 'selected-id': selectedId, 'known-hosts': knownHosts } =
this.patch.getData().ui.marketplace
if (knownHosts[selectedId].name !== this.data.name) {
this.api.setDbValue({
pointer: `/marketplace/known-hosts/${selectedId}/name`,
value: this.data.name,
})
}
} catch (e) {
this.data = undefined
this.eos = undefined
this.pkgs = []
throw e
}
@@ -61,10 +49,18 @@ export class MarketplaceService {
async getUpdates(localPkgs: {
[id: string]: PackageDataEntry
}): Promise<MarketplacePkg[]> {
const idAndCurrentVersions = Object.keys(localPkgs).map(key => ({
id: key,
version: localPkgs[key].manifest.version,
}))
const idAndCurrentVersions = Object.keys(localPkgs)
.map(key => ({
id: key,
version: localPkgs[key].manifest.version,
marketplaceUrl: localPkgs[key].installed['marketplace-url'],
}))
.filter(pkg => {
return (
pkg.marketplaceUrl ===
this.patch.getData().ui.marketplace['known-hosts']['selected-id'].url
)
})
const latestPkgs = await this.api.getMarketplacePkgs({
ids: idAndCurrentVersions,
'eos-version-compat':

View File

@@ -22,7 +22,7 @@
<ion-item
[button]="mp.key !== patch.data.ui.marketplace['selected-id']"
detail="false"
*ngFor="let mp of patch.data.ui.marketplace.options | keyvalue"
*ngFor="let mp of patch.data.ui.marketplace['known-hosts'] | keyvalue"
(click)="presentAction(mp.key)"
>
<div

View File

@@ -12,11 +12,8 @@ import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
import { v4 } from 'uuid'
import { MarketplaceService } from '../../marketplace-routes/marketplace.service'
import {
DataModel,
UIData,
UIMarketplaceData,
} from '../../../services/patch-db/data-model'
import { UIMarketplaceData } from '../../../services/patch-db/data-model'
import { ConfigService } from '../../../services/config.service'
@Component({
selector: 'marketplaces',
@@ -31,6 +28,7 @@ export class MarketplacesPage {
private readonly errToast: ErrorToastService,
private readonly actionCtrl: ActionSheetController,
private readonly marketplaceService: MarketplaceService,
private readonly config: ConfigService,
public readonly patch: PatchDbService,
) {}
@@ -95,10 +93,10 @@ export class MarketplacesPage {
}
private async connect(id: string): Promise<void> {
const marketplace = JSON.parse(
const marketplace: UIMarketplaceData = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
)
const newMarketplace = marketplace.options[id]
const newMarketplace = marketplace['known-hosts'][id]
const loader = await this.loadingCtrl.create({
spinner: 'lines',
@@ -141,7 +139,7 @@ export class MarketplacesPage {
}
private async delete(id: string): Promise<void> {
const marketplace = JSON.parse(
const marketplace: UIMarketplaceData = JSON.parse(
JSON.stringify(this.patch.data.ui.marketplace),
)
@@ -153,7 +151,7 @@ export class MarketplacesPage {
await loader.present()
try {
delete marketplace.options[id]
delete marketplace['known-hosts'][id]
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
} catch (e) {
this.errToast.present(e)
@@ -168,7 +166,7 @@ export class MarketplacesPage {
) as UIMarketplaceData
// no-op on duplicates
const currentUrls = Object.values(marketplace.options).map(
const currentUrls = Object.values(marketplace['known-hosts']).map(
u => new URL(u.url).hostname,
)
if (currentUrls.includes(new URL(url).hostname)) return
@@ -184,7 +182,7 @@ export class MarketplacesPage {
try {
const id = v4()
const { name } = await this.api.getMarketplaceData({}, url)
marketplace.options[id] = { name, url }
marketplace['known-hosts'][id] = { name, url }
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)
loader.dismiss()
@@ -208,7 +206,7 @@ export class MarketplacesPage {
) as UIMarketplaceData
// no-op on duplicates
const currentUrls = Object.values(marketplace.options).map(
const currentUrls = Object.values(marketplace['known-hosts']).map(
u => new URL(u.url).hostname,
)
if (currentUrls.includes(new URL(url).hostname)) return
@@ -223,7 +221,7 @@ export class MarketplacesPage {
try {
const id = v4()
const { name } = await this.api.getMarketplaceData({}, url)
marketplace.options[id] = { name, url }
marketplace['known-hosts'][id] = { name, url }
marketplace['selected-id'] = id
} catch (e) {
this.errToast.present({ message: `Could not connect to ${url}` } as any)

View File

@@ -1,7 +1,12 @@
<ion-header>
<ion-toolbar>
<ion-title *ngIf="!patch.loaded">Loading<span class="loading-dots"></span></ion-title>
<ion-title *ngIf="patch.loaded">{{ patch.data.ui.name || "Embassy-" + patch.data['server-info'].id }}</ion-title>
<ion-title *ngIf="!patch.loaded"
>Loading<span class="loading-dots"></span
></ion-title>
<ion-title *ngIf="patch.loaded"
>{{ patch.data.ui.name || "Embassy-" + patch.data['server-info'].id
}}</ion-title
>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
@@ -19,25 +24,56 @@
<ng-template #data>
<ion-item-group>
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider>
<ion-item button *ngFor="let button of cat.value" [detail]="button.detail" [disabled]="button.disabled | async" (click)="button.action()">
<ion-item-divider
><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider
>
<ion-item
button
*ngFor="let button of cat.value"
[detail]="button.detail"
[disabled]="button.disabled | async"
(click)="button.action()"
>
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>
<h2>{{ button.title }}</h2>
<p *ngIf="button.description">{{ button.description }}</p>
<!-- "Create Backup" button only -->
<p *ngIf="button.title === 'Create Backup'">
<ng-container *ngIf="patch.data['server-info'].status as status">
<ion-text color="warning" *ngIf="status === ServerStatus.Running">
Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }}
<ion-text
color="warning"
*ngIf="status === ServerStatus.Running"
>
Last Backup: {{ patch.data['server-info']['last-backup'] ?
(patch.data['server-info']['last-backup'] | date: 'short') :
'never' }}
</ion-text>
<span *ngIf="status === ServerStatus.BackingUp" class="inline">
<ion-spinner color="success" style="height: 12px; width: 12px; margin-right: 6px;"></ion-spinner>
<ion-text color="success">
Backing up
</ion-text>
<ion-spinner
color="success"
style="height: 12px; width: 12px; margin-right: 6px"
></ion-spinner>
<ion-text color="success"> Backing up </ion-text>
</span>
</ng-container>
</p>
<!-- "Software Update" button only -->
<p *ngIf="button.title === 'Software Update'">
<ng-container
*ngIf="eosService.updateAvailable$ | async; else check"
>
<ion-text class="inline" color="success">
<ion-icon name="repeat"></ion-icon>
Update Available
</ion-text>
</ng-container>
<ng-template #check>
<i>Check for updates</i>
</ng-template>
</p>
</ion-label>
</ion-item>
</div>

View File

@@ -4,6 +4,7 @@ import {
LoadingController,
NavController,
IonicSafeString,
ModalController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
@@ -11,7 +12,11 @@ import { ErrorToastService } from 'src/app/services/error-toast.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ServerStatus } from 'src/app/services/patch-db/data-model'
import { Observable, of } from 'rxjs'
import { map } from 'rxjs/operators'
import { filter, map, take } from 'rxjs/operators'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { exists, isEmptyObject } from 'src/app/util/misc.util'
import { EOSService } from 'src/app/services/eos.service'
@Component({
selector: 'server-show',
@@ -20,17 +25,57 @@ import { map } from 'rxjs/operators'
})
export class ServerShowPage {
ServerStatus = ServerStatus
hasRecoveredPackage: boolean
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly wizardBaker: WizardBaker,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
public readonly eosService: EOSService,
public readonly patch: PatchDbService,
) {}
ngOnInit() {
this.patch
.watch$('recovered-packages')
.pipe(filter(exists), take(1))
.subscribe(rps => {
this.hasRecoveredPackage = !isEmptyObject(rps)
})
}
async updateEos(): Promise<void> {
if (this.hasRecoveredPackage) {
const alert = await this.alertCtrl.create({
header: 'Cannot Update',
message:
'You cannot update EmbassyOS when you have unresolved recovered services.',
buttons: ['OK'],
})
await alert.present()
} else {
const {
version,
headline,
'release-notes': releaseNotes,
} = this.eosService.eos
await wizardModal(
this.modalCtrl,
this.wizardBaker.updateOS({
version,
headline,
releaseNotes,
}),
)
}
}
async presentAlertRestart() {
const alert = await this.alertCtrl.create({
header: 'Confirm',
@@ -54,7 +99,6 @@ export class ServerShowPage {
}
async presentAlertShutdown() {
const sts = this.patch.data['server-info'].status
const alert = await this.alertCtrl.create({
header: 'Warning',
message:
@@ -81,7 +125,7 @@ export class ServerShowPage {
const alert = await this.alertCtrl.create({
header: 'System Rebuild',
message: new IonicSafeString(
`<ion-text color="warning">Important:</ion-text> This will tear down all service containers and rebuild them from scratch. This may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`,
`<ion-text color="warning">Warning:</ion-text> This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`,
),
buttons: [
{
@@ -151,6 +195,23 @@ export class ServerShowPage {
}
}
private async checkForEosUpdate(): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Checking for updates',
cssClass: 'loader',
})
await loader.present()
try {
await this.eosService.getEOS()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
settings: ServerSettings = {
Backups: [
{
@@ -179,6 +240,17 @@ export class ServerShowPage {
},
],
Insights: [
{
title: 'Software Update',
description: 'Get the latest version of EmbassyOS',
icon: 'cog-outline',
action: () =>
this.eosService.updateAvailable$.getValue()
? this.updateEos()
: this.checkForEosUpdate(),
detail: false,
disabled: of(false),
},
{
title: 'About',
description: 'Basic information about your Embassy',
@@ -303,12 +375,14 @@ export class ServerShowPage {
}
interface ServerSettings {
[key: string]: {
title: string
description: string
icon: string
action: Function
detail: boolean
disabled: Observable<boolean>
}[]
[key: string]: SettingBtn[]
}
interface SettingBtn {
title: string
description: string
icon: string
action: Function
detail: boolean
disabled: Observable<boolean>
}

File diff suppressed because one or more lines are too long

View File

@@ -64,14 +64,6 @@ export module RR {
export type KillSessionsReq = WithExpire<{ ids: string[] }> // sessions.kill
export type KillSessionsRes = WithRevision<null>
// marketplace URLs
export type SetEosMarketplaceReq = WithExpire<{ url: string }> // marketplace.eos.set
export type SetEosMarketplaceRes = WithRevision<null>
export type SetPackageMarketplaceReq = WithExpire<{ url: string }> // marketplace.package.set
export type SetPackageMarketplaceRes = WithRevision<null>
// password
export type UpdatePasswordReq = { password: string } // password.set
@@ -267,15 +259,11 @@ export module RR {
query?: string
page?: string
'per-page'?: string
url?: string
}
export type GetMarketplacePackagesRes = MarketplacePkg[]
export type GetReleaseNotesReq = { id: string }
export type GetReleaseNotesRes = { [version: string]: string }
export type GetLatestVersionReq = { ids: string[] }
export type GetLatestVersionRes = { [id: string]: string }
}
export type WithExpire<T> = { 'expire-id'?: string } & T

View File

@@ -113,15 +113,6 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
params: RR.GetReleaseNotesReq,
): Promise<RR.GetReleaseNotesRes>
abstract getLatestVersion(
params: RR.GetLatestVersionReq,
): Promise<RR.GetLatestVersionRes>
// protected abstract setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes>
// setPackageMarketplace = (params: RR.SetPackageMarketplaceReq) => this.syncResponse(
// () => this.setPackageMarketplaceRaw(params),
// )()
// password
// abstract updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes>

View File

@@ -4,15 +4,22 @@ import { ApiService } from './embassy-api.service'
import { RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { PatchDbService } from '../patch-db/patch-db.service'
import { ConfigService } from '../config.service'
@Injectable()
export class LiveApiService extends ApiService {
private marketplaceUrl: string
constructor(
private readonly http: HttpService,
private readonly patch: PatchDbService,
private readonly config: ConfigService,
) {
super()
;(window as any).rpcClient = this
this.patch.watch$('ui', 'marketplace', 'selected-id').subscribe(id => {
this.marketplaceUrl = id
})
}
async getStatic(url: string): Promise<string> {
@@ -106,10 +113,7 @@ export class LiveApiService extends ApiService {
params: {},
url?: string,
): Promise<T> {
if (!url) {
const id = this.patch.data.ui.marketplace['selected-id']
url = this.patch.data.ui.marketplace.options[id].url
}
url = url || this.marketplaceUrl
const fullURL = `${url}${path}?${new URLSearchParams(params).toString()}`
return this.http.rpcRequest({
method: 'marketplace.get',
@@ -120,25 +124,25 @@ export class LiveApiService extends ApiService {
async getEos(
params: RR.GetMarketplaceEOSReq,
): Promise<RR.GetMarketplaceEOSRes> {
return this.http.httpRequest({
method: Method.GET,
url: '/marketplace/eos/latest',
return this.marketplaceProxy(
'/eos/latest',
params,
})
this.config.eosMarketplaceUrl,
)
}
async getMarketplaceData(
params: RR.GetMarketplaceDataReq,
url?: string,
): Promise<RR.GetMarketplaceDataRes> {
return this.marketplaceProxy('/marketplace/package/data', params, url)
return this.marketplaceProxy('/package/data', params, url)
}
async getMarketplacePkgs(
params: RR.GetMarketplacePackagesReq,
): Promise<RR.GetMarketplacePackagesRes> {
if (params.query) params.category = undefined
return this.marketplaceProxy('/marketplace/package/index', {
return this.marketplaceProxy('/package/index', {
...params,
ids: JSON.stringify(params.ids),
})
@@ -147,27 +151,9 @@ export class LiveApiService extends ApiService {
async getReleaseNotes(
params: RR.GetReleaseNotesReq,
): Promise<RR.GetReleaseNotesRes> {
return this.http.httpRequest({
method: Method.GET,
url: '/marketplace/package/release-notes',
params,
})
return this.marketplaceProxy('/package/release-notes', params)
}
async getLatestVersion(
params: RR.GetLatestVersionReq,
): Promise<RR.GetLatestVersionRes> {
return this.http.httpRequest({
method: Method.GET,
url: '/marketplace/latest-version',
params,
})
}
// async setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes> {
// return this.http.rpcRequest({ method: 'marketplace.package.set', params })
// }
// password
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
// return this.http.rpcRequest({ method: 'password.set', params })

View File

@@ -228,16 +228,6 @@ export class MockApiService extends ApiService {
return Mock.ReleaseNotes
}
async getLatestVersion (
params: RR.GetLatestVersionReq,
): Promise<RR.GetLatestVersionRes> {
await pauseFor(2000)
return params.ids.reduce((obj, id) => {
obj[id] = '1.3.0'
return obj
}, {})
}
// password
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
// await pauseFor(2000)

View File

@@ -16,15 +16,7 @@ export const mockPatchData: DataModel = {
'pkg-order': [],
'ack-welcome': '1.0.0',
'ack-share-stats': false,
marketplace: {
'selected-id': 'asdfasdf',
options: {
asdfasdf: {
name: 'Start9',
url: 'start9marketplace.com',
},
},
},
marketplace: undefined,
},
'server-info': {
id: 'embassy-abcdefgh',
@@ -439,6 +431,8 @@ export const mockPatchData: DataModel = {
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'marketplace-url.com',
'developer-key': 'developer-key',
},
},
lnd: {
@@ -644,6 +638,8 @@ export const mockPatchData: DataModel = {
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
},
},
'marketplace-url': 'marketplace-url.com',
'developer-key': 'developer-key',
},
},
},

View File

@@ -9,7 +9,7 @@ import { WorkspaceConfig } from '@shared'
const {
useMocks,
ui: { gitHash, patchDb, api, mocks },
ui: { gitHash, patchDb, api, mocks, eosMarketplaceURL },
} = require('../../../../../config.json') as WorkspaceConfig
@Injectable({
@@ -25,6 +25,7 @@ export class ConfigService {
gitHash = gitHash
patchDb = patchDb
api = api
eosMarketplaceUrl = eosMarketplaceURL
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
isConsulate = window['platform'] === 'ios'

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { MarketplaceEOS } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Emver } from 'src/app/services/emver.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Injectable({
providedIn: 'root',
})
export class EOSService {
eos: MarketplaceEOS
updateAvailable$ = new BehaviorSubject<boolean>(false)
constructor(
private readonly api: ApiService,
private readonly emver: Emver,
private readonly patch: PatchDbService,
) {}
async getEOS(): Promise<void> {
this.eos = await this.api.getEos({
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
})
const updateAvailable =
this.emver.compare(
this.eos.version,
this.patch.data['server-info'].version,
) === 1
this.updateAvailable$.next(updateAvailable)
}
}

View File

@@ -17,8 +17,8 @@ export interface UIData {
}
export interface UIMarketplaceData {
'selected-id': string
options: {
'selected-id': string | null
'known-hosts': {
[id: string]: {
url: string
name: string
@@ -94,6 +94,8 @@ export interface InstalledPackageDataEntry {
'interface-addresses': {
[id: string]: { 'tor-address': string; 'lan-address': string }
}
'marketplace-url': string | null
'developer-key': string
}
export interface CurrentDependencyInfo {

View File

@@ -1,26 +1,14 @@
import { Injectable } from '@angular/core'
import {
AlertController,
IonicSafeString,
ModalController,
NavController,
} from '@ionic/angular'
import { wizardModal } from '../components/install-wizard/install-wizard.component'
import { ModalController } from '@ionic/angular'
import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
import { displayEmver } from '../pipes/emver.pipe'
import { RR } from './api/api.types'
import { ConfigService } from './config.service'
import { Emver } from './emver.service'
import { MarketplaceService } from '../pages/marketplace-routes/marketplace.service'
import { DataModel } from './patch-db/data-model'
import { PatchDbService } from './patch-db/patch-db.service'
import { filter, take } from 'rxjs/operators'
import { isEmptyObject } from '../util/misc.util'
import { ApiService } from './api/embassy-api.service'
import { Subscription } from 'rxjs'
import { ServerConfigService } from './server-config.service'
import { v4 } from 'uuid'
@Injectable({
providedIn: 'root',
@@ -29,13 +17,9 @@ export class StartupAlertsService {
private checks: Check<any>[]
constructor(
private readonly alertCtrl: AlertController,
private readonly navCtrl: NavController,
private readonly config: ConfigService,
private readonly modalCtrl: ModalController,
private readonly marketplaceService: MarketplaceService,
private readonly api: ApiService,
private readonly emver: Emver,
private readonly wizardBaker: WizardBaker,
private readonly patch: PatchDbService,
private readonly serverConfig: ServerConfigService,
@@ -43,22 +27,14 @@ export class StartupAlertsService {
const osWelcome: Check<boolean> = {
name: 'osWelcome',
shouldRun: () => this.shouldRunOsWelcome(),
check: async () => true,
display: () => this.displayOsWelcome(),
}
const shareStats: Check<boolean> = {
name: 'shareStats',
shouldRun: () => this.shouldRunShareStats(),
check: async () => true,
display: () => this.displayShareStats(),
}
const osUpdate: Check<RR.GetMarketplaceEOSRes | undefined> = {
name: 'osUpdate',
shouldRun: () => this.shouldRunOsUpdateCheck(),
check: () => this.osUpdateCheck(),
display: pkg => this.displayOsUpdateCheck(pkg),
}
this.checks = [osWelcome, shareStats, osUpdate]
this.checks = [osWelcome, shareStats]
}
// This takes our three checks and filters down to those that should run.
@@ -72,37 +48,14 @@ export class StartupAlertsService {
filter(data => !isEmptyObject(data)),
take(1),
)
.subscribe(async data => {
if (!data.ui.marketplace) {
const uuid = v4()
const value = {
'selected-id': uuid,
options: {
[uuid]: {
url: 'marketplaceurl.com',
name: 'Start9',
},
},
}
await this.api.setDbValue({ pointer: 'marketplace', value })
}
.subscribe(async () => {
await this.checks
.filter(c => !this.config.skipStartupAlerts && c.shouldRun())
// returning true in the below block means to continue to next modal
// returning false means to skip all subsequent modals
.reduce(async (previousDisplay, c) => {
let checkRes: any
try {
checkRes = await c.check()
} catch (e) {
console.error(`Exception in ${c.name} check:`, e)
return true
}
const displayRes = await previousDisplay
if (!checkRes) return true
if (displayRes) return c.display(checkRes)
if (displayRes) return c.display()
}, Promise.resolve(true))
})
}
@@ -120,21 +73,6 @@ export class StartupAlertsService {
return this.patch.getData().ui['auto-check-updates']
}
// ** check **
private async osUpdateCheck(): Promise<RR.GetMarketplaceEOSRes | undefined> {
const res = await this.api.getEos({
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
})
if (this.emver.compare(this.config.version, res.version) === -1) {
return res
} else {
return undefined
}
}
// ** display **
private async displayOsWelcome(): Promise<boolean> {
@@ -174,69 +112,14 @@ export class StartupAlertsService {
})
})
}
private async displayOsUpdateCheck(
eos: RR.GetMarketplaceEOSRes,
): Promise<boolean> {
const { update } = await this.presentAlertNewOS(eos.version)
if (update) {
const { cancelled } = await wizardModal(
this.modalCtrl,
this.wizardBaker.updateOS({
version: eos.version,
headline: eos.headline,
releaseNotes: eos['release-notes'],
}),
)
if (cancelled) return true
return false
}
return true
}
// more
private async presentAlertNewOS(
versionLatest: string,
): Promise<{ cancel?: true; update?: true }> {
return new Promise(async resolve => {
const alert = await this.alertCtrl.create({
header: 'New EmbassyOS Version!',
message: new IonicSafeString(
`<div style="display: flex; flex-direction: column; justify-content: space-around; min-height: 100px">
<div>Update EmbassyOS to version ${displayEmver(
versionLatest,
)}?</div>
<div style="font-size:x-small">You can disable these checks in your Embassy Config</div>
</div>
`,
),
buttons: [
{
text: 'Not now',
role: 'cancel',
handler: () => resolve({ cancel: true }),
},
{
text: 'Update',
handler: () => resolve({ update: true }),
cssClass: 'enter-click',
},
],
})
await alert.present()
})
}
}
type Check<T> = {
// validates whether a check should run based on server properties
shouldRun: () => boolean
// executes a check, often requiring api call. It should return a false-y value if there should be no display.
check: () => Promise<T>
// display an alert based on the result of the check.
// return false if subsequent modals should not be displayed
display: (a: T) => Promise<boolean>
display: () => Promise<boolean>
// for logging purposes
name: string
}