rework LAN display and service launchability

This commit is contained in:
Matt Hill
2021-03-04 23:57:02 -07:00
committed by Keagan McClelland
parent daf701a76c
commit deb0b1e561
14 changed files with 152 additions and 64 deletions

View File

@@ -37,18 +37,23 @@ export interface AppInstalledPreview extends BaseApp {
lanAddress?: string
torAddress: string
versionInstalled: string
ui: boolean
lanUi: boolean
torUi: boolean
// FE state only
hasUI: boolean
launchable: boolean
}
export interface AppInstalledFull extends AppInstalledPreview {
instructions: string | null
lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured
hasFetchedFull: boolean
startAlert?: string
uninstallAlert?: string
restoreAlert?: string
actions: Actions
// FE state only
hasFetchedFull: boolean
}
export type Actions = ServiceAction[]

View File

@@ -22,11 +22,17 @@
<ion-grid>
<ion-row>
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
<ng-container *ngIf="{ status: app.subject.status | async, ui: app.subject.ui | async, iconURL: app.subject.iconURL | async | iconParse, title: app.subject.title | async } as vars" >
<ng-container *ngIf="{
status: app.subject.status | async,
hasUI: app.subject.hasUI | async,
launchable: app.subject.launchable | async,
iconURL: app.subject.iconURL | async | iconParse,
title: app.subject.title | async
} as vars">
<ion-card class="installed-card" [class.installed-card-on]="vars.status === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<div class="launch-container" *ngIf="vars.ui && !isConsulate">
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="vars.status !== AppStatus.RUNNING">
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<div class="launch-container" *ngIf="vars.hasUI">
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
<ion-icon name="rocket-outline"></ion-icon>
</div>
</div>

View File

