mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
refactor downstream for 036 changes (#2577)
refactor codebase for 036 changes
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
|||||||
DriveComponent,
|
DriveComponent,
|
||||||
LoadingModule,
|
LoadingModule,
|
||||||
RELATIVE_URL,
|
RELATIVE_URL,
|
||||||
UnitConversionPipesModule,
|
|
||||||
WorkspaceConfig,
|
WorkspaceConfig,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { TuiDialogModule, TuiRootModule } from '@taiga-ui/core'
|
import { TuiDialogModule, TuiRootModule } from '@taiga-ui/core'
|
||||||
@@ -42,7 +41,6 @@ const {
|
|||||||
TuiIconModule,
|
TuiIconModule,
|
||||||
TuiSurfaceModule,
|
TuiSurfaceModule,
|
||||||
TuiTitleModule,
|
TuiTitleModule,
|
||||||
UnitConversionPipesModule,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="box-container">
|
<div class="box-container">
|
||||||
<div class="box-container-title">
|
<div class="box-container-title">
|
||||||
<h3 class="small-caps">What's new</h3>
|
<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="small-caps">Latest Release</span>
|
||||||
-
|
-
|
||||||
<span class="box-container-title-date">
|
<span class="box-container-title-date">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<p
|
<p
|
||||||
safeLinks
|
safeLinks
|
||||||
class="box-container-details-notes"
|
class="box-container-details-notes"
|
||||||
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
|
[innerHTML]="pkg.manifest.releaseNotes | markdown | dompurify"
|
||||||
></p>
|
></p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
import {
|
import {
|
||||||
CopyService,
|
CopyService,
|
||||||
copyToClipboard,
|
|
||||||
displayEmver,
|
displayEmver,
|
||||||
Emver,
|
Emver,
|
||||||
MarkdownComponent,
|
MarkdownComponent,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
import { MarketplacePkg } from '../../../types'
|
import { MarketplacePkg } from '../../../types'
|
||||||
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-package-hero',
|
selector: 'marketplace-package-hero',
|
||||||
@@ -11,14 +10,14 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
|
|||||||
<div class="inner-container box-shadow-lg">
|
<div class="inner-container box-shadow-lg">
|
||||||
<!-- icon -->
|
<!-- icon -->
|
||||||
<img
|
<img
|
||||||
[src]="pkg | mimeType | trustUrl"
|
[src]="pkg.icon | trustUrl"
|
||||||
class="box-shadow-lg"
|
class="box-shadow-lg"
|
||||||
alt="{{ pkg.manifest.title }} Icon"
|
alt="{{ pkg.manifest.title }} Icon"
|
||||||
/>
|
/>
|
||||||
<!-- color background -->
|
<!-- color background -->
|
||||||
<div class="color-background">
|
<div class="color-background">
|
||||||
<img
|
<img
|
||||||
[src]="pkg | mimeType | trustUrl"
|
[src]="pkg.icon | trustUrl"
|
||||||
alt="{{ pkg.manifest.title }} background image"
|
alt="{{ pkg.manifest.title }} background image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +143,7 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
|
|||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, SharedPipesModule, MimeTypePipeModule],
|
imports: [CommonModule, SharedPipesModule],
|
||||||
})
|
})
|
||||||
export class MarketplacePackageHeroComponent {
|
export class MarketplacePackageHeroComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export class CifsComponent {
|
|||||||
const target: CifsBackupTarget = {
|
const target: CifsBackupTarget = {
|
||||||
...this.form.getRawValue(),
|
...this.form.getRawValue(),
|
||||||
mountable: true,
|
mountable: true,
|
||||||
'embassy-os': diskInfo,
|
startOs: diskInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogs
|
this.dialogs
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export class PasswordComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
|
const passwordHash = this.target!.startOs?.passwordHash || ''
|
||||||
|
|
||||||
argon2.verify(passwordHash, this.password.value)
|
argon2.verify(passwordHash, this.password.value)
|
||||||
this.context.completeWith(this.password.value)
|
this.context.completeWith(this.password.value)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
TuiCellModule,
|
TuiCellModule,
|
||||||
TuiIconModule,
|
TuiIconModule,
|
||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
`,
|
`,
|
||||||
imports: [RouterLink, TuiIconModule, TuiCellModule, TuiTitleModule],
|
imports: [RouterModule, TuiIconModule, TuiCellModule, TuiTitleModule],
|
||||||
})
|
})
|
||||||
export class RecoverComponent {
|
export class RecoverComponent {
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, inject, OnInit } from '@angular/core'
|
import { Component, inject, OnInit } from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { ErrorService } from '@start9labs/shared'
|
import { ErrorService } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
@@ -110,7 +110,7 @@ import { StateService } from 'src/app/services/state.service'
|
|||||||
`,
|
`,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterLink,
|
RouterModule,
|
||||||
TuiCardModule,
|
TuiCardModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiIconsModule,
|
TuiIconsModule,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default class RecoverPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
empty(drive: DiskBackupTarget) {
|
empty(drive: DiskBackupTarget) {
|
||||||
return !drive['embassy-os']?.full
|
return !drive.startOs?.full
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDrives() {
|
async getDrives() {
|
||||||
|
|||||||
@@ -162,9 +162,9 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
try {
|
try {
|
||||||
const ret = await this.api.complete()
|
const ret = await this.api.complete()
|
||||||
if (!this.isKiosk) {
|
if (!this.isKiosk) {
|
||||||
this.torAddress = ret['tor-address'].replace(/^https:/, 'http:')
|
this.torAddress = ret.torAddress.replace(/^https:/, 'http:')
|
||||||
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
|
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
|
||||||
this.cert = ret['root-ca']
|
this.cert = ret.rootCa
|
||||||
|
|
||||||
await this.api.exit()
|
await this.api.exit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class MockApiService extends ApiService {
|
|||||||
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return {
|
return {
|
||||||
'start-cursor': 'fakestartcursor',
|
startCursor: 'fakestartcursor',
|
||||||
guid: 'fake-guid',
|
guid: 'fake-guid',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
NgZone,
|
NgZone,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { DOCUMENT } from '@angular/common'
|
|
||||||
import { ANIMATION_FRAME } from '@ng-web-apis/common'
|
import { ANIMATION_FRAME } from '@ng-web-apis/common'
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
import { tuiZonefree } from '@taiga-ui/cdk'
|
import { tuiZonefree } from '@taiga-ui/cdk'
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export async function getSetupStatusMock(): Promise<SetupStatus | null> {
|
|||||||
const progress = tries - 1
|
const progress = tries - 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'bytes-transferred': restoreOrMigrate ? progress : 0,
|
bytesTransferred: restoreOrMigrate ? progress : 0,
|
||||||
'total-bytes': restoreOrMigrate ? total : null,
|
totalBytes: restoreOrMigrate ? total : null,
|
||||||
complete: progress === total,
|
complete: progress === total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export class SetupService extends Observable<number> {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return progress['total-bytes']
|
return progress.totalBytes
|
||||||
? progress['bytes-transferred'] / progress['total-bytes']
|
? progress.bytesTransferred / progress.totalBytes
|
||||||
: 0
|
: 0
|
||||||
}),
|
}),
|
||||||
takeWhile(value => value !== 1, true),
|
takeWhile(value => value !== 1, true),
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class LogsPage implements OnInit {
|
|||||||
|
|
||||||
if (!response.entries.length) return
|
if (!response.entries.length) return
|
||||||
|
|
||||||
this.startCursor = response['start-cursor']
|
this.startCursor = response.startCursor
|
||||||
this.logs = [convertAnsi(response.entries), ...this.logs]
|
this.logs = [convertAnsi(response.entries), ...this.logs]
|
||||||
this.scrollTop = this.scrollbar?.nativeElement.scrollTop || 0
|
this.scrollTop = this.scrollbar?.nativeElement.scrollTop || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -1,145 +1,145 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
// import { CommonModule } from '@angular/common'
|
||||||
import {
|
// import {
|
||||||
ChangeDetectionStrategy,
|
// ChangeDetectionStrategy,
|
||||||
Component,
|
// Component,
|
||||||
HostListener,
|
// HostListener,
|
||||||
Input,
|
// Input,
|
||||||
} from '@angular/core'
|
// } from '@angular/core'
|
||||||
import {
|
// import {
|
||||||
TuiBadgedContentModule,
|
// TuiBadgedContentModule,
|
||||||
TuiBadgeNotificationModule,
|
// TuiBadgeNotificationModule,
|
||||||
TuiButtonModule,
|
// TuiButtonModule,
|
||||||
TuiIconModule,
|
// TuiIconModule,
|
||||||
} from '@taiga-ui/experimental'
|
// } from '@taiga-ui/experimental'
|
||||||
import { RouterLink } from '@angular/router'
|
// import { RouterLink } from '@angular/router'
|
||||||
import { TickerModule } from '@start9labs/shared'
|
// import { TickerModule } from '@start9labs/shared'
|
||||||
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
// import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||||
import { Action, ActionsComponent } from './actions.component'
|
// import { Action, ActionsComponent } from './actions.component'
|
||||||
|
|
||||||
@Component({
|
// @Component({
|
||||||
selector: '[appCard]',
|
// selector: '[appCard]',
|
||||||
template: `
|
// template: `
|
||||||
<span class="link">
|
// <span class="link">
|
||||||
<tui-badged-content [style.--tui-radius.rem]="1.5">
|
// <tui-badged-content [style.--tui-radius.rem]="1.5">
|
||||||
@if (badge) {
|
// @if (badge) {
|
||||||
<tui-badge-notification size="m" tuiSlot="top">
|
// <tui-badge-notification size="m" tuiSlot="top">
|
||||||
{{ badge }}
|
// {{ badge }}
|
||||||
</tui-badge-notification>
|
// </tui-badge-notification>
|
||||||
}
|
// }
|
||||||
@if (icon?.startsWith('tuiIcon')) {
|
// @if (icon?.startsWith('tuiIcon')) {
|
||||||
<tui-icon class="icon" [icon]="icon" />
|
// <tui-icon class="icon" [icon]="icon" />
|
||||||
} @else {
|
// } @else {
|
||||||
<img alt="" class="icon" [src]="icon" />
|
// <img alt="" class="icon" [src]="icon" />
|
||||||
}
|
// }
|
||||||
</tui-badged-content>
|
// </tui-badged-content>
|
||||||
<label ticker class="title">{{ title }}</label>
|
// <label ticker class="title">{{ title }}</label>
|
||||||
</span>
|
// </span>
|
||||||
@if (isService) {
|
// @if (isService) {
|
||||||
<span class="side">
|
// <span class="side">
|
||||||
<tui-hosted-dropdown
|
// <tui-hosted-dropdown
|
||||||
[content]="content"
|
// [content]="content"
|
||||||
(click.stop.prevent)="(0)"
|
// (click.stop.prevent)="(0)"
|
||||||
(pointerdown.stop)="(0)"
|
// (pointerdown.stop)="(0)"
|
||||||
>
|
// >
|
||||||
<button
|
// <button
|
||||||
tuiIconButton
|
// tuiIconButton
|
||||||
appearance="outline"
|
// appearance="outline"
|
||||||
size="xs"
|
// size="xs"
|
||||||
iconLeft="tuiIconMoreHorizontal"
|
// iconLeft="tuiIconMoreHorizontal"
|
||||||
[style.border-radius.%]="100"
|
// [style.border-radius.%]="100"
|
||||||
>
|
// >
|
||||||
Actions
|
// Actions
|
||||||
</button>
|
// </button>
|
||||||
<ng-template #content let-close="close">
|
// <ng-template #content let-close="close">
|
||||||
<app-actions [actions]="actions" (click)="close()">
|
// <app-actions [actions]="actions" (click)="close()">
|
||||||
{{ title }}
|
// {{ title }}
|
||||||
</app-actions>
|
// </app-actions>
|
||||||
</ng-template>
|
// </ng-template>
|
||||||
</tui-hosted-dropdown>
|
// </tui-hosted-dropdown>
|
||||||
</span>
|
// </span>
|
||||||
}
|
// }
|
||||||
`,
|
// `,
|
||||||
styles: [
|
// styles: [
|
||||||
`
|
// `
|
||||||
:host {
|
// :host {
|
||||||
display: flex;
|
// display: flex;
|
||||||
height: 5.5rem;
|
// height: 5.5rem;
|
||||||
width: 12.5rem;
|
// width: 12.5rem;
|
||||||
border-radius: var(--tui-radius-l);
|
// border-radius: var(--tui-radius-l);
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
// box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||||
// TODO: Theme
|
// // TODO: Theme
|
||||||
background: rgb(111 109 109);
|
// background: rgb(111 109 109);
|
||||||
}
|
// }
|
||||||
|
|
||||||
.link {
|
// .link {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex: 1;
|
// flex: 1;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
justify-content: center;
|
// justify-content: center;
|
||||||
color: white;
|
// color: white;
|
||||||
gap: 0.25rem;
|
// gap: 0.25rem;
|
||||||
padding: 0 0.5rem;
|
// padding: 0 0.5rem;
|
||||||
font: var(--tui-font-text-m);
|
// font: var(--tui-font-text-m);
|
||||||
white-space: nowrap;
|
// white-space: nowrap;
|
||||||
overflow: hidden;
|
// overflow: hidden;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.icon {
|
// .icon {
|
||||||
width: 2.5rem;
|
// width: 2.5rem;
|
||||||
height: 2.5rem;
|
// height: 2.5rem;
|
||||||
border-radius: 100%;
|
// border-radius: 100%;
|
||||||
color: var(--tui-text-01-night);
|
// color: var(--tui-text-01-night);
|
||||||
}
|
// }
|
||||||
|
|
||||||
.side {
|
// .side {
|
||||||
width: 3rem;
|
// width: 3rem;
|
||||||
display: flex;
|
// display: flex;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
justify-content: center;
|
// justify-content: center;
|
||||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
// box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
||||||
// TODO: Theme
|
// // TODO: Theme
|
||||||
background: #4b4a4a;
|
// background: #4b4a4a;
|
||||||
}
|
// }
|
||||||
`,
|
// `,
|
||||||
],
|
// ],
|
||||||
standalone: true,
|
// standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
// imports: [
|
||||||
CommonModule,
|
// CommonModule,
|
||||||
RouterLink,
|
// RouterLink,
|
||||||
TuiButtonModule,
|
// TuiButtonModule,
|
||||||
TuiHostedDropdownModule,
|
// TuiHostedDropdownModule,
|
||||||
TuiDataListModule,
|
// TuiDataListModule,
|
||||||
TuiIconModule,
|
// TuiIconModule,
|
||||||
TickerModule,
|
// TickerModule,
|
||||||
TuiBadgedContentModule,
|
// TuiBadgedContentModule,
|
||||||
TuiBadgeNotificationModule,
|
// TuiBadgeNotificationModule,
|
||||||
ActionsComponent,
|
// ActionsComponent,
|
||||||
],
|
// ],
|
||||||
})
|
// })
|
||||||
export class CardComponent {
|
// export class CardComponent {
|
||||||
@Input({ required: true })
|
// @Input({ required: true })
|
||||||
id!: string
|
// id!: string
|
||||||
|
|
||||||
@Input({ required: true })
|
// @Input({ required: true })
|
||||||
icon!: string
|
// icon!: string
|
||||||
|
|
||||||
@Input({ required: true })
|
// @Input({ required: true })
|
||||||
title!: string
|
// title!: string
|
||||||
|
|
||||||
@Input()
|
// @Input()
|
||||||
actions: Record<string, readonly Action[]> = {}
|
// actions: Record<string, readonly Action[]> = {}
|
||||||
|
|
||||||
@Input()
|
// @Input()
|
||||||
badge: number | null = null
|
// badge: number | null = null
|
||||||
|
|
||||||
get isService(): boolean {
|
// get isService(): boolean {
|
||||||
return !this.id.includes('/')
|
// return !this.id.includes('/')
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Prevents Firefox from starting a native drag
|
// // Prevents Firefox from starting a native drag
|
||||||
@HostListener('pointerdown.prevent')
|
// @HostListener('pointerdown.prevent')
|
||||||
onDown() {}
|
// onDown() {}
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
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 {
|
import {
|
||||||
tuiMarkControlAsTouchedAndValidate,
|
tuiMarkControlAsTouchedAndValidate,
|
||||||
TuiValueChangesModule,
|
TuiValueChangesModule,
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ import { ConfigService } from 'src/app/services/config.service'
|
|||||||
<div tuiCell>
|
<div tuiCell>
|
||||||
<div tuiTitle>
|
<div tuiTitle>
|
||||||
<strong>CA fingerprint</strong>
|
<strong>CA fingerprint</strong>
|
||||||
<div tuiSubtitle>{{ server['ca-fingerprint'] }}</div>
|
<div tuiSubtitle>{{ server.caFingerprint }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
iconLeft="tuiIconCopy"
|
iconLeft="tuiIconCopy"
|
||||||
(click)="copyService.copy(server['ca-fingerprint'])"
|
(click)="copyService.copy(server.caFingerprint)"
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +62,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutComponent {
|
||||||
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
|
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
|
||||||
readonly copyService = inject(CopyService)
|
readonly copyService = inject(CopyService)
|
||||||
readonly gitHash = inject(ConfigService).gitHash
|
readonly gitHash = inject(ConfigService).gitHash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export class HeaderConnectionComponent {
|
|||||||
inject(ConnectionService).networkConnected$,
|
inject(ConnectionService).networkConnected$,
|
||||||
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
|
inject(ConnectionService).websocketConnected$.pipe(startWith(false)),
|
||||||
inject(PatchDB<DataModel>)
|
inject(PatchDB<DataModel>)
|
||||||
.watch$('server-info', 'status-info')
|
.watch$('serverInfo', 'statusInfo')
|
||||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
.pipe(startWith({ restarting: false, shuttingDown: false })),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([network, websocket, status]) => {
|
map(([network, websocket, status]) => {
|
||||||
if (!network)
|
if (!network)
|
||||||
@@ -68,7 +68,7 @@ export class HeaderConnectionComponent {
|
|||||||
icon: 'tuiIconCloudOff',
|
icon: 'tuiIconCloudOff',
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
}
|
}
|
||||||
if (status['shutting-down'])
|
if (status.shuttingDown)
|
||||||
return {
|
return {
|
||||||
message: 'Shutting Down',
|
message: 'Shutting Down',
|
||||||
color: 'var(--tui-neutral-fill)',
|
color: 'var(--tui-neutral-fill)',
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export class HeaderComponent {
|
|||||||
'ui',
|
'ui',
|
||||||
'gaming',
|
'gaming',
|
||||||
'snake',
|
'snake',
|
||||||
'high-score',
|
'highScore',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { NotificationService } from '../../services/notification.service'
|
|||||||
tuiCell
|
tuiCell
|
||||||
[notification]="not"
|
[notification]="not"
|
||||||
>
|
>
|
||||||
<ng-container *ngIf="not['package-id'] as pkgId">
|
<ng-container *ngIf="not.packageId as pkgId">
|
||||||
{{ $any(packageData[pkgId])?.manifest.title || pkgId }}
|
{{ $any(packageData[pkgId])?.manifest.title || pkgId }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button
|
<button
|
||||||
@@ -57,11 +57,11 @@ import { NotificationService } from '../../services/notification.service'
|
|||||||
(click)="markSeen(notifications, not)"
|
(click)="markSeen(notifications, not)"
|
||||||
></button>
|
></button>
|
||||||
<a
|
<a
|
||||||
*ngIf="not['package-id'] && packageData[not['package-id']]"
|
*ngIf="not.packageId && packageData[not.packageId]"
|
||||||
tuiButton
|
tuiButton
|
||||||
size="xs"
|
size="xs"
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
[routerLink]="getLink(not['package-id'] || '')"
|
[routerLink]="getLink(not.packageId || '')"
|
||||||
>
|
>
|
||||||
View Service
|
View Service
|
||||||
</a>
|
</a>
|
||||||
@@ -104,7 +104,7 @@ export class HeaderNotificationsComponent {
|
|||||||
private readonly patch = inject(PatchDB<DataModel>)
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
private readonly service = inject(NotificationService)
|
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>()
|
readonly notifications$ = new Subject<ServerNotifications>()
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export class HeaderNotificationsComponent {
|
|||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.patch
|
this.patch
|
||||||
.watch$('server-info', 'unreadNotifications', 'recent')
|
.watch$('serverInfo', 'unreadNotifications', 'recent')
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(recent => this.notifications$.next(recent)),
|
tap(recent => this.notifications$.next(recent)),
|
||||||
first(),
|
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 {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -20,10 +20,9 @@ import {
|
|||||||
} from 'src/app/apps/portal/components/interfaces/interface.utils'
|
} from 'src/app/apps/portal/components/interfaces/interface.utils'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DomainInfo, NetworkInfo } from 'src/app/services/patch-db/data-model'
|
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { getClearnetAddress } from 'src/app/util/clearnetAddress'
|
import { InterfaceAddressComponent } from './interface-addresses.component'
|
||||||
import { InterfaceComponent } from './interface.component'
|
import { InterfaceComponent } from './interface.component'
|
||||||
import { InterfacesComponent } from './interfaces.component'
|
|
||||||
|
|
||||||
type ClearnetForm = {
|
type ClearnetForm = {
|
||||||
domain: string
|
domain: string
|
||||||
@@ -45,32 +44,36 @@ type ClearnetForm = {
|
|||||||
</a>
|
</a>
|
||||||
</em>
|
</em>
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="interfaces.addressInfo.domainInfo as domainInfo; else noClearnet"
|
*ngIf="
|
||||||
|
interface.serviceInterface.addresses.clearnet as addresses;
|
||||||
|
else empty
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<app-interface
|
<app-interface-address
|
||||||
label="Clearnet"
|
*ngFor="let address of addresses"
|
||||||
[hostname]="getClearnet(domainInfo)"
|
[label]="address.label"
|
||||||
[isUi]="interfaces.isUi"
|
[address]="address.url"
|
||||||
|
[isMasked]="interface.serviceInterface.masked"
|
||||||
|
[isUi]="interface.serviceInterface.type === 'ui'"
|
||||||
/>
|
/>
|
||||||
<div [style.display]="'flex'" [style.gap.rem]="1">
|
<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()">
|
<button tuiButton size="s" appearance="danger-solid" (click)="remove()">
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #noClearnet>
|
<ng-template #empty>
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
iconLeft="tuiIconPlus"
|
iconLeft="tuiIconPlus"
|
||||||
[style.align-self]="'flex-start'"
|
[style.align-self]="'flex-start'"
|
||||||
(click)="add()"
|
(click)="add()"
|
||||||
>
|
>
|
||||||
Add Clearnet
|
Add Address
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
`,
|
`,
|
||||||
imports: [InterfaceComponent, NgIf, TuiButtonModule],
|
imports: [NgForOf, InterfaceAddressComponent, NgIf, TuiButtonModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class InterfaceClearnetComponent {
|
export class InterfaceClearnetComponent {
|
||||||
@@ -79,21 +82,14 @@ export class InterfaceClearnetComponent {
|
|||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
readonly interfaces = inject(InterfacesComponent)
|
readonly interface = inject(InterfaceComponent)
|
||||||
|
|
||||||
@Input({ required: true }) network!: NetworkInfo
|
@Input({ required: true }) network!: NetworkInfo
|
||||||
|
|
||||||
getClearnet(clearnet: DomainInfo): string {
|
|
||||||
return getClearnetAddress('https', clearnet)
|
|
||||||
}
|
|
||||||
|
|
||||||
async add() {
|
async add() {
|
||||||
const { domainInfo } = this.interfaces.addressInfo
|
|
||||||
const { domain = '', subdomain = '' } = domainInfo || {}
|
|
||||||
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
||||||
label: 'Select Domain/Subdomain',
|
label: 'Select Domain/Subdomain',
|
||||||
data: {
|
data: {
|
||||||
value: { domain, subdomain },
|
|
||||||
spec: await getClearnetSpec(this.network),
|
spec: await getClearnetSpec(this.network),
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
@@ -118,9 +114,9 @@ export class InterfaceClearnetComponent {
|
|||||||
const loader = this.loader.open('Removing...').subscribe()
|
const loader = this.loader.open('Removing...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.interfaces.packageContext) {
|
if (this.interface.packageContext) {
|
||||||
await this.api.setInterfaceClearnetAddress({
|
await this.api.setInterfaceClearnetAddress({
|
||||||
...this.interfaces.packageContext,
|
...this.interface.packageContext,
|
||||||
domainInfo: null,
|
domainInfo: null,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -138,9 +134,9 @@ export class InterfaceClearnetComponent {
|
|||||||
const loader = this.loader.open('Saving...').subscribe()
|
const loader = this.loader.open('Saving...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.interfaces.packageContext) {
|
if (this.interface.packageContext) {
|
||||||
await this.api.setInterfaceClearnetAddress({
|
await this.api.setInterfaceClearnetAddress({
|
||||||
...this.interfaces.packageContext,
|
...this.interface.packageContext,
|
||||||
domainInfo,
|
domainInfo,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { NgForOf, NgIf } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { InterfacesComponent } from './interfaces.component'
|
|
||||||
import { InterfaceComponent } from './interface.component'
|
import { InterfaceComponent } from './interface.component'
|
||||||
|
import { InterfaceAddressComponent } from './interface-addresses.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -19,41 +19,44 @@ import { InterfaceComponent } from './interface.component'
|
|||||||
<strong>View instructions</strong>
|
<strong>View instructions</strong>
|
||||||
</a>
|
</a>
|
||||||
</em>
|
</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
|
<ng-container
|
||||||
*ngFor="let iface of interfaces.addressInfo.ipInfo | keyvalue"
|
*ngIf="
|
||||||
|
interface.serviceInterface.addresses.local as addresses;
|
||||||
|
else empty
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<app-interface
|
<app-interface-address
|
||||||
*ngIf="iface.value.ipv4 as ipv4"
|
*ngFor="let address of addresses"
|
||||||
[label]="iface.key + ' (IPv4)'"
|
[label]="address.label"
|
||||||
[hostname]="ipv4"
|
[address]="address.url"
|
||||||
[isUi]="interfaces.isUi"
|
[isMasked]="interface.serviceInterface.masked"
|
||||||
></app-interface>
|
[isUi]="interface.serviceInterface.type === 'ui'"
|
||||||
<app-interface
|
/>
|
||||||
*ngIf="iface.value.ipv6 as ipv6"
|
<div [style.display]="'flex'" [style.gap.rem]="1">
|
||||||
[label]="iface.key + ' (IPv6)'"
|
<button tuiButton size="s" appearance="danger-solid" (click)="remove()">
|
||||||
[hostname]="ipv6"
|
Remove
|
||||||
[isUi]="interfaces.isUi"
|
</button>
|
||||||
></app-interface>
|
</div>
|
||||||
</ng-container>
|
</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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class InterfaceLocalComponent {
|
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 { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { InterfaceAddressComponent } from './interface-addresses.component'
|
||||||
import { InterfaceComponent } from './interface.component'
|
import { InterfaceComponent } from './interface.component'
|
||||||
import { InterfacesComponent } from './interfaces.component'
|
import { NgForOf, NgIf } from '@angular/common'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -17,15 +18,49 @@ import { InterfacesComponent } from './interfaces.component'
|
|||||||
<strong>View instructions</strong>
|
<strong>View instructions</strong>
|
||||||
</a>
|
</a>
|
||||||
</em>
|
</em>
|
||||||
<app-interface
|
|
||||||
label="Tor"
|
<ng-container
|
||||||
[hostname]="interfaces.addressInfo.torHostname"
|
*ngIf="interface.serviceInterface.addresses.tor as addresses; else empty"
|
||||||
[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>
|
||||||
|
|
||||||
|
<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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class InterfaceTorComponent {
|
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 {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { WINDOW } from '@ng-web-apis/common'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { CopyService } from '@start9labs/shared'
|
import { TuiCardModule, TuiSurfaceModule } from '@taiga-ui/experimental'
|
||||||
import { TuiDialogService } from '@taiga-ui/core'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import { InterfaceClearnetComponent } from 'src/app/apps/portal/components/interfaces/interface-clearnet.component'
|
||||||
TuiButtonModule,
|
import { InterfaceLocalComponent } from 'src/app/apps/portal/components/interfaces/interface-local.component'
|
||||||
TuiCellModule,
|
import { InterfaceTorComponent } from 'src/app/apps/portal/components/interfaces/interface-tor.component'
|
||||||
TuiTitleModule,
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
} from '@taiga-ui/experimental'
|
import { AddressDetails } from './interface.utils'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
|
||||||
import { QRComponent } from 'src/app/common/qr.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'app-interface',
|
selector: 'app-interface',
|
||||||
template: `
|
template: `
|
||||||
<div tuiCell>
|
<h3 class="g-title">Clearnet</h3>
|
||||||
<h3 tuiTitle>
|
<app-interface-clearnet
|
||||||
{{ label }}
|
*ngIf="network$ | async as network"
|
||||||
<span tuiSubtitle>{{ hostname }}</span>
|
tuiCardLarge="compact"
|
||||||
</h3>
|
tuiSurface="elevated"
|
||||||
<button
|
[network]="network"
|
||||||
*ngIf="isUi"
|
/>
|
||||||
tuiIconButton
|
|
||||||
iconLeft="tuiIconExternalLink"
|
<h3 class="g-title">Tor</h3>
|
||||||
appearance="icon"
|
<app-interface-tor tuiCardLarge="compact" tuiSurface="elevated" />
|
||||||
(click)="launch(hostname)"
|
|
||||||
>
|
<h3 class="g-title">Local</h3>
|
||||||
Launch
|
<app-interface-local tuiCardLarge="compact" tuiSurface="elevated" />
|
||||||
</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>
|
|
||||||
`,
|
`,
|
||||||
imports: [NgIf, TuiCellModule, TuiTitleModule, TuiButtonModule],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
InterfaceTorComponent,
|
||||||
|
InterfaceLocalComponent,
|
||||||
|
InterfaceClearnetComponent,
|
||||||
|
TuiCardModule,
|
||||||
|
TuiSurfaceModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class InterfaceComponent {
|
export class InterfaceComponent {
|
||||||
private readonly window = inject(WINDOW)
|
readonly network$ = inject(PatchDB<DataModel>).watch$('serverInfo', 'network')
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
|
||||||
readonly copyService = inject(CopyService)
|
|
||||||
|
|
||||||
@Input({ required: true }) label = ''
|
@Input() packageContext?: {
|
||||||
@Input({ required: true }) hostname = ''
|
packageId: string
|
||||||
@Input({ required: true }) isUi = false
|
interfaceId: string
|
||||||
|
|
||||||
launch(url: string): void {
|
|
||||||
this.window.open(url, '_blank', 'noreferrer')
|
|
||||||
}
|
}
|
||||||
|
@Input({ required: true }) serviceInterface!: ServiceInterfaceWithAddresses
|
||||||
|
}
|
||||||
|
|
||||||
showQR(data: string) {
|
export type ServiceInterfaceWithAddresses = T.ServiceInterface & {
|
||||||
this.dialogs
|
addresses: {
|
||||||
.open(new PolymorpheusComponent(QRComponent), {
|
clearnet: AddressDetails[]
|
||||||
size: 'auto',
|
local: AddressDetails[]
|
||||||
data,
|
tor: AddressDetails[]
|
||||||
})
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
|
||||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
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 { TuiDialogOptions } from '@taiga-ui/core'
|
||||||
import { TuiPromptData } from '@taiga-ui/kit'
|
import { TuiPromptData } from '@taiga-ui/kit'
|
||||||
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
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)),
|
map(({ entries }) => convertAnsi(entries)),
|
||||||
catchError(e => {
|
catchError(e => {
|
||||||
this.errors.handleError(e)
|
this.errors.handleError(e)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class LogsPipe implements PipeTransform {
|
|||||||
map(() => getMessage(true)),
|
map(() => getMessage(true)),
|
||||||
),
|
),
|
||||||
defer(() => followLogs(this.options)).pipe(
|
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))),
|
switchMap(r => this.api.openLogsWebsocket$(this.toConfig(r.guid))),
|
||||||
bufferTime(1000),
|
bufferTime(1000),
|
||||||
filter(logs => !!logs.length),
|
filter(logs => !!logs.length),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
isEmptyObject,
|
isEmptyObject,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} 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 { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import {
|
import {
|
||||||
TuiDialogContext,
|
TuiDialogContext,
|
||||||
@@ -27,7 +27,11 @@ import {
|
|||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
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 { Breakages } from 'src/app/services/api/api.types'
|
||||||
import { InvalidService } from 'src/app/common/form/invalid.service'
|
import { InvalidService } from 'src/app/common/form/invalid.service'
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +39,7 @@ import {
|
|||||||
FormComponent,
|
FormComponent,
|
||||||
} from 'src/app/apps/portal/components/form.component'
|
} from 'src/app/apps/portal/components/form.component'
|
||||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||||
|
import { ToManifestPipe } from '../pipes/to-manifest'
|
||||||
|
|
||||||
export interface PackageConfigData {
|
export interface PackageConfigData {
|
||||||
readonly pkgId: string
|
readonly pkgId: string
|
||||||
@@ -52,23 +57,26 @@ export interface PackageConfigData {
|
|||||||
<div [innerHTML]="loadingError"></div>
|
<div [innerHTML]="loadingError"></div>
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
|
|
||||||
<ng-container *ngIf="!loadingText && !loadingError && pkg">
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
!loadingText && !loadingError && pkg && (pkg | toManifest) as manifest
|
||||||
|
"
|
||||||
|
>
|
||||||
<tui-notification *ngIf="success" status="success">
|
<tui-notification *ngIf="success" status="success">
|
||||||
{{ pkg.manifest.title }} has been automatically configured with
|
{{ manifest.title }} has been automatically configured with recommended
|
||||||
recommended defaults. Make whatever changes you want, then click "Save".
|
defaults. Make whatever changes you want, then click "Save".
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
|
|
||||||
<config-dep
|
<config-dep
|
||||||
*ngIf="dependentInfo && value && original"
|
*ngIf="dependentInfo && value && original"
|
||||||
[package]="pkg.manifest.title"
|
[package]="manifest.title"
|
||||||
[dep]="dependentInfo.title"
|
[dep]="dependentInfo.title"
|
||||||
[original]="original"
|
[original]="original"
|
||||||
[value]="value"
|
[value]="value"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<tui-notification *ngIf="!pkg.installed?.['has-config']" status="warning">
|
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
|
||||||
No config options for {{ pkg.manifest.title }}
|
No config options for {{ manifest.title }} {{ manifest.version }}.
|
||||||
{{ pkg.manifest.version }}.
|
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
|
|
||||||
<app-form
|
<app-form
|
||||||
@@ -106,6 +114,7 @@ export interface PackageConfigData {
|
|||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiModeModule,
|
TuiModeModule,
|
||||||
ConfigDepComponent,
|
ConfigDepComponent,
|
||||||
|
ToManifestPipe,
|
||||||
],
|
],
|
||||||
providers: [InvalidService],
|
providers: [InvalidService],
|
||||||
})
|
})
|
||||||
@@ -149,7 +158,7 @@ export class ServiceConfigModal {
|
|||||||
!!this.form &&
|
!!this.form &&
|
||||||
!this.form.form.dirty &&
|
!this.form.form.dirty &&
|
||||||
!this.original &&
|
!this.original &&
|
||||||
!this.pkg?.installed?.status?.configured
|
!this.pkg?.status?.configured
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,12 +174,12 @@ export class ServiceConfigModal {
|
|||||||
|
|
||||||
if (this.dependentInfo) {
|
if (this.dependentInfo) {
|
||||||
const depConfig = await this.embassyApi.dryConfigureDependency({
|
const depConfig = await this.embassyApi.dryConfigureDependency({
|
||||||
'dependency-id': this.pkgId,
|
dependencyId: this.pkgId,
|
||||||
'dependent-id': this.dependentInfo.id,
|
dependentId: this.dependentInfo.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.original = depConfig['old-config']
|
this.original = depConfig.oldConfig
|
||||||
this.value = depConfig['new-config'] || this.original
|
this.value = depConfig.newConfig || this.original
|
||||||
this.spec = depConfig.spec
|
this.spec = depConfig.spec
|
||||||
this.patch = compare(this.original, this.value)
|
this.patch = compare(this.original, this.value)
|
||||||
} else {
|
} else {
|
||||||
@@ -195,7 +204,7 @@ export class ServiceConfigModal {
|
|||||||
try {
|
try {
|
||||||
await this.uploadFiles(config, loader)
|
await this.uploadFiles(config, loader)
|
||||||
|
|
||||||
if (hasCurrentDeps(this.pkg!)) {
|
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
|
||||||
await this.configureDeps(config, loader)
|
await this.configureDeps(config, loader)
|
||||||
} else {
|
} else {
|
||||||
await this.configure(config, loader)
|
await this.configure(config, loader)
|
||||||
@@ -260,7 +269,7 @@ export class ServiceConfigModal {
|
|||||||
const message =
|
const message =
|
||||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
'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(
|
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>`
|
)}</ul>`
|
||||||
const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' }
|
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,
|
TuiButtonModule,
|
||||||
tuiButtonOptionsProvider,
|
tuiButtonOptionsProvider,
|
||||||
} from '@taiga-ui/experimental'
|
} from '@taiga-ui/experimental'
|
||||||
import { map, of } from 'rxjs'
|
import { map, Observable } from 'rxjs'
|
||||||
import { UIComponent } from 'src/app/apps/portal/routes/dashboard/ui.component'
|
import { UILaunchComponent } from 'src/app/apps/portal/routes/dashboard/ui.component'
|
||||||
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
|
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
|
||||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
import {
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
PackageDataEntry,
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
PackageMainStatus,
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'fieldset[appControls]',
|
selector: 'fieldset[appControls]',
|
||||||
template: `
|
template: `
|
||||||
@if (isRunning) {
|
@if (pkg.status.main.status === 'running') {
|
||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
iconLeft="tuiIconSquare"
|
iconLeft="tuiIconSquare"
|
||||||
(click)="actions.stop(appControls)"
|
(click)="actions.stop(manifest)"
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
@@ -35,17 +34,17 @@ import {
|
|||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
iconLeft="tuiIconRotateCw"
|
iconLeft="tuiIconRotateCw"
|
||||||
(click)="actions.restart(appControls)"
|
(click)="actions.restart(manifest)"
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
*tuiLet="hasUnmet(appControls) | async as hasUnmet"
|
*tuiLet="hasUnmet(pkg) | async as hasUnmet"
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
iconLeft="tuiIconPlay"
|
iconLeft="tuiIconPlay"
|
||||||
[disabled]="!isConfigured"
|
[disabled]="!this.pkg.status.configured"
|
||||||
(click)="actions.start(appControls, !!hasUnmet)"
|
(click)="actions.start(manifest, !!hasUnmet)"
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
@@ -53,48 +52,39 @@ import {
|
|||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
iconLeft="tuiIconTool"
|
iconLeft="tuiIconTool"
|
||||||
(click)="actions.configure(appControls)"
|
(click)="actions.configure(manifest)"
|
||||||
>
|
>
|
||||||
Configure
|
Configure
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<app-ui [pkg]="appControls" />
|
<app-ui-launch [pkg]="pkg" />
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiButtonModule, UIComponent, TuiLetModule, AsyncPipe],
|
imports: [TuiButtonModule, UILaunchComponent, TuiLetModule, AsyncPipe],
|
||||||
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
||||||
})
|
})
|
||||||
export class ControlsComponent {
|
export class ControlsComponent {
|
||||||
private readonly errors = inject(DepErrorService)
|
private readonly errors = inject(DepErrorService)
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
appControls!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
|
get manifest(): Manifest {
|
||||||
|
return getManifest(this.pkg)
|
||||||
|
}
|
||||||
|
|
||||||
readonly actions = inject(ActionsService)
|
readonly actions = inject(ActionsService)
|
||||||
|
|
||||||
get isRunning(): boolean {
|
@tuiPure
|
||||||
return (
|
hasUnmet(pkg: PackageDataEntry): Observable<boolean> {
|
||||||
this.appControls.installed?.status.main.status ===
|
const id = getManifest(pkg).id
|
||||||
PackageMainStatus.Running
|
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 { StatusComponent } from 'src/app/apps/portal/routes/dashboard/status.component'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
|
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
|
||||||
import {
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
PackageDataEntry,
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
PackageState,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'tr[appService]',
|
selector: 'tr[appService]',
|
||||||
template: `
|
template: `
|
||||||
<td><img alt="logo" [src]="appService.icon" /></td>
|
<td><img alt="logo" [src]="pkg.icon" /></td>
|
||||||
<td>
|
<td>
|
||||||
<a [routerLink]="routerLink">{{ appService.manifest.title }}</a>
|
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ appService.manifest.version }}</td>
|
<td>{{ manifest.version }}</td>
|
||||||
<td
|
<td appStatus [pkg]="pkg" [hasDepErrors]="hasError(depErrors)"></td>
|
||||||
[appStatus]="appService"
|
|
||||||
[appStatusError]="hasError(appServiceError)"
|
|
||||||
></td>
|
|
||||||
<td [style.text-align]="'center'">
|
<td [style.text-align]="'center'">
|
||||||
<fieldset
|
<fieldset
|
||||||
[disabled]="!installed || !(connected$ | async)"
|
appControls
|
||||||
[appControls]="appService"
|
[disabled]="
|
||||||
|
this.pkg.stateInfo.state !== 'installed' || !(connected$ | async)
|
||||||
|
"
|
||||||
|
[pkg]="pkg"
|
||||||
></fieldset>
|
></fieldset>
|
||||||
</td>
|
</td>
|
||||||
`,
|
`,
|
||||||
@@ -57,19 +54,19 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
|||||||
})
|
})
|
||||||
export class ServiceComponent {
|
export class ServiceComponent {
|
||||||
@Input()
|
@Input()
|
||||||
appService!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
appServiceError?: PkgDependencyErrors
|
depErrors?: PkgDependencyErrors
|
||||||
|
|
||||||
readonly connected$ = inject(ConnectionService).connected$
|
readonly connected$ = inject(ConnectionService).connected$
|
||||||
|
|
||||||
get routerLink() {
|
get manifest() {
|
||||||
return `/portal/service/${this.appService.manifest.id}`
|
return getManifest(this.pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
get installed(): boolean {
|
get routerLink() {
|
||||||
return this.appService.state === PackageState.Installed
|
return `/portal/service/${this.manifest.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|||||||
import { ServiceComponent } from 'src/app/apps/portal/routes/dashboard/service.component'
|
import { ServiceComponent } from 'src/app/apps/portal/routes/dashboard/service.component'
|
||||||
import { ServicesService } from 'src/app/apps/portal/services/services.service'
|
import { ServicesService } from 'src/app/apps/portal/services/services.service'
|
||||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
|
import { ToManifestPipe } from '../../pipes/to-manifest'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -21,10 +22,11 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@if (errors$ | async; as errors) {
|
@if (errors$ | async; as errors) {
|
||||||
@for (service of services$ | async; track $index) {
|
@for (pkg of services$ | async; track $index) {
|
||||||
<tr
|
<tr
|
||||||
[appService]="service"
|
appService
|
||||||
[appServiceError]="errors[service.manifest.id]"
|
[pkg]="pkg"
|
||||||
|
[depErrors]="errors[(pkg | toManifest).id]"
|
||||||
></tr>
|
></tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
@@ -78,7 +80,7 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ServiceComponent, AsyncPipe],
|
imports: [ServiceComponent, AsyncPipe, ToManifestPipe],
|
||||||
})
|
})
|
||||||
export class ServicesComponent {
|
export class ServicesComponent {
|
||||||
readonly services$ = inject(ServicesService)
|
readonly services$ = inject(ServicesService)
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
|||||||
import { tuiPure } from '@taiga-ui/cdk'
|
import { tuiPure } from '@taiga-ui/cdk'
|
||||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||||
import { TuiIconModule } from '@taiga-ui/experimental'
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import {
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
PackageDataEntry,
|
|
||||||
PackageState,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import {
|
import {
|
||||||
HealthStatus,
|
HealthStatus,
|
||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
renderPkgStatus,
|
renderPkgStatus,
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} 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({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -41,27 +38,23 @@ import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
|||||||
})
|
})
|
||||||
export class StatusComponent {
|
export class StatusComponent {
|
||||||
@Input()
|
@Input()
|
||||||
appStatus!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
appStatusError = false
|
hasDepErrors = false
|
||||||
|
|
||||||
get healthy(): boolean {
|
get healthy(): boolean {
|
||||||
const status = this.getStatus(this.appStatus)
|
const status = this.getStatus(this.pkg)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!this.appStatusError && // no deps error
|
!this.hasDepErrors && // no deps error
|
||||||
!!this.appStatus.installed?.status.configured && // no config needed
|
!!this.pkg.status.configured && // no config needed
|
||||||
status.primary !== PackageState.NeedsUpdate && // no update needed
|
|
||||||
status.health !== HealthStatus.Failure // no health issues
|
status.health !== HealthStatus.Failure // no health issues
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get loading(): boolean {
|
get loading(): boolean {
|
||||||
return (
|
return !!this.pkg.stateInfo || this.color === 'var(--tui-info-fill)'
|
||||||
!!this.appStatus['install-progress'] ||
|
|
||||||
this.color === 'var(--tui-info-fill)'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
@@ -70,17 +63,15 @@ export class StatusComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get status(): string {
|
get status(): string {
|
||||||
if (this.appStatus['install-progress']) {
|
if (this.pkg.stateInfo.installingInfo) {
|
||||||
return `Installing... ${packageLoadingProgress(this.appStatus['install-progress'])?.totalProgress || 0}%`
|
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:
|
case PrimaryStatus.Running:
|
||||||
return 'Running'
|
return 'Running'
|
||||||
case PrimaryStatus.Stopped:
|
case PrimaryStatus.Stopped:
|
||||||
return 'Stopped'
|
return 'Stopped'
|
||||||
case PackageState.NeedsUpdate:
|
|
||||||
return 'Needs Update'
|
|
||||||
case PrimaryStatus.NeedsConfig:
|
case PrimaryStatus.NeedsConfig:
|
||||||
return 'Needs Config'
|
return 'Needs Config'
|
||||||
case PrimaryStatus.Updating:
|
case PrimaryStatus.Updating:
|
||||||
@@ -103,14 +94,13 @@ export class StatusComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get color(): string {
|
get color(): string {
|
||||||
if (this.appStatus['install-progress']) {
|
if (this.pkg.stateInfo.installingInfo) {
|
||||||
return 'var(--tui-info-fill)'
|
return 'var(--tui-info-fill)'
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.getStatus(this.appStatus).primary) {
|
switch (this.getStatus(this.pkg).primary) {
|
||||||
case PrimaryStatus.Running:
|
case PrimaryStatus.Running:
|
||||||
return 'var(--tui-success-fill)'
|
return 'var(--tui-success-fill)'
|
||||||
case PackageState.NeedsUpdate:
|
|
||||||
case PrimaryStatus.NeedsConfig:
|
case PrimaryStatus.NeedsConfig:
|
||||||
return 'var(--tui-warning-fill)'
|
return 'var(--tui-warning-fill)'
|
||||||
case PrimaryStatus.Updating:
|
case PrimaryStatus.Updating:
|
||||||
|
|||||||
@@ -4,20 +4,19 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { tuiPure } from '@taiga-ui/cdk'
|
import { tuiPure } from '@taiga-ui/cdk'
|
||||||
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core'
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import {
|
import {
|
||||||
InstalledPackageInfo,
|
|
||||||
InterfaceInfo,
|
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'app-ui',
|
selector: 'app-ui-launch',
|
||||||
template: `
|
template: `
|
||||||
@if (interfaces.length > 1) {
|
@if (interfaces.length > 1) {
|
||||||
<tui-hosted-dropdown [content]="content">
|
<tui-hosted-dropdown [content]="content">
|
||||||
@@ -26,7 +25,7 @@ import {
|
|||||||
iconLeft="tuiIconExternalLink"
|
iconLeft="tuiIconExternalLink"
|
||||||
[disabled]="!isRunning"
|
[disabled]="!isRunning"
|
||||||
>
|
>
|
||||||
Interfaces
|
Launch UI
|
||||||
</button>
|
</button>
|
||||||
<ng-template #content>
|
<ng-template #content>
|
||||||
<tui-data-list>
|
<tui-data-list>
|
||||||
@@ -44,42 +43,44 @@ import {
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</tui-hosted-dropdown>
|
</tui-hosted-dropdown>
|
||||||
} @else {
|
} @else {
|
||||||
<a
|
@if (interfaces[0]; as info) {
|
||||||
tuiIconButton
|
<a
|
||||||
iconLeft="tuiIconExternalLink"
|
tuiIconButton
|
||||||
target="_blank"
|
iconLeft="tuiIconExternalLink"
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
[attr.href]="getHref(interfaces[0])"
|
rel="noreferrer"
|
||||||
>
|
[attr.href]="getHref(info)"
|
||||||
{{ interfaces[0]?.name }}
|
>
|
||||||
</a>
|
{{ info.name }}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule],
|
imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule],
|
||||||
})
|
})
|
||||||
export class UIComponent {
|
export class UILaunchComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
pkg!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
get interfaces(): readonly InterfaceInfo[] {
|
get interfaces(): readonly T.ServiceInterfaceWithHostInfo[] {
|
||||||
return this.getInterfaces(this.pkg.installed)
|
return this.getInterfaces(this.pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRunning(): boolean {
|
get isRunning(): boolean {
|
||||||
return this.pkg.installed?.status.main.status === PackageMainStatus.Running
|
return this.pkg.status.main.status === PackageMainStatus.Running
|
||||||
}
|
}
|
||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
getInterfaces(info?: InstalledPackageInfo): InterfaceInfo[] {
|
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterfaceWithHostInfo[] {
|
||||||
return info
|
return pkg
|
||||||
? Object.values(info.interfaceInfo).filter(({ type }) => type === 'ui')
|
? 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
|
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 { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
|
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
|
||||||
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
|
import { ActionsService } from 'src/app/apps/portal/services/actions.service'
|
||||||
import {
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
PackageDataEntry,
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
PackageMainStatus,
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'service-actions',
|
selector: 'service-actions',
|
||||||
template: `
|
template: `
|
||||||
@if (isRunning) {
|
@if (pkg.status.main.status === 'running') {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
appearance="secondary-destructive"
|
appearance="secondary-destructive"
|
||||||
iconLeft="tuiIconSquare"
|
iconLeft="tuiIconSquare"
|
||||||
(click)="actions.stop(service)"
|
(click)="actions.stop(manifest)"
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
@@ -30,17 +29,17 @@ import {
|
|||||||
tuiButton
|
tuiButton
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
iconLeft="tuiIconRotateCw"
|
iconLeft="tuiIconRotateCw"
|
||||||
(click)="actions.restart(service)"
|
(click)="actions.restart(manifest)"
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (isStopped && isConfigured) {
|
@if (pkg.status.main.status === 'stopped' && isConfigured) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
iconLeft="tuiIconPlay"
|
iconLeft="tuiIconPlay"
|
||||||
(click)="actions.start(service, hasUnmet(dependencies))"
|
(click)="actions.start(manifest, hasUnmet(dependencies))"
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
@@ -51,7 +50,7 @@ import {
|
|||||||
tuiButton
|
tuiButton
|
||||||
appearance="secondary-warning"
|
appearance="secondary-warning"
|
||||||
iconLeft="tuiIconTool"
|
iconLeft="tuiIconTool"
|
||||||
(click)="actions.configure(service)"
|
(click)="actions.configure(manifest)"
|
||||||
>
|
>
|
||||||
Configure
|
Configure
|
||||||
</button>
|
</button>
|
||||||
@@ -64,7 +63,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class ServiceActionsComponent {
|
export class ServiceActionsComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
service!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
dependencies: readonly DependencyInfo[] = []
|
dependencies: readonly DependencyInfo[] = []
|
||||||
@@ -72,19 +71,11 @@ export class ServiceActionsComponent {
|
|||||||
readonly actions = inject(ActionsService)
|
readonly actions = inject(ActionsService)
|
||||||
|
|
||||||
get isConfigured(): boolean {
|
get isConfigured(): boolean {
|
||||||
return this.service.installed!.status.configured
|
return this.pkg.status.configured
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRunning(): boolean {
|
get manifest(): Manifest {
|
||||||
return (
|
return getManifest(this.pkg)
|
||||||
this.service.installed?.status.main.status === PackageMainStatus.Running
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get isStopped(): boolean {
|
|
||||||
return (
|
|
||||||
this.service.installed?.status.main.status === PackageMainStatus.Stopped
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ServiceAdditionalItemComponent } from './additional-item.component'
|
|||||||
selector: 'service-additional',
|
selector: 'service-additional',
|
||||||
template: `
|
template: `
|
||||||
<h3 class="g-title">Additional Info</h3>
|
<h3 class="g-title">Additional Info</h3>
|
||||||
<ng-container *ngFor="let additional of service | toAdditional">
|
<ng-container *ngFor="let additional of pkg | toAdditional">
|
||||||
<a
|
<a
|
||||||
*ngIf="additional.description.startsWith('http'); else button"
|
*ngIf="additional.description.startsWith('http'); else button"
|
||||||
class="g-action"
|
class="g-action"
|
||||||
@@ -30,5 +30,5 @@ import { ServiceAdditionalItemComponent } from './additional-item.component'
|
|||||||
})
|
})
|
||||||
export class ServiceAdditionalComponent {
|
export class ServiceAdditionalComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
service!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,23 +91,16 @@ export class ServiceHealthCheckComponent {
|
|||||||
return 'Awaiting result...'
|
return 'Awaiting result...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix =
|
|
||||||
this.check.result !== HealthResult.Failure &&
|
|
||||||
this.check.result !== HealthResult.Loading
|
|
||||||
? this.check.result
|
|
||||||
: ''
|
|
||||||
|
|
||||||
switch (this.check.result) {
|
switch (this.check.result) {
|
||||||
case HealthResult.Failure:
|
|
||||||
return prefix + this.check.error
|
|
||||||
case HealthResult.Starting:
|
case HealthResult.Starting:
|
||||||
return `${prefix}...`
|
return 'Starting...'
|
||||||
case HealthResult.Success:
|
case HealthResult.Success:
|
||||||
return `${prefix}: ${this.check.message}`
|
return `Success: ${this.check.message}`
|
||||||
case HealthResult.Loading:
|
case HealthResult.Loading:
|
||||||
return prefix + this.check.message
|
case HealthResult.Failure:
|
||||||
|
return this.check.message
|
||||||
default:
|
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'
|
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'a[serviceInterface]',
|
selector: 'a[serviceInterfaceListItem]',
|
||||||
template: `
|
template: `
|
||||||
<tui-svg [src]="info.icon" [style.color]="info.color"></tui-svg>
|
<tui-svg [src]="info.icon" [style.color]="info.color"></tui-svg>
|
||||||
<div [style.flex]="1">
|
<div [style.flex]="1">
|
||||||
@@ -35,10 +35,10 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiButtonModule, CommonModule, TuiSvgModule],
|
imports: [TuiButtonModule, CommonModule, TuiSvgModule],
|
||||||
})
|
})
|
||||||
export class ServiceInterfaceComponent {
|
export class ServiceInterfaceListItemComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
@Input({ required: true, alias: 'serviceInterface' })
|
@Input({ required: true, alias: 'serviceInterfaceListItem' })
|
||||||
info!: ExtendedInterfaceInfo
|
info!: ExtendedInterfaceInfo
|
||||||
|
|
||||||
@Input()
|
@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',
|
selector: 'service-menu',
|
||||||
template: `
|
template: `
|
||||||
<h3 class="g-title">Menu</h3>
|
<h3 class="g-title">Menu</h3>
|
||||||
@for (menu of service | toMenu; track $index) {
|
@for (menu of pkg | toMenu; track $index) {
|
||||||
@if (menu.routerLink) {
|
@if (menu.routerLink) {
|
||||||
<a
|
<a
|
||||||
class="g-action"
|
class="g-action"
|
||||||
@@ -23,7 +23,7 @@ import { RouterLink } from '@angular/router'
|
|||||||
(click)="menu.action?.()"
|
(click)="menu.action?.()"
|
||||||
>
|
>
|
||||||
@if (menu.name === 'Outbound Proxy') {
|
@if (menu.name === 'Outbound Proxy') {
|
||||||
<div [style.color]="color">{{ proxy }}</div>
|
<div [style.color]="color">{{ pkg.outboundProxy || 'None' }}</div>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -35,22 +35,11 @@ import { RouterLink } from '@angular/router'
|
|||||||
})
|
})
|
||||||
export class ServiceMenuComponent {
|
export class ServiceMenuComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
service!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
get color(): string {
|
get color(): string {
|
||||||
return this.service.installed?.outboundProxy
|
return this.pkg.outboundProxy
|
||||||
? 'var(--tui-success-fill)'
|
? 'var(--tui-success-fill)'
|
||||||
: 'var(--tui-warning-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 { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { TuiProgressModule } from '@taiga-ui/kit'
|
import { TuiProgressModule } from '@taiga-ui/kit'
|
||||||
|
import { Progress } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { InstallingProgressPipe } from '../pipes/install-progress.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[progress]',
|
selector: '[progress]',
|
||||||
template: `
|
template: `
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
: {{ progress }}%
|
@if (progress | installingProgress; as decimal) {
|
||||||
<progress
|
: {{ decimal * 100 }}%
|
||||||
tuiProgressBar
|
<progress
|
||||||
new
|
tuiProgressBar
|
||||||
size="xs"
|
new
|
||||||
[style.color]="
|
size="xs"
|
||||||
progress === 100 ? 'var(--tui-positive)' : 'var(--tui-link)'
|
[style.color]="
|
||||||
"
|
progress === true ? 'var(--tui-positive)' : 'var(--tui-link)'
|
||||||
[value]="progress / 100"
|
"
|
||||||
></progress>
|
[value]="decimal * 100"
|
||||||
|
></progress>
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
styles: [':host { line-height: 2rem }'],
|
styles: [':host { line-height: 2rem }'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiProgressModule],
|
imports: [TuiProgressModule, InstallingProgressPipe],
|
||||||
})
|
})
|
||||||
export class ServiceProgressComponent {
|
export class ServiceProgressComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true }) progress!: Progress
|
||||||
progress = 0
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TuiLabelModule } from '@taiga-ui/core'
|
|||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'service-credential',
|
selector: 'service-property',
|
||||||
template: `
|
template: `
|
||||||
<label [style.flex]="1" [tuiLabel]="label">
|
<label [style.flex]="1" [tuiLabel]="label">
|
||||||
{{ masked ? mask : value }}
|
{{ masked ? mask : value }}
|
||||||
@@ -48,7 +48,7 @@ import { TuiButtonModule } from '@taiga-ui/experimental'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiButtonModule, TuiLabelModule],
|
imports: [TuiButtonModule, TuiLabelModule],
|
||||||
})
|
})
|
||||||
export class ServiceCredentialComponent {
|
export class ServicePropertyComponent {
|
||||||
@Input()
|
@Input()
|
||||||
label = ''
|
label = ''
|
||||||
|
|
||||||
@@ -5,22 +5,27 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
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({
|
@Component({
|
||||||
selector: 'service-status',
|
selector: 'service-status',
|
||||||
template: `
|
template: `
|
||||||
@if (installProgress) {
|
@if (installingInfo) {
|
||||||
<strong>
|
<strong>
|
||||||
Installing
|
Installing
|
||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
{{ installProgress | installProgress }}
|
{{ installingInfo.progress.overall | installingProgressString }}
|
||||||
</strong>
|
</strong>
|
||||||
} @else {
|
} @else {
|
||||||
{{ connected ? rendering.display : 'Unknown' }}
|
{{ 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>
|
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -35,18 +40,24 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
|||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, InstallProgressPipe],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
InstallingProgressDisplayPipe,
|
||||||
|
UnitConversionPipesModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ServiceStatusComponent {
|
export class ServiceStatusComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
rendering!: StatusRendering
|
rendering!: StatusRendering
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
installProgress?: InstallProgress
|
installingInfo?: InstallingInfo
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
connected = false
|
connected = false
|
||||||
|
|
||||||
|
@Input() sigtermTimeout?: string | null = null
|
||||||
|
|
||||||
@HostBinding('style.color')
|
@HostBinding('style.color')
|
||||||
get color(): string {
|
get color(): string {
|
||||||
if (!this.connected) return 'var(--tui-text-02)'
|
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 { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ServiceCredentialComponent } from '../components/credential.component'
|
import { ServicePropertyComponent } from '../components/property.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (loading$ | async) {
|
@if (loading$ | async) {
|
||||||
<tui-loader />
|
<tui-loader />
|
||||||
} @else {
|
} @else {
|
||||||
@for (cred of credentials | keyvalue: asIsOrder; track cred) {
|
@for (prop of properties | keyvalue: asIsOrder; track prop) {
|
||||||
<service-credential [label]="cred.key" [value]="cred.value" />
|
<service-property [label]="prop.key" [value]="prop.value" />
|
||||||
} @empty {
|
} @empty {
|
||||||
No credentials
|
No properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<button tuiButton iconLeft="tuiIconRefreshCwLarge" (click)="refresh()">
|
<button tuiButton iconLeft="tuiIconRefreshCwLarge" (click)="refresh()">
|
||||||
@@ -36,32 +36,32 @@ import { ServiceCredentialComponent } from '../components/credential.component'
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
ServiceCredentialComponent,
|
ServicePropertyComponent,
|
||||||
TuiLoaderModule,
|
TuiLoaderModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceCredentialsModal {
|
export class ServicePropertiesModal {
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
|
|
||||||
readonly id = inject<{ data: string }>(POLYMORPHEUS_CONTEXT).data
|
readonly id = inject<{ data: string }>(POLYMORPHEUS_CONTEXT).data
|
||||||
readonly loading$ = new BehaviorSubject(true)
|
readonly loading$ = new BehaviorSubject(true)
|
||||||
|
|
||||||
credentials: Record<string, string> = {}
|
properties: Record<string, string> = {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await this.getCredentials()
|
await this.getProperties()
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
await this.getCredentials()
|
await this.getProperties()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCredentials(): Promise<void> {
|
private async getProperties(): Promise<void> {
|
||||||
this.loading$.next(true)
|
this.loading$.next(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.credentials = await this.api.getPackageCredentials({ id: this.id })
|
this.properties = await this.api.getPackageProperties({ id: this.id })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { WithId } from '@start9labs/shared'
|
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({
|
@Pipe({
|
||||||
name: 'groupActions',
|
name: 'groupActions',
|
||||||
@@ -9,12 +10,12 @@ import { Action, PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
export class GroupActionsPipe implements PipeTransform {
|
export class GroupActionsPipe implements PipeTransform {
|
||||||
transform(
|
transform(
|
||||||
actions: PackageDataEntry['actions'],
|
actions: PackageDataEntry['actions'],
|
||||||
): Array<Array<WithId<Action>>> | null {
|
): Array<Array<WithId<ActionMetadata>>> | null {
|
||||||
if (!actions) return null
|
if (!actions) return null
|
||||||
|
|
||||||
const noGroup = 'noGroup'
|
const noGroup = 'noGroup'
|
||||||
const grouped = Object.entries(actions).reduce<
|
const grouped = Object.entries(actions).reduce<
|
||||||
Record<string, WithId<Action>[]>
|
Record<string, WithId<ActionMetadata>[]>
|
||||||
>((groups, [id, action]) => {
|
>((groups, [id, action]) => {
|
||||||
const actionWithId = { id, ...action }
|
const actionWithId = { id, ...action }
|
||||||
const groupKey = action.group || noGroup
|
const groupKey = action.group || noGroup
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
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({
|
@Pipe({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import {
|
import { ServiceInterfaceWithHostInfo } from '@start9labs/start-sdk/cjs/sdk/lib/types'
|
||||||
InterfaceInfo,
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
PackageDataEntry,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
export interface ExtendedInterfaceInfo extends InterfaceInfo {
|
export interface ExtendedInterfaceInfo extends ServiceInterfaceWithHostInfo {
|
||||||
id: string
|
id: string
|
||||||
icon: string
|
icon: string
|
||||||
color: string
|
color: string
|
||||||
@@ -17,8 +15,8 @@ export interface ExtendedInterfaceInfo extends InterfaceInfo {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class InterfaceInfoPipe implements PipeTransform {
|
export class InterfaceInfoPipe implements PipeTransform {
|
||||||
transform({ installed }: PackageDataEntry): ExtendedInterfaceInfo[] {
|
transform(pkg: PackageDataEntry): ExtendedInterfaceInfo[] {
|
||||||
return Object.entries(installed!.interfaceInfo).map(([id, val]) => {
|
return Object.entries(pkg.serviceInterfaces).map(([id, val]) => {
|
||||||
let color: string
|
let color: string
|
||||||
let icon: string
|
let icon: string
|
||||||
let typeDetail: string
|
let typeDetail: string
|
||||||
@@ -39,11 +37,6 @@ export class InterfaceInfoPipe implements PipeTransform {
|
|||||||
icon = 'tuiIconTerminalLarge'
|
icon = 'tuiIconTerminalLarge'
|
||||||
typeDetail = 'Application Program Interface (API)'
|
typeDetail = 'Application Program Interface (API)'
|
||||||
break
|
break
|
||||||
case 'other':
|
|
||||||
color = 'var(--tui-text-02)'
|
|
||||||
icon = 'tuiIconBoxLarge'
|
|
||||||
typeDetail = 'Unknown Interface Type'
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { CopyService, MarkdownComponent } from '@start9labs/shared'
|
|||||||
import { from } from 'rxjs'
|
import { from } from 'rxjs'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
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'
|
export const FALLBACK_URL = 'Not provided'
|
||||||
|
|
||||||
@@ -25,21 +26,22 @@ export class ToAdditionalPipe implements PipeTransform {
|
|||||||
private readonly copyService = inject(CopyService)
|
private readonly copyService = inject(CopyService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|
||||||
transform({ manifest, installed }: PackageDataEntry): AdditionalItem[] {
|
transform(pkg: PackageDataEntry): AdditionalItem[] {
|
||||||
|
const manifest = getManifest(pkg)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Installed',
|
name: 'Installed',
|
||||||
description: new Intl.DateTimeFormat('en-US', {
|
description: new Intl.DateTimeFormat('en-US', {
|
||||||
dateStyle: 'medium',
|
dateStyle: 'medium',
|
||||||
timeStyle: 'medium',
|
timeStyle: 'medium',
|
||||||
}).format(new Date(installed?.['installed-at'] || 0)),
|
}).format(new Date(pkg.installedAt || 0)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Git Hash',
|
name: 'Git Hash',
|
||||||
description: manifest['git-hash'] || 'Unknown',
|
description: manifest.gitHash || 'Unknown',
|
||||||
icon: manifest['git-hash'] ? 'tuiIconCopyLarge' : '',
|
icon: manifest.gitHash ? 'tuiIconCopyLarge' : '',
|
||||||
action: () =>
|
action: () =>
|
||||||
manifest['git-hash'] && this.copyService.copy(manifest['git-hash']),
|
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'License',
|
name: 'License',
|
||||||
@@ -49,19 +51,19 @@ export class ToAdditionalPipe implements PipeTransform {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Website',
|
name: 'Website',
|
||||||
description: manifest['marketing-site'] || FALLBACK_URL,
|
description: manifest.marketingSite || FALLBACK_URL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Source Repository',
|
name: 'Source Repository',
|
||||||
description: manifest['upstream-repo'],
|
description: manifest.upstreamRepo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Support Site',
|
name: 'Support Site',
|
||||||
description: manifest['support-site'] || FALLBACK_URL,
|
description: manifest.supportSite || FALLBACK_URL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Donation Link',
|
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'
|
} from 'src/app/apps/portal/modals/config.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import {
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
InstalledPackageInfo,
|
|
||||||
PackageDataEntry,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import { ProxyService } from 'src/app/services/proxy.service'
|
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 {
|
export interface ServiceMenu {
|
||||||
icon: string
|
icon: string
|
||||||
@@ -37,8 +35,8 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly proxyService = inject(ProxyService)
|
private readonly proxyService = inject(ProxyService)
|
||||||
|
|
||||||
transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] {
|
transform(pkg: PackageDataEntry): ServiceMenu[] {
|
||||||
const url = installed?.['marketplace-url']
|
const manifest = getManifest(pkg)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -55,11 +53,11 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'tuiIconKeyLarge',
|
icon: 'tuiIconKeyLarge',
|
||||||
name: 'Credentials',
|
name: 'Properties',
|
||||||
description: `Password, keys, or other credentials of interest`,
|
description: `Runtime information, credentials, and other values of interest`,
|
||||||
action: () =>
|
action: () =>
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(new PolymorpheusComponent(ServiceCredentialsModal), {
|
.open(new PolymorpheusComponent(ServicePropertiesModal), {
|
||||||
label: `${manifest.title} credentials`,
|
label: `${manifest.title} credentials`,
|
||||||
data: manifest.id,
|
data: manifest.id,
|
||||||
})
|
})
|
||||||
@@ -75,7 +73,11 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
icon: 'tuiIconShieldLarge',
|
icon: 'tuiIconShieldLarge',
|
||||||
name: 'Outbound Proxy',
|
name: 'Outbound Proxy',
|
||||||
description: `Proxy all outbound traffic from ${manifest.title}`,
|
description: `Proxy all outbound traffic from ${manifest.title}`,
|
||||||
action: () => this.setProxy(manifest, installed!),
|
action: () =>
|
||||||
|
this.proxyService.presentModalSetOutboundProxy(
|
||||||
|
pkg.outboundProxy,
|
||||||
|
manifest.id,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'tuiIconFileTextLarge',
|
icon: 'tuiIconFileTextLarge',
|
||||||
@@ -83,13 +85,13 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
description: `Raw, unfiltered logs`,
|
description: `Raw, unfiltered logs`,
|
||||||
routerLink: 'logs',
|
routerLink: 'logs',
|
||||||
},
|
},
|
||||||
url
|
pkg.marketplaceUrl
|
||||||
? {
|
? {
|
||||||
icon: 'tuiIconShoppingBagLarge',
|
icon: 'tuiIconShoppingBagLarge',
|
||||||
name: 'Marketplace Listing',
|
name: 'Marketplace Listing',
|
||||||
description: `View ${manifest.title} on the Marketplace`,
|
description: `View ${manifest.title} on the Marketplace`,
|
||||||
routerLink: `/portal/system/marketplace`,
|
routerLink: `/portal/system/marketplace`,
|
||||||
params: { url, id: manifest.id },
|
params: { url: pkg.marketplaceUrl, id: manifest.id },
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
icon: 'tuiIconShoppingBagLarge',
|
icon: 'tuiIconShoppingBagLarge',
|
||||||
@@ -125,15 +127,4 @@ export class ToMenuPipe implements PipeTransform {
|
|||||||
data: { pkgId: id },
|
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 { FormComponent } from 'src/app/apps/portal/components/form.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
Action,
|
|
||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageState,
|
PackageState,
|
||||||
@@ -26,10 +25,13 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|||||||
import { ServiceActionComponent } from '../components/action.component'
|
import { ServiceActionComponent } from '../components/action.component'
|
||||||
import { ServiceActionSuccessComponent } from '../components/action-success.component'
|
import { ServiceActionSuccessComponent } from '../components/action-success.component'
|
||||||
import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
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({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<ng-container *ngIf="pkg$ | async as pkg">
|
@if (pkg$ | async; as pkg) {
|
||||||
<section>
|
<section>
|
||||||
<h3 class="g-title">Standard Actions</h3>
|
<h3 class="g-title">Standard Actions</h3>
|
||||||
<button
|
<button
|
||||||
@@ -40,7 +42,7 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
|||||||
</section>
|
</section>
|
||||||
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
|
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
|
||||||
<h3 *ngIf="actionGroups.length" class="g-title">
|
<h3 *ngIf="actionGroups.length" class="g-title">
|
||||||
Actions for {{ pkg.manifest.title }}
|
Actions for {{ (pkg | toManifest).title }}
|
||||||
</h3>
|
</h3>
|
||||||
<div *ngFor="let group of actionGroups">
|
<div *ngFor="let group of actionGroups">
|
||||||
<button
|
<button
|
||||||
@@ -55,18 +57,18 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
|||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ServiceActionComponent, GroupActionsPipe],
|
imports: [CommonModule, ServiceActionComponent, GroupActionsPipe, ToManifestPipe],
|
||||||
})
|
})
|
||||||
export class ServiceActionsRoute {
|
export class ServiceActionsRoute {
|
||||||
private readonly id = getPkgId(inject(ActivatedRoute))
|
private readonly id = getPkgId(inject(ActivatedRoute))
|
||||||
|
|
||||||
readonly pkg$ = this.patch
|
readonly pkg$ = this.patch
|
||||||
.watch$('package-data', this.id)
|
.watch$('packageData', this.id)
|
||||||
.pipe(filter(pkg => pkg.state === PackageState.Installed))
|
.pipe(filter(pkg => pkg.stateInfo.state === PackageState.Installed))
|
||||||
|
|
||||||
readonly action = {
|
readonly action = {
|
||||||
icon: 'tuiIconTrash2Large',
|
icon: 'tuiIconTrash2Large',
|
||||||
@@ -85,7 +87,7 @@ export class ServiceActionsRoute {
|
|||||||
private readonly formDialog: FormDialogService,
|
private readonly formDialog: FormDialogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleAction(action: WithId<Action>) {
|
async handleAction(action: WithId<ActionMetadata>) {
|
||||||
if (action.disabled) {
|
if (action.disabled) {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(action.disabled, {
|
.open(action.disabled, {
|
||||||
@@ -94,11 +96,11 @@ export class ServiceActionsRoute {
|
|||||||
})
|
})
|
||||||
.subscribe()
|
.subscribe()
|
||||||
} else {
|
} else {
|
||||||
if (action['input-spec'] && !isEmptyObject(action['input-spec'])) {
|
if (action.input && !isEmptyObject(action.input)) {
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: action.name,
|
label: action.name,
|
||||||
data: {
|
data: {
|
||||||
spec: action['input-spec'],
|
spec: action.input,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Execute',
|
text: 'Execute',
|
||||||
@@ -128,13 +130,13 @@ export class ServiceActionsRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
||||||
const { title, alerts } = pkg.manifest
|
const { title, alerts, id } = getManifest(pkg)
|
||||||
|
|
||||||
let content =
|
let content =
|
||||||
alerts.uninstall ||
|
alerts.uninstall ||
|
||||||
`Uninstalling ${title} will permanently delete its data`
|
`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`
|
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ export class ServiceActionsRoute {
|
|||||||
try {
|
try {
|
||||||
const data = await this.embassyApi.executePackageAction({
|
const data = await this.embassyApi.executePackageAction({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
'action-id': actionId,
|
actionId,
|
||||||
input,
|
input,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
import { PatchDB } from 'patch-db-client'
|
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 { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<app-interfaces
|
<app-interface
|
||||||
*ngIf="interfaceInfo$ | async as interfaceInfo"
|
*ngIf="interfaceInfo$ | async as interfaceInfo"
|
||||||
[packageContext]="context"
|
[packageContext]="context"
|
||||||
[addressInfo]="interfaceInfo.addressInfo"
|
[serviceInterface]="interfaceInfo"
|
||||||
[isUi]="interfaceInfo.type === 'ui'"
|
|
||||||
/>
|
/>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, InterfacesComponent],
|
imports: [CommonModule, InterfaceComponent],
|
||||||
})
|
})
|
||||||
export class ServiceInterfaceRoute {
|
export class ServiceInterfaceRoute {
|
||||||
private readonly route = inject(ActivatedRoute)
|
private readonly route = inject(ActivatedRoute)
|
||||||
@@ -27,11 +28,17 @@ export class ServiceInterfaceRoute {
|
|||||||
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
|
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly interfaceInfo$ = inject(PatchDB<DataModel>).watch$(
|
readonly interfaceInfo$ = inject(PatchDB<DataModel>)
|
||||||
'package-data',
|
.watch$(
|
||||||
this.context.packageId,
|
'packageData',
|
||||||
'state-info',
|
this.context.packageId,
|
||||||
'interfaceInfo',
|
'serviceInterfaces',
|
||||||
this.context.interfaceId,
|
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')),
|
map(() => this.route.firstChild?.snapshot.paramMap?.get('pkgId')),
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap(id => this.patch.watch$('package-data', id)),
|
switchMap(id => this.patch.watch$('packageData', id)),
|
||||||
tap(pkg => {
|
tap(pkg => {
|
||||||
// if package disappears, navigate to list page
|
// if package disappears, navigate to list page
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
|
||||||
import { Manifest } from '@start9labs/marketplace'
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
import { getPkgId, isEmptyObject } from '@start9labs/shared'
|
import { isEmptyObject } from '@start9labs/shared'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { combineLatest, map, switchMap } from 'rxjs'
|
import { combineLatest, map, switchMap } from 'rxjs'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
@@ -15,15 +15,12 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
HealthCheckResult,
|
HealthCheckResult,
|
||||||
InstalledPackageInfo,
|
|
||||||
MainStatus,
|
MainStatus,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageState,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
import {
|
||||||
PackageStatus,
|
PackageStatus,
|
||||||
PrimaryRendering,
|
PrimaryRendering,
|
||||||
PrimaryStatus,
|
|
||||||
renderPkgStatus,
|
renderPkgStatus,
|
||||||
StatusRendering,
|
StatusRendering,
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} 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 { ServiceAdditionalComponent } from '../components/additional.component'
|
||||||
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
||||||
import { ServiceHealthChecksComponent } from '../components/health-checks.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 { ServiceMenuComponent } from '../components/menu.component'
|
||||||
import { ServiceProgressComponent } from '../components/progress.component'
|
import { ServiceProgressComponent } from '../components/progress.component'
|
||||||
import { ServiceStatusComponent } from '../components/status.component'
|
import { ServiceStatusComponent } from '../components/status.component'
|
||||||
@@ -40,43 +37,60 @@ import {
|
|||||||
PackageConfigData,
|
PackageConfigData,
|
||||||
ServiceConfigModal,
|
ServiceConfigModal,
|
||||||
} from 'src/app/apps/portal/modals/config.component'
|
} from 'src/app/apps/portal/modals/config.component'
|
||||||
import { ProgressDataPipe } from '../pipes/progress-data.pipe'
|
|
||||||
import { DependencyInfo } from '../types/dependency-info'
|
import { DependencyInfo } from '../types/dependency-info'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
const STATES = [
|
import { InstallingProgressPipe } from 'src/app/apps/portal/routes/service/pipes/install-progress.pipe'
|
||||||
PackageState.Installing,
|
|
||||||
PackageState.Updating,
|
|
||||||
PackageState.Restoring,
|
|
||||||
]
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (service$ | async; as service) {
|
@if (service$ | async; as service) {
|
||||||
@if (showProgress(service.pkg)) {
|
<h3 class="g-title">Status</h3>
|
||||||
@if (service.pkg | progressData; as progress) {
|
<service-status
|
||||||
<p [progress]="progress.downloadProgress">Downloading</p>
|
[connected]="!!(connected$ | async)"
|
||||||
<p [progress]="progress.validateProgress">Validating</p>
|
[installingInfo]="service.pkg.stateInfo.installingInfo"
|
||||||
<p [progress]="progress.unpackProgress">Unpacking</p>
|
[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 {
|
} @else {
|
||||||
<h3 class="g-title">Status</h3>
|
@if (
|
||||||
<service-status
|
service.pkg.stateInfo.state === 'installed' &&
|
||||||
[connected]="!!(connected$ | async)"
|
service.status.primary !== 'backing-up'
|
||||||
[installProgress]="service.pkg['install-progress']"
|
) {
|
||||||
[rendering]="$any(getRendering(service.status))"
|
@if (connected$ | async) {
|
||||||
/>
|
<service-actions
|
||||||
|
[pkg]="service.pkg"
|
||||||
|
[dependencies]="service.dependencies"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
@if (isInstalled(service.pkg) && (connected$ | async)) {
|
<service-interface-list
|
||||||
<service-actions
|
[pkg]="service.pkg"
|
||||||
[service]="service.pkg"
|
[status]="service.status"
|
||||||
[dependencies]="service.dependencies"
|
]
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
|
|
||||||
@if (isInstalled(service.pkg) && !isBackingUp(service.status)) {
|
@if (
|
||||||
<service-interfaces [service]="service" />
|
service.status.primary === 'running' && (health$ | async);
|
||||||
|
as checks
|
||||||
@if (isRunning(service.status) && (health$ | async); as checks) {
|
) {
|
||||||
<service-health-checks [checks]="checks" />
|
<service-health-checks [checks]="checks" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +98,8 @@ const STATES = [
|
|||||||
<service-dependencies [dependencies]="service.dependencies" />
|
<service-dependencies [dependencies]="service.dependencies" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<service-menu [service]="service.pkg" />
|
<service-menu [pkg]="service.pkg" />
|
||||||
<service-additional [service]="service.pkg" />
|
<service-additional [pkg]="service.pkg" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,17 +108,15 @@ const STATES = [
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|
||||||
ServiceProgressComponent,
|
ServiceProgressComponent,
|
||||||
ServiceStatusComponent,
|
ServiceStatusComponent,
|
||||||
ServiceActionsComponent,
|
ServiceActionsComponent,
|
||||||
ServiceInterfacesComponent,
|
ServiceInterfaceListComponent,
|
||||||
ServiceHealthChecksComponent,
|
ServiceHealthChecksComponent,
|
||||||
ServiceDependenciesComponent,
|
ServiceDependenciesComponent,
|
||||||
ServiceMenuComponent,
|
ServiceMenuComponent,
|
||||||
ServiceAdditionalComponent,
|
ServiceAdditionalComponent,
|
||||||
|
InstallingProgressPipe,
|
||||||
ProgressDataPipe,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceRoute {
|
export class ServiceRoute {
|
||||||
@@ -115,12 +127,12 @@ export class ServiceRoute {
|
|||||||
private readonly depErrorService = inject(DepErrorService)
|
private readonly depErrorService = inject(DepErrorService)
|
||||||
private readonly router = inject(Router)
|
private readonly router = inject(Router)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
|
||||||
readonly connected$ = inject(ConnectionService).connected$
|
readonly connected$ = inject(ConnectionService).connected$
|
||||||
|
|
||||||
readonly service$ = this.pkgId$.pipe(
|
readonly service$ = this.pkgId$.pipe(
|
||||||
switchMap(pkgId =>
|
switchMap(pkgId =>
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.patch.watch$('package-data', pkgId),
|
this.patch.watch$('packageData', pkgId),
|
||||||
this.depErrorService.getPkgDepErrors$(pkgId),
|
this.depErrorService.getPkgDepErrors$(pkgId),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@@ -132,9 +144,10 @@ export class ServiceRoute {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly health$ = this.pkgId$.pipe(
|
readonly health$ = this.pkgId$.pipe(
|
||||||
switchMap(pkgId =>
|
switchMap(pkgId =>
|
||||||
this.patch.watch$('package-data', pkgId, 'installed', 'status', 'main'),
|
this.patch.watch$('packageData', pkgId, 'status', 'main'),
|
||||||
),
|
),
|
||||||
map(toHealthCheck),
|
map(toHealthCheck),
|
||||||
)
|
)
|
||||||
@@ -143,53 +156,35 @@ export class ServiceRoute {
|
|||||||
return PrimaryRendering[primary]
|
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(
|
private getDepInfo(
|
||||||
{ installed, manifest }: PackageDataEntry,
|
pkg: PackageDataEntry,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
): DependencyInfo[] {
|
): DependencyInfo[] {
|
||||||
return installed
|
const manifest = getManifest(pkg)
|
||||||
? Object.keys(installed['current-dependencies'])
|
|
||||||
.filter(depId => !!manifest.dependencies[depId])
|
return Object.keys(pkg.currentDependencies)
|
||||||
.map(depId =>
|
.filter(id => !!manifest.dependencies[id])
|
||||||
this.getDepValues(installed, manifest, depId, depErrors),
|
.map(id => this.getDepValues(pkg, manifest, id, depErrors))
|
||||||
)
|
|
||||||
: []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDepValues(
|
private getDepValues(
|
||||||
pkgInstalled: InstalledPackageInfo,
|
pkg: PackageDataEntry,
|
||||||
pkgManifest: Manifest,
|
pkgManifest: Manifest,
|
||||||
depId: string,
|
depId: string,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
): DependencyInfo {
|
): DependencyInfo {
|
||||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||||
pkgInstalled,
|
pkg,
|
||||||
pkgManifest,
|
pkgManifest,
|
||||||
depId,
|
depId,
|
||||||
depErrors,
|
depErrors,
|
||||||
)
|
)
|
||||||
|
|
||||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
const depInfo = pkg.dependencyInfo[depId]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: depId,
|
id: depId,
|
||||||
version: pkgManifest.dependencies[depId].version, // do we want this version range?
|
version: pkg.currentDependencies[depId].versionRange,
|
||||||
title: depInfo?.title || depId,
|
title: depInfo?.title || depId,
|
||||||
icon: depInfo?.icon || '',
|
icon: depInfo?.icon || '',
|
||||||
errorText: errorText
|
errorText: errorText
|
||||||
@@ -205,12 +200,12 @@ export class ServiceRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDepErrors(
|
private getDepErrors(
|
||||||
pkgInstalled: InstalledPackageInfo,
|
pkg: PackageDataEntry,
|
||||||
pkgManifest: Manifest,
|
pkgManifest: Manifest,
|
||||||
depId: string,
|
depId: string,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
) {
|
) {
|
||||||
const depError = (depErrors[pkgManifest.id] as any)?.[depId] // @TODO fix
|
const depError = depErrors[pkgManifest.id]
|
||||||
|
|
||||||
let errorText: string | null = null
|
let errorText: string | null = null
|
||||||
let fixText: string | null = null
|
let fixText: string | null = null
|
||||||
@@ -220,18 +215,15 @@ export class ServiceRoute {
|
|||||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||||
errorText = 'Not installed'
|
errorText = 'Not installed'
|
||||||
fixText = 'Install'
|
fixText = 'Install'
|
||||||
fixAction = () =>
|
fixAction = () => this.fixDep(pkg, pkgManifest, 'install', depId)
|
||||||
this.fixDep(pkgInstalled, pkgManifest, 'install', depId)
|
|
||||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||||
errorText = 'Incorrect version'
|
errorText = 'Incorrect version'
|
||||||
fixText = 'Update'
|
fixText = 'Update'
|
||||||
fixAction = () =>
|
fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId)
|
||||||
this.fixDep(pkgInstalled, pkgManifest, 'update', depId)
|
|
||||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||||
errorText = 'Config not satisfied'
|
errorText = 'Config not satisfied'
|
||||||
fixText = 'Auto config'
|
fixText = 'Auto config'
|
||||||
fixAction = () =>
|
fixAction = () => this.fixDep(pkg, pkgManifest, 'configure', depId)
|
||||||
this.fixDep(pkgInstalled, pkgManifest, 'configure', depId)
|
|
||||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||||
errorText = 'Not running'
|
errorText = 'Not running'
|
||||||
fixText = 'Start'
|
fixText = 'Start'
|
||||||
@@ -250,7 +242,7 @@ export class ServiceRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fixDep(
|
async fixDep(
|
||||||
pkgInstalled: InstalledPackageInfo,
|
pkg: PackageDataEntry,
|
||||||
pkgManifest: Manifest,
|
pkgManifest: Manifest,
|
||||||
action: 'install' | 'update' | 'configure',
|
action: 'install' | 'update' | 'configure',
|
||||||
depId: string,
|
depId: string,
|
||||||
@@ -258,10 +250,10 @@ export class ServiceRoute {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'install':
|
case 'install':
|
||||||
case 'update':
|
case 'update':
|
||||||
return this.installDep(pkgManifest, depId)
|
return this.installDep(pkg, pkgManifest, depId)
|
||||||
case 'configure':
|
case 'configure':
|
||||||
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||||
label: `${pkgInstalled!['dependency-info'][depId].title} config`,
|
label: `${pkg.dependencyInfo[depId].title} config`,
|
||||||
data: {
|
data: {
|
||||||
pkgId: depId,
|
pkgId: depId,
|
||||||
dependentInfo: pkgManifest,
|
dependentInfo: pkgManifest,
|
||||||
@@ -270,13 +262,15 @@ export class ServiceRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
private async installDep(
|
||||||
const version = manifest.dependencies[depId].version
|
pkg: PackageDataEntry,
|
||||||
|
manifest: Manifest,
|
||||||
|
depId: string,
|
||||||
|
): Promise<void> {
|
||||||
const dependentInfo: DependentInfo = {
|
const dependentInfo: DependentInfo = {
|
||||||
id: manifest.id,
|
id: manifest.id,
|
||||||
title: manifest.title,
|
title: manifest.title,
|
||||||
version,
|
version: pkg.currentDependencies[depId].versionRange,
|
||||||
}
|
}
|
||||||
const navigationExtras: NavigationExtras = {
|
const navigationExtras: NavigationExtras = {
|
||||||
state: { dependentInfo },
|
state: { dependentInfo },
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export class BackupsStatusComponent {
|
|||||||
|
|
||||||
private get hasBackup(): boolean {
|
private get hasBackup(): boolean {
|
||||||
return (
|
return (
|
||||||
!!this.target['embassy-os'] &&
|
!!this.target.startOs &&
|
||||||
this.emver.compare(this.target['embassy-os'].version, '0.3.0') !== -1
|
this.emver.compare(this.target.startOs.version, '0.3.0') !== -1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
|
|||||||
Running
|
Running
|
||||||
</span>
|
</span>
|
||||||
<ng-template #notRunning>
|
<ng-template #notRunning>
|
||||||
{{ job.next | date : 'MMM d, y, h:mm a' }}
|
{{ job.next | date: 'MMM d, y, h:mm a' }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ job.name }}</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>
|
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
|
||||||
{{ job.target.name }}
|
{{ job.target.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>Packages: {{ job['package-ids'].length }}</td>
|
<td>Packages: {{ job.packageIds.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template #blank>
|
<ng-template #blank>
|
||||||
<tr><td colspan="5">You have no active or upcoming backup jobs</td></tr>
|
<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 {
|
export class BackupsUpcomingComponent {
|
||||||
readonly current$ = inject(PatchDB<DataModel>)
|
readonly current$ = inject(PatchDB<DataModel>)
|
||||||
.watch$('server-info', 'status-info', 'current-backup', 'job')
|
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
|
||||||
.pipe(map(job => job || {}))
|
.pipe(map(job => job || {}))
|
||||||
|
|
||||||
readonly upcoming$ = from(inject(ApiService).getBackupJobs({})).pipe(
|
readonly upcoming$ = from(inject(ApiService).getBackupJobs({})).pipe(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { firstValueFrom, map } from 'rxjs'
|
import { firstValueFrom, map } from 'rxjs'
|
||||||
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
interface Package {
|
interface Package {
|
||||||
id: string
|
id: string
|
||||||
@@ -90,16 +91,19 @@ export class BackupsBackupModal {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.pkgs = await firstValueFrom(
|
this.pkgs = await firstValueFrom(
|
||||||
this.patch.watch$('package-data').pipe(
|
this.patch.watch$('packageData').pipe(
|
||||||
map(pkgs =>
|
map(pkgs =>
|
||||||
Object.values(pkgs)
|
Object.values(pkgs)
|
||||||
.map(({ manifest: { id, title }, icon, state }) => ({
|
.map(pkg => {
|
||||||
id,
|
const { id, title } = getManifest(pkg)
|
||||||
title,
|
return {
|
||||||
icon,
|
id,
|
||||||
disabled: state !== PackageState.Installed,
|
title,
|
||||||
checked: false,
|
icon: pkg.icon,
|
||||||
}))
|
disabled: pkg.stateInfo.state !== PackageState.Installed,
|
||||||
|
checked: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
|
|||||||
(click)="selectPackages()"
|
(click)="selectPackages()"
|
||||||
>
|
>
|
||||||
Packages
|
Packages
|
||||||
<tui-badge
|
<tui-badge [appearance]="job.packageIds.length ? 'success' : 'warning'">
|
||||||
[appearance]="job['package-ids'].length ? 'success' : 'warning'"
|
{{ job.packageIds.length + ' selected' }}
|
||||||
>
|
|
||||||
{{ job['package-ids'].length + ' selected' }}
|
|
||||||
</tui-badge>
|
</tui-badge>
|
||||||
</button>
|
</button>
|
||||||
<tui-input name="cron" [(ngModel)]="job.cron">
|
<tui-input name="cron" [(ngModel)]="job.cron">
|
||||||
@@ -145,7 +143,7 @@ export class BackupsEditModal {
|
|||||||
|
|
||||||
selectPackages() {
|
selectPackages() {
|
||||||
this.dialogs.open<string[]>(BACKUP, BACKUP_OPTIONS).subscribe(id => {
|
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)' : ''"
|
[style.background]="selected[index] ? 'var(--tui-clear)' : ''"
|
||||||
>
|
>
|
||||||
<td><tui-checkbox [(ngModel)]="selected[index]"></tui-checkbox></td>
|
<td><tui-checkbox [(ngModel)]="selected[index]"></tui-checkbox></td>
|
||||||
<td>{{ run['started-at'] | date : 'medium' }}</td>
|
<td>{{ run.startedAt | date: 'medium' }}</td>
|
||||||
<td>
|
<td>{{ run.startedAt | duration: run.completedAt }} Minutes</td>
|
||||||
{{ run['started-at'] | duration : run['completed-at'] }} Minutes
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<tui-svg
|
<tui-svg
|
||||||
*ngIf="run.report | hasError; else noError"
|
*ngIf="run.report | hasError; else noError"
|
||||||
@@ -178,7 +176,7 @@ export class BackupsHistoryModal {
|
|||||||
label: 'Backup Report',
|
label: 'Backup Report',
|
||||||
data: {
|
data: {
|
||||||
report: run.report,
|
report: run.report,
|
||||||
timestamp: run['completed-at'],
|
timestamp: run.completedAt,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { EDIT } from './edit.component'
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<tui-notification>
|
<tui-notification>
|
||||||
Scheduling automatic backups is an excellent way to ensure your Embassy
|
Scheduling automatic backups is an excellent way to ensure your StartOS
|
||||||
data is safely backed up. Your Embassy will issue a notification whenever
|
data is safely backed up. StartOS will issue a notification whenever one
|
||||||
one of your scheduled backups succeeds or fails.
|
of your scheduled backups succeeds or fails.
|
||||||
<a
|
<a
|
||||||
href="https://docs.start9.com/latest/user-manual/backups/backup-jobs"
|
href="https://docs.start9.com/latest/user-manual/backups/backup-jobs"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -57,7 +57,7 @@ import { EDIT } from './edit.component'
|
|||||||
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
|
<tui-svg [src]="job.target.type | getBackupIcon"></tui-svg>
|
||||||
{{ job.target.name }}
|
{{ job.target.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>Packages: {{ job['package-ids'].length }}</td>
|
<td>Packages: {{ job.packageIds.length }}</td>
|
||||||
<td>{{ (job.cron | toHumanCron).message }}</td>
|
<td>{{ (job.cron | toHumanCron).message }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
@@ -143,7 +143,7 @@ export class BackupsJobsModal implements OnInit {
|
|||||||
data.name = job.name
|
data.name = job.name
|
||||||
data.target = job.target
|
data.target = job.target
|
||||||
data.cron = job.cron
|
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({
|
@Component({
|
||||||
template: `
|
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">
|
<div tuiGroup orientation="vertical" [style.width.%]="100">
|
||||||
<tui-checkbox-block
|
<tui-checkbox-block
|
||||||
*ngFor="let option of options"
|
*ngFor="let option of options"
|
||||||
[disabled]="option.installed || option['newer-eos']"
|
[disabled]="option.installed || option.newerStartOs"
|
||||||
[(ngModel)]="option.checked"
|
[(ngModel)]="option.checked"
|
||||||
>
|
>
|
||||||
<div [style.margin]="'0.75rem 0'">
|
<div [style.margin]="'0.75rem 0'">
|
||||||
<strong>{{ option.title }}</strong>
|
<strong>{{ option.title }}</strong>
|
||||||
<div>Version {{ option.version }}</div>
|
<div>Version {{ option.version }}</div>
|
||||||
<div>Backup made: {{ option.timestamp | date : 'medium' }}</div>
|
<div>Backup made: {{ option.timestamp | date: 'medium' }}</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="option | tuiMapper : toMessage as message"
|
*ngIf="option | tuiMapper: toMessage as message"
|
||||||
[style.color]="message.color"
|
[style.color]="message.color"
|
||||||
>
|
>
|
||||||
{{ message.text }}
|
{{ message.text }}
|
||||||
@@ -73,11 +73,11 @@ export class BackupsRecoverModal {
|
|||||||
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
|
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
|
||||||
|
|
||||||
readonly packageData$ = inject(PatchDB<DataModel>)
|
readonly packageData$ = inject(PatchDB<DataModel>)
|
||||||
.watch$('package-data')
|
.watch$('packageData')
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
|
|
||||||
readonly toMessage = (option: RecoverOption) => {
|
readonly toMessage = (option: RecoverOption) => {
|
||||||
if (option['newer-eos']) {
|
if (option.newerStartOs) {
|
||||||
return {
|
return {
|
||||||
text: `Unavailable. Backup was made on a newer version of StartOS.`,
|
text: `Unavailable. Backup was made on a newer version of StartOS.`,
|
||||||
color: 'var(--tui-error-fill)',
|
color: 'var(--tui-error-fill)',
|
||||||
@@ -98,7 +98,7 @@ export class BackupsRecoverModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get backups(): Record<string, PackageBackupInfo> {
|
get backups(): Record<string, PackageBackupInfo> {
|
||||||
return this.context.data.backupInfo['package-backups']
|
return this.context.data.backupInfo.packageBackups
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisabled(options: RecoverOption[]): boolean {
|
isDisabled(options: RecoverOption[]): boolean {
|
||||||
@@ -109,11 +109,13 @@ export class BackupsRecoverModal {
|
|||||||
const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
|
const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id)
|
||||||
const loader = this.loader.open('Initializing...').subscribe()
|
const loader = this.loader.open('Initializing...').subscribe()
|
||||||
|
|
||||||
|
const { targetId, password } = this.context.data
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.restorePackages({
|
await this.api.restorePackages({
|
||||||
ids,
|
ids,
|
||||||
'target-id': this.context.data.targetId,
|
targetId,
|
||||||
password: this.context.data.password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.context.$implicit.complete()
|
this.context.$implicit.complete()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class BackupsTargetModal {
|
|||||||
isDisabled(target: BackupTarget): boolean {
|
isDisabled(target: BackupTarget): boolean {
|
||||||
return (
|
return (
|
||||||
!target.mountable ||
|
!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 {
|
import {
|
||||||
unionSelectKey,
|
unionSelectKey,
|
||||||
unionValueKey,
|
unionValueKey,
|
||||||
} from '@start9labs/start-sdk/lib/config/configTypes'
|
} from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
|
||||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||||
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule, TuiFadeModule } from '@taiga-ui/experimental'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
@@ -55,7 +55,7 @@ import { BackupsTargetsComponent } from '../components/targets.component'
|
|||||||
<div class="g-hidden-scrollbar" tuiFade>
|
<div class="g-hidden-scrollbar" tuiFade>
|
||||||
<table
|
<table
|
||||||
class="g-table"
|
class="g-table"
|
||||||
[backupsPhysical]="targets?.['unknown-disks'] || null"
|
[backupsPhysical]="targets?.unknownDisks || null"
|
||||||
(add)="addPhysical($event)"
|
(add)="addPhysical($event)"
|
||||||
></table>
|
></table>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +106,7 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
this.targets = await this.api.getBackupTargets({})
|
this.targets = await this.api.getBackupTargets({})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
this.targets = { 'unknown-disks': [], saved: [] }
|
this.targets = { unknownDisks: [], saved: [] }
|
||||||
} finally {
|
} finally {
|
||||||
this.loading$.next(false)
|
this.loading$.next(false)
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
}).then(response => {
|
}).then(response => {
|
||||||
this.setTargets(
|
this.setTargets(
|
||||||
this.targets?.saved.concat(response),
|
this.targets?.saved.concat(response),
|
||||||
this.targets?.['unknown-disks'].filter(a => a !== disk),
|
this.targets?.unknownDisks.filter(a => a !== disk),
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
@@ -225,9 +225,9 @@ export class BackupsTargetsModal implements OnInit {
|
|||||||
|
|
||||||
private setTargets(
|
private setTargets(
|
||||||
saved: BackupTarget[] = this.targets?.saved || [],
|
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) {
|
private async getSpec(target: BackupTarget) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class ToOptionsPipe implements PipeTransform {
|
|||||||
id,
|
id,
|
||||||
installed: !!packageData[id],
|
installed: !!packageData[id],
|
||||||
checked: false,
|
checked: false,
|
||||||
'newer-eos': this.compare(packageBackups[id].osVersion),
|
newerStartOs: this.compare(packageBackups[id].osVersion),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class BackupsCreateService {
|
|||||||
const loader = this.loader.open('Beginning backup...').subscribe()
|
const loader = this.loader.open('Beginning backup...').subscribe()
|
||||||
|
|
||||||
await this.api
|
await this.api
|
||||||
.createBackup({ 'target-id': targetId, 'package-ids': pkgIds })
|
.createBackup({ targetId, packageIds: pkgIds })
|
||||||
.finally(() => loader.unsubscribe())
|
.finally(() => loader.unsubscribe())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class BackupsRestoreService {
|
|||||||
this.getRecoverData(
|
this.getRecoverData(
|
||||||
target.id,
|
target.id,
|
||||||
password,
|
password,
|
||||||
target['embassy-os']?.['password-hash'] || '',
|
target.startOs?.passwordHash || '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
take(1),
|
take(1),
|
||||||
@@ -73,7 +73,7 @@ export class BackupsRestoreService {
|
|||||||
const loader = this.loader.open('Decrypting drive...').subscribe()
|
const loader = this.loader.open('Decrypting drive...').subscribe()
|
||||||
|
|
||||||
return this.api
|
return this.api
|
||||||
.getBackupInfo({ 'target-id': targetId, password })
|
.getBackupInfo({ targetId, password })
|
||||||
.finally(() => loader.unsubscribe())
|
.finally(() => loader.unsubscribe())
|
||||||
}),
|
}),
|
||||||
catchError(e => {
|
catchError(e => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
unionSelectKey,
|
unionSelectKey,
|
||||||
unionValueKey,
|
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'
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
|
|
||||||
export type BackupConfig =
|
export type BackupConfig =
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ export interface RecoverOption extends PackageBackupInfo {
|
|||||||
id: string
|
id: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
installed: boolean
|
installed: boolean
|
||||||
'newer-eos': boolean
|
newerStartOs: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
|
||||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
|
||||||
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
|
import { Variants } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/variants'
|
||||||
|
|
||||||
export const dropboxSpec = Config.of({
|
export const dropboxSpec = Config.of({
|
||||||
name: Value.text({
|
name: Value.text({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export class BackupJobBuilder {
|
|||||||
name: string
|
name: string
|
||||||
target: BackupTarget
|
target: BackupTarget
|
||||||
cron: string
|
cron: string
|
||||||
'package-ids': string[]
|
packageIds: string[]
|
||||||
now = false
|
now = false
|
||||||
|
|
||||||
constructor(readonly job: Partial<BackupJob>) {
|
constructor(readonly job: Partial<BackupJob>) {
|
||||||
@@ -12,7 +12,7 @@ export class BackupJobBuilder {
|
|||||||
this.name = name || ''
|
this.name = name || ''
|
||||||
this.target = target || ({} as BackupTarget)
|
this.target = target || ({} as BackupTarget)
|
||||||
this.cron = cron || '0 2 * * *'
|
this.cron = cron || '0 2 * * *'
|
||||||
this['package-ids'] = job['package-ids'] || []
|
this.packageIds = job.packageIds || []
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCreate(): RR.CreateBackupJobReq {
|
buildCreate(): RR.CreateBackupJobReq {
|
||||||
@@ -20,9 +20,9 @@ export class BackupJobBuilder {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
'target-id': target.id,
|
targetId: target.id,
|
||||||
cron,
|
cron,
|
||||||
'package-ids': this['package-ids'],
|
packageIds: this.packageIds,
|
||||||
now,
|
now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,9 @@ export class BackupJobBuilder {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
'target-id': target.id,
|
targetId: target.id,
|
||||||
cron,
|
cron,
|
||||||
'package-ids': this['package-ids'],
|
packageIds: this.packageIds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ import {
|
|||||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
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 { dryUpdate } from 'src/app/util/dry-update'
|
||||||
import { MarketplaceAlertsService } from '../services/alerts.service'
|
import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||||
|
import { ToManifestPipe } from 'src/app/apps/portal/pipes/to-manifest'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-controls',
|
selector: 'marketplace-controls',
|
||||||
@@ -45,8 +46,11 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
|||||||
>
|
>
|
||||||
View Installed
|
View Installed
|
||||||
</button>
|
</button>
|
||||||
@if (installed) {
|
@if (
|
||||||
@switch (localVersion | compareEmver: pkg.manifest.version) {
|
localPkg.stateInfo.state === 'installed' && (localPkg | toManifest);
|
||||||
|
as localManifest
|
||||||
|
) {
|
||||||
|
@switch (localManifest.version | compareEmver: pkg.manifest.version) {
|
||||||
@case (1) {
|
@case (1) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
@@ -94,7 +98,7 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
|||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, EmverPipesModule, TuiButtonModule],
|
imports: [CommonModule, EmverPipesModule, TuiButtonModule, ToManifestPipe],
|
||||||
})
|
})
|
||||||
export class MarketplaceControlsComponent {
|
export class MarketplaceControlsComponent {
|
||||||
private readonly alerts = inject(MarketplaceAlertsService)
|
private readonly alerts = inject(MarketplaceAlertsService)
|
||||||
@@ -118,18 +122,10 @@ export class MarketplaceControlsComponent {
|
|||||||
|
|
||||||
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
|
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
|
||||||
|
|
||||||
get installed(): boolean {
|
|
||||||
return this.localPkg?.state === PackageState.Installed
|
|
||||||
}
|
|
||||||
|
|
||||||
get localVersion(): string {
|
|
||||||
return this.localPkg?.manifest.version || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async tryInstall() {
|
async tryInstall() {
|
||||||
const current = await firstValueFrom(this.marketplace.getSelectedHost$())
|
const current = await firstValueFrom(this.marketplace.getSelectedHost$())
|
||||||
const url = this.url || current.url
|
const url = this.url || current.url
|
||||||
const originalUrl = this.localPkg?.installed?.['marketplace-url'] || ''
|
const originalUrl = this.localPkg?.marketplaceUrl || ''
|
||||||
|
|
||||||
if (!this.localPkg) {
|
if (!this.localPkg) {
|
||||||
if (await this.alerts.alertInstall(this.pkg)) this.install(url)
|
if (await this.alerts.alertInstall(this.pkg)) this.install(url)
|
||||||
@@ -144,9 +140,11 @@ export class MarketplaceControlsComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localManifest = getManifest(this.localPkg)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasCurrentDeps(this.localPkg) &&
|
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
|
||||||
this.emver.compare(this.localVersion, this.pkg.manifest.version) !== 0
|
this.emver.compare(localManifest.version, this.pkg.manifest.version) !== 0
|
||||||
) {
|
) {
|
||||||
this.dryInstall(url)
|
this.dryInstall(url)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component, inject, Input } from '@angular/core'
|
import { Component, inject, Input } from '@angular/core'
|
||||||
import { NgIf } from '@angular/common'
|
|
||||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export class MarketplaceRegistryModal {
|
|||||||
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
|
private readonly hosts$ = inject(PatchDB<DataModel>).watch$(
|
||||||
'ui',
|
'ui',
|
||||||
'marketplace',
|
'marketplace',
|
||||||
'known-hosts',
|
'knownHosts',
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly stores$ = combineLatest([
|
readonly stores$ = combineLatest([
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export class ToLocalPipe implements PipeTransform {
|
|||||||
private readonly patch = inject(PatchDB<DataModel>)
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
|
|
||||||
transform(id: string): Observable<PackageDataEntry> {
|
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> {
|
async alertMarketplace(url: string, originalUrl: string): Promise<boolean> {
|
||||||
const marketplaces = await firstValueFrom(this.marketplace$)
|
const marketplaces = await firstValueFrom(this.marketplace$)
|
||||||
const name = marketplaces['known-hosts'][url]?.name || url
|
const name = marketplaces.knownHosts[url]?.name || url
|
||||||
const source = marketplaces['known-hosts'][originalUrl]?.name || originalUrl
|
const source = marketplaces.knownHosts[originalUrl]?.name || originalUrl
|
||||||
const message = source ? `installed from ${source}` : 'side loaded'
|
const message = source ? `installed from ${source}` : 'side loaded'
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
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 { TuiDialogOptions } from '@taiga-ui/core'
|
||||||
import { TuiPromptData } from '@taiga-ui/kit'
|
import { TuiPromptData } from '@taiga-ui/kit'
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { toRouterLink } from '../../../utils/to-router-link'
|
|||||||
selector: '[notificationItem]',
|
selector: '[notificationItem]',
|
||||||
template: `
|
template: `
|
||||||
<td><ng-content /></td>
|
<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">
|
<td [style.color]="color">
|
||||||
<tui-svg [style.color]="color" [src]="icon"></tui-svg>
|
<tui-svg [style.color]="color" [src]="icon"></tui-svg>
|
||||||
{{ notificationItem.title }}
|
{{ notificationItem.title }}
|
||||||
@@ -70,8 +70,9 @@ export class NotificationItemComponent {
|
|||||||
get manifest$(): Observable<Manifest> {
|
get manifest$(): Observable<Manifest> {
|
||||||
return this.patch
|
return this.patch
|
||||||
.watch$(
|
.watch$(
|
||||||
'package-data',
|
'packageData',
|
||||||
this.notificationItem['package-id'] || '',
|
this.notificationItem.packageId || '',
|
||||||
|
'stateInfo',
|
||||||
'manifest',
|
'manifest',
|
||||||
)
|
)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import { SettingsUpdateComponent } from './update.component'
|
|||||||
selector: 'settings-menu',
|
selector: 'settings-menu',
|
||||||
template: `
|
template: `
|
||||||
<ng-container *ngIf="server$ | async as server; else loading">
|
<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">
|
<section *ngFor="let cat of service.settings | keyvalue: asIsOrder">
|
||||||
<h3 class="g-title" (click)="addClick(cat.key)">{{ cat.key }}</h3>
|
<h3 class="g-title" (click)="addClick(cat.key)">{{ cat.key }}</h3>
|
||||||
<settings-update
|
<settings-update
|
||||||
*ngIf="cat.key === 'General'"
|
*ngIf="cat.key === 'General'"
|
||||||
[updated]="server['status-info'].updated"
|
[updated]="server.statusInfo.updated"
|
||||||
/>
|
/>
|
||||||
<ng-container *ngFor="let btn of cat.value">
|
<ng-container *ngFor="let btn of cat.value">
|
||||||
<settings-button [button]="btn">
|
<settings-button [button]="btn">
|
||||||
@@ -32,13 +32,7 @@ import { SettingsUpdateComponent } from './update.component'
|
|||||||
: 'var(--tui-success-fill)'
|
: 'var(--tui-success-fill)'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{ server.network.outboundProxy || 'None' }}
|
||||||
!server.network.outboundProxy
|
|
||||||
? 'None'
|
|
||||||
: server.network.outboundProxy === 'primary'
|
|
||||||
? 'System Primary'
|
|
||||||
: server.network.outboundProxy.proxyId
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</settings-button>
|
</settings-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -76,7 +70,7 @@ export class SettingsMenuComponent {
|
|||||||
private readonly clientStorageService = inject(ClientStorageService)
|
private readonly clientStorageService = inject(ClientStorageService)
|
||||||
private readonly alerts = inject(TuiAlertService)
|
private readonly alerts = inject(TuiAlertService)
|
||||||
|
|
||||||
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
|
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
|
||||||
readonly service = inject(SettingsService)
|
readonly service = inject(SettingsService)
|
||||||
|
|
||||||
manageClicks = 0
|
manageClicks = 0
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import { EOSService } from 'src/app/services/eos.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsUpdateModal {
|
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))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(([version, notes]) => ({
|
.map(([version, notes]) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
|
||||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
|
||||||
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
|
import { Variants } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/variants'
|
||||||
import { Proxy } from 'src/app/services/patch-db/data-model'
|
import { Proxy } from 'src/app/services/patch-db/data-model'
|
||||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||||
|
|
||||||
@@ -59,38 +59,18 @@ function getStrategyUnion(proxies: Proxy[]) {
|
|||||||
proxy: {
|
proxy: {
|
||||||
name: 'Proxy',
|
name: 'Proxy',
|
||||||
spec: Config.of({
|
spec: Config.of({
|
||||||
proxyStrategy: Value.union(
|
proxyId: Value.select({
|
||||||
{
|
name: 'Select Proxy',
|
||||||
name: 'Proxy Strategy',
|
required: { default: null },
|
||||||
required: { default: 'primary' },
|
values: inboundProxies,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStart9ToSpec(proxies: Proxy[]) {
|
export function getStart9ToSpec(proxies: Proxy[]) {
|
||||||
return configBuilderToSpec(
|
return configBuilderToSpec(
|
||||||
Config.of({
|
Config.of({
|
||||||
strategy: getStrategyUnion(proxies),
|
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(
|
return configBuilderToSpec(
|
||||||
Config.of({
|
Config.of({
|
||||||
hostname: Value.text({
|
hostname: Value.text({
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class SettingsDomainsComponent {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|
||||||
readonly domains$ = this.patch.watch$('server-info', 'network').pipe(
|
readonly domains$ = this.patch.watch$('serverInfo', 'network').pipe(
|
||||||
map(network => {
|
map(network => {
|
||||||
const start9ToSubdomain = network.start9ToSubdomain
|
const start9ToSubdomain = network.start9ToSubdomain
|
||||||
const start9To = !start9ToSubdomain
|
const start9To = !start9ToSubdomain
|
||||||
@@ -103,7 +103,7 @@ export class SettingsDomainsComponent {
|
|||||||
|
|
||||||
async add() {
|
async add() {
|
||||||
const proxies = await firstValueFrom(
|
const proxies = await firstValueFrom(
|
||||||
this.patch.watch$('server-info', 'network', 'proxies'),
|
this.patch.watch$('serverInfo', 'network', 'proxies'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
||||||
@@ -128,7 +128,7 @@ export class SettingsDomainsComponent {
|
|||||||
|
|
||||||
async claim() {
|
async claim() {
|
||||||
const proxies = await firstValueFrom(
|
const proxies = await firstValueFrom(
|
||||||
this.patch.watch$('server-info', 'network', 'proxies'),
|
this.patch.watch$('serverInfo', 'network', 'proxies'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
||||||
@@ -150,13 +150,11 @@ export class SettingsDomainsComponent {
|
|||||||
|
|
||||||
this.formDialog.open(FormComponent, options)
|
this.formDialog.open(FormComponent, options)
|
||||||
}
|
}
|
||||||
|
// @TODO figure out how to get types here
|
||||||
private getNetworkStrategy(strategy: any) {
|
private getNetworkStrategy(strategy: any) {
|
||||||
const { ipStrategy, proxyStrategy = {} } = strategy.unionValueKey
|
return strategy.unionSelectKey === 'local'
|
||||||
const { unionSelectKey, unionValueKey = {} } = proxyStrategy
|
? { ipStrategy: strategy.unionValueKey.ipStrategy }
|
||||||
const proxyId = unionSelectKey === 'primary' ? null : unionValueKey.proxyId
|
: { proxy: strategy.unionValueKey.proxyId }
|
||||||
|
|
||||||
return strategy.unionSelectKey === 'local' ? { ipStrategy } : { proxyId }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteDomain(hostname?: string) {
|
private async deleteDomain(hostname?: string) {
|
||||||
@@ -174,7 +172,7 @@ export class SettingsDomainsComponent {
|
|||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// @TODO figure out how to get types here
|
||||||
private async claimDomain({ strategy }: any): Promise<boolean> {
|
private async claimDomain({ strategy }: any): Promise<boolean> {
|
||||||
const loader = this.loader.open('Saving...').subscribe()
|
const loader = this.loader.open('Saving...').subscribe()
|
||||||
const networkStrategy = this.getNetworkStrategy(strategy)
|
const networkStrategy = this.getNetworkStrategy(strategy)
|
||||||
@@ -189,7 +187,7 @@ export class SettingsDomainsComponent {
|
|||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// @TODO figure out how to get types here
|
||||||
private async save({ provider, strategy, hostname }: any): Promise<boolean> {
|
private async save({ provider, strategy, hostname }: any): Promise<boolean> {
|
||||||
const loader = this.loader.open('Saving...').subscribe()
|
const loader = this.loader.open('Saving...').subscribe()
|
||||||
const name = provider.unionSelectKey
|
const name = provider.unionSelectKey
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import { TuiDialogService } from '@taiga-ui/core'
|
|||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { TuiInputModule } from '@taiga-ui/kit'
|
import { TuiInputModule } from '@taiga-ui/kit'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
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 { PatchDB } from 'patch-db-client'
|
||||||
import { switchMap } from 'rxjs'
|
import { switchMap } from 'rxjs'
|
||||||
import { FormModule } from 'src/app/common/form/form.module'
|
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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormService } from 'src/app/services/form.service'
|
import { FormService } from 'src/app/services/form.service'
|
||||||
import { EmailInfoComponent } from './info.component'
|
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({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -84,7 +84,7 @@ export class SettingsEmailComponent {
|
|||||||
testAddress = ''
|
testAddress = ''
|
||||||
readonly spec: Promise<InputSpec> = configBuilderToSpec(customSmtp)
|
readonly spec: Promise<InputSpec> = configBuilderToSpec(customSmtp)
|
||||||
readonly form$ = this.patch
|
readonly form$ = this.patch
|
||||||
.watch$('server-info', 'smtp')
|
.watch$('serverInfo', 'smtp')
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(async value =>
|
switchMap(async value =>
|
||||||
this.formService.createForm(await this.spec, value),
|
this.formService.createForm(await this.spec, value),
|
||||||
|
|||||||
@@ -93,37 +93,6 @@ export class SettingsExperimentalComponent {
|
|||||||
.subscribe(() => this.resetTor(this.wipe))
|
.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) {
|
private async resetTor(wipeState: boolean) {
|
||||||
const loader = this.loader.open('Resetting Tor...').subscribe()
|
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 { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
|
||||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
|
||||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||||
import { TuiPromptData } from '@taiga-ui/kit'
|
import { TuiPromptData } from '@taiga-ui/kit'
|
||||||
|
|
||||||
@@ -27,8 +27,6 @@ export const wireguardSpec = Config.of({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export type WireguardSpec = typeof wireguardSpec.validator._TYPE
|
export type WireguardSpec = typeof wireguardSpec.validator._TYPE
|
||||||
export type ProxyUpdate = Partial<{
|
export type ProxyUpdate = {
|
||||||
name: string
|
name: string
|
||||||
primaryInbound: true
|
}
|
||||||
primaryOutbound: true
|
|
||||||
}>
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
|
||||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import {
|
import {
|
||||||
TuiDataListModule,
|
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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DELETE_OPTIONS, ProxyUpdate } from './constants'
|
import { DELETE_OPTIONS, ProxyUpdate } from './constants'
|
||||||
|
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'proxies-menu',
|
selector: 'proxies-menu',
|
||||||
@@ -45,23 +45,6 @@ import { DELETE_OPTIONS, ProxyUpdate } from './constants'
|
|||||||
</tui-hosted-dropdown>
|
</tui-hosted-dropdown>
|
||||||
<ng-template #dropdown>
|
<ng-template #dropdown>
|
||||||
<tui-data-list>
|
<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>
|
<button tuiOption (click)="rename()">Rename</button>
|
||||||
<tui-opt-group>
|
<tui-opt-group>
|
||||||
<button tuiOption (click)="delete()">Delete</button>
|
<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() {
|
async rename() {
|
||||||
const spec = { name: 'Name', required: { default: this.proxy.name } }
|
const spec = { name: 'Name', required: { default: this.proxy.name } }
|
||||||
const name = await Value.text(spec).build({} as any)
|
const name = await Value.text(spec).build({} as any)
|
||||||
@@ -137,4 +106,18 @@ export class ProxiesMenuComponent {
|
|||||||
|
|
||||||
this.formDialog.open(FormComponent, options)
|
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 { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
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 { TuiButtonModule } from '@taiga-ui/experimental'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,7 @@ export class SettingsProxiesComponent {
|
|||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
|
||||||
readonly proxies$ = inject(PatchDB<DataModel>).watch$(
|
readonly proxies$ = inject(PatchDB<DataModel>).watch$(
|
||||||
'server-info',
|
'serverInfo',
|
||||||
'network',
|
'network',
|
||||||
'proxies',
|
'proxies',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { ProxiesMenuComponent } from './menu.component'
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Primary</th>
|
|
||||||
<th>Used By</th>
|
<th>Used By</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -28,21 +27,8 @@ import { ProxiesMenuComponent } from './menu.component'
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let proxy of proxies">
|
<tr *ngFor="let proxy of proxies">
|
||||||
<td>{{ proxy.name }}</td>
|
<td>{{ proxy.name }}</td>
|
||||||
<td>{{ proxy.createdAt | date : 'short' }}</td>
|
<td>{{ proxy.createdAt | date: 'short' }}</td>
|
||||||
<td>{{ proxy.type }}</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>
|
<td>
|
||||||
<button
|
<button
|
||||||
*ngIf="getLength(proxy); else unused"
|
*ngIf="getLength(proxy); else unused"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
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({
|
@Pipe({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
name: 'primaryIp',
|
name: 'primaryIp',
|
||||||
})
|
})
|
||||||
export class PrimaryIpPipe implements PipeTransform {
|
export class PrimaryIpPipe implements PipeTransform {
|
||||||
transform(ipInfo: IpInfo): string {
|
transform(hostnames: HostnameInfo[]): string {
|
||||||
return Object.values(ipInfo)
|
return (
|
||||||
.filter(iface => iface.ipv4)
|
hostnames.map(
|
||||||
.sort((a, b) => (a.wireless ? -1 : 1))[0].ipv4!
|
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">
|
<ng-container *ngIf="server$ | async as server">
|
||||||
<router-info [enabled]="!server.network.wanConfig.upnp" />
|
<router-info [enabled]="!server.network.wanConfig.upnp" />
|
||||||
<table
|
<table
|
||||||
*ngIf="server.ui.ipInfo | primaryIp as ip"
|
*ngIf="server.ui | primaryIp as ip"
|
||||||
tuiTextfieldAppearance="unstyled"
|
tuiTextfieldAppearance="unstyled"
|
||||||
tuiTextfieldSize="m"
|
tuiTextfieldSize="m"
|
||||||
[tuiTextfieldLabelOutside]="true"
|
[tuiTextfieldLabelOutside]="true"
|
||||||
@@ -65,5 +65,5 @@ import { RouterPortComponent } from './table.component'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsRouterComponent {
|
export class SettingsRouterComponent {
|
||||||
readonly server$ = inject(PatchDB<DataModel>).watch$('server-info')
|
readonly server$ = inject(PatchDB<DataModel>).watch$('serverInfo')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
SimpleChanges,
|
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { CopyService, ErrorService, LoadingService } from '@start9labs/shared'
|
import { CopyService, ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ export class SettingsSessionsComponent {
|
|||||||
}))
|
}))
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b['last-active']).valueOf() -
|
new Date(b.lastActive).valueOf() -
|
||||||
new Date(a['last-active']).valueOf(),
|
new Date(a.lastActive).valueOf(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ import { FormsModule } from '@angular/forms'
|
|||||||
[ngModel]="selected$.value.includes(session)"
|
[ngModel]="selected$.value.includes(session)"
|
||||||
(ngModelChange)="onToggle(session)"
|
(ngModelChange)="onToggle(session)"
|
||||||
/>
|
/>
|
||||||
{{ session['user-agent'] }}
|
{{ session.userAgent }}
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="session.metadata.platforms | platformInfo as info">
|
<td *ngIf="session.metadata.platforms | platformInfo as info">
|
||||||
<tui-icon [icon]="info.icon"></tui-icon>
|
<tui-icon [icon]="info.icon"></tui-icon>
|
||||||
{{ info.name }}
|
{{ info.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ session['last-active'] }}</td>
|
<td>{{ session.lastActive }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template #loading>
|
<ng-template #loading>
|
||||||
<tr *ngFor="let _ of single ? [''] : ['', '']">
|
<tr *ngFor="let _ of single ? [''] : ['', '']">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { TuiForModule } from '@taiga-ui/cdk'
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let key of keys; else: loading">
|
<tr *ngFor="let key of keys; else: loading">
|
||||||
<td>{{ key.hostname }}</td>
|
<td>{{ key.hostname }}</td>
|
||||||
<td>{{ key['created-at'] | date: 'medium' }}</td>
|
<td>{{ key.createdAt | date: 'medium' }}</td>
|
||||||
<td>{{ key.alg }}</td>
|
<td>{{ key.alg }}</td>
|
||||||
<td>{{ key.fingerprint }}</td>
|
<td>{{ key.fingerprint }}</td>
|
||||||
<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 { AvailableWifi } from 'src/app/services/api/api.types'
|
||||||
import { RR } 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 {
|
export function parseWifi(res: RR.GetWifiRes): WifiData {
|
||||||
return {
|
return {
|
||||||
available: res['available-wifi'],
|
available: res.availableWifi,
|
||||||
known: Object.entries(res.ssids).map(([ssid, strength]) => ({
|
known: Object.entries(res.ssids).map(([ssid, strength]) => ({
|
||||||
ssid,
|
ssid,
|
||||||
strength,
|
strength,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user