mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
[Fix] websocket connecting and patchDB connection monitoring (#1738)
* refactor how we handle rpc responses and patchdb connection monitoring * websockets only * remove unused global error handlers * chore: clear storage inside auth service * feat: convert all global toasts to declarative approach (#1754) * no more reference to serverID Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
@@ -18,4 +18,5 @@
|
||||
<ion-footer>
|
||||
<footer appFooter></footer>
|
||||
</ion-footer>
|
||||
<toast-container></toast-container>
|
||||
</ion-app>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, Inject, OnDestroy } from '@angular/core'
|
||||
import { Component, OnDestroy } from '@angular/core'
|
||||
import { merge } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { merge, Observable } from 'rxjs'
|
||||
import { GLOBAL_SERVICE } from './app/global/global.module'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -10,13 +11,13 @@ import { GLOBAL_SERVICE } from './app/global/global.module'
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent implements OnDestroy {
|
||||
readonly subscription = merge(...this.services).subscribe()
|
||||
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_SERVICE)
|
||||
private readonly services: readonly Observable<unknown>[],
|
||||
readonly authService: AuthService,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
splitPaneVisible({ detail }: any) {
|
||||
|
||||
@@ -17,8 +17,8 @@ import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
import { EnterModule } from './app/enter/enter.module'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { GlobalModule } from './app/global/global.module'
|
||||
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||
import { ToastContainerModule } from './components/toast-container/toast-container.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@@ -45,8 +45,8 @@ import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||
MonacoEditorModule,
|
||||
SharedPipesModule,
|
||||
MarketplaceModule,
|
||||
GlobalModule,
|
||||
PatchDbModule,
|
||||
ToastContainerModule,
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -5,12 +5,10 @@ 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 { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
|
||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { LocalStorageService } from './services/local-storage.service'
|
||||
import { DataModel } from './services/patch-db/data-model'
|
||||
@@ -30,10 +28,6 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { heightCollapse } from '../../util/animations'
|
||||
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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'
|
||||
|
||||
export const GLOBAL_SERVICE = new InjectionToken<
|
||||
readonly Observable<unknown>[]
|
||||
>('A multi token of global Observable services')
|
||||
|
||||
// This module is purely for providers organization purposes
|
||||
@NgModule({
|
||||
providers: [
|
||||
[
|
||||
ConnectionMonitorService,
|
||||
LogoutService,
|
||||
OfflineService,
|
||||
RefreshToastService,
|
||||
UnreadToastService,
|
||||
UpdateToastService,
|
||||
].map(useClass),
|
||||
[PatchDataService, PatchMonitorService].map(useExisting),
|
||||
],
|
||||
})
|
||||
export class GlobalModule {}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { 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',
|
||||
}
|
||||
default:
|
||||
return { message: '' }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
readonly link?: string
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ToastController, ToastOptions } from '@ionic/angular'
|
||||
import { EMPTY, Observable, ObservableInput } from 'rxjs'
|
||||
import { filter, pairwise, switchMap, tap } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PatchDataService } from './patch-data.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
// Watch unread notification count to display toast
|
||||
@Injectable()
|
||||
export class UnreadToastService extends Observable<unknown> {
|
||||
private unreadToast?: HTMLIonToastElement
|
||||
|
||||
private readonly stream$ = this.patchData.pipe(
|
||||
switchMap<DataModel | null, ObservableInput<number>>(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()
|
||||
}),
|
||||
)
|
||||
|
||||
TOAST: ToastOptions = {
|
||||
header: 'Embassy',
|
||||
message: `New notifications`,
|
||||
position: 'bottom',
|
||||
duration: 4000,
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => true,
|
||||
},
|
||||
{
|
||||
side: 'end',
|
||||
text: 'View',
|
||||
handler: () => {
|
||||
this.router.navigate(['/notifications'], {
|
||||
queryParams: { toast: true },
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly patchData: PatchDataService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
|
||||
private async showToast() {
|
||||
await this.unreadToast?.dismiss()
|
||||
|
||||
this.unreadToast = await this.toastCtrl.create(this.TOAST)
|
||||
this.unreadToast.buttons?.push()
|
||||
|
||||
await this.unreadToast.present()
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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))
|
||||
}
|
||||
|
||||
LOADER: LoadingOptions = {
|
||||
message: 'Restarting...',
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
side: 'end',
|
||||
text: 'Restart',
|
||||
handler: () => {
|
||||
this.restart()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
private async showToast() {
|
||||
await this.updateToast?.dismiss()
|
||||
|
||||
this.updateToast = await this.toastCtrl.create(this.TOAST)
|
||||
|
||||
await this.updateToast.present()
|
||||
}
|
||||
|
||||
private async restart(): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create(this.LOADER)
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
} catch (e: any) {
|
||||
await this.errToast.present(e)
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export class MenuComponent {
|
||||
|
||||
// should wipe cache independent of actual BE logout
|
||||
private logout() {
|
||||
this.embassyApi.logout({})
|
||||
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
this.authService.setUnverified()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="wrapper">
|
||||
<ion-badge
|
||||
*ngIf="unreadCount && !sidebarOpen"
|
||||
*ngIf="!(sidebarOpen$ | async) && (unreadCount$ | async) as unreadCount"
|
||||
mode="md"
|
||||
class="md-badge"
|
||||
color="danger"
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { combineLatest, Subscription } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'badge-menu-button',
|
||||
templateUrl: './badge-menu.component.html',
|
||||
styleUrls: ['./badge-menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BadgeMenuComponent {
|
||||
unreadCount = 0
|
||||
sidebarOpen = false
|
||||
|
||||
subs: Subscription[] = []
|
||||
unreadCount$ = this.patch.watch$('server-info', 'unread-notification-count')
|
||||
sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
constructor(
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDbService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.subs = [
|
||||
combineLatest([
|
||||
this.patch.watch$('server-info', 'unread-notification-count'),
|
||||
this.splitPane.sidebarOpen$,
|
||||
]).subscribe(([unread, menu]) => {
|
||||
this.unreadCount = unread
|
||||
this.sidebarOpen = menu
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
id="scroller"
|
||||
*ngIf="!loading && needInfinite"
|
||||
position="top"
|
||||
threshold="0"
|
||||
threshold="1000"
|
||||
(ionInfinite)="doInfinite($event)"
|
||||
>
|
||||
<ion-infinite-scroll-content
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Component, Inject, Input, ViewChild } from '@angular/core'
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonContent, LoadingController } from '@ionic/angular'
|
||||
import { map, takeUntil, timer } from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
@@ -48,11 +47,10 @@ export class LogsComponent {
|
||||
isOnBottom = true
|
||||
autoScroll = true
|
||||
websocketFail = false
|
||||
limit = 200
|
||||
limit = 400
|
||||
toProcess: Log[] = []
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly api: ApiService,
|
||||
@@ -63,17 +61,13 @@ export class LogsComponent {
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const { 'start-cursor': startCursor, guid } = await this.followLogs({
|
||||
limit: 100,
|
||||
limit: this.limit,
|
||||
})
|
||||
|
||||
this.startCursor = startCursor
|
||||
|
||||
const host = this.document.location.host
|
||||
const protocol =
|
||||
this.document.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
const config: WebSocketSubjectConfig<Log> = {
|
||||
url: `${protocol}://${host}/ws/rpc/${guid}`,
|
||||
url: `/rpc/${guid}`,
|
||||
openObserver: {
|
||||
next: () => {
|
||||
console.log('**** LOGS WEBSOCKET OPEN ****')
|
||||
@@ -159,7 +153,7 @@ export class LogsComponent {
|
||||
}
|
||||
|
||||
private processJob() {
|
||||
timer(0, 500)
|
||||
timer(100, 500)
|
||||
.pipe(
|
||||
map((_, index) => index),
|
||||
takeUntil(this.destroy$),
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<p
|
||||
[style.color]="
|
||||
(disconnected$ | async)
|
||||
? 'gray'
|
||||
: 'var(--ion-color-' + rendering.color + ')'
|
||||
(connected$ | async) ? 'var(--ion-color-' + rendering.color + ')' : 'gray'
|
||||
"
|
||||
[style.font-size]="size"
|
||||
[style.font-style]="style"
|
||||
[style.font-weight]="weight"
|
||||
>
|
||||
<span *ngIf="!installProgress">
|
||||
{{ (disconnected$ | async) ? 'Unknown' : rendering.display }}
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
<span
|
||||
*ngIf="
|
||||
|
||||
@@ -23,7 +23,7 @@ export class StatusComponent {
|
||||
@Input() installProgress?: InstallProgress
|
||||
@Input() sigtermTimeout?: string | null = null
|
||||
|
||||
disconnected$ = this.connectionService.watchDisconnected$()
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<toast
|
||||
*ngIf="visible$ | async as message"
|
||||
header="Embassy"
|
||||
[duration]="4000"
|
||||
(dismiss)="onDismiss()"
|
||||
>
|
||||
New notifications
|
||||
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||
<a
|
||||
toastButton
|
||||
side="end"
|
||||
routerLink="/notifications"
|
||||
[queryParams]="{ toast: true }"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</toast>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Observable, Subject, merge } from 'rxjs'
|
||||
|
||||
import { NotificationsToastService } from './notifications-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'notifications-toast',
|
||||
templateUrl: './notifications-toast.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NotificationsToastComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(
|
||||
this.dismiss$,
|
||||
this.notifications$,
|
||||
)
|
||||
|
||||
constructor(
|
||||
@Inject(NotificationsToastService)
|
||||
private readonly notifications$: Observable<boolean>,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { endWith, Observable } from 'rxjs'
|
||||
import { filter, map, pairwise } from 'rxjs/operators'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NotificationsToastService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch
|
||||
.watch$('server-info', 'unread-notification-count')
|
||||
.pipe(
|
||||
filter(exists),
|
||||
pairwise(),
|
||||
map(([prev, cur]) => cur > prev),
|
||||
endWith(false),
|
||||
)
|
||||
|
||||
constructor(private readonly patch: PatchDbService) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<toast
|
||||
*ngIf="content$ | async as content"
|
||||
class="warning-toast"
|
||||
header="Unable to Connect"
|
||||
(dismiss)="onDismiss()"
|
||||
>
|
||||
{{ content.message }}
|
||||
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||
<a
|
||||
*ngIf="content.link"
|
||||
toastButton
|
||||
side="end"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="content.link"
|
||||
>
|
||||
View solutions
|
||||
</a>
|
||||
</toast>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Observable, Subject, merge, tap, map } from 'rxjs'
|
||||
|
||||
import { OfflineMessage, OfflineToastService } from './offline-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'offline-toast',
|
||||
templateUrl: './offline-toast.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OfflineToastComponent {
|
||||
private readonly dismiss$ = new Subject<null>()
|
||||
|
||||
readonly content$ = merge(this.dismiss$, this.failure$)
|
||||
|
||||
constructor(
|
||||
@Inject(OfflineToastService)
|
||||
private readonly failure$: Observable<OfflineMessage | null>,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { combineLatest, Observable, of } from 'rxjs'
|
||||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
export interface OfflineMessage {
|
||||
readonly message: string
|
||||
readonly link?: string
|
||||
}
|
||||
|
||||
// Watch for connection status
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OfflineToastService extends Observable<OfflineMessage | null> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
switchMap(verified => (verified ? this.failure$ : of(null))),
|
||||
)
|
||||
|
||||
private readonly failure$ = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
this.connectionService.websocketConnected$,
|
||||
]).pipe(
|
||||
map(([network, websocket]) => {
|
||||
if (!network) return { message: 'No Internet' }
|
||||
if (!websocket)
|
||||
return {
|
||||
message: 'Connecting to Embassy...',
|
||||
link: 'https://start9.com/latest/support/common-issues',
|
||||
}
|
||||
return null
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<alert *ngIf="show$ | async" header="Refresh needed" (dismiss)="onDismiss()">
|
||||
Your user interface is cached and out of date. Hard refresh the page to get
|
||||
the latest UI.
|
||||
<a alertButton class="enter-click" href=".">Refresh Page</a>
|
||||
</alert>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Observable, Subject, merge } from 'rxjs'
|
||||
|
||||
import { RefreshAlertService } from './refresh-alert.service'
|
||||
|
||||
@Component({
|
||||
selector: 'refresh-alert',
|
||||
templateUrl: './refresh-alert.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly show$ = merge(this.dismiss$, this.refresh$)
|
||||
|
||||
constructor(
|
||||
@Inject(RefreshAlertService) private readonly refresh$: Observable<boolean>,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { endWith, Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
|
||||
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||
import { ConfigService } from '../../../services/config.service'
|
||||
|
||||
// Watch for connection status
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RefreshAlertService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch.watch$('server-info', 'version').pipe(
|
||||
map(version => !!this.emver.compare(this.config.version, version)),
|
||||
endWith(false),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly emver: Emver,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<notifications-toast></notifications-toast>
|
||||
<offline-toast></offline-toast>
|
||||
<refresh-alert></refresh-alert>
|
||||
<update-toast></update-toast>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'toast-container',
|
||||
templateUrl: './toast-container.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToastContainerComponent {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { AlertModule, ToastModule } from '@start9labs/shared'
|
||||
|
||||
import { ToastContainerComponent } from './toast-container.component'
|
||||
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'
|
||||
import { OfflineToastComponent } from './offline-toast/offline-toast.component'
|
||||
import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component'
|
||||
import { UpdateToastComponent } from './update-toast/update-toast.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ToastModule, AlertModule, RouterModule],
|
||||
declarations: [
|
||||
ToastContainerComponent,
|
||||
NotificationsToastComponent,
|
||||
OfflineToastComponent,
|
||||
RefreshAlertComponent,
|
||||
UpdateToastComponent,
|
||||
],
|
||||
exports: [ToastContainerComponent],
|
||||
})
|
||||
export class ToastContainerModule {}
|
||||
@@ -0,0 +1,11 @@
|
||||
<toast
|
||||
*ngIf="visible$ | async as message"
|
||||
class="success-toast"
|
||||
header="EOS download complete!"
|
||||
(dismiss)="onDismiss()"
|
||||
>
|
||||
Restart your Embassy for these updates to take effect. It can take several
|
||||
minutes to come back online.
|
||||
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||
<button toastButton side="end" (click)="restart()">Restart</button>
|
||||
</toast>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { LoadingController } from '@ionic/angular'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { Observable, Subject, merge } from 'rxjs'
|
||||
|
||||
import { UpdateToastService } from './update-toast.service'
|
||||
import { ApiService } from '../../../services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'update-toast',
|
||||
templateUrl: './update-toast.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UpdateToastComponent {
|
||||
private readonly dismiss$ = new Subject<boolean>()
|
||||
|
||||
readonly visible$: Observable<boolean> = merge(this.dismiss$, this.update$)
|
||||
|
||||
constructor(
|
||||
@Inject(UpdateToastService) private readonly update$: Observable<boolean>,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
) {}
|
||||
|
||||
onDismiss() {
|
||||
this.dismiss$.next(false)
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
this.onDismiss()
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Restarting...',
|
||||
})
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
} catch (e: any) {
|
||||
await this.errToast.present(e)
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { endWith, Observable } from 'rxjs'
|
||||
import { distinctUntilChanged, filter } from 'rxjs/operators'
|
||||
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UpdateToastService extends Observable<boolean> {
|
||||
private readonly stream$ = this.patch
|
||||
.watch$('server-info', 'status-info', 'updated')
|
||||
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false))
|
||||
|
||||
constructor(private readonly patch: PatchDbService) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { map, take } from 'rxjs/operators'
|
||||
import { filter, map, take } from 'rxjs/operators'
|
||||
import { PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@@ -29,6 +29,7 @@ export class BackupSelectPage {
|
||||
this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
map(pkgs => {
|
||||
return Object.values(pkgs).map(pkg => {
|
||||
const { id, title } = pkg.manifest
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
selector: 'app-actions',
|
||||
templateUrl: './app-actions.page.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppActionsPage {
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
@@ -103,7 +104,7 @@ export class AppActionsPage {
|
||||
} else if (last) {
|
||||
statusesStr = `${last}`
|
||||
} else {
|
||||
error = `There is state for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
||||
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
||||
}
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Forbidden',
|
||||
@@ -158,10 +159,12 @@ export class AppActionsPage {
|
||||
|
||||
try {
|
||||
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
||||
this.embassyApi.setDbValue({
|
||||
pointer: `/ack-instructions/${this.pkgId}`,
|
||||
value: false,
|
||||
})
|
||||
this.embassyApi
|
||||
.setDbValue({
|
||||
pointer: `/ack-instructions/${this.pkgId}`,
|
||||
value: false,
|
||||
})
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
@@ -185,7 +188,7 @@ export class AppActionsPage {
|
||||
'action-id': actionId,
|
||||
input,
|
||||
})
|
||||
this.modalCtrl.dismiss()
|
||||
|
||||
const successModal = await this.modalCtrl.create({
|
||||
component: ActionSuccessPage,
|
||||
componentProps: {
|
||||
@@ -193,8 +196,8 @@ export class AppActionsPage {
|
||||
},
|
||||
})
|
||||
|
||||
setTimeout(() => successModal.present(), 400)
|
||||
return true
|
||||
setTimeout(() => successModal.present(), 500)
|
||||
return false
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
return false
|
||||
@@ -218,6 +221,7 @@ interface LocalAction {
|
||||
selector: 'app-actions-item',
|
||||
templateUrl: './app-actions-item.component.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppActionsItemComponent {
|
||||
@Input() action!: LocalAction
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<div
|
||||
*ngIf="disconnected$ | async; else connected"
|
||||
class="bulb"
|
||||
[style.background-color]="'var(--ion-color-dark)'"
|
||||
></div>
|
||||
<ng-template #connected>
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ion-icon
|
||||
*ngIf="pkg.error; else noError"
|
||||
class="warning-icon"
|
||||
@@ -28,4 +23,8 @@
|
||||
></div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #disconnected>
|
||||
<div class="bulb" [style.background-color]="'var(--ion-color-dark)'"></div>
|
||||
</ng-template>
|
||||
|
||||
@@ -12,7 +12,7 @@ export class AppListIconComponent {
|
||||
@Input()
|
||||
pkg!: PkgInfo
|
||||
|
||||
disconnected$ = this.connectionService.watchDisconnected$()
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
|
||||
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
|
||||
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators'
|
||||
import { RecoveredInfo } from 'src/app/util/parse-data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@@ -103,7 +103,7 @@ function loading(
|
||||
// Show notification on error
|
||||
catchError(e => from(errToast.present(e))),
|
||||
// Map any result to false to stop loading indicator
|
||||
mapTo(false),
|
||||
map(() => false),
|
||||
// Start operation with true
|
||||
startWith(true),
|
||||
)
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<!-- loading -->
|
||||
<ng-template #loading>
|
||||
<ng-container *ngIf="loading else loaded">
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-container *ngIf="connected$ | async else loading">
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
<app-list-empty
|
||||
*ngIf="empty; else list"
|
||||
class="ion-text-center ion-padding"
|
||||
@@ -39,5 +39,5 @@
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -14,12 +14,11 @@ import { parseDataModel, RecoveredInfo } from 'src/app/util/parse-data-model'
|
||||
providers: [DestroyService],
|
||||
})
|
||||
export class AppListPage {
|
||||
loading = true
|
||||
pkgs: readonly PackageDataEntry[] = []
|
||||
recoveredPkgs: readonly RecoveredInfo[] = []
|
||||
reordering = false
|
||||
|
||||
readonly connected$ = this.patch.connected$
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly destroy$: DestroyService,
|
||||
@@ -38,6 +37,7 @@ export class AppListPage {
|
||||
take(1),
|
||||
map(parseDataModel),
|
||||
tap(({ pkgs, recoveredPkgs }) => {
|
||||
this.loading = false
|
||||
this.pkgs = pkgs
|
||||
this.recoveredPkgs = recoveredPkgs
|
||||
}),
|
||||
|
||||
@@ -56,8 +56,8 @@ export class AppShowPage {
|
||||
readonly currentMarketplace$: Observable<Marketplace> =
|
||||
this.marketplaceService.getMarketplace()
|
||||
|
||||
readonly altMarketplaceData$: Observable<UIMarketplaceData | undefined> =
|
||||
this.marketplaceService.getAltMarketplace()
|
||||
readonly altMarketplaceData$: Observable<UIMarketplaceData> =
|
||||
this.marketplaceService.getAltMarketplaceData()
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
|
||||
@@ -3,19 +3,10 @@
|
||||
>
|
||||
<ng-container *ngIf="checks.length">
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<ng-container *ngIf="disconnected$ | async; else connected">
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-template #connected>
|
||||
<!-- connected -->
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<!-- result -->
|
||||
<ng-container *ngIf="health.value?.result as result; else noResult">
|
||||
<ion-spinner
|
||||
*ngIf="isLoading(result)"
|
||||
@@ -69,6 +60,7 @@
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ng-container>
|
||||
<!-- no result -->
|
||||
<ng-template #noResult>
|
||||
<ion-spinner
|
||||
class="icon-spinner"
|
||||
@@ -83,6 +75,18 @@
|
||||
</ion-label>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<!-- disconnected -->
|
||||
<ng-template #disconnected>
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -17,7 +17,7 @@ export class AppShowHealthChecksComponent {
|
||||
|
||||
HealthResult = HealthResult
|
||||
|
||||
readonly disconnected$ = this.connectionService.watchDisconnected$()
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="isInstalled && !(disconnected$ | async)">
|
||||
<ng-container *ngIf="isInstalled && (connected$ | async)">
|
||||
<ion-grid>
|
||||
<ion-row style="padding-left: 12px">
|
||||
<ion-col>
|
||||
|
||||
@@ -37,7 +37,7 @@ export class AppShowStatusComponent {
|
||||
|
||||
PR = PrimaryRendering
|
||||
|
||||
disconnected$ = this.connectionService.watchDisconnected$()
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
|
||||
@@ -117,10 +117,12 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
}
|
||||
|
||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||
this.apiService.setDbValue({
|
||||
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
||||
value: true,
|
||||
})
|
||||
this.apiService
|
||||
.setDbValue({
|
||||
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
||||
value: true,
|
||||
})
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
@@ -49,7 +48,7 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
'dependency-errors',
|
||||
),
|
||||
]).pipe(
|
||||
filter(deps => deps.every(exists) && !!pkg.installed),
|
||||
filter(deps => deps.every(Boolean) && !!pkg.installed),
|
||||
map(([currentDeps, depErrors]) =>
|
||||
Object.keys(currentDeps)
|
||||
.filter(id => !!pkg.manifest.dependencies[id])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { debounce, exists, ErrorToastService } from '@start9labs/shared'
|
||||
import { debounce, ErrorToastService } from '@start9labs/shared'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -31,7 +31,7 @@ export class DevConfigPage {
|
||||
ngOnInit() {
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId, 'config')
|
||||
.pipe(filter(exists), take(1))
|
||||
.pipe(filter(Boolean), take(1))
|
||||
.subscribe(config => {
|
||||
this.code = config
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import { filter, take } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
debounce,
|
||||
exists,
|
||||
ErrorToastService,
|
||||
MarkdownComponent,
|
||||
} from '@start9labs/shared'
|
||||
@@ -20,8 +19,8 @@ import { getProjectId } from 'src/app/util/get-project-id'
|
||||
export class DevInstructionsPage {
|
||||
readonly projectId = getProjectId(this.route)
|
||||
editorOptions = { theme: 'vs-dark', language: 'markdown' }
|
||||
code: string = ''
|
||||
saving: boolean = false
|
||||
code = ''
|
||||
saving = false
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
@@ -34,7 +33,7 @@ export class DevInstructionsPage {
|
||||
ngOnInit() {
|
||||
this.patchDb
|
||||
.watch$('ui', 'dev', this.projectId, 'instructions')
|
||||
.pipe(filter(exists), take(1))
|
||||
.pipe(filter(Boolean), take(1))
|
||||
.subscribe(config => {
|
||||
this.code = config
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
GenericInputComponent,
|
||||
@@ -17,7 +16,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { v4 } from 'uuid'
|
||||
import { DevData } from 'src/app/services/patch-db/data-model'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
|
||||
@@ -36,8 +34,6 @@ export class DeveloperListPage {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
@@ -13,6 +13,7 @@ import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
||||
selector: 'developer-menu',
|
||||
templateUrl: 'developer-menu.page.html',
|
||||
styleUrls: ['developer-menu.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DeveloperMenuPage {
|
||||
readonly projectId = getProjectId(this.route)
|
||||
|
||||
@@ -3,11 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
} from '@start9labs/shared'
|
||||
import { SharedPipesModule, EmverPipesModule } from '@start9labs/shared'
|
||||
import {
|
||||
FilterPackagesPipeModule,
|
||||
CategoriesModule,
|
||||
@@ -16,7 +12,6 @@ import {
|
||||
SkeletonModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||
import { MarketplaceListPage } from './marketplace-list.page'
|
||||
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
|
||||
@@ -34,7 +29,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
RouterModule.forChild(routes),
|
||||
TextSpinnerComponentModule,
|
||||
SharedPipesModule,
|
||||
EmverPipesModule,
|
||||
FilterPackagesPipeModule,
|
||||
|
||||
@@ -8,14 +8,9 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<marketplace-list-content
|
||||
*ngIf="connected$ | async else loading"
|
||||
[localPkgs]="(localPkgs$ | async) || {}"
|
||||
[pkgs]="pkgs$ | async"
|
||||
[categories]="categories$ | async"
|
||||
[name]="(name$ | async) || ''"
|
||||
></marketplace-list-content>
|
||||
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,45 +1,30 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, first, map, startWith, switchMapTo } from 'rxjs/operators'
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { AbstractMarketplaceService } 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 { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-list',
|
||||
templateUrl: './marketplace-list.page.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
readonly connected$ = this.patch.connected$
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
readonly localPkgs$: Observable<Record<string, PackageDataEntry>> = this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
startWith({}),
|
||||
)
|
||||
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
readonly categories$ = this.marketplaceService.getCategories()
|
||||
|
||||
readonly pkgs$: Observable<MarketplacePkg[]> = this.patch
|
||||
.watch$('server-info')
|
||||
.pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
first(),
|
||||
switchMapTo(this.marketplaceService.getPackages()),
|
||||
)
|
||||
readonly pkgs$ = this.marketplaceService.getPackages()
|
||||
|
||||
readonly name$: Observable<string> = this.marketplaceService
|
||||
readonly name$ = this.marketplaceService
|
||||
.getMarketplace()
|
||||
.pipe(map(({ name }) => name))
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
ServerNotifications,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
selector: 'lan',
|
||||
templateUrl: './lan.page.html',
|
||||
styleUrls: ['./lan.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LANPage {
|
||||
readonly downloadIsDisabled = !this.config.isTor()
|
||||
|
||||
@@ -11,12 +11,9 @@
|
||||
<ng-container *ngIf="ui$ | async as ui">
|
||||
<ion-item-group *ngIf="server$ | async as server">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
(click)="presentModalName('Embassy-' + server.id, ui.name)"
|
||||
>
|
||||
<ion-item button (click)="presentModalName('My Embassy', ui.name)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
|
||||
<ion-note slot="end">{{ ui.name || 'My Embassy' }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import {
|
||||
LoadingController,
|
||||
@@ -17,6 +17,7 @@ import { LocalStorageService } from '../../../services/local-storage.service'
|
||||
selector: 'preferences',
|
||||
templateUrl: './preferences.page.html',
|
||||
styleUrls: ['./preferences.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PreferencesPage {
|
||||
clicks = 0
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PipeTransform,
|
||||
} from '@angular/core'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@@ -15,7 +15,9 @@ import { Observable } from 'rxjs'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BackingUpComponent {
|
||||
readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1))
|
||||
readonly pkgs$ = this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(filter(Boolean), take(1))
|
||||
readonly backupProgress$ = this.patch.watch$(
|
||||
'server-info',
|
||||
'status-info',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title *ngIf="connected$ | async else loading">
|
||||
{{ (ui$ | async)?.name || "Embassy-" + (server$ | async)?.id }}
|
||||
<ion-title *ngIf="ui$ | async as ui; else loadingTitle">
|
||||
{{ ui.name || "My Embassy" }}
|
||||
</ion-title>
|
||||
<ng-template #loading>
|
||||
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
||||
<ng-template #loadingTitle>
|
||||
<ion-title>
|
||||
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
||||
</ion-title>
|
||||
</ng-template>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
@@ -14,86 +16,80 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<!-- loading -->
|
||||
<ng-template #spinner>
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-template>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-container *ngIf="connected$ | async else spinner">
|
||||
<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'"
|
||||
(click)="addClick()"
|
||||
>
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let button of cat.value">
|
||||
<ion-item
|
||||
button
|
||||
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
||||
[detail]="button.detail"
|
||||
[disabled]="button.disabled | async"
|
||||
(click)="button.action()"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
<!-- loaded -->
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<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'" (click)="addClick()">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let button of cat.value">
|
||||
<ion-item
|
||||
button
|
||||
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
||||
[detail]="button.detail"
|
||||
[disabled]="button.disabled$ | async"
|
||||
(click)="button.action()"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
|
||||
<!-- "Create Backup" button only -->
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||
<ion-text
|
||||
color="warning"
|
||||
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
||||
>
|
||||
Last Backup: {{ server['last-backup'] ?
|
||||
(server['last-backup'] | date: 'medium') : 'never' }}
|
||||
</ion-text>
|
||||
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
||||
<ion-spinner
|
||||
color="success"
|
||||
style="height: 12px; width: 12px; margin-right: 6px"
|
||||
></ion-spinner>
|
||||
<ion-text color="success">Backing up</ion-text>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<!-- "Software Update" button only -->
|
||||
<p *ngIf="button.title === 'Software Update'">
|
||||
<!-- "Create Backup" button only -->
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||
<ion-text
|
||||
*ngIf="server['status-info'].updated; else notUpdated"
|
||||
class="inline"
|
||||
color="warning"
|
||||
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
||||
>
|
||||
Update Complete. Restart to apply changes
|
||||
Last Backup: {{ server['last-backup'] ? (server['last-backup']
|
||||
| date: 'medium') : 'never' }}
|
||||
</ion-text>
|
||||
<ng-template #notUpdated>
|
||||
<ng-container *ngIf="showUpdate$ | async; else check">
|
||||
<ion-text class="inline" color="success">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
Update Available
|
||||
</ion-text>
|
||||
</ng-container>
|
||||
<ng-template #check>
|
||||
<ion-text class="inline" color="dark">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
Check for updates
|
||||
</ion-text>
|
||||
</ng-template>
|
||||
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
||||
<ion-spinner
|
||||
color="success"
|
||||
style="height: 12px; width: 12px; margin-right: 6px"
|
||||
></ion-spinner>
|
||||
<ion-text color="success">Backing up</ion-text>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<!-- "Software Update" button only -->
|
||||
<p *ngIf="button.title === 'Software Update'">
|
||||
<ion-text
|
||||
*ngIf="server['status-info'].updated; else notUpdated"
|
||||
class="inline"
|
||||
color="warning"
|
||||
>
|
||||
Update Complete. Restart to apply changes
|
||||
</ion-text>
|
||||
<ng-template #notUpdated>
|
||||
<ng-container *ngIf="showUpdate$ | async; else check">
|
||||
<ion-text class="inline" color="success">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
Update Available
|
||||
</ion-text>
|
||||
</ng-container>
|
||||
<ng-template #check>
|
||||
<ion-text class="inline" color="dark">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
Check for updates
|
||||
</ion-text>
|
||||
</ng-template>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -9,11 +9,10 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { filter, take, tap } from 'rxjs/operators'
|
||||
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
import { RecoveredPackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
|
||||
import { getAllPackages } from '../../../util/get-package-data'
|
||||
|
||||
@@ -28,7 +27,6 @@ export class ServerShowPage {
|
||||
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
readonly connected$ = this.patch.connected$
|
||||
readonly showUpdate$ = this.eosService.showUpdate$
|
||||
readonly showDiskRepair$ = this.localStorageService.showDiskRepair$
|
||||
|
||||
@@ -48,10 +46,12 @@ export class ServerShowPage {
|
||||
ngOnInit() {
|
||||
this.patch
|
||||
.watch$('recovered-packages')
|
||||
.pipe(filter(exists), take(1))
|
||||
.subscribe((rps: { [id: string]: RecoveredPackageDataEntry }) => {
|
||||
this.hasRecoveredPackage = !isEmptyObject(rps)
|
||||
})
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
tap(data => (this.hasRecoveredPackage = !isEmptyObject(data))),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async updateEos(): Promise<void> {
|
||||
@@ -290,7 +290,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Restore From Backup',
|
||||
@@ -299,7 +299,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: this.eosService.updatingOrBackingUp$,
|
||||
disabled$: this.eosService.updatingOrBackingUp$,
|
||||
},
|
||||
],
|
||||
Settings: [
|
||||
@@ -312,7 +312,7 @@ export class ServerShowPage {
|
||||
? this.updateEos()
|
||||
: this.checkForEosUpdate(),
|
||||
detail: false,
|
||||
disabled: this.eosService.updatingOrBackingUp$,
|
||||
disabled$: this.eosService.updatingOrBackingUp$,
|
||||
},
|
||||
{
|
||||
title: 'Preferences',
|
||||
@@ -323,7 +323,7 @@ export class ServerShowPage {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'LAN',
|
||||
@@ -332,7 +332,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
@@ -341,7 +341,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
@@ -350,7 +350,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Sideload Service',
|
||||
@@ -361,7 +361,7 @@ export class ServerShowPage {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Marketplace Settings',
|
||||
@@ -372,7 +372,7 @@ export class ServerShowPage {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
Insights: [
|
||||
@@ -383,7 +383,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Monitor',
|
||||
@@ -392,7 +392,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Active Sessions',
|
||||
@@ -403,7 +403,7 @@ export class ServerShowPage {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'OS Logs',
|
||||
@@ -412,7 +412,7 @@ export class ServerShowPage {
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Kernel Logs',
|
||||
@@ -424,7 +424,7 @@ export class ServerShowPage {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
Support: [
|
||||
@@ -439,7 +439,7 @@ export class ServerShowPage {
|
||||
'noreferrer',
|
||||
),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Contact Support',
|
||||
@@ -452,7 +452,7 @@ export class ServerShowPage {
|
||||
'noreferrer',
|
||||
),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
Power: [
|
||||
@@ -462,7 +462,7 @@ export class ServerShowPage {
|
||||
icon: 'reload',
|
||||
action: () => this.presentAlertRestart(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Shutdown',
|
||||
@@ -470,7 +470,7 @@ export class ServerShowPage {
|
||||
icon: 'power',
|
||||
action: () => this.presentAlertShutdown(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'System Rebuild',
|
||||
@@ -478,7 +478,7 @@ export class ServerShowPage {
|
||||
icon: 'construct-outline',
|
||||
action: () => this.presentAlertSystemRebuild(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Repair Disk',
|
||||
@@ -486,7 +486,7 @@ export class ServerShowPage {
|
||||
icon: 'medkit-outline',
|
||||
action: () => this.presentAlertRepairDisk(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -517,5 +517,5 @@ interface SettingBtn {
|
||||
icon: string
|
||||
action: Function
|
||||
detail: boolean
|
||||
disabled: Observable<boolean>
|
||||
disabled$: Observable<boolean>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
@@ -8,6 +8,7 @@ import { copyToClipboard } from '@start9labs/shared'
|
||||
selector: 'server-specs',
|
||||
templateUrl: './server-specs.page.html',
|
||||
styleUrls: ['./server-specs.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServerSpecsPage {
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
|
||||
@@ -29,6 +29,9 @@ export module RR {
|
||||
|
||||
// server
|
||||
|
||||
export type EchoReq = { message: string } // server.echo
|
||||
export type EchoRes = string
|
||||
|
||||
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsRes = LogsRes
|
||||
|
||||
|
||||
@@ -1,35 +1,12 @@
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import {
|
||||
Http,
|
||||
Update,
|
||||
Operation,
|
||||
Revision,
|
||||
Source,
|
||||
Store,
|
||||
RPCResponse,
|
||||
} from 'patch-db-client'
|
||||
import { Update, Operation, Revision } from 'patch-db-client'
|
||||
import { RR } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, RequestError } from '@start9labs/shared'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
|
||||
export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
protected readonly sync$ = new Subject<Update<DataModel>>()
|
||||
|
||||
/** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */
|
||||
// sequenceStream '_' is not used by the live api, but is overridden by the mock
|
||||
watch$(_?: Store<DataModel>): Observable<RPCResponse<Update<DataModel>>> {
|
||||
return this.sync$
|
||||
.asObservable()
|
||||
.pipe(map(result => ({ result, jsonrpc: '2.0' })))
|
||||
}
|
||||
|
||||
// websocket
|
||||
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
export abstract class ApiService {
|
||||
readonly sync$ = new Subject<Update<DataModel>>()
|
||||
|
||||
// http
|
||||
|
||||
@@ -63,6 +40,14 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
|
||||
// server
|
||||
|
||||
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
|
||||
|
||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
|
||||
abstract getServerLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService, Log, LogsRes, Method } from '@start9labs/shared'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
HttpService,
|
||||
Log,
|
||||
Method,
|
||||
RPCError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Observable, timeout } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { Update } from 'patch-db-client'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly http: HttpService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly auth: AuthService,
|
||||
) {
|
||||
super()
|
||||
; (window as any).rpcClient = this
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return webSocket(config)
|
||||
}
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
@@ -41,93 +49,114 @@ export class LiveApiService extends ApiService {
|
||||
// db
|
||||
|
||||
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
||||
return this.http.rpcRequest({ method: 'db.revisions', params: { since } })
|
||||
return this.rpcRequest({ method: 'db.revisions', params: { since } })
|
||||
}
|
||||
|
||||
async getDump(): Promise<RR.GetDumpRes> {
|
||||
return this.http.rpcRequest({ method: 'db.dump', params: {} })
|
||||
return this.rpcRequest({ method: 'db.dump', params: {} })
|
||||
}
|
||||
|
||||
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
return this.http.rpcRequest({ method: 'db.put.ui', params })
|
||||
return this.rpcRequest({ method: 'db.put.ui', params })
|
||||
}
|
||||
|
||||
// auth
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.login', params })
|
||||
return this.rpcRequest({ method: 'auth.login', params })
|
||||
}
|
||||
|
||||
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.logout', params })
|
||||
return this.rpcRequest({ method: 'auth.logout', params })
|
||||
}
|
||||
|
||||
async getSessions(params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.session.list', params })
|
||||
return this.rpcRequest({ method: 'auth.session.list', params })
|
||||
}
|
||||
|
||||
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
||||
return this.http.rpcRequest({ method: 'auth.session.kill', params })
|
||||
return this.rpcRequest({ method: 'auth.session.kill', params })
|
||||
}
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params })
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
const config: WebSocketSubjectConfig<Update<DataModel>> = {
|
||||
url: `/db`,
|
||||
closeObserver: {
|
||||
next: val => {
|
||||
if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return this.openWebsocket(config).pipe(timeout({ first: 21000 }))
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return this.openWebsocket(config)
|
||||
}
|
||||
|
||||
async getServerLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.logs', params })
|
||||
return this.rpcRequest({ method: 'server.logs', params })
|
||||
}
|
||||
|
||||
async getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.kernel-logs', params })
|
||||
return this.rpcRequest({ method: 'server.kernel-logs', params })
|
||||
}
|
||||
|
||||
async followServerLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.logs.follow', params })
|
||||
return this.rpcRequest({ method: 'server.logs.follow', params })
|
||||
}
|
||||
|
||||
async followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.kernel-logs.follow', params })
|
||||
return this.rpcRequest({ method: 'server.kernel-logs.follow', params })
|
||||
}
|
||||
|
||||
async getServerMetrics(
|
||||
params: RR.GetServerMetricsReq,
|
||||
): Promise<RR.GetServerMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'server.metrics', params })
|
||||
return this.rpcRequest({ method: 'server.metrics', params })
|
||||
}
|
||||
|
||||
async updateServerRaw(
|
||||
params: RR.UpdateServerReq,
|
||||
): Promise<RR.UpdateServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.update', params })
|
||||
return this.rpcRequest({ method: 'server.update', params })
|
||||
}
|
||||
|
||||
async restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.restart', params })
|
||||
return this.rpcRequest({ method: 'server.restart', params })
|
||||
}
|
||||
|
||||
async shutdownServer(
|
||||
params: RR.ShutdownServerReq,
|
||||
): Promise<RR.ShutdownServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.shutdown', params })
|
||||
return this.rpcRequest({ method: 'server.shutdown', params })
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'server.rebuild', params })
|
||||
return this.rpcRequest({ method: 'server.rebuild', params })
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.repair', params })
|
||||
return this.rpcRequest({ method: 'disk.repair', params })
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
@@ -135,7 +164,7 @@ export class LiveApiService extends ApiService {
|
||||
async marketplaceProxy<T>(path: string, qp: {}, url: string): Promise<T> {
|
||||
Object.assign(qp, { arch: this.config.targetArch })
|
||||
const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}`
|
||||
return this.http.rpcRequest({
|
||||
return this.rpcRequest({
|
||||
method: 'marketplace.get',
|
||||
params: { url: fullURL },
|
||||
})
|
||||
@@ -156,19 +185,19 @@ export class LiveApiService extends ApiService {
|
||||
async getNotificationsRaw(
|
||||
params: RR.GetNotificationsReq,
|
||||
): Promise<RR.GetNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.list', params })
|
||||
return this.rpcRequest({ method: 'notification.list', params })
|
||||
}
|
||||
|
||||
async deleteNotification(
|
||||
params: RR.DeleteNotificationReq,
|
||||
): Promise<RR.DeleteNotificationRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.delete', params })
|
||||
return this.rpcRequest({ method: 'notification.delete', params })
|
||||
}
|
||||
|
||||
async deleteAllNotifications(
|
||||
params: RR.DeleteAllNotificationsReq,
|
||||
): Promise<RR.DeleteAllNotificationsRes> {
|
||||
return this.http.rpcRequest({
|
||||
return this.rpcRequest({
|
||||
method: 'notification.delete-before',
|
||||
params,
|
||||
})
|
||||
@@ -180,39 +209,39 @@ export class LiveApiService extends ApiService {
|
||||
params: RR.GetWifiReq,
|
||||
timeout?: number,
|
||||
): Promise<RR.GetWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.get', params, timeout })
|
||||
return this.rpcRequest({ method: 'wifi.get', params, timeout })
|
||||
}
|
||||
|
||||
async setWifiCountry(
|
||||
params: RR.SetWifiCountryReq,
|
||||
): Promise<RR.SetWifiCountryRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.country.set', params })
|
||||
return this.rpcRequest({ method: 'wifi.country.set', params })
|
||||
}
|
||||
|
||||
async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.add', params })
|
||||
return this.rpcRequest({ method: 'wifi.add', params })
|
||||
}
|
||||
|
||||
async connectWifi(params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.connect', params })
|
||||
return this.rpcRequest({ method: 'wifi.connect', params })
|
||||
}
|
||||
|
||||
async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.delete', params })
|
||||
return this.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.list', params })
|
||||
return this.rpcRequest({ method: 'ssh.list', params })
|
||||
}
|
||||
|
||||
async addSshKey(params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.add', params })
|
||||
return this.rpcRequest({ method: 'ssh.add', params })
|
||||
}
|
||||
|
||||
async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.delete', params })
|
||||
return this.rpcRequest({ method: 'ssh.delete', params })
|
||||
}
|
||||
|
||||
// backup
|
||||
@@ -220,38 +249,38 @@ export class LiveApiService extends ApiService {
|
||||
async getBackupTargets(
|
||||
params: RR.GetBackupTargetsReq,
|
||||
): Promise<RR.GetBackupTargetsRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.list', params })
|
||||
return this.rpcRequest({ method: 'backup.target.list', params })
|
||||
}
|
||||
|
||||
async addBackupTarget(
|
||||
params: RR.AddBackupTargetReq,
|
||||
): Promise<RR.AddBackupTargetRes> {
|
||||
params.path = params.path.replace('/\\/g', '/')
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||
}
|
||||
|
||||
async updateBackupTarget(
|
||||
params: RR.UpdateBackupTargetReq,
|
||||
): Promise<RR.UpdateBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||
}
|
||||
|
||||
async removeBackupTarget(
|
||||
params: RR.RemoveBackupTargetReq,
|
||||
): Promise<RR.RemoveBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||
return this.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||
}
|
||||
|
||||
async getBackupInfo(
|
||||
params: RR.GetBackupInfoReq,
|
||||
): Promise<RR.GetBackupInfoRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.info', params })
|
||||
return this.rpcRequest({ method: 'backup.target.info', params })
|
||||
}
|
||||
|
||||
async createBackupRaw(
|
||||
params: RR.CreateBackupReq,
|
||||
): Promise<RR.CreateBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
return this.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
// package
|
||||
@@ -267,95 +296,95 @@ export class LiveApiService extends ApiService {
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'package.logs', params })
|
||||
return this.rpcRequest({ method: 'package.logs', params })
|
||||
}
|
||||
|
||||
async followPackageLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.http.rpcRequest({ method: 'package.logs.follow', params })
|
||||
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||
}
|
||||
|
||||
async getPkgMetrics(
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'package.metrics', params })
|
||||
return this.rpcRequest({ method: 'package.metrics', params })
|
||||
}
|
||||
|
||||
async installPackageRaw(
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.install', params })
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.update.dry', params })
|
||||
return this.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.get', params })
|
||||
return this.rpcRequest({ method: 'package.config.get', params })
|
||||
}
|
||||
|
||||
async drySetPackageConfig(
|
||||
params: RR.DrySetPackageConfigReq,
|
||||
): Promise<RR.DrySetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
return this.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
}
|
||||
|
||||
async setPackageConfigRaw(
|
||||
params: RR.SetPackageConfigReq,
|
||||
): Promise<RR.SetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
||||
return this.rpcRequest({ method: 'package.config.set', params })
|
||||
}
|
||||
|
||||
async restorePackagesRaw(
|
||||
params: RR.RestorePackagesReq,
|
||||
): Promise<RR.RestorePackagesRes> {
|
||||
return this.http.rpcRequest({ method: 'package.backup.restore', params })
|
||||
return this.rpcRequest({ method: 'package.backup.restore', params })
|
||||
}
|
||||
|
||||
async executePackageAction(
|
||||
params: RR.ExecutePackageActionReq,
|
||||
): Promise<RR.ExecutePackageActionRes> {
|
||||
return this.http.rpcRequest({ method: 'package.action', params })
|
||||
return this.rpcRequest({ method: 'package.action', params })
|
||||
}
|
||||
|
||||
async startPackageRaw(
|
||||
params: RR.StartPackageReq,
|
||||
): Promise<RR.StartPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.start', params })
|
||||
return this.rpcRequest({ method: 'package.start', params })
|
||||
}
|
||||
|
||||
async restartPackageRaw(
|
||||
params: RR.RestartPackageReq,
|
||||
): Promise<RR.RestartPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.restart', params })
|
||||
return this.rpcRequest({ method: 'package.restart', params })
|
||||
}
|
||||
|
||||
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop', params })
|
||||
return this.rpcRequest({ method: 'package.stop', params })
|
||||
}
|
||||
|
||||
async deleteRecoveredPackageRaw(
|
||||
params: RR.DeleteRecoveredPackageReq,
|
||||
): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.delete-recovered', params })
|
||||
return this.rpcRequest({ method: 'package.delete-recovered', params })
|
||||
}
|
||||
|
||||
async uninstallPackageRaw(
|
||||
params: RR.UninstallPackageReq,
|
||||
): Promise<RR.UninstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.uninstall', params })
|
||||
return this.rpcRequest({ method: 'package.uninstall', params })
|
||||
}
|
||||
|
||||
async dryConfigureDependency(
|
||||
params: RR.DryConfigureDependencyReq,
|
||||
): Promise<RR.DryConfigureDependencyRes> {
|
||||
return this.http.rpcRequest({
|
||||
return this.rpcRequest({
|
||||
method: 'package.dependency.configure.dry',
|
||||
params,
|
||||
})
|
||||
@@ -364,9 +393,29 @@ export class LiveApiService extends ApiService {
|
||||
async sideloadPackage(
|
||||
params: RR.SideloadPackageReq,
|
||||
): Promise<RR.SideloadPacakgeRes> {
|
||||
return this.http.rpcRequest({
|
||||
return this.rpcRequest({
|
||||
method: 'package.sideload',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
|
||||
config.url = `${protocol}://${host}/ws${config.url}`
|
||||
|
||||
return webSocket(config)
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(options: RPCOptions): Promise<T> {
|
||||
return this.http.rpcRequest<T>(options).catch(e => {
|
||||
if ((e as RPCError).error.code === 34) {
|
||||
console.error('Unauthenticated, logging out')
|
||||
this.auth.setUnverified()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,16 +44,6 @@ export class MockApiService extends ApiService {
|
||||
super()
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(100).pipe(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
@@ -120,6 +110,25 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
return this.mockPatch$
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(100).pipe(
|
||||
map((_, index) => {
|
||||
// mock fire open observer
|
||||
if (index === 0) config.openObserver?.next(new Event(''))
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async getServerLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -291,7 +300,6 @@ export class MockApiService extends ApiService {
|
||||
value: 0,
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch, Mock.Notifications)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Observable, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import { ReplaySubject } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { Router } from '@angular/router'
|
||||
|
||||
export enum AuthState {
|
||||
UNVERIFIED,
|
||||
@@ -14,19 +15,23 @@ export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||
|
||||
readonly isVerified$ = this.watch$().pipe(
|
||||
readonly isVerified$ = this.authState$.pipe(
|
||||
map(state => state === AuthState.VERIFIED),
|
||||
)
|
||||
|
||||
constructor(private readonly storage: Storage) {}
|
||||
constructor(
|
||||
private readonly storage: Storage,
|
||||
private readonly zone: NgZone,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
||||
this.authState$.next(loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
||||
}
|
||||
|
||||
watch$(): Observable<AuthState> {
|
||||
return this.authState$.pipe(distinctUntilChanged())
|
||||
if (loggedIn) {
|
||||
this.setVerified()
|
||||
} else {
|
||||
this.setUnverified()
|
||||
}
|
||||
}
|
||||
|
||||
async setVerified(): Promise<void> {
|
||||
@@ -34,7 +39,11 @@ export class AuthService {
|
||||
this.authState$.next(AuthState.VERIFIED)
|
||||
}
|
||||
|
||||
async setUnverified(): Promise<void> {
|
||||
setUnverified(): void {
|
||||
this.authState$.next(AuthState.UNVERIFIED)
|
||||
this.storage.clear()
|
||||
this.zone.run(() => {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const {
|
||||
targetArch,
|
||||
gitHash,
|
||||
useMocks,
|
||||
ui: { patchDb, api, mocks, marketplace },
|
||||
ui: { api, mocks, marketplace },
|
||||
} = require('../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@Injectable({
|
||||
@@ -24,7 +24,6 @@ export class ConfigService {
|
||||
mocks = mocks
|
||||
targetArch = targetArch
|
||||
gitHash = gitHash
|
||||
patchDb = patchDb
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
|
||||
@@ -1,83 +1,22 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
} from 'rxjs'
|
||||
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
mapTo,
|
||||
startWith,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { ConfigService } from './config.service'
|
||||
import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs'
|
||||
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConnectionService {
|
||||
private readonly networkState$ = merge(
|
||||
fromEvent(window, 'online').pipe(mapTo(true)),
|
||||
fromEvent(window, 'offline').pipe(mapTo(false)),
|
||||
readonly networkConnected$ = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).pipe(
|
||||
startWith(null),
|
||||
map(() => navigator.onLine),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
|
||||
private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(
|
||||
ConnectionFailure.None,
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly patch: PatchDbService,
|
||||
) {}
|
||||
|
||||
watchFailure$() {
|
||||
return this.connectionFailure$.asObservable()
|
||||
}
|
||||
|
||||
watchDisconnected$() {
|
||||
return this.connectionFailure$.pipe(
|
||||
map(failure => failure !== ConnectionFailure.None),
|
||||
)
|
||||
}
|
||||
|
||||
start(): Observable<unknown> {
|
||||
return combineLatest([
|
||||
// 1
|
||||
this.networkState$.pipe(distinctUntilChanged()),
|
||||
// 2
|
||||
this.patch.watchPatchConnection$().pipe(distinctUntilChanged()),
|
||||
// 3
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info', 'update-progress')
|
||||
.pipe(distinctUntilChanged()),
|
||||
]).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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export enum ConnectionFailure {
|
||||
None = 'none',
|
||||
Network = 'network',
|
||||
Tor = 'tor',
|
||||
Lan = 'lan',
|
||||
readonly websocketConnected$ = new ReplaySubject<boolean>(1)
|
||||
readonly connected$ = combineLatest([
|
||||
this.networkConnected$,
|
||||
this.websocketConnected$,
|
||||
]).pipe(map(([network, websocket]) => network && websocket))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators'
|
||||
|
||||
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -16,6 +16,7 @@ export class EOSService {
|
||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||
|
||||
readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe(
|
||||
filter(Boolean),
|
||||
map(status => !!status['update-progress'] || status.updated),
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ErrorHandler, Injectable } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
|
||||
handleError (e: any): void {
|
||||
console.error(e)
|
||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
||||
|
||||
if (chunkFailedMessage.test(e.message)) {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
@@ -32,9 +33,14 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
private readonly notes = new Map<string, Record<string, string>>()
|
||||
private readonly hasPackages$ = new Subject<boolean>()
|
||||
|
||||
private readonly uiMarketplaceData$: Observable<
|
||||
UIMarketplaceData | undefined
|
||||
> = this.patch.watch$('ui', 'marketplace').pipe(shareReplay(1))
|
||||
private readonly uiMarketplaceData$: Observable<UIMarketplaceData> =
|
||||
this.patch.watch$('ui', 'marketplace').pipe(
|
||||
filter(Boolean),
|
||||
distinctUntilChanged(
|
||||
(prev, curr) => prev['selected-id'] === curr['selected-id'],
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly marketplace$ = this.uiMarketplaceData$.pipe(
|
||||
map(data => this.toMarketplace(data)),
|
||||
@@ -42,19 +48,19 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
|
||||
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
||||
.watch$('server-info')
|
||||
.pipe(take(1), shareReplay())
|
||||
.pipe(filter(Boolean), take(1), shareReplay())
|
||||
|
||||
private readonly registryData$: Observable<MarketplaceData> =
|
||||
this.uiMarketplaceData$.pipe(
|
||||
switchMap(uiMarketplaceData =>
|
||||
switchMap(data =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(
|
||||
this.getMarketplaceData(
|
||||
{ 'server-id': id },
|
||||
this.toMarketplace(uiMarketplaceData).url,
|
||||
this.toMarketplace(data).url,
|
||||
),
|
||||
).pipe(tap(({ name }) => this.updateName(uiMarketplaceData, name))),
|
||||
).pipe(tap(({ name }) => this.updateName(data, name))),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -126,7 +132,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
||||
return this.marketplace$
|
||||
}
|
||||
|
||||
getAltMarketplace(): Observable<UIMarketplaceData | undefined> {
|
||||
getAltMarketplaceData(): Observable<UIMarketplaceData> {
|
||||
return this.uiMarketplaceData$
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.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'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { AbstractMarketplaceService } from '../../../../../../marketplace/src/services/marketplace.service'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ConnectionService } from 'src/app/services/connection.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 and services
|
||||
this.checkForUpdates(ui)
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui['ack-welcome'])
|
||||
}),
|
||||
)
|
||||
: of(null),
|
||||
),
|
||||
export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$()),
|
||||
filter(obj => exists(obj) && !isEmptyObject(obj)),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to EOS and services
|
||||
this.checkForUpdates(ui)
|
||||
// show eos welcome message
|
||||
this.showEosWelcome(ui['ack-welcome'])
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patchMonitor: PatchMonitorService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly eosService: EOSService,
|
||||
private readonly config: ConfigService,
|
||||
@@ -46,6 +40,7 @@ export class PatchDataService extends Observable<DataModel | null> {
|
||||
private readonly embassyApi: ApiService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
@@ -41,8 +41,8 @@ export interface DevData {
|
||||
|
||||
export interface DevProjectData {
|
||||
name: string
|
||||
instructions?: string
|
||||
config?: string
|
||||
instructions: string
|
||||
config: string
|
||||
'basic-info'?: BasicInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
import { InjectionToken } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import {
|
||||
Bootstrapper,
|
||||
DBCache,
|
||||
MockSource,
|
||||
PollSource,
|
||||
Source,
|
||||
WebsocketSource,
|
||||
} from 'patch-db-client'
|
||||
|
||||
import { ConfigService } from '../config.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
||||
import { catchError, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { Bootstrapper, DBCache, Update } from 'patch-db-client'
|
||||
import { DataModel } from './data-model'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { EMPTY, from, interval, merge, Observable } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
|
||||
// [wsSources, pollSources]
|
||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>('')
|
||||
export const PATCH_SOURCE$ = new InjectionToken<
|
||||
BehaviorSubject<Source<DataModel>[]>
|
||||
>('')
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>>>(
|
||||
'',
|
||||
)
|
||||
export const PATCH_CACHE = new InjectionToken<DBCache<DataModel>>('', {
|
||||
factory: () => ({} as any),
|
||||
})
|
||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
||||
|
||||
export function mockSourceFactory({
|
||||
mockPatch$,
|
||||
}: MockApiService): Source<DataModel>[] {
|
||||
return Array(2).fill(
|
||||
new MockSource<DataModel>(mockPatch$.pipe(filter(exists))),
|
||||
export function sourceFactory(
|
||||
api: ApiService,
|
||||
authService: AuthService,
|
||||
connectionService: ConnectionService,
|
||||
): Observable<Update<DataModel>> {
|
||||
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||
catchError((_, watch$) => {
|
||||
connectionService.websocketConnected$.next(false)
|
||||
|
||||
return interval(4000).pipe(
|
||||
switchMap(() =>
|
||||
from(api.echo({ message: 'ping' })).pipe(catchError(() => EMPTY)),
|
||||
),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
)
|
||||
}),
|
||||
tap(() => connectionService.websocketConnected$.next(true)),
|
||||
)
|
||||
|
||||
return authService.isVerified$.pipe(
|
||||
switchMap(verified => (verified ? merge(websocket$, api.sync$) : EMPTY)),
|
||||
)
|
||||
}
|
||||
|
||||
export function realSourceFactory(
|
||||
embassyApi: ApiService,
|
||||
config: ConfigService,
|
||||
{ defaultView }: Document,
|
||||
): Source<DataModel>[] {
|
||||
const { patchDb } = config
|
||||
const host = defaultView?.location.host
|
||||
const protocol = defaultView?.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
|
||||
return [
|
||||
new WebsocketSource<DataModel>(`${protocol}://${host}/ws/db`),
|
||||
new PollSource<DataModel>({ ...patchDb.poll }, embassyApi),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
import {
|
||||
BOOTSTRAPPER,
|
||||
mockSourceFactory,
|
||||
PATCH_CACHE,
|
||||
PATCH_SOURCE,
|
||||
PATCH_SOURCE$,
|
||||
realSourceFactory,
|
||||
sourceFactory,
|
||||
} from './patch-db.factory'
|
||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { ReplaySubject } from 'rxjs'
|
||||
|
||||
const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
|
||||
// This module is purely for providers organization purposes
|
||||
@NgModule({
|
||||
@@ -27,16 +20,12 @@ const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
|
||||
},
|
||||
{
|
||||
provide: PATCH_SOURCE,
|
||||
deps: [ApiService, ConfigService, DOCUMENT],
|
||||
useFactory: useMocks ? mockSourceFactory : realSourceFactory,
|
||||
},
|
||||
{
|
||||
provide: PATCH_SOURCE$,
|
||||
useValue: new ReplaySubject(1),
|
||||
deps: [ApiService, AuthService, ConnectionService],
|
||||
useFactory: sourceFactory,
|
||||
},
|
||||
{
|
||||
provide: PatchDB,
|
||||
deps: [PATCH_SOURCE$, ApiService, PATCH_CACHE],
|
||||
deps: [PATCH_SOURCE, PATCH_CACHE],
|
||||
useClass: PatchDB,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,165 +1,49 @@
|
||||
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,
|
||||
ReplaySubject,
|
||||
Subscription,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
finalize,
|
||||
mergeMap,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { Bootstrapper, PatchDB, Store } from 'patch-db-client'
|
||||
import { Observable, of, Subscription } from 'rxjs'
|
||||
import { catchError, debounceTime, finalize, tap } from 'rxjs/operators'
|
||||
import { DataModel } from './data-model'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { BOOTSTRAPPER, PATCH_SOURCE, PATCH_SOURCE$ } from './patch-db.factory'
|
||||
|
||||
export enum PatchConnection {
|
||||
Initializing = 'initializing',
|
||||
Connected = 'connected',
|
||||
Disconnected = 'disconnected',
|
||||
}
|
||||
import { BOOTSTRAPPER } from './patch-db.factory'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PatchDbService {
|
||||
private readonly WS_SUCCESS = 'wsSuccess'
|
||||
private readonly patchConnection$ = new ReplaySubject<PatchConnection>(1)
|
||||
private readonly wsSuccess$ = new BehaviorSubject(false)
|
||||
private readonly polling$ = new BehaviorSubject(false)
|
||||
private subs: Subscription[] = []
|
||||
|
||||
readonly connected$ = this.watchPatchConnection$().pipe(
|
||||
filter(status => status === PatchConnection.Connected),
|
||||
take(1),
|
||||
shareReplay(),
|
||||
)
|
||||
private sub?: Subscription
|
||||
|
||||
constructor(
|
||||
// [wsSources, pollSources]
|
||||
@Inject(PATCH_SOURCE) private readonly sources: Source<DataModel>[],
|
||||
@Inject(BOOTSTRAPPER)
|
||||
private readonly bootstrapper: Bootstrapper<DataModel>,
|
||||
@Inject(PATCH_SOURCE$)
|
||||
private readonly sources$: BehaviorSubject<Source<DataModel>[]>,
|
||||
private readonly http: ApiService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly storage: Storage,
|
||||
private readonly patchDb: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
}
|
||||
start(): void {
|
||||
// Early return if already started
|
||||
if (this.sub) {
|
||||
return
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.init()
|
||||
|
||||
this.subs.push(
|
||||
// Connection Error
|
||||
this.patchDb.connectionError$
|
||||
.pipe(
|
||||
debounceTime(420),
|
||||
withLatestFrom(this.polling$),
|
||||
mergeMap(async ([e, polling]) => {
|
||||
if (polling) {
|
||||
console.log('patchDB: POLLING FAILED', e)
|
||||
this.patchConnection$.next(PatchConnection.Disconnected)
|
||||
await pauseFor(2000)
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
return
|
||||
}
|
||||
|
||||
console.log('patchDB: WEBSOCKET FAILED', e)
|
||||
this.polling$.next(true)
|
||||
this.sources$.next([this.sources[1], this.http])
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
console.log('patchDB: STARTING')
|
||||
this.sub = this.patchDb.cache$
|
||||
.pipe(
|
||||
debounceTime(420),
|
||||
tap(cache => {
|
||||
this.bootstrapper.update(cache)
|
||||
}),
|
||||
|
||||
// RPC ERROR
|
||||
this.patchDb.rpcError$
|
||||
.pipe(
|
||||
tap(({ error }) => {
|
||||
if (error.code === 34) {
|
||||
console.log('patchDB: Unauthorized. Logging out.')
|
||||
this.auth.setUnverified()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
}),
|
||||
|
||||
// GOOD CONNECTION
|
||||
this.patchDb.cache$
|
||||
.pipe(
|
||||
debounceTime(420),
|
||||
withLatestFrom(this.patchConnection$, this.wsSuccess$, this.polling$),
|
||||
tap(async ([cache, connection, wsSuccess, polling]) => {
|
||||
this.bootstrapper.update(cache)
|
||||
|
||||
if (connection === PatchConnection.Initializing) {
|
||||
console.log(
|
||||
polling
|
||||
? 'patchDB: POLL CONNECTED'
|
||||
: 'patchDB: WEBSOCKET CONNECTED',
|
||||
)
|
||||
this.patchConnection$.next(PatchConnection.Connected)
|
||||
if (!wsSuccess && !polling) {
|
||||
console.log('patchDB: WEBSOCKET SUCCESS')
|
||||
this.storage.set(this.WS_SUCCESS, 'true')
|
||||
this.wsSuccess$.next(true)
|
||||
}
|
||||
} else if (
|
||||
connection === PatchConnection.Disconnected &&
|
||||
wsSuccess
|
||||
) {
|
||||
console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.polling$.next(false)
|
||||
this.sources$.next([this.sources[0], this.http])
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.warn('patchDB: SYNC COMPLETE')
|
||||
},
|
||||
}),
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
console.log('patchDB: STOPPING')
|
||||
this.patchConnection$.next(PatchConnection.Initializing)
|
||||
this.patchDb.store.reset()
|
||||
this.subs.forEach(x => x.unsubscribe())
|
||||
this.subs = []
|
||||
}
|
||||
// Early return if already stopped
|
||||
if (!this.sub) {
|
||||
return
|
||||
}
|
||||
|
||||
watchPatchConnection$(): Observable<PatchConnection> {
|
||||
return this.patchConnection$.asObservable()
|
||||
console.log('patchDB: STOPPING')
|
||||
this.patchDb.store.reset()
|
||||
this.sub.unsubscribe()
|
||||
this.sub = undefined
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@@ -168,10 +52,7 @@ export class PatchDbService {
|
||||
|
||||
console.log('patchDB: WATCHING ', argsString)
|
||||
|
||||
return this.patchConnection$.pipe(
|
||||
filter(status => status === PatchConnection.Connected),
|
||||
take(1),
|
||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
||||
return this.patchDb.store.watch$(...(args as [])).pipe(
|
||||
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
|
||||
catchError(e => {
|
||||
console.error('patchDB: WATCH ERROR', e)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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 { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
|
||||
@@ -12,23 +10,19 @@ import { AuthService } from 'src/app/services/auth.service'
|
||||
})
|
||||
export class PatchMonitorService extends Observable<boolean> {
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
switchMap(verified => {
|
||||
map(verified => {
|
||||
if (verified) {
|
||||
return from(this.patch.start()).pipe(mapTo(true))
|
||||
this.patch.start()
|
||||
return true
|
||||
}
|
||||
|
||||
this.patch.stop()
|
||||
this.storage.clear()
|
||||
|
||||
return of(false)
|
||||
return false
|
||||
}),
|
||||
share(),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly storage: Storage,
|
||||
) {
|
||||
super(subscriber => this.stream$.subscribe(subscriber))
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { first } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { UIMarketplaceData } from 'src/app/services/patch-db/data-model'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { filter, firstValueFrom } from 'rxjs'
|
||||
|
||||
export function getMarketplace(
|
||||
patch: PatchDbService,
|
||||
): Promise<UIMarketplaceData> {
|
||||
return firstValueFrom(patch.watch$('ui', 'marketplace'))
|
||||
return firstValueFrom(patch.watch$('ui', 'marketplace').pipe(filter(Boolean)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { first } from 'rxjs/operators'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { filter, firstValueFrom } from 'rxjs'
|
||||
|
||||
export function getPackage(
|
||||
patch: PatchDbService,
|
||||
@@ -13,5 +12,5 @@ export function getPackage(
|
||||
export function getAllPackages(
|
||||
patch: PatchDbService,
|
||||
): Promise<Record<string, PackageDataEntry>> {
|
||||
return firstValueFrom(patch.watch$('package-data'))
|
||||
return firstValueFrom(patch.watch$('package-data').pipe(filter(Boolean)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ServerInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { filter, firstValueFrom } from 'rxjs'
|
||||
|
||||
export function getServerInfo(patch: PatchDbService): Promise<ServerInfo> {
|
||||
return firstValueFrom(patch.watch$('server-info'))
|
||||
return firstValueFrom(patch.watch$('server-info').pipe(filter(Boolean)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user