@@ -35,19 +35,15 @@ export class AppInstalledListPage extends Cleanup {
segmentValue: 'services' | 'embassy' = 'services'
showCertDownload : boolean
isConsulate: boolean
isTor: boolean
constructor (
private readonly serverModel: ServerModel,
private readonly appModel: AppModel,
private readonly preload: ModelPreload,
private readonly syncDaemon: SyncDaemon,
config: ConfigService,
private readonly config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateAndroid || config.isConsulateIos
this.isTor = config.isTor()
}
ngOnDestroy () {
@@ -105,12 +101,11 @@ export class AppInstalledListPage extends Cleanup {
const app = this.apps.find(app => app.id === id).subject
let uiAddress: string
if (this.isTor) {
if (this.config.isTor()) {
uiAddress = `http://${app.torAddress.getValue()}`
} else {
uiAddress = `https://${app.lanAddress.getValue()}`
}
console.log(uiAddress)
return window.open(uiAddress, '_blank')
}

View File

@@ -20,7 +20,8 @@
hasFetchedFull: app.hasFetchedFull | async,
iconURL: app.iconURL | async,
title: app.title | async,
ui: app.ui | async,
hasUI: app.hasUI | async,
launchable: app.launchable | async,
lanAddress: app.lanAddress | async
} as vars" class="ion-padding-bottom">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
@@ -77,7 +78,7 @@
</ion-label>
</ion-item>
<ion-button size="small" *ngIf="vars.status === AppStatus.RUNNING && vars.ui && !isConsulate" class="launch-button" expand="block" fill="outline" (click)="launchUiTab()">
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button>
@@ -96,7 +97,7 @@
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
</ion-button>
</ion-item>
<ion-item *ngIf="vars.lanAddress" lines="none">
<ion-item *ngIf="!hideLAN" lines="none">
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>{{ vars.lanAddress }}</p>

View File

@@ -32,8 +32,8 @@ export class AppInstalledShowPage extends Cleanup {
appId: string
AppStatus = AppStatus
showInstructions = false
isConsulate: boolean
isTor: boolean
hideLAN: boolean
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
@@ -51,11 +51,9 @@ export class AppInstalledShowPage extends Cleanup {
private readonly wizardBaker: WizardBaker,
private readonly appModel: AppModel,
private readonly popoverController: PopoverController,
config: ConfigService,
private readonly config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateIos || config.isConsulateAndroid
this.isTor = config.isTor()
}
async ngOnInit () {
@@ -64,8 +62,12 @@ export class AppInstalledShowPage extends Cleanup {
this.cleanup(
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
.pipe(
tap(app => this.app = app),
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
tap(app => {
this.app = app
const appP = peekProperties(this.app)
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
}),
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
catchError(e => of(this.setError(e))),
).subscribe(),
)
@@ -98,7 +100,7 @@ export class AppInstalledShowPage extends Cleanup {
async launchUiTab () {
let uiAddress: string
if (this.isTor) {
if (this.config.isTor()) {
uiAddress = `http://${this.app.torAddress.getValue()}`
} else {
uiAddress = `https://${this.app.lanAddress.getValue()}`

View File

@@ -130,7 +130,6 @@ export class AppMetricsPage {
toggleMask (key: string) {
this.unmasked[key] = !this.unmasked[key]
console.log(this.unmasked)
}
asIsOrder (a: any, b: any) {

View File

@@ -16,10 +16,8 @@ export class LANPage {
lanDocs = 'docs.start9labs.com/user-manual/general/secure-lan'
lanAddress: string
isTor: boolean
fullDocumentationLink: string
isConsulate: boolean
lanDisabled: LanSetupIssue = undefined
lanDisabled: LanSetupIssue
readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = {
NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`,
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
@@ -40,8 +38,6 @@ export class LANPage {
this.lanDisabled = 'NotTor'
}
this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid
if (this.config.isTor()) {
this.fullDocumentationLink = `http://${this.torDocs}`
} else {

View File

@@ -1,5 +1,5 @@
import { ConfigSpec } from 'src/app/app-config/config-types'
import { AppAvailableFull, AppInstalledFull } from 'src/app/models/app-types'
import { AppAvailableFull, AppInstalledFull, AppInstalledPreview } from 'src/app/models/app-types'
import { Rules } from '../../models/app-model'
import { SSHFingerprint, ServerStatus, ServerSpecs } from '../../models/server-model'
@@ -23,7 +23,9 @@ export interface ApiServer {
/** APPS **/
export type ApiAppAvailableFull = Omit<AppAvailableFull, 'versionViewing'>
export type ApiAppInstalledFull = Omit<AppInstalledFull, 'hasFetchedFull'>
export type ApiAppInstalledPreview = Omit<AppInstalledPreview, 'hasUI' | 'launchable'>
export type ApiAppInstalledFull = Omit<AppInstalledFull, 'hasFetchedFull' | 'hasUI' | 'launchable'>
export interface ApiAppConfig {
spec: ConfigSpec

View File

@@ -7,8 +7,8 @@ import { ConfigService } from '../config.service'
export function ApiServiceFactory (config: ConfigService, http: HttpService, appModel: AppModel, serverModel: ServerModel) {
if (config.api.useMocks) {
return new MockApiService(appModel, serverModel)
return new MockApiService(appModel, serverModel, config)
} else {
return new LiveApiService(http, appModel, serverModel)
return new LiveApiService(http, appModel, serverModel, config)
}
}

View File

@@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
import { Subject, Observable } from 'rxjs'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull } from './api-types'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types'
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
import { ConfigSpec } from 'src/app/app-config/config-types'
@@ -102,7 +102,7 @@ export module ReqRes {
export type GetAppLogsRes = string[]
export type GetServerLogsRes = string[]
export type GetAppMetricsRes = AppMetricsVersioned<number>
export type GetAppsInstalledRes = AppInstalledPreview[]
export type GetAppsInstalledRes = ApiAppInstalledPreview[]
export type PostInstallAppReq = { version: string }
export type PostInstallAppRes = ApiAppInstalledFull & { breakages: DependentBreakage[] }
export type PostUpdateAgentReq = { version: string }

View File

@@ -4,8 +4,8 @@ import { AppModel, AppStatus } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
import { ApiService, ReqRes } from './api.service'
import { ApiServer, Unit } from './api-types'
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types'
import { HttpErrorResponse } from '@angular/common/http'
import { isUnauthorized } from 'src/app/util/web.util'
import { Replace } from 'src/app/util/types.util'
import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
@@ -13,7 +13,7 @@ import { modulateTime } from 'src/app/util/misc.util'
import { Observable, of, throwError } from 'rxjs'
import { catchError, mapTo } from 'rxjs/operators'
import * as uuid from 'uuid'
import { METHODS } from 'http'
import { ConfigService } from '../config.service'
@Injectable()
export class LiveApiService extends ApiService {
@@ -22,6 +22,7 @@ export class LiveApiService extends ApiService {
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
private readonly appModel: AppModel,
private readonly serverModel: ServerModel,
private readonly config: ConfigService,
) { super() }
testConnection(url: string): Promise<true> {
@@ -116,11 +117,27 @@ export class LiveApiService extends ApiService {
async getInstalledApp(appId: string): Promise<AppInstalledFull> {
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
.then(app => ({ ...app, hasFetchedFull: true }))
.then(app => {
return {
...app,
hasFetchedFull: true,
hasUI: this.config.hasUI(app),
launchable: this.config.isLaunchable(app),
}
})
}
async getInstalledApps(): Promise<AppInstalledPreview[]> {
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
.then(apps => {
return apps.map(app => {
return {
...app,
hasUI: this.config.hasUI(app),
launchable: this.config.isLaunchable(app),
}
})
})
}
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> {
@@ -145,7 +162,14 @@ export class LiveApiService extends ApiService {
version,
}
return this.authRequest<ReqRes.PostInstallAppRes>({ method: Method.POST, url: `/apps/${appId}/install${dryRunParam(dryRun, true)}`, data })
.then(res => ({ ...res, hasFetchedFull: false }))
.then(app => {
return {
...app,
hasFetchedFull: false,
hasUI: this.config.hasUI(app),
launchable: this.config.isLaunchable(app),
}
})
}
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {

View File

@@ -4,9 +4,10 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
import { pauseFor } from '../../util/misc.util'
import { ApiService, ReqRes } from './api.service'
import { ApiServer, Unit as EmptyResponse, Unit } from './api-types'
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types'
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
import { ConfigService } from '../config.service'
//@TODO consider moving to test folders.
@Injectable()
@@ -16,6 +17,7 @@ export class MockApiService extends ApiService {
constructor (
private readonly appModel: AppModel,
private readonly serverModel: ServerModel,
private readonly config: ConfigService,
) {
super()
}
@@ -107,6 +109,14 @@ export class MockApiService extends ApiService {
async getInstalledApp (appId: string): Promise<AppInstalledFull> {
return mockGetInstalledApp(appId)
.then(app => {
return {
...app,
hasFetchedFull: false,
hasUI: this.hasUI(app),
launchable: this.isLaunchable(app),
}
})
}
async getAppMetrics (appId: string): Promise<AppMetrics> {
@@ -115,6 +125,15 @@ export class MockApiService extends ApiService {
async getInstalledApps (): Promise<AppInstalledPreview[]> {
return mockGetInstalledApps()
.then(apps => {
return apps.map(app => {
return {
...app,
hasUI: this.hasUI(app),
launchable: this.isLaunchable(app),
}
})
})
}
async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
@@ -131,6 +150,14 @@ export class MockApiService extends ApiService {
async installApp (appId: string, version: string, dryRun: boolean): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
return mockInstallApp(appId)
.then(app => {
return {
...app,
hasFetchedFull: true,
hasUI: this.hasUI(app),
launchable: this.isLaunchable(app),
}
})
}
async uninstallApp (appId: string, dryRun: boolean): Promise<{ breakages: DependentBreakage[] }> {
@@ -230,7 +257,6 @@ export class MockApiService extends ApiService {
}
async serviceAction (appId: string, action: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
console.log('service action', appId, action)
await pauseFor(1000)
return {
jsonrpc: '2.0',
@@ -243,9 +269,22 @@ export class MockApiService extends ApiService {
}
}
refreshLAN (): Promise<Unit> {
async refreshLAN (): Promise<Unit> {
return mockRefreshLAN()
}
private hasUI (app: ApiAppInstalledPreview): boolean {
return app.lanUi || app.torUi
}
private isLaunchable (app: ApiAppInstalledPreview): boolean {
return !this.config.isConsulate &&
app.status === AppStatus.RUNNING &&
(
(app.torAddress && app.torUi && this.config.isTor()) ||
(app.lanAddress && app.lanUi && !this.config.isTor())
)
}
}
async function mockGetServer (): Promise<ReqRes.GetServerRes> {
@@ -294,12 +333,12 @@ async function mockGetAvailableApps (): Promise<ReqRes.GetAppsAvailableRes> {
return Object.values(mockApiAppAvailableFull)
}
async function mockGetInstalledApp (appId: string): Promise<AppInstalledFull> {
async function mockGetInstalledApp (appId: string): Promise<ReqRes.GetAppInstalledRes> {
await pauseFor(1000)
return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true }
return { ...mockApiAppInstalledFull[appId] }
}
async function mockGetInstalledApps (): Promise<AppInstalledPreview[]> {
async function mockGetInstalledApps (): Promise<ApiAppInstalledPreview[]> {
await pauseFor(1000)
return Object.values(mockApiAppInstalledFull).map(toInstalledPreview).filter(({ versionInstalled}) => !!versionInstalled)
}
@@ -329,9 +368,9 @@ async function mockGetAppConfig (): Promise<ReqRes.GetAppConfigRes> {
return mockApiAppConfig
}
async function mockInstallApp (appId: string): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
async function mockInstallApp (appId: string): Promise<ApiAppInstalledFull & { breakages: DependentBreakage[] }> {
await pauseFor(1000)
return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true, ...mockAppDependentBreakages }
return { ...mockApiAppInstalledFull[appId], ...mockAppDependentBreakages }
}
async function mockUninstallApp (): Promise< { breakages: DependentBreakage[] } > {

View File

@@ -1,6 +1,7 @@
import { AppStatus } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppDependency, BaseApp, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types'
import { modulateTime } from 'src/app/util/misc.util'
import { ApiAppInstalledFull } from './api-types'
export function toAvailablePreview (f: AppAvailableFull): AppAvailablePreview {
return {
@@ -23,8 +24,11 @@ export function toInstalledPreview (f: AppInstalledFull): AppInstalledPreview {
title: f.title,
iconURL: f.iconURL,
torAddress: f.torAddress,
ui: f.ui,
lanAddress: f.lanAddress,
lanUi: f.lanUi,
torUi: f.torUi,
hasUI: f.hasUI,
launchable: f.launchable,
}
}
@@ -45,7 +49,7 @@ export function toServiceBreakage (f: BaseApp): DependentBreakage {
}
}
export const bitcoinI: AppInstalledFull = {
export const bitcoinI: ApiAppInstalledFull = {
id: 'bitcoind',
versionInstalled: '0.18.1',
lanAddress: 'bitcoinLan.local',
@@ -57,8 +61,8 @@ export const bitcoinI: AppInstalledFull = {
instructions: 'some instructions',
lastBackup: new Date().toISOString(),
configuredRequirements: [],
hasFetchedFull: true,
ui: false,
lanUi: false,
torUi: false,
restoreAlert: 'if you restore this app horrible things will happen to the people you love.',
actions: [
{ id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]},
@@ -66,14 +70,14 @@ export const bitcoinI: AppInstalledFull = {
],
}
export const lightningI: AppInstalledFull = {
id: 'c-lightning',
export const lightningI: ApiAppInstalledFull = {
id: 'lightning',
lanAddress: 'lightningLan.local',
status: AppStatus.RUNNING,
title: 'C Lightning',
versionInstalled: '1.0.0',
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
iconURL: 'assets/img/service-icons/bitwarden.png',
iconURL: 'assets/img/service-icons/c-lightning.png',
instructions: 'some instructions',
lastBackup: new Date().toISOString(),
configuredRequirements: [
@@ -86,12 +90,12 @@ export const lightningI: AppInstalledFull = {
violation: null,
}),
],
hasFetchedFull: true,
ui: true,
lanUi: false,
torUi: true,
actions: [],
}
export const cupsI: AppInstalledFull = {
export const cupsI: ApiAppInstalledFull = {
id: 'cups',
lanAddress: 'cupsLan.local',
versionInstalled: '2.1.0',
@@ -102,7 +106,6 @@ export const cupsI: AppInstalledFull = {
instructions: 'some instructions',
lastBackup: new Date().toISOString(),
ui: true,
uninstallAlert: 'This is A GREAT APP man, I just don\'t know',
configuredRequirements: [
toServiceRequirement(lightningI,
@@ -133,7 +136,8 @@ export const cupsI: AppInstalledFull = {
violation: { name: 'incompatible-config', ruleViolations: ['bro', 'seriously', 'fix this'] },
}),
],
hasFetchedFull: true,
lanUi: true,
torUi: true,
actions: [],
}
@@ -296,7 +300,7 @@ export const mockApiAppAvailableFull: { [appId: string]: AppAvailableFull; } = {
bitwarden: bitwardenA,
}
export const mockApiAppInstalledFull: { [appId: string]: AppInstalledFull; } = {
export const mockApiAppInstalledFull: { [appId: string]: ApiAppInstalledFull; } = {
bitcoind: bitcoinI,
cups: cupsI,
lightning: lightningI,

View File

@@ -1,4 +1,6 @@
import { Injectable } from '@angular/core'
import { AppStatus } from '../models/app-model'
import { ApiAppInstalledPreview } from './api/api-types'
const { useMocks, mockOver, skipStartupAlerts } = require('../../../use-mocks.json') as UseMocks
@@ -22,12 +24,25 @@ export class ConfigService {
}
skipStartupAlerts = skipStartupAlerts
isConsulateIos = window['platform'] === 'ios'
isConsulateAndroid = window['platform'] === 'android'
isConsulate = window['platform'] === 'ios'
isTor () : boolean {
return (this.api.useMocks && mockOver === 'tor') || this.origin.endsWith('.onion')
}
hasUI (app: ApiAppInstalledPreview): boolean {
return app.lanUi || app.torUi
}
isLaunchable (app: ApiAppInstalledPreview): boolean {
return !this.isConsulate &&
app.status === AppStatus.RUNNING &&
(
(app.torAddress && app.torUi && this.isTor()) ||
(app.lanAddress && app.lanUi && !this.isTor())
)
}
}
function removeProtocol (str: string): string {