From c491dfdd3a0d47e8aff1f16827fa94a96aade2e3 Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Thu, 9 Nov 2023 23:23:58 +0400 Subject: [PATCH] feat: use routes for service sections (#2502) * feat: use routes for service sections * chore: fix comment --- .../components/card/card.component.html | 2 +- .../navigation/navigation.component.html | 3 +- .../navigation/navigation.component.ts | 6 +- ...-desktop-item.ts => to-navigation-item.ts} | 8 +- .../routes/desktop/desktop.component.html | 8 +- .../portal/routes/desktop/desktop.module.ts | 4 +- .../service/components/actions.component.ts | 2 +- .../components/additional-item.component.ts | 47 ++++++ .../components/additional.component.ts | 56 +++----- .../components/dependencies.component.ts | 24 ++++ .../components/health-checks.component.ts | 32 +++++ .../service/components/interface.component.ts | 2 +- .../components/interfaces.component.ts | 42 ++++++ .../service/components/menu-item.component.ts | 25 ++++ .../service/components/menu.component.ts | 49 +++++-- .../service/components/status.component.ts | 9 ++ .../service/pipes/interface-info.pipe.ts | 26 +--- .../service/pipes/to-dependencies.pipe.ts | 47 +++--- .../routes/service/pipes/to-menu.pipe.ts | 21 +-- .../{modals => routes}/actions.component.ts | 43 +++--- .../{modals => routes}/interface.component.ts | 22 +-- .../{modals => routes}/logs.component.ts | 20 ++- .../routes/service/routes/outlet.component.ts | 86 +++++++++++ .../service/routes/service.component.ts | 134 ++++++++++++++++++ .../routes/service/service.component.html | 106 -------------- .../routes/service/service.component.scss | 15 -- .../routes/service/service.component.ts | 100 ------------- .../portal/routes/service/service.module.ts | 79 +++++------ .../portal/routes/service/utils/update-tab.ts | 9 ++ .../system/sideload/package.component.ts | 3 +- .../portal/routes/system/system.module.ts | 16 +-- .../portal/services/navigation.service.ts | 30 +++- ...-desktop-item.ts => to-navigation-item.ts} | 2 +- .../services/app-show/app-show.module.ts | 2 +- 34 files changed, 637 insertions(+), 443 deletions(-) rename frontend/projects/ui/src/app/apps/portal/pipes/{to-desktop-item.ts => to-navigation-item.ts} (61%) create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/components/additional-item.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/components/dependencies.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/components/health-checks.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/components/interfaces.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/components/menu-item.component.ts rename frontend/projects/ui/src/app/apps/portal/routes/service/{modals => routes}/actions.component.ts (82%) rename frontend/projects/ui/src/app/apps/portal/routes/service/{modals => routes}/interface.component.ts (67%) rename frontend/projects/ui/src/app/apps/portal/routes/service/{modals => routes}/logs.component.ts (67%) create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/routes/outlet.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/routes/service.component.ts delete mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/service.component.html delete mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/service.component.scss delete mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/service.component.ts create mode 100644 frontend/projects/ui/src/app/apps/portal/routes/service/utils/update-tab.ts rename frontend/projects/ui/src/app/apps/portal/utils/{to-desktop-item.ts => to-navigation-item.ts} (94%) diff --git a/frontend/projects/ui/src/app/apps/portal/components/card/card.component.html b/frontend/projects/ui/src/app/apps/portal/components/card/card.component.html index 17f8fe9f2..d0dd4ff08 100644 --- a/frontend/projects/ui/src/app/apps/portal/components/card/card.component.html +++ b/frontend/projects/ui/src/app/apps/portal/components/card/card.component.html @@ -4,7 +4,7 @@ {{ badge }} diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html index 4b9705b98..a098c17c7 100644 --- a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html @@ -11,7 +11,6 @@ #rla="routerLinkActive" class="tab" routerLinkActive="tab_active" - [routerLinkActiveOptions]="{ exact: true }" [routerLink]="tab.routerLink" > Close diff --git a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts index c702b9816..c34a5ac4b 100644 --- a/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts @@ -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']) } } diff --git a/frontend/projects/ui/src/app/apps/portal/pipes/to-desktop-item.ts b/frontend/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts similarity index 61% rename from frontend/projects/ui/src/app/apps/portal/pipes/to-desktop-item.ts rename to frontend/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts index f3204b58a..39d1d91c7 100644 --- a/frontend/projects/ui/src/app/apps/portal/pipes/to-desktop-item.ts +++ b/frontend/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts @@ -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, id: string, ): NavigationItem | null { - return id ? toDesktopItem(id, packages) : null + return id ? toNavigationItem(id, packages) : null } } diff --git a/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html b/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html index a4016fe29..4b993c6c5 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html +++ b/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html @@ -28,15 +28,15 @@ [desktopItem]="item" > diff --git a/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts b/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts index 6354bc61c..a68955baf 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts @@ -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, diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts index 8016e382f..234e5ac90 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts @@ -119,7 +119,7 @@ export class ServiceActionsComponent { } async tryStart(): Promise { - 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) diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional-item.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional-item.component.ts new file mode 100644 index 000000000..f4e60de41 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional-item.component.ts @@ -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: ` +
+ {{ additionalItem.name }} +
{{ additionalItem.description }}
+
+ + `, + 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 + } +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional.component.ts index 9b74fcba3..db233569d 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/additional.component.ts @@ -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: ` -
- {{ additional.name }} -
{{ additional.description }}
-
- +

Additional Info

+ + + + + + `, - 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 } diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/dependencies.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/dependencies.component.ts new file mode 100644 index 000000000..d735d5398 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/dependencies.component.ts @@ -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: ` +

Dependencies

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, ServiceDependencyComponent], +}) +export class ServiceDependenciesComponent { + @Input({ required: true }) + dependencies: readonly DependencyInfo[] = [] +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/health-checks.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/health-checks.component.ts new file mode 100644 index 000000000..54928a408 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/health-checks.component.ts @@ -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: ` +

Health Checks

+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, ServiceHealthCheckComponent], +}) +export class ServiceHealthChecksComponent { + @Input({ required: true }) + checks: readonly HealthCheckResult[] = [] + + readonly connected$ = inject(ConnectionService).connected$ +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts index 2b39eb027..45ded6d6d 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts @@ -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: `
diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/interfaces.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/interfaces.component.ts new file mode 100644 index 000000000..f70261748 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/interfaces.component.ts @@ -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: ` +

Interfaces

+ + `, + 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 + } +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu-item.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu-item.component.ts new file mode 100644 index 000000000..6f6503f6f --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu-item.component.ts @@ -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: ` + +
+ {{ menu.name }} +
+ {{ menu.description }} + +
+
+ + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [TuiSvgModule], +}) +export class ServiceMenuItemComponent { + @Input({ required: true, alias: 'serviceMenuItem' }) + menu!: ServiceMenu +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu.component.ts index a227d4a6f..cc0fef09e 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/menu.component.ts @@ -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: ` - -
- {{ menu.name }} -
- {{ menu.description }} - +

