refactor downstream for 036 changes (#2577)

refactor codebase for 036 changes
This commit is contained in:
Matt Hill
2024-03-24 12:12:55 -06:00
committed by GitHub
parent 22cd2e3337
commit 3b669193f6
152 changed files with 1360 additions and 1352 deletions

View File

@@ -5,7 +5,6 @@ import {
DriveComponent,
LoadingModule,
RELATIVE_URL,
UnitConversionPipesModule,
WorkspaceConfig,
} from '@start9labs/shared'
import { TuiDialogModule, TuiRootModule } from '@taiga-ui/core'
@@ -42,7 +41,6 @@ const {
TuiIconModule,
TuiSurfaceModule,
TuiTitleModule,
UnitConversionPipesModule,
],
providers: [
{

View File

@@ -2,7 +2,7 @@
<div class="box-container">
<div class="box-container-title">
<h3 class="small-caps">What's new</h3>
<p *ngIf="pkg['published-at'] as published">
<p *ngIf="pkg.publishedAt as published">
<span class="small-caps">Latest Release</span>
&nbsp;-&nbsp;
<span class="box-container-title-date">
@@ -16,7 +16,7 @@
<p
safeLinks
class="box-container-details-notes"
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
[innerHTML]="pkg.manifest.releaseNotes | markdown | dompurify"
></p>
</div>
<button

View File

@@ -16,7 +16,6 @@ import {
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import {
CopyService,
copyToClipboard,
displayEmver,
Emver,
MarkdownComponent,

View File

@@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@Component({
selector: 'marketplace-package-hero',
@@ -11,14 +10,14 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
<div class="inner-container box-shadow-lg">
<!-- icon -->
<img
[src]="pkg | mimeType | trustUrl"
[src]="pkg.icon | trustUrl"
class="box-shadow-lg"
alt="{{ pkg.manifest.title }} Icon"
/>
<!-- color background -->
<div class="color-background">
<img
[src]="pkg | mimeType | trustUrl"
[src]="pkg.icon | trustUrl"
alt="{{ pkg.manifest.title }} background image"
/>
</div>
@@ -144,7 +143,7 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, SharedPipesModule, MimeTypePipeModule],
imports: [CommonModule, SharedPipesModule],
})
export class MarketplacePackageHeroComponent {
@Input({ required: true })

View File

@@ -164,7 +164,7 @@ export class CifsComponent {
const target: CifsBackupTarget = {
...this.form.getRawValue(),
mountable: true,
'embassy-os': diskInfo,
startOs: diskInfo,
}
this.dialogs

View File

@@ -111,7 +111,7 @@ export class PasswordComponent {
}
try {
const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
const passwordHash = this.target!.startOs?.passwordHash || ''
argon2.verify(passwordHash, this.password.value)
this.context.completeWith(this.password.value)

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { RouterLink } from '@angular/router'
import { RouterModule } from '@angular/router'
import {
TuiCellModule,
TuiIconModule,
@@ -37,7 +37,7 @@ import {
</span>
</a>
`,
imports: [RouterLink, TuiIconModule, TuiCellModule, TuiTitleModule],
imports: [RouterModule, TuiIconModule, TuiCellModule, TuiTitleModule],
})
export class RecoverComponent {
@Input() disabled = false

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'
import { Component, inject, OnInit } from '@angular/core'
import { RouterLink } from '@angular/router'
import { RouterModule } from '@angular/router'
import { ErrorService } from '@start9labs/shared'
import {
TuiButtonModule,
@@ -110,7 +110,7 @@ import { StateService } from 'src/app/services/state.service'
`,
imports: [
CommonModule,
RouterLink,
RouterModule,
TuiCardModule,
TuiButtonModule,
TuiIconsModule,

View File

@@ -98,7 +98,7 @@ export default class RecoverPage {
}
empty(drive: DiskBackupTarget) {
return !drive['embassy-os']?.full
return !drive.startOs?.full
}
async getDrives() {

View File

@@ -162,9 +162,9 @@ export default class SuccessPage implements AfterViewInit {
try {
const ret = await this.api.complete()
if (!this.isKiosk) {
this.torAddress = ret['tor-address'].replace(/^https:/, 'http:')
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
this.cert = ret['root-ca']
this.torAddress = ret.torAddress.replace(/^https:/, 'http:')
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
this.cert = ret.rootCa
await this.api.exit()
}

View File

@@ -139,7 +139,7 @@ export class MockApiService extends ApiService {
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
await pauseFor(1000)
return {
'start-cursor': 'fakestartcursor',
startCursor: 'fakestartcursor',
guid: 'fake-guid',
}
}

View File

@@ -5,7 +5,6 @@ import {
inject,
NgZone,
} from '@angular/core'
import { DOCUMENT } from '@angular/common'
import { ANIMATION_FRAME } from '@ng-web-apis/common'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { tuiZonefree } from '@taiga-ui/cdk'

View File

@@ -18,8 +18,8 @@ export async function getSetupStatusMock(): Promise<SetupStatus | null> {
const progress = tries - 1
return {
'bytes-transferred': restoreOrMigrate ? progress : 0,
'total-bytes': restoreOrMigrate ? total : null,
bytesTransferred: restoreOrMigrate ? progress : 0,
totalBytes: restoreOrMigrate ? total : null,
complete: progress === total,
}
}

View File

@@ -43,8 +43,8 @@ export class SetupService extends Observable<number> {
return 1
}
return progress['total-bytes']
? progress['bytes-transferred'] / progress['total-bytes']
return progress.totalBytes
? progress.bytesTransferred / progress.totalBytes
: 0
}),
takeWhile(value => value !== 1, true),

View File

@@ -67,7 +67,7 @@ export class LogsPage implements OnInit {
if (!response.entries.length) return
this.startCursor = response['start-cursor']
this.startCursor = response.startCursor
this.logs = [convertAnsi(response.entries), ...this.logs]
this.scrollTop = this.scrollbar?.nativeElement.scrollTop || 0
} catch (e: any) {

View File

@@ -1,145 +1,145 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
HostListener,
Input,
} from '@angular/core'
import {
TuiBadgedContentModule,
TuiBadgeNotificationModule,
TuiButtonModule,
TuiIconModule,
} from '@taiga-ui/experimental'
import { RouterLink } from '@angular/router'
import { TickerModule } from '@start9labs/shared'
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
import { Action, ActionsComponent } from './actions.component'
// import { CommonModule } from '@angular/common'
// import {
// ChangeDetectionStrategy,
// Component,
// HostListener,
// Input,
// } from '@angular/core'
// import {
// TuiBadgedContentModule,
// TuiBadgeNotificationModule,
// TuiButtonModule,
// TuiIconModule,
// } from '@taiga-ui/experimental'
// import { RouterLink } from '@angular/router'
// import { TickerModule } from '@start9labs/shared'
// import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
// import { Action, ActionsComponent } from './actions.component'
@Component({
selector: '[appCard]',
template: `
<span class="link">
<tui-badged-content [style.--tui-radius.rem]="1.5">
@if (badge) {
<tui-badge-notification size="m" tuiSlot="top">
{{ badge }}
</tui-badge-notification>
}
@if (icon?.startsWith('tuiIcon')) {
<tui-icon class="icon" [icon]="icon" />
} @else {
<img alt="" class="icon" [src]="icon" />
}
</tui-badged-content>
<label ticker class="title">{{ title }}</label>
</span>
@if (isService) {
<span class="side">
<tui-hosted-dropdown
[content]="content"
(click.stop.prevent)="(0)"
(pointerdown.stop)="(0)"
>
<button
tuiIconButton
appearance="outline"
size="xs"
iconLeft="tuiIconMoreHorizontal"
[style.border-radius.%]="100"
>
Actions
</button>
<ng-template #content let-close="close">
<app-actions [actions]="actions" (click)="close()">
{{ title }}
</app-actions>
</ng-template>
</tui-hosted-dropdown>
</span>
}
`,
styles: [
`
:host {
display: flex;
height: 5.5rem;
width: 12.5rem;
border-radius: var(--tui-radius-l);
overflow: hidden;
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Theme
background: rgb(111 109 109);
}
// @Component({
// selector: '[appCard]',
// template: `
// <span class="link">
// <tui-badged-content [style.--tui-radius.rem]="1.5">
// @if (badge) {
// <tui-badge-notification size="m" tuiSlot="top">
// {{ badge }}
// </tui-badge-notification>
// }
// @if (icon?.startsWith('tuiIcon')) {
// <tui-icon class="icon" [icon]="icon" />
// } @else {
// <img alt="" class="icon" [src]="icon" />
// }
// </tui-badged-content>
// <label ticker class="title">{{ title }}</label>
// </span>
// @if (isService) {
// <span class="side">
// <tui-hosted-dropdown
// [content]="content"
// (click.stop.prevent)="(0)"
// (pointerdown.stop)="(0)"
// >
// <button
// tuiIconButton
// appearance="outline"
// size="xs"
// iconLeft="tuiIconMoreHorizontal"
// [style.border-radius.%]="100"
// >
// Actions
// </button>
// <ng-template #content let-close="close">
// <app-actions [actions]="actions" (click)="close()">
// {{ title }}
// </app-actions>
// </ng-template>
// </tui-hosted-dropdown>
// </span>
// }
// `,
// styles: [
// `
// :host {
// display: flex;
// height: 5.5rem;
// width: 12.5rem;
// border-radius: var(--tui-radius-l);
// overflow: hidden;
// box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// // TODO: Theme
// background: rgb(111 109 109);
// }
.link {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
gap: 0.25rem;
padding: 0 0.5rem;
font: var(--tui-font-text-m);
white-space: nowrap;
overflow: hidden;
}
// .link {
// display: flex;
// flex: 1;
// flex-direction: column;
// align-items: center;
// justify-content: center;
// color: white;
// gap: 0.25rem;
// padding: 0 0.5rem;
// font: var(--tui-font-text-m);
// white-space: nowrap;
// overflow: hidden;
// }
.icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 100%;
color: var(--tui-text-01-night);
}
// .icon {
// width: 2.5rem;
// height: 2.5rem;
// border-radius: 100%;
// color: var(--tui-text-01-night);
// }
.side {
width: 3rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// TODO: Theme
background: #4b4a4a;
}
`,
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
RouterLink,
TuiButtonModule,
TuiHostedDropdownModule,
TuiDataListModule,
TuiIconModule,
TickerModule,
TuiBadgedContentModule,
TuiBadgeNotificationModule,
ActionsComponent,
],
})
export class CardComponent {
@Input({ required: true })
id!: string
// .side {
// width: 3rem;
// display: flex;
// align-items: center;
// justify-content: center;
// box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
// // TODO: Theme
// background: #4b4a4a;
// }
// `,
// ],
// standalone: true,
// changeDetection: ChangeDetectionStrategy.OnPush,
// imports: [
// CommonModule,
// RouterLink,
// TuiButtonModule,
// TuiHostedDropdownModule,
// TuiDataListModule,
// TuiIconModule,
// TickerModule,
// TuiBadgedContentModule,
// TuiBadgeNotificationModule,
// ActionsComponent,
// ],
// })
// export class CardComponent {
// @Input({ required: true })
// id!: string
@Input({ required: true })
icon!: string
// @Input({ required: true })
// icon!: string
@Input({ required: true })
title!: string
// @Input({ required: true })
// title!: string
@Input()
actions: Record<string, readonly Action[]> = {}
// @Input()
// actions: Record<string, readonly Action[]> = {}
@Input()
badge: number | null = null
// @Input()
// badge: number | null = null
get isService(): boolean {
return !this.id.includes('/')
}
// get isService(): boolean {
// return !this.id.includes('/')
// }
// Prevents Firefox from starting a native drag
@HostListener('pointerdown.prevent')
onDown() {}
}
// // Prevents Firefox from starting a native drag
// @HostListener('pointerdown.prevent')
// onDown() {}
// }

View File

@@ -8,7 +8,7 @@ import {
} from '@angular/core'
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import {
tuiMarkControlAsTouchedAndValidate,
TuiValueChangesModule,

View File

@@ -37,13 +37,13 @@ import { ConfigService } from 'src/app/services/config.service'
<div tuiCell>
<div tuiTitle>
<strong>CA fingerprint</strong>
<div tuiSubtitle>{{ server['ca-fingerprint'] }}</div>
<div tuiSubtitle>{{ server.caFingerprint }}</div>
</div>
<button
tuiIconButton
appearance="icon"
iconLeft="tuiIconCopy"
(click)="copyService.copy(server['ca-fingerprint'])"
(click)="copyService.copy(server.caFingerprint)"
>
Copy
</button>
@@ -62,7 +62,7 @@ import { ConfigService } from 'src/app/services/config.service'
],
})
export class AboutComponent {
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
readonly copyService = inject(CopyService)
readonly gitHash = inject(ConfigService).gitHash
}

View File

@@ -50,8 +50,8 @@ export class HeaderConnectionComponent {
inject(ConnectionService).networkConnected$,
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
inject(PatchDB<DataModel>)
.watch$('server-info', 'status-info')
.pipe(startWith({ restarting: false, 'shutting-down': false })),
.watch$('serverInfo', 'statusInfo')
.pipe(startWith({ restarting: false, shuttingDown: false })),
]).pipe(
map(([network, websocket, status]) => {
if (!network)
@@ -68,7 +68,7 @@ export class HeaderConnectionComponent {
icon: 'tuiIconCloudOff',
status: 'warning',
}
if (status['shutting-down'])
if (status.shuttingDown)
return {
message: 'Shutting Down',
color: 'var(--tui-neutral-fill)',

View File

@@ -169,7 +169,7 @@ export class HeaderComponent {
'ui',
'gaming',
'snake',
'high-score',
'highScore',
)
}

View File

@@ -46,7 +46,7 @@ import { NotificationService } from '../../services/notification.service'
tuiCell
[notification]="not"
>
<ng-container *ngIf="not['package-id'] as pkgId">
<ng-container *ngIf="not.packageId as pkgId">
{{ $any(packageData[pkgId])?.manifest.title || pkgId }}
</ng-container>
<button
@@ -57,11 +57,11 @@ import { NotificationService } from '../../services/notification.service'
(click)="markSeen(notifications, not)"
></button>
<a
*ngIf="not['package-id'] && packageData[not['package-id']]"
*ngIf="not.packageId && packageData[not.packageId]"
tuiButton
size="xs"
appearance="secondary"
[routerLink]="getLink(not['package-id'] || '')"
[routerLink]="getLink(not.packageId || '')"
>
View Service
</a>
@@ -104,7 +104,7 @@ export class HeaderNotificationsComponent {
private readonly patch = inject(PatchDB<DataModel>)
private readonly service = inject(NotificationService)
readonly packageData$ = this.patch.watch$('package-data').pipe(first())
readonly packageData$ = this.patch.watch$('packageData').pipe(first())
readonly notifications$ = new Subject<ServerNotifications>()
@@ -112,7 +112,7 @@ export class HeaderNotificationsComponent {
ngAfterViewInit() {
this.patch
.watch$('server-info', 'unreadNotifications', 'recent')
.watch$('serverInfo', 'unreadNotifications', 'recent')
.pipe(
tap(recent => this.notifications$.next(recent)),
first(),

View File

@@ -0,0 +1,87 @@
import { NgIf } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { WINDOW } from '@ng-web-apis/common'
import { CopyService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import {
TuiButtonModule,
TuiCellModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { QRComponent } from 'src/app/common/qr.component'
import { mask } from 'src/app/util/mask'
@Component({
standalone: true,
selector: 'app-interface-address',
template: `
<div tuiCell>
<h3 tuiTitle>
<span tuiSubtitle>{{ isMasked ? mask : address }}</span>
</h3>
<tui-badge appearance="success">
{{ label }}
</tui-badge>
<button
*ngIf="isUi"
tuiIconButton
iconLeft="tuiIconExternalLink"
appearance="icon"
(click)="launch(address)"
>
Launch
</button>
<button
tuiIconButton
iconLeft="tuiIconGrid"
appearance="icon"
(click)="showQR(address)"
>
Show QR code
</button>
<button
tuiIconButton
iconLeft="tuiIconCopy"
appearance="icon"
(click)="copyService.copy(address)"
>
Copy QR code
</button>
</div>
`,
imports: [NgIf, TuiCellModule, TuiTitleModule, TuiButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceAddressComponent {
private readonly window = inject(WINDOW)
private readonly dialogs = inject(TuiDialogService)
readonly copyService = inject(CopyService)
@Input() label?: string
@Input({ required: true }) address!: string
@Input({ required: true }) isMasked!: boolean
@Input({ required: true }) isUi!: boolean
get mask(): string {
return mask(this.address, 64)
}
launch(url: string): void {
this.window.open(url, '_blank', 'noreferrer')
}
showQR(data: string) {
this.dialogs
.open(new PolymorpheusComponent(QRComponent), {
size: 'auto',
data,
})
.subscribe()
}
}

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common'
import { NgForOf, NgIf } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
@@ -20,10 +20,9 @@ import {
} from 'src/app/apps/portal/components/interfaces/interface.utils'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DomainInfo, NetworkInfo } from 'src/app/services/patch-db/data-model'
import { getClearnetAddress } from 'src/app/util/clearnetAddress'
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
import { InterfaceAddressComponent } from './interface-addresses.component'
import { InterfaceComponent } from './interface.component'
import { InterfacesComponent } from './interfaces.component'
type ClearnetForm = {
domain: string
@@ -45,32 +44,36 @@ type ClearnetForm = {
</a>
</em>
<ng-container
*ngIf="interfaces.addressInfo.domainInfo as domainInfo; else noClearnet"
*ngIf="
interface.serviceInterface.addresses.clearnet as addresses;
else empty
"
>
<app-interface
label="Clearnet"
[hostname]="getClearnet(domainInfo)"
[isUi]="interfaces.isUi"
<app-interface-address
*ngFor="let address of addresses"
[label]="address.label"
[address]="address.url"
[isMasked]="interface.serviceInterface.masked"
[isUi]="interface.serviceInterface.type === 'ui'"
/>
<div [style.display]="'flex'" [style.gap.rem]="1">
<button tuiButton size="s" (click)="add()">Update</button>
<button tuiButton size="s" appearance="danger-solid" (click)="remove()">
Remove
</button>
</div>
</ng-container>
<ng-template #noClearnet>
<ng-template #empty>
<button
tuiButton
iconLeft="tuiIconPlus"
[style.align-self]="'flex-start'"
(click)="add()"
>
Add Clearnet
Add Address
</button>
</ng-template>
`,
imports: [InterfaceComponent, NgIf, TuiButtonModule],
imports: [NgForOf, InterfaceAddressComponent, NgIf, TuiButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceClearnetComponent {
@@ -79,21 +82,14 @@ export class InterfaceClearnetComponent {
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly dialogs = inject(TuiDialogService)
readonly interfaces = inject(InterfacesComponent)
readonly interface = inject(InterfaceComponent)
@Input({ required: true }) network!: NetworkInfo
getClearnet(clearnet: DomainInfo): string {
return getClearnetAddress('https', clearnet)
}
async add() {
const { domainInfo } = this.interfaces.addressInfo
const { domain = '', subdomain = '' } = domainInfo || {}
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
label: 'Select Domain/Subdomain',
data: {
value: { domain, subdomain },
spec: await getClearnetSpec(this.network),
buttons: [
{
@@ -118,9 +114,9 @@ export class InterfaceClearnetComponent {
const loader = this.loader.open('Removing...').subscribe()
try {
if (this.interfaces.packageContext) {
if (this.interface.packageContext) {
await this.api.setInterfaceClearnetAddress({
...this.interfaces.packageContext,
...this.interface.packageContext,
domainInfo: null,
})
} else {
@@ -138,9 +134,9 @@ export class InterfaceClearnetComponent {
const loader = this.loader.open('Saving...').subscribe()
try {
if (this.interfaces.packageContext) {
if (this.interface.packageContext) {
await this.api.setInterfaceClearnetAddress({
...this.interfaces.packageContext,
...this.interface.packageContext,
domainInfo,
})
} else {

View File

@@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'
import { NgForOf, NgIf } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { InterfacesComponent } from './interfaces.component'
import { InterfaceComponent } from './interface.component'
import { InterfaceAddressComponent } from './interface-addresses.component'
@Component({
standalone: true,
@@ -19,41 +19,44 @@ import { InterfaceComponent } from './interface.component'
<strong>View instructions</strong>
</a>
</em>
<a
*ngIf="!interfaces.packageContext"
tuiButton
iconLeft="tuiIconDownload"
href="/public/eos/local.crt"
[download]="interfaces.addressInfo.lanHostname + '.crt'"
[style.align-self]="'flex-start'"
>
Download Root CA
</a>
<app-interface
label="Local"
[hostname]="interfaces.addressInfo.lanHostname"
[isUi]="interfaces.isUi"
></app-interface>
<ng-container
*ngFor="let iface of interfaces.addressInfo.ipInfo | keyvalue"
*ngIf="
interface.serviceInterface.addresses.local as addresses;
else empty
"
>
<app-interface
*ngIf="iface.value.ipv4 as ipv4"
[label]="iface.key + ' (IPv4)'"
[hostname]="ipv4"
[isUi]="interfaces.isUi"
></app-interface>
<app-interface
*ngIf="iface.value.ipv6 as ipv6"
[label]="iface.key + ' (IPv6)'"
[hostname]="ipv6"
[isUi]="interfaces.isUi"
></app-interface>
<app-interface-address
*ngFor="let address of addresses"
[label]="address.label"
[address]="address.url"
[isMasked]="interface.serviceInterface.masked"
[isUi]="interface.serviceInterface.type === 'ui'"
/>
<div [style.display]="'flex'" [style.gap.rem]="1">
<button tuiButton size="s" appearance="danger-solid" (click)="remove()">
Remove
</button>
</div>
</ng-container>
<ng-template #empty>
<button
tuiButton
iconLeft="tuiIconPlus"
[style.align-self]="'flex-start'"
(click)="add()"
>
Add Address
</button>
</ng-template>
`,
imports: [InterfaceComponent, CommonModule, TuiButtonModule],
imports: [NgForOf, NgIf, InterfaceAddressComponent, TuiButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceLocalComponent {
readonly interfaces = inject(InterfacesComponent)
readonly interface = inject(InterfaceComponent)
async add() {}
async remove() {}
}

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { InterfaceAddressComponent } from './interface-addresses.component'
import { InterfaceComponent } from './interface.component'
import { InterfacesComponent } from './interfaces.component'
import { NgForOf, NgIf } from '@angular/common'
@Component({
standalone: true,
@@ -17,15 +18,49 @@ import { InterfacesComponent } from './interfaces.component'
<strong>View instructions</strong>
</a>
</em>
<app-interface
label="Tor"
[hostname]="interfaces.addressInfo.torHostname"
[isUi]="interfaces.isUi"
></app-interface>
<ng-container
*ngIf="interface.serviceInterface.addresses.tor as addresses; else empty"
>
<app-interface-address
*ngFor="let address of addresses"
[label]="address.label"
[address]="address.url"
[isMasked]="interface.serviceInterface.masked"
[isUi]="interface.serviceInterface.type === 'ui'"
/>
<div [style.display]="'flex'" [style.gap.rem]="1">
<button tuiButton size="s" appearance="danger-solid" (click)="remove()">
Remove
</button>
</div>
</ng-container>
<ng-template #empty>
<button
tuiButton
iconLeft="tuiIconPlus"
[style.align-self]="'flex-start'"
(click)="add()"
>
Add Address
</button>
</ng-template>
<app-interface-address
*ngFor="let address of interface.serviceInterface.addresses.tor"
[label]="address.label"
[address]="address.url"
[isMasked]="interface.serviceInterface.masked"
[isUi]="interface.serviceInterface.type === 'ui'"
/>
`,
imports: [InterfaceComponent],
imports: [NgForOf, NgIf, InterfaceAddressComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceTorComponent {
readonly interfaces = inject(InterfacesComponent)
readonly interface = inject(InterfaceComponent)
async add() {}
async remove() {}
}

View File

@@ -1,79 +1,61 @@
import { NgIf } from '@angular/common'
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { WINDOW } from '@ng-web-apis/common'
import { CopyService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import {
TuiButtonModule,
TuiCellModule,
TuiTitleModule,
} from '@taiga-ui/experimental'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { QRComponent } from 'src/app/common/qr.component'
import { T } from '@start9labs/start-sdk'
import { TuiCardModule, TuiSurfaceModule } from '@taiga-ui/experimental'
import { PatchDB } from 'patch-db-client'
import { InterfaceClearnetComponent } from 'src/app/apps/portal/components/interfaces/interface-clearnet.component'
import { InterfaceLocalComponent } from 'src/app/apps/portal/components/interfaces/interface-local.component'
import { InterfaceTorComponent } from 'src/app/apps/portal/components/interfaces/interface-tor.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { AddressDetails } from './interface.utils'
@Component({
standalone: true,
selector: 'app-interface',
template: `
<div tuiCell>
<h3 tuiTitle>
{{ label }}
<span tuiSubtitle>{{ hostname }}</span>
</h3>
<button
*ngIf="isUi"
tuiIconButton
iconLeft="tuiIconExternalLink"
appearance="icon"
(click)="launch(hostname)"
>
Launch
</button>
<button
tuiIconButton
iconLeft="tuiIconGrid"
appearance="icon"
(click)="showQR(hostname)"
>
Show QR code
</button>
<button
tuiIconButton
iconLeft="tuiIconCopy"
appearance="icon"
(click)="copyService.copy(hostname)"
>
Copy QR code
</button>
</div>
<h3 class="g-title">Clearnet</h3>
<app-interface-clearnet
*ngIf="network$ | async as network"
tuiCardLarge="compact"
tuiSurface="elevated"
[network]="network"
/>
<h3 class="g-title">Tor</h3>
<app-interface-tor tuiCardLarge="compact" tuiSurface="elevated" />
<h3 class="g-title">Local</h3>
<app-interface-local tuiCardLarge="compact" tuiSurface="elevated" />
`,
imports: [NgIf, TuiCellModule, TuiTitleModule, TuiButtonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
InterfaceTorComponent,
InterfaceLocalComponent,
InterfaceClearnetComponent,
TuiCardModule,
TuiSurfaceModule,
],
})
export class InterfaceComponent {
private readonly window = inject(WINDOW)
private readonly dialogs = inject(TuiDialogService)
readonly copyService = inject(CopyService)
readonly network$ = inject(PatchDB<DataModel>).watch$('serverInfo', 'network')
@Input({ required: true }) label = ''
@Input({ required: true }) hostname = ''
@Input({ required: true }) isUi = false
launch(url: string): void {
this.window.open(url, '_blank', 'noreferrer')
@Input() packageContext?: {
packageId: string
interfaceId: string
}
@Input({ required: true }) serviceInterface!: ServiceInterfaceWithAddresses
}
showQR(data: string) {
this.dialogs
.open(new PolymorpheusComponent(QRComponent), {
size: 'auto',
data,
})
.subscribe()
export type ServiceInterfaceWithAddresses = T.ServiceInterface & {
addresses: {
clearnet: AddressDetails[]
local: AddressDetails[]
tor: AddressDetails[]
}
}

View File

@@ -1,6 +1,7 @@
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { T } from '@start9labs/start-sdk'
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiPromptData } from '@taiga-ui/kit'
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
@@ -44,3 +45,96 @@ export function getClearnetSpec({
}),
)
}
export type AddressDetails = {
label?: string
url: string
}
export function getAddresses(
serviceInterface: T.ServiceInterfaceWithHostInfo,
): {
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 clearnet: AddressDetails[] = []
const local: AddressDetails[] = []
const tor: AddressDetails[] = []
hostnames.forEach(h => {
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,
),
})
} else {
local.push({
label:
hostnameKind === 'local'
? 'Local'
: `${h.networkInterfaceId} (${hostnameKind})`,
url: toHref(scheme, username, h.hostname.value, port, suffix),
})
}
}
})
return {
clearnet,
local,
tor,
}
}
function toHref(
scheme: string,
username: string,
hostname: string,
port: string,
suffix: string,
): string {
return `${scheme}${username}${hostname}${port}${suffix}`
}

View File

@@ -1,55 +0,0 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { TuiCardModule, TuiSurfaceModule } from '@taiga-ui/experimental'
import { PatchDB } from 'patch-db-client'
import { InterfaceClearnetComponent } from 'src/app/apps/portal/components/interfaces/interface-clearnet.component'
import { InterfaceLocalComponent } from 'src/app/apps/portal/components/interfaces/interface-local.component'
import { InterfaceTorComponent } from 'src/app/apps/portal/components/interfaces/interface-tor.component'
import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model'
@Component({
standalone: true,
selector: 'app-interfaces',
template: `
<h3 class="g-title">Clearnet</h3>
<app-interface-clearnet
*ngIf="network$ | async as network"
tuiCardLarge="compact"
tuiSurface="elevated"
[network]="network"
/>
<h3 class="g-title">Tor</h3>
<app-interface-tor tuiCardLarge="compact" tuiSurface="elevated" />
<h3 class="g-title">Local</h3>
<app-interface-local tuiCardLarge="compact" tuiSurface="elevated" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
InterfaceTorComponent,
InterfaceLocalComponent,
InterfaceClearnetComponent,
TuiCardModule,
TuiSurfaceModule,
],
})
export class InterfacesComponent {
readonly network$ = inject(PatchDB<DataModel>).watch$(
'server-info',
'network',
)
@Input() packageContext?: {
packageId: string
interfaceId: string
}
@Input({ required: true }) addressInfo!: AddressInfo
@Input({ required: true }) isUi!: boolean
}

View File

@@ -25,7 +25,7 @@ export class LogsFetchDirective {
}),
),
),
tap(res => this.component.setCursor(res['start-cursor'])),
tap(res => this.component.setCursor(res.startCursor)),
map(({ entries }) => convertAnsi(entries)),
catchError(e => {
this.errors.handleError(e)

View File

@@ -43,7 +43,7 @@ export class LogsPipe implements PipeTransform {
map(() => getMessage(true)),
),
defer(() => followLogs(this.options)).pipe(
tap(r => this.logs.setCursor(r['start-cursor'])),
tap(r => this.logs.setCursor(r.startCursor)),
switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))),
bufferTime(1000),
filter(logs => !!logs.length),

View File

@@ -6,7 +6,7 @@ import {
isEmptyObject,
LoadingService,
} from '@start9labs/shared'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiDialogContext,
@@ -27,7 +27,11 @@ import {
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages, getPackage } from 'src/app/util/get-package-data'
import {
getAllPackages,
getManifest,
getPackage,
} from 'src/app/util/get-package-data'
import { Breakages } from 'src/app/services/api/api.types'
import { InvalidService } from 'src/app/common/form/invalid.service'
import {
@@ -35,6 +39,7 @@ import {
FormComponent,
} from 'src/app/apps/portal/components/form.component'
import { DependentInfo } from 'src/app/types/dependent-info'
import { ToManifestPipe } from '../pipes/to-manifest'
export interface PackageConfigData {
readonly pkgId: string
@@ -52,23 +57,26 @@ export interface PackageConfigData {
<div [innerHTML]="loadingError"></div>
</tui-notification>
<ng-container *ngIf="!loadingText && !loadingError && pkg">
<ng-container
*ngIf="
!loadingText && !loadingError && pkg && (pkg | toManifest) as manifest
"
>
<tui-notification *ngIf="success" status="success">
{{ pkg.manifest.title }} has been automatically configured with
recommended defaults. Make whatever changes you want, then click "Save".
{{ manifest.title }} has been automatically configured with recommended
defaults. Make whatever changes you want, then click "Save".
</tui-notification>
<config-dep
*ngIf="dependentInfo && value && original"
[package]="pkg.manifest.title"
[package]="manifest.title"
[dep]="dependentInfo.title"
[original]="original"
[value]="value"
/>
<tui-notification *ngIf="!pkg.installed?.['has-config']" status="warning">
No config options for {{ pkg.manifest.title }}
{{ pkg.manifest.version }}.
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
No config options for {{ manifest.title }} {{ manifest.version }}.
</tui-notification>
<app-form
@@ -106,6 +114,7 @@ export interface PackageConfigData {
TuiButtonModule,
TuiModeModule,
ConfigDepComponent,
ToManifestPipe,
],
providers: [InvalidService],
})
@@ -149,7 +158,7 @@ export class ServiceConfigModal {
!!this.form &&
!this.form.form.dirty &&
!this.original &&
!this.pkg?.installed?.status?.configured
!this.pkg?.status?.configured
)
}
@@ -165,12 +174,12 @@ export class ServiceConfigModal {
if (this.dependentInfo) {
const depConfig = await this.embassyApi.dryConfigureDependency({
'dependency-id': this.pkgId,
'dependent-id': this.dependentInfo.id,
dependencyId: this.pkgId,
dependentId: this.dependentInfo.id,
})
this.original = depConfig['old-config']
this.value = depConfig['new-config'] || this.original
this.original = depConfig.oldConfig
this.value = depConfig.newConfig || this.original
this.spec = depConfig.spec
this.patch = compare(this.original, this.value)
} else {
@@ -195,7 +204,7 @@ export class ServiceConfigModal {
try {
await this.uploadFiles(config, loader)
if (hasCurrentDeps(this.pkg!)) {
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
await this.configureDeps(config, loader)
} else {
await this.configure(config, loader)
@@ -260,7 +269,7 @@ export class ServiceConfigModal {
const message =
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const content = `${message}${Object.keys(breakages).map(
id => `<li><b>${packages[id].manifest.title}</b></li>`,
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
)}</ul>`
const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' }

View File

@@ -1,15 +0,0 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { BadgeService } from '../services/badge.service'
import { Observable } from 'rxjs'
@Pipe({
name: 'toBadge',
standalone: true,
})
export class ToBadgePipe implements PipeTransform {
readonly badge = inject(BadgeService)
transform(id: string): Observable<number> {
return this.badge.getCount(id)
}
}

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Manifest } from '@start9labs/marketplace'
import { getManifest } from 'src/app/util/get-package-data'
@Pipe({
name: 'toManifest',
standalone: true,
})
export class ToManifestPipe implements PipeTransform {
transform(pkg: PackageDataEntry): Manifest {
return getManifest(pkg)
}
}

View File

@@ -10,24 +10,23 @@ import {
TuiButtonModule,
tuiButtonOptionsProvider,
} from '@taiga-ui/experimental'
import { map, of } from 'rxjs'
import { UIComponent } from 'src/app/apps/portal/routes/dashboard/ui.component'
import { map, Observable } from 'rxjs'
import { UILaunchComponent } from 'src/app/apps/portal/routes/dashboard/ui.component'
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
import { DepErrorService } from 'src/app/services/dep-error.service'
import {
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data'
import { Manifest } from '@start9labs/marketplace'
@Component({
standalone: true,
selector: 'fieldset[appControls]',
template: `
@if (isRunning) {
@if (pkg.status.main.status === 'running') {
<button
tuiIconButton
iconLeft="tuiIconSquare"
(click)="actions.stop(appControls)"
(click)="actions.stop(manifest)"
>
Stop
</button>
@@ -35,17 +34,17 @@ import {
<button
tuiIconButton
iconLeft="tuiIconRotateCw"
(click)="actions.restart(appControls)"
(click)="actions.restart(manifest)"
>
Restart
</button>
} @else {
<button
*tuiLet="hasUnmet(appControls) | async as hasUnmet"
*tuiLet="hasUnmet(pkg) | async as hasUnmet"
tuiIconButton
iconLeft="tuiIconPlay"
[disabled]="!isConfigured"
(click)="actions.start(appControls, !!hasUnmet)"
[disabled]="!this.pkg.status.configured"
(click)="actions.start(manifest, !!hasUnmet)"
>
Start
</button>
@@ -53,48 +52,39 @@ import {
<button
tuiIconButton
iconLeft="tuiIconTool"
(click)="actions.configure(appControls)"
(click)="actions.configure(manifest)"
>
Configure
</button>
}
<app-ui [pkg]="appControls" />
<app-ui-launch [pkg]="pkg" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButtonModule, UIComponent, TuiLetModule, AsyncPipe],
imports: [TuiButtonModule, UILaunchComponent, TuiLetModule, AsyncPipe],
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
})
export class ControlsComponent {
private readonly errors = inject(DepErrorService)
@Input()
appControls!: PackageDataEntry
pkg!: PackageDataEntry
get manifest(): Manifest {
return getManifest(this.pkg)
}
readonly actions = inject(ActionsService)
get isRunning(): boolean {
return (
this.appControls.installed?.status.main.status ===
PackageMainStatus.Running
@tuiPure
hasUnmet(pkg: PackageDataEntry): Observable<boolean> {
const id = getManifest(pkg).id
return this.errors.getPkgDepErrors$(id).pipe(
map(errors =>
Object.keys(pkg.currentDependencies)
.map(id => !!(errors[id] as any)?.[id]) // @TODO fix
.some(Boolean),
),
)
}
get isConfigured(): boolean {
return !!this.appControls.installed?.status.configured
}
@tuiPure
hasUnmet({ installed, manifest }: PackageDataEntry) {
return installed
? this.errors.getPkgDepErrors$(manifest.id).pipe(
map(errors =>
Object.keys(installed['current-dependencies'])
.filter(id => !!manifest.dependencies[id])
.map(id => !!(errors[manifest.id] as any)?.[id]) // @TODO fix
.some(Boolean),
),
)
: of(false)
}
}

View File

@@ -11,29 +11,26 @@ import { ControlsComponent } from 'src/app/apps/portal/routes/dashboard/controls
import { StatusComponent } from 'src/app/apps/portal/routes/dashboard/status.component'
import { ConnectionService } from 'src/app/services/connection.service'
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
import {
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data'
@Component({
standalone: true,
selector: 'tr[appService]',
template: `
<td><img alt="logo" [src]="appService.icon" /></td>
<td><img alt="logo" [src]="pkg.icon" /></td>
<td>
<a [routerLink]="routerLink">{{ appService.manifest.title }}</a>
<a [routerLink]="routerLink">{{ manifest.title }}</a>
</td>
<td>{{ appService.manifest.version }}</td>
<td
[appStatus]="appService"
[appStatusError]="hasError(appServiceError)"
></td>
<td>{{ manifest.version }}</td>
<td appStatus [pkg]="pkg" [hasDepErrors]="hasError(depErrors)"></td>
<td [style.text-align]="'center'">
<fieldset
[disabled]="!installed || !(connected$ | async)"
[appControls]="appService"
appControls
[disabled]="
this.pkg.stateInfo.state !== 'installed' || !(connected$ | async)
"
[pkg]="pkg"
></fieldset>
</td>
`,
@@ -57,19 +54,19 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
})
export class ServiceComponent {
@Input()
appService!: PackageDataEntry
pkg!: PackageDataEntry
@Input()
appServiceError?: PkgDependencyErrors
depErrors?: PkgDependencyErrors
readonly connected$ = inject(ConnectionService).connected$
get routerLink() {
return `/portal/service/${this.appService.manifest.id}`
get manifest() {
return getManifest(this.pkg)
}
get installed(): boolean {
return this.appService.state === PackageState.Installed
get routerLink() {
return `/portal/service/${this.manifest.id}`
}
@tuiPure

View File

@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ServiceComponent } from 'src/app/apps/portal/routes/dashboard/service.component'
import { ServicesService } from 'src/app/apps/portal/services/services.service'
import { DepErrorService } from 'src/app/services/dep-error.service'
import { ToManifestPipe } from '../../pipes/to-manifest'
@Component({
standalone: true,
@@ -21,10 +22,11 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
</thead>
<tbody>
@if (errors$ | async; as errors) {
@for (service of services$ | async; track $index) {
@for (pkg of services$ | async; track $index) {
<tr
[appService]="service"
[appServiceError]="errors[service.manifest.id]"
appService
[pkg]="pkg"
[depErrors]="errors[(pkg | toManifest).id]"
></tr>
} @empty {
<tr>
@@ -78,7 +80,7 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ServiceComponent, AsyncPipe],
imports: [ServiceComponent, AsyncPipe, ToManifestPipe],
})
export class ServicesComponent {
readonly services$ = inject(ServicesService)

View File

@@ -2,16 +2,13 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiLoaderModule } from '@taiga-ui/core'
import { TuiIconModule } from '@taiga-ui/experimental'
import {
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import {
HealthStatus,
PrimaryStatus,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
import { InstallingProgressDisplayPipe } from '../service/pipes/install-progress.pipe'
@Component({
standalone: true,
@@ -41,27 +38,23 @@ import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
})
export class StatusComponent {
@Input()
appStatus!: PackageDataEntry
pkg!: PackageDataEntry
@Input()
appStatusError = false
hasDepErrors = false
get healthy(): boolean {
const status = this.getStatus(this.appStatus)
const status = this.getStatus(this.pkg)
return (
!this.appStatusError && // no deps error
!!this.appStatus.installed?.status.configured && // no config needed
status.primary !== PackageState.NeedsUpdate && // no update needed
!this.hasDepErrors && // no deps error
!!this.pkg.status.configured && // no config needed
status.health !== HealthStatus.Failure // no health issues
)
}
get loading(): boolean {
return (
!!this.appStatus['install-progress'] ||
this.color === 'var(--tui-info-fill)'
)
return !!this.pkg.stateInfo || this.color === 'var(--tui-info-fill)'
}
@tuiPure
@@ -70,17 +63,15 @@ export class StatusComponent {
}
get status(): string {
if (this.appStatus['install-progress']) {
return `Installing... ${packageLoadingProgress(this.appStatus['install-progress'])?.totalProgress || 0}%`
if (this.pkg.stateInfo.installingInfo) {
return `Installing...${new InstallingProgressDisplayPipe().transform(this.pkg.stateInfo.installingInfo.progress.overall)}`
}
switch (this.getStatus(this.appStatus).primary) {
switch (this.getStatus(this.pkg).primary) {
case PrimaryStatus.Running:
return 'Running'
case PrimaryStatus.Stopped:
return 'Stopped'
case PackageState.NeedsUpdate:
return 'Needs Update'
case PrimaryStatus.NeedsConfig:
return 'Needs Config'
case PrimaryStatus.Updating:
@@ -103,14 +94,13 @@ export class StatusComponent {
}
get color(): string {
if (this.appStatus['install-progress']) {
if (this.pkg.stateInfo.installingInfo) {
return 'var(--tui-info-fill)'
}
switch (this.getStatus(this.appStatus).primary) {
switch (this.getStatus(this.pkg).primary) {
case PrimaryStatus.Running:
return 'var(--tui-success-fill)'
case PackageState.NeedsUpdate:
case PrimaryStatus.NeedsConfig:
return 'var(--tui-warning-fill)'
case PrimaryStatus.Updating:

View File

@@ -4,20 +4,19 @@ import {
inject,
Input,
} from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { ConfigService } from 'src/app/services/config.service'
import {
InstalledPackageInfo,
InterfaceInfo,
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
@Component({
standalone: true,
selector: 'app-ui',
selector: 'app-ui-launch',
template: `
@if (interfaces.length > 1) {
<tui-hosted-dropdown [content]="content">
@@ -26,7 +25,7 @@ import {
iconLeft="tuiIconExternalLink"
[disabled]="!isRunning"
>
Interfaces
Launch UI
</button>
<ng-template #content>
<tui-data-list>
@@ -44,42 +43,44 @@ import {
</ng-template>
</tui-hosted-dropdown>
} @else {
<a
tuiIconButton
iconLeft="tuiIconExternalLink"
target="_blank"
rel="noreferrer"
[attr.href]="getHref(interfaces[0])"
>
{{ interfaces[0]?.name }}
</a>
@if (interfaces[0]; as info) {
<a
tuiIconButton
iconLeft="tuiIconExternalLink"
target="_blank"
rel="noreferrer"
[attr.href]="getHref(info)"
>
{{ info.name }}
</a>
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule],
})
export class UIComponent {
export class UILaunchComponent {
private readonly config = inject(ConfigService)
@Input()
pkg!: PackageDataEntry
get interfaces(): readonly InterfaceInfo[] {
return this.getInterfaces(this.pkg.installed)
get interfaces(): readonly T.ServiceInterfaceWithHostInfo[] {
return this.getInterfaces(this.pkg)
}
get isRunning(): boolean {
return this.pkg.installed?.status.main.status === PackageMainStatus.Running
return this.pkg.status.main.status === PackageMainStatus.Running
}
@tuiPure
getInterfaces(info?: InstalledPackageInfo): InterfaceInfo[] {
return info
? Object.values(info.interfaceInfo).filter(({ type }) => type === 'ui')
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterfaceWithHostInfo[] {
return pkg
? Object.values(pkg.serviceInterfaces).filter(({ type }) => type === 'ui')
: []
}
getHref(info?: InterfaceInfo): string | null {
getHref(info?: T.ServiceInterfaceWithHostInfo): string | null {
return info && this.isRunning ? this.config.launchableAddress(info) : null
}
}

View File

@@ -8,20 +8,19 @@ import { tuiPure } from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
import {
PackageDataEntry,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Manifest } from '@start9labs/marketplace'
import { getManifest } from 'src/app/util/get-package-data'
@Component({
selector: 'service-actions',
template: `
@if (isRunning) {
@if (pkg.status.main.status === 'running') {
<button
tuiButton
appearance="secondary-destructive"
iconLeft="tuiIconSquare"
(click)="actions.stop(service)"
(click)="actions.stop(manifest)"
>
Stop
</button>
@@ -30,17 +29,17 @@ import {
tuiButton
appearance="secondary"
iconLeft="tuiIconRotateCw"
(click)="actions.restart(service)"
(click)="actions.restart(manifest)"
>
Restart
</button>
}
@if (isStopped && isConfigured) {
@if (pkg.status.main.status === 'stopped' && isConfigured) {
<button
tuiButton
iconLeft="tuiIconPlay"
(click)="actions.start(service, hasUnmet(dependencies))"
(click)="actions.start(manifest, hasUnmet(dependencies))"
>
Start
</button>
@@ -51,7 +50,7 @@ import {
tuiButton
appearance="secondary-warning"
iconLeft="tuiIconTool"
(click)="actions.configure(service)"
(click)="actions.configure(manifest)"
>
Configure
</button>
@@ -64,7 +63,7 @@ import {
})
export class ServiceActionsComponent {
@Input({ required: true })
service!: PackageDataEntry
pkg!: PackageDataEntry
@Input({ required: true })
dependencies: readonly DependencyInfo[] = []
@@ -72,19 +71,11 @@ export class ServiceActionsComponent {
readonly actions = inject(ActionsService)
get isConfigured(): boolean {
return this.service.installed!.status.configured
return this.pkg.status.configured
}
get isRunning(): boolean {
return (
this.service.installed?.status.main.status === PackageMainStatus.Running
)
}
get isStopped(): boolean {
return (
this.service.installed?.status.main.status === PackageMainStatus.Stopped
)
get manifest(): Manifest {
return getManifest(this.pkg)
}
@tuiPure

View File

@@ -8,7 +8,7 @@ import { ServiceAdditionalItemComponent } from './additional-item.component'
selector: 'service-additional',
template: `
<h3 class="g-title">Additional Info</h3>
<ng-container *ngFor="let additional of service | toAdditional">
<ng-container *ngFor="let additional of pkg | toAdditional">
<a
*ngIf="additional.description.startsWith('http'); else button"
class="g-action"
@@ -30,5 +30,5 @@ import { ServiceAdditionalItemComponent } from './additional-item.component'
})
export class ServiceAdditionalComponent {
@Input({ required: true })
service!: PackageDataEntry
pkg!: PackageDataEntry
}

View File

@@ -91,23 +91,16 @@ export class ServiceHealthCheckComponent {
return 'Awaiting result...'
}
const prefix =
this.check.result !== HealthResult.Failure &&
this.check.result !== HealthResult.Loading
? this.check.result
: ''
switch (this.check.result) {
case HealthResult.Failure:
return prefix + this.check.error
case HealthResult.Starting:
return `${prefix}...`
return 'Starting...'
case HealthResult.Success:
return `${prefix}: ${this.check.message}`
return `Success: ${this.check.message}`
case HealthResult.Loading:
return prefix + this.check.message
case HealthResult.Failure:
return this.check.message
default:
return prefix
return this.check.result
}
}
}

View File

@@ -11,7 +11,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
@Component({
selector: 'a[serviceInterface]',
selector: 'a[serviceInterfaceListItem]',
template: `
<tui-svg [src]="info.icon" [style.color]="info.color"></tui-svg>
<div [style.flex]="1">
@@ -35,10 +35,10 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
standalone: true,
imports: [TuiButtonModule, CommonModule, TuiSvgModule],
})
export class ServiceInterfaceComponent {
export class ServiceInterfaceListItemComponent {
private readonly config = inject(ConfigService)
@Input({ required: true, alias: 'serviceInterface' })
@Input({ required: true, alias: 'serviceInterfaceListItem' })
info!: ExtendedInterfaceInfo
@Input()

View File

@@ -0,0 +1,36 @@
import { NgForOf } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
import { InterfaceInfoPipe } from '../pipes/interface-info.pipe'
import { ServiceInterfaceListItemComponent } from './interface-list-item.component'
import { RouterLink } from '@angular/router'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'service-interface-list',
template: `
<h3 class="g-title">Service Interfaces</h3>
<a
*ngFor="let info of pkg | interfaceInfo"
class="g-action"
[serviceInterfaceListItem]="info"
[disabled]="status.primary !== 'running'"
[routerLink]="info.routerLink"
></a>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NgForOf,
RouterLink,
InterfaceInfoPipe,
ServiceInterfaceListItemComponent,
],
})
export class ServiceInterfaceListComponent {
@Input({ required: true })
pkg!: PackageDataEntry
@Input({ required: true })
status!: PackageStatus
}

View File

@@ -1,35 +0,0 @@
import { NgForOf } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackagePlus } from 'src/app/services/patch-db/data-model'
import {
PackageStatus,
PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service'
import { InterfaceInfoPipe } from '../pipes/interface-info.pipe'
import { ServiceInterfaceComponent } from './interface.component'
import { RouterLink } from '@angular/router'
@Component({
selector: 'service-interfaces',
template: `
<h3 class="g-title">Interfaces</h3>
<a
*ngFor="let info of service.pkg | interfaceInfo"
class="g-action"
[serviceInterface]="info"
[disabled]="!isRunning(service.status)"
[routerLink]="info.routerLink"
></a>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgForOf, RouterLink, InterfaceInfoPipe, ServiceInterfaceComponent],
})
export class ServiceInterfacesComponent {
@Input({ required: true })
service!: PackagePlus
isRunning({ primary }: PackageStatus): boolean {
return primary === PrimaryStatus.Running
}
}

View File

@@ -8,7 +8,7 @@ import { RouterLink } from '@angular/router'
selector: 'service-menu',
template: `
<h3 class="g-title">Menu</h3>
@for (menu of service | toMenu; track $index) {
@for (menu of pkg | toMenu; track $index) {
@if (menu.routerLink) {
<a
class="g-action"
@@ -23,7 +23,7 @@ import { RouterLink } from '@angular/router'
(click)="menu.action?.()"
>
@if (menu.name === 'Outbound Proxy') {
<div [style.color]="color">{{ proxy }}</div>
<div [style.color]="color">{{ pkg.outboundProxy || 'None' }}</div>
}
</button>
}
@@ -35,22 +35,11 @@ import { RouterLink } from '@angular/router'
})
export class ServiceMenuComponent {
@Input({ required: true })
service!: PackageDataEntry
pkg!: PackageDataEntry
get color(): string {
return this.service.installed?.outboundProxy
return this.pkg.outboundProxy
? 'var(--tui-success-fill)'
: 'var(--tui-warning-fill)'
}
get proxy(): string {
switch (this.service.installed?.outboundProxy) {
case 'primary':
return 'System Primary'
case 'mirror':
return 'Mirror P2P'
default:
return this.service.installed?.outboundProxy?.proxyId || 'None'
}
}
}

View File

@@ -1,27 +1,30 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiProgressModule } from '@taiga-ui/kit'
import { Progress } from 'src/app/services/patch-db/data-model'
import { InstallingProgressPipe } from '../pipes/install-progress.pipe'
@Component({
selector: '[progress]',
template: `
<ng-content></ng-content>
: {{ progress }}%
<progress
tuiProgressBar
new
size="xs"
[style.color]="
progress === 100 ? 'var(--tui-positive)' : 'var(--tui-link)'
"
[value]="progress / 100"
></progress>
@if (progress | installingProgress; as decimal) {
: {{ decimal * 100 }}%
<progress
tuiProgressBar
new
size="xs"
[style.color]="
progress === true ? 'var(--tui-positive)' : 'var(--tui-link)'
"
[value]="decimal * 100"
></progress>
}
`,
styles: [':host { line-height: 2rem }'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiProgressModule],
imports: [TuiProgressModule, InstallingProgressPipe],
})
export class ServiceProgressComponent {
@Input({ required: true })
progress = 0
@Input({ required: true }) progress!: Progress
}

View File

@@ -10,7 +10,7 @@ import { TuiLabelModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
@Component({
selector: 'service-credential',
selector: 'service-property',
template: `
<label [style.flex]="1" [tuiLabel]="label">
{{ masked ? mask : value }}
@@ -48,7 +48,7 @@ import { TuiButtonModule } from '@taiga-ui/experimental'
standalone: true,
imports: [TuiButtonModule, TuiLabelModule],
})
export class ServiceCredentialComponent {
export class ServicePropertyComponent {
@Input()
label = ''

View File

@@ -5,22 +5,27 @@ import {
HostBinding,
Input,
} from '@angular/core'
import { InstallProgress } from 'src/app/services/patch-db/data-model'
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
import { InstallProgressPipe } from '../pipes/install-progress.pipe'
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { UnitConversionPipesModule } from '@start9labs/shared'
@Component({
selector: 'service-status',
template: `
@if (installProgress) {
@if (installingInfo) {
<strong>
Installing
<span class="loading-dots"></span>
{{ installProgress | installProgress }}
{{ installingInfo.progress.overall | installingProgressString }}
</strong>
} @else {
{{ connected ? rendering.display : 'Unknown' }}
<!-- @TODO should show 'this may take a while' if sigtermTimeout is > 30s -->
<span *ngIf="sigtermTimeout && (sigtermTimeout | durationToSeconds) > 30">
. This may take a while
</span>
<span *ngIf="rendering.showDots" class="loading-dots"></span>
}
`,
@@ -35,18 +40,24 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, InstallProgressPipe],
imports: [
CommonModule,
InstallingProgressDisplayPipe,
UnitConversionPipesModule,
],
})
export class ServiceStatusComponent {
@Input({ required: true })
rendering!: StatusRendering
@Input()
installProgress?: InstallProgress
installingInfo?: InstallingInfo
@Input()
connected = false
@Input() sigtermTimeout?: string | null = null
@HostBinding('style.color')
get color(): string {
if (!this.connected) return 'var(--tui-text-02)'

View File

@@ -6,17 +6,17 @@ import { TuiButtonModule } from '@taiga-ui/experimental'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { BehaviorSubject } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ServiceCredentialComponent } from '../components/credential.component'
import { ServicePropertyComponent } from '../components/property.component'
@Component({
template: `
@if (loading$ | async) {
<tui-loader />
} @else {
@for (cred of credentials | keyvalue: asIsOrder; track cred) {
<service-credential [label]="cred.key" [value]="cred.value" />
@for (prop of properties | keyvalue: asIsOrder; track prop) {
<service-property [label]="prop.key" [value]="prop.value" />
} @empty {
No credentials
No properties
}
}
<button tuiButton iconLeft="tuiIconRefreshCwLarge" (click)="refresh()">
@@ -36,32 +36,32 @@ import { ServiceCredentialComponent } from '../components/credential.component'
imports: [
CommonModule,
TuiButtonModule,
ServiceCredentialComponent,
ServicePropertyComponent,
TuiLoaderModule,
],
})
export class ServiceCredentialsModal {
export class ServicePropertiesModal {
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
readonly id = inject<{ data: string }>(POLYMORPHEUS_CONTEXT).data
readonly loading$ = new BehaviorSubject(true)
credentials: Record<string, string> = {}
properties: Record<string, string> = {}
async ngOnInit() {
await this.getCredentials()
await this.getProperties()
}
async refresh() {
await this.getCredentials()
await this.getProperties()
}
private async getCredentials(): Promise<void> {
private async getProperties(): Promise<void> {
this.loading$.next(true)
try {
this.credentials = await this.api.getPackageCredentials({ id: this.id })
this.properties = await this.api.getPackageProperties({ id: this.id })
} catch (e: any) {
this.errorService.handleError(e)
} finally {

View File

@@ -1,6 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core'
import { WithId } from '@start9labs/shared'
import { Action, PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ActionMetadata } from '@start9labs/start-sdk/cjs/sdk/lib/types'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'groupActions',
@@ -9,12 +10,12 @@ import { Action, PackageDataEntry } from 'src/app/services/patch-db/data-model'
export class GroupActionsPipe implements PipeTransform {
transform(
actions: PackageDataEntry['actions'],
): Array<Array<WithId<Action>>> | null {
): Array<Array<WithId<ActionMetadata>>> | null {
if (!actions) return null
const noGroup = 'noGroup'
const grouped = Object.entries(actions).reduce<
Record<string, WithId<Action>[]>
Record<string, WithId<ActionMetadata>[]>
>((groups, [id, action]) => {
const actionWithId = { id, ...action }
const groupKey = action.group || noGroup

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Progress } from 'src/app/services/patch-db/data-model'
import { Progress } from '../../../../../services/patch-db/data-model'
@Pipe({
standalone: true,

View File

@@ -1,10 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core'
import {
InterfaceInfo,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ServiceInterfaceWithHostInfo } from '@start9labs/start-sdk/cjs/sdk/lib/types'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
export interface ExtendedInterfaceInfo extends InterfaceInfo {
export interface ExtendedInterfaceInfo extends ServiceInterfaceWithHostInfo {
id: string
icon: string
color: string
@@ -17,8 +15,8 @@ export interface ExtendedInterfaceInfo extends InterfaceInfo {
standalone: true,
})
export class InterfaceInfoPipe implements PipeTransform {
transform({ installed }: PackageDataEntry): ExtendedInterfaceInfo[] {
return Object.entries(installed!.interfaceInfo).map(([id, val]) => {
transform(pkg: PackageDataEntry): ExtendedInterfaceInfo[] {
return Object.entries(pkg.serviceInterfaces).map(([id, val]) => {
let color: string
let icon: string
let typeDetail: string
@@ -39,11 +37,6 @@ export class InterfaceInfoPipe implements PipeTransform {
icon = 'tuiIconTerminalLarge'
typeDetail = 'Application Program Interface (API)'
break
case 'other':
color = 'var(--tui-text-02)'
icon = 'tuiIconBoxLarge'
typeDetail = 'Unknown Interface Type'
break
}
return {

View File

@@ -6,6 +6,7 @@ import { CopyService, MarkdownComponent } from '@start9labs/shared'
import { from } from 'rxjs'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getManifest } from 'src/app/util/get-package-data'
export const FALLBACK_URL = 'Not provided'
@@ -25,21 +26,22 @@ export class ToAdditionalPipe implements PipeTransform {
private readonly copyService = inject(CopyService)
private readonly dialogs = inject(TuiDialogService)
transform({ manifest, installed }: PackageDataEntry): AdditionalItem[] {
transform(pkg: PackageDataEntry): AdditionalItem[] {
const manifest = getManifest(pkg)
return [
{
name: 'Installed',
description: new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'medium',
}).format(new Date(installed?.['installed-at'] || 0)),
}).format(new Date(pkg.installedAt || 0)),
},
{
name: 'Git Hash',
description: manifest['git-hash'] || 'Unknown',
icon: manifest['git-hash'] ? 'tuiIconCopyLarge' : '',
description: manifest.gitHash || 'Unknown',
icon: manifest.gitHash ? 'tuiIconCopyLarge' : '',
action: () =>
manifest['git-hash'] && this.copyService.copy(manifest['git-hash']),
manifest.gitHash && this.copyService.copy(manifest.gitHash),
},
{
name: 'License',
@@ -49,19 +51,19 @@ export class ToAdditionalPipe implements PipeTransform {
},
{
name: 'Website',
description: manifest['marketing-site'] || FALLBACK_URL,
description: manifest.marketingSite || FALLBACK_URL,
},
{
name: 'Source Repository',
description: manifest['upstream-repo'],
description: manifest.upstreamRepo,
},
{
name: 'Support Site',
description: manifest['support-site'] || FALLBACK_URL,
description: manifest.supportSite || FALLBACK_URL,
},
{
name: 'Donation Link',
description: manifest['donation-url'] || FALLBACK_URL,
description: manifest.donationUrl || FALLBACK_URL,
},
]
}

View File

@@ -11,12 +11,10 @@ import {
} from 'src/app/apps/portal/modals/config.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import {
InstalledPackageInfo,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ProxyService } from 'src/app/services/proxy.service'
import { ServiceCredentialsModal } from '../modals/credentials.component'
import { ServicePropertiesModal } from '../modals/properties.component'
import { getManifest } from 'src/app/util/get-package-data'
export interface ServiceMenu {
icon: string
@@ -37,8 +35,8 @@ export class ToMenuPipe implements PipeTransform {
private readonly formDialog = inject(FormDialogService)
private readonly proxyService = inject(ProxyService)
transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] {
const url = installed?.['marketplace-url']
transform(pkg: PackageDataEntry): ServiceMenu[] {
const manifest = getManifest(pkg)
return [
{
@@ -55,11 +53,11 @@ export class ToMenuPipe implements PipeTransform {
},
{
icon: 'tuiIconKeyLarge',
name: 'Credentials',
description: `Password, keys, or other credentials of interest`,
name: 'Properties',
description: `Runtime information, credentials, and other values of interest`,
action: () =>
this.dialogs
.open(new PolymorpheusComponent(ServiceCredentialsModal), {
.open(new PolymorpheusComponent(ServicePropertiesModal), {
label: `${manifest.title} credentials`,
data: manifest.id,
})
@@ -75,7 +73,11 @@ export class ToMenuPipe implements PipeTransform {
icon: 'tuiIconShieldLarge',
name: 'Outbound Proxy',
description: `Proxy all outbound traffic from ${manifest.title}`,
action: () => this.setProxy(manifest, installed!),
action: () =>
this.proxyService.presentModalSetOutboundProxy(
pkg.outboundProxy,
manifest.id,
),
},
{
icon: 'tuiIconFileTextLarge',
@@ -83,13 +85,13 @@ export class ToMenuPipe implements PipeTransform {
description: `Raw, unfiltered logs`,
routerLink: 'logs',
},
url
pkg.marketplaceUrl
? {
icon: 'tuiIconShoppingBagLarge',
name: 'Marketplace Listing',
description: `View ${manifest.title} on the Marketplace`,
routerLink: `/portal/system/marketplace`,
params: { url, id: manifest.id },
params: { url: pkg.marketplaceUrl, id: manifest.id },
}
: {
icon: 'tuiIconShoppingBagLarge',
@@ -125,15 +127,4 @@ export class ToMenuPipe implements PipeTransform {
data: { pkgId: id },
})
}
private setProxy(
{ id }: Manifest,
{ outboundProxy, interfaceInfo }: InstalledPackageInfo,
) {
this.proxyService.presentModalSetOutboundProxy({
outboundProxy,
packageId: id,
hasP2P: Object.values(interfaceInfo).some(i => i.type === 'p2p'),
})
}
}

View File

@@ -16,7 +16,6 @@ import { filter, switchMap, timer } from 'rxjs'
import { FormComponent } from 'src/app/apps/portal/components/form.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
Action,
DataModel,
PackageDataEntry,
PackageState,
@@ -26,10 +25,13 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { ServiceActionComponent } from '../components/action.component'
import { ServiceActionSuccessComponent } from '../components/action-success.component'
import { GroupActionsPipe } from '../pipes/group-actions.pipe'
import { ToManifestPipe } from 'src/app/apps/portal/pipes/to-manifest'
import { ActionMetadata } from '@start9labs/start-sdk/cjs/sdk/lib/types'
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
@Component({
template: `
<ng-container *ngIf="pkg$ | async as pkg">
@if (pkg$ | async; as pkg) {
<section>
<h3 class="g-title">Standard Actions</h3>
<button
@@ -40,7 +42,7 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
</section>
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
<h3 *ngIf="actionGroups.length" class="g-title">
Actions for {{ pkg.manifest.title }}
Actions for {{ (pkg | toManifest).title }}
</h3>
<div *ngFor="let group of actionGroups">
<button
@@ -55,18 +57,18 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
></button>
</div>
</ng-container>
</ng-container>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, ServiceActionComponent, GroupActionsPipe],
imports: [CommonModule, ServiceActionComponent, GroupActionsPipe, ToManifestPipe],
})
export class ServiceActionsRoute {
private readonly id = getPkgId(inject(ActivatedRoute))
readonly pkg$ = this.patch
.watch$('package-data', this.id)
.pipe(filter(pkg => pkg.state === PackageState.Installed))
.watch$('packageData', this.id)
.pipe(filter(pkg => pkg.stateInfo.state === PackageState.Installed))
readonly action = {
icon: 'tuiIconTrash2Large',
@@ -85,7 +87,7 @@ export class ServiceActionsRoute {
private readonly formDialog: FormDialogService,
) {}
async handleAction(action: WithId<Action>) {
async handleAction(action: WithId<ActionMetadata>) {
if (action.disabled) {
this.dialogs
.open(action.disabled, {
@@ -94,11 +96,11 @@ export class ServiceActionsRoute {
})
.subscribe()
} else {
if (action['input-spec'] && !isEmptyObject(action['input-spec'])) {
if (action.input && !isEmptyObject(action.input)) {
this.formDialog.open(FormComponent, {
label: action.name,
data: {
spec: action['input-spec'],
spec: action.input,
buttons: [
{
text: 'Execute',
@@ -128,13 +130,13 @@ export class ServiceActionsRoute {
}
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
const { title, alerts } = pkg.manifest
const { title, alerts, id } = getManifest(pkg)
let content =
alerts.uninstall ||
`Uninstalling ${title} will permanently delete its data`
if (hasCurrentDeps(pkg)) {
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
}
@@ -177,7 +179,7 @@ export class ServiceActionsRoute {
try {
const data = await this.embassyApi.executePackageAction({
id: this.id,
'action-id': actionId,
actionId,
input,
})

View File

@@ -3,21 +3,22 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { InterfacesComponent } from 'src/app/apps/portal/components/interfaces/interfaces.component'
import { map } from 'rxjs'
import { InterfaceComponent } from 'src/app/apps/portal/components/interfaces/interface.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getAddresses } from '../../../components/interfaces/interface.utils'
@Component({
template: `
<app-interfaces
<app-interface
*ngIf="interfaceInfo$ | async as interfaceInfo"
[packageContext]="context"
[addressInfo]="interfaceInfo.addressInfo"
[isUi]="interfaceInfo.type === 'ui'"
[serviceInterface]="interfaceInfo"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, InterfacesComponent],
imports: [CommonModule, InterfaceComponent],
})
export class ServiceInterfaceRoute {
private readonly route = inject(ActivatedRoute)
@@ -27,11 +28,17 @@ export class ServiceInterfaceRoute {
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
}
readonly interfaceInfo$ = inject(PatchDB<DataModel>).watch$(
'package-data',
this.context.packageId,
'state-info',
'interfaceInfo',
this.context.interfaceId,
)
readonly interfaceInfo$ = inject(PatchDB<DataModel>)
.watch$(
'packageData',
this.context.packageId,
'serviceInterfaces',
this.context.interfaceId,
)
.pipe(
map(info => ({
...info,
addresses: getAddresses(info),
})),
)
}

View File

@@ -24,7 +24,7 @@ export class ServiceOutletComponent {
map(() => this.route.firstChild?.snapshot.paramMap?.get('pkgId')),
filter(Boolean),
distinctUntilChanged(),
switchMap(id => this.patch.watch$('package-data', id)),
switchMap(id => this.patch.watch$('packageData', id)),
tap(pkg => {
// if package disappears, navigate to list page
if (!pkg) {

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
import { Manifest } from '@start9labs/marketplace'
import { getPkgId, isEmptyObject } from '@start9labs/shared'
import { isEmptyObject } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, switchMap } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
@@ -15,15 +15,12 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import {
DataModel,
HealthCheckResult,
InstalledPackageInfo,
MainStatus,
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import {
PackageStatus,
PrimaryRendering,
PrimaryStatus,
renderPkgStatus,
StatusRendering,
} from 'src/app/services/pkg-status-rendering.service'
@@ -32,7 +29,7 @@ import { ServiceActionsComponent } from '../components/actions.component'
import { ServiceAdditionalComponent } from '../components/additional.component'
import { ServiceDependenciesComponent } from '../components/dependencies.component'
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
import { ServiceInterfacesComponent } from '../components/interfaces.component'
import { ServiceInterfaceListComponent } from '../components/interface-list.component'
import { ServiceMenuComponent } from '../components/menu.component'
import { ServiceProgressComponent } from '../components/progress.component'
import { ServiceStatusComponent } from '../components/status.component'
@@ -40,43 +37,60 @@ import {
PackageConfigData,
ServiceConfigModal,
} from 'src/app/apps/portal/modals/config.component'
import { ProgressDataPipe } from '../pipes/progress-data.pipe'
import { DependencyInfo } from '../types/dependency-info'
const STATES = [
PackageState.Installing,
PackageState.Updating,
PackageState.Restoring,
]
import { getManifest } from 'src/app/util/get-package-data'
import { InstallingProgressPipe } from 'src/app/apps/portal/routes/service/pipes/install-progress.pipe'
@Component({
template: `
@if (service$ | async; as service) {
@if (showProgress(service.pkg)) {
@if (service.pkg | progressData; as progress) {
<p [progress]="progress.downloadProgress">Downloading</p>
<p [progress]="progress.validateProgress">Validating</p>
<p [progress]="progress.unpackProgress">Unpacking</p>
}
<h3 class="g-title">Status</h3>
<service-status
[connected]="!!(connected$ | async)"
[installingInfo]="service.pkg.stateInfo.installingInfo"
[rendering]="getRendering(service.status)"
[sigtermTimeout]="
service.pkg.status.main.status === 'stopping'
? service.pkg.status.main.timeout
: null
"
/>
@if (
service.pkg.stateInfo.state === 'installing' ||
service.pkg.stateInfo.state === 'updating' ||
service.pkg.stateInfo.state === 'restoring'
) {
<p
*ngFor="
let phase of service.pkg.stateInfo.installingInfo.progress.phases
"
[progress]="phase.progress"
>
{{ phase.name }}
</p>
} @else {
<h3 class="g-title">Status</h3>
<service-status
[connected]="!!(connected$ | async)"
[installProgress]="service.pkg['install-progress']"
[rendering]="$any(getRendering(service.status))"
/>
@if (
service.pkg.stateInfo.state === 'installed' &&
service.status.primary !== 'backing-up'
) {
@if (connected$ | async) {
<service-actions
[pkg]="service.pkg"
[dependencies]="service.dependencies"
/>
}
@if (isInstalled(service.pkg) && (connected$ | async)) {
<service-actions
[service]="service.pkg"
[dependencies]="service.dependencies"
<service-interface-list
[pkg]="service.pkg"
[status]="service.status"
]
/>
}
@if (isInstalled(service.pkg) && !isBackingUp(service.status)) {
<service-interfaces [service]="service" />
@if (isRunning(service.status) && (health$ | async); as checks) {
@if (
service.status.primary === 'running' && (health$ | async);
as checks
) {
<service-health-checks [checks]="checks" />
}
@@ -84,8 +98,8 @@ const STATES = [
<service-dependencies [dependencies]="service.dependencies" />
}
<service-menu [service]="service.pkg" />
<service-additional [service]="service.pkg" />
<service-menu [pkg]="service.pkg" />
<service-additional [pkg]="service.pkg" />
}
}
}
@@ -94,17 +108,15 @@ const STATES = [
standalone: true,
imports: [
CommonModule,
ServiceProgressComponent,
ServiceStatusComponent,
ServiceActionsComponent,
ServiceInterfacesComponent,
ServiceInterfaceListComponent,
ServiceHealthChecksComponent,
ServiceDependenciesComponent,
ServiceMenuComponent,
ServiceAdditionalComponent,
ProgressDataPipe,
InstallingProgressPipe,
],
})
export class ServiceRoute {
@@ -115,12 +127,12 @@ export class ServiceRoute {
private readonly depErrorService = inject(DepErrorService)
private readonly router = inject(Router)
private readonly formDialog = inject(FormDialogService)
readonly connected$ = inject(ConnectionService).connected$
readonly service$ = this.pkgId$.pipe(
switchMap(pkgId =>
combineLatest([
this.patch.watch$('package-data', pkgId),
this.patch.watch$('packageData', pkgId),
this.depErrorService.getPkgDepErrors$(pkgId),
]),
),
@@ -132,9 +144,10 @@ export class ServiceRoute {
}
}),
)
readonly health$ = this.pkgId$.pipe(
switchMap(pkgId =>
this.patch.watch$('package-data', pkgId, 'installed', 'status', 'main'),
this.patch.watch$('packageData', pkgId, 'status', 'main'),
),
map(toHealthCheck),
)
@@ -143,53 +156,35 @@ export class ServiceRoute {
return PrimaryRendering[primary]
}
isInstalled({ state }: PackageDataEntry): boolean {
return state === PackageState.Installed
}
isRunning({ primary }: PackageStatus): boolean {
return primary === PrimaryStatus.Running
}
isBackingUp({ primary }: PackageStatus): boolean {
return primary === PrimaryStatus.BackingUp
}
showProgress({ state }: PackageDataEntry): boolean {
return STATES.includes(state)
}
private getDepInfo(
{ installed, manifest }: PackageDataEntry,
pkg: PackageDataEntry,
depErrors: PkgDependencyErrors,
): DependencyInfo[] {
return installed
? Object.keys(installed['current-dependencies'])
.filter(depId => !!manifest.dependencies[depId])
.map(depId =>
this.getDepValues(installed, manifest, depId, depErrors),
)
: []
const manifest = getManifest(pkg)
return Object.keys(pkg.currentDependencies)
.filter(id => !!manifest.dependencies[id])
.map(id => this.getDepValues(pkg, manifest, id, depErrors))
}
private getDepValues(
pkgInstalled: InstalledPackageInfo,
pkg: PackageDataEntry,
pkgManifest: Manifest,
depId: string,
depErrors: PkgDependencyErrors,
): DependencyInfo {
const { errorText, fixText, fixAction } = this.getDepErrors(
pkgInstalled,
pkg,
pkgManifest,
depId,
depErrors,
)
const depInfo = pkgInstalled['dependency-info'][depId]
const depInfo = pkg.dependencyInfo[depId]
return {
id: depId,
version: pkgManifest.dependencies[depId].version, // do we want this version range?
version: pkg.currentDependencies[depId].versionRange,
title: depInfo?.title || depId,
icon: depInfo?.icon || '',
errorText: errorText
@@ -205,12 +200,12 @@ export class ServiceRoute {
}
private getDepErrors(
pkgInstalled: InstalledPackageInfo,
pkg: PackageDataEntry,
pkgManifest: Manifest,
depId: string,
depErrors: PkgDependencyErrors,
) {
const depError = (depErrors[pkgManifest.id] as any)?.[depId] // @TODO fix
const depError = depErrors[pkgManifest.id]
let errorText: string | null = null
let fixText: string | null = null
@@ -220,18 +215,15 @@ export class ServiceRoute {
if (depError.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed'
fixText = 'Install'
fixAction = () =>
this.fixDep(pkgInstalled, pkgManifest, 'install', depId)
fixAction = () => this.fixDep(pkg, pkgManifest, 'install', depId)
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version'
fixText = 'Update'
fixAction = () =>
this.fixDep(pkgInstalled, pkgManifest, 'update', depId)
fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId)
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied'
fixText = 'Auto config'
fixAction = () =>
this.fixDep(pkgInstalled, pkgManifest, 'configure', depId)
fixAction = () => this.fixDep(pkg, pkgManifest, 'configure', depId)
} else if (depError.type === DependencyErrorType.NotRunning) {
errorText = 'Not running'
fixText = 'Start'
@@ -250,7 +242,7 @@ export class ServiceRoute {
}
async fixDep(
pkgInstalled: InstalledPackageInfo,
pkg: PackageDataEntry,
pkgManifest: Manifest,
action: 'install' | 'update' | 'configure',
depId: string,
@@ -258,10 +250,10 @@ export class ServiceRoute {
switch (action) {
case 'install':
case 'update':
return this.installDep(pkgManifest, depId)
return this.installDep(pkg, pkgManifest, depId)
case 'configure':
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
label: `${pkgInstalled!['dependency-info'][depId].title} config`,
label: `${pkg.dependencyInfo[depId].title} config`,
data: {
pkgId: depId,
dependentInfo: pkgManifest,
@@ -270,13 +262,15 @@ export class ServiceRoute {
}
}
private async installDep(manifest: Manifest, depId: string): Promise<void> {
const version = manifest.dependencies[depId].version
private async installDep(
pkg: PackageDataEntry,
manifest: Manifest,
depId: string,
): Promise<void> {
const dependentInfo: DependentInfo = {
id: manifest.id,
title: manifest.title,
version,
version: pkg.currentDependencies[depId].versionRange,
}
const navigationExtras: NavigationExtras = {
state: { dependentInfo },

View File

@@ -62,8 +62,8 @@ export class BackupsStatusComponent {
private get hasBackup(): boolean {
return (
!!this.target['embassy-os'] &&
this.emver.compare(this.target['embassy-os'].version, '0.3.0') !== -1
!!this.target.startOs &&
this.emver.compare(this.target.startOs.version, '0.3.0') !== -1
)
}
}

View File

@@ -30,7 +30,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
Running
</span>
<ng-template #notRunning>
{{ job.next | date : 'MMM d, y, h:mm a' }}
{{ job.next | date: 'MMM d, y, h:mm a' }}
</ng-template>
</td>
<td>{{ job.name }}</td>
@@ -38,7 +38,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
{{ job.target.name }}
</td>
<td>Packages: {{ job['package-ids'].length }}</td>
<td>Packages: {{ job.packageIds.length }}</td>
</tr>
<ng-template #blank>
<tr><td colspan="5">You have no active or upcoming backup jobs</td></tr>
@@ -56,7 +56,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
})
export class BackupsUpcomingComponent {
readonly current$ = inject(PatchDB<DataModel>)
.watch$('server-info', 'status-info', 'current-backup', 'job')
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
.pipe(map(job => job || {}))
readonly upcoming$ = from(inject(ApiService).getBackupJobs({})).pipe(

View File

@@ -17,6 +17,7 @@ import {
import { PatchDB } from 'patch-db-client'
import { firstValueFrom, map } from 'rxjs'
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data'
interface Package {
id: string
@@ -90,16 +91,19 @@ export class BackupsBackupModal {
async ngOnInit() {
this.pkgs = await firstValueFrom(
this.patch.watch$('package-data').pipe(
this.patch.watch$('packageData').pipe(
map(pkgs =>
Object.values(pkgs)
.map(({ manifest: { id, title }, icon, state }) => ({
id,
title,
icon,
disabled: state !== PackageState.Installed,
checked: false,
}))
.map(pkg => {
const { id, title } = getManifest(pkg)
return {
id,
title,
icon: pkg.icon,
disabled: pkg.stateInfo.state !== PackageState.Installed,
checked: false,
}
})
.sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
),

View File

@@ -53,10 +53,8 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
(click)="selectPackages()"
>
Packages
<tui-badge
[appearance]="job['package-ids'].length ? 'success' : 'warning'"
>
{{ job['package-ids'].length + ' selected' }}
<tui-badge [appearance]="job.packageIds.length ? 'success' : 'warning'">
{{ job.packageIds.length + ' selected' }}
</tui-badge>
</button>
<tui-input name="cron" [(ngModel)]="job.cron">
@@ -145,7 +143,7 @@ export class BackupsEditModal {
selectPackages() {
this.dialogs.open<string[]>(BACKUP, BACKUP_OPTIONS).subscribe(id => {
this.job['package-ids'] = id
this.job.packageIds = id
})
}
}

View File

@@ -62,10 +62,8 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
[style.background]="selected[index] ? 'var(--tui-clear)' : ''"
>
<td><tui-checkbox [(ngModel)]="selected[index]"></tui-checkbox></td>
<td>{{ run['started-at'] | date : 'medium' }}</td>
<td>
{{ run['started-at'] | duration : run['completed-at'] }} Minutes
</td>
<td>{{ run.startedAt | date: 'medium' }}</td>
<td>{{ run.startedAt | duration: run.completedAt }} Minutes</td>
<td>
<tui-svg
*ngIf="run.report | hasError; else noError"
@@ -178,7 +176,7 @@ export class BackupsHistoryModal {
label: 'Backup Report',
data: {
report: run.report,
timestamp: run['completed-at'],
timestamp: run.completedAt,
},
})
.subscribe()

View File

@@ -22,9 +22,9 @@ import { EDIT } from './edit.component'
@Component({
template: `
<tui-notification>
Scheduling automatic backups is an excellent way to ensure your Embassy
data is safely backed up. Your Embassy will issue a notification whenever
one of your scheduled backups succeeds or fails.
Scheduling automatic backups is an excellent way to ensure your StartOS
data is safely backed up. StartOS will issue a notification whenever one
of your scheduled backups succeeds or fails.
<a
href="https://docs.start9.com/latest/user-manual/backups/backup-jobs"
target="_blank"
@@ -57,7 +57,7 @@ import { EDIT } from './edit.component'
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
{{ job.target.name }}
</td>
<td>Packages: {{ job['package-ids'].length }}</td>
<td>Packages: {{ job.packageIds.length }}</td>
<td>{{ (job.cron | toHumanCron).message }}</td>
<td>
<button
@@ -143,7 +143,7 @@ export class BackupsJobsModal implements OnInit {
data.name = job.name
data.target = job.target
data.cron = job.cron
data['package-ids'] = job['package-ids']
data.packageIds = job.packageIds
})
}

View File

@@ -21,19 +21,19 @@ import { TuiMapperPipeModule } from '@taiga-ui/cdk'
@Component({
template: `
<ng-container *ngIf="packageData$ | toOptions : backups | async as options">
<ng-container *ngIf="packageData$ | toOptions: backups | async as options">
<div tuiGroup orientation="vertical" [style.width.%]="100">
<tui-checkbox-block
*ngFor="let option of options"
[disabled]="option.installed || option['newer-eos']"
[disabled]="option.installed || option.newerStartOs"
[(ngModel)]="option.checked"
>
<div [style.margin]="'0.75rem 0'">
<strong>{{ option.title }}</strong>
<div>Version {{ option.version }}</div>
<div>Backup made: {{ option.timestamp | date : 'medium' }}</div>
<div>Backup made: {{ option.timestamp | date: 'medium' }}</div>
<div
*ngIf="option | tuiMapper : toMessage as message"
*ngIf="option | tuiMapper: toMessage as message"
[style.color]="message.color"
>
{{ message.text }}
@@ -73,11 +73,11 @@ export class BackupsRecoverModal {
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
readonly packageData$ = inject(PatchDB<DataModel>)
.watch$('package-data')
.watch$('packageData')
.pipe(take(1))
readonly toMessage = (option: RecoverOption) => {
if (option['newer-eos']) {
if (option.newerStartOs) {
return {
text: `Unavailable. Backup was made on a newer version of StartOS.`,
color: 'var(--tui-error-fill)',
@@ -98,7 +98,7 @@ export class BackupsRecoverModal {
}
get backups(): Record<string, PackageBackupInfo> {
return this.context.data.backupInfo['package-backups']
return this.context.data.backupInfo.packageBackups
}
isDisabled(options: RecoverOption[]): boolean {
@@ -109,11 +109,13 @@ export class BackupsRecoverModal {
const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
const loader = this.loader.open('Initializing...').subscribe()
const { targetId, password } = this.context.data
try {
await this.api.restorePackages({
ids,
'target-id': this.context.data.targetId,
password: this.context.data.password,
targetId,
password,
})
this.context.$implicit.complete()

View File

@@ -102,7 +102,7 @@ export class BackupsTargetModal {
isDisabled(target: BackupTarget): boolean {
return (
!target.mountable ||
(this.context.data.type === 'restore' && !target['embassy-os'])
(this.context.data.type === 'restore' && !target.startOs)
)
}

View File

@@ -4,7 +4,7 @@ import { ErrorService, LoadingService } from '@start9labs/shared'
import {
unionSelectKey,
unionValueKey,
} from '@start9labs/start-sdk/lib/config/configTypes'
} from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { TuiNotificationModule } from '@taiga-ui/core'
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
@@ -55,7 +55,7 @@ import { BackupsTargetsComponent } from '../components/targets.component'
<div class="g-hidden-scrollbar" tuiFade>
<table
class="g-table"
[backupsPhysical]="targets?.['unknown-disks'] || null"
[backupsPhysical]="targets?.unknownDisks || null"
(add)="addPhysical($event)"
></table>
</div>
@@ -106,7 +106,7 @@ export class BackupsTargetsModal implements OnInit {
this.targets = await this.api.getBackupTargets({})
} catch (e: any) {
this.errorService.handleError(e)
this.targets = { 'unknown-disks': [], saved: [] }
this.targets = { unknownDisks: [], saved: [] }
} finally {
this.loading$.next(false)
}
@@ -162,7 +162,7 @@ export class BackupsTargetsModal implements OnInit {
}).then(response => {
this.setTargets(
this.targets?.saved.concat(response),
this.targets?.['unknown-disks'].filter(a => a !== disk),
this.targets?.unknownDisks.filter(a => a !== disk),
)
return true
}),
@@ -225,9 +225,9 @@ export class BackupsTargetsModal implements OnInit {
private setTargets(
saved: BackupTarget[] = this.targets?.saved || [],
unknown: UnknownDisk[] = this.targets?.['unknown-disks'] || [],
unknownDisks: UnknownDisk[] = this.targets?.unknownDisks || [],
) {
this.targets = { ['unknown-disks']: unknown, saved }
this.targets = { unknownDisks, saved }
}
private async getSpec(target: BackupTarget) {

View File

@@ -26,7 +26,7 @@ export class ToOptionsPipe implements PipeTransform {
id,
installed: !!packageData[id],
checked: false,
'newer-eos': this.compare(packageBackups[id].osVersion),
newerStartOs: this.compare(packageBackups[id].osVersion),
}))
.sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,

View File

@@ -35,7 +35,7 @@ export class BackupsCreateService {
const loader = this.loader.open('Beginning backup...').subscribe()
await this.api
.createBackup({ 'target-id': targetId, 'package-ids': pkgIds })
.createBackup({ targetId, packageIds: pkgIds })
.finally(() => loader.unsubscribe())
}
}

View File

@@ -44,7 +44,7 @@ export class BackupsRestoreService {
this.getRecoverData(
target.id,
password,
target['embassy-os']?.['password-hash'] || '',
target.startOs?.passwordHash || '',
),
),
take(1),
@@ -73,7 +73,7 @@ export class BackupsRestoreService {
const loader = this.loader.open('Decrypting drive...').subscribe()
return this.api
.getBackupInfo({ 'target-id': targetId, password })
.getBackupInfo({ targetId, password })
.finally(() => loader.unsubscribe())
}),
catchError(e => {

View File

@@ -1,7 +1,7 @@
import {
unionSelectKey,
unionValueKey,
} from '@start9labs/start-sdk/lib/config/configTypes'
} from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { RR } from 'src/app/services/api/api.types'
export type BackupConfig =

View File

@@ -4,5 +4,5 @@ export interface RecoverOption extends PackageBackupInfo {
id: string
checked: boolean
installed: boolean
'newer-eos': boolean
newerStartOs: boolean
}

View File

@@ -1,6 +1,6 @@
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/variants'
export const dropboxSpec = Config.of({
name: Value.text({

View File

@@ -4,7 +4,7 @@ export class BackupJobBuilder {
name: string
target: BackupTarget
cron: string
'package-ids': string[]
packageIds: string[]
now = false
constructor(readonly job: Partial<BackupJob>) {
@@ -12,7 +12,7 @@ export class BackupJobBuilder {
this.name = name || ''
this.target = target || ({} as BackupTarget)
this.cron = cron || '0 2 * * *'
this['package-ids'] = job['package-ids'] || []
this.packageIds = job.packageIds || []
}
buildCreate(): RR.CreateBackupJobReq {
@@ -20,9 +20,9 @@ export class BackupJobBuilder {
return {
name,
'target-id': target.id,
targetId: target.id,
cron,
'package-ids': this['package-ids'],
packageIds: this.packageIds,
now,
}
}
@@ -33,9 +33,9 @@ export class BackupJobBuilder {
return {
id,
name,
'target-id': target.id,
targetId: target.id,
cron,
'package-ids': this['package-ids'],
packageIds: this.packageIds,
}
}
}

View File

@@ -29,9 +29,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 { getAllPackages } from 'src/app/util/get-package-data'
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
import { dryUpdate } from 'src/app/util/dry-update'
import { MarketplaceAlertsService } from '../services/alerts.service'
import { ToManifestPipe } from 'src/app/apps/portal/pipes/to-manifest'
@Component({
selector: 'marketplace-controls',
@@ -45,8 +46,11 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
>
View Installed
</button>
@if (installed) {
@switch (localVersion | compareEmver: pkg.manifest.version) {
@if (
localPkg.stateInfo.state === 'installed' && (localPkg | toManifest);
as localManifest
) {
@switch (localManifest.version | compareEmver: pkg.manifest.version) {
@case (1) {
<button
tuiButton
@@ -94,7 +98,7 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, EmverPipesModule, TuiButtonModule],
imports: [CommonModule, EmverPipesModule, TuiButtonModule, ToManifestPipe],
})
export class MarketplaceControlsComponent {
private readonly alerts = inject(MarketplaceAlertsService)
@@ -118,18 +122,10 @@ export class MarketplaceControlsComponent {
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
get installed(): boolean {
return this.localPkg?.state === PackageState.Installed
}
get localVersion(): string {
return this.localPkg?.manifest.version || ''
}
async tryInstall() {
const current = await firstValueFrom(this.marketplace.getSelectedHost$())
const url = this.url || current.url
const originalUrl = this.localPkg?.installed?.['marketplace-url'] || ''
const originalUrl = this.localPkg?.marketplaceUrl || ''
if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) this.install(url)
@@ -144,9 +140,11 @@ export class MarketplaceControlsComponent {
return
}
const localManifest = getManifest(this.localPkg)
if (
hasCurrentDeps(this.localPkg) &&
this.emver.compare(this.localVersion, this.pkg.manifest.version) !== 0
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
this.emver.compare(localManifest.version, this.pkg.manifest.version) !== 0
) {
this.dryInstall(url)
} else {

View File

@@ -1,5 +1,4 @@
import { Component, inject, Input } from '@angular/core'
import { NgIf } from '@angular/common'
import { TuiNotificationModule } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service'

View File

@@ -92,7 +92,7 @@ export class MarketplaceRegistryModal {
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
'ui',
'marketplace',
'known-hosts',
'knownHosts',
)
readonly stores$ = combineLatest([

View File

@@ -12,6 +12,6 @@ export class ToLocalPipe implements PipeTransform {
private readonly patch = inject(PatchDB<DataModel>)
transform(id: string): Observable<PackageDataEntry> {
return this.patch.watch$('package-data', id).pipe(filter(Boolean))
return this.patch.watch$('packageData', id).pipe(filter(Boolean))
}
}

View File

@@ -18,8 +18,8 @@ export class MarketplaceAlertsService {
async alertMarketplace(url: string, originalUrl: string): Promise<boolean> {
const marketplaces = await firstValueFrom(this.marketplace$)
const name = marketplaces['known-hosts'][url]?.name || url
const source = marketplaces['known-hosts'][originalUrl]?.name || originalUrl
const name = marketplaces.knownHosts[url]?.name || url
const source = marketplaces.knownHosts[originalUrl]?.name || originalUrl
const message = source ? `installed from ${source}` : 'side loaded'
return new Promise(async resolve => {

View File

@@ -1,4 +1,4 @@
import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes'
import { ValueSpecObject } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiPromptData } from '@taiga-ui/kit'

View File

@@ -21,7 +21,7 @@ import { toRouterLink } from '../../../utils/to-router-link'
selector: '[notificationItem]',
template: `
<td><ng-content /></td>
<td>{{ notificationItem['created-at'] | date : 'MMM d, y, h:mm a' }}</td>
<td>{{ notificationItem.createdAt | date: 'MMM d, y, h:mm a' }}</td>
<td [style.color]="color">
<tui-svg [style.color]="color" [src]="icon"></tui-svg>
{{ notificationItem.title }}
@@ -70,8 +70,9 @@ export class NotificationItemComponent {
get manifest$(): Observable<Manifest> {
return this.patch
.watch$(
'package-data',
this.notificationItem['package-id'] || '',
'packageData',
this.notificationItem.packageId || '',
'stateInfo',
'manifest',
)
.pipe(first())

View File

@@ -14,12 +14,12 @@ import { SettingsUpdateComponent } from './update.component'
selector: 'settings-menu',
template: `
<ng-container *ngIf="server$ | async as server; else loading">
<settings-sync *ngIf="!server['ntp-synced']" />
<settings-sync *ngIf="!server.ntpSynced" />
<section *ngFor="let cat of service.settings | keyvalue: asIsOrder">
<h3 class="g-title" (click)="addClick(cat.key)">{{ cat.key }}</h3>
<settings-update
*ngIf="cat.key === 'General'"
[updated]="server['status-info'].updated"
[updated]="server.statusInfo.updated"
/>
<ng-container *ngFor="let btn of cat.value">
<settings-button [button]="btn">
@@ -32,13 +32,7 @@ import { SettingsUpdateComponent } from './update.component'
: 'var(--tui-success-fill)'
"
>
{{
!server.network.outboundProxy
? 'None'
: server.network.outboundProxy === 'primary'
? 'System Primary'
: server.network.outboundProxy.proxyId
}}
{{ server.network.outboundProxy || 'None' }}
</div>
</settings-button>
</ng-container>
@@ -76,7 +70,7 @@ export class SettingsMenuComponent {
private readonly clientStorageService = inject(ClientStorageService)
private readonly alerts = inject(TuiAlertService)
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
readonly service = inject(SettingsService)
manageClicks = 0

View File

@@ -48,7 +48,7 @@ import { EOSService } from 'src/app/services/eos.service'
],
})
export class SettingsUpdateModal {
readonly versions = Object.entries(this.eosService.eos?.['release-notes']!)
readonly versions = Object.entries(this.eosService.eos?.releaseNotes!)
.sort(([a], [b]) => a.localeCompare(b))
.reverse()
.map(([version, notes]) => ({

View File

@@ -1,6 +1,6 @@
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/variants'
import { Proxy } from 'src/app/services/patch-db/data-model'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
@@ -59,38 +59,18 @@ function getStrategyUnion(proxies: Proxy[]) {
proxy: {
name: 'Proxy',
spec: Config.of({
proxyStrategy: Value.union(
{
name: 'Proxy Strategy',
required: { default: 'primary' },
description: `<h5>Primary</h5>Use the <i>Primary Inbound</i> proxy from your proxy settings. If you do not have any inbound proxies, no proxy will be used
<h5>Other</h5>Use a specific proxy from your proxy settings
`,
},
Variants.of({
primary: {
name: 'Primary',
spec: Config.of({}),
},
other: {
name: 'Specific',
spec: Config.of({
proxyId: Value.select({
name: 'Select Proxy',
required: { default: null },
values: inboundProxies,
}),
}),
},
}),
),
proxyId: Value.select({
name: 'Select Proxy',
required: { default: null },
values: inboundProxies,
}),
}),
},
}),
)
}
export async function getStart9ToSpec(proxies: Proxy[]) {
export function getStart9ToSpec(proxies: Proxy[]) {
return configBuilderToSpec(
Config.of({
strategy: getStrategyUnion(proxies),
@@ -98,7 +78,7 @@ export async function getStart9ToSpec(proxies: Proxy[]) {
)
}
export async function getCustomSpec(proxies: Proxy[]) {
export function getCustomSpec(proxies: Proxy[]) {
return configBuilderToSpec(
Config.of({
hostname: Value.text({

View File

@@ -69,7 +69,7 @@ export class SettingsDomainsComponent {
private readonly api = inject(ApiService)
private readonly dialogs = inject(TuiDialogService)
readonly domains$ = this.patch.watch$('server-info', 'network').pipe(
readonly domains$ = this.patch.watch$('serverInfo', 'network').pipe(
map(network => {
const start9ToSubdomain = network.start9ToSubdomain
const start9To = !start9ToSubdomain
@@ -103,7 +103,7 @@ export class SettingsDomainsComponent {
async add() {
const proxies = await firstValueFrom(
this.patch.watch$('server-info', 'network', 'proxies'),
this.patch.watch$('serverInfo', 'network', 'proxies'),
)
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
@@ -128,7 +128,7 @@ export class SettingsDomainsComponent {
async claim() {
const proxies = await firstValueFrom(
this.patch.watch$('server-info', 'network', 'proxies'),
this.patch.watch$('serverInfo', 'network', 'proxies'),
)
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
@@ -150,13 +150,11 @@ export class SettingsDomainsComponent {
this.formDialog.open(FormComponent, options)
}
// @TODO figure out how to get types here
private getNetworkStrategy(strategy: any) {
const { ipStrategy, proxyStrategy = {} } = strategy.unionValueKey
const { unionSelectKey, unionValueKey = {} } = proxyStrategy
const proxyId = unionSelectKey === 'primary' ? null : unionValueKey.proxyId
return strategy.unionSelectKey === 'local' ? { ipStrategy } : { proxyId }
return strategy.unionSelectKey === 'local'
? { ipStrategy: strategy.unionValueKey.ipStrategy }
: { proxy: strategy.unionValueKey.proxyId }
}
private async deleteDomain(hostname?: string) {
@@ -174,7 +172,7 @@ export class SettingsDomainsComponent {
loader.unsubscribe()
}
}
// @TODO figure out how to get types here
private async claimDomain({ strategy }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe()
const networkStrategy = this.getNetworkStrategy(strategy)
@@ -189,7 +187,7 @@ export class SettingsDomainsComponent {
loader.unsubscribe()
}
}
// @TODO figure out how to get types here
private async save({ provider, strategy, hostname }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe()
const name = provider.unionSelectKey

View File

@@ -9,8 +9,6 @@ import { TuiDialogService } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiInputModule } from '@taiga-ui/kit'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
import { PatchDB } from 'patch-db-client'
import { switchMap } from 'rxjs'
import { FormModule } from 'src/app/common/form/form.module'
@@ -19,6 +17,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormService } from 'src/app/services/form.service'
import { EmailInfoComponent } from './info.component'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { customSmtp } from '@start9labs/start-sdk/cjs/sdk/lib/config/configConstants'
@Component({
template: `
@@ -84,7 +84,7 @@ export class SettingsEmailComponent {
testAddress = ''
readonly spec: Promise<InputSpec> = configBuilderToSpec(customSmtp)
readonly form$ = this.patch
.watch$('server-info', 'smtp')
.watch$('serverInfo', 'smtp')
.pipe(
switchMap(async value =>
this.formService.createForm(await this.spec, value),

View File

@@ -93,37 +93,6 @@ export class SettingsExperimentalComponent {
.subscribe(() => this.resetTor(this.wipe))
}
zram(enabled: boolean) {
this.dialogs
.open(TUI_PROMPT, {
label: 'Confirm',
data: {
content: enabled
? 'Are you sure you want to disable zram? It provides significant performance benefits on low RAM devices.'
: 'Enable zram? It will only make a difference on lower RAM devices.',
yes: enabled ? 'Disable' : 'Enable',
no: 'Cancel',
},
})
.pipe(filter(Boolean))
.subscribe(() => this.toggleZram(enabled))
}
private async toggleZram(enabled: boolean) {
const loader = this.loader
.open(enabled ? 'Disabling zram...' : 'Enabling zram...')
.subscribe()
try {
await this.api.toggleZram({ enable: !enabled })
this.alerts.open(`Zram ${enabled ? 'disabled' : 'enabled'}`).subscribe()
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private async resetTor(wipeState: boolean) {
const loader = this.loader.open('Resetting Tor...').subscribe()

View File

@@ -1,22 +0,0 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { InterfacesComponent } from 'src/app/apps/portal/components/interfaces/interfaces.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
template: `
<app-interfaces
*ngIf="ui$ | async as ui"
[style.max-width.rem]="50"
[addressInfo]="ui"
[isUi]="true"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, InterfacesComponent],
})
export class SettingsInterfacesComponent {
readonly ui$ = inject(PatchDB<DataModel>).watch$('server-info', 'ui')
}

View File

@@ -0,0 +1,70 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client'
import { Observable, map } from 'rxjs'
import {
InterfaceComponent,
ServiceInterfaceWithAddresses,
} from 'src/app/apps/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/apps/portal/components/interfaces/interface.utils'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
template: `
<app-interface
*ngIf="ui$ | async as ui"
[style.max-width.rem]="50"
[serviceInterface]="ui"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, InterfaceComponent],
})
export class StartOsUiComponent {
readonly ui$: Observable<ServiceInterfaceWithAddresses> = inject(
PatchDB<DataModel>,
)
.watch$('serverInfo', 'ui')
.pipe(
map(hosts => {
const serviceInterface: T.ServiceInterfaceWithHostInfo = {
id: 'startos-ui',
name: 'StartOS UI',
description: 'The primary web user interface for StartOS',
type: 'ui',
hasPrimary: false,
disabled: false,
masked: false,
addressInfo: {
hostId: '',
username: null,
suffix: '',
bindOptions: {
scheme: 'http',
preferredExternalPort: 80,
addSsl: {
scheme: 'https',
preferredExternalPort: 443,
addXForwardedHeaders: null,
},
secure: {
ssl: false,
},
},
},
hostInfo: {
id: 'start-os-ui-host',
kind: 'multi',
hostnames: hosts,
},
}
return {
...serviceInterface,
addresses: getAddresses(serviceInterface),
}
}),
)
}

View File

@@ -1,5 +1,5 @@
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiPromptData } from '@taiga-ui/kit'
@@ -27,8 +27,6 @@ export const wireguardSpec = Config.of({
})
export type WireguardSpec = typeof wireguardSpec.validator._TYPE
export type ProxyUpdate = Partial<{
export type ProxyUpdate = {
name: string
primaryInbound: true
primaryOutbound: true
}>
}

View File

@@ -6,7 +6,6 @@ import {
Input,
} from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiDataListModule,
@@ -25,6 +24,7 @@ import { Proxy } from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DELETE_OPTIONS, ProxyUpdate } from './constants'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
@Component({
selector: 'proxies-menu',
@@ -45,23 +45,6 @@ import { DELETE_OPTIONS, ProxyUpdate } from './constants'
</tui-hosted-dropdown>
<ng-template #dropdown>
<tui-data-list>
<button
*ngIf="!proxy.primaryInbound && proxy.type === 'inbound-outbound'"
tuiOption
(click)="update({ primaryInbound: true })"
>
Make Primary Inbound
</button>
<button
*ngIf="
!proxy.primaryOutbound &&
(proxy.type === 'inbound-outbound' || proxy.type === 'outbound')
"
tuiOption
(click)="update({ primaryOutbound: true })"
>
Make Primary Outbound
</button>
<button tuiOption (click)="rename()">Rename</button>
<tui-opt-group>
<button tuiOption (click)="delete()">Delete</button>
@@ -105,20 +88,6 @@ export class ProxiesMenuComponent {
})
}
async update(value: ProxyUpdate): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe()
try {
await this.api.updateProxy(value)
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
async rename() {
const spec = { name: 'Name', required: { default: this.proxy.name } }
const name = await Value.text(spec).build({} as any)
@@ -137,4 +106,18 @@ export class ProxiesMenuComponent {
this.formDialog.open(FormComponent, options)
}
private async update(value: ProxyUpdate): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe()
try {
await this.api.updateProxy(value)
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import { TuiDialogOptions } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { PatchDB } from 'patch-db-client'
import {
@@ -42,7 +42,7 @@ export class SettingsProxiesComponent {
private readonly formDialog = inject(FormDialogService)
readonly proxies$ = inject(PatchDB<DataModel>).watch$(
'server-info',
'serverInfo',
'network',
'proxies',
)

View File

@@ -20,7 +20,6 @@ import { ProxiesMenuComponent } from './menu.component'
<th>Name</th>
<th>Created</th>
<th>Type</th>
<th>Primary</th>
<th>Used By</th>
<th></th>
</tr>
@@ -28,21 +27,8 @@ import { ProxiesMenuComponent } from './menu.component'
<tbody>
<tr *ngFor="let proxy of proxies">
<td>{{ proxy.name }}</td>
<td>{{ proxy.createdAt | date : 'short' }}</td>
<td>{{ proxy.createdAt | date: 'short' }}</td>
<td>{{ proxy.type }}</td>
<td>
<tui-badge
*ngIf="proxy.primaryInbound"
appearance="success"
size="m"
[style.margin-right.rem]="0.25"
>
Inbound
</tui-badge>
<tui-badge *ngIf="proxy.primaryOutbound" appearance="info" size="m">
Outbound
</tui-badge>
</td>
<td>
<button
*ngIf="getLength(proxy); else unused"

View File

@@ -1,14 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core'
import { IpInfo } from 'src/app/services/patch-db/data-model'
import { HostnameInfo } from '@start9labs/start-sdk/cjs/sdk/lib/types'
@Pipe({
standalone: true,
name: 'primaryIp',
})
export class PrimaryIpPipe implements PipeTransform {
transform(ipInfo: IpInfo): string {
return Object.values(ipInfo)
.filter(iface => iface.ipv4)
.sort((a, b) => (a.wireless ? -1 : 1))[0].ipv4!
transform(hostnames: HostnameInfo[]): string {
return (
hostnames.map(
h => h.kind === 'ip' && h.hostname.kind === 'ipv4' && h.hostname.value,
)[0] || ''
)
}
}

View File

@@ -12,7 +12,7 @@ import { RouterPortComponent } from './table.component'
<ng-container *ngIf="server$ | async as server">
<router-info [enabled]="!server.network.wanConfig.upnp" />
<table
*ngIf="server.ui.ipInfo | primaryIp as ip"
*ngIf="server.ui | primaryIp as ip"
tuiTextfieldAppearance="unstyled"
tuiTextfieldSize="m"
[tuiTextfieldLabelOutside]="true"
@@ -65,5 +65,5 @@ import { RouterPortComponent } from './table.component'
],
})
export class SettingsRouterComponent {
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
}

View File

@@ -5,7 +5,6 @@ import {
inject,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { CopyService, ErrorService, LoadingService } from '@start9labs/shared'

View File

@@ -58,8 +58,8 @@ export class SettingsSessionsComponent {
}))
.sort(
(a, b) =>
new Date(b['last-active']).valueOf() -
new Date(a['last-active']).valueOf(),
new Date(b.lastActive).valueOf() -
new Date(a.lastActive).valueOf(),
),
),
),

View File

@@ -49,13 +49,13 @@ import { FormsModule } from '@angular/forms'
[ngModel]="selected$.value.includes(session)"
(ngModelChange)="onToggle(session)"
/>
{{ session['user-agent'] }}
{{ session.userAgent }}
</td>
<td *ngIf="session.metadata.platforms | platformInfo as info">
<tui-icon [icon]="info.icon"></tui-icon>
{{ info.name }}
</td>
<td>{{ session['last-active'] }}</td>
<td>{{ session.lastActive }}</td>
</tr>
<ng-template #loading>
<tr *ngFor="let _ of single ? [''] : ['', '']">

View File

@@ -35,7 +35,7 @@ import { TuiForModule } from '@taiga-ui/cdk'
<tbody>
<tr *ngFor="let key of keys; else: loading">
<td>{{ key.hostname }}</td>
<td>{{ key['created-at'] | date: 'medium' }}</td>
<td>{{ key.createdAt | date: 'medium' }}</td>
<td>{{ key.alg }}</td>
<td>{{ key.fingerprint }}</td>
<td>

View File

@@ -1,4 +1,4 @@
import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes'
import { ValueSpecObject } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { AvailableWifi } from 'src/app/services/api/api.types'
import { RR } from 'src/app/services/api/api.types'
@@ -18,7 +18,7 @@ export interface WifiData {
export function parseWifi(res: RR.GetWifiRes): WifiData {
return {
available: res['available-wifi'],
available: res.availableWifi,
known: Object.entries(res.ssids).map(([ssid, strength]) => ({
ssid,
strength,

Some files were not shown because too many files have changed in this diff Show More