use hostname from patchDB as default server name (#1758)

* replace offline toast with global indicator

* use hostname from patchDB as default server name

* add alert to marketplace delete and reword logout alert
This commit is contained in:
Matt Hill
2022-08-29 14:59:09 -06:00
committed by GitHub
parent 8cd2fac9b9
commit 705653465a
27 changed files with 247 additions and 184 deletions

View File

@@ -18,5 +18,10 @@
<ion-footer>
<footer appFooter></footer>
</ion-footer>
<ion-footer
*ngIf="(authService.isVerified$ | async) && !(sidebarOpen$ | async)"
>
<connection-bar></connection-bar>
</ion-footer>
<toast-container></toast-container>
</ion-app>

View File

@@ -12,6 +12,7 @@ import { PatchMonitorService } from './services/patch-monitor.service'
})
export class AppComponent implements OnDestroy {
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
constructor(
private readonly patchData: PatchDataService,

View File

@@ -19,6 +19,7 @@ import { EnterModule } from './app/enter/enter.module'
import { APP_PROVIDERS } from './app.providers'
import { PatchDbModule } from './services/patch-db/patch-db.module'
import { ToastContainerModule } from './components/toast-container/toast-container.module'
import { ConnectionBarComponentModule } from './components/connection-bar/connection-bar.component.module'
@NgModule({
declarations: [AppComponent],
@@ -47,6 +48,7 @@ import { ToastContainerModule } from './components/toast-container/toast-contain
MarketplaceModule,
PatchDbModule,
ToastContainerModule,
ConnectionBarComponentModule,
],
providers: APP_PROVIDERS,
bootstrap: [AppComponent],

View File

@@ -49,6 +49,7 @@
</ion-item>
</ion-menu-toggle>
</ion-item-group>
<img
appSnek
class="snek"
@@ -56,21 +57,6 @@
src="assets/img/icons/snek.png"
[appSnekHighScore]="snekScore$ | async"
/>
<div class="bottom">
<div class="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false">
<ion-item
button
lines="none"
style="--background: transparent; margin-bottom: 86px; text-align: center"
fill="clear"
(click)="presentAlertLogout()"
>
<ion-label class="inline">
<h2>Log Out</h2>
&nbsp;
<ion-icon name="log-out-outline"></ion-icon>
</ion-label>
</ion-item>
</ion-menu-toggle>
</div>
<ion-footer class="bottom">
<connection-bar></connection-bar>
</ion-footer>

View File

@@ -27,8 +27,8 @@
.snek {
position: absolute;
bottom: 90px;
left: 20px;
bottom: 56px;
right: 20px;
width: 20px;
cursor: pointer;
}
@@ -36,8 +36,4 @@
.bottom {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 75px;
text-align: center;
}

View File

@@ -1,9 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { AlertController } from '@ionic/angular'
import { LocalStorageService } from '../../services/local-storage.service'
import { EOSService } from '../../services/eos.service'
import { ApiService } from '../../services/api/embassy-api.service'
import { AuthService } from '../../services/auth.service'
import { PatchDbService } from '../../services/patch-db/patch-db.service'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
@@ -61,40 +58,10 @@ export class MenuComponent {
.pipe(map(pkgs => pkgs.length))
constructor(
private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService,
private readonly authService: AuthService,
private readonly patch: PatchDbService,
private readonly localStorageService: LocalStorageService,
private readonly eosService: EOSService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}
async presentAlertLogout() {
const alert = await this.alertCtrl.create({
header: 'Caution',
message:
'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Logout',
handler: () => this.logout(),
cssClass: 'enter-click',
},
],
})
await alert.present()
}
// should wipe cache independent of actual BE logout
private logout() {
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
this.authService.setUnverified()
}
}

View File

@@ -4,9 +4,16 @@ import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { MenuComponent } from './menu.component'
import { SnekModule } from '../snek/snek.module'
import { ConnectionBarComponentModule } from 'src/app/components/connection-bar/connection-bar.component.module'
@NgModule({
imports: [CommonModule, IonicModule, RouterModule, SnekModule],
imports: [
CommonModule,
IonicModule,
RouterModule,
SnekModule,
ConnectionBarComponentModule,
],
declarations: [MenuComponent],
exports: [MenuComponent],
})

View File

@@ -19,6 +19,7 @@ const ICONS = [
'chevron-forward',
'close',
'cloud-outline',
'cloud-done',
'cloud-done-outline',
'cloud-download-outline',
'cloud-offline-outline',

View File

@@ -0,0 +1,21 @@
<ion-toolbar
*ngIf="connection$ | async as connection"
class="connection-toolbar"
[color]="connection.color"
>
<div class="inline" slot="start">
<ion-icon
slot="end"
[name]="connection.icon"
class="icon"
color="light"
></ion-icon>
<p style="margin: 8px 0; font-weight: 600">{{ connection.message }}</p>
<ion-spinner
*ngIf="connection.dots"
name="dots"
color="light"
class="ion-margin-start"
></ion-spinner>
</div>
</ion-toolbar>

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { ConnectionBarComponent } from './connection-bar.component'
@NgModule({
declarations: [ConnectionBarComponent],
imports: [CommonModule, IonicModule],
exports: [ConnectionBarComponent],
})
export class ConnectionBarComponentModule {}

View File

@@ -0,0 +1,9 @@
.connection-toolbar {
padding: 0 24px;
--min-height: 36px;
}
.icon {
font-size: 23px;
padding-right: 12px;
}

View File

@@ -0,0 +1,53 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { combineLatest, map, Observable, startWith, tap } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'connection-bar',
templateUrl: './connection-bar.component.html',
styleUrls: ['./connection-bar.component.scss'],
// changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConnectionBarComponent {
private readonly websocket$ = this.connectionService.websocketConnected$
readonly connection$: Observable<{
message: string
icon: string
color: string
dots: boolean
}> = combineLatest([
this.connectionService.networkConnected$,
this.websocket$,
]).pipe(
map(([network, websocket]) => {
if (!network)
return {
message: 'No Internet',
icon: 'cloud-offline-outline',
color: 'dark',
dots: false,
}
if (!websocket)
return {
message: 'Connecting',
icon: 'cloud-offline-outline',
color: 'warning',
dots: true,
}
return {
message: 'Connected',
icon: 'cloud-done',
color: 'success',
dots: false,
}
}),
)
constructor(
private readonly connectionService: ConnectionService,
private readonly patch: PatchDbService,
) {}
}

View File

@@ -1,19 +0,0 @@
<toast
*ngIf="content$ | async as content"
class="warning-toast"
header="Unable to Connect"
(dismiss)="onDismiss()"
>
{{ content.message }}
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
<a
*ngIf="content.link"
toastButton
side="end"
target="_blank"
rel="noreferrer"
[href]="content.link"
>
View solutions
</a>
</toast>

View File

@@ -1,24 +0,0 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { Observable, Subject, merge, tap, map } from 'rxjs'
import { OfflineMessage, OfflineToastService } from './offline-toast.service'
@Component({
selector: 'offline-toast',
templateUrl: './offline-toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OfflineToastComponent {
private readonly dismiss$ = new Subject<null>()
readonly content$ = merge(this.dismiss$, this.failure$)
constructor(
@Inject(OfflineToastService)
private readonly failure$: Observable<OfflineMessage | null>,
) {}
onDismiss() {
this.dismiss$.next(null)
}
}

View File

@@ -1,40 +0,0 @@
import { Injectable } from '@angular/core'
import { combineLatest, Observable, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { AuthService } from 'src/app/services/auth.service'
import { ConnectionService } from 'src/app/services/connection.service'
export interface OfflineMessage {
readonly message: string
readonly link?: string
}
// Watch for connection status
@Injectable({ providedIn: 'root' })
export class OfflineToastService extends Observable<OfflineMessage | null> {
private readonly stream$ = this.authService.isVerified$.pipe(
switchMap(verified => (verified ? this.failure$ : of(null))),
)
private readonly failure$ = combineLatest([
this.connectionService.networkConnected$,
this.connectionService.websocketConnected$,
]).pipe(
map(([network, websocket]) => {
if (!network) return { message: 'No Internet' }
if (!websocket)
return {
message: 'Connecting to Embassy...',
link: 'https://start9.com/latest/support/common-issues',
}
return null
}),
)
constructor(
private readonly authService: AuthService,
private readonly connectionService: ConnectionService,
) {
super(subscriber => this.stream$.subscribe(subscriber))
}
}

View File

@@ -1,4 +1,3 @@
<notifications-toast></notifications-toast>
<offline-toast></offline-toast>
<refresh-alert></refresh-alert>
<update-toast></update-toast>

View File

@@ -5,7 +5,6 @@ import { AlertModule, ToastModule } from '@start9labs/shared'
import { ToastContainerComponent } from './toast-container.component'
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'
import { OfflineToastComponent } from './offline-toast/offline-toast.component'
import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component'
import { UpdateToastComponent } from './update-toast/update-toast.component'
@@ -14,7 +13,6 @@ import { UpdateToastComponent } from './update-toast/update-toast.component'
declarations: [
ToastContainerComponent,
NotificationsToastComponent,
OfflineToastComponent,
RefreshAlertComponent,
UpdateToastComponent,
],

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { Component } from '@angular/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
ServerNotifications,

View File

@@ -1,6 +1,7 @@
import { Component, Inject } from '@angular/core'
import {
ActionSheetController,
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
@@ -51,6 +52,7 @@ export class MarketplacesPage {
private readonly config: ConfigService,
private readonly patch: PatchDbService,
private readonly destroy$: DestroyService,
private readonly alertCtrl: AlertController,
) {}
ngOnInit() {
@@ -129,7 +131,7 @@ export class MarketplacesPage {
text: 'Delete',
role: 'destructive',
handler: () => {
this.delete(id)
this.presentAlertDelete(id)
},
})
}
@@ -189,6 +191,28 @@ export class MarketplacesPage {
.subscribe()
}
private async presentAlertDelete(id: string) {
const name = this.marketplaces.find(m => m.id === id)?.name
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: `Are you sure you want to delete ${name}?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Delete',
handler: () => this.delete(id),
cssClass: 'enter-click',
},
],
})
await alert.present()
}
private async delete(id: string): Promise<void> {
const data = await getMarketplace(this.patch)
const marketplace: UIMarketplaceData = JSON.parse(JSON.stringify(data))

View File

@@ -8,24 +8,21 @@
</ion-header>
<ion-content class="ion-padding-top">
<ng-container *ngIf="ui$ | async as ui">
<ion-item-group *ngIf="server$ | async as server">
<ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName('My Embassy', ui.name)">
<ion-label>Device Name</ion-label>
<ion-note slot="end">{{ ui.name || 'My Embassy' }}</ion-note>
</ion-item>
<ion-item-group *ngIf="name$ | async as name">
<ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName(name)">
<ion-label>Device Name</ion-label>
<ion-note slot="end">{{ name.current }}</ion-note>
</ion-item>
<ion-item-divider>Marketplace</ion-item-divider>
<ion-item
button
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)"
>
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">
{{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }}
</ion-note>
</ion-item>
</ion-item-group>
</ng-container>
<ion-item-divider>Marketplace</ion-item-divider>
<ion-item
*ngIf="autoCheck$ | async as auto"
button
(click)="serverConfig.presentAlert('auto-check-updates', auto)"
>
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end"> {{ auto ? 'Enabled' : 'Disabled' }} </ion-note>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -12,6 +12,10 @@ import {
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { LocalStorageService } from '../../../services/local-storage.service'
import {
ServerNameInfo,
ServerNameService,
} from 'src/app/services/server-name.service'
@Component({
selector: 'preferences',
@@ -22,8 +26,9 @@ import { LocalStorageService } from '../../../services/local-storage.service'
export class PreferencesPage {
clicks = 0
readonly ui$ = this.patch.watch$('ui')
readonly autoCheck$ = this.patch.watch$('ui', 'auto-check-updates')
readonly server$ = this.patch.watch$('server-info')
readonly name$ = this.serverNameService.name$
constructor(
private readonly loadingCtrl: LoadingController,
@@ -32,24 +37,22 @@ export class PreferencesPage {
private readonly toastCtrl: ToastController,
private readonly localStorageService: LocalStorageService,
private readonly patch: PatchDbService,
private readonly serverNameService: ServerNameService,
readonly serverConfig: ServerConfigService,
) {}
async presentModalName(
placeholder: string,
initialValue: string,
): Promise<void> {
async presentModalName(name: ServerNameInfo): Promise<void> {
const options: GenericInputOptions = {
title: 'Edit Device Name',
message: 'This is for your reference only.',
label: 'Device Name',
useMask: false,
placeholder,
placeholder: name.default,
nullable: true,
initialValue,
initialValue: name.current,
buttonText: 'Save',
submitFn: (value: string) =>
this.setDbValue('name', value || placeholder),
this.setDbValue('name', value || name.default),
}
const modal = await this.modalCtrl.create({

View File

@@ -1,7 +1,7 @@
<ion-header>
<ion-toolbar>
<ion-title *ngIf="ui$ | async as ui; else loadingTitle">
{{ ui.name || "My Embassy" }}
<ion-title *ngIf="name$ | async as name; else loadingTitle">
{{ name.current }}
</ion-title>
<ng-template #loadingTitle>
<ion-title>

View File

@@ -8,6 +8,7 @@ import {
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActivatedRoute } from '@angular/router'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ServerNameService } from 'src/app/services/server-name.service'
import { Observable, of } from 'rxjs'
import { filter, take, tap } from 'rxjs/operators'
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
@@ -15,6 +16,7 @@ import { EOSService } from 'src/app/services/eos.service'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
import { getAllPackages } from '../../../util/get-package-data'
import { AuthService } from 'src/app/services/auth.service'
@Component({
selector: 'server-show',
@@ -26,7 +28,7 @@ export class ServerShowPage {
clicks = 0
readonly server$ = this.patch.watch$('server-info')
readonly ui$ = this.patch.watch$('ui')
readonly name$ = this.serverNameService.name$
readonly showUpdate$ = this.eosService.showUpdate$
readonly showDiskRepair$ = this.localStorageService.showDiskRepair$
@@ -41,6 +43,8 @@ export class ServerShowPage {
private readonly patch: PatchDbService,
private readonly eosService: EOSService,
private readonly localStorageService: LocalStorageService,
private readonly serverNameService: ServerNameService,
private readonly authService: AuthService,
) {}
ngOnInit() {
@@ -74,6 +78,26 @@ export class ServerShowPage {
}
}
async presentAlertLogout() {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Are you sure you want to log out?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Logout',
handler: () => this.logout(),
cssClass: 'enter-click',
},
],
})
await alert.present()
}
async presentAlertRestart() {
const alert = await this.alertCtrl.create({
header: 'Restart',
@@ -171,6 +195,12 @@ export class ServerShowPage {
await alert.present()
}
// should wipe cache independent of actual BE logout
private logout() {
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
this.authService.setUnverified()
}
private async restart() {
const action = 'Restart'
@@ -456,6 +486,14 @@ export class ServerShowPage {
},
],
Power: [
{
title: 'Log Out',
description: '',
icon: 'log-out-outline',
action: () => this.presentAlertLogout(),
detail: false,
disabled$: of(false),
},
{
title: 'Restart',
description: '',

View File

@@ -95,18 +95,14 @@ export class SideloadPage {
manifest: this.toUpload.manifest!,
icon: this.toUpload.icon!,
})
this.api
.uploadPackage(guid, await blobToBuffer(this.toUpload.file!))
.catch(e => {
this.errToast.present(e)
})
const buffer = await blobToBuffer(this.toUpload.file!)
this.api.uploadPackage(guid, buffer).catch(e => console.error(e))
this.navCtrl.navigateRoot('/services')
} catch (e: any) {
this.errToast.present(e)
} finally {
loader.dismiss()
await this.navCtrl.navigateForward(
`/services/${this.toUpload.manifest!.id}`,
)
this.clearToUpload()
}
}

View File

@@ -41,6 +41,7 @@ export const mockPatchData: DataModel = {
updated: false,
'update-progress': null,
},
hostname: 'random-words',
},
'recovered-packages': {
'btc-rpc-proxy': {

View File

@@ -56,6 +56,7 @@ export interface ServerInfo {
'status-info': ServerStatusInfo
'eos-version-compat': string
'password-hash': string
hostname: string
}
export interface ServerStatusInfo {

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core'
import { PatchDbService } from './patch-db/patch-db.service'
import { combineLatest, filter, map, Observable } from 'rxjs'
export interface ServerNameInfo {
current: string
default: string
}
@Injectable({ providedIn: 'root' })
export class ServerNameService {
private readonly chosenName$ = this.patch.watch$('ui', 'name')
private readonly hostname$ = this.patch
.watch$('server-info', 'hostname')
.pipe(filter(Boolean))
readonly name$: Observable<ServerNameInfo> = combineLatest([
this.chosenName$,
this.hostname$,
]).pipe(
map(([chosen, hostname]) => {
return {
current: chosen || hostname,
default: hostname,
}
}),
)
constructor(private readonly patch: PatchDbService) {}
}