mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
feat: use routes for service sections (#2502)
* feat: use routes for service sections * chore: fix comment
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
{{ badge }}
|
||||
</tui-badge-notification>
|
||||
<tui-svg
|
||||
*ngIf="icon.startsWith('tuiIcon'); else url"
|
||||
*ngIf="icon?.startsWith('tuiIcon'); else url"
|
||||
class="icon"
|
||||
[src]="icon"
|
||||
></tui-svg>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#rla="routerLinkActive"
|
||||
class="tab"
|
||||
routerLinkActive="tab_active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="tab.routerLink"
|
||||
>
|
||||
<tui-svg
|
||||
@@ -28,7 +27,7 @@
|
||||
iconLeft="tuiIconClose"
|
||||
appearance="icon"
|
||||
class="close"
|
||||
(click.stop.prevent)="removeTab(tab, rla.isActive)"
|
||||
(click.stop.prevent)="removeTab(tab.routerLink, rla.isActive)"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@@ -20,9 +20,9 @@ export class NavigationComponent {
|
||||
|
||||
readonly tabs$ = this.navigation.getTabs()
|
||||
|
||||
removeTab(tab: NavigationItem, active: boolean) {
|
||||
this.navigation.removeTab(tab)
|
||||
removeTab(routerLink: string, active: boolean) {
|
||||
this.navigation.removeTab(routerLink)
|
||||
|
||||
if (active) this.router.navigate(['./portal/desktop'])
|
||||
if (active) this.router.navigate(['/portal/desktop'])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { NavigationItem } from '../types/navigation-item'
|
||||
import { toDesktopItem } from '../utils/to-desktop-item'
|
||||
import { toNavigationItem } from '../utils/to-navigation-item'
|
||||
|
||||
@Pipe({
|
||||
name: 'toDesktopItem',
|
||||
name: 'toNavigationItem',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToDesktopItemPipe implements PipeTransform {
|
||||
export class ToNavigationItemPipe implements PipeTransform {
|
||||
transform(
|
||||
packages: Record<string, PackageDataEntry>,
|
||||
id: string,
|
||||
): NavigationItem | null {
|
||||
return id ? toDesktopItem(id, packages) : null
|
||||
return id ? toNavigationItem(id, packages) : null
|
||||
}
|
||||
}
|
||||
@@ -28,15 +28,15 @@
|
||||
[desktopItem]="item"
|
||||
>
|
||||
<a
|
||||
*ngIf="packages | toDesktopItem : item as desktopItem"
|
||||
*ngIf="packages | toNavigationItem : item as navigationItem"
|
||||
tuiTileHandle
|
||||
appCard
|
||||
@tuiFadeIn
|
||||
[id]="item"
|
||||
[badge]="item | toNotifications | async"
|
||||
[title]="desktopItem.title"
|
||||
[icon]="desktopItem.icon"
|
||||
[routerLink]="desktopItem.routerLink"
|
||||
[title]="navigationItem.title"
|
||||
[icon]="navigationItem.icon"
|
||||
[routerLink]="navigationItem.routerLink"
|
||||
></a>
|
||||
</tui-tile>
|
||||
</tui-tiles>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TuiFadeModule } from '@taiga-ui/experimental'
|
||||
import { TuiTilesModule } from '@taiga-ui/kit'
|
||||
import { DesktopComponent } from './desktop.component'
|
||||
import { CardComponent } from '../../components/card/card.component'
|
||||
import { ToDesktopItemPipe } from '../../pipes/to-desktop-item'
|
||||
import { ToNavigationItemPipe } from '../../pipes/to-navigation-item'
|
||||
import { ToNotificationsPipe } from '../../pipes/to-notifications'
|
||||
import { DesktopItemDirective } from './desktop-item.directive'
|
||||
|
||||
@@ -26,7 +26,7 @@ const ROUTES: Routes = [
|
||||
TuiSvgModule,
|
||||
TuiLoaderModule,
|
||||
TuiTilesModule,
|
||||
ToDesktopItemPipe,
|
||||
ToNavigationItemPipe,
|
||||
RouterModule.forChild(ROUTES),
|
||||
TuiFadeModule,
|
||||
DragScrollerDirective,
|
||||
|
||||
@@ -119,7 +119,7 @@ export class ServiceActionsComponent {
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.transform(this.service).some(d => !!d.errorText)) {
|
||||
if (this.dependencies.transform(this.service)?.some(d => !!d.errorText)) {
|
||||
const depErrMsg = `${this.service.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { AdditionalItem, FALLBACK_URL } from '../pipes/to-additional.pipe'
|
||||
|
||||
@Component({
|
||||
selector: '[additionalItem]',
|
||||
template: `
|
||||
<div [style.flex]="1">
|
||||
<strong>{{ additionalItem.name }}</strong>
|
||||
<div>{{ additionalItem.description }}</div>
|
||||
</div>
|
||||
<tui-svg *ngIf="icon" [src]="icon"></tui-svg>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host._disabled {
|
||||
pointer-events: none;
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
`,
|
||||
],
|
||||
host: {
|
||||
rel: 'noreferrer',
|
||||
target: '_blank',
|
||||
'[class._disabled]': 'disabled',
|
||||
'[attr.href]':
|
||||
'additionalItem.description.startsWith("http") ? additionalItem.description : null',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiSvgModule],
|
||||
})
|
||||
export class ServiceAdditionalItemComponent {
|
||||
@Input({ required: true })
|
||||
additionalItem!: AdditionalItem
|
||||
|
||||
get disabled(): boolean {
|
||||
return this.additionalItem.description === FALLBACK_URL
|
||||
}
|
||||
|
||||
get icon(): string | undefined {
|
||||
return this.additionalItem.description.startsWith('http')
|
||||
? 'tuiIconExternalLinkLarge'
|
||||
: this.additionalItem.icon
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,34 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { AdditionalItem, FALLBACK_URL } from '../pipes/to-additional.pipe'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ToAdditionalPipe } from '../pipes/to-additional.pipe'
|
||||
import { ServiceAdditionalItemComponent } from './additional-item.component'
|
||||
|
||||
@Component({
|
||||
selector: '[additional]',
|
||||
selector: 'service-additional',
|
||||
template: `
|
||||
<div [style.flex]="1">
|
||||
<strong>{{ additional.name }}</strong>
|
||||
<div>{{ additional.description }}</div>
|
||||
</div>
|
||||
<tui-svg *ngIf="icon" [src]="icon"></tui-svg>
|
||||
<h3 class="g-title">Additional Info</h3>
|
||||
<ng-container *ngFor="let additional of service | toAdditional">
|
||||
<a
|
||||
*ngIf="additional.description.startsWith('http'); else button"
|
||||
class="g-action"
|
||||
[additionalItem]="additional"
|
||||
></a>
|
||||
<ng-template #button>
|
||||
<button
|
||||
class="g-action"
|
||||
[style.pointer-events]="!additional.icon ? 'none' : null"
|
||||
[additionalItem]="additional"
|
||||
(click)="additional.action?.()"
|
||||
></button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host._disabled {
|
||||
pointer-events: none;
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
}
|
||||
`,
|
||||
],
|
||||
host: {
|
||||
'[attr.href]': 'additional.description',
|
||||
'[class._disabled]': 'disabled',
|
||||
target: '_blank',
|
||||
rel: 'noreferrer',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiSvgModule],
|
||||
imports: [CommonModule, ToAdditionalPipe, ServiceAdditionalItemComponent],
|
||||
})
|
||||
export class ServiceAdditionalComponent {
|
||||
@Input({ required: true })
|
||||
additional!: AdditionalItem
|
||||
|
||||
get disabled(): boolean {
|
||||
return this.additional.description === FALLBACK_URL
|
||||
}
|
||||
|
||||
get icon(): string | undefined {
|
||||
return this.additional.description.startsWith('http')
|
||||
? 'tuiIconExternalLinkLarge'
|
||||
: this.additional.icon
|
||||
}
|
||||
service!: PackageDataEntry
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ServiceDependencyComponent } from './dependency.component'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
|
||||
@Component({
|
||||
selector: 'service-dependencies',
|
||||
template: `
|
||||
<h3 class="g-title">Dependencies</h3>
|
||||
<button
|
||||
*ngFor="let dep of dependencies"
|
||||
class="g-action"
|
||||
[serviceDependency]="dep"
|
||||
(click)="dep.action()"
|
||||
></button>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, ServiceDependencyComponent],
|
||||
})
|
||||
export class ServiceDependenciesComponent {
|
||||
@Input({ required: true })
|
||||
dependencies: readonly DependencyInfo[] = []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { HealthCheckResult } from 'src/app/services/patch-db/data-model'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ServiceHealthCheckComponent } from './health-check.component'
|
||||
|
||||
@Component({
|
||||
selector: 'service-health-checks',
|
||||
template: `
|
||||
<h3 class="g-title">Health Checks</h3>
|
||||
<service-health-check
|
||||
*ngFor="let check of checks"
|
||||
class="g-action"
|
||||
[check]="check"
|
||||
[connected]="!!(connected$ | async)"
|
||||
></service-health-check>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, ServiceHealthCheckComponent],
|
||||
})
|
||||
export class ServiceHealthChecksComponent {
|
||||
@Input({ required: true })
|
||||
checks: readonly HealthCheckResult[] = []
|
||||
|
||||
readonly connected$ = inject(ConnectionService).connected$
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { InterfaceInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'button[serviceInterface]',
|
||||
selector: 'a[serviceInterface]',
|
||||
template: `
|
||||
<tui-svg [src]="info.icon" [style.color]="info.color"></tui-svg>
|
||||
<div [style.flex]="1">
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NgForOf } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { PackageDataEntry } 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 { ToStatusPipe } from '../pipes/to-status.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 | interfaceInfo"
|
||||
class="g-action"
|
||||
[serviceInterface]="info"
|
||||
[disabled]="!isRunning(service | toStatus)"
|
||||
[routerLink]="info.routerLink"
|
||||
></a>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
RouterLink,
|
||||
InterfaceInfoPipe,
|
||||
ServiceInterfaceComponent,
|
||||
ToStatusPipe,
|
||||
],
|
||||
})
|
||||
export class ServiceInterfacesComponent {
|
||||
@Input({ required: true })
|
||||
service!: PackageDataEntry
|
||||
|
||||
isRunning({ primary }: PackageStatus): boolean {
|
||||
return primary === PrimaryStatus.Running
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { ServiceMenu } from '../pipes/to-menu.pipe'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceMenuItem]',
|
||||
template: `
|
||||
<tui-svg [src]="menu.icon"></tui-svg>
|
||||
<div [style.flex]="1">
|
||||
<strong>{{ menu.name }}</strong>
|
||||
<div>
|
||||
{{ menu.description }}
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<tui-svg src="tuiIconChevronRightLarge"></tui-svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiSvgModule],
|
||||
})
|
||||
export class ServiceMenuItemComponent {
|
||||
@Input({ required: true, alias: 'serviceMenuItem' })
|
||||
menu!: ServiceMenu
|
||||
}
|
||||
@@ -1,25 +1,46 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { ServiceMenu } from '../pipes/to-menu.pipe'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ToMenuPipe } from '../pipes/to-menu.pipe'
|
||||
import { ServiceMenuItemComponent } from './menu-item.component'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceMenu]',
|
||||
selector: 'service-menu',
|
||||
template: `
|
||||
<tui-svg [src]="menu.icon"></tui-svg>
|
||||
<div [style.flex]="1">
|
||||
<strong>{{ menu.name }}</strong>
|
||||
<div>
|
||||
{{ menu.description }}
|
||||
<ng-content></ng-content>
|
||||
<h3 class="g-title">Menu</h3>
|
||||
<button
|
||||
*ngFor="let menu of service | toMenu"
|
||||
class="g-action"
|
||||
[serviceMenuItem]="menu"
|
||||
(click)="menu.action()"
|
||||
>
|
||||
<div *ngIf="menu.name === 'Outbound Proxy'" [style.color]="color">
|
||||
{{ proxy }}
|
||||
</div>
|
||||
</div>
|
||||
<tui-svg src="tuiIconChevronRightLarge"></tui-svg>
|
||||
</button>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiSvgModule],
|
||||
imports: [CommonModule, ToMenuPipe, ServiceMenuItemComponent],
|
||||
})
|
||||
export class ServiceMenuComponent {
|
||||
@Input({ required: true, alias: 'serviceMenu' })
|
||||
menu!: ServiceMenu
|
||||
@Input({ required: true })
|
||||
service!: PackageDataEntry
|
||||
|
||||
get color(): string {
|
||||
return this.service.installed?.outboundProxy
|
||||
? 'var(--tui-success-fill)'
|
||||
: 'var(--tui-warning-fill)'
|
||||
}
|
||||
|
||||
get proxy(): string {
|
||||
switch (this.service.installed?.outboundProxy) {
|
||||
case 'primary':
|
||||
return 'System Primary'
|
||||
case 'mirror':
|
||||
return 'Mirror P2P'
|
||||
default:
|
||||
return this.service.installed?.outboundProxy?.proxyId || 'None'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ import { InstallProgressPipeModule } from 'src/app/common/install-progress/insta
|
||||
</strong>
|
||||
</ng-template>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
font-size: x-large;
|
||||
margin: 1em 0;
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, InstallProgressPipeModule],
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
InterfaceInfo,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ServiceInterfaceModal } from '../modals/interface.component'
|
||||
|
||||
export interface ExtendedInterfaceInfo extends InterfaceInfo {
|
||||
id: string
|
||||
icon: string
|
||||
color: string
|
||||
typeDetail: string
|
||||
action: () => void
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
@@ -20,12 +17,7 @@ export interface ExtendedInterfaceInfo extends InterfaceInfo {
|
||||
standalone: true,
|
||||
})
|
||||
export class InterfaceInfoPipe implements PipeTransform {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
transform({
|
||||
manifest,
|
||||
installed,
|
||||
}: PackageDataEntry): ExtendedInterfaceInfo[] {
|
||||
transform({ installed }: PackageDataEntry): ExtendedInterfaceInfo[] {
|
||||
return Object.entries(installed!.interfaceInfo).map(([id, val]) => {
|
||||
let color: string
|
||||
let icon: string
|
||||
@@ -60,17 +52,7 @@ export class InterfaceInfoPipe implements PipeTransform {
|
||||
color,
|
||||
icon,
|
||||
typeDetail,
|
||||
action: () =>
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ServiceInterfaceModal), {
|
||||
label: val.name,
|
||||
size: 'l',
|
||||
data: {
|
||||
packageId: manifest.id,
|
||||
interfaceId: id,
|
||||
},
|
||||
})
|
||||
.subscribe(),
|
||||
routerLink: `./interface/${id}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ServiceConfigModal } from '../modals/config.component'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
import { PackageConfigData } from '../types/package-config-data'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
import { toRouterLink } from '../../../utils/to-router-link'
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
@@ -19,23 +21,32 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly navigation: NavigationService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] {
|
||||
if (!pkg.installed) return []
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] | null {
|
||||
if (!pkg.installed) return null
|
||||
|
||||
return Object.keys(pkg.installed['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
const deps = Object.keys(pkg.installed['current-dependencies'])
|
||||
.filter(depId => pkg.manifest.dependencies[depId])
|
||||
.map(depId => this.setDepValues(pkg, depId))
|
||||
|
||||
return deps.length ? deps : null
|
||||
}
|
||||
|
||||
private setDepValues(pkg: PackageDataEntry, depId: string): DependencyInfo {
|
||||
private setDepValues(pkg: PackageDataEntry, id: string): DependencyInfo {
|
||||
const error = pkg.installed!.status['dependency-errors'][id]
|
||||
const depInfo = pkg.installed!['dependency-info'][id]
|
||||
const version = pkg.manifest.dependencies[id].version
|
||||
const title = depInfo?.title || id
|
||||
const icon = depInfo?.icon || ''
|
||||
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action = (): unknown =>
|
||||
this.router.navigate([`portal`, `service`, depId])
|
||||
|
||||
const error = pkg.installed!.status['dependency-errors'][depId]
|
||||
let action = () => {
|
||||
this.navigation.addTab({ icon, title, routerLink: toRouterLink(id) })
|
||||
this.router.navigate([`portal`, `service`, id])
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
@@ -45,12 +56,12 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', depId)
|
||||
action = () => this.fixDep(pkg, 'install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', depId)
|
||||
action = () => this.fixDep(pkg, 'update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
@@ -59,24 +70,14 @@ export class ToDependenciesPipe implements PipeTransform {
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', depId)
|
||||
action = () => this.fixDep(pkg, 'configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed!['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkg.manifest.dependencies[depId].version,
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
return { id, icon, title, version, errorText, actionText, action }
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
|
||||
@@ -14,9 +14,7 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
import { PackageConfigData } from '../types/package-config-data'
|
||||
import { ServiceConfigModal } from '../modals/config.component'
|
||||
import { ServiceLogsModal } from '../modals/logs.component'
|
||||
import { ServiceCredentialsModal } from '../modals/credentials.component'
|
||||
import { ServiceActionsModal } from '../modals/actions.component'
|
||||
|
||||
export interface ServiceMenu {
|
||||
icon: string
|
||||
@@ -29,7 +27,7 @@ export interface ServiceMenu {
|
||||
name: 'toMenu',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToMenusPipe implements PipeTransform {
|
||||
export class ToMenuPipe implements PipeTransform {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
@@ -69,11 +67,9 @@ export class ToMenusPipe implements PipeTransform {
|
||||
name: 'Actions',
|
||||
description: `Uninstall and other commands specific to ${manifest.title}`,
|
||||
action: () =>
|
||||
this.showDialog(
|
||||
`${manifest.title} credentials`,
|
||||
manifest.id,
|
||||
ServiceActionsModal,
|
||||
),
|
||||
this.router.navigate(['actions'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
},
|
||||
{
|
||||
icon: 'tuiIconShieldLarge',
|
||||
@@ -86,11 +82,9 @@ export class ToMenusPipe implements PipeTransform {
|
||||
name: 'Logs',
|
||||
description: `Raw, unfiltered logs`,
|
||||
action: () =>
|
||||
this.showDialog(
|
||||
`${manifest.title} logs`,
|
||||
manifest.id,
|
||||
ServiceLogsModal,
|
||||
),
|
||||
this.router.navigate(['logs'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
},
|
||||
url
|
||||
? {
|
||||
@@ -99,7 +93,6 @@ export class ToMenusPipe implements PipeTransform {
|
||||
description: `View ${manifest.title} on the Marketplace`,
|
||||
action: () =>
|
||||
this.router.navigate(['marketplace', manifest.id], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { url },
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
isEmptyObject,
|
||||
WithId,
|
||||
ErrorService,
|
||||
LoadingService,
|
||||
getPkgId,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, switchMap, timer } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -27,7 +25,11 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormPage } from 'src/app/apps/ui/modals/form/form.page'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
import { ServiceActionSuccessComponent } from '../components/action-success.component'
|
||||
import { DesktopService } from '../../../services/desktop.service'
|
||||
import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
||||
import { updateTab } from '../utils/update-tab'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
import { toRouterLink } from '../../../utils/to-router-link'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -41,7 +43,9 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
||||
></button>
|
||||
</section>
|
||||
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
|
||||
<h3>Actions for {{ pkg.manifest.title }}</h3>
|
||||
<h3 *ngIf="actionGroups.length" class="g-title">
|
||||
Actions for {{ pkg.manifest.title }}
|
||||
</h3>
|
||||
<div *ngFor="let group of actionGroups">
|
||||
<button
|
||||
*ngFor="let action of group"
|
||||
@@ -61,9 +65,11 @@ import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
||||
standalone: true,
|
||||
imports: [CommonModule, ServiceActionComponent, GroupActionsPipe],
|
||||
})
|
||||
export class ServiceActionsModal {
|
||||
export class ServiceActionsRoute {
|
||||
private readonly id = getPkgId(inject(ActivatedRoute))
|
||||
|
||||
readonly pkg$ = this.patch
|
||||
.watch$('package-data', this.context.data)
|
||||
.watch$('package-data', this.id)
|
||||
.pipe(filter(pkg => pkg.state === PackageState.Installed))
|
||||
|
||||
readonly action = {
|
||||
@@ -74,8 +80,6 @@ export class ServiceActionsModal {
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<void, string>,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
@@ -83,7 +87,11 @@ export class ServiceActionsModal {
|
||||
private readonly router: Router,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
private readonly desktop: DesktopService,
|
||||
private readonly navigation: NavigationService,
|
||||
) {
|
||||
updateTab('/actions')
|
||||
}
|
||||
|
||||
async handleAction(action: WithId<Action>) {
|
||||
if (action.disabled) {
|
||||
@@ -156,12 +164,13 @@ export class ServiceActionsModal {
|
||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.uninstallPackage({ id: this.context.data })
|
||||
await this.embassyApi.uninstallPackage({ id: this.id })
|
||||
this.embassyApi
|
||||
.setDbValue<boolean>(['ack-instructions', this.context.data], false)
|
||||
.setDbValue<boolean>(['ack-instructions', this.id], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navigation.removeTab(toRouterLink(this.id))
|
||||
this.desktop.remove(this.id)
|
||||
this.router.navigate(['portal', 'desktop'])
|
||||
this.context.$implicit.complete()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -177,7 +186,7 @@ export class ServiceActionsModal {
|
||||
|
||||
try {
|
||||
const data = await this.embassyApi.executePackageAction({
|
||||
id: this.context.data,
|
||||
id: this.id,
|
||||
'action-id': actionId,
|
||||
input,
|
||||
})
|
||||
@@ -1,14 +1,11 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { InterfaceAddressesComponentModule } from 'src/app/common/interface-addresses/interface-addresses.module'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
interface Context {
|
||||
packageId: string
|
||||
interfaceId: string
|
||||
}
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { updateTab } from '../utils/update-tab'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -23,8 +20,13 @@ interface Context {
|
||||
standalone: true,
|
||||
imports: [CommonModule, InterfaceAddressesComponentModule],
|
||||
})
|
||||
export class ServiceInterfaceModal {
|
||||
readonly context = inject<{ data: Context }>(POLYMORPHEUS_CONTEXT).data
|
||||
export class ServiceInterfaceRoute {
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
|
||||
readonly context = {
|
||||
packageId: getPkgId(this.route),
|
||||
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
|
||||
}
|
||||
|
||||
readonly interfaceInfo$ = inject(PatchDB<DataModel>).watch$(
|
||||
'package-data',
|
||||
@@ -33,4 +35,8 @@ export class ServiceInterfaceModal {
|
||||
'interfaceInfo',
|
||||
this.context.interfaceId,
|
||||
)
|
||||
|
||||
constructor() {
|
||||
updateTab(`/interface/${this.context.interfaceId}`)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { LogsComponentModule } from 'src/app/common/logs/logs.component.module'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { LogsComponentModule } from 'src/app/common/logs/logs.component.module'
|
||||
import { updateTab } from '../utils/update-tab'
|
||||
|
||||
@Component({
|
||||
template:
|
||||
'<logs [fetchLogs]="fetch" [followLogs]="follow" [context]="id"></logs>',
|
||||
template: '<logs [fetchLogs]="fetch" [followLogs]="follow" [context]="id" />',
|
||||
styles: [
|
||||
`
|
||||
logs {
|
||||
display: block;
|
||||
height: 60vh;
|
||||
height: calc(100% - 9rem);
|
||||
min-height: 20rem;
|
||||
margin-bottom: 5rem;
|
||||
|
||||
::ng-deep ion-header {
|
||||
@@ -24,14 +26,18 @@ import { LogsComponentModule } from 'src/app/common/logs/logs.component.module'
|
||||
standalone: true,
|
||||
imports: [LogsComponentModule],
|
||||
})
|
||||
export class ServiceLogsModal {
|
||||
export class ServiceLogsRoute {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly id = inject<{ data: string }>(POLYMORPHEUS_CONTEXT).data
|
||||
readonly id = getPkgId(inject(ActivatedRoute))
|
||||
|
||||
readonly follow = async (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followPackageLogs({ id: this.id, ...params })
|
||||
|
||||
readonly fetch = async (params: RR.GetServerLogsReq) =>
|
||||
this.api.getPackageLogs({ id: this.id, ...params })
|
||||
|
||||
constructor() {
|
||||
updateTab('/logs')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||
import { TuiSvgModule } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { toRouterLink } from '../../../utils/to-router-link'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<a
|
||||
*ngIf="service$ | async as service"
|
||||
routerLinkActive="_current"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="getLink(service.manifest.id)"
|
||||
(isActiveChange)="onActive(service, $event)"
|
||||
>
|
||||
<tui-svg src="tuiIconChevronLeftLarge" />
|
||||
{{ service.manifest.title }}
|
||||
</a>
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--tui-text-01);
|
||||
}
|
||||
|
||||
._current {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
],
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule, TuiSvgModule],
|
||||
})
|
||||
export class ServiceOutletComponent {
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
|
||||
readonly service$ = this.router.events.pipe(
|
||||
map(() => this.route.firstChild?.snapshot.paramMap?.get('pkgId')),
|
||||
filter(Boolean),
|
||||
distinctUntilChanged(),
|
||||
switchMap(id => this.patch.watch$('package-data', id)),
|
||||
tap(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.router.navigate(['./portal/desktop'])
|
||||
} else {
|
||||
this.onActive(
|
||||
pkg,
|
||||
!this.navigation.hasSubtab(this.getLink(pkg.manifest.id)),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
getLink(id: string): string {
|
||||
return toRouterLink(id)
|
||||
}
|
||||
|
||||
onActive({ icon, manifest }: PackageDataEntry, active: boolean): void {
|
||||
if (!active) return
|
||||
|
||||
this.navigation.addTab({
|
||||
icon,
|
||||
title: manifest.title,
|
||||
routerLink: this.getLink(manifest.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId, isEmptyObject } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
MainStatus,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
StatusRendering,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ServiceProgressComponent } from '../components/progress.component'
|
||||
import { ServiceStatusComponent } from '../components/status.component'
|
||||
import { ServiceActionsComponent } from '../components/actions.component'
|
||||
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
||||
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
||||
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
||||
import { ServiceMenuComponent } from '../components/menu.component'
|
||||
import { ServiceAdditionalComponent } from '../components/additional.component'
|
||||
import { ProgressDataPipe } from '../pipes/progress-data.pipe'
|
||||
import { ToDependenciesPipe } from '../pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from '../pipes/to-status.pipe'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
PackageState.Updating,
|
||||
PackageState.Restoring,
|
||||
]
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="service$ | async as service">
|
||||
<ng-container *ngIf="showProgress(service); else installed">
|
||||
<ng-container *ngIf="service | progressData as progress">
|
||||
<p [progress]="progress.downloadProgress">Downloading</p>
|
||||
<p [progress]="progress.validateProgress">Validating</p>
|
||||
<p [progress]="progress.unpackProgress">Unpacking</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #installed>
|
||||
<ng-container *ngIf="service | toStatus as status">
|
||||
<h3 class="g-title">Status</h3>
|
||||
<service-status
|
||||
[connected]="!!(connected$ | async)"
|
||||
[installProgress]="service['install-progress']"
|
||||
[rendering]="$any(getRendering(status))"
|
||||
/>
|
||||
<service-actions
|
||||
*ngIf="isInstalled(service) && (connected$ | async)"
|
||||
[service]="service"
|
||||
/>
|
||||
|
||||
<ng-container *ngIf="isInstalled(service) && !isBackingUp(status)">
|
||||
<service-interfaces [service]="service" />
|
||||
<service-health-checks
|
||||
*ngIf="isRunning(status) && (health$ | async) as checks"
|
||||
[checks]="checks"
|
||||
/>
|
||||
<service-dependencies
|
||||
*ngIf="service | toDependencies as dependencies"
|
||||
[dependencies]="dependencies"
|
||||
/>
|
||||
<service-menu [service]="service" />
|
||||
<service-additional [service]="service" />
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
ServiceProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceActionsComponent,
|
||||
ServiceInterfacesComponent,
|
||||
ServiceHealthChecksComponent,
|
||||
ServiceDependenciesComponent,
|
||||
ServiceMenuComponent,
|
||||
ServiceAdditionalComponent,
|
||||
|
||||
ProgressDataPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
],
|
||||
})
|
||||
export class ServiceRoute {
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly pkgId = getPkgId(inject(ActivatedRoute))
|
||||
|
||||
readonly connected$ = inject(ConnectionService).connected$
|
||||
readonly service$ = this.patch.watch$('package-data', this.pkgId)
|
||||
readonly health$ = this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.pipe(map(toHealthCheck))
|
||||
|
||||
getRendering({ primary }: PackageStatus): StatusRendering {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function toHealthCheck(main: MainStatus): HealthCheckResult[] | null {
|
||||
return main.status !== 'running' || isEmptyObject(main.health)
|
||||
? null
|
||||
: Object.values(main.health)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<ng-container *ngIf="service$ | async as service">
|
||||
<ng-container *tuiLet="!!(connected$ | async) as connected">
|
||||
<ng-container *ngIf="showProgress(service); else installed">
|
||||
<ng-container *ngIf="service | progressData as progress">
|
||||
<p [progress]="progress.downloadProgress">Downloading</p>
|
||||
<p [progress]="progress.validateProgress">Validating</p>
|
||||
<p [progress]="progress.unpackProgress">Unpacking</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #installed>
|
||||
<ng-container *ngIf="service | toStatus as status">
|
||||
<section>
|
||||
<h3 class="g-title">Status</h3>
|
||||
<service-status
|
||||
class="status"
|
||||
[connected]="connected"
|
||||
[installProgress]="service['install-progress']"
|
||||
[rendering]="$any(getRendering(status))"
|
||||
></service-status>
|
||||
<service-actions
|
||||
*ngIf="isInstalled(service) && connected"
|
||||
[service]="service"
|
||||
></service-actions>
|
||||
</section>
|
||||
|
||||
<ng-container *ngIf="isInstalled(service) && !isBackingUp(status)">
|
||||
<section>
|
||||
<h3 class="g-title">Interfaces</h3>
|
||||
<button
|
||||
*ngFor="let info of service | interfaceInfo"
|
||||
class="g-action"
|
||||
[serviceInterface]="info"
|
||||
[disabled]="!isRunning(status)"
|
||||
(click)="info.action()"
|
||||
></button>
|
||||
</section>
|
||||
|
||||
<ng-container *ngIf="isRunning(status)">
|
||||
<section *ngIf="health$ | async as checks">
|
||||
<h3 class="g-title">Health Checks</h3>
|
||||
<service-health-check
|
||||
*ngFor="let check of checks"
|
||||
class="g-action"
|
||||
[check]="check"
|
||||
[connected]="connected"
|
||||
></service-health-check>
|
||||
</section>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="service | toDependencies as dependencies">
|
||||
<section *ngIf="dependencies.length">
|
||||
<h3 class="g-title">Dependencies</h3>
|
||||
<button
|
||||
*ngFor="let dep of dependencies"
|
||||
class="g-action"
|
||||
[serviceDependency]="dep"
|
||||
(click)="dep.action()"
|
||||
></button>
|
||||
</section>
|
||||
</ng-container>
|
||||
|
||||
<section>
|
||||
<h3 class="g-title">Menu</h3>
|
||||
<button
|
||||
*ngFor="let menu of service | toMenu"
|
||||
class="g-action"
|
||||
[serviceMenu]="menu"
|
||||
(click)="menu.action()"
|
||||
>
|
||||
<div
|
||||
*ngIf="menu.name === 'Outbound Proxy'"
|
||||
[style.color]="
|
||||
service.installed?.outboundProxy
|
||||
? 'var(--tui-success-fill)'
|
||||
: 'var(--tui-warning-fill)'
|
||||
"
|
||||
>
|
||||
{{ this.getProxy(service.installed?.outboundProxy) }}
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="g-title">Additional Info</h3>
|
||||
<ng-container *ngFor="let additional of service | toAdditional">
|
||||
<a
|
||||
*ngIf="additional.description.startsWith('http'); else button"
|
||||
class="g-action"
|
||||
[additional]="additional"
|
||||
></a>
|
||||
<ng-template #button>
|
||||
<button
|
||||
class="g-action"
|
||||
[class.g-action_static]="!additional.icon"
|
||||
[additional]="additional"
|
||||
(click)="additional.action?.()"
|
||||
></button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</section>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,15 +0,0 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
.status {
|
||||
font-size: x-large;
|
||||
margin: 1em 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.g-action_static {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { getPkgId, isEmptyObject } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map, Observable, tap } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
ServiceOutboundProxy,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
StatusRendering,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { NavigationService } from '../../services/navigation.service'
|
||||
import { toRouterLink } from '../../utils/to-router-link'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
PackageState.Updating,
|
||||
PackageState.Restoring,
|
||||
]
|
||||
|
||||
@Component({
|
||||
templateUrl: 'service.component.html',
|
||||
styleUrls: ['service.component.scss'],
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServiceComponent {
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly connected$ = inject(ConnectionService).connected$
|
||||
|
||||
readonly service$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
tap(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.router.navigate(['..'], { relativeTo: this.route })
|
||||
} else {
|
||||
this.navigation.addTab({
|
||||
icon: pkg.icon,
|
||||
title: pkg.manifest.title,
|
||||
routerLink: toRouterLink(pkg.manifest.id),
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
readonly health$: Observable<HealthCheckResult[] | null> = this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
map(main =>
|
||||
main.status !== 'running' || isEmptyObject(main.health)
|
||||
? null
|
||||
: Object.values(main.health),
|
||||
),
|
||||
)
|
||||
|
||||
getRendering({ primary }: PackageStatus): StatusRendering {
|
||||
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)
|
||||
}
|
||||
|
||||
getProxy(proxy?: ServiceOutboundProxy): string {
|
||||
switch (proxy) {
|
||||
case 'primary':
|
||||
return 'System Primary'
|
||||
case 'mirror':
|
||||
return 'Mirror P2P'
|
||||
default:
|
||||
return proxy?.proxyId || 'None'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,43 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiLetModule } from '@taiga-ui/cdk'
|
||||
|
||||
import { ServiceComponent } from './service.component'
|
||||
import { ServiceProgressComponent } from './components/progress.component'
|
||||
import { ServiceStatusComponent } from './components/status.component'
|
||||
import { ServiceActionsComponent } from './components/actions.component'
|
||||
import { ServiceInterfaceComponent } from './components/interface.component'
|
||||
import { ServiceHealthCheckComponent } from './components/health-check.component'
|
||||
import { ServiceDependencyComponent } from './components/dependency.component'
|
||||
import { ServiceMenuComponent } from './components/menu.component'
|
||||
import { ServiceAdditionalComponent } from './components/additional.component'
|
||||
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { InterfaceInfoPipe } from './pipes/interface-info.pipe'
|
||||
import { ToMenusPipe } from './pipes/to-menu.pipe'
|
||||
import { ToAdditionalPipe } from './pipes/to-additional.pipe'
|
||||
import { ServiceOutletComponent } from './routes/outlet.component'
|
||||
import { ServiceRoute } from './routes/service.component'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: ':pkgId',
|
||||
component: ServiceComponent,
|
||||
path: '',
|
||||
component: ServiceOutletComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':pkgId',
|
||||
component: ServiceRoute,
|
||||
},
|
||||
{
|
||||
path: ':pkgId/actions',
|
||||
loadComponent: () =>
|
||||
import('./routes/actions.component').then(m => m.ServiceActionsRoute),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/interface/:interfaceId',
|
||||
loadComponent: () =>
|
||||
import('./routes/interface.component').then(
|
||||
m => m.ServiceInterfaceRoute,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/logs',
|
||||
loadComponent: () =>
|
||||
import('./routes/logs.component').then(m => m.ServiceLogsRoute),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/portal/desktop',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiLetModule,
|
||||
|
||||
ServiceProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceActionsComponent,
|
||||
ServiceInterfaceComponent,
|
||||
ServiceHealthCheckComponent,
|
||||
ServiceDependencyComponent,
|
||||
ServiceMenuComponent,
|
||||
ServiceAdditionalComponent,
|
||||
|
||||
ProgressDataPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
InterfaceInfoPipe,
|
||||
ToMenusPipe,
|
||||
ToAdditionalPipe,
|
||||
|
||||
RouterModule.forChild(ROUTES),
|
||||
],
|
||||
declarations: [ServiceComponent],
|
||||
exports: [ServiceComponent],
|
||||
})
|
||||
@NgModule({ imports: [RouterModule.forChild(ROUTES)] })
|
||||
export class ServiceModule {}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { NavigationService } from 'src/app/apps/portal/services/navigation.service'
|
||||
import { toRouterLink } from 'src/app/apps/portal/utils/to-router-link'
|
||||
|
||||
export function updateTab(path: string, id = getPkgId(inject(ActivatedRoute))) {
|
||||
inject(NavigationService).updateTab(toRouterLink(id), toRouterLink(id) + path)
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
|
||||
import { toDesktopItem } from '../../../utils/to-desktop-item'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
import { SideloadDependenciesComponent } from './dependencies.component'
|
||||
|
||||
@@ -118,7 +117,7 @@ export class SideloadPackageComponent {
|
||||
await this.api.uploadPackage(pkg, this.file)
|
||||
await this.router.navigate(['/portal/service', manifest.id])
|
||||
|
||||
this.navigation.removeTab(toDesktopItem('/portal/system/sideload'))
|
||||
this.navigation.removeTab('/portal/system/sideload')
|
||||
this.alerts
|
||||
.open('Package uploaded successfully', { status: 'success' })
|
||||
.subscribe()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { systemTabResolver } from '../../utils/system-tab-resolver'
|
||||
import { toDesktopItem } from '../../utils/to-desktop-item'
|
||||
import { toNavigationItem } from '../../utils/to-navigation-item'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
@@ -9,34 +9,30 @@ const ROUTES: Routes = [
|
||||
path: 'backups',
|
||||
loadComponent: () =>
|
||||
import('./backups/backups.component').then(m => m.BackupsComponent),
|
||||
data: toDesktopItem('/portal/system/backups'),
|
||||
data: toNavigationItem('/portal/system/backups'),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'sideload',
|
||||
loadComponent: () =>
|
||||
import('./sideload/sideload.component').then(m => m.SideloadComponent),
|
||||
data: toDesktopItem('/portal/system/sideload'),
|
||||
data: toNavigationItem('/portal/system/sideload'),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'updates',
|
||||
loadComponent: () =>
|
||||
import('./updates/updates.component').then(m => m.UpdatesComponent),
|
||||
data: toDesktopItem('/portal/system/updates'),
|
||||
data: toNavigationItem('/portal/system/updates'),
|
||||
},
|
||||
{
|
||||
title: systemTabResolver,
|
||||
path: 'snek',
|
||||
loadComponent: () =>
|
||||
import('./snek/snek.component').then(m => m.SnekComponent),
|
||||
data: toDesktopItem('/portal/system/snek'),
|
||||
data: toNavigationItem('/portal/system/snek'),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
declarations: [],
|
||||
exports: [],
|
||||
})
|
||||
@NgModule({ imports: [RouterModule.forChild(ROUTES)] })
|
||||
export class SystemModule {}
|
||||
|
||||
@@ -12,17 +12,37 @@ export class NavigationService {
|
||||
return this.tabs
|
||||
}
|
||||
|
||||
removeTab({ routerLink }: NavigationItem) {
|
||||
this.tabs.next(this.tabs.value.filter(t => t.routerLink !== routerLink))
|
||||
removeTab(routerLink: string) {
|
||||
this.tabs.next(
|
||||
this.tabs.value.filter(t => !t.routerLink.startsWith(routerLink)),
|
||||
)
|
||||
}
|
||||
|
||||
addTab(tab: NavigationItem) {
|
||||
if (this.tabs.value.every(t => t.routerLink !== tab.routerLink)) {
|
||||
this.tabs.next([...this.tabs.value, tab])
|
||||
}
|
||||
const current = this.tabs.value.find(t =>
|
||||
t.routerLink.startsWith(tab.routerLink),
|
||||
)
|
||||
|
||||
this.tabs.next(
|
||||
current
|
||||
? this.tabs.value.map(t => (t === current ? tab : t))
|
||||
: this.tabs.value.concat(tab),
|
||||
)
|
||||
}
|
||||
|
||||
updateTab(old: string, routerLink: string) {
|
||||
this.tabs.next(
|
||||
this.tabs.value.map(t =>
|
||||
t.routerLink === old ? { ...t, routerLink } : t,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
hasTab(path: string): boolean {
|
||||
return this.tabs.value.some(t => t.routerLink === path)
|
||||
}
|
||||
|
||||
hasSubtab(path: string): boolean {
|
||||
return this.tabs.value.some(t => t.routerLink.startsWith(path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SYSTEM_UTILITIES } from '../constants/system-utilities'
|
||||
import { NavigationItem } from '../types/navigation-item'
|
||||
import { toRouterLink } from './to-router-link'
|
||||
|
||||
export function toDesktopItem(
|
||||
export function toNavigationItem(
|
||||
id: string,
|
||||
packages: Record<string, PackageDataEntry> = {},
|
||||
): NavigationItem {
|
||||
@@ -64,6 +64,6 @@ const routes: Routes = [
|
||||
InsecureWarningComponentModule,
|
||||
LaunchMenuComponentModule,
|
||||
],
|
||||
exports: [InterfaceInfoPipe],
|
||||
exports: [InterfaceInfoPipe, ToStatusPipe],
|
||||
})
|
||||
export class AppShowPageModule {}
|
||||
|
||||
Reference in New Issue
Block a user