+
!isEmptyObject(data as object)),
+ filter(obj => !isEmptyObject(obj)),
take(1),
)
.subscribe(_ => {
this.subscriptions = this.subscriptions.concat([
this.watchStatus(),
- // // watch version to refresh browser window
+ // watch version to refresh browser window
this.watchVersion(),
- // // watch unread notification count to display toast
+ // watch unread notification count to display toast
this.watchNotifications(),
- // // run startup alerts
+ // run startup alerts
this.startupAlertsService.runChecks(),
])
})
@@ -135,15 +139,19 @@ export class AppComponent {
this.patch.stop()
this.storage.clear()
if (this.errToast) this.errToast.dismiss()
+ if (this.updateToast) this.updateToast.dismiss()
+ if (this.notificationToast) this.notificationToast.dismiss()
if (this.offlineToast) this.offlineToast.dismiss()
- this.router.navigate(['/login'], { replaceUrl: true })
+ this.zone.run(() => {
+ this.router.navigate(['/login'], { replaceUrl: true })
+ })
}
})
}
async goToWebsite (): Promise {
let url: string
- if (this.config.isTor) {
+ if (this.config.isTor()) {
url = 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
} else {
url = 'https://start9.com'
@@ -238,13 +246,16 @@ export class AppComponent {
this.showMenu = true
this.router.navigate([''], { replaceUrl: true })
}
- if (ServerStatus.BackingUp === status && !route.startsWith(maintenance)) {
+ if (status === ServerStatus.BackingUp && !route.startsWith(maintenance)) {
this.showMenu = false
this.router.navigate([maintenance], { replaceUrl: true })
}
- if (ServerStatus.Updating === status) {
+ if (status === ServerStatus.Updating) {
this.watchUpdateProgress()
}
+ if (status === ServerStatus.Updated && !this.updateToast) {
+ this.presentToastUpdated()
+ }
})
}
@@ -253,13 +264,14 @@ export class AppComponent {
.pipe(
filter(progress => !!progress),
takeWhile(progress => progress.downloaded < progress.size),
+ // @TODO will there be a maintenance page while server is updating to new version?
finalize(async () => {
- const maintenance = '/maintenance'
- const route = this.router.url
- if (!route.startsWith(maintenance)) {
- this.showMenu = false
- this.router.navigate([maintenance], { replaceUrl: true })
- }
+ // const maintenance = '/maintenance'
+ // const route = this.router.url
+ // if (!route.startsWith(maintenance)) {
+ // this.showMenu = false
+ // this.router.navigate([maintenance], { replaceUrl: true })
+ // }
if (this.osUpdateProgress) this.osUpdateProgress.downloaded = this.osUpdateProgress.size
await pauseFor(200)
this.osUpdateProgress = undefined
@@ -307,8 +319,39 @@ export class AppComponent {
await alert.present()
}
+ private async presentToastUpdated () {
+ if (this.updateToast) return
+
+ this.updateToast = await this.toastCtrl.create({
+ header: 'EOS download complete!',
+ message: `Restart Embassy for changes to take effect.`,
+ position: 'bottom',
+ duration: 0,
+ cssClass: 'success-toast',
+ buttons: [
+ {
+ side: 'start',
+ icon: 'close',
+ handler: () => {
+ return true
+ },
+ },
+ {
+ side: 'end',
+ text: 'Restart',
+ handler: () => {
+ this.restart()
+ },
+ },
+ ],
+ })
+ await this.updateToast.present()
+ }
+
private async presentToastNotifications () {
- const toast = await this.toastCtrl.create({
+ if (this.notificationToast) return
+
+ this.notificationToast = await this.toastCtrl.create({
header: 'Embassy',
message: `New notifications`,
position: 'bottom',
@@ -330,7 +373,7 @@ export class AppComponent {
},
],
})
- await toast.present()
+ await this.notificationToast.present()
}
private async presentToastOffline (message: string | IonicSafeString, link?: string) {
@@ -373,6 +416,23 @@ export class AppComponent {
await this.offlineToast.present()
}
+ private async restart (): Promise {
+ const loader = await this.loadingCtrl.create({
+ spinner: 'lines',
+ message: 'Restarting...',
+ cssClass: 'loader',
+ })
+ await loader.present()
+
+ try {
+ await this.embassyApi.restartServer({ })
+ } catch (e) {
+ this.errToast.present(e)
+ } finally {
+ loader.dismiss()
+ }
+ }
+
splitPaneVisible (e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
diff --git a/ui/src/app/components/install-wizard/alert/alert.component.ts b/ui/src/app/components/install-wizard/alert/alert.component.ts
index 8e2427961..9e7b0f1c6 100644
--- a/ui/src/app/components/install-wizard/alert/alert.component.ts
+++ b/ui/src/app/components/install-wizard/alert/alert.component.ts
@@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core'
+import { BehaviorSubject, Subject } from 'rxjs'
@Component({
selector: 'alert',
@@ -12,5 +13,8 @@ export class AlertComponent {
titleColor: string
}
+ loading$ = new BehaviorSubject(false)
+ cancel$ = new Subject()
+
load () { }
}
diff --git a/ui/src/app/components/status/status.component.html b/ui/src/app/components/status/status.component.html
index 98bb71356..353c72485 100644
--- a/ui/src/app/components/status/status.component.html
+++ b/ui/src/app/components/status/status.component.html
@@ -6,4 +6,5 @@
>
{{ disconnected ? 'Unknown' : rendering.display }}
+ {{ installProgress }}%
diff --git a/ui/src/app/components/status/status.component.ts b/ui/src/app/components/status/status.component.ts
index eb0cd0897..302c84c35 100644
--- a/ui/src/app/components/status/status.component.ts
+++ b/ui/src/app/components/status/status.component.ts
@@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core'
+import { InstallProgress } from 'src/app/services/patch-db/data-model'
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
@Component({
@@ -12,5 +13,6 @@ export class StatusComponent {
@Input() style?: string = 'regular'
@Input() weight?: string = 'normal'
@Input() disconnected?: boolean = false
+ @Input() installProgress?: number
}
diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/ui/src/app/pages/apps-routes/app-list/app-list.page.html
index 9e58787c3..d4f05ce32 100644
--- a/ui/src/app/pages/apps-routes/app-list/app-list.page.html
+++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.html
@@ -1,13 +1,13 @@
- Installed Services
+ Services
-
+
@@ -26,55 +26,139 @@
-
-
-
-
-
-
-
-
-
-
-
![]()
-

-
-
- {{ pkg.value.entry.manifest.title }}
-
-
-
-
-
-
- {{ pkg.value.entry.state | titlecase }} {{ pkg.value.installProgress.totalProgress }}%
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Recovered Services
+
+
+
+
+
+ {{ rec.title }}
+ {{ rec.version | displayEmver }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.scss b/ui/src/app/pages/apps-routes/app-list/app-list.page.scss
index ebb03daaf..c3947fe21 100644
--- a/ui/src/app/pages/apps-routes/app-list/app-list.page.scss
+++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.scss
@@ -1,85 +1,24 @@
-.installed-card {
- margin: 4px;
- padding: 4px;
- background: linear-gradient(37deg, #333333, #131313);
- border-radius: 6px 0 6px 6px;
- text-align: center;
-
- ion-card-header {
- padding: 0 10px;
-
- ion-card-title {
- font-family: 'Montserrat';
- font-size: calc(10px + .5vw);
- margin-bottom: 12px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-}
-
-.main-img {
- width: 40%;
- margin: 12px;
+ion-item-divider {
+ margin-bottom: 16px;
}
.bulb {
- height: calc(10px + .5vw);
- width: calc(10px + .5vw);
- border-radius: 100%;
- box-shadow: 0 0 4px 4px rgba(255,213,52, 0.1);
position: absolute !important;
- left: 10px !important;
- top: 10px !important;
+ left: 9px !important;
+ top: 8px !important;
+ height: 14px;
+ width: 14px;
+ border-radius: 100%;
+ box-shadow: 0 0 6px 6px rgba(255,213,52, 0.1);
}
.warning-icon {
position: absolute !important;
- left: 8px !important;
- top: 8px !important;
- font-size: 19px;
+ left: 6px !important;
+ top: 0 !important;
+ font-size: 12px;
border-radius: 100%;
padding: 1px;
background-color: rgba(255,213,52, 0.1);
box-shadow: 0 0 4px 4px rgba(255,213,52, 0.1);
}
-
-.launch-button-triangle {
- right: 0px;
-
- border-style: solid;
- border-width: 24px;
- border-color: rgb(70 193 255 / 75%) rgb(70 193 255 / 75%) transparent transparent;
- &:hover {
- border-color: rgb(70 193 255) rgb(70 193 255) transparent transparent;
- }
- ion-icon {
- position: absolute;
- right: 8px;
- top: 8px;
- color: white;
- }
-}
-
-.launch-disabled {
- pointer-events: none;
- border-color: transparent;
- &:hover {
- border-color: transparent;
- }
- ion-icon {
- color: var(--ion-color-dark-shade);
- }
-}
-
-.launch-container {
- position: absolute;
- right: 0px;
- top: 0px;
-}
-
-.status-toolbar {
- --min-height: 36px;
- border-radius: 6px;
-}
diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts
index 80205c947..72350e8f7 100644
--- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts
+++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts
@@ -2,12 +2,14 @@ import { Component } from '@angular/core'
import { ConfigService } from 'src/app/services/config.service'
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
-import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
-import { Subscription } from 'rxjs'
+import { DataModel, PackageDataEntry, PackageState, RecoveredPackageDataEntry } from 'src/app/services/patch-db/data-model'
+import { combineLatest, Observable, Subscription } from 'rxjs'
import { DependencyStatus, HealthStatus, PrimaryRendering, renderPkgStatus, StatusRendering } from 'src/app/services/pkg-status-rendering.service'
-import { filter } from 'rxjs/operators'
-import { isEmptyObject } from 'src/app/util/misc.util'
+import { filter, take, tap } from 'rxjs/operators'
+import { isEmptyObject, exists } from 'src/app/util/misc.util'
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-list',
@@ -19,75 +21,74 @@ export class AppListPage {
subs: Subscription[] = []
connectionFailure: boolean
- pkgs: { [id: string]: PkgInfo } = { }
+ pkgs: PkgInfo[] = []
+ recoveredPkgs: RecoveredInfo[] = []
+ order: string[] = []
loading = true
empty = false
+ reordering = false
constructor (
private readonly config: ConfigService,
private readonly connectionService: ConnectionService,
private readonly pkgLoading: PackageLoadingService,
- public readonly patch: PatchDbService,
+ private readonly api: ApiService,
+ private readonly patch: PatchDbService,
+ private readonly errToast: ErrorToastService,
) { }
ngOnInit () {
- this.subs = [
- this.patch.watch$('package-data')
- .pipe(
- filter(obj => {
- return obj &&
- (
- isEmptyObject(obj) ||
- Object.keys(obj).length !== Object.keys(this.pkgs).length
- )
- }),
- )
- .subscribe(pkgs => {
- this.loading = false
+ this.patch.watch$()
+ .pipe(
+ filter(data => exists(data) && !isEmptyObject(data)),
+ take(1),
+ )
+ .subscribe(data => {
+ this.loading = false
+ const pkgs = JSON.parse(JSON.stringify(data['package-data'])) as { [id: string]: PackageDataEntry }
+ this.recoveredPkgs = Object.entries(data['recovered-packages']).map(([id, val]) => {
+ return {
+ ...val,
+ id,
+ installing: false,
+ }
+ })
+ this.order = [...data.ui['pkg-order'] || []]
- const ids = Object.keys(pkgs)
+ // add known pkgs in preferential order
+ this.order.forEach(id => {
+ if (pkgs[id]) {
+ this.pkgs.push(this.buildPkg(pkgs[id]))
+ delete pkgs[id]
+ }
+ })
- this.empty = !ids.length
-
- Object.keys(this.pkgs).forEach(id => {
- if (!ids.includes(id)) {
- this.pkgs[id].sub.unsubscribe()
- delete this.pkgs[id]
- }
+ // add unknown packages to end and set order in UI DB
+ if (!isEmptyObject(pkgs)) {
+ Object.values(pkgs).forEach(pkg => {
+ this.pkgs.unshift(this.buildPkg(pkg))
+ this.order.unshift(pkg.manifest.id)
})
+ this.setOrder()
+ }
- ids.forEach(id => {
- // if already subscribed, return
- if (this.pkgs[id]) return
- this.pkgs[id] = {
- entry: pkgs[id],
- primaryRendering: PrimaryRendering[renderPkgStatus(pkgs[id]).primary],
- installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.pkgLoading.transform(pkgs[id]['install-progress']) : undefined,
- error: false,
- sub: null,
- }
- // subscribe to pkg
- this.pkgs[id].sub = this.patch.watch$('package-data', id).subscribe(pkg => {
- if (!pkg) return
- const statuses = renderPkgStatus(pkg)
- const primaryRendering = PrimaryRendering[statuses.primary]
- this.pkgs[id].entry = pkg
- this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined
- this.pkgs[id].primaryRendering = primaryRendering
- this.pkgs[id].error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, DependencyStatus.Critical].includes(statuses.dependency)
- })
- })
- }),
+ if (!this.pkgs.length && isEmptyObject(this.recoveredPkgs)) {
+ this.empty = true
+ }
+ this.subs.push(this.subscribeBoth())
+ })
+
+ this.subs.push(
this.connectionService.watchFailure$()
.subscribe(connectionFailure => {
this.connectionFailure = connectionFailure !== ConnectionFailure.None
}),
- ]
+ )
}
ngOnDestroy () {
- Object.values(this.pkgs).forEach(pkg => pkg.sub.unsubscribe())
+ this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
this.subs.forEach(sub => sub.unsubscribe())
}
@@ -97,15 +98,128 @@ export class AppListPage {
window.open(this.config.launchableURL(pkg), '_blank', 'noreferrer')
}
+ toggleReorder (): void {
+ if (this.reordering) {
+ const newPkgs = []
+ this.order.forEach(id => {
+ const pkg = this.pkgs.find(pkg => pkg.entry.manifest.id === id)
+ if (pkg) {
+ newPkgs.push(pkg)
+ }
+ })
+ this.pkgs = newPkgs
+ this.setOrder()
+ }
+ this.reordering = !this.reordering
+ }
+
+ async reorder (ev: any): Promise {
+ ev.detail.complete()
+ const toMove = this.order.splice(ev.detail.from, 1)[0]
+ this.order.splice(ev.detail.to, 0, toMove)
+ }
+
+ async install (pkg: RecoveredInfo): Promise {
+ pkg.installing = true
+ try {
+ await this.api.installPackage({ id: pkg.id, version: pkg.version })
+ } catch (e) {
+ this.errToast.present(e)
+ pkg.installing = false
+ }
+ }
+
+ async uninstall (pkg: RecoveredInfo, index: number): Promise {
+ pkg.installing = true
+ try {
+ await this.api.uninstallPackage({ id: pkg.id })
+ this.recoveredPkgs.splice(index, 1)
+ } catch (e) {
+ this.errToast.present(e)
+ pkg.installing = false
+ }
+ }
+
+ private subscribeBoth (): Subscription {
+ return combineLatest([this.watchPkgs(), this.patch.watch$('recovered-packages')])
+ .subscribe(([pkgs, recoveredPkgs]) => {
+ Object.keys(recoveredPkgs).forEach(id => {
+ const inPkgs = !!pkgs[id]
+ const recoveredIndex = this.recoveredPkgs.findIndex(rec => rec.id === id)
+ if (inPkgs && recoveredIndex > -1) {
+ this.recoveredPkgs.splice(recoveredIndex, 1)
+ }
+ })
+ })
+ }
+
+ private watchPkgs (): Observable {
+ return this.patch.watch$('package-data')
+ .pipe(
+ filter(obj => {
+ return Object.keys(obj).length !== this.pkgs.length
+ }),
+ tap(pkgs => {
+ const ids = Object.keys(pkgs)
+
+ this.pkgs.forEach((pkg, i) => {
+ const id = pkg.entry.manifest.id
+ if (!ids.includes(id)) {
+ pkg.sub.unsubscribe()
+ this.pkgs.splice(i, 1)
+ }
+ })
+
+ ids.forEach(id => {
+ // if already exists, return
+ const pkg = this.pkgs.find(p => p.entry.manifest.id === id)
+ if (pkg) return
+ // otherwise add new entry to beginning of array
+ this.pkgs.unshift(this.buildPkg(pkgs[id]))
+ })
+ }),
+ )
+ }
+
+ private setOrder (): void {
+ this.api.setDbValue({ pointer: '/pkg-order', value: this.order })
+ }
+
+ private buildPkg (pkg: PackageDataEntry): PkgInfo {
+ const pkgInfo: PkgInfo = {
+ entry: pkg,
+ primaryRendering: PrimaryRendering[renderPkgStatus(pkg).primary],
+ installProgress: !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined,
+ error: false,
+ sub: null,
+ }
+ // subscribe to pkg
+ pkgInfo.sub = this.patch.watch$('package-data', pkg.manifest.id).subscribe(update => {
+ if (!update) return
+ const statuses = renderPkgStatus(update)
+ const primaryRendering = PrimaryRendering[statuses.primary]
+ pkgInfo.entry = update
+ pkgInfo.installProgress = !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined
+ pkgInfo.primaryRendering = primaryRendering
+ pkgInfo.error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, DependencyStatus.Critical].includes(statuses.dependency)
+ })
+ return pkgInfo
+ }
+
asIsOrder () {
return 0
}
}
+interface RecoveredInfo extends RecoveredPackageDataEntry {
+ id: string
+ installing: boolean
+}
+
interface PkgInfo {
entry: PackageDataEntry
primaryRendering: StatusRendering
installProgress: ProgressData
error: boolean
sub: Subscription | null
-}
+}
\ No newline at end of file
diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/ui/src/app/pages/apps-routes/app-show/app-show.page.html
index b42d52ca5..af4505ad2 100644
--- a/ui/src/app/pages/apps-routes/app-show/app-show.page.html
+++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.html
@@ -23,7 +23,13 @@
Status
-
+
@@ -64,7 +70,7 @@
-
+
{{ health.key }}
@@ -86,7 +92,7 @@
-
+
{{ dep.value.title }}
{{ dep.value.version | displayEmver }}
diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html
index e14e1eb54..b85c123c2 100644
--- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html
+++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html
@@ -79,7 +79,7 @@
*ngIf="!pkgs.length && category ==='updates'"
style="text-align: center;"
>
- 👏👏👏 Up to date! 👏👏👏
+ All services are up to date!