mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
refactor: isolate network toast and login redirect to separate services (#1412)
* refactor: isolate network toast and login redirect to separate services * chore: remove accidentally committed sketch of a service * chore: tidying things up * feat: add `GlobalModule` encapsulating all global subscription services * remove angular build cache when building deps * chore: fix more issues found while testing * chore: fix issues reported by testing * chore: fix template error * chore: fix server-info * chore: fix server-info * fix: switch to Observable to fix race conditions * fix embassy name display on load * update patchdb * clean up patch data watch Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"check:diagnostic-ui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup-wizard": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"build:deps": "cd ../patch-db/client && npm install && npm run build",
|
||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm install && npm run build",
|
||||
"build:diagnostic-ui": "ng run diagnostic-ui:build",
|
||||
"build:setup-wizard": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build && tsc projects/ui/postprocess.ts && node projects/ui/postprocess.js && git log | head -n1 > dist/ui/git-hash.txt",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
|
||||
import { HttpError, RpcError } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -25,31 +26,6 @@ export class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError(e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
|
||||
if (typeof data === 'string') {
|
||||
this.message = `${message}\n\n${data}`
|
||||
} else {
|
||||
if (data.details) {
|
||||
this.message = `${message}\n\n${data.details}`
|
||||
} else {
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError(e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
this.revision = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result>(
|
||||
arg: { error: Error } | { result: Result },
|
||||
): arg is { error: Error } {
|
||||
@@ -102,7 +78,3 @@ export interface RPCError extends RPCBase {
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & {
|
||||
error: { code: string; message: string }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class AppComponent {
|
||||
this.stateService.isMigrating = false
|
||||
await this.navCtrl.navigateForward(`/recover`)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class EmbassyPage {
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
@@ -141,7 +141,7 @@ export class EmbassyPage {
|
||||
} else {
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present({
|
||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||
})
|
||||
|
||||
@@ -119,7 +119,7 @@ export class RecoverPage {
|
||||
await alert.present()
|
||||
this.hasShownGuidAlert = true
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
@@ -205,7 +205,7 @@ export class RecoverPage {
|
||||
try {
|
||||
await this.stateService.importDrive(guid)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -30,7 +30,7 @@ export class SuccessPage {
|
||||
encodeURIComponent(this.stateService.cert),
|
||||
)
|
||||
this.download()
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
await this.errCtrl.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { Observable } from 'rxjs'
|
||||
import * as aesjs from 'aes-js'
|
||||
import * as pbkdf2 from 'pbkdf2'
|
||||
import { HttpError, RpcError } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -80,7 +81,7 @@ export class HttpService {
|
||||
.then(res => JSON.parse(res))
|
||||
.catch(e => {
|
||||
if (!e.status && !e.statusText) {
|
||||
throw new EncryptionError(e)
|
||||
throw new EncryptionError()
|
||||
} else {
|
||||
throw new HttpError(e)
|
||||
}
|
||||
@@ -120,34 +121,10 @@ export class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError(e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
|
||||
if (typeof data === 'string') {
|
||||
this.message = `${message}\n\n${data}`
|
||||
} else {
|
||||
if (data.details) {
|
||||
this.message = `${message}\n\n${data.details}`
|
||||
} else {
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError(e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
}
|
||||
|
||||
function EncryptionError(e: HttpErrorResponse): void {
|
||||
this.code = null
|
||||
this.message = 'Invalid Key'
|
||||
this.details = null
|
||||
class EncryptionError {
|
||||
readonly code = null
|
||||
readonly message = 'Invalid Key'
|
||||
readonly details = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result>(
|
||||
@@ -205,10 +182,6 @@ export interface RPCError extends RPCBase {
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & {
|
||||
error: { code: string; message: string }
|
||||
}
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
|
||||
@@ -49,7 +49,7 @@ export class StateService {
|
||||
let progress
|
||||
try {
|
||||
progress = await this.apiService.getRecoveryStatus()
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present({
|
||||
message: `${e.message}\n\nRestart Embassy to try again.`,
|
||||
})
|
||||
|
||||
10
frontend/projects/shared/src/classes/http-error.ts
Normal file
10
frontend/projects/shared/src/classes/http-error.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
|
||||
export class HttpError {
|
||||
readonly code = this.error.status
|
||||
readonly message = this.error.statusText
|
||||
readonly details = null
|
||||
readonly revision = null
|
||||
|
||||
constructor(private readonly error: HttpErrorResponse) {}
|
||||
}
|
||||
25
frontend/projects/shared/src/classes/rpc-error.ts
Normal file
25
frontend/projects/shared/src/classes/rpc-error.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { RpcErrorDetails } from '../types/rpc-error-details'
|
||||
|
||||
export class RpcError<T> {
|
||||
readonly code = this.error.code
|
||||
readonly message = this.getMessage()
|
||||
readonly revision = this.getRevision()
|
||||
|
||||
constructor(private readonly error: RpcErrorDetails<T>) {}
|
||||
|
||||
private getMessage(): string {
|
||||
if (typeof this.error.data === 'string') {
|
||||
return `${this.error.message}\n\n${this.error.data}`
|
||||
}
|
||||
|
||||
return this.error.data.details
|
||||
? `${this.error.message}\n\n${this.error.data.details}`
|
||||
: this.error.message
|
||||
}
|
||||
|
||||
private getRevision(): T | null {
|
||||
return typeof this.error.data === 'string'
|
||||
? null
|
||||
: this.error.data.revision || null
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
* Public API Surface of @start9labs/shared
|
||||
*/
|
||||
|
||||
export * from './classes/http-error'
|
||||
export * from './classes/rpc-error'
|
||||
|
||||
export * from './components/markdown/markdown.component'
|
||||
export * from './components/markdown/markdown.module'
|
||||
export * from './components/text-spinner/text-spinner.component.module'
|
||||
@@ -27,6 +30,7 @@ export * from './services/destroy.service'
|
||||
export * from './services/emver.service'
|
||||
export * from './services/error-toast.service'
|
||||
|
||||
export * from './types/rpc-error-details'
|
||||
export * from './types/url'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
|
||||
10
frontend/projects/shared/src/types/rpc-error-details.ts
Normal file
10
frontend/projects/shared/src/types/rpc-error-details.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface RpcErrorDetails<T> {
|
||||
code: number
|
||||
message: string
|
||||
data?:
|
||||
| {
|
||||
details: string
|
||||
revision?: T | null
|
||||
}
|
||||
| string
|
||||
}
|
||||
@@ -14,14 +14,14 @@ export function capitalizeFirstLetter(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
export const exists = (t: any) => {
|
||||
export function exists<T>(t: T | undefined): t is T {
|
||||
return t !== undefined
|
||||
}
|
||||
|
||||
export function debounce(delay: number = 300): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
propertyKey: string | symbol,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const timeoutKey = Symbol()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<ion-app>
|
||||
<ion-app appEnter>
|
||||
<ion-content>
|
||||
<ion-split-pane
|
||||
contentId="main-content"
|
||||
[disabled]="!showMenu"
|
||||
[disabled]="!(authService.isVerified$ | async)"
|
||||
(ionSplitPaneVisible)="splitPaneVisible($event)"
|
||||
>
|
||||
<ion-menu contentId="main-content" type="overlay">
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
import { Component, HostListener, NgZone } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSafeString,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { ToastButton } from '@ionic/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import {
|
||||
debounce,
|
||||
isEmptyObject,
|
||||
Emver,
|
||||
ErrorToastService,
|
||||
} from '@start9labs/shared'
|
||||
import { Subscription } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
take,
|
||||
} from 'rxjs/operators'
|
||||
import { AuthService, AuthState } from './services/auth.service'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { Component } from '@angular/core'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDbService } from './services/patch-db/patch-db.service'
|
||||
import {
|
||||
ConnectionFailure,
|
||||
ConnectionService,
|
||||
} from './services/connection.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
import { EOSService } from './services/eos.service'
|
||||
import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -42,333 +8,12 @@ import { OSWelcomePage } from './modals/os-welcome/os-welcome.page'
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
showMenu = false
|
||||
offlineToast: HTMLIonToastElement
|
||||
updateToast: HTMLIonToastElement
|
||||
notificationToast: HTMLIonToastElement
|
||||
subscriptions: Subscription[] = []
|
||||
|
||||
constructor(
|
||||
private readonly storage: Storage,
|
||||
private readonly authService: AuthService,
|
||||
private readonly router: Router,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly zone: NgZone,
|
||||
readonly authService: AuthService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly localStorageService: LocalStorageService,
|
||||
private readonly eosService: EOSService,
|
||||
) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.enter', ['$event'])
|
||||
@debounce()
|
||||
handleKeyboardEvent() {
|
||||
const elems = document.getElementsByClassName('enter-click')
|
||||
const elem = elems[elems.length - 1] as HTMLButtonElement
|
||||
|
||||
if (elem && !elem.classList.contains('no-click') && !elem.disabled) {
|
||||
elem.click()
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
splitPaneVisible({ detail }: any) {
|
||||
this.splitPane.sidebarOpen$.next(detail.visible)
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.storage.create()
|
||||
await this.authService.init()
|
||||
await this.localStorageService.init()
|
||||
|
||||
this.router.initialNavigation()
|
||||
|
||||
// watch auth
|
||||
this.authService.watch$().subscribe(async auth => {
|
||||
// VERIFIED
|
||||
if (auth === AuthState.VERIFIED) {
|
||||
await this.patch.start()
|
||||
|
||||
this.showMenu = true
|
||||
// if on the login screen, route to dashboard
|
||||
if (this.router.url.startsWith('/login')) {
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
|
||||
this.subscriptions = this.subscriptions.concat([
|
||||
// start the connection monitor
|
||||
...this.connectionService.start(),
|
||||
// watch connection to display connectivity issues
|
||||
this.watchConnection(),
|
||||
])
|
||||
|
||||
this.patch
|
||||
.watch$()
|
||||
.pipe(
|
||||
filter(obj => !isEmptyObject(obj)),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(data => {
|
||||
// check for updates to EOS
|
||||
this.checkForEosUpdate(data.ui)
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(data.ui['ack-welcome'])
|
||||
|
||||
this.subscriptions = this.subscriptions.concat([
|
||||
// watch status to present toast for updated state
|
||||
this.watchStatus(),
|
||||
// watch version to refresh browser window
|
||||
this.watchVersion(),
|
||||
// watch unread notification count to display toast
|
||||
this.watchNotifications(),
|
||||
])
|
||||
})
|
||||
// UNVERIFIED
|
||||
} else if (auth === AuthState.UNVERIFIED) {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe())
|
||||
this.subscriptions = []
|
||||
this.showMenu = false
|
||||
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.zone.run(() => {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private checkForEosUpdate(ui: UIData): void {
|
||||
if (ui['auto-check-updates'] !== false) {
|
||||
this.eosService.getEOS()
|
||||
}
|
||||
}
|
||||
|
||||
private async showEosWelcome(ackVersion: string): Promise<void> {
|
||||
if (!this.config.skipStartupAlerts && ackVersion !== this.config.version) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: OSWelcomePage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
backdropDismiss: false,
|
||||
componentProps: {
|
||||
version: this.config.version,
|
||||
},
|
||||
})
|
||||
modal.onWillDismiss().then(() => {
|
||||
this.embassyApi
|
||||
.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
||||
.catch()
|
||||
})
|
||||
modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
private watchConnection(): Subscription {
|
||||
return this.connectionService
|
||||
.watchFailure$()
|
||||
.pipe(distinctUntilChanged(), debounceTime(500))
|
||||
.subscribe(async connectionFailure => {
|
||||
if (connectionFailure === ConnectionFailure.None) {
|
||||
if (this.offlineToast) {
|
||||
await this.offlineToast.dismiss()
|
||||
this.offlineToast = undefined
|
||||
}
|
||||
} else {
|
||||
let message: string | IonicSafeString
|
||||
let link: string
|
||||
switch (connectionFailure) {
|
||||
case ConnectionFailure.Network:
|
||||
message = 'Phone or computer has no network connection.'
|
||||
break
|
||||
case ConnectionFailure.Tor:
|
||||
message = 'Browser unable to connect over Tor.'
|
||||
link = 'https://start9.com/latest/support/common-issues'
|
||||
break
|
||||
case ConnectionFailure.Lan:
|
||||
message = 'Embassy not found on Local Area Network.'
|
||||
link = 'https://start9.com/latest/support/common-issues'
|
||||
break
|
||||
}
|
||||
await this.presentToastOffline(message, link)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private watchStatus(): Subscription {
|
||||
return this.patch
|
||||
.watch$('server-info', 'status-info', 'updated')
|
||||
.subscribe(isUpdated => {
|
||||
if (isUpdated && !this.updateToast) {
|
||||
this.presentToastUpdated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private watchVersion(): Subscription {
|
||||
return this.patch.watch$('server-info', 'version').subscribe(version => {
|
||||
if (this.emver.compare(this.config.version, version) !== 0) {
|
||||
this.presentAlertRefreshNeeded()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private watchNotifications(): Subscription {
|
||||
let previous: number
|
||||
return this.patch
|
||||
.watch$('server-info', 'unread-notification-count')
|
||||
.subscribe(count => {
|
||||
if (previous !== undefined && count > previous)
|
||||
this.presentToastNotifications()
|
||||
previous = count
|
||||
})
|
||||
}
|
||||
|
||||
private async presentAlertRefreshNeeded() {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: true,
|
||||
header: 'Refresh Needed',
|
||||
message:
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Refresh Page',
|
||||
cssClass: 'enter-click',
|
||||
handler: () => {
|
||||
location.reload()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async presentToastUpdated() {
|
||||
if (this.updateToast) return
|
||||
|
||||
this.updateToast = await this.toastCtrl.create({
|
||||
header: 'EOS download complete!',
|
||||
message:
|
||||
'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
|
||||
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() {
|
||||
if (this.notificationToast) return
|
||||
|
||||
this.notificationToast = await this.toastCtrl.create({
|
||||
header: 'Embassy',
|
||||
message: `New notifications`,
|
||||
position: 'bottom',
|
||||
duration: 4000,
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
side: 'end',
|
||||
text: 'View',
|
||||
handler: () => {
|
||||
this.router.navigate(['/notifications'], {
|
||||
queryParams: { toast: true },
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.notificationToast.present()
|
||||
}
|
||||
|
||||
private async presentToastOffline(
|
||||
message: string | IonicSafeString,
|
||||
link?: string,
|
||||
) {
|
||||
if (this.offlineToast) {
|
||||
this.offlineToast.message = message
|
||||
return
|
||||
}
|
||||
|
||||
let buttons: ToastButton[] = [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (link) {
|
||||
buttons.push({
|
||||
side: 'end',
|
||||
text: 'View solutions',
|
||||
handler: () => {
|
||||
window.open(link, '_blank', 'noreferrer')
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.offlineToast = await this.toastCtrl.create({
|
||||
header: 'Unable to Connect',
|
||||
cssClass: 'warning-toast',
|
||||
message,
|
||||
position: 'bottom',
|
||||
duration: 0,
|
||||
buttons,
|
||||
})
|
||||
await this.offlineToast.present()
|
||||
}
|
||||
|
||||
private async restart(): Promise<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
import { NgModule, ErrorHandler } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicModule, IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||
import { Drivers } from '@ionic/storage'
|
||||
import { IonicStorageModule, Storage } from '@ionic/storage-angular'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { Drivers } from '@ionic/storage'
|
||||
import { IonicStorageModule } from '@ionic/storage-angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import { MarkdownModule, SharedPipesModule } from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { PatchDbServiceFactory } from './services/patch-db/patch-db.factory'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
|
||||
import { PatchDbService } from './services/patch-db/patch-db.service'
|
||||
import { LocalStorageBootstrap } from './services/patch-db/local-storage-bootstrap'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import {
|
||||
MarkdownModule,
|
||||
SharedPipesModule,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { MarketplaceModule } from './marketplace.module'
|
||||
import { PreloaderModule } from './app/preloader/preloader.module'
|
||||
import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
|
||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
import { EnterModule } from './app/enter/enter.module'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { GlobalModule } from './app/global/global.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@@ -51,40 +38,16 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
MenuModule,
|
||||
PreloaderModule,
|
||||
FooterModule,
|
||||
EnterModule,
|
||||
OSWelcomePageModule,
|
||||
MarkdownModule,
|
||||
GenericInputComponentModule,
|
||||
MonacoEditorModule,
|
||||
SharedPipesModule,
|
||||
MarketplaceModule,
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
IonNav,
|
||||
{
|
||||
provide: RouteReuseStrategy,
|
||||
useClass: IonicRouteStrategy,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PatchDbService,
|
||||
useFactory: PatchDbServiceFactory,
|
||||
deps: [
|
||||
ConfigService,
|
||||
ApiService,
|
||||
LocalStorageBootstrap,
|
||||
AuthService,
|
||||
Storage,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler,
|
||||
},
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
65
frontend/projects/ui/src/app/app.providers.ts
Normal file
65
frontend/projects/ui/src/app/app.providers.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { APP_INITIALIZER, ErrorHandler, Provider } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { Router, RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import {
|
||||
PATCH_SOURCE,
|
||||
mockSourceFactory,
|
||||
realSourceFactory,
|
||||
} from './services/patch-db/patch-db.factory'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
|
||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
|
||||
export const APP_PROVIDERS: Provider[] = [
|
||||
FormBuilder,
|
||||
IonNav,
|
||||
{
|
||||
provide: RouteReuseStrategy,
|
||||
useClass: IonicRouteStrategy,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PATCH_SOURCE,
|
||||
deps: [ApiService, ConfigService, DOCUMENT],
|
||||
useFactory: useMocks ? mockSourceFactory : realSourceFactory,
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [Storage, AuthService, LocalStorageService, Router],
|
||||
useFactory: appInitializer,
|
||||
multi: true,
|
||||
},
|
||||
]
|
||||
|
||||
export function appInitializer(
|
||||
storage: Storage,
|
||||
auth: AuthService,
|
||||
localStorage: LocalStorageService,
|
||||
router: Router,
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
await storage.create()
|
||||
await auth.init()
|
||||
await localStorage.init()
|
||||
|
||||
router.initialNavigation()
|
||||
}
|
||||
}
|
||||
21
frontend/projects/ui/src/app/app/enter/enter.directive.ts
Normal file
21
frontend/projects/ui/src/app/app/enter/enter.directive.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Directive, HostListener, Inject } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { debounce } from '@start9labs/shared'
|
||||
|
||||
@Directive({
|
||||
selector: '[appEnter]',
|
||||
})
|
||||
export class EnterDirective {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
@HostListener('document:keydown.enter')
|
||||
@debounce()
|
||||
handleKeyboardEvent() {
|
||||
const elems = this.document.querySelectorAll('.enter-click')
|
||||
const elem = elems[elems.length - 1] as HTMLButtonElement
|
||||
|
||||
if (elem && !elem.classList.contains('no-click') && !elem.disabled) {
|
||||
elem.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
frontend/projects/ui/src/app/app/enter/enter.module.ts
Normal file
9
frontend/projects/ui/src/app/app/enter/enter.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { EnterDirective } from './enter.directive'
|
||||
|
||||
@NgModule({
|
||||
declarations: [EnterDirective],
|
||||
exports: [EnterDirective],
|
||||
})
|
||||
export class EnterModule {}
|
||||
64
frontend/projects/ui/src/app/app/global/global.module.ts
Normal file
64
frontend/projects/ui/src/app/app/global/global.module.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ClassProvider,
|
||||
ExistingProvider,
|
||||
Inject,
|
||||
InjectionToken,
|
||||
NgModule,
|
||||
OnDestroy,
|
||||
Type,
|
||||
} from '@angular/core'
|
||||
import { merge, Observable } from 'rxjs'
|
||||
import { OfflineService } from './services/offline.service'
|
||||
import { LogoutService } from './services/logout.service'
|
||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
import { ConnectionMonitorService } from './services/connection-monitor.service'
|
||||
import { UnreadToastService } from './services/unread-toast.service'
|
||||
import { RefreshToastService } from './services/refresh-toast.service'
|
||||
import { UpdateToastService } from './services/update-toast.service'
|
||||
|
||||
const GLOBAL_SERVICE = new InjectionToken<readonly Observable<unknown>[]>(
|
||||
'A multi token of global Observable services',
|
||||
)
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
[
|
||||
ConnectionMonitorService,
|
||||
LogoutService,
|
||||
OfflineService,
|
||||
RefreshToastService,
|
||||
UnreadToastService,
|
||||
UpdateToastService,
|
||||
].map(useClass),
|
||||
[PatchDataService, PatchMonitorService].map(useExisting),
|
||||
],
|
||||
})
|
||||
export class GlobalModule implements OnDestroy {
|
||||
readonly subscription = merge(...this.services).subscribe()
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_SERVICE)
|
||||
private readonly services: readonly Observable<unknown>[],
|
||||
) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
function useClass(useClass: Type<unknown>): ClassProvider {
|
||||
return {
|
||||
provide: GLOBAL_SERVICE,
|
||||
multi: true,
|
||||
useClass,
|
||||
}
|
||||
}
|
||||
|
||||
function useExisting(useExisting: Type<unknown>): ExistingProvider {
|
||||
return {
|
||||
provide: GLOBAL_SERVICE,
|
||||
multi: true,
|
||||
useExisting,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { EMPTY, Observable } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
|
||||
import { PatchMonitorService } from './patch-monitor.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
// Start connection monitor upon PatchDb start
|
||||
@Injectable()
|
||||
export class ConnectionMonitorService extends Observable<unknown> {
|
||||
private readonly stream$ = this.patchMonitor.pipe(
|
||||
switchMap(started => (started ? this.connectionService.start() : EMPTY)),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { filter, tap } from 'rxjs/operators'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
|
||||
// Redirect to login page upon broken authorization
|
||||
@Injectable()
|
||||
export class LogoutService extends Observable<unknown> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
filter(verified => !verified),
|
||||
tap(() => {
|
||||
this.zone.run(() => {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly zone: NgZone,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular'
|
||||
import { ToastButton } from '@ionic/core'
|
||||
import { EMPTY, from, Observable } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import {
|
||||
ConnectionFailure,
|
||||
ConnectionService,
|
||||
} from 'src/app/services/connection.service'
|
||||
|
||||
// Watch for connection status
|
||||
@Injectable()
|
||||
export class OfflineService extends Observable<unknown> {
|
||||
private current?: HTMLIonToastElement
|
||||
|
||||
private readonly connection$ = this.connectionService
|
||||
.watchFailure$()
|
||||
.pipe(distinctUntilChanged(), debounceTime(500))
|
||||
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
// Close on logout
|
||||
tap(() => this.current?.dismiss()),
|
||||
switchMap(verified => (verified ? this.connection$ : EMPTY)),
|
||||
// Close on change to connection state
|
||||
tap(() => this.current?.dismiss()),
|
||||
filter(connection => connection !== ConnectionFailure.None),
|
||||
map(getMessage),
|
||||
switchMap(({ message, link }) =>
|
||||
this.getToast().pipe(
|
||||
tap(toast => {
|
||||
this.current = toast
|
||||
|
||||
toast.message = message
|
||||
toast.buttons = getButtons(link)
|
||||
toast.present()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private getToast(): Observable<HTMLIonToastElement> {
|
||||
return from(this.toastCtrl.create(TOAST))
|
||||
}
|
||||
}
|
||||
|
||||
const TOAST: ToastOptions = {
|
||||
header: 'Unable to Connect',
|
||||
cssClass: 'warning-toast',
|
||||
message: '',
|
||||
position: 'bottom',
|
||||
duration: 0,
|
||||
buttons: [],
|
||||
}
|
||||
|
||||
function getMessage(failure: ConnectionFailure): OfflineMessage {
|
||||
switch (failure) {
|
||||
case ConnectionFailure.Network:
|
||||
return { message: 'Phone or computer has no network connection.' }
|
||||
case ConnectionFailure.Tor:
|
||||
return {
|
||||
message: 'Browser unable to connect over Tor.',
|
||||
link: 'https://start9.com/latest/support/common-issues',
|
||||
}
|
||||
case ConnectionFailure.Lan:
|
||||
return {
|
||||
message: 'Embassy not found on Local Area Network.',
|
||||
link: 'https://start9.com/latest/support/common-issues',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getButtons(link?: string): ToastButton[] {
|
||||
const buttons: ToastButton[] = [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => true,
|
||||
},
|
||||
]
|
||||
|
||||
if (link) {
|
||||
buttons.push({
|
||||
side: 'end',
|
||||
text: 'View solutions',
|
||||
handler: () => {
|
||||
window.open(link, '_blank', 'noreferrer')
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
interface OfflineMessage {
|
||||
readonly message: string | IonicSafeString
|
||||
readonly link?: string
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchMonitorService } from './patch-monitor.service'
|
||||
|
||||
// Get data from PatchDb after is starts and act upon it
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDataService extends Observable<DataModel | null> {
|
||||
private readonly stream$ = this.patchMonitor.pipe(
|
||||
switchMap(started =>
|
||||
started
|
||||
? this.patch.watch$().pipe(
|
||||
filter(obj => !isEmptyObject(obj)),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to EOS
|
||||
this.checkForEosUpdate(ui)
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui['ack-welcome'])
|
||||
}),
|
||||
)
|
||||
: of(null),
|
||||
),
|
||||
share(),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly storage: Storage,
|
||||
private readonly eosService: EOSService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private checkForEosUpdate(ui: UIData): void {
|
||||
if (ui['auto-check-updates'] !== false) {
|
||||
this.eosService.getEOS()
|
||||
}
|
||||
}
|
||||
|
||||
private async showEosWelcome(ackVersion: string): Promise<void> {
|
||||
if (this.config.skipStartupAlerts || ackVersion === this.config.version) {
|
||||
return
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: OSWelcomePage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
backdropDismiss: false,
|
||||
componentProps: {
|
||||
version: this.config.version,
|
||||
},
|
||||
})
|
||||
modal.onWillDismiss().then(() => {
|
||||
this.embassyApi
|
||||
.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
||||
.catch()
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { from, Observable, of } from 'rxjs'
|
||||
import { mapTo, share, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
// Start and stop PatchDb upon verification
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchMonitorService extends Observable<boolean> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
switchMap(verified => {
|
||||
if (verified) {
|
||||
return from(this.patch.start()).pipe(mapTo(true))
|
||||
}
|
||||
|
||||
this.patch.stop()
|
||||
this.storage.clear()
|
||||
|
||||
return of(false)
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly storage: Storage,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AlertController, AlertOptions } from '@ionic/angular'
|
||||
import { EMPTY, from, Observable } from 'rxjs'
|
||||
import { filter, switchMap } from 'rxjs/operators'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PatchDataService } from './patch-data.service'
|
||||
|
||||
// Watch version to refresh browser window
|
||||
@Injectable()
|
||||
export class RefreshToastService extends Observable<unknown> {
|
||||
private readonly stream$ = this.patchData.pipe(
|
||||
switchMap(data =>
|
||||
data ? this.patch.watch$('server-info', 'version') : EMPTY,
|
||||
),
|
||||
filter(version => !!this.emver.compare(this.config.version, version)),
|
||||
switchMap(() => this.getAlert()),
|
||||
switchMap(alert => alert.present()),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly emver: Emver,
|
||||
private readonly config: ConfigService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private getAlert(): Observable<HTMLIonAlertElement> {
|
||||
return from(this.alertCtrl.create(ALERT))
|
||||
}
|
||||
}
|
||||
|
||||
const ALERT: AlertOptions = {
|
||||
backdropDismiss: true,
|
||||
header: 'Refresh Needed',
|
||||
message:
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Refresh Page',
|
||||
cssClass: 'enter-click',
|
||||
handler: () => {
|
||||
location.reload()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
LoadingController,
|
||||
ToastController,
|
||||
ToastOptions,
|
||||
} from '@ionic/angular'
|
||||
import { EMPTY, merge, Observable } from 'rxjs'
|
||||
import { filter, pairwise, switchMap, tap } from 'rxjs/operators'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDataService } from './patch-data.service'
|
||||
|
||||
// Watch unread notification count to display toast
|
||||
@Injectable()
|
||||
export class UnreadToastService extends Observable<unknown> {
|
||||
private unreadToast: HTMLIonToastElement
|
||||
|
||||
private readonly stream$ = this.patchData.pipe(
|
||||
switchMap(data => {
|
||||
if (data) {
|
||||
return this.patch.watch$('server-info', 'unread-notification-count')
|
||||
}
|
||||
|
||||
this.unreadToast?.dismiss()
|
||||
|
||||
return EMPTY
|
||||
}),
|
||||
pairwise(),
|
||||
filter(([prev, cur]) => cur > prev),
|
||||
tap(() => {
|
||||
this.showToast()
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private async showToast() {
|
||||
await this.unreadToast?.dismiss()
|
||||
|
||||
this.unreadToast = await this.toastCtrl.create(TOAST)
|
||||
this.unreadToast.buttons.push({
|
||||
side: 'end',
|
||||
text: 'View',
|
||||
handler: () => {
|
||||
this.router.navigate(['/notifications'], {
|
||||
queryParams: { toast: true },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await this.unreadToast.present()
|
||||
}
|
||||
}
|
||||
|
||||
const TOAST: ToastOptions = {
|
||||
header: 'Embassy',
|
||||
message: `New notifications`,
|
||||
position: 'bottom',
|
||||
duration: 4000,
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
LoadingController,
|
||||
LoadingOptions,
|
||||
ToastController,
|
||||
ToastOptions,
|
||||
} from '@ionic/angular'
|
||||
import { EMPTY, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDataService } from './patch-data.service'
|
||||
|
||||
// Watch status to present toast for updated state
|
||||
@Injectable()
|
||||
export class UpdateToastService extends Observable<unknown> {
|
||||
private updateToast: HTMLIonToastElement
|
||||
|
||||
private readonly stream$ = this.patchData.pipe(
|
||||
switchMap(data => {
|
||||
if (data) {
|
||||
return this.patch.watch$('server-info', 'status-info', 'updated')
|
||||
}
|
||||
|
||||
this.errToast.dismiss()
|
||||
this.updateToast?.dismiss()
|
||||
|
||||
return EMPTY
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
filter(Boolean),
|
||||
switchMap(() => this.showToast()),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private async showToast() {
|
||||
await this.updateToast?.dismiss()
|
||||
|
||||
this.updateToast = await this.toastCtrl.create(TOAST)
|
||||
this.updateToast.buttons.push({
|
||||
side: 'end',
|
||||
text: 'Restart',
|
||||
handler: () => {
|
||||
this.restart()
|
||||
},
|
||||
})
|
||||
|
||||
await this.updateToast.present()
|
||||
}
|
||||
|
||||
private async restart(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create(LOADER)
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
} catch (e: any) {
|
||||
await this.errToast.present(e)
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LOADER: LoadingOptions = {
|
||||
spinner: 'lines',
|
||||
message: 'Restarting...',
|
||||
cssClass: 'loader',
|
||||
}
|
||||
|
||||
const TOAST: ToastOptions = {
|
||||
header: 'EOS download complete!',
|
||||
message:
|
||||
'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
|
||||
position: 'bottom',
|
||||
duration: 0,
|
||||
cssClass: 'success-toast',
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class SnekDirective {
|
||||
pointer: '/gaming',
|
||||
value: { snake: { 'high-score': data.highScore } },
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loadingCtrl.dismiss()
|
||||
|
||||
@@ -157,7 +157,7 @@ export class BackupDrivesComponent {
|
||||
entry,
|
||||
})
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
return false
|
||||
} finally {
|
||||
@@ -209,9 +209,8 @@ export class BackupDrivesComponent {
|
||||
|
||||
try {
|
||||
const res = await this.embassyApi.updateBackupTarget(value)
|
||||
const entry = Object.values(res)[0]
|
||||
this.backupService.cifs[index].entry = entry
|
||||
} catch (e) {
|
||||
this.backupService.cifs[index].entry = Object.values(res)[0]
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -229,7 +228,7 @@ export class BackupDrivesComponent {
|
||||
try {
|
||||
await this.embassyApi.removeBackupTarget({ id })
|
||||
this.backupService.cifs.splice(index, 1)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -48,7 +48,7 @@ export class BackupService {
|
||||
entry: drive as DiskBackupTarget,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
||||
@@ -212,8 +212,7 @@ export class FormObjectComponent {
|
||||
component: EnumListPage,
|
||||
})
|
||||
|
||||
modal.onWillDismiss().then((res: { data: string[] }) => {
|
||||
const data = res.data
|
||||
modal.onWillDismiss<string[]>().then(({ data }) => {
|
||||
if (!data) return
|
||||
this.updateEnumList(key, current, data)
|
||||
})
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<div *ngIf="loading$ | async" class="center-spinner">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<ion-label class="long-message">Checking for installed services which depend on {{ params.title }}...</ion-label>
|
||||
<ion-label class="long-message">
|
||||
Checking for installed services which depend on
|
||||
{{ params.title }}...
|
||||
</ion-label>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!(loading$ | async) && !!dependentViolation" class="slide-content">
|
||||
<div style="margin-top: 25px;">
|
||||
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label color="warning" style="font-size: xx-large; font-weight: bold;">
|
||||
WARNING
|
||||
</ion-label>
|
||||
<div class="wrapper">
|
||||
<div class="warning">
|
||||
<ion-label color="warning" class="label">WARNING</ion-label>
|
||||
</div>
|
||||
|
||||
<div class="long-message">
|
||||
{{ dependentViolation }}
|
||||
</div>
|
||||
|
||||
<div style="margin: 25px 0px;">
|
||||
<div style="border-width: 0px 0px 1px 0px; font-size: unset; text-align: left; font-weight: bold; margin-left: 13px; border-style: solid; border-color: var(--ion-color-light-tint);">
|
||||
<div *ngIf="patch.data['package-data']" class="items">
|
||||
<div class="affected">
|
||||
<ion-text color="warning">Affected Services</ion-text>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,11 @@
|
||||
style="--ion-item-background: margin-top: 5px"
|
||||
*ngFor="let dep of dependentBreakages | keyvalue"
|
||||
>
|
||||
<ion-thumbnail style="position: relative; height: 4vh; width: 4vh" slot="start">
|
||||
<img [src]="patch.data['package-data'][dep.key]['static-files'].icon" />
|
||||
<ion-thumbnail class="thumbnail" slot="start">
|
||||
<img
|
||||
alt=""
|
||||
[src]="patch.data['package-data'][dep.key]['static-files'].icon"
|
||||
/>
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h5>{{ patch.data['package-data'][dep.key].manifest.title }}</h5>
|
||||
@@ -34,4 +37,4 @@
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
.wrapper {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.items {
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.affected {
|
||||
border-width: 0 0 1px 0;
|
||||
font-size: unset;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
margin-left: 13px;
|
||||
border-style: solid;
|
||||
border-color: var(--ion-color-light-tint);
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: relative;
|
||||
height: 4vh;
|
||||
width: 4vh;
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import { WizardAction } from '../wizard-types'
|
||||
@Component({
|
||||
selector: 'dependents',
|
||||
templateUrl: './dependents.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
styleUrls: [
|
||||
'./dependents.component.scss',
|
||||
'../install-wizard.component.scss',
|
||||
],
|
||||
})
|
||||
export class DependentsComponent {
|
||||
@Input() params: {
|
||||
|
||||
@@ -54,7 +54,7 @@ export class LogsPage {
|
||||
this.loading = false
|
||||
|
||||
return logsRes.entries
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CanActivate, Router, CanActivateChild } from '@angular/router'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { AuthState, AuthService } from '../services/auth.service'
|
||||
import { CanActivate, Router, CanActivateChild, UrlTree } from '@angular/router'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { AuthService } from '../services/auth.service'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
authState: AuthState
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
this.authService.watch$()
|
||||
.pipe(
|
||||
tap(auth => this.authState = auth),
|
||||
).subscribe()
|
||||
}
|
||||
) {}
|
||||
|
||||
canActivate (): boolean {
|
||||
canActivate(): Observable<boolean | UrlTree> {
|
||||
return this.runAuthCheck()
|
||||
}
|
||||
|
||||
canActivateChild (): boolean {
|
||||
canActivateChild(): Observable<boolean | UrlTree> {
|
||||
return this.runAuthCheck()
|
||||
}
|
||||
|
||||
private runAuthCheck (): boolean {
|
||||
if (this.authState === AuthState.VERIFIED) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
return false
|
||||
}
|
||||
private runAuthCheck(): Observable<boolean | UrlTree> {
|
||||
return this.authService.isVerified$.pipe(
|
||||
map(verified => verified || this.router.parseUrl('/login')),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CanActivate, Router } from '@angular/router'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { AuthService, AuthState } from '../services/auth.service'
|
||||
import { CanActivate, Router, UrlTree } from '@angular/router'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { AuthService } from '../services/auth.service'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UnauthGuard implements CanActivate {
|
||||
authState: AuthState
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
this.authService.watch$()
|
||||
.pipe(
|
||||
tap(auth => this.authState = auth),
|
||||
).subscribe()
|
||||
}
|
||||
) {}
|
||||
|
||||
canActivate (): boolean {
|
||||
if (this.authState === AuthState.VERIFIED) {
|
||||
this.router.navigateByUrl('')
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
canActivate(): Observable<boolean | UrlTree> {
|
||||
return this.authService.isVerified$.pipe(
|
||||
map(verified => !verified || this.router.parseUrl('')),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons *ngIf="!loadingText && !loadingError && hasConfig" slot="end" class="ion-padding-end">
|
||||
<ion-buttons
|
||||
*ngIf="!loadingText && !loadingError && hasConfig"
|
||||
slot="end"
|
||||
class="ion-padding-end"
|
||||
>
|
||||
<ion-button fill="clear" (click)="resetDefaults()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Reset Defaults
|
||||
@@ -16,38 +20,52 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner>
|
||||
<text-spinner *ngIf="loadingText" [text]="loadingText"></text-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-template #loaded>
|
||||
|
||||
<ng-container *ngIf="!loadingText && pkg">
|
||||
<ion-item *ngIf="loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ loadingError }}
|
||||
</ion-text>
|
||||
<ion-text color="danger"> {{ loadingError }} </ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #noError>
|
||||
<ion-item *ngIf="hasConfig && !pkg.installed.status.configured && !configForm.dirty">
|
||||
<ion-item
|
||||
*ngIf="hasConfig && !pkg.installed.status.configured && !configForm.dirty"
|
||||
>
|
||||
<ion-label>
|
||||
<ion-text color="success">To use the default config for {{ pkg.manifest.title }}, click "Save" below.</ion-text>
|
||||
<ion-text color="success"
|
||||
>To use the default config for {{ pkg.manifest.title }}, click
|
||||
"Save" below.</ion-text
|
||||
>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<!-- auto-config -->
|
||||
<ion-item lines="none" *ngIf="dependentInfo" class="rec-item" style="margin-bottom: 48px;">
|
||||
<ion-item
|
||||
lines="none"
|
||||
*ngIf="dependentInfo"
|
||||
class="rec-item"
|
||||
style="margin-bottom: 48px"
|
||||
>
|
||||
<ion-label>
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<img style="width: 18px; margin: 4px;" [src]="pkg['static-files'].icon" [alt]="pkg.manifest.title"/>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: 18px;">{{ pkg.manifest.title }}</ion-text>
|
||||
<h2 style="display: flex; align-items: center">
|
||||
<img
|
||||
style="width: 18px; margin: 4px"
|
||||
[src]="pkg['static-files'].icon"
|
||||
[alt]="pkg.manifest.title"
|
||||
/>
|
||||
<ion-text
|
||||
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
||||
>{{ pkg.manifest.title }}</ion-text
|
||||
>
|
||||
</h2>
|
||||
<p>
|
||||
<ion-text color="dark">
|
||||
The following modifications have been made to {{ pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:
|
||||
The following modifications have been made to {{
|
||||
pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHtml]="d"></li>
|
||||
</ul>
|
||||
@@ -56,14 +74,17 @@
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label>
|
||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||
<p>
|
||||
No config options for {{ pkg.manifest.title }} {{
|
||||
pkg.manifest.version }}.
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<!-- has config -->
|
||||
<form *ngIf="hasConfig" [formGroup]="configForm" novalidate>
|
||||
<form-object
|
||||
@@ -74,16 +95,32 @@
|
||||
></form-object>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons *ngIf="!loadingText && !loadingError" slot="end" class="ion-padding-end">
|
||||
<ion-button *ngIf="hasConfig" fill="outline" [disabled]="saving" (click)="save()" class="enter-click" [class.no-click]="saving">
|
||||
<ion-buttons
|
||||
*ngIf="!loadingText && !loadingError"
|
||||
slot="end"
|
||||
class="ion-padding-end"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="hasConfig"
|
||||
fill="outline"
|
||||
[disabled]="saving"
|
||||
(click)="save()"
|
||||
class="enter-click"
|
||||
[class.no-click]="saving"
|
||||
>
|
||||
Save
|
||||
</ion-button>
|
||||
<ion-button *ngIf="!hasConfig" fill="outline" (click)="dismiss()" class="enter-click">
|
||||
<ion-button
|
||||
*ngIf="!hasConfig"
|
||||
fill="outline"
|
||||
(click)="dismiss()"
|
||||
class="enter-click"
|
||||
>
|
||||
Close
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
@@ -36,7 +36,7 @@ export class AppConfigPage {
|
||||
@Input() pkgId: string
|
||||
@Input() dependentInfo?: DependentInfo
|
||||
diff: string[] // only if dependent info
|
||||
pkg: PackageDataEntry
|
||||
pkg?: PackageDataEntry
|
||||
loadingText: string | undefined
|
||||
configSpec: ConfigSpec
|
||||
configForm: FormGroup
|
||||
@@ -58,7 +58,7 @@ export class AppConfigPage {
|
||||
|
||||
async ngOnInit() {
|
||||
this.pkg = this.patch.getData()['package-data'][this.pkgId]
|
||||
this.hasConfig = !!this.pkg.manifest.config
|
||||
this.hasConfig = !!this.pkg?.manifest.config
|
||||
|
||||
if (!this.hasConfig) return
|
||||
|
||||
@@ -102,7 +102,7 @@ export class AppConfigPage {
|
||||
this.diff = this.getDiff(patch)
|
||||
this.markDirty(patch)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
this.loadingText = undefined
|
||||
@@ -133,7 +133,7 @@ export class AppConfigPage {
|
||||
if (this.configForm.invalid) {
|
||||
document
|
||||
.getElementsByClassName('validation-error')[0]
|
||||
.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export class AppConfigPage {
|
||||
config,
|
||||
})
|
||||
this.modalCtrl.dismiss()
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.saving = false
|
||||
|
||||
@@ -82,7 +82,7 @@ export class AppRecoverSelectPage {
|
||||
password: this.password,
|
||||
})
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.error = getErrorMessage(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -50,7 +50,7 @@ export class GenericFormPage {
|
||||
this.formGroup.markAllAsTouched()
|
||||
document
|
||||
.getElementsByClassName('validation-error')[0]
|
||||
.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
?.parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class GenericInputComponent {
|
||||
try {
|
||||
await this.options.submitFn(value)
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.error = getErrorMessage(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export class AppActionsPage {
|
||||
})
|
||||
|
||||
setTimeout(() => successModal.present(), 400)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
return false
|
||||
} finally {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: "app-list-empty",
|
||||
templateUrl: "app-list-empty.component.html",
|
||||
styleUrls: ["app-list-empty.component.scss"],
|
||||
selector: 'app-list-empty',
|
||||
templateUrl: 'app-list-empty.component.html',
|
||||
styleUrls: ['app-list-empty.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListEmptyComponent {}
|
||||
|
||||
@@ -18,6 +18,7 @@ export class AppListPage {
|
||||
recoveredPkgs: readonly RecoveredInfo[] = []
|
||||
order: readonly string[] = []
|
||||
reordering = false
|
||||
loading = true
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
@@ -26,7 +27,9 @@ export class AppListPage {
|
||||
) {}
|
||||
|
||||
get empty(): boolean {
|
||||
return !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
|
||||
return (
|
||||
!this.loading && !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
|
||||
)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -40,6 +43,7 @@ export class AppListPage {
|
||||
this.pkgs = pkgs
|
||||
this.recoveredPkgs = recoveredPkgs
|
||||
this.order = order
|
||||
this.loading = false
|
||||
|
||||
// set order in UI DB if there were unknown packages
|
||||
if (order.length < pkgs.length) {
|
||||
|
||||
@@ -12,10 +12,12 @@ export class PackageInfoPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDbService) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
|
||||
return this.patch.watch$('package-data', pkg.manifest.id).pipe(
|
||||
filter(v => !!v),
|
||||
map(getPackageInfo),
|
||||
startWith(getPackageInfo(pkg)),
|
||||
)
|
||||
return this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(
|
||||
filter<PackageDataEntry>(Boolean),
|
||||
startWith(pkg),
|
||||
map(getPackageInfo),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class AppMetricsPage {
|
||||
async getMetrics(): Promise<void> {
|
||||
try {
|
||||
this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
this.stopDaemon()
|
||||
} finally {
|
||||
|
||||
@@ -149,7 +149,7 @@ export class AppPropertiesPage {
|
||||
id: this.pkgId,
|
||||
})
|
||||
this.node = getValueByPointer(this.properties, this.pointer || '')
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
||||
@@ -122,7 +122,7 @@ export class AppShowStatusComponent {
|
||||
|
||||
try {
|
||||
await this.embassyApi.stopPackage({ id })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -149,7 +149,7 @@ export class AppShowStatusComponent {
|
||||
|
||||
try {
|
||||
await this.embassyApi.startPackage({ id: this.pkg.manifest.id })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -42,7 +42,7 @@ export class DevConfigPage {
|
||||
let doc: any
|
||||
try {
|
||||
doc = yaml.load(this.code)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class DevConfigPage {
|
||||
pointer: `/dev/${this.projectId}/config`,
|
||||
value: this.code,
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.saving = false
|
||||
|
||||
@@ -60,7 +60,7 @@ export class DevInstructionsPage {
|
||||
pointer: `/dev/${this.projectId}/instructions`,
|
||||
value: this.code,
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.saving = false
|
||||
|
||||
@@ -159,7 +159,7 @@ export class DeveloperListPage {
|
||||
} else {
|
||||
await this.api.setDbValue({ pointer: `/dev`, value: { [id]: def } })
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -197,7 +197,7 @@ export class DeveloperListPage {
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -216,7 +216,7 @@ export class DeveloperListPage {
|
||||
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
||||
delete devDataToSave[id]
|
||||
await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DeveloperMenuPage {
|
||||
pointer: `/dev/${this.projectId}/basic-info`,
|
||||
value: basicInfo,
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { LoadingController, getPlatforms } from '@ionic/angular'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
@@ -13,30 +13,24 @@ export class LoginPage {
|
||||
password = ''
|
||||
unmasked = false
|
||||
error = ''
|
||||
loader: HTMLIonLoadingElement
|
||||
patchConnectionSub: Subscription
|
||||
loader?: HTMLIonLoadingElement
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly authService: AuthService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly api: ApiService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.loader) {
|
||||
this.loader.dismiss()
|
||||
this.loader = undefined
|
||||
}
|
||||
if (this.patchConnectionSub) {
|
||||
this.patchConnectionSub.unsubscribe()
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.loader?.dismiss()
|
||||
}
|
||||
|
||||
toggleMask () {
|
||||
toggleMask() {
|
||||
this.unmasked = !this.unmasked
|
||||
}
|
||||
|
||||
async submit () {
|
||||
async submit() {
|
||||
this.error = ''
|
||||
|
||||
this.loader = await this.loadingCtrl.create({
|
||||
@@ -53,9 +47,11 @@ export class LoginPage {
|
||||
metadata: { platforms: getPlatforms() },
|
||||
})
|
||||
|
||||
this.authService.setVerified()
|
||||
this.password = ''
|
||||
} catch (e) {
|
||||
this.authService
|
||||
.setVerified()
|
||||
.then(() => this.router.navigate([''], { replaceUrl: true }))
|
||||
} catch (e: any) {
|
||||
this.error = e.code === 34 ? 'Invalid Password' : e.message
|
||||
} finally {
|
||||
this.loader.dismiss()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, first, map, startWith, switchMapTo, tap } from 'rxjs/operators'
|
||||
import { filter, first, map, startWith, switchMapTo } from 'rxjs/operators'
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { spreadProgress } from '../utils/spread-progress'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-list',
|
||||
@@ -20,7 +19,6 @@ export class MarketplaceListPage {
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
tap(pkgs => Object.values(pkgs).forEach(spreadProgress)),
|
||||
startWith({}),
|
||||
)
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ import {
|
||||
} from '@start9labs/marketplace'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { BehaviorSubject, defer, Observable, of } from 'rxjs'
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
||||
import { catchError, filter, shareReplay, switchMap, tap } from 'rxjs/operators'
|
||||
|
||||
import { spreadProgress } from '../utils/spread-progress'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show',
|
||||
templateUrl: './marketplace-show.page.html',
|
||||
@@ -23,13 +21,12 @@ export class MarketplaceShowPage {
|
||||
|
||||
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||
|
||||
readonly localPkg$ = defer(() =>
|
||||
this.patch.watch$('package-data', this.pkgId),
|
||||
).pipe(
|
||||
filter<PackageDataEntry>(Boolean),
|
||||
tap(spreadProgress),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
readonly localPkg$ = this.patch
|
||||
.watch$('package-data', this.pkgId)
|
||||
.pipe(
|
||||
filter<PackageDataEntry>(Boolean),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly pkg$: Observable<MarketplacePkg> = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export function spreadProgress(pkg: PackageDataEntry) {
|
||||
pkg['install-progress'] = { ...pkg['install-progress'] }
|
||||
}
|
||||
@@ -75,8 +75,8 @@
|
||||
<ion-label>
|
||||
<h2>
|
||||
<b>
|
||||
<span *ngIf="not['package-id']"
|
||||
>{{ patch.data['package-data'][not['package-id']] ?
|
||||
<span *ngIf="not['package-id'] && patch.data['package-data']">
|
||||
{{ patch.data['package-data'][not['package-id']] ?
|
||||
patch.data['package-data'][not['package-id']].manifest.title :
|
||||
not['package-id'] }} -
|
||||
</span>
|
||||
|
||||
@@ -59,7 +59,7 @@ export class NotificationsPage {
|
||||
})
|
||||
this.beforeCursor = notifications[notifications.length - 1]?.id
|
||||
this.needInfinite = notifications.length >= this.perPage
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
return notifications
|
||||
@@ -78,7 +78,7 @@ export class NotificationsPage {
|
||||
await this.embassyApi.deleteNotification({ id })
|
||||
this.notifications.splice(index, 1)
|
||||
this.beforeCursor = this.notifications[this.notifications.length - 1]?.id
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -169,7 +169,7 @@ export class NotificationsPage {
|
||||
})
|
||||
this.notifications = []
|
||||
this.beforeCursor = undefined
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -147,7 +147,7 @@ export class MarketplacesPage {
|
||||
{ 'server-id': this.patch.getData()['server-info'].id },
|
||||
url,
|
||||
)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
@@ -158,7 +158,7 @@ export class MarketplacesPage {
|
||||
try {
|
||||
marketplace['selected-id'] = id
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export class MarketplacesPage {
|
||||
try {
|
||||
delete marketplace['known-hosts'][id]
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -223,7 +223,7 @@ export class MarketplacesPage {
|
||||
url,
|
||||
)
|
||||
marketplace['known-hosts'][id] = { name, url }
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
@@ -233,7 +233,7 @@ export class MarketplacesPage {
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -266,7 +266,7 @@ export class MarketplacesPage {
|
||||
)
|
||||
marketplace['known-hosts'][id] = { name, url }
|
||||
marketplace['selected-id'] = id
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
@@ -276,7 +276,7 @@ export class MarketplacesPage {
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
|
||||
@@ -7,24 +7,23 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group *ngIf="patch.data['server-info'] as server">
|
||||
<ion-content *ngIf="ui$ | async as ui" class="ion-padding-top">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalName()">
|
||||
<ion-item button (click)="presentModalName('Embassy-' + server.id)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ patch.data.ui.name || defaultName }}</ion-note>
|
||||
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'] !== false)"
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'] !== false)"
|
||||
>
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end"
|
||||
>{{ patch.data.ui['auto-check-updates'] !== false ? 'Enabled' :
|
||||
'Disabled' }}</ion-note
|
||||
>
|
||||
<ion-note slot="end">
|
||||
{{ ui['auto-check-updates'] !== false ? 'Enabled' : 'Disabled' }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -21,39 +21,37 @@ import { LocalStorageService } from '../../../services/local-storage.service'
|
||||
})
|
||||
export class PreferencesPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
defaultName: string
|
||||
clicks = 0
|
||||
|
||||
constructor (
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly api: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly localStorageService: LocalStorageService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
private readonly patch: PatchDbService,
|
||||
readonly serverConfig: ServerConfigService,
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}`
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
async presentModalName (): Promise<void> {
|
||||
async presentModalName(placeholder: string): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: this.defaultName,
|
||||
placeholder,
|
||||
nullable: true,
|
||||
initialValue: this.patch.getData().ui.name,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) =>
|
||||
this.setDbValue('name', value || this.defaultName),
|
||||
this.setDbValue('name', value || placeholder),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
@@ -66,7 +64,7 @@ export class PreferencesPage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setDbValue (key: string, value: string): Promise<void> {
|
||||
private async setDbValue(key: string, value: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Saving...',
|
||||
@@ -81,7 +79,7 @@ export class PreferencesPage {
|
||||
}
|
||||
}
|
||||
|
||||
async addClick () {
|
||||
async addClick() {
|
||||
this.clicks++
|
||||
if (this.clicks >= 5) {
|
||||
this.clicks = 0
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ServerMetricsPage {
|
||||
private async getMetrics(): Promise<void> {
|
||||
try {
|
||||
this.metrics = await this.embassyApi.getServerMetrics({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
this.stopDaemon()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title *ngIf="!patch.loaded"
|
||||
>Loading<span class="loading-dots"></span
|
||||
></ion-title>
|
||||
<ion-title *ngIf="patch.loaded"
|
||||
>{{ patch.data.ui.name || "Embassy-" + patch.data['server-info'].id
|
||||
}}</ion-title
|
||||
>
|
||||
<ion-title *ngIf="patch.loaded else loading">
|
||||
{{ (ui$ | async).name || "Embassy-" + (server$ | async).id }}
|
||||
</ion-title>
|
||||
<ng-template #loading>
|
||||
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
||||
</ng-template>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
@@ -22,18 +21,19 @@
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-template #data>
|
||||
<ion-item-group>
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider>
|
||||
<ion-text color="dark" *ngIf="cat.key !== 'Power'"
|
||||
>{{ cat.key }}</ion-text
|
||||
>
|
||||
<ion-text color="dark" *ngIf="cat.key !== 'Power'">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
<ion-text
|
||||
color="dark"
|
||||
*ngIf="cat.key === 'Power'"
|
||||
(click)="addClick()"
|
||||
>{{ cat.key }}</ion-text
|
||||
>
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let button of cat.value">
|
||||
<ion-item
|
||||
@@ -50,16 +50,13 @@
|
||||
|
||||
<!-- "Create Backup" button only -->
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ng-container
|
||||
*ngIf="patch.data['server-info']['status-info'] as statusInfo"
|
||||
>
|
||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||
<ion-text
|
||||
color="warning"
|
||||
*ngIf="!statusInfo['backing-up'] && !statusInfo['update-progress']"
|
||||
>
|
||||
Last Backup: {{ patch.data['server-info']['last-backup'] ?
|
||||
(patch.data['server-info']['last-backup'] | date: 'short') :
|
||||
'never' }}
|
||||
Last Backup: {{ server['last-backup'] ?
|
||||
(server['last-backup'] | date: 'short') : 'never' }}
|
||||
</ion-text>
|
||||
<span *ngIf="!!statusInfo['backing-up']" class="inline">
|
||||
<ion-spinner
|
||||
@@ -74,7 +71,7 @@
|
||||
<p *ngIf="button.title === 'Software Update'">
|
||||
<ng-container *ngIf="button.disabled | async; else enabled">
|
||||
<ion-text
|
||||
*ngIf="patch.data['server-info']['status-info'].updated"
|
||||
*ngIf="server['status-info'].updated"
|
||||
class="inline"
|
||||
color="warning"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
|
||||
@Component({
|
||||
@@ -24,10 +23,12 @@ import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
ServerStatus = ServerStatus
|
||||
hasRecoveredPackage: boolean
|
||||
clicks = 0
|
||||
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
@@ -164,7 +165,7 @@ export class ServerShowPage {
|
||||
this.embassyApi.repairDisk({}).then(_ => {
|
||||
this.restart()
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
},
|
||||
@@ -185,7 +186,7 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -202,7 +203,7 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.shutdownServer({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -219,7 +220,7 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.systemRebuild({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -239,7 +240,7 @@ export class ServerShowPage {
|
||||
if (updateAvailable) {
|
||||
this.updateEos()
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -387,7 +388,8 @@ export class ServerShowPage {
|
||||
},
|
||||
{
|
||||
title: 'Kernel Logs',
|
||||
description: 'Diagnostic log stream for device drivers and other kernel processes',
|
||||
description:
|
||||
'Diagnostic log stream for device drivers and other kernel processes',
|
||||
icon: 'receipt-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['kernel-logs'], {
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
<ion-item-divider>Basic</ion-item-divider>
|
||||
|
||||
<ion-item-group *ngIf="patch.data['server-info'] as server">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>EmbassyOS Version</h2>
|
||||
@@ -47,5 +46,4 @@
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
|
||||
@@ -12,20 +12,23 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
export class ServerSpecsPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
public readonly patch: PatchDbService,
|
||||
public readonly config: ConfigService,
|
||||
) { }
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
|
||||
ngAfterViewInit () {
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly patch: PatchDbService,
|
||||
public readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
async copy (address: string) {
|
||||
async copy(address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
.then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
await copyToClipboard(address || '').then(success => {
|
||||
message = success ? 'copied to clipboard!' : 'failed to copy'
|
||||
})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
@@ -35,7 +38,7 @@ export class ServerSpecsPage {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class SessionsPage {
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.sessionInfo = await this.embassyApi.getSessions({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
@@ -62,7 +62,7 @@ export class SessionsPage {
|
||||
try {
|
||||
await this.embassyApi.killSessions({ ids: [id] })
|
||||
delete this.sessionInfo.sessions[id]
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -37,7 +37,7 @@ export class SSHKeysPage {
|
||||
async getKeys(): Promise<void> {
|
||||
try {
|
||||
this.sshKeys = await this.embassyApi.getSshKeys({})
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
@@ -111,7 +111,7 @@ export class SSHKeysPage {
|
||||
const entry = this.sshKeys[i]
|
||||
await this.embassyApi.deleteSshKey({ fingerprint: entry.fingerprint })
|
||||
this.sshKeys.splice(i, 1)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -49,7 +49,7 @@ export class WifiPage {
|
||||
if (!this.wifi.country) {
|
||||
await this.presentAlertCountry()
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
@@ -179,7 +179,7 @@ export class WifiPage {
|
||||
await this.api.setWifiCountry({ country })
|
||||
await this.getWifi()
|
||||
this.wifi.country = country
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -271,7 +271,7 @@ export class WifiPage {
|
||||
try {
|
||||
await this.api.connectWifi({ ssid })
|
||||
await this.confirmWifi(ssid)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -290,7 +290,7 @@ export class WifiPage {
|
||||
await this.api.deleteWifi({ ssid })
|
||||
await this.getWifi()
|
||||
delete this.wifi.ssids[ssid]
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -313,7 +313,7 @@ export class WifiPage {
|
||||
connect: false,
|
||||
})
|
||||
await this.getWifi()
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
@@ -337,7 +337,7 @@ export class WifiPage {
|
||||
})
|
||||
|
||||
await this.confirmWifi(ssid, true)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -266,7 +266,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
private syncResponse<
|
||||
T,
|
||||
F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>,
|
||||
>(f: F, temp?: Operation): (...args: Parameters<F>) => Promise<T> {
|
||||
>(f: F, temp?: Operation<unknown>): (...args: Parameters<F>) => Promise<T> {
|
||||
return (...a) => {
|
||||
// let expireId = undefined
|
||||
// if (temp) {
|
||||
|
||||
@@ -19,9 +19,23 @@ import { BehaviorSubject } from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
|
||||
const PROGRESS: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 0,
|
||||
'download-complete': false,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>(undefined)
|
||||
readonly mockPatch$ = new BehaviorSubject<Update<DataModel>>({
|
||||
id: 1,
|
||||
value: mockPatchData,
|
||||
expireId: null,
|
||||
})
|
||||
private readonly revertTime = 4000
|
||||
sequence: number
|
||||
|
||||
@@ -391,8 +405,13 @@ export class MockApiService extends ApiService {
|
||||
|
||||
await pauseFor(8000)
|
||||
|
||||
appPatch[0].value = PackageMainStatus.Stopped
|
||||
this.updateMock(appPatch)
|
||||
const newPatch = [
|
||||
{
|
||||
...appPatch[0],
|
||||
value: PackageMainStatus.Stopped,
|
||||
},
|
||||
]
|
||||
this.updateMock(newPatch)
|
||||
}
|
||||
|
||||
await pauseFor(1000)
|
||||
@@ -454,32 +473,21 @@ export class MockApiService extends ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 0,
|
||||
'download-complete': false,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(params.id, initialProgress)
|
||||
this.updateProgress(params.id)
|
||||
}, 1000)
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
const patch = [
|
||||
const patch: Operation<PackageDataEntry>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${params.id}`,
|
||||
value: pkg,
|
||||
value: {
|
||||
...Mock.LocalPkgs[params.id],
|
||||
state: PackageState.Installing,
|
||||
'install-progress': { ...PROGRESS },
|
||||
installed: undefined,
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
@@ -527,32 +535,20 @@ export class MockApiService extends ApiService {
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes> {
|
||||
await pauseFor(2000)
|
||||
const patch: Operation[] = params.ids.map(id => {
|
||||
const initialProgress: InstallProgress = {
|
||||
size: 120,
|
||||
downloaded: 120,
|
||||
'download-complete': true,
|
||||
validated: 0,
|
||||
'validation-complete': false,
|
||||
unpacked: 0,
|
||||
'unpack-complete': false,
|
||||
}
|
||||
|
||||
const pkg: PackageDataEntry = {
|
||||
...Mock.LocalPkgs[id],
|
||||
state: PackageState.Restoring,
|
||||
'install-progress': initialProgress,
|
||||
installed: undefined,
|
||||
}
|
||||
|
||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
||||
setTimeout(async () => {
|
||||
this.updateProgress(id, initialProgress)
|
||||
this.updateProgress(id)
|
||||
}, 2000)
|
||||
|
||||
return {
|
||||
op: PatchOp.ADD,
|
||||
path: `/package-data/${id}`,
|
||||
value: pkg,
|
||||
value: {
|
||||
...Mock.LocalPkgs[id],
|
||||
state: PackageState.Restoring,
|
||||
'install-progress': { ...PROGRESS },
|
||||
installed: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -703,11 +699,11 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
const patch2: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
},
|
||||
]
|
||||
this.updateMock(patch2)
|
||||
}, this.revertTime)
|
||||
@@ -727,11 +723,11 @@ export class MockApiService extends ApiService {
|
||||
params: RR.DeleteRecoveredPackageReq,
|
||||
): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/recovered-packages/${params.id}`,
|
||||
} as RemoveOperation,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
@@ -747,30 +743,30 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateProgress(
|
||||
id: string,
|
||||
initialProgress: InstallProgress,
|
||||
): Promise<void> {
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
const progress = { ...PROGRESS }
|
||||
const phases = [
|
||||
{ progress: 'downloaded', completion: 'download-complete' },
|
||||
{ progress: 'validated', completion: 'validation-complete' },
|
||||
{ progress: 'unpacked', completion: 'unpack-complete' },
|
||||
]
|
||||
|
||||
for (let phase of phases) {
|
||||
let i = initialProgress[phase.progress]
|
||||
while (i < initialProgress.size) {
|
||||
let i = progress[phase.progress]
|
||||
while (i < progress.size) {
|
||||
await pauseFor(250)
|
||||
i = Math.min(i + 5, initialProgress.size)
|
||||
initialProgress[phase.progress] = i
|
||||
if (i === initialProgress.size) {
|
||||
initialProgress[phase.completion] = true
|
||||
i = Math.min(i + 5, progress.size)
|
||||
progress[phase.progress] = i
|
||||
|
||||
if (i === progress.size) {
|
||||
progress[phase.completion] = true
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
value: initialProgress,
|
||||
value: { ...progress },
|
||||
},
|
||||
]
|
||||
this.updateMock(patch)
|
||||
@@ -778,7 +774,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2: any = [
|
||||
const patch2: Operation<PackageDataEntry>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}`,
|
||||
@@ -822,7 +818,7 @@ export class MockApiService extends ApiService {
|
||||
this.updateMock(patch2)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch3: Operation[] = [
|
||||
const patch3: Operation<ServerStatus>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status',
|
||||
@@ -847,7 +843,7 @@ export class MockApiService extends ApiService {
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private async updateMock(patch: Operation[]): Promise<void> {
|
||||
private async updateMock<T>(patch: Operation<T>[]): Promise<void> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = await this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
@@ -861,7 +857,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
private async withRevision<T>(
|
||||
patch: Operation[],
|
||||
patch: Operation<unknown>[],
|
||||
response: T = null,
|
||||
): Promise<WithRevision<T>> {
|
||||
if (!this.sequence) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import { Observable, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
|
||||
export enum AuthState {
|
||||
@@ -12,27 +12,29 @@ export enum AuthState {
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(undefined)
|
||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||
|
||||
constructor (
|
||||
private readonly storage: Storage,
|
||||
) { }
|
||||
readonly isVerified$ = this.watch$().pipe(
|
||||
map(state => state === AuthState.VERIFIED),
|
||||
)
|
||||
|
||||
async init (): Promise<void> {
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
||||
this.authState$.next( loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
this.authState$.next(loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
}
|
||||
|
||||
watch$ (): Observable<AuthState> {
|
||||
watch$(): Observable<AuthState> {
|
||||
return this.authState$.pipe(distinctUntilChanged())
|
||||
}
|
||||
|
||||
async setVerified (): Promise<void> {
|
||||
async setVerified(): Promise<void> {
|
||||
await this.storage.set(this.LOGGED_IN_KEY, true)
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
}
|
||||
|
||||
async setUnverified (): Promise<void> {
|
||||
async setUnverified(): Promise<void> {
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,34 @@ import {
|
||||
combineLatest,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
Subscription,
|
||||
} from 'rxjs'
|
||||
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
|
||||
import { distinctUntilChanged } from 'rxjs/operators'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
mapTo,
|
||||
startWith,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
private readonly networkState$ = new BehaviorSubject<boolean>(true)
|
||||
private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(
|
||||
ConnectionFailure.None,
|
||||
private readonly networkState$ = merge(
|
||||
fromEvent(window, 'online').pipe(mapTo(true)),
|
||||
fromEvent(window, 'offline').pipe(mapTo(false)),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => navigator.onLine),
|
||||
)
|
||||
|
||||
private readonly connectionFailure$ = new Subject<ConnectionFailure>()
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly patch: PatchDbService,
|
||||
@@ -28,15 +41,8 @@ export class ConnectionService {
|
||||
return this.connectionFailure$.asObservable()
|
||||
}
|
||||
|
||||
start(): Subscription[] {
|
||||
const sub1 = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).subscribe(event => {
|
||||
this.networkState$.next(event.type === 'online')
|
||||
})
|
||||
|
||||
const sub2 = combineLatest([
|
||||
start(): Observable<unknown> {
|
||||
return combineLatest([
|
||||
// 1
|
||||
this.networkState$.pipe(distinctUntilChanged()),
|
||||
// 2
|
||||
@@ -45,20 +51,21 @@ export class ConnectionService {
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info', 'update-progress')
|
||||
.pipe(distinctUntilChanged()),
|
||||
]).subscribe(async ([network, patchConnection, progress]) => {
|
||||
if (!network) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!!progress && progress.downloaded === progress.size) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!this.configService.isTor()) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
||||
} else {
|
||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
||||
}
|
||||
})
|
||||
return [sub1, sub2]
|
||||
]).pipe(
|
||||
tap(([network, patchConnection, progress]) => {
|
||||
if (!network) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!!progress && progress.downloaded === progress.size) {
|
||||
this.connectionFailure$.next(ConnectionFailure.None)
|
||||
} else if (!this.configService.isTor()) {
|
||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
||||
} else {
|
||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
@@ -69,7 +67,7 @@ export class FormService {
|
||||
}
|
||||
|
||||
getListItem(spec: ValueSpecList, entry: any) {
|
||||
const listItemValidators = this.getListItemValidators(spec)
|
||||
const listItemValidators = getListItemValidators(spec)
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.formBuilder.control(entry, listItemValidators)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
@@ -83,14 +81,6 @@ export class FormService {
|
||||
}
|
||||
}
|
||||
|
||||
private getListItemValidators(spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return this.stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return this.numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
|
||||
private getFormGroup(
|
||||
config: ConfigSpec,
|
||||
validators: ValidatorFn[] = [],
|
||||
@@ -112,7 +102,7 @@ export class FormService {
|
||||
let value: any
|
||||
switch (spec.type) {
|
||||
case 'string':
|
||||
validators = this.stringValidators(spec)
|
||||
validators = stringValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
@@ -120,7 +110,7 @@ export class FormService {
|
||||
}
|
||||
return this.formBuilder.control(value, validators)
|
||||
case 'number':
|
||||
validators = this.numberValidators(spec)
|
||||
validators = numberValidators(spec)
|
||||
if (currentValue !== undefined) {
|
||||
value = currentValue
|
||||
} else {
|
||||
@@ -130,7 +120,7 @@ export class FormService {
|
||||
case 'object':
|
||||
return this.getFormGroup(spec.spec, [], currentValue)
|
||||
case 'list':
|
||||
validators = this.listValidators(spec)
|
||||
validators = listValidators(spec)
|
||||
const mapped = (
|
||||
Array.isArray(currentValue) ? currentValue : (spec.default as any[])
|
||||
).map(entry => {
|
||||
@@ -149,56 +139,64 @@ export class FormService {
|
||||
return this.formBuilder.control(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stringValidators(
|
||||
spec: ValueSpecString | ListValueSpecString,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
function getListItemValidators(spec: ValueSpecList) {
|
||||
if (isValueSpecListOf(spec, 'string')) {
|
||||
return stringValidators(spec.spec)
|
||||
} else if (isValueSpecListOf(spec, 'number')) {
|
||||
return numberValidators(spec.spec)
|
||||
}
|
||||
}
|
||||
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
function stringValidators(
|
||||
spec: ValueSpecString | ListValueSpecString,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
}
|
||||
|
||||
return validators
|
||||
if (!(spec as ValueSpecString).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
private numberValidators(
|
||||
spec: ValueSpecNumber | ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
|
||||
return validators
|
||||
if (spec.pattern) {
|
||||
validators.push(Validators.pattern(spec.pattern))
|
||||
}
|
||||
|
||||
private listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
return validators
|
||||
}
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
function numberValidators(
|
||||
spec: ValueSpecNumber | ListValueSpecNumber,
|
||||
): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listItemIssue())
|
||||
validators.push(isNumber())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
|
||||
return validators
|
||||
if (!(spec as ValueSpecNumber).nullable) {
|
||||
validators.push(Validators.required)
|
||||
}
|
||||
|
||||
if (spec.integral) {
|
||||
validators.push(isInteger())
|
||||
}
|
||||
|
||||
validators.push(numberInRange(spec.range))
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function listValidators(spec: ValueSpecList): ValidatorFn[] {
|
||||
const validators: ValidatorFn[] = []
|
||||
|
||||
validators.push(listInRange(spec.range))
|
||||
|
||||
validators.push(listItemIssue())
|
||||
|
||||
if (!isValueSpecListOf(spec, 'enum')) {
|
||||
validators.push(listUnique(spec))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
function isFullUnion(
|
||||
@@ -208,48 +206,47 @@ function isFullUnion(
|
||||
}
|
||||
|
||||
export function numberInRange(stringRange: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return control => {
|
||||
const value = control.value
|
||||
if (!value) return null
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(value)
|
||||
return null
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return { numberNotInRange: { value: `Number must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumber(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Number(control.value)
|
||||
return control =>
|
||||
!control.value || control.value == Number(control.value)
|
||||
? null
|
||||
: { notNumber: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function isInteger(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return !control.value || control.value == Math.trunc(control.value)
|
||||
return control =>
|
||||
!control.value || control.value == Math.trunc(control.value)
|
||||
? null
|
||||
: { numberNotInteger: { value: control.value } }
|
||||
}
|
||||
}
|
||||
|
||||
export function listInRange(stringRange: string): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
return control => {
|
||||
try {
|
||||
Range.from(stringRange).checkIncludes(control.value.length)
|
||||
return null
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
return { listNotInRange: { value: `List must be ${e.message}` } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listItemIssue(): ValidatorFn {
|
||||
return (parentControl: FormArray): ValidationErrors | null => {
|
||||
const problemChild = parentControl.controls.find(c => c.invalid)
|
||||
return parentControl => {
|
||||
const { controls } = parentControl as FormArray
|
||||
const problemChild = controls.find(c => c.invalid)
|
||||
if (problemChild) {
|
||||
return { listItemIssue: { value: 'Invalid entries' } }
|
||||
} else {
|
||||
@@ -259,7 +256,7 @@ export function listItemIssue(): ValidatorFn {
|
||||
}
|
||||
|
||||
export function listUnique(spec: ValueSpecList): ValidatorFn {
|
||||
return (control: FormArray): ValidationErrors | null => {
|
||||
return control => {
|
||||
const list = control.value
|
||||
for (let idx = 0; idx < list.length; idx++) {
|
||||
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
|
||||
@@ -516,25 +513,29 @@ export function convertValuesRecursive(
|
||||
convertValuesRecursive(spec, control)
|
||||
} else if (valueSpec.type === 'list') {
|
||||
const formArr = group.get(key) as FormArray
|
||||
const { controls } = formArr
|
||||
|
||||
if (valueSpec.subtype === 'number') {
|
||||
formArr.controls.forEach(control => {
|
||||
controls.forEach(control => {
|
||||
control.setValue(control.value ? Number(control.value) : null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'string') {
|
||||
formArr.controls.forEach(control => {
|
||||
controls.forEach(control => {
|
||||
if (!control.value) control.setValue(null)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'object') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
controls.forEach(formGroup => {
|
||||
const objectSpec = valueSpec.spec as ListValueSpecObject
|
||||
convertValuesRecursive(objectSpec.spec, formGroup)
|
||||
convertValuesRecursive(objectSpec.spec, formGroup as FormGroup)
|
||||
})
|
||||
} else if (valueSpec.subtype === 'union') {
|
||||
formArr.controls.forEach((formGroup: FormGroup) => {
|
||||
controls.forEach(formGroup => {
|
||||
const unionSpec = valueSpec.spec as ListValueSpecUnion
|
||||
const spec =
|
||||
unionSpec.variants[formGroup.controls[unionSpec.tag.id].value]
|
||||
convertValuesRecursive(spec, formGroup)
|
||||
unionSpec.variants[
|
||||
(formGroup as FormGroup).controls[unionSpec.tag.id].value
|
||||
]
|
||||
convertValuesRecursive(spec, formGroup as FormGroup)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { map, take } from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
import { Revision } from 'patch-db-client'
|
||||
import { AuthService } from './auth.service'
|
||||
import { HttpError, RpcError } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -98,33 +99,6 @@ export class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError(e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
|
||||
if (typeof data === 'string') {
|
||||
this.message = `${message}\n\n${data}`
|
||||
this.revision = null
|
||||
} else {
|
||||
if (data.details) {
|
||||
this.message = `${message}\n\n${data.details}`
|
||||
} else {
|
||||
this.message = message
|
||||
}
|
||||
this.revision = data.revision
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError(e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
this.revision = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result>(
|
||||
arg: { error: Error } | { result: Result },
|
||||
): arg is { error: Error } {
|
||||
@@ -188,10 +162,6 @@ export interface RPCError extends RPCBase {
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & {
|
||||
error: { code: string; message: string }
|
||||
}
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
|
||||
@@ -8,8 +8,9 @@ const SHOW_DISK_REPAIR = 'SHOW_DISK_REPAIR'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LocalStorageService {
|
||||
showDevTools$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
showDiskRepair$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
readonly showDevTools$ = new BehaviorSubject<boolean>(false)
|
||||
readonly showDiskRepair$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
async init() {
|
||||
|
||||
@@ -27,16 +27,16 @@ import {
|
||||
export class MarketplaceService extends AbstractMarketplaceService {
|
||||
private readonly notes = new Map<string, Record<string, string>>()
|
||||
|
||||
private readonly init$: Observable<Marketplace> = defer(() =>
|
||||
this.patch.watch$('ui', 'marketplace'),
|
||||
).pipe(
|
||||
map(marketplace =>
|
||||
marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace,
|
||||
),
|
||||
shareReplay(),
|
||||
)
|
||||
private readonly init$: Observable<Marketplace> = this.patch
|
||||
.watch$('ui', 'marketplace')
|
||||
.pipe(
|
||||
map(marketplace =>
|
||||
marketplace?.['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace,
|
||||
),
|
||||
shareReplay(),
|
||||
)
|
||||
|
||||
private readonly data$: Observable<MarketplaceData> = this.init$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import { MockSource, PollSource, WebsocketSource } from 'patch-db-client'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { PatchDbService } from './patch-db.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { inject, InjectionToken } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import {
|
||||
Bootstrapper,
|
||||
MockSource,
|
||||
PollSource,
|
||||
Source,
|
||||
WebsocketSource,
|
||||
} from 'patch-db-client'
|
||||
|
||||
export function PatchDbServiceFactory(
|
||||
config: ConfigService,
|
||||
embassyApi: ApiService,
|
||||
bootstrapper: LocalStorageBootstrap,
|
||||
auth: AuthService,
|
||||
storage: Storage,
|
||||
): PatchDbService {
|
||||
const {
|
||||
useMocks,
|
||||
patchDb: { poll },
|
||||
} = config
|
||||
import { ConfigService } from '../config.service'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { DataModel } from './data-model'
|
||||
|
||||
if (useMocks) {
|
||||
const source = new MockSource<DataModel>(
|
||||
(embassyApi as MockApiService).mockPatch$.pipe(filter(exists)),
|
||||
)
|
||||
return new PatchDbService(
|
||||
source,
|
||||
source,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
} else {
|
||||
const protocol = window.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = window.location.host
|
||||
const wsSource = new WebsocketSource<DataModel>(
|
||||
`${protocol}://${host}/ws/db`,
|
||||
)
|
||||
const pollSource = new PollSource<DataModel>({ ...poll }, embassyApi)
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>(
|
||||
'[wsSources, pollSources]',
|
||||
)
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('', {
|
||||
factory: () => inject(LocalStorageBootstrap),
|
||||
})
|
||||
|
||||
return new PatchDbService(
|
||||
wsSource,
|
||||
pollSource,
|
||||
embassyApi,
|
||||
bootstrapper,
|
||||
auth,
|
||||
storage,
|
||||
)
|
||||
}
|
||||
export function mockSourceFactory({
|
||||
mockPatch$,
|
||||
}: MockApiService): Source<DataModel>[] {
|
||||
return Array(2).fill(
|
||||
new MockSource<DataModel>(mockPatch$.pipe(filter(exists))),
|
||||
)
|
||||
}
|
||||
|
||||
export function realSourceFactory(
|
||||
embassyApi: ApiService,
|
||||
config: ConfigService,
|
||||
{ defaultView }: Document,
|
||||
): Source<DataModel>[] {
|
||||
const { patchDb } = config
|
||||
const { host } = defaultView.location
|
||||
const protocol = defaultView.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
return [
|
||||
new WebsocketSource<DataModel>(`${protocol}://${host}/ws/db`),
|
||||
new PollSource<DataModel>({ ...patchDb.poll }, embassyApi),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client'
|
||||
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
Subscription,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
finalize,
|
||||
mergeMap,
|
||||
skip,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
@@ -17,13 +23,7 @@ import { isEmptyObject, pauseFor } from '@start9labs/shared'
|
||||
import { DataModel } from './data-model'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { patch } from '@start9labs/emver'
|
||||
|
||||
export const PATCH_HTTP = new InjectionToken<Source<DataModel>>('')
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>>('')
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
||||
export const AUTH = new InjectionToken<AuthService>('')
|
||||
export const STORAGE = new InjectionToken<Storage>('')
|
||||
import { BOOTSTRAPPER, PATCH_SOURCE } from './patch-db.factory'
|
||||
|
||||
export enum PatchConnection {
|
||||
Initializing = 'initializing',
|
||||
@@ -36,13 +36,13 @@ export enum PatchConnection {
|
||||
})
|
||||
export class PatchDbService {
|
||||
private readonly WS_SUCCESS = 'wsSuccess'
|
||||
private patchConnection$ = new BehaviorSubject(PatchConnection.Initializing)
|
||||
private patchConnection$ = new ReplaySubject<PatchConnection>(1)
|
||||
private wsSuccess$ = new BehaviorSubject(false)
|
||||
private polling$ = new BehaviorSubject(false)
|
||||
private patchDb: PatchDB<DataModel>
|
||||
private subs: Subscription[] = []
|
||||
private sources$: BehaviorSubject<Source<DataModel>[]> = new BehaviorSubject([
|
||||
this.wsSource,
|
||||
this.sources[0],
|
||||
])
|
||||
|
||||
data: DataModel
|
||||
@@ -61,18 +61,18 @@ export class PatchDbService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(PATCH_SOURCE) private readonly wsSource: Source<DataModel>,
|
||||
@Inject(PATCH_SOURCE) private readonly pollSource: Source<DataModel>,
|
||||
@Inject(PATCH_HTTP) private readonly http: ApiService,
|
||||
// [wsSources, pollSources]
|
||||
@Inject(PATCH_SOURCE) private readonly sources: Source<DataModel>[],
|
||||
@Inject(BOOTSTRAPPER)
|
||||
private readonly bootstrapper: Bootstrapper<DataModel>,
|
||||
@Inject(AUTH) private readonly auth: AuthService,
|
||||
@Inject(STORAGE) private readonly storage: Storage,
|
||||
private readonly http: ApiService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly storage: Storage,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const cache = await this.bootstrapper.init()
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
|
||||
this.patchDb = new PatchDB(this.sources$, this.http, cache)
|
||||
|
||||
@@ -94,13 +94,13 @@ export class PatchDbService {
|
||||
console.log('patchDB: POLLING FAILED', e)
|
||||
this.patchConnection$.next(PatchConnection.Disconnected)
|
||||
await pauseFor(2000)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
return
|
||||
}
|
||||
|
||||
console.log('patchDB: WEBSOCKET FAILED', e)
|
||||
this.polling$.next(true)
|
||||
this.sources$.next([this.pollSource, this.http])
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
@@ -152,7 +152,7 @@ export class PatchDbService {
|
||||
console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.polling$.next(false)
|
||||
this.sources$.next([this.wsSource, this.http])
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -180,19 +180,14 @@ export class PatchDbService {
|
||||
|
||||
// prettier-ignore
|
||||
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
|
||||
// TODO: refactor with a better solution to race condition
|
||||
const argsString = '/' + args.join('/')
|
||||
const source$ =
|
||||
this.patchDb?.store.watch$(...(args as [])) ||
|
||||
this.patchConnection$.pipe(
|
||||
skip(1),
|
||||
take(1),
|
||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
||||
)
|
||||
|
||||
console.log('patchDB: WATCHING ', argsString)
|
||||
|
||||
return source$.pipe(
|
||||
return this.patchConnection$.pipe(
|
||||
filter(status => status === PatchConnection.Connected),
|
||||
take(1),
|
||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
||||
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
|
||||
catchError(e => {
|
||||
console.error('patchDB: WATCH ERROR', e)
|
||||
|
||||
@@ -47,18 +47,25 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus {
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus {
|
||||
if (status.main.status === PackageMainStatus.Running) {
|
||||
const values = Object.values(status.main.health)
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return HealthStatus.Failure
|
||||
} else if (values.some(h => h.result === 'starting')) {
|
||||
return HealthStatus.Starting
|
||||
} else if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
} else {
|
||||
return HealthStatus.Healthy
|
||||
}
|
||||
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
||||
return
|
||||
}
|
||||
|
||||
const values = Object.values(status.main.health)
|
||||
|
||||
if (values.some(h => h.result === 'failure')) {
|
||||
return HealthStatus.Failure
|
||||
}
|
||||
|
||||
if (values.some(h => h.result === 'starting')) {
|
||||
return HealthStatus.Starting
|
||||
}
|
||||
|
||||
if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
}
|
||||
|
||||
return HealthStatus.Healthy
|
||||
}
|
||||
|
||||
export interface StatusRendering {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ServerConfigService {
|
||||
|
||||
try {
|
||||
await this.saveFns[key](data)
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
|
||||
@@ -5,5 +5,5 @@ import { Injectable } from '@angular/core'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SplitPaneTracker {
|
||||
sidebarOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||
}
|
||||
readonly sidebarOpen$ = new BehaviorSubject<boolean>(false)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface InstallProgress {
|
||||
size: number | null
|
||||
downloaded: number
|
||||
'download-complete': boolean
|
||||
validated: number
|
||||
'validation-complete': boolean
|
||||
unpacked: number
|
||||
'unpack-complete': boolean
|
||||
readonly size: number | null
|
||||
readonly downloaded: number
|
||||
readonly 'download-complete': boolean
|
||||
readonly validated: number
|
||||
readonly 'validation-complete': boolean
|
||||
readonly unpacked: number
|
||||
readonly 'unpack-complete': boolean
|
||||
}
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
import { Observable, from, interval, race, OperatorFunction, Observer, combineLatest } from 'rxjs'
|
||||
import { take, map, concatMap } from 'rxjs/operators'
|
||||
import {
|
||||
Observable,
|
||||
from,
|
||||
interval,
|
||||
race,
|
||||
OperatorFunction,
|
||||
Observer,
|
||||
combineLatest,
|
||||
} from 'rxjs'
|
||||
import { take, map } from 'rxjs/operators'
|
||||
|
||||
export function fromAsync$<S, T> (async: (s: S) => Promise<T>, s: S): Observable<T>
|
||||
export function fromAsync$<T> (async: () => Promise<T>): Observable<T>
|
||||
export function fromAsync$<S, T> (async: (s: S) => Promise<T>, s?: S): Observable<T> {
|
||||
export function fromAsync$<S, T>(
|
||||
async: (s: S) => Promise<T>,
|
||||
s: S,
|
||||
): Observable<T>
|
||||
export function fromAsync$<T>(async: () => Promise<T>): Observable<T>
|
||||
export function fromAsync$<S, T>(
|
||||
async: (s: S) => Promise<T>,
|
||||
s?: S,
|
||||
): Observable<T> {
|
||||
return from(async(s as S))
|
||||
}
|
||||
|
||||
export function fromAsyncP<T> (async: () => Promise<T>): Promise<T>
|
||||
export function fromAsyncP<S, T> (async: (s: S) => Promise<T>, s?: S): Promise<T> {
|
||||
export function fromAsyncP<T>(async: () => Promise<T>): Promise<T>
|
||||
export function fromAsyncP<S, T>(
|
||||
async: (s: S) => Promise<T>,
|
||||
s?: S,
|
||||
): Promise<T> {
|
||||
return async(s as S)
|
||||
}
|
||||
|
||||
// emits + completes after ms
|
||||
export function emitAfter$ (ms: number): Observable<number> {
|
||||
export function emitAfter$(ms: number): Observable<number> {
|
||||
return interval(ms).pipe(take(1))
|
||||
}
|
||||
|
||||
// throws unless source observable emits withing timeout
|
||||
export function throwIn<T> (timeout: number): OperatorFunction<T, T> {
|
||||
return o => race(
|
||||
o,
|
||||
emitAfter$(timeout).pipe(map(() => { throw new Error('timeout') } )))
|
||||
export function throwIn<T>(timeout: number): OperatorFunction<T, T> {
|
||||
return o =>
|
||||
race(
|
||||
o,
|
||||
emitAfter$(timeout).pipe(
|
||||
map(() => {
|
||||
throw new Error('timeout')
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// o.pipe(squash) : Observable<void> regardless of o emission type.
|
||||
export const squash = map(() => { })
|
||||
export const squash = map(() => {})
|
||||
|
||||
/*
|
||||
The main purpose of fromSync$ is to normalize error handling during a sequence
|
||||
@@ -59,10 +82,10 @@ export const squash = map(() => { })
|
||||
}
|
||||
```
|
||||
*/
|
||||
export function fromSync$<S, T> (sync: (s: S) => T, s: S): Observable<T>
|
||||
export function fromSync$<T> (sync: () => T): Observable<T>
|
||||
export function fromSync$<S, T> (sync: (s: S) => T, s?: S): Observable<T> {
|
||||
return new Observable( (subscriber: Observer<T>) => {
|
||||
export function fromSync$<S, T>(sync: (s: S) => T, s: S): Observable<T>
|
||||
export function fromSync$<T>(sync: () => T): Observable<T>
|
||||
export function fromSync$<S, T>(sync: (s: S) => T, s?: S): Observable<T> {
|
||||
return new Observable((subscriber: Observer<T>) => {
|
||||
try {
|
||||
subscriber.next(sync(s as S))
|
||||
subscriber.complete()
|
||||
@@ -71,24 +94,3 @@ export function fromSync$<S, T> (sync: (s: S) => T, s?: S): Observable<T> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
this function concats the current values (e.g in behavior subjects) or next values (in traditional observables) to a collection of values in a pipe.
|
||||
e.g. if t: Observable<T>, and o1: Observable<O1> o2: Observable<O2> then t.pipe(concatObservableValues([o1, o2])): Observable<[T1, O1, O2]> and emits iff t emits.
|
||||
Note that the standard combineLatest([t, o1, o2]) is also of type Observable<[T, O2, O2]>, but this observable triggers when any of t, o1, o2 emits.
|
||||
*/
|
||||
export function concatObservableValues<T, O> (observables: [Observable<O>]): OperatorFunction<T, [T, O]>
|
||||
export function concatObservableValues<T, O> (observables: [Observable<O>]): OperatorFunction<[T], [T, O]>
|
||||
export function concatObservableValues<T1, T2, O> (observables: [Observable<O>]): OperatorFunction<[T1, T2], [T1, T2, O]>
|
||||
export function concatObservableValues<T, O1, O2> (observables: [Observable<O1>, Observable<O2>]): OperatorFunction<[T], [T, O1, O2]>
|
||||
export function concatObservableValues<T1, T2, O1, O2> (observables: [Observable<O1>, Observable<O2>]): OperatorFunction<[T1, T2], [T1, T2, O1, O2]>
|
||||
export function concatObservableValues (observables: Observable<any>[]): OperatorFunction<any[], any[]> {
|
||||
return o => o.pipe(concatMap(args => combineLatest(observables).pipe(
|
||||
map(obs => {
|
||||
if (!(args instanceof Array)) return [args, ...obs]
|
||||
return [...args, ...obs]
|
||||
}),
|
||||
take(1),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -8,6 +8,16 @@
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"alwaysStrict": true,
|
||||
// "strictNullChecks": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
// "strictPropertyInitialization": true,
|
||||
// "noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
|
||||
2
patch-db
2
patch-db
Submodule patch-db updated: 35973d7aef...6f6c26acd4
Reference in New Issue
Block a user