address comments and more

This commit is contained in:
Matt Hill
2024-08-15 08:05:37 -06:00
parent a730543c76
commit 015131f198
24 changed files with 316 additions and 396 deletions

View File

@@ -33,7 +33,7 @@ export default class InitializingPage {
private readonly state = inject(StateService)
readonly progress = toSignal(
defer(() => from(this.api.initGetProgress())).pipe(
defer(() => from(this.api.initFollowProgress())).pipe(
switchMap(({ guid, progress }) =>
this.api
.openWebsocket$<T.FullProgress>(guid, {})

View File

@@ -1,4 +1,4 @@
import { CB, CT, T } from '@start9labs/start-sdk'
import { CB, CT, T, utils } from '@start9labs/start-sdk'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiConfirmData } from '@taiga-ui/kit'
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
@@ -53,75 +53,44 @@ export type AddressDetails = {
url: string
}
// @TODO Matt these types have change significantly
export function getAddresses(serviceInterface: any): {
// T.ServiceInterface): {
export function getMultihostAddresses(
serviceInterface: T.ServiceInterface,
host: T.Host,
): {
clearnet: AddressDetails[]
local: AddressDetails[]
tor: AddressDetails[]
} {
const host = serviceInterface.hostInfo
const addressInfo = serviceInterface.addressInfo
const username = addressInfo.username ? addressInfo.username + '@' : ''
const suffix = addressInfo.suffix || ''
const hostnames =
host.kind === 'multi'
? host.hostnames
: host.hostname
? [host.hostname]
: []
const hostnamesInfo = host.hostnameInfo[addressInfo.internalPort]
const clearnet: AddressDetails[] = []
const local: AddressDetails[] = []
const tor: AddressDetails[] = []
hostnames.forEach((h: any) => {
let scheme = ''
let port = ''
if (h.hostname.sslPort) {
port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}`
scheme = addressInfo.bindOptions.addSsl?.scheme
? `${addressInfo.bindOptions.addSsl.scheme}://`
: ''
}
if (h.hostname.port) {
port = h.hostname.port === 80 ? '' : `:${h.hostname.port}`
scheme = addressInfo.bindOptions.scheme
? `${addressInfo.bindOptions.scheme}://`
: ''
}
if (h.kind === 'onion') {
tor.push({
label: h.hostname.sslPort ? 'HTTPS' : 'HTTP',
url: toHref(scheme, username, h.hostname.value, port, suffix),
})
} else {
const hostnameKind = h.hostname.kind
if (hostnameKind === 'domain') {
tor.push({
url: toHref(
scheme,
username,
`${h.hostname.subdomain}.${h.hostname.domain}`,
port,
suffix,
),
})
hostnamesInfo.forEach(hostnameInfo => {
utils.addressHostToUrl(addressInfo, hostnameInfo).forEach(url => {
// Onion
if (hostnameInfo.kind === 'onion') {
tor.push({ url })
// IP
} else {
local.push({
label:
hostnameKind === 'local'
? 'Local'
: `${h.networkInterfaceId} (${hostnameKind})`,
url: toHref(scheme, username, h.hostname.value, port, suffix),
})
// Domain
if (hostnameInfo.hostname.kind === 'domain') {
clearnet.push({ url })
// Local
} else {
const hostnameKind = hostnameInfo.hostname.kind
local.push({
label:
hostnameKind === 'local'
? 'Local'
: `${hostnameInfo.networkInterfaceId} (${hostnameKind})`,
url,
})
}
}
}
})
})
return {
@@ -130,13 +99,3 @@ export function getAddresses(serviceInterface: any): {
tor,
}
}
function toHref(
scheme: string,
username: string,
hostname: string,
port: string,
suffix: string,
): string {
return `${scheme}${username}${hostname}${port}${suffix}`
}

View File

@@ -1,5 +1,5 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { convertAnsi, toLocalIsoString } from '@start9labs/shared'
import { convertAnsi, Log, toLocalIsoString } from '@start9labs/shared'
import {
bufferTime,
catchError,
@@ -44,7 +44,13 @@ export class LogsPipe implements PipeTransform {
),
defer(() => followLogs(this.options)).pipe(
tap(r => this.logs.setCursor(r.startCursor)),
switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))),
switchMap(r =>
this.api.openWebsocket$<Log>(r.guid, {
openObserver: {
next: () => this.logs.status$.next('connected'),
},
}),
),
bufferTime(1000),
filter(logs => !!logs.length),
map(convertAnsi),
@@ -67,15 +73,6 @@ export class LogsPipe implements PipeTransform {
private get options() {
return this.logs.status$.value === 'connected' ? { limit: 400 } : {}
}
private toConfig(guid: string) {
return {
url: `/rpc/${guid}`,
openObserver: {
next: () => this.logs.status$.next('connected'),
},
}
}
}
function getMessage(success: boolean): string {

View File

@@ -3,17 +3,17 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
import { combineLatest, map } from 'rxjs'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getAddresses } from '../../../components/interfaces/interface.utils'
import { getMultihostAddresses } from '../../../components/interfaces/interface.utils'
@Component({
template: `
<app-interface
*ngIf="interfaceInfo$ | async as interfaceInfo"
*ngIf="interfacesWithAddresses$ | async as serviceInterface"
[packageContext]="context"
[serviceInterface]="interfaceInfo"
[serviceInterface]="serviceInterface"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -22,23 +22,25 @@ import { getAddresses } from '../../../components/interfaces/interface.utils'
})
export class ServiceInterfaceRoute {
private readonly route = inject(ActivatedRoute)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly context = {
packageId: getPkgId(this.route),
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
}
readonly interfaceInfo$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$(
readonly interfacesWithAddresses$ = combineLatest([
this.patch.watch$(
'packageData',
this.context.packageId,
'serviceInterfaces',
this.context.interfaceId,
)
.pipe(
map(info => ({
...info,
addresses: getAddresses(info),
})),
)
),
this.patch.watch$('packageData', this.context.packageId, 'hosts'),
]).pipe(
map(([iFace, hosts]) => ({
...iFace,
addresses: getMultihostAddresses(iFace, hosts[iFace.addressInfo.hostId]),
})),
)
}

View File

@@ -23,6 +23,7 @@ import { BackupType } from '../types/backup-type'
export class BackupsStatusComponent {
private readonly exver = inject(Exver)
@Input({ required: true }) serverId!: string
@Input({ required: true }) type!: BackupType
@Input({ required: true }) target!: BackupTarget
@@ -61,8 +62,12 @@ export class BackupsStatusComponent {
}
private get hasBackup(): boolean {
return !!this.target.startOs
// @TODO Matt types changed
// && this.exver.compareExver(this.target.startOs.version, '0.3.0') !== -1
return (
this.target.startOs[this.serverId] &&
this.exver.compareOsVersion(
this.target.startOs[this.serverId].version,
'0.3.6',
) !== 'less'
)
}
}

View File

@@ -23,6 +23,9 @@ import { BackupsStatusComponent } from '../components/status.component'
import { GetDisplayInfoPipe } from '../pipes/get-display-info.pipe'
import { BackupType } from '../types/backup-type'
import { TARGETS } from './targets.component'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
template: `
@@ -40,7 +43,12 @@ import { TARGETS } from './targets.component'
<tui-icon [icon]="displayInfo.icon" />
<div>
<strong>{{ displayInfo.name }}</strong>
<backups-status [type]="context.data.type" [target]="target" />
<backups-status
[type]="context.data.type"
[target]="target"
[serverId]="serverId"
]
/>
<div [style.color]="'var(--tui-text-secondary'">
{{ displayInfo.description }}
<br />
@@ -69,6 +77,7 @@ export class BackupsTargetModal {
private readonly dialogs = inject(TuiDialogService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly patch = inject(PatchDB<DataModel>)
readonly context =
inject<TuiDialogContext<BackupTarget, { type: BackupType }>>(
@@ -81,10 +90,12 @@ export class BackupsTargetModal {
? 'Loading Backup Targets'
: 'Loading Backup Sources'
serverId = ''
targets: BackupTarget[] = []
async ngOnInit() {
try {
this.serverId = (await getServerInfo(this.patch)).id
this.targets = (await this.api.getBackupTargets({})).saved
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -1,18 +1,32 @@
import { inject, Injectable } from '@angular/core'
import { Observable, retry, shareReplay } from 'rxjs'
import { Metrics } from 'src/app/services/api/api.types'
import {
defer,
Observable,
retry,
shareReplay,
startWith,
switchMap,
} from 'rxjs'
import { ServerMetrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@Injectable({
providedIn: 'root',
})
export class MetricsService extends Observable<Metrics> {
// @TODO get real url, 'rpc/{guid}' or something like that
private readonly metrics$ = inject(ApiService)
.openMetricsWebsocket$({
url: '',
})
.pipe(retry(), shareReplay(1))
export class MetricsService extends Observable<ServerMetrics> {
private readonly api = inject(ApiService)
// @TODO Alex do we need to use defer? I am unsure when this is necessary.
private readonly metrics$ = defer(() =>
this.api.followServerMetrics({}),
).pipe(
switchMap(({ guid, metrics }) =>
this.api.openWebsocket$<ServerMetrics>(guid).pipe(startWith(metrics)),
),
// @TODO Alex how to handle failure and reconnection here? Simple retry() will not work. Seems like we need a general solution for reconnecting websockets: patchDB, logs, metrics, progress, and any future. Reconnection should depend on server state, then we need to get a new guid, then reconnect. Similar to how patchDB websocket currently behaves on disconnect/reconnect.
retry(),
shareReplay(),
)
constructor() {
super(subscriber => this.metrics$.subscribe(subscriber))

View File

@@ -7,7 +7,7 @@ import {
InterfaceComponent,
ServiceInterfaceWithAddresses,
} from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { getMultihostAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
@@ -29,7 +29,6 @@ export class StartOsUiComponent {
.watch$('serverInfo', 'ui')
.pipe(
map(hosts => {
// @TODO Matt fix types
const serviceInterface: T.ServiceInterface = {
id: 'startos-ui',
name: 'StartOS UI',
@@ -41,31 +40,26 @@ export class StartOsUiComponent {
addressInfo: {
hostId: '',
username: null,
internalPort: 80,
scheme: 'http',
sslScheme: 'https',
suffix: '',
bindOptions: {
scheme: 'http',
preferredExternalPort: 80,
addSsl: {
scheme: 'https',
preferredExternalPort: 443,
// @TODO is this alpn correct?
alpn: { specified: ['http/1.1', 'h2'] },
},
secure: {
ssl: false,
},
},
},
hostInfo: {
id: 'start-os-ui-host',
kind: 'multi',
hostnames: hosts,
}
// @TODO Aiden confirm this is correct
const host: T.Host = {
kind: 'multi',
bindings: {},
hostnameInfo: {
80: hosts,
},
} as any
addresses: [],
}
return {
...serviceInterface,
addresses: getAddresses(serviceInterface),
addresses: getMultihostAddresses(serviceInterface, host),
}
}),
)