From a43ff976a209c17b509c3661df076f2ab6e86a2e Mon Sep 17 00:00:00 2001 From: Drew Ansbacher Date: Thu, 29 Jul 2021 16:59:03 -0400 Subject: [PATCH] Drew cleanup (#380) * accordion works * cleanup * styling * more styling * App show change (#387) * show page change * no marketplace * app show changes * update marketplace list * icon * top left icon * toolbar * right size * out of toolbar * no service details * fix skeleton text and server metrics page * stuck * add session management * complete sessions feature * app show page * remove unnecessary icons * add cli to list of possible sessions * Modal global (#383) * modal checkpoint * global modal * black looks good now * black looks good now * not smaller Co-authored-by: Drew Ansbacher Co-authored-by: Drew Ansbacher Co-authored-by: Drew Ansbacher Co-authored-by: Drew Ansbacher Co-authored-by: Matt Hill Co-authored-by: Matt Hill --- ui/src/app/app.component.html | 51 +++-- ui/src/app/app.component.scss | 17 +- ui/src/app/app.component.ts | 5 +- .../dependents/dependents.component.html | 6 +- .../install-wizard.component.html | 6 +- .../object-config-item.component.html | 2 +- .../skeleton-list.component.scss | 2 +- .../app/components/status/status.component.ts | 2 +- .../app-action-input.page.html | 4 +- .../app-config-list/app-config-list.page.html | 2 +- .../app-config-list/app-config-list.page.ts | 1 - .../app-config-object.page.ts | 1 - .../app-config-value.page.html | 22 +- .../app-config-value/app-config-value.page.ts | 1 - .../app-restore/app-restore.component.html} | 19 +- .../app-restore.component.module.ts | 25 +++ .../app-restore/app-restore.component.scss} | 0 .../app-restore/app-restore.component.ts} | 41 ++-- .../backup-confirmation.component.html | 4 +- .../app-actions/app-actions.module.ts | 2 + .../app-actions/app-actions.page.html | 52 ++++- .../app-actions/app-actions.page.ts | 21 +- .../app-config/app-config.page.html | 4 +- .../apps-routes/app-config/app-config.page.ts | 1 - .../app-interfaces/app-interfaces.page.html | 72 +++--- .../apps-routes/app-list/app-list.page.html | 4 +- .../apps-routes/app-list/app-list.page.scss | 3 +- .../apps-routes/app-logs/app-logs.page.html | 2 +- .../app-metrics/app-metrics.page.html | 65 +----- .../apps-routes/app-show/app-show.module.ts | 2 - .../apps-routes/app-show/app-show.page.html | 210 +++++++++--------- .../apps-routes/app-show/app-show.page.scss | 47 +--- .../apps-routes/app-show/app-show.page.ts | 43 ++-- .../pages/apps-routes/apps-routing.module.ts | 4 - ui/src/app/pages/login/login.page.html | 58 ++--- ui/src/app/pages/login/login.page.scss | 29 ++- ui/src/app/pages/login/login.page.ts | 2 +- .../app-release-notes.page.html | 18 +- .../app-release-notes.page.scss | 10 +- .../app-release-notes.page.ts | 5 + .../marketplace-list.page.html | 51 +++-- .../marketplace-list.page.scss | 20 +- .../marketplace-show.module.ts | 2 - .../marketplace-show.page.html | 18 +- .../notifications/notifications.module.ts | 2 + .../notifications/notifications.page.html | 8 +- .../security-options.page.html | 3 + .../security-routing.module.ts | 4 + .../sessions/sessions.module.ts} | 14 +- .../sessions/sessions.page.html | 41 ++++ .../sessions/sessions.page.scss | 0 .../security-routes/sessions/sessions.page.ts | 99 +++++++++ .../ssh-keys/ssh-keys.page.html | 2 +- .../security-routes/ssh-keys/ssh-keys.page.ts | 1 - .../server-logs/server-logs.page.html | 2 +- .../server-metrics/server-metrics.page.html | 8 +- .../server-metrics/server-metrics.page.scss | 2 +- .../server-show/server-show.page.html | 7 +- .../server-show/server-show.page.ts | 12 +- .../server-specs/server-specs.page.html | 2 +- .../app/pages/server-routes/wifi/wifi.page.ts | 1 - ui/src/app/services/api/api.fixures.ts | 20 ++ ui/src/app/services/api/api.types.ts | 25 ++- .../api/embassy/embassy-api.service.ts | 4 + .../api/embassy/embassy-live-api.service.ts | 8 + .../api/embassy/embassy-mock-api.service.ts | 10 + ui/src/app/services/auth.service.ts | 6 +- ui/src/app/services/config.service.ts | 5 +- ui/src/assets/img/icon.png | Bin 0 -> 16380 bytes ui/src/global.scss | 85 +++---- ui/src/theme/variables.scss | 175 ++------------- 71 files changed, 808 insertions(+), 694 deletions(-) rename ui/src/app/{pages/apps-routes/app-restore/app-restore.page.html => modals/app-restore/app-restore.component.html} (83%) create mode 100644 ui/src/app/modals/app-restore/app-restore.component.module.ts rename ui/src/app/{pages/apps-routes/app-restore/app-restore.page.scss => modals/app-restore/app-restore.component.scss} (100%) rename ui/src/app/{pages/apps-routes/app-restore/app-restore.page.ts => modals/app-restore/app-restore.component.ts} (73%) rename ui/src/app/pages/{apps-routes/app-restore/app-restore.module.ts => server-routes/security-routes/sessions/sessions.module.ts} (68%) create mode 100644 ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html create mode 100644 ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.scss create mode 100644 ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts create mode 100644 ui/src/assets/img/icon.png diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 4f57d664c..e70261339 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -1,34 +1,48 @@ - - + +
+ +
+
+ - - {{ page.title }} + + + {{ page.title }} + {{ unreadCount }} -
+ +
+
+ + + + Log Out + + + +
- - - - - Logout - - - -
@@ -80,6 +94,8 @@ + + @@ -89,10 +105,8 @@ - - @@ -100,7 +114,6 @@ - @@ -137,11 +150,9 @@ - load bold - diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss index aee63ce2d..1f58d0b37 100644 --- a/ui/src/app/app.component.scss +++ b/ui/src/app/app.component.scss @@ -1,18 +1,9 @@ -.selected { - --background: linear-gradient(120deg, #1e1e1e -1%, var(--ion-color-danger) 100%); +.bold { + font-weight: bold; } -.menu-style { - border-style: solid; - border-width: 0px 1px 0px 0px; - border-color: var(--ion-color-danger); - ion-item::part(native) { - height: 56px; - } -} - -.selected-badge { - background-color: #1e1e1e; +.dim { + color: var(--ion-color-dark-shade); } ion-split-pane { diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 5ede9b38c..3b00243d5 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -71,9 +71,7 @@ export class AppComponent { private readonly config: ConfigService, readonly splitPane: SplitPaneTracker, ) { - // set dark theme - document.body.classList.toggle('dark', true) - this.init() + this.init() } async init () { @@ -266,7 +264,6 @@ export class AppComponent { }, { text: 'Logout', - cssClass: 'alert-danger', handler: () => { this.logout() }, diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.html b/ui/src/app/components/install-wizard/dependents/dependents.component.html index 94a04c346..6ca972529 100644 --- a/ui/src/app/components/install-wizard/dependents/dependents.component.html +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.html @@ -26,12 +26,12 @@ Will Stop - + - +
{{ dep.value.title }}
diff --git a/ui/src/app/components/install-wizard/install-wizard.component.html b/ui/src/app/components/install-wizard/install-wizard.component.html index 277767d9b..48d5ddcd0 100644 --- a/ui/src/app/components/install-wizard/install-wizard.component.html +++ b/ui/src/app/components/install-wizard/install-wizard.component.html @@ -2,7 +2,7 @@

{{ params.toolbar.title }}

-

{{ params.toolbar.action }} {{ params.toolbar.version | displayEmver }}

+

{{ params.toolbar.action }} {{ params.toolbar.version | displayEmver }}

@@ -37,11 +37,11 @@ - + {{ cancel.text }} - + {{ cancel.text }} diff --git a/ui/src/app/components/object-config/object-config-item.component.html b/ui/src/app/components/object-config/object-config-item.component.html index 7ed4bd2c7..7092aa6b9 100644 --- a/ui/src/app/components/object-config/object-config-item.component.html +++ b/ui/src/app/components/object-config/object-config-item.component.html @@ -6,7 +6,7 @@
- {{ spec.name }} + {{ spec.name }} (new)
diff --git a/ui/src/app/components/skeleton-list/skeleton-list.component.scss b/ui/src/app/components/skeleton-list/skeleton-list.component.scss index 79ca6ab3f..730c11ca5 100644 --- a/ui/src/app/components/skeleton-list/skeleton-list.component.scss +++ b/ui/src/app/components/skeleton-list/skeleton-list.component.scss @@ -1,3 +1,3 @@ ion-note { width: 50%; -} \ No newline at end of file +} diff --git a/ui/src/app/components/status/status.component.ts b/ui/src/app/components/status/status.component.ts index bf326035d..405b0bb43 100644 --- a/ui/src/app/components/status/status.component.ts +++ b/ui/src/app/components/status/status.component.ts @@ -8,7 +8,7 @@ import { PkgStatusRendering } from 'src/app/services/pkg-status-rendering.servic }) export class StatusComponent { @Input() rendering: PkgStatusRendering - @Input() size?: 'small' | 'medium' | 'large' = 'large' + @Input() size?: 'small' | 'medium' | 'large' | 'x-large' = 'large' @Input() style?: string = 'regular' @Input() weight?: string = 'normal' } diff --git a/ui/src/app/modals/app-action-input/app-action-input.page.html b/ui/src/app/modals/app-action-input/app-action-input.page.html index bc17ab980..1626472db 100644 --- a/ui/src/app/modals/app-action-input/app-action-input.page.html +++ b/ui/src/app/modals/app-action-input/app-action-input.page.html @@ -1,8 +1,8 @@ - - + + {{ action.name }} diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.html b/ui/src/app/modals/app-config-list/app-config-list.page.html index 2d1270b25..a9e23fb2c 100644 --- a/ui/src/app/modals/app-config-list/app-config-list.page.html +++ b/ui/src/app/modals/app-config-list/app-config-list.page.html @@ -55,7 +55,7 @@ {{ valueString[i] }} - +
diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.ts b/ui/src/app/modals/app-config-list/app-config-list.page.ts index c35b48482..0a16fb4e1 100644 --- a/ui/src/app/modals/app-config-list/app-config-list.page.ts +++ b/ui/src/app/modals/app-config-list/app-config-list.page.ts @@ -118,7 +118,6 @@ export class AppConfigListPage extends ModalPresentable { }, { text: 'Delete', - cssClass: 'alert-danger', handler: () => { if (typeof key === 'number') { (this.value as any[]).splice(key, 1) diff --git a/ui/src/app/modals/app-config-object/app-config-object.page.ts b/ui/src/app/modals/app-config-object/app-config-object.page.ts index 8f6fde306..d5bac463c 100644 --- a/ui/src/app/modals/app-config-object/app-config-object.page.ts +++ b/ui/src/app/modals/app-config-object/app-config-object.page.ts @@ -41,7 +41,6 @@ export class AppConfigObjectPage { }, { text: 'Delete', - cssClass: 'alert-danger', handler: () => { this.dismiss(true) }, diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.html b/ui/src/app/modals/app-config-value/app-config-value.page.html index 8c2d95fb4..ed74f6133 100644 --- a/ui/src/app/modals/app-config-value/app-config-value.page.html +++ b/ui/src/app/modals/app-config-value/app-config-value.page.html @@ -33,7 +33,7 @@ - + @@ -41,8 +41,8 @@ - {{ spec.units }} - + {{ spec.units }} + @@ -63,20 +63,18 @@

- {{ spec.patternDescription }} + {{ spec.patternDescription }}

- {{ integralDescription }} + {{ integralDescription }}

- {{ rangeDescription }} -

-

- -

Default: {{ defaultDescription }}

-

Units: {{ spec.units }}

- + {{ rangeDescription }}

+ +

Default: {{ defaultDescription }}

+

Units: {{ spec.units }}

+
diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.ts b/ui/src/app/modals/app-config-value/app-config-value.page.ts index b4d124824..60f8407a9 100644 --- a/ui/src/app/modals/app-config-value/app-config-value.page.ts +++ b/ui/src/app/modals/app-config-value/app-config-value.page.ts @@ -176,7 +176,6 @@ export class AppConfigValuePage { }, { text: `Leave`, - cssClass: 'alert-danger', handler: () => { this.modalCtrl.dismiss() }, diff --git a/ui/src/app/pages/apps-routes/app-restore/app-restore.page.html b/ui/src/app/modals/app-restore/app-restore.component.html similarity index 83% rename from ui/src/app/pages/apps-routes/app-restore/app-restore.page.html rename to ui/src/app/modals/app-restore/app-restore.component.html index 02efcf81b..8c84baeef 100644 --- a/ui/src/app/pages/apps-routes/app-restore/app-restore.page.html +++ b/ui/src/app/modals/app-restore/app-restore.component.html @@ -1,22 +1,21 @@ - - - Restore From Backup - - - + + + + Restore From Backup + - - + + - +

Warning

@@ -56,5 +55,5 @@ -
+
diff --git a/ui/src/app/modals/app-restore/app-restore.component.module.ts b/ui/src/app/modals/app-restore/app-restore.component.module.ts new file mode 100644 index 000000000..bb729830f --- /dev/null +++ b/ui/src/app/modals/app-restore/app-restore.component.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppRestoreComponent } from './app-restore.component' +import { PwaBackComponentModule } from '../../components/pwa-back-button/pwa-back.component.module' +import { BackupConfirmationComponentModule } from '../backup-confirmation/backup-confirmation.component.module' +import { SharingModule } from '../../modules/sharing.module' +import { TextSpinnerComponentModule } from '../../components/text-spinner/text-spinner.component.module' + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + SharingModule, + BackupConfirmationComponentModule, + PwaBackComponentModule, + TextSpinnerComponentModule, + ], + declarations: [ + AppRestoreComponent, + ], + exports: [AppRestoreComponent], + +}) +export class AppRestoreComponentModule { } \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-restore/app-restore.page.scss b/ui/src/app/modals/app-restore/app-restore.component.scss similarity index 100% rename from ui/src/app/pages/apps-routes/app-restore/app-restore.page.scss rename to ui/src/app/modals/app-restore/app-restore.component.scss diff --git a/ui/src/app/pages/apps-routes/app-restore/app-restore.page.ts b/ui/src/app/modals/app-restore/app-restore.component.ts similarity index 73% rename from ui/src/app/pages/apps-routes/app-restore/app-restore.page.ts rename to ui/src/app/modals/app-restore/app-restore.component.ts index b694e1db6..ff3213ef4 100644 --- a/ui/src/app/pages/apps-routes/app-restore/app-restore.page.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.ts @@ -1,31 +1,28 @@ -import { Component, ViewChild } from '@angular/core' -import { IonContent, LoadingController, ModalController } from '@ionic/angular' +import { Component, Input } from '@angular/core' +import { LoadingController, ModalController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component' import { DiskInfo } from 'src/app/services/api/api.types' -import { ActivatedRoute } from '@angular/router' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { Subscription } from 'rxjs' -import { take } from 'rxjs/operators' import { ErrorToastService } from 'src/app/services/error-toast.service' @Component({ selector: 'app-restore', - templateUrl: './app-restore.page.html', - styleUrls: ['./app-restore.page.scss'], + templateUrl: './app-restore.component.html', + styleUrls: ['./app-restore.component.scss'], }) -export class AppRestorePage { +export class AppRestoreComponent { + @Input() pkgId: string disks: DiskInfo - pkgId: string title: string loading = true + submitting = false allPartitionsMounted: boolean - @ViewChild(IonContent) content: IonContent subs: Subscription[] = [] constructor ( - private readonly route: ActivatedRoute, private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, private readonly loadingCtrl: LoadingController, @@ -34,13 +31,13 @@ export class AppRestorePage { ) { } ngOnInit () { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') + console.log('initing') this.getExternalDisks() } - ngAfterViewInit () { - this.content.scrollToPoint(undefined, 1) - } + // ngAfterViewInit () { + // this.content.scrollToPoint(undefined, 1) + // } async refresh () { this.loading = true @@ -54,6 +51,7 @@ export class AppRestorePage { } catch (e) { this.errToast.present(e) } finally { + console.log('loading false') this.loading = false } } @@ -77,11 +75,14 @@ export class AppRestorePage { await m.present() } + dismiss () { + this.modalCtrl.dismiss({ }) + } + private async restore (logicalname: string, password: string): Promise { - const loader = await this.loadingCtrl.create({ - spinner: 'lines', - }) - await loader.present() + console.log('here here here') + this.submitting = true + // await loader.present() try { await this.embassyApi.restorePackage({ @@ -90,9 +91,9 @@ export class AppRestorePage { password, }) } catch (e) { - this.errToast.present(e) + this.modalCtrl.dismiss({ error: e }) } finally { - loader.dismiss() + this.modalCtrl.dismiss({ }) } } } diff --git a/ui/src/app/modals/backup-confirmation/backup-confirmation.component.html b/ui/src/app/modals/backup-confirmation/backup-confirmation.component.html index 4b115f653..33322e8e5 100644 --- a/ui/src/app/modals/backup-confirmation/backup-confirmation.component.html +++ b/ui/src/app/modals/backup-confirmation/backup-confirmation.component.html @@ -2,11 +2,11 @@

Encrypt Backup

-

Enter your master password to create an encrypted backup.

+

Enter your master password to create an encrypted backup.

Decrypt Backup

-

Enter the password that was originally used to encrypt this backup.

+

Enter the password that was originally used to encrypt this backup.

diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts index a769ef9b9..8d5cb6e52 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts @@ -7,6 +7,7 @@ import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-b import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { SharingModule } from 'src/app/modules/sharing.module' import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module' +import { AppRestoreComponentModule } from 'src/app/modals/app-restore/app-restore.component.module' const routes: Routes = [ { @@ -24,6 +25,7 @@ const routes: Routes = [ QRComponentModule, SharingModule, AppActionInputPageModule, + AppRestoreComponentModule, ], declarations: [AppActionsPage], }) diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index 0567483dd..db49895c2 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -7,9 +7,57 @@ + + - + + + + + + + + + + + {{ action.value.name }} + + + {{ action.value.description }} + + + + + + + + + + Restore From Backup + + + All changes since backup will be lost. + + + + + + + + + + Uninstall + + + This will uninstall the service from your Embassy and delete all data permanently. + + + + + + + \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 09bfe0f5f..7ac13751c 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -10,6 +10,7 @@ import { Subscription } from 'rxjs' import { ConfigCursor } from 'src/app/pkg-config/config-cursor' import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page' import { ErrorToastService } from 'src/app/services/error-toast.service' +import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component' @Component({ selector: 'app-actions', @@ -79,7 +80,7 @@ export class AppActionsPage { await alert.present() } } else { - const statuses = [...action.value['allowedStatuses']] + const statuses = [...action.value['allowed-statuses']] const last = statuses.pop() let statusesStr = statuses.join(', ') let error = null @@ -103,6 +104,23 @@ export class AppActionsPage { } } + async restore (): Promise { + const m = await this.modalCtrl.create({ + componentProps: { + pkgId: this.pkgId, + }, + component: AppRestoreComponent, + backdropDismiss: false, + }) + + m.onWillDismiss().then(res => { + const data = res.data + if (data.error) this.errToast.present(data.error) + }) + + return await m.present() + } + async uninstall (manifest: Manifest) { const { id, title, version, alerts } = manifest const data = await wizardModal( @@ -134,7 +152,6 @@ export class AppActionsPage { header: 'Execution Complete', message: res.message.split('\n').join('

'), buttons: ['OK'], - cssClass: 'alert-success-message', }) await successAlert.present() } catch (e) { diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.html b/ui/src/app/pages/apps-routes/app-config/app-config.page.html index 0674cb66e..97c9d0636 100644 --- a/ui/src/app/pages/apps-routes/app-config/app-config.page.html +++ b/ui/src/app/pages/apps-routes/app-config/app-config.page.html @@ -52,9 +52,9 @@

- + - + {{ rec.dependentTitle }}

diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.ts b/ui/src/app/pages/apps-routes/app-config/app-config.page.ts index 1bfeae07d..f47146bd8 100644 --- a/ui/src/app/pages/apps-routes/app-config/app-config.page.ts +++ b/ui/src/app/pages/apps-routes/app-config/app-config.page.ts @@ -206,7 +206,6 @@ export class AppConfigPage { }, { text: `Leave`, - cssClass: 'alert-danger', handler: () => { this.navCtrl.back() }, diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index 57fc03920..1d82350a7 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -7,43 +7,37 @@ - - - - - - - {{ interface.value.name }} - {{ interface.value.description }} - - Launch - - - - - - - -

Tor Address

-

{{ 'http://' + int['tor-address'] }}

-
- - - -
- - -

LAN Address

-

{{ 'https://' + int['lan-address'] }}

-
- - - -
-
-
-
-
-
-
+ + + + {{ interface.value.name }} + {{ interface.value.description }} + + Launch + + + + + + + +

Tor Address

+

{{ 'http://' + int['tor-address'] }}

+
+ + + +
+ + +

LAN Address

+

{{ 'https://' + int['lan-address'] }}

+
+ + + +
+
+
+
\ No newline at end of file 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 baf87a241..abde30d58 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 @@ -10,10 +10,10 @@
-

Welcome to your Embassy

+

Welcome to Embassy

Get started by installing your first service.

- + Marketplace 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 0678b11ce..a821184cb 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 @@ -25,7 +25,6 @@ .main-img { width: 50%; margin: 12px; - border-radius: var(--icon-border-radius); } .bulb-on { @@ -70,7 +69,7 @@ border-color: transparent; } ion-icon { - color: var(--ion-color-medium); + color: var(--ion-color-dark-shade); } } diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html index 23a61df63..b6ca9e725 100644 --- a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html @@ -12,7 +12,7 @@ - + diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html index a85baed77..5aee68a73 100644 --- a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html @@ -3,62 +3,19 @@ - Health + Monitor - + - - - - - Health Checks - - - - - - No health checks - - - - - -
- - - - - -

{{ health.key }}

-

{{ health.value.error }}

-
-
-
-
-
-
- - - - - - Metrics - - - - - - - - {{ metric.key }} - - - {{ metric.value.value }} {{ metric.value.unit }} - - - - - + + + + {{ metric.key }} + + {{ metric.value.value }} {{ metric.value.unit }} + + +
diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 565484e3b..fdc51942e 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -6,7 +6,6 @@ import { AppShowPage } from './app-show.page' import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { SharingModule } from 'src/app/modules/sharing.module' import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' const routes: Routes = [ @@ -24,7 +23,6 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), PwaBackComponentModule, - BadgeMenuComponentModule, InstallWizardComponentModule, ], declarations: [AppShowPage], 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 c847f5e01..43a6b06a5 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 @@ -3,53 +3,118 @@ - Service Details - - - + + + + + +

+ {{ pkg.manifest.title }} +

+

{{ pkg.manifest.version | displayEmver }}

+
+
- + - -
- - - - - -

- {{ pkg.manifest.title }} -

-
{{ pkg.manifest.version | displayEmver }}
+ + + Status + + + - - -
- - - Configure - - - Stop - - - Fix - - - Start - -
- - - - Launch Web Interface + + Web - -
+ + Configure + + + Stop + + + Fix + + + Start + + + + + + + + Health Checks + + + + + + +

{{ health.key }}

+

{{ health.value.result }}

+

{{ health.value.error }}

+
+
+
+ + + Menu + + + {{ button.title }} + + + + + Dependencies + + + + + + +

{{ patch.data['package-data'][dep.key] ? patch.data['package-data'][dep.key].manifest.title : pkg.installed.status['dependency-errors'][dep.key]?.title }}

+

{{ pkg.manifest.dependencies[dep.key].version | displayEmver }}

+

{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}

+
+ + + View + + + + + Install + + + + + Start + + + Update + + + Configure + + + +
+ +
+
+
+
+
+ + +

Downloading: {{ (pkg['install-progress'] | installState).downloadProgress }}%

- - - - - - - -
- -

- {{ button.title }} -
-
-
-
-
- - - - - Dependencies - - - - - - - - - -

{{ patch.data['package-data'][dep.key] ? patch.data['package-data'][dep.key].manifest.title : pkg.installed.status['dependency-errors'][dep.key]?.title }}

-

{{ pkg.manifest.dependencies[dep.key].version | displayEmver }}

-

{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}

-
- - - View - - - - - Install - - - - - Start - - - Update - - - Configure - - - -
- -
-
-
-
-
-
-
-
-
diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.scss b/ui/src/app/pages/apps-routes/app-show/app-show.page.scss index 4eaf15be4..504da9ea1 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.scss +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.scss @@ -1,45 +1,14 @@ .less-large { - font-size: 20px !important; + font-size: 18px !important; } -.top-plate { - background: var(--ion-item-background); - margin: 0 16px; - padding: 12px; - border-radius: 10px; - border-style: solid; - border-color: #373737; - ion-item { - border-radius: 10px; - } -} - -.status-readout { - --background: transparent; - display: flex; - justify-content: space-between; - padding: 8px 16px; - border-radius: 10px; - align-items: center; - background: var(--ion-background-color); +.action-button { margin: 10px; - border-style: solid; - border-width: 1px; - border-color: #404040; + min-height: 36px; + min-width: 72px; } -.launch-button { - --background: rgb(70 193 255 / 75%); - --background-hover: rgb(70 193 255); - --background-hover-opacity: 100%; - --border-style: none; - --color: white; - --border-radius: 10px; - margin: 12px 10px; -} - -.dep-card { - border-radius: 10px; - border-style: solid; - border-color: #404040; -} +.icon-spinner { + height: 20px; + width: 20px; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 65f234782..0b1c6e51b 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -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 { ConfigService } from 'src/app/services/config.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' -import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model' +import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, MainStatus, PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model' import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { ConnectionService } from 'src/app/services/connection.service' import { ErrorToastService } from 'src/app/services/error-toast.service' @@ -29,6 +29,8 @@ export class AppShowPage { DependencyErrorType = DependencyErrorType rendering: PkgStatusRendering Math = Math + mainStatus: MainStatus + @ViewChild(IonContent) content: IonContent subs: Subscription[] = [] @@ -59,6 +61,11 @@ export class AppShowPage { this.connected = connected this.rendering = renderPkgStatus(pkg.state, pkg.installed.status) }), + this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main') + .subscribe(main => { + this.mainStatus = main + console.log(this.mainStatus) + }), ] this.setButtons() } @@ -236,17 +243,9 @@ export class AppShowPage { color: 'danger', disabled: [], }, - { - action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), - title: 'Monitor', - icon: 'medkit-outline', - color: 'danger', - // @TODO make the disabled check better. Don't want to list every status here. Monitor should be disabled except is pkg is running. - disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], - }, { action: () => this.navCtrl.navigateForward(['config'], { relativeTo: this.route }), - title: 'Configure', + title: 'Settings', icon: 'construct-outline', color: 'danger', disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], @@ -272,6 +271,14 @@ export class AppShowPage { color: 'danger', disabled: [], }, + { + action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + title: 'Monitor', + icon: 'pulse-outline', + color: 'danger', + // @TODO make the disabled check better. Don't want to list every status here. Monitor should be disabled except is pkg is running. + disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], + }, { action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), title: 'Logs', @@ -279,13 +286,6 @@ export class AppShowPage { color: 'danger', disabled: [], }, - { - action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }), - title: 'Restore From Backup', - icon: 'color-wand-outline', - color: 'danger', - disabled: [FEStatus.Connecting, FEStatus.Installing, FEStatus.Updating, FEStatus.Stopping, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring], - }, { action: () => this.navCtrl.navigateForward(['manifest'], { relativeTo: this.route }), title: 'Package Details', @@ -295,18 +295,11 @@ export class AppShowPage { }, { action: () => this.donate(), - title: 'Support Project', + title: 'Donate', icon: 'logo-bitcoin', color: 'danger', disabled: [], }, - { - action: () => this.navCtrl.navigateForward(['/marketplace', this.pkgId], { relativeTo: this.route }), - title: 'Marketplace Listing', - icon: 'storefront-outline', - color: 'danger', - disabled: [], - }, ] } } diff --git a/ui/src/app/pages/apps-routes/apps-routing.module.ts b/ui/src/app/pages/apps-routes/apps-routing.module.ts index 05ae01bc0..7816170c1 100644 --- a/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -51,10 +51,6 @@ const routes: Routes = [ path: ':pkgId/properties', loadChildren: () => import('./app-properties/app-properties.module').then(m => m.AppPropertiesPageModule), }, - { - path: ':pkgId/restore', - loadChildren: () => import('./app-restore/app-restore.module').then(m => m.AppRestorePageModule), - }, ] @NgModule({ diff --git a/ui/src/app/pages/login/login.page.html b/ui/src/app/pages/login/login.page.html index c90bc5177..ef6a33ed2 100644 --- a/ui/src/app/pages/login/login.page.html +++ b/ui/src/app/pages/login/login.page.html @@ -1,31 +1,35 @@ - - + + - - -
- - - - -
- - - - - - - - - {{ error }} - - - - Next - -
-
-
+ + +
+ +
+ + + + Log in to Embassy + + + +
+ +

Password

+ + + + + + + +

{{ error }}

+
+ +
+
diff --git a/ui/src/app/pages/login/login.page.scss b/ui/src/app/pages/login/login.page.scss index 3f4f0de05..eca54b7a1 100644 --- a/ui/src/app/pages/login/login.page.scss +++ b/ui/src/app/pages/login/login.page.scss @@ -1,3 +1,28 @@ -.sharp-button { - --border-radius: 1px; +ion-card-title { + margin: 24px 0; + font-family: 'Montserrat'; + font-size: x-large; + --color: var(--ion-color-light); +} + +ion-item { + --border-radius: 4px; + --border-style: solid; + --border-width: 1px; + --border-color: var(--ion-color-light); +} + +.input-label { + text-align: left; + padding-bottom: 2px; + font-size: small; + font-weight: bold; +} + +.login-button { + margin-inline-start: 0; + margin-inline-end: 0; + margin-top: 24px; + height: 48px; + --background: linear-gradient(45deg, var(--ion-color-light) 16%, var(--ion-color-dark) 150%); } \ No newline at end of file diff --git a/ui/src/app/pages/login/login.page.ts b/ui/src/app/pages/login/login.page.ts index f0efb80d1..dd40cbd80 100644 --- a/ui/src/app/pages/login/login.page.ts +++ b/ui/src/app/pages/login/login.page.ts @@ -33,7 +33,7 @@ export class LoginPage { this.error = '' this.loader = await this.loadingCtrl.create({ - message: 'Authenticating', + message: 'Logging in', spinner: 'lines', }) await this.loader.present() diff --git a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html index e6fd347b6..a42d165e9 100644 --- a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html +++ b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html @@ -11,11 +11,23 @@ -
- +
+

{{ note.key | displayEmver }}

- +
diff --git a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss index 7a499cb08..e122c8b4b 100644 --- a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss +++ b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.scss @@ -1,8 +1,8 @@ -.metric-note { - font-size: 16px; +.panel { + margin: 0px; + padding: 0px 24px; } -.acc-text { - margin: 0px 0px 10px 0px; - padding: 15px; +.active { + border: 5px solid #4d4d4d; } \ No newline at end of file diff --git a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts index aaa52d4b4..7621c8494 100644 --- a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts +++ b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.ts @@ -37,6 +37,11 @@ export class AppReleaseNotes { } } + getDocSize (selected: string) { + const element = document.getElementById(selected) + return `${element.scrollHeight}px` + } + asIsOrder (a: any, b: any) { return 0 } 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 17e93fbe4..0113c999f 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 @@ -1,59 +1,59 @@ - - Service Marketplace + + - - - - + - - - Now Available... - EmbassyOS Version {{ eos.version }} - - - {{ eos.headline }} - - +

Embassy Marketplace

-

Categories

- -
+
{{ cat }}
+
+
- +
+ + + + + + +

Now Available...

+

Embassy OS {{ eos.version }}

+

{{ eos.headline }}

+
+
+
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.description.short }}

- +

{{ pkg.manifest.title }}

+

{{ pkg.manifest.description.short }}

+

Installed Update Available @@ -66,6 +66,9 @@

+ +

Not Installed

+
diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss index 595a7579d..d1ba64ed8 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.scss @@ -2,7 +2,7 @@ overflow: auto; white-space: nowrap; // background-color: var(--ion-color-light); - height: 80px; + height: 60px; /* Hide scrollbar for Chrome, Safari and Opera */ ::-webkit-scrollbar { @@ -14,14 +14,16 @@ scrollbar-width: none; /* Firefox */ } -.eos-card { - --background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%); - margin: 0 10px 16px 10px; - cursor: pointer; +.eos-item { + --border-style: none; + --background: linear-gradient(45deg, var(--ion-color-dark) -380%, var(--ion-color-medium) 100%) } -.cat-selected { - border-width: 0 0 1px 0; - border-style: solid; - border-color: var(--ion-color-primary); +.selected { + font-weight: bold; +} + +.dim { + font-weight: 300; + color: var(--ion-color-dark-shade); } diff --git a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 54c0ec21c..7023a22e8 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -5,7 +5,6 @@ import { IonicModule } from '@ionic/angular' import { MarketplaceShowPage } from './marketplace-show.page' import { SharingModule } from 'src/app/modules/sharing.module' import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module' @@ -26,7 +25,6 @@ const routes: Routes = [ RouterModule.forChild(routes), SharingModule, PwaBackComponentModule, - BadgeMenuComponentModule, InstallWizardComponentModule, ], declarations: [MarketplaceShowPage], diff --git a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index 2ff0e04e2..bdd67a882 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -4,9 +4,6 @@ Listing - - - @@ -27,13 +24,14 @@

- Not Installed + Not Installed

- Installed at {{ localPkg.manifest.version | displayEmver }} + Installed + Update Available

@@ -51,17 +49,17 @@ - + Install - + Update - + Downgrade @@ -74,9 +72,9 @@

- + - + {{ rec.dependentTitle }}

diff --git a/ui/src/app/pages/notifications/notifications.module.ts b/ui/src/app/pages/notifications/notifications.module.ts index 7cbcf5c40..0aaf655cf 100644 --- a/ui/src/app/pages/notifications/notifications.module.ts +++ b/ui/src/app/pages/notifications/notifications.module.ts @@ -6,6 +6,7 @@ import { NotificationsPage } from './notifications.page' import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { SharingModule } from 'src/app/modules/sharing.module' +import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module' const routes: Routes = [ { @@ -22,6 +23,7 @@ const routes: Routes = [ PwaBackComponentModule, BadgeMenuComponentModule, SharingModule, + TextSpinnerComponentModule, ], declarations: [NotificationsPage], }) diff --git a/ui/src/app/pages/notifications/notifications.page.html b/ui/src/app/pages/notifications/notifications.page.html index db6deade7..ae6351eed 100644 --- a/ui/src/app/pages/notifications/notifications.page.html +++ b/ui/src/app/pages/notifications/notifications.page.html @@ -15,14 +15,12 @@ - + -

- Notifications about Embassy and services will appear here. -

+ Notifications about Embassy and services will appear here.
@@ -47,7 +45,7 @@

- + diff --git a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html index 1bc181e2b..287159381 100644 --- a/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html +++ b/ui/src/app/pages/server-routes/security-routes/security-options/security-options.page.html @@ -38,6 +38,9 @@ SSH Keys + + Active Sessions + \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/security-routes/security-routing.module.ts b/ui/src/app/pages/server-routes/security-routes/security-routing.module.ts index 92ccf1dbd..8bcccafd8 100644 --- a/ui/src/app/pages/server-routes/security-routes/security-routing.module.ts +++ b/ui/src/app/pages/server-routes/security-routes/security-routing.module.ts @@ -6,6 +6,10 @@ const routes: Routes = [ path: '', loadChildren: () => import('./security-options/security-options.module').then(m => m.SecurityOptionsPageModule), }, + { + path: 'sessions', + loadChildren: () => import('./sessions/sessions.module').then(m => m.SessionsPageModule), + }, { path: 'ssh-keys', loadChildren: () => import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule), diff --git a/ui/src/app/pages/apps-routes/app-restore/app-restore.module.ts b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts similarity index 68% rename from ui/src/app/pages/apps-routes/app-restore/app-restore.module.ts rename to ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts index 965ddf52b..a8006122d 100644 --- a/ui/src/app/pages/apps-routes/app-restore/app-restore.module.ts +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.module.ts @@ -1,17 +1,16 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' -import { AppRestorePage } from './app-restore.page' import { RouterModule, Routes } from '@angular/router' +import { SessionsPage } from './sessions.page' import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' -import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module' import { SharingModule } from 'src/app/modules/sharing.module' import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module' const routes: Routes = [ { path: '', - component: AppRestorePage, + component: SessionsPage, }, ] @@ -19,14 +18,11 @@ const routes: Routes = [ imports: [ CommonModule, IonicModule, - SharingModule, RouterModule.forChild(routes), - BackupConfirmationComponentModule, PwaBackComponentModule, + SharingModule, TextSpinnerComponentModule, ], - declarations: [ - AppRestorePage, - ], + declarations: [SessionsPage], }) -export class AppRestorePageModule { } \ No newline at end of file +export class SessionsPageModule { } diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html new file mode 100644 index 000000000..331171c7a --- /dev/null +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.html @@ -0,0 +1,41 @@ + + + + + + Active Sessions + + + + + + + + + Current Session + + + +

{{ getPlatformName(current.metadata.platforms) }}

+

Last Active: {{ current['last-active'] | date : 'medium' }}

+

{{ current['user-agent'] }}

+
+
+ + Other Sessions +
+ + + +

{{ getPlatformName(session.value.metadata.platforms) }}

+

Last Active: {{ session.value['last-active'] | date : 'medium' }}

+

{{ session.value['user-agent'] }}

+
+ + + +
+
+
+ +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.scss b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts new file mode 100644 index 000000000..bfba3f89b --- /dev/null +++ b/ui/src/app/pages/server-routes/security-routes/sessions/sessions.page.ts @@ -0,0 +1,99 @@ +import { Component } from '@angular/core' +import { AlertController, getPlatforms, LoadingController } from '@ionic/angular' +import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ApiService } from 'src/app/services/api/embassy/embassy-api.service' +import { PlatformType, RR, SessionMetadata } from 'src/app/services/api/api.types' + +@Component({ + selector: 'sessions', + templateUrl: 'sessions.page.html', + styleUrls: ['sessions.page.scss'], +}) +export class SessionsPage { + loading = true + sessionInfo: RR.GetSessionsRes + + constructor ( + private readonly loadingCtrl: LoadingController, + private readonly errToast: ErrorToastService, + private readonly alertCtrl: AlertController, + private readonly embassyApi: ApiService, + ) { } + + async ngOnInit () { + getPlatforms() + this.sessionInfo = await this.embassyApi.getSessions({ }) + this.loading = false + } + + async presentAlertKill (hash: string) { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Caution', + message: `Are you sure you want to kill this session?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Kill', + handler: () => { + this.kill(hash) + }, + }, + ], + }) + await alert.present() + } + + async kill (hash: string): Promise { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Killing session...', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.embassyApi.killSessions({ hashes: [hash] }) + delete this.sessionInfo.sessions[hash] + } catch (e) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + getPlatformIcon (platforms: PlatformType[]): string { + if (platforms.includes('cli')) { + return 'terminal-outline' + } else if (platforms.includes('desktop')) { + return 'desktop-outline' + } else { + return 'phone-portrait-outline' + } + } + + getPlatformName (platforms: PlatformType[]): string { + if (platforms.includes('cli')) { + return 'CLI' + } else if (platforms.includes('desktop')) { + return 'Desktop/Laptop' + } else if (platforms.includes('android')) { + return 'Android Device' + } else if (platforms.includes('iphone')) { + return 'iPhone' + } else if (platforms.includes('ipad')) { + return 'iPad' + } else if (platforms.includes('ios')) { + return 'iOS Device' + } else { + return 'Unknown Device' + } + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html index b851aa581..0572593d8 100644 --- a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html +++ b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.html @@ -22,7 +22,7 @@ {{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }} - + diff --git a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts index 662c16f81..7e2beeb3f 100644 --- a/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts +++ b/ui/src/app/pages/server-routes/security-routes/ssh-keys/ssh-keys.page.ts @@ -57,7 +57,6 @@ export class SSHKeysPage { }, { text: 'Delete', - cssClass: 'alert-danger', handler: () => { this.delete(hash) }, diff --git a/ui/src/app/pages/server-routes/server-logs/server-logs.page.html b/ui/src/app/pages/server-routes/server-logs/server-logs.page.html index 6747d77a8..dd90d13d2 100644 --- a/ui/src/app/pages/server-routes/server-logs/server-logs.page.html +++ b/ui/src/app/pages/server-routes/server-logs/server-logs.page.html @@ -12,7 +12,7 @@ - + diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html index 895b2bb6c..082c4e4cf 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -7,16 +7,14 @@ - + - {{ metricGroup.key }} + {{ metricGroup.key }} - - {{ metric.key }} - + {{ metric.key }} {{ metric.value.value }} {{ metric.value.unit }} diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss index eea898305..bee398a1b 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss @@ -1,3 +1,3 @@ .metric-note { font-size: 16px; -} \ No newline at end of file +} diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.html b/ui/src/app/pages/server-routes/server-show/server-show.page.html index 872b39284..b33060521 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -7,12 +7,11 @@ - - +
- {{ cat.key }} - + {{ cat.key }} + {{ button.title }} diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/ui/src/app/pages/server-routes/server-show/server-show.page.ts index b0d33d0a8..c2abedb70 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -37,7 +37,6 @@ export class ServerShowPage { }, { text: 'Restart', - cssClass: 'alert-danger', handler: () => { this.restart() }, @@ -59,7 +58,6 @@ export class ServerShowPage { }, { text: 'Shutdown', - cssClass: 'alert-danger', handler: () => { this.shutdown() }, @@ -110,16 +108,19 @@ export class ServerShowPage { title: 'Privacy and Security', icon: 'shield-checkmark-outline', action: () => this.navCtrl.navigateForward(['security'], { relativeTo: this.route }), + detail: true, }, { title: 'LAN', icon: 'home-outline', action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), + detail: true, }, { title: 'WiFi', icon: 'wifi', action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + detail: true, }, ], 'Insights': [ @@ -127,16 +128,19 @@ export class ServerShowPage { title: 'About', icon: 'information-circle-outline', action: () => this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), + detail: true, }, { title: 'Monitor', icon: 'pulse', action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + detail: true, }, { title: 'Logs', icon: 'newspaper-outline', action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), + detail: true, }, ], 'Backups': [ @@ -144,6 +148,7 @@ export class ServerShowPage { title: 'Create Backup', icon: 'save-outline', action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }), + detail: true, }, ], 'Power': [ @@ -151,11 +156,13 @@ export class ServerShowPage { title: 'Restart', icon: 'reload-outline', action: () => this.presentAlertRestart(), + detail: false, }, { title: 'Shutdown', icon: 'power', action: () => this.presentAlertShutdown(), + detail: false, }, ], } @@ -171,5 +178,6 @@ interface ServerSettings { title: string icon: string action: Function + detail: boolean }[] } diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index 7246ce56b..110a56c29 100644 --- a/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -14,7 +14,7 @@ -

