mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors
This commit is contained in:
@@ -38,12 +38,13 @@ export class HttpService {
|
||||
|
||||
async rpcRequest<T>(
|
||||
opts: RPCOptions,
|
||||
fullUrl?: string,
|
||||
): Promise<LocalHttpResponse<RPCResponse<T>>> {
|
||||
const { method, headers, params, timeout } = opts
|
||||
|
||||
return this.httpRequest<RPCResponse<T>>({
|
||||
method: Method.POST,
|
||||
url: this.relativeUrl,
|
||||
url: fullUrl || this.relativeUrl,
|
||||
headers,
|
||||
body: { method, params },
|
||||
timeout,
|
||||
|
||||
@@ -15,7 +15,9 @@ export type WorkspaceConfig = {
|
||||
community: 'https://community-registry.start9.com/'
|
||||
}
|
||||
mocks: {
|
||||
maskAs: 'tor' | 'lan'
|
||||
maskAs: 'tor' | 'local' | 'localhost'
|
||||
// enables local development in secure mode
|
||||
maskAsHttps: boolean
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { RouterModule, Routes } from '@angular/router'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { LoginPage } from './login.page'
|
||||
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiHintModule, TuiTooltipModule } from '@taiga-ui/core'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,7 +22,9 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
RouterModule.forChild(routes),
|
||||
TuiTooltipModule,
|
||||
TuiHintModule,
|
||||
],
|
||||
declarations: [LoginPage],
|
||||
declarations: [LoginPage, CAWizardComponent],
|
||||
})
|
||||
export class LoginPageModule {}
|
||||
|
||||
@@ -1,54 +1,81 @@
|
||||
<ion-content class="content">
|
||||
<ion-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
||||
<!-- Local HTTP -->
|
||||
<ng-container *ngIf="config.isLocalHttp(); else notLanHttp">
|
||||
<ca-wizard></ca-wizard>
|
||||
</ng-container>
|
||||
|
||||
<ion-card class="card">
|
||||
<ion-card-header>
|
||||
<ion-card-title class="title">StartOS Login</ion-card-title>
|
||||
</ion-card-header>
|
||||
<!-- not Local HTTP -->
|
||||
<ng-template #notLanHttp>
|
||||
<div *ngIf="config.isTorHttp()" class="banner">
|
||||
<ion-item color="warning">
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
<p style="font-weight: 600">Tor is faster over https.</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" color="light" (click)="launchHttps()">
|
||||
Open Https
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form class="form" (submit)="submit()">
|
||||
<ion-item-group>
|
||||
<ion-item color="dark">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="key-outline"
|
||||
style="margin-right: 16px"
|
||||
></ion-icon>
|
||||
<ion-input
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
[type]="unmasked ? 'text' : 'password'"
|
||||
[(ngModel)]="password"
|
||||
(ionChange)="error = ''"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
||||
|
||||
<ion-card class="card">
|
||||
<ion-card-header>
|
||||
<ion-card-title class="title">StartOS Login</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form class="form" (submit)="submit()">
|
||||
<ion-item-group>
|
||||
<ion-item color="dark" class="login-item">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||
slot="start"
|
||||
name="key-outline"
|
||||
style="margin-right: 16px"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-button
|
||||
class="login-button"
|
||||
type="submit"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
>
|
||||
Login
|
||||
</ion-button>
|
||||
</form>
|
||||
<p class="error">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<ion-input
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
[type]="unmasked ? 'text' : 'password'"
|
||||
[(ngModel)]="password"
|
||||
(ionChange)="error = ''"
|
||||
maxlength="64"
|
||||
></ion-input>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
color="light"
|
||||
(click)="unmasked = !unmasked"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-button
|
||||
class="login-button side-button"
|
||||
type="submit"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
>
|
||||
Login
|
||||
</ion-button>
|
||||
</form>
|
||||
<p class="error">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 90%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -34,18 +34,30 @@
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
.banner {
|
||||
position: absolute;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
ion-item {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.side-button {
|
||||
--border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
.login-item {
|
||||
--border-style: solid;
|
||||
--border-color: var(--ion-color-light);
|
||||
--border-radius: 4px 0 0 4px;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
ion-button {
|
||||
.side-button {
|
||||
--border-radius: 4px;
|
||||
}
|
||||
}
|
||||
@@ -80,12 +92,12 @@ ion-card {
|
||||
|
||||
|
||||
@media (max-width: 500px) {
|
||||
ion-button {
|
||||
.side-button {
|
||||
--border-radius: 4px;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
.login-item {
|
||||
--border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { LoadingController, getPlatforms } from '@ionic/angular'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { getPlatforms, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { Router } from '@angular/router'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { pauseFor, RELATIVE_URL } from '@start9labs/shared'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
@@ -14,42 +17,71 @@ export class LoginPage {
|
||||
password = ''
|
||||
unmasked = false
|
||||
error = ''
|
||||
loader?: HTMLIonLoadingElement
|
||||
secure = this.config.isSecure()
|
||||
|
||||
downloadClicked = false
|
||||
instructionsClicked = false
|
||||
polling = false
|
||||
caTrusted = false
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly authService: AuthService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly api: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
public readonly config: ConfigService,
|
||||
@Inject(RELATIVE_URL) private readonly relativeUrl: string,
|
||||
@Inject(DOCUMENT) public readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
async ionViewDidEnter() {
|
||||
if (!this.secure) {
|
||||
async ngOnInit() {
|
||||
if (!this.config.isSecure()) {
|
||||
await this.testHttps().catch(e =>
|
||||
console.warn('Failed Https connection attempt'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
download() {
|
||||
this.downloadClicked = true
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
instructions() {
|
||||
this.windowRef.open(
|
||||
'https://docs.start9.com/getting-started/trust-ca/#trust-your-server-s-root-ca',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
this.instructionsClicked = true
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
private async startDaemon(): Promise<void> {
|
||||
this.polling = true
|
||||
while (this.polling) {
|
||||
try {
|
||||
await this.api.getPubKey()
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
await this.testHttps()
|
||||
this.polling = false
|
||||
} catch (e) {
|
||||
console.warn('Failed Https connection attempt')
|
||||
await pauseFor(2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.loader?.dismiss()
|
||||
}
|
||||
|
||||
toggleMask() {
|
||||
this.unmasked = !this.unmasked
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.error = ''
|
||||
|
||||
this.loader = await this.loadingCtrl.create({
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Logging in...',
|
||||
})
|
||||
await this.loader.present()
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
document.cookie = ''
|
||||
@@ -58,9 +90,7 @@ export class LoginPage {
|
||||
return
|
||||
}
|
||||
await this.api.login({
|
||||
password: this.secure
|
||||
? this.password
|
||||
: await this.api.encrypt(this.password),
|
||||
password: this.password,
|
||||
metadata: { platforms: getPlatforms() },
|
||||
})
|
||||
|
||||
@@ -71,7 +101,16 @@ export class LoginPage {
|
||||
// code 7 is for incorrect password
|
||||
this.error = e.code === 7 ? 'Invalid Password' : e.message
|
||||
} finally {
|
||||
this.loader.dismiss()
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async testHttps() {
|
||||
const url = `https://${this.document.location.host}${this.relativeUrl}`
|
||||
await this.api.echo({ message: 'ping' }, url).then(() => {
|
||||
this.downloadClicked = true
|
||||
this.instructionsClicked = true
|
||||
this.caTrusted = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,10 @@ import {
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-controls',
|
||||
@@ -57,7 +56,6 @@ export class MarketplaceShowControlsComponent {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly emver: Emver,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
@@ -141,30 +139,19 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
|
||||
private async dryInstall(url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking dependent services...',
|
||||
})
|
||||
await loader.present()
|
||||
const breakages = dryUpdate(
|
||||
this.pkg.manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
try {
|
||||
const breakages = await this.embassyApi.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url, loader)
|
||||
} else {
|
||||
await loader.dismiss()
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,14 +180,11 @@ export class MarketplaceShowControlsComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async install(url: string, loader?: HTMLIonLoadingElement) {
|
||||
const message = 'Beginning Install...'
|
||||
if (loader) {
|
||||
loader.message = message
|
||||
} else {
|
||||
loader = await this.loadingCtrl.create({ message })
|
||||
await loader.present()
|
||||
}
|
||||
private async install(url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Beginning Install...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
@@ -213,14 +197,10 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
|
||||
private async presentAlertBreakages(breakages: string[]): Promise<boolean> {
|
||||
let message: string =
|
||||
'As a result of this update, the following services will no longer work properly and may crash:<ul>'
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
})
|
||||
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
|
||||
message = `${message}${bullets.join('')}</ul>`
|
||||
|
||||
return new Promise(async resolve => {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="welcome-header">
|
||||
<h1>Welcome to StartOS</h1>
|
||||
</div>
|
||||
<widget-list></widget-list>
|
||||
<!-- <widget-list></widget-list> -->
|
||||
</ng-container>
|
||||
|
||||
<ng-template #list>
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map, startWith, Observable } from 'rxjs'
|
||||
import { Observable, combineLatest } from 'rxjs'
|
||||
import { filter, map, startWith } from 'rxjs/operators'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { getPackageInfo } from 'src/app/util/get-package-info'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'packageInfo',
|
||||
})
|
||||
export class PackageInfoPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
|
||||
return this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(filter(Boolean), startWith(pkg), map(getPackageInfo))
|
||||
return combineLatest([
|
||||
this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(filter(Boolean), startWith(pkg)),
|
||||
this.depErrorService.depErrors$,
|
||||
]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import { AppShowHealthChecksComponent } from './components/app-show-health-check
|
||||
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
import { InsecureWarningComponentModule } from 'src/app/common/insecure-warning/insecure-warning.module'
|
||||
import { LaunchMenuComponentModule } from '../launch-menu/launch-menu.module'
|
||||
@@ -39,8 +37,6 @@ const routes: Routes = [
|
||||
HealthColorPipe,
|
||||
ProgressDataPipe,
|
||||
ToButtonsPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
AppShowHeaderComponent,
|
||||
AppShowProgressComponent,
|
||||
AppShowStatusComponent,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ng-container *ngIf="pkg$ | async as pkg">
|
||||
<ng-container *ngIf="pkgPlus$ | async as pkgPlus">
|
||||
<!-- header -->
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
<app-show-header [pkg]="pkgPlus.pkg"></app-show-header>
|
||||
|
||||
<!-- content -->
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ion-content *ngIf="pkgPlus.pkg as pkg" class="ion-padding with-widgets">
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||
<app-show-progress
|
||||
@@ -15,41 +15,27 @@
|
||||
|
||||
<!-- Installed -->
|
||||
<ng-template #installed>
|
||||
<!-- SECURE -->
|
||||
<ng-container *ngIf="secure; else insecure">
|
||||
<ng-container *ngIf="pkg | toDependencies as dependencies">
|
||||
<ion-item-group *ngIf="pkg | toStatus as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status
|
||||
[pkg]="pkg"
|
||||
[dependencies]="dependencies"
|
||||
[status]="status"
|
||||
></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkgId]="pkgId"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="dependencies.length"
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
<ion-item-group *ngIf="pkgPlus.status as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkgId]="pkgId"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="pkgPlus.dependencies.length"
|
||||
[dependencies]="pkgPlus.dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- INSECURE -->
|
||||
<ng-template #insecure>
|
||||
<insecure-warning></insecure-warning>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { tap } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
InstalledPackageInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { map, tap } from 'rxjs/operators'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import {
|
||||
DepErrorService,
|
||||
DependencyErrorType,
|
||||
PackageDependencyErrors,
|
||||
} from 'src/app/services/dep-error.service'
|
||||
import { combineLatest } from 'rxjs'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from './modals/app-config/app-config.page'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -27,14 +51,23 @@ const STATES = [
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowPage {
|
||||
readonly secure = this.config.isSecure()
|
||||
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
tap(pkg => {
|
||||
readonly pkgPlus$ = combineLatest([
|
||||
this.patch.watch$('package-data'),
|
||||
this.depErrorService.depErrors$,
|
||||
]).pipe(
|
||||
tap(([pkgs, _]) => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) this.navCtrl.navigateRoot('/services')
|
||||
if (!pkgs[this.pkgId]) this.navCtrl.navigateRoot('/services')
|
||||
}),
|
||||
map(([pkgs, depErrors]) => {
|
||||
const pkg = pkgs[this.pkgId]
|
||||
return {
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -42,7 +75,8 @@ export class AppShowPage {
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
isInstalled({ state }: PackageDataEntry): boolean {
|
||||
@@ -60,4 +94,140 @@ export class AppShowPage {
|
||||
showProgress({ state }: PackageDataEntry): boolean {
|
||||
return STATES.includes(state)
|
||||
}
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const pkgInstalled = pkg.installed
|
||||
|
||||
if (!pkgInstalled) return []
|
||||
|
||||
const pkgManifest = pkg.manifest
|
||||
|
||||
return Object.keys(pkgInstalled['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
.map(depId =>
|
||||
this.getDepValues(pkgInstalled, pkgManifest, depId, depErrors),
|
||||
)
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyInfo {
|
||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||
pkgManifest,
|
||||
depId,
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkgManifest.dependencies[depId].version, // do we want this version range?
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText: errorText
|
||||
? `${errorText}. ${pkgManifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)),
|
||||
}
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PackageDependencyErrors,
|
||||
) {
|
||||
const depError = depErrors[pkgManifest.id][depId]
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'install', depId)
|
||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'update', depId)
|
||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
fixText = 'Auto config'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'configure', depId)
|
||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
} else if (depError.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Health check failed'
|
||||
} else if (depError.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorText,
|
||||
fixText,
|
||||
fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
private async fixDep(
|
||||
pkgManifest: Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkgManifest, id)
|
||||
case 'configure':
|
||||
return this.configureDep(pkgManifest, id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
manifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
}
|
||||
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: 'Config',
|
||||
data: {
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { DependencyInfo } from '../../app-show.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-dependencies',
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../../modals/app-config/app-config.page'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
@@ -45,8 +44,7 @@ export class AppShowStatusComponent {
|
||||
@Input()
|
||||
status!: PackageStatus
|
||||
|
||||
@Input()
|
||||
dependencies: DependencyInfo[] = []
|
||||
PR = PrimaryRendering
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
@@ -101,7 +99,7 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.some(d => !!d.errorText)) {
|
||||
if (this.status.dependency === 'warning') {
|
||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { NavigationExtras } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../modals/app-config/app-config.page'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
})
|
||||
export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] {
|
||||
if (!pkg.installed) return []
|
||||
|
||||
return Object.keys(pkg.installed['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
.map(depId => this.setDepValues(pkg, depId))
|
||||
}
|
||||
|
||||
private setDepValues(pkg: PackageDataEntry, depId: string): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () =>
|
||||
this.navCtrl.navigateForward(`/services/${depId}`)
|
||||
|
||||
const error = pkg.installed!.status['dependency-errors'][depId]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if (error.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', depId)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', depId)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', depId)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed!['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkg.manifest.dependencies[depId].version,
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg.manifest, depId)
|
||||
case 'configure':
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: `${
|
||||
pkg.installed!['dependency-info'][depId].title
|
||||
} configuration`,
|
||||
data: {
|
||||
pkgId: depId,
|
||||
dependentInfo: pkg.manifest,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
manifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
}
|
||||
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: 'Config',
|
||||
data: {
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toStatus',
|
||||
})
|
||||
export class ToStatusPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,5 @@
|
||||
</ion-item-group>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/public/eos/local.crt"
|
||||
[download]="crtName$ | async"
|
||||
></a>
|
||||
<a id="install-cert" href="/eos/local.crt" [download]="crtName$ | async"></a>
|
||||
</ion-content>
|
||||
|
||||
@@ -15,7 +15,26 @@
|
||||
|
||||
<!-- loaded -->
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<insecure-warning *ngIf="!secure"></insecure-warning>
|
||||
<ion-item *ngIf="isTorHttp" color="warning">
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
<p style="font-weight: 600">
|
||||
Tor is faster over https.
|
||||
<a
|
||||
[routerLink]="['/system', 'root-ca']"
|
||||
style="color: var(--ion-color-light)"
|
||||
>
|
||||
Download and trust your server's Root CA
|
||||
</a>
|
||||
, then switch to https.
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" color="light" (click)="launchHttps()">
|
||||
Open Https
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider>
|
||||
|
||||
@@ -44,9 +44,7 @@ export class ServerShowPage {
|
||||
readonly showUpdate$ = this.eosService.showUpdate$
|
||||
readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$
|
||||
|
||||
readonly secure = this.config.isSecure()
|
||||
readonly isTorHttp =
|
||||
this.config.isTor() && this.document.location.protocol === 'http:'
|
||||
readonly isTorHttp = this.config.isTorHttp()
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
@@ -316,6 +314,11 @@ export class ServerShowPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async launchHttps() {
|
||||
const { 'tor-address': torAddress } = await getServerInfo(this.patch)
|
||||
window.open(torAddress)
|
||||
}
|
||||
|
||||
private async setName(value: string | null): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
@@ -520,7 +523,7 @@ export class ServerShowPage {
|
||||
icon: 'key-outline',
|
||||
action: () => this.presentAlertResetPassword(),
|
||||
detail: false,
|
||||
disabled$: of(!this.secure),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Experimental Features',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -12,16 +11,12 @@ import {
|
||||
Manifest,
|
||||
StoreIdentity,
|
||||
} from '@start9labs/marketplace'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { Emver, isEmptyObject } from '@start9labs/shared'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
|
||||
interface UpdatesData {
|
||||
hosts: StoreIdentity[]
|
||||
@@ -46,11 +41,10 @@ export class UpdatesPage {
|
||||
constructor(
|
||||
@Inject(AbstractMarketplaceService)
|
||||
readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
|
||||
viewInMarketplace(event: Event, url: string, id: string) {
|
||||
@@ -74,56 +68,41 @@ export class UpdatesPage {
|
||||
delete this.marketplaceService.updateErrors[id]
|
||||
this.marketplaceService.updateQueue[id] = true
|
||||
|
||||
if (await hasCurrentDeps(this.patch, local.manifest.id)) {
|
||||
this.dryUpdate(manifest, url)
|
||||
if (hasCurrentDeps(local)) {
|
||||
this.dryInstall(manifest, url)
|
||||
} else {
|
||||
this.update(id, version, url)
|
||||
this.install(id, version, url)
|
||||
}
|
||||
}
|
||||
|
||||
private async dryUpdate(manifest: Manifest, url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking dependent services...',
|
||||
})
|
||||
await loader.present()
|
||||
private async dryInstall(manifest: Manifest, url: string) {
|
||||
const { id, version, title } = manifest
|
||||
|
||||
const { id, version } = manifest
|
||||
const breakages = dryUpdate(
|
||||
manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
try {
|
||||
const breakages = await this.api.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
await loader.dismiss()
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.update(id, version, url)
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(title, breakages)
|
||||
if (proceed) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(
|
||||
manifest.title,
|
||||
breakages,
|
||||
)
|
||||
if (proceed) {
|
||||
this.update(id, version, url)
|
||||
} else {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
} catch (e: any) {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
this.marketplaceService.updateErrors[id] = e.message
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(
|
||||
title: string,
|
||||
breakages: Breakages,
|
||||
breakages: string[],
|
||||
): Promise<boolean> {
|
||||
let message: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:<ul>`
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
const bullets = breakages.map(depTitle => {
|
||||
return `<li><b>${depTitle}</b></li>`
|
||||
})
|
||||
message = `${message}${bullets.join('')}</ul>`
|
||||
|
||||
@@ -154,7 +133,7 @@ export class UpdatesPage {
|
||||
})
|
||||
}
|
||||
|
||||
private async update(id: string, version: string, url: string) {
|
||||
private async install(id: string, version: string, url: string) {
|
||||
try {
|
||||
await this.marketplaceService.installPackage(id, version, url)
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { map } from 'rxjs/operators'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getPackageInfo } from 'src/app/util/get-package-info'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
@@ -21,11 +24,13 @@ export class HealthComponent {
|
||||
'Transitioning',
|
||||
] as const
|
||||
|
||||
readonly data$ = inject(PatchDB)
|
||||
readonly data$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(data => {
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(getPackageInfo)
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(
|
||||
pkg => getPackageInfo(pkg, {}), // @TODO hack because not currently using widget
|
||||
)
|
||||
const result = this.labels.reduce<Record<string, number>>(
|
||||
(acc, label) => ({
|
||||
...acc,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<ion-grid class="grid-wiz">
|
||||
<img width="60px" height="60px" src="/assets/img/icon_transparent.png" />
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-icon name="lock-closed-outline" class="wiz-icon"></ion-icon>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<h2><b>Trust your Root Certificate Authority (CA)</b></h2>
|
||||
<p>
|
||||
Download and trust your server's Root CA to establish secure, encrypted
|
||||
(
|
||||
<b>HTTPS</b>
|
||||
) connections with your server
|
||||
</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card">
|
||||
<ion-row class="ion-justify-content-between">
|
||||
<b class="wiz-step">1</b>
|
||||
<tui-tooltip
|
||||
content="Your server uses its Root CA to generate SSL/TLS certificates for itself and its installed services. These certificates are used to encrypt network traffic with your client devices."
|
||||
direction="right"
|
||||
></tui-tooltip>
|
||||
</ion-row>
|
||||
<div class="ion-text-center">
|
||||
<h2>Download Root CA</h2>
|
||||
<p>Download your server's Root CA</p>
|
||||
</div>
|
||||
<ion-button class="wiz-card-button" shape="round" (click)="download()">
|
||||
<ion-icon slot="start" name="download-outline"></ion-icon>
|
||||
Download
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card" [class.disabled]="!downloadClicked">
|
||||
<ion-row class="ion-justify-content-between">
|
||||
<b class="wiz-step">2</b>
|
||||
<tui-tooltip
|
||||
content="By trusting your server's Root CA, your device can verify the authenticity of its encrypted communications with your server and installed services. You will need to trust the Root CA on every device used to connect to your server."
|
||||
direction="right"
|
||||
></tui-tooltip>
|
||||
</ion-row>
|
||||
<div class="ion-text-center">
|
||||
<h2>Trust Root CA</h2>
|
||||
<p>Follow instructions for your OS</p>
|
||||
</div>
|
||||
<ion-button
|
||||
class="wiz-card-button"
|
||||
shape="round"
|
||||
(click)="instructions()"
|
||||
[disabled]="!downloadClicked"
|
||||
>
|
||||
View Docs
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
<ion-col sizeXs="12" sizeLg="4">
|
||||
<div class="wiz-card" [class.disabled]="!polling && !caTrusted">
|
||||
<b class="wiz-step">3</b>
|
||||
<div class="ion-text-center">
|
||||
<h2>Go To Login</h2>
|
||||
<p *ngIf="instructionsClicked; else space" class="inline-center">
|
||||
<ion-spinner
|
||||
class="wiz-spinner"
|
||||
*ngIf="!caTrusted; else trusted"
|
||||
></ion-spinner>
|
||||
<ng-template #trusted>
|
||||
<ion-icon name="ribbon-outline" color="success"></ion-icon>
|
||||
</ng-template>
|
||||
{{ caTrusted ? 'Root CA trusted!' : 'Waiting for trust...' }}
|
||||
</p>
|
||||
<ng-template #space>
|
||||
<!-- to keep alignment -->
|
||||
<p><br /></p>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ion-button
|
||||
class="wiz-card-button"
|
||||
shape="round"
|
||||
(click)="launchHttps()"
|
||||
[disabled]="!caTrusted"
|
||||
>
|
||||
Open
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<ion-button fill="clear" (click)="launchHttps()" [disabled]="caTrusted">
|
||||
Skip
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/public/eos/local.crt"
|
||||
[download]="document.location.hostname"
|
||||
></a>
|
||||
@@ -0,0 +1,44 @@
|
||||
.grid-wiz {
|
||||
--ion-grid-padding: 36px;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.wiz-icon {
|
||||
font-size: 84px;
|
||||
}
|
||||
|
||||
.wiz-card {
|
||||
background: #414141;
|
||||
margin: 24px;
|
||||
padding: 16px;
|
||||
height: 280px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
|
||||
& h2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.wiz-card-button {
|
||||
justify-self: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.wiz-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
filter: saturate(0.2) contrast(0.5)
|
||||
}
|
||||
|
||||
.wiz-step {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.inline-center {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { pauseFor, RELATIVE_URL } from '@start9labs/shared'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
@Component({
|
||||
selector: 'ca-wizard',
|
||||
templateUrl: './ca-wizard.component.html',
|
||||
styleUrls: ['./ca-wizard.component.scss'],
|
||||
})
|
||||
export class CAWizardComponent {
|
||||
downloadClicked = false
|
||||
instructionsClicked = false
|
||||
polling = false
|
||||
caTrusted = false
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
public readonly config: ConfigService,
|
||||
@Inject(RELATIVE_URL) private readonly relativeUrl: string,
|
||||
@Inject(DOCUMENT) public readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.config.isSecure()) {
|
||||
await this.testHttps().catch(e =>
|
||||
console.warn('Failed Https connection attempt'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
download() {
|
||||
this.downloadClicked = true
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
instructions() {
|
||||
this.windowRef.open(
|
||||
'https://docs.start9.com/getting-started/trust-ca/#trust-your-root-ca',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
this.instructionsClicked = true
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
private async startDaemon(): Promise<void> {
|
||||
this.polling = true
|
||||
while (this.polling) {
|
||||
try {
|
||||
await this.testHttps()
|
||||
this.polling = false
|
||||
} catch (e) {
|
||||
console.warn('Failed Https connection attempt')
|
||||
await pauseFor(2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
private async testHttps() {
|
||||
const url = `https://${this.document.location.host}${this.relativeUrl}`
|
||||
await this.api.echo({ message: 'ping' }, url).then(() => {
|
||||
this.downloadClicked = true
|
||||
this.instructionsClicked = true
|
||||
this.caTrusted = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -845,7 +845,7 @@ export module Mock {
|
||||
integer: false,
|
||||
}),
|
||||
}),
|
||||
displayAs: 'I\'m {{last-name}}, {{first-name}} {{last-name}}',
|
||||
displayAs: "I'm {{last-name}}, {{first-name}} {{last-name}}",
|
||||
uniqueBy: 'last-name',
|
||||
},
|
||||
),
|
||||
@@ -1258,7 +1258,7 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
'address-info': {
|
||||
rpc: {
|
||||
@@ -1282,6 +1282,7 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
'current-dependencies': {},
|
||||
'current-dependents': {},
|
||||
'dependency-info': {},
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
@@ -1334,7 +1335,7 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
'address-info': {
|
||||
rpc: {
|
||||
@@ -1347,6 +1348,7 @@ export module Mock {
|
||||
ui: false,
|
||||
},
|
||||
},
|
||||
'current-dependents': {},
|
||||
'current-dependencies': {
|
||||
bitcoind: {
|
||||
'health-checks': [],
|
||||
@@ -1377,11 +1379,8 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'btc-rpc-proxy': {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
error: 'This is a config unsatisfied error',
|
||||
},
|
||||
'dependency-config-errors': {
|
||||
'btc-rpc-proxy': 'Username not found',
|
||||
},
|
||||
},
|
||||
'address-info': {
|
||||
@@ -1414,6 +1413,7 @@ export module Mock {
|
||||
'health-checks': [],
|
||||
},
|
||||
},
|
||||
'current-dependents': {},
|
||||
'dependency-info': {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
|
||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyError,
|
||||
HealthCheckResult,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
@@ -21,7 +21,7 @@ export module RR {
|
||||
// auth
|
||||
|
||||
export type LoginReq = {
|
||||
password: Encrypted | string
|
||||
password: string
|
||||
metadata: SessionMetadata
|
||||
} // auth.login - unauthed
|
||||
export type loginRes = null
|
||||
@@ -252,9 +252,6 @@ export module RR {
|
||||
} // package.install
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry
|
||||
export type DryUpdatePackageRes = Breakages
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: InputSpec; config: object }
|
||||
|
||||
@@ -552,3 +549,49 @@ export type Encrypted = {
|
||||
}
|
||||
|
||||
export type CloudProvider = 'dropbox' | 'google-drive'
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { RR, Encrypted, BackupTargetType, Metrics } from './api.types'
|
||||
import { RR, BackupTargetType, Metrics } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, SetupStatus } from '@start9labs/shared'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import type { JWK } from 'node-jose'
|
||||
|
||||
export abstract class ApiService {
|
||||
protected readonly jose = import('node-jose')
|
||||
|
||||
readonly patchStream$ = new BehaviorSubject<Update<DataModel>[]>([])
|
||||
pubkey?: JWK.Key
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||
const { pubkey } = this
|
||||
|
||||
if (!pubkey) throw new Error('No pubkey found!')
|
||||
|
||||
const encrypted = await this.jose.then(jose =>
|
||||
jose.JWE.createEncrypt(pubkey).update(toEncrypt).final(),
|
||||
)
|
||||
|
||||
return { encrypted }
|
||||
}
|
||||
|
||||
// http
|
||||
|
||||
@@ -43,8 +27,6 @@ export abstract class ApiService {
|
||||
|
||||
// auth
|
||||
|
||||
abstract getPubKey(): Promise<void>
|
||||
|
||||
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
|
||||
|
||||
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>
|
||||
@@ -59,7 +41,7 @@ export abstract class ApiService {
|
||||
|
||||
// server
|
||||
|
||||
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
|
||||
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||
|
||||
@@ -250,10 +232,6 @@ export abstract class ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes>
|
||||
|
||||
abstract dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes>
|
||||
|
||||
abstract getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes>
|
||||
|
||||
@@ -77,18 +77,6 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// auth
|
||||
|
||||
/**
|
||||
* We want to update the pubkey, which means that we will call in clearnet the
|
||||
* getPubKey, and all the information is never in the clear, and only public
|
||||
* information is sent across the network.
|
||||
*/
|
||||
async getPubKey() {
|
||||
this.pubkey = await this.rpcRequest({
|
||||
method: 'auth.get-pubkey',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
return this.rpcRequest({ method: 'auth.login', params }, false)
|
||||
}
|
||||
@@ -113,8 +101,8 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, false)
|
||||
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, false, urlOverride)
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
@@ -427,12 +415,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
@@ -521,6 +503,7 @@ export class LiveApiService extends ApiService {
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
addHeader = true,
|
||||
urlOverride?: string,
|
||||
): Promise<T> {
|
||||
if (addHeader) {
|
||||
options.headers = {
|
||||
@@ -529,7 +512,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
const res = await this.http.rpcRequest<T>(options)
|
||||
const res = await this.http.rpcRequest<T>(options, urlOverride)
|
||||
const encodedUpdates = res.headers.get('x-patch-updates')
|
||||
const encodedError = res.headers.get('x-patch-error')
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyErrorType,
|
||||
InstallProgress,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
@@ -115,24 +114,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// auth
|
||||
|
||||
async getPubKey() {
|
||||
await pauseFor(1000)
|
||||
|
||||
// randomly generated
|
||||
// const keystore = jose.JWK.createKeyStore()
|
||||
// this.pubkey = await keystore.generate('EC', 'P-256')
|
||||
|
||||
// generated from backend
|
||||
this.pubkey = await this.jose.then(jose =>
|
||||
jose.JWK.asKey({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4',
|
||||
y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
@@ -167,7 +148,13 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
async echo(params: RR.EchoReq, url?: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
console.warn(num)
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
}
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
}
|
||||
@@ -731,22 +718,6 @@ export class MockApiService extends ApiService {
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
lnd: {
|
||||
dependency: 'bitcoind',
|
||||
error: {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: '>0.23.0',
|
||||
received: params.version,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
|
||||
@@ -21,6 +21,10 @@ export class ConfigService {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
hostname = this.document.location.hostname
|
||||
// includes port
|
||||
host = this.document.location.host
|
||||
// includes ":" (e.g. "http:")
|
||||
protocol = this.document.location.protocol
|
||||
version = require('../../../../../package.json').version as string
|
||||
useMocks = useMocks
|
||||
mocks = mocks
|
||||
@@ -34,22 +38,41 @@ export class ConfigService {
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
|
||||
isTor(): boolean {
|
||||
return (
|
||||
this.hostname.endsWith('.onion') || (useMocks && mocks.maskAs === 'tor')
|
||||
)
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
}
|
||||
|
||||
isLocal(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'local'
|
||||
: this.hostname.endsWith('.local')
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost'
|
||||
}
|
||||
|
||||
isLan(): boolean {
|
||||
return (
|
||||
this.hostname === 'localhost' ||
|
||||
this.hostname.endsWith('.local') ||
|
||||
(useMocks && mocks.maskAs === 'lan')
|
||||
)
|
||||
// @TODO will not work once clearnet arrives
|
||||
return !this.isTor()
|
||||
}
|
||||
|
||||
isTorHttp(): boolean {
|
||||
return this.isTor() && !this.isHttps()
|
||||
}
|
||||
|
||||
isLocalHttp(): boolean {
|
||||
return this.isLocal() && !this.isHttps()
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
|
||||
private isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
}
|
||||
|
||||
export function hasUi(
|
||||
|
||||
211
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
211
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { map, shareReplay } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
HealthResult,
|
||||
InstalledPackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from './patch-db/data-model'
|
||||
|
||||
export type PackageDependencyErrors = Record<string, DependencyErrors>
|
||||
export type DependencyErrors = Record<string, DependencyError | null>
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DepErrorService {
|
||||
readonly depErrors$ = this.patch.watch$('package-data').pipe(
|
||||
map(pkgs =>
|
||||
Object.keys(pkgs)
|
||||
.map(id => ({
|
||||
id,
|
||||
depth: dependencyDepth(pkgs, id),
|
||||
}))
|
||||
.sort((a, b) => (b.depth > a.depth ? -1 : 1))
|
||||
.reduce(
|
||||
(errors, { id }): PackageDependencyErrors => ({
|
||||
...errors,
|
||||
[id]: this.getDepErrors(pkgs, id, errors),
|
||||
}),
|
||||
{} as PackageDependencyErrors,
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
private getDepErrors(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgId: string,
|
||||
outerErrors: PackageDependencyErrors,
|
||||
): DependencyErrors {
|
||||
const pkgInstalled = pkgs[pkgId].installed
|
||||
|
||||
if (!pkgInstalled) return {}
|
||||
|
||||
return currentDeps(pkgs, pkgId).reduce(
|
||||
(innerErrors, depId): DependencyErrors => ({
|
||||
...innerErrors,
|
||||
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
|
||||
}),
|
||||
{} as DependencyErrors,
|
||||
)
|
||||
}
|
||||
|
||||
private getDepError(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgInstalled: InstalledPackageDataEntry,
|
||||
depId: string,
|
||||
outerErrors: PackageDependencyErrors,
|
||||
): DependencyError | null {
|
||||
const depInstalled = pkgs[depId]?.installed
|
||||
|
||||
// not installed
|
||||
if (!depInstalled) {
|
||||
return {
|
||||
type: DependencyErrorType.NotInstalled,
|
||||
}
|
||||
}
|
||||
|
||||
const pkgManifest = pkgInstalled.manifest
|
||||
const depManifest = depInstalled.manifest
|
||||
|
||||
// incorrect version
|
||||
if (
|
||||
!this.emver.satisfies(
|
||||
depManifest.version,
|
||||
pkgManifest.dependencies[depId].version,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: pkgManifest.dependencies[depId].version,
|
||||
received: depManifest.version,
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
if (
|
||||
Object.values(pkgInstalled.status['dependency-config-errors']).some(
|
||||
err => !!err,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
}
|
||||
}
|
||||
|
||||
const depStatus = depInstalled.status.main.status
|
||||
|
||||
// not running
|
||||
if (
|
||||
depStatus !== PackageMainStatus.Running &&
|
||||
depStatus !== PackageMainStatus.Starting &&
|
||||
!(
|
||||
depStatus === PackageMainStatus.BackingUp &&
|
||||
depInstalled.status.main.started
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.NotRunning,
|
||||
}
|
||||
}
|
||||
|
||||
// health check failure
|
||||
if (depStatus === PackageMainStatus.Running) {
|
||||
for (let id of pkgInstalled['current-dependencies'][depId][
|
||||
'health-checks'
|
||||
]) {
|
||||
if (
|
||||
depInstalled.status.main.health[id].result !== HealthResult.Success
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.HealthChecksFailed,
|
||||
check: depInstalled.status.main.health[id],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transitive
|
||||
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
|
||||
Object.values(outerErrors[transitiveId]).some(err => !!err),
|
||||
)
|
||||
|
||||
if (transitiveError) {
|
||||
return {
|
||||
type: DependencyErrorType.Transitive,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
|
||||
return Object.keys(
|
||||
pkgs[id]?.installed?.['current-dependencies'] || {},
|
||||
).filter(depId => depId !== id)
|
||||
}
|
||||
|
||||
function dependencyDepth(
|
||||
pkgs: DataModel['package-data'],
|
||||
id: string,
|
||||
depth = 0,
|
||||
): number {
|
||||
return currentDeps(pkgs, id).reduce(
|
||||
(prev, depId) => dependencyDepth(pkgs, depId, prev + 1),
|
||||
depth,
|
||||
)
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'notInstalled',
|
||||
NotRunning = 'notRunning',
|
||||
IncorrectVersion = 'incorrectVersion',
|
||||
ConfigUnsatisfied = 'configUnsatisfied',
|
||||
HealthChecksFailed = 'healthChecksFailed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
@@ -158,6 +158,7 @@ export interface InstalledPackageInfo {
|
||||
'last-backup': string | null
|
||||
'installed-at': string
|
||||
'current-dependencies': Record<string, CurrentDependencyInfo>
|
||||
'current-dependents': Record<string, CurrentDependencyInfo>
|
||||
'dependency-info': Record<string, { title: string; icon: Url }>
|
||||
'address-info': Record<string, AddressInfo>
|
||||
'marketplace-url': string | null
|
||||
@@ -188,7 +189,7 @@ export interface Action {
|
||||
export interface Status {
|
||||
configured: boolean
|
||||
main: MainStatus
|
||||
'dependency-errors': { [id: string]: DependencyError | null }
|
||||
'dependency-config-errors': { [id: string]: string | null }
|
||||
}
|
||||
|
||||
export type MainStatus =
|
||||
@@ -280,51 +281,6 @@ export interface HealthCheckResultFailure {
|
||||
error: string
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
export interface InstallProgress {
|
||||
readonly size: number | null
|
||||
readonly downloaded: number
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
InstalledPackageInfo,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
Status,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PackageDependencyErrors } from './dep-error.service'
|
||||
import { Manifest } from '../../../../marketplace/src/types'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
@@ -13,14 +13,17 @@ export interface PackageStatus {
|
||||
health: HealthStatus | null
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
export function renderPkgStatus(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): PackageStatus {
|
||||
let primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
|
||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg.installed)
|
||||
dependency = getDependencyStatus(pkg.manifest, depErrors)
|
||||
health = getHealthStatus(pkg.installed.status)
|
||||
} else {
|
||||
primary = pkg.state
|
||||
@@ -38,14 +41,12 @@ function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
|
||||
}
|
||||
|
||||
function getDependencyStatus(
|
||||
installed: InstalledPackageInfo,
|
||||
): DependencyStatus | null {
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
manifest: Manifest,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyStatus {
|
||||
return Object.values(depErrors[manifest.id]).some(err => !!err)
|
||||
? DependencyStatus.Warning
|
||||
: DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus | null {
|
||||
|
||||
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { DataModel } from '../services/patch-db/data-model'
|
||||
|
||||
export function dryUpdate(
|
||||
{ id, version }: { id: string; version: string },
|
||||
pkgs: DataModel['package-data'],
|
||||
emver: Emver,
|
||||
): string[] {
|
||||
return Object.values(pkgs)
|
||||
.filter(
|
||||
pkg =>
|
||||
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
|
||||
pkgId => pkgId === id,
|
||||
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version),
|
||||
)
|
||||
.map(pkg => pkg.manifest.title)
|
||||
}
|
||||
@@ -14,6 +14,6 @@ export async function getPackage(
|
||||
|
||||
export async function getAllPackages(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<Record<string, PackageDataEntry>> {
|
||||
): Promise<DataModel['package-data']> {
|
||||
return firstValueFrom(patch.watch$('package-data'))
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
} from '../services/pkg-status-rendering.service'
|
||||
import { PkgInfo } from '../types/pkg-info'
|
||||
import { packageLoadingProgress } from './package-loading-progress'
|
||||
import { PackageDependencyErrors } from '../services/dep-error.service'
|
||||
|
||||
export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry)
|
||||
export function getPackageInfo(
|
||||
entry: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry, depErrors)
|
||||
const primaryRendering = PrimaryRendering[statuses.primary]
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from '../services/patch-db/data-model'
|
||||
import { getAllPackages } from './get-package-data'
|
||||
import { PackageDataEntry } from '../services/patch-db/data-model'
|
||||
|
||||
export async function hasCurrentDeps(
|
||||
patch: PatchDB<DataModel>,
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
const pkgs = await getAllPackages(patch)
|
||||
return !!Object.keys(pkgs)
|
||||
.filter(pkgId => pkgId !== id)
|
||||
.find(pkgId => pkgs[pkgId].installed?.['current-dependencies'][pkgId])
|
||||
export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
|
||||
return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter(
|
||||
depId => depId !== pkg.manifest.id,
|
||||
).length
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
*/
|
||||
|
||||
(window as any).global = window
|
||||
global.Buffer = global.Buffer || require('buffer').Buffer;
|
||||
(window as any).process = { env: { DEBUG: undefined }, browser: true }
|
||||
; (window as any).process = { env: { DEBUG: undefined }, browser: true }
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
@@ -62,8 +61,7 @@ import './zone-flags'
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
|
||||
@@ -395,3 +395,11 @@ tui-hint[data-appearance='onDark'] {
|
||||
cursor: pointer;
|
||||
margin: 0 12px 6px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user