Menu

+
- + `, 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' + } + } } diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts index 59eb44e8b..a636fbc68 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts @@ -24,6 +24,15 @@ import { InstallProgressPipeModule } from 'src/app/common/install-progress/insta `, + styles: [ + ` + :host { + font-size: x-large; + margin: 1em 0; + display: block; + } + `, + ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, InstallProgressPipeModule], diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/interface-info.pipe.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/interface-info.pipe.ts index 8ab557b2b..cc1d793dc 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/interface-info.pipe.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/interface-info.pipe.ts @@ -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}`, } }) } diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-dependencies.pipe.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-dependencies.pipe.ts index c0951d6d6..9e575e9ba 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-dependencies.pipe.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-dependencies.pipe.ts @@ -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( diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts index 133269fad..b1110eb5d 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts @@ -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 }, }), } diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/modals/actions.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts similarity index 82% rename from frontend/projects/ui/src/app/apps/portal/routes/service/modals/actions.component.ts rename to frontend/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts index cae649863..2529d8535 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/modals/actions.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts @@ -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' > -

Actions for {{ pkg.manifest.title }}

+

+ Actions for {{ pkg.manifest.title }} +

- - - -
-

Health Checks

- -
-
- - -
-

Dependencies

- -
-
- -
-

Menu

- -
- -
-

Additional Info

- - - - - - -
- - - - - diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.scss b/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.scss deleted file mode 100644 index 913a5907f..000000000 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.scss +++ /dev/null @@ -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; - } -} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.ts deleted file mode 100644 index 2a49413ba..000000000 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/service.component.ts +++ /dev/null @@ -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) - - 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 = 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' - } - } -} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/service.module.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/service.module.ts index f4cb13244..fb2d6b8a1 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/service/service.module.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/service.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/service/utils/update-tab.ts b/frontend/projects/ui/src/app/apps/portal/routes/service/utils/update-tab.ts new file mode 100644 index 000000000..cbc717315 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/portal/routes/service/utils/update-tab.ts @@ -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) +} diff --git a/frontend/projects/ui/src/app/apps/portal/routes/system/sideload/package.component.ts b/frontend/projects/ui/src/app/apps/portal/routes/system/sideload/package.component.ts index 68a41b30f..94930baa0 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/system/sideload/package.component.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/system/sideload/package.component.ts @@ -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() diff --git a/frontend/projects/ui/src/app/apps/portal/routes/system/system.module.ts b/frontend/projects/ui/src/app/apps/portal/routes/system/system.module.ts index e0ea03716..cc5638b33 100644 --- a/frontend/projects/ui/src/app/apps/portal/routes/system/system.module.ts +++ b/frontend/projects/ui/src/app/apps/portal/routes/system/system.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/apps/portal/services/navigation.service.ts b/frontend/projects/ui/src/app/apps/portal/services/navigation.service.ts index 1c91b9585..a4a789d22 100644 --- a/frontend/projects/ui/src/app/apps/portal/services/navigation.service.ts +++ b/frontend/projects/ui/src/app/apps/portal/services/navigation.service.ts @@ -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)) + } } diff --git a/frontend/projects/ui/src/app/apps/portal/utils/to-desktop-item.ts b/frontend/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts similarity index 94% rename from frontend/projects/ui/src/app/apps/portal/utils/to-desktop-item.ts rename to frontend/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts index cbeed8dfe..b33afe3a4 100644 --- a/frontend/projects/ui/src/app/apps/portal/utils/to-desktop-item.ts +++ b/frontend/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts @@ -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 = {}, ): NavigationItem { diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts index 704b867d6..efa1ad187 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts @@ -64,6 +64,6 @@ const routes: Routes = [ InsecureWarningComponentModule, LaunchMenuComponentModule, ], - exports: [InterfaceInfoPipe], + exports: [InterfaceInfoPipe, ToStatusPipe], }) export class AppShowPageModule {}