mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
refactor downstream for 036 changes (#2577)
refactor codebase for 036 changes
This commit is contained in:
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
-
|
||||
<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
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
CopyService,
|
||||
copyToClipboard,
|
||||
displayEmver,
|
||||
Emver,
|
||||
MarkdownComponent,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -164,7 +164,7 @@ export class CifsComponent {
|
||||
const target: CifsBackupTarget = {
|
||||
...this.form.getRawValue(),
|
||||
mountable: true,
|
||||
'embassy-os': diskInfo,
|
||||
startOs: diskInfo,
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -98,7 +98,7 @@ export default class RecoverPage {
|
||||
}
|
||||
|
||||
empty(drive: DiskBackupTarget) {
|
||||
return !drive['embassy-os']?.full
|
||||
return !drive.startOs?.full
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {}
|
||||
// }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -169,7 +169,7 @@ export class HeaderComponent {
|
||||
'ui',
|
||||
'gaming',
|
||||
'snake',
|
||||
'high-score',
|
||||
'highScore',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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' }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
14
web/projects/ui/src/app/apps/portal/pipes/to-manifest.ts
Normal file
14
web/projects/ui/src/app/apps/portal/pipes/to-manifest.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -4,5 +4,5 @@ export interface RecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
'newer-eos': boolean
|
||||
newerStartOs: boolean
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export class MarketplaceRegistryModal {
|
||||
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
|
||||
'ui',
|
||||
'marketplace',
|
||||
'known-hosts',
|
||||
'knownHosts',
|
||||
)
|
||||
|
||||
readonly stores$ = combineLatest([
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) => ({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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] || ''
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { CopyService, ErrorService, LoadingService } from '@start9labs/shared'
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 ? [''] : ['', '']">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user