Version

+

EmbassyOS Version

{{ server.version | displayEmver }}

diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 14a82d7db..c4614c2a9 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -29,7 +29,6 @@ export class WifiListPage { const buttons: ActionSheetButton[] = [ { text: 'Forget', - cssClass: 'alert-danger', handler: () => { this.delete(ssid) }, diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index 70c9d03a8..ff8987501 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -739,6 +739,26 @@ export module Mock { }, ] + export const Sessions: RR.GetSessionsRes = { + current: 'b7b1a9cef4284f00af9e9dda6e676177', + sessions: { + '9513226517c54ddd8107d6d7b9d8aed7': { + 'last-active': '2021-07-14T20:49:17.774Z', + 'user-agent': 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', + metadata: { + platforms: ['iphone', 'mobileweb', 'mobile', 'ios'], + }, + }, + 'b7b1a9cef4284f00af9e9dda6e676177': { + 'last-active': '2021-06-14T20:49:17.774Z', + 'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', + metadata: { + platforms: ['desktop'], + }, + }, + }, + } + export const SshKeys: RR.GetSSHKeysRes = { '28:d2:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53': { alg: 'ed25519', diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index f452fae9e..6e25cd8d5 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -16,7 +16,7 @@ export module RR { // auth - export type LoginReq = { password: string } // auth.login - unauthed + export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed export type loginRes = null export type LogoutReq = { } // auth.logout @@ -47,6 +47,17 @@ export module RR { export type RefreshLanReq = { } // network.lan.refresh export type RefreshLanRes = null + // sessions + + export type GetSessionsReq = { } // sessions.list + export type GetSessionsRes = { + current: string, + sessions: { [hash: string]: Session } + } + + export type KillSessionsReq = WithExpire<{ hashes: string[] }> // sessions.kill + export type KillSessionsRes = WithRevision + // marketplace URLs export type SetEosMarketplaceReq = WithExpire<{ url: string }> // marketplace.eos.set @@ -253,6 +264,18 @@ export interface Metric { } } +export interface Session { + 'last-active': string + 'user-agent': string + metadata: SessionMetadata +} + +export interface SessionMetadata { + platforms: PlatformType[] +} + +export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phablet' | 'tablet' | 'cordova' | 'capacitor' | 'electron' | 'pwa' | 'mobile' | 'mobileweb' | 'desktop' | 'hybrid' + export interface DiskInfo { [id: string]: DiskInfoEntry } diff --git a/ui/src/app/services/api/embassy/embassy-api.service.ts b/ui/src/app/services/api/embassy/embassy-api.service.ts index eadbe58a7..fcb27b42a 100644 --- a/ui/src/app/services/api/embassy/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-api.service.ts @@ -32,6 +32,10 @@ export abstract class ApiService implements Source, Http { abstract logout (params: RR.LogoutReq): Promise + abstract getSessions (params: RR.GetSessionsReq): Promise + + abstract killSessions (params: RR.KillSessionsReq): Promise + // server protected abstract setShareStatsRaw (params: RR.SetShareStatsReq): Promise diff --git a/ui/src/app/services/api/embassy/embassy-live-api.service.ts b/ui/src/app/services/api/embassy/embassy-live-api.service.ts index fa942e58b..1c395798e 100644 --- a/ui/src/app/services/api/embassy/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-live-api.service.ts @@ -41,6 +41,14 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'auth.logout', params }) } + async getSessions (params: RR.GetSessionsReq): Promise { + return this.http.rpcRequest({ method: 'auth.session.list', params }) + } + + async killSessions (params: RR.KillSessionsReq): Promise { + return this.http.rpcRequest({ method: 'auth.session.kill', params }) + } + // server async setShareStatsRaw (params: RR.SetShareStatsReq): Promise { diff --git a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts index 6e29b7ddc..97aebdc9d 100644 --- a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts @@ -50,6 +50,16 @@ export class MockApiService extends ApiService { return null } + async getSessions (params: RR.GetSessionsReq): Promise { + await pauseFor(2000) + return Mock.Sessions + } + + async killSessions (params: RR.KillSessionsReq): Promise { + await pauseFor(2000) + return null + } + // server async setShareStatsRaw (params: RR.SetShareStatsReq): Promise { diff --git a/ui/src/app/services/auth.service.ts b/ui/src/app/services/auth.service.ts index 12136c207..248886f3f 100644 --- a/ui/src/app/services/auth.service.ts +++ b/ui/src/app/services/auth.service.ts @@ -3,6 +3,7 @@ import { BehaviorSubject, Observable } from 'rxjs' import { distinctUntilChanged } from 'rxjs/operators' import { ApiService } from './api/embassy/embassy-api.service' import { Storage } from '@ionic/storage' +import { getPlatforms, isPlatform } from '@ionic/angular' export enum AuthState { UNVERIFIED, @@ -31,7 +32,10 @@ export class AuthService { } async login (password: string): Promise { - await this.embassyApi.login({ password }) + await this.embassyApi.login({ + password, + metadata: { platforms: getPlatforms() }, + }) await this.storage.set(this.LOGGED_IN_KEY, true) this.authState$.next(AuthState.VERIFIED) } diff --git a/ui/src/app/services/config.service.ts b/ui/src/app/services/config.service.ts index 1f2727448..adb429747 100644 --- a/ui/src/app/services/config.service.ts +++ b/ui/src/app/services/config.service.ts @@ -67,6 +67,7 @@ export class ConfigService { } launchableURL (pkg: PackageDataEntry): string { + console.log('PKGPKGPKG', pkg) return this.isTor() ? `http://${torUiAddress(pkg)}` : `https://${lanUiAddress(pkg)}` } } @@ -85,7 +86,7 @@ export function torUiAddress (pkg: PackageDataEntry): string { const val = interfaces[key] return val.ui && val['tor-config'] }) - return pkg['interface-info'].addresses[id]['tor-address'] + return pkg.installed['interface-info'].addresses[id]['tor-address'] } export function lanUiAddress (pkg: PackageDataEntry): string { @@ -94,7 +95,7 @@ export function lanUiAddress (pkg: PackageDataEntry): string { const val = interfaces[key] return val.ui && val['lan-config'] }) - return pkg['interface-info'].addresses[id]['lan-address'] + return pkg.installed['interface-info'].addresses[id]['lan-address'] } export function hasUi (interfaces: { [id: string]: InterfaceDef }): boolean { diff --git a/ui/src/assets/img/icon.png b/ui/src/assets/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..928a04921704cd80d28280f1895d0b843b9f5476 GIT binary patch literal 16380 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 Hb