From f4fadd366e2c648e6634a1f6d06bc52522224f6f Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Tue, 19 Mar 2024 22:56:16 +0800 Subject: [PATCH] feat: add new dashboard (#2574) * feat: add new dashboard * chore: comments * fix duplicate --------- Co-authored-by: Matt Hill --- web/projects/shared/styles/taiga.scss | 6 + .../drawer/drawer-item.directive.ts | 96 ------- .../components/drawer/drawer.component.html | 57 ---- .../components/drawer/drawer.component.scss | 77 ------ .../components/drawer/drawer.component.ts | 67 ----- .../components/header/header.component.ts | 2 +- .../components/header/mobile.component.ts | 2 +- .../components/skeleton-list.component.ts | 26 -- .../apps/portal/constants/system-utilities.ts | 4 - .../config-dep.component.ts | 0 .../service => }/modals/config.component.ts | 17 +- .../apps/portal/pipes/to-navigation-item.ts | 17 -- .../src/app/apps/portal/portal.component.ts | 4 +- .../ui/src/app/apps/portal/portal.routes.ts | 8 +- .../routes/dashboard/controls.component.ts | 100 +++++++ .../routes/dashboard/dashboard.component.ts | 89 +++++++ .../routes/dashboard/metrics.component.ts | 42 +++ .../routes/dashboard/service.component.ts | 79 ++++++ .../routes/dashboard/services.component.ts | 86 ++++++ .../routes/dashboard/status.component.ts | 128 +++++++++ .../portal/routes/dashboard/ui.component.ts | 85 ++++++ .../routes/dashboard/utilities.component.ts | 93 +++++++ .../routes/desktop/dektop-loading.service.ts | 37 --- .../routes/desktop/desktop-item.directive.ts | 34 --- .../routes/desktop/desktop.component.html | 44 --- .../routes/desktop/desktop.component.scss | 68 ----- .../routes/desktop/desktop.component.ts | 77 ------ .../service/components/actions.component.ts | 252 ++++-------------- .../service/components/interface.component.ts | 22 +- .../service/components/status.component.ts | 17 +- .../routes/service/pipes/to-menu.pipe.ts | 12 +- .../service/routes/actions.component.ts | 5 +- .../routes/service/routes/outlet.component.ts | 2 +- .../service/routes/service.component.ts | 132 ++++----- .../portal/routes/service/service.module.ts | 2 +- .../service/types/package-config-data.ts | 6 - .../backups/services/restore.service.ts | 2 +- .../settings/components/menu.component.ts | 8 +- .../apps/portal/services/actions.service.ts | 138 ++++++++++ .../app/apps/portal/services/badge.service.ts | 13 +- .../apps/portal/services/desktop.service.ts | 53 ---- .../apps/portal/services/services.service.ts | 11 +- .../app/apps/portal/types/navigation-item.ts | 5 - .../apps/portal/utils/to-navigation-item.ts | 7 +- .../app/common/svg-definitions.component.ts | 14 + .../ui/src/app/services/config.service.ts | 12 +- web/projects/ui/src/styles.scss | 51 ++-- 47 files changed, 1093 insertions(+), 1016 deletions(-) delete mode 100644 web/projects/ui/src/app/apps/portal/components/drawer/drawer-item.directive.ts delete mode 100644 web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.html delete mode 100644 web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.scss delete mode 100644 web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.ts delete mode 100644 web/projects/ui/src/app/apps/portal/components/skeleton-list.component.ts rename web/projects/ui/src/app/apps/portal/{routes/service/components => modals}/config-dep.component.ts (100%) rename web/projects/ui/src/app/apps/portal/{routes/service => }/modals/config.component.ts (95%) delete mode 100644 web/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/controls.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/dashboard.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/metrics.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/service.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/services.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/status.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/ui.component.ts create mode 100644 web/projects/ui/src/app/apps/portal/routes/dashboard/utilities.component.ts delete mode 100644 web/projects/ui/src/app/apps/portal/routes/desktop/dektop-loading.service.ts delete mode 100644 web/projects/ui/src/app/apps/portal/routes/desktop/desktop-item.directive.ts delete mode 100644 web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html delete mode 100644 web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.scss delete mode 100644 web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.ts delete mode 100644 web/projects/ui/src/app/apps/portal/routes/service/types/package-config-data.ts create mode 100644 web/projects/ui/src/app/apps/portal/services/actions.service.ts delete mode 100644 web/projects/ui/src/app/apps/portal/services/desktop.service.ts delete mode 100644 web/projects/ui/src/app/apps/portal/types/navigation-item.ts diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index ed89a5be9..5adcc6739 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -195,3 +195,9 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] { backdrop-filter: blur(1rem); background: rgb(34 34 34 / 80%); } + +// TODO: Move to Taiga UI +a[tuiIconButton]:not([href]) { + pointer-events: none; + opacity: var(--tui-disabled-opacity); +} diff --git a/web/projects/ui/src/app/apps/portal/components/drawer/drawer-item.directive.ts b/web/projects/ui/src/app/apps/portal/components/drawer/drawer-item.directive.ts deleted file mode 100644 index 6e29ff0ca..000000000 --- a/web/projects/ui/src/app/apps/portal/components/drawer/drawer-item.directive.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - Directive, - ElementRef, - HostListener, - inject, - Input, -} from '@angular/core' -import { tuiGetActualTarget, tuiIsElement, tuiPx } from '@taiga-ui/cdk' -import { DrawerComponent } from './drawer.component' -import { DesktopService } from '../../services/desktop.service' -import { TuiAlertService } from '@taiga-ui/core' - -/** - * This directive is responsible for drag and drop of the drawer item. - * It saves item to desktop when dropped. - */ -@Directive({ - selector: '[drawerItem]', - standalone: true, - host: { - '[style.userSelect]': '"none"', - '[style.touchAction]': '"none"', - }, -}) -export class DrawerItemDirective { - private readonly alerts = inject(TuiAlertService) - private readonly desktop = inject(DesktopService) - private readonly drawer = inject(DrawerComponent) - private readonly element: HTMLElement = inject(ElementRef).nativeElement - - private x = NaN - private y = NaN - - @Input() - drawerItem = '' - - @HostListener('pointerdown.silent', ['$event']) - onStart(event: PointerEvent): void { - const target = tuiGetActualTarget(event) - const { x, y, pointerId } = event - const { left, top } = this.element.getBoundingClientRect() - - if (tuiIsElement(target)) { - target.releasePointerCapture(pointerId) - } - - this.drawer.open = false - this.onPointer(x - left, y - top) - } - - @HostListener('document:pointerup.silent') - onPointer(x = NaN, y = NaN): void { - // Some other element is dragged - if (Number.isNaN(this.x) && Number.isNaN(x)) return - - this.x = x - this.y = y - this.process(NaN, NaN) - } - - @HostListener('document:pointermove.silent', ['$event.x', '$event.y']) - onMove(x: number, y: number): void { - // This element is not dragged - if (Number.isNaN(this.x)) return - // This element is already on the desktop - if (this.desktop.items.includes(this.drawerItem)) { - this.onPointer() - this.alerts - .open('This item is already added', { status: 'warning' }) - .subscribe() - - return - } - - this.process(x, y) - this.desktop.add('') - } - - private process(x: number, y: number) { - const { style } = this.element - const { items } = this.desktop - const dragged = !Number.isNaN(this.x + x) - - style.pointerEvents = dragged ? 'none' : '' - style.position = dragged ? 'fixed' : '' - style.top = dragged ? tuiPx(y - this.y) : '' - style.left = dragged ? tuiPx(x - this.x) : '' - - if (dragged || !items.includes('')) { - return - } - - this.desktop.items = items.map(item => item || this.drawerItem) - this.desktop.reorder(this.desktop.order) - } -} diff --git a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.html b/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.html deleted file mode 100644 index 35a941eba..000000000 --- a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.html +++ /dev/null @@ -1,57 +0,0 @@ -
- - - Enter service name - - -

System Utilities

-
- @for ( - item of system | keyvalue | tuiFilter: bySearch : search; - track $index - ) { - - } @empty { - Nothing found - } -
-

Installed services

-
- @for ( - item of (services$ | async) || [] | tuiFilter: bySearch : search; - track $index - ) { - - } @empty { - Nothing found - } -
-
-
diff --git a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.scss b/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.scss deleted file mode 100644 index 49dd3e7ee..000000000 --- a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.scss +++ /dev/null @@ -1,77 +0,0 @@ -@import '@taiga-ui/core/styles/taiga-ui-local'; - -:host { - @include transition(top); - - position: absolute; - top: 100%; - left: 0; - width: 100%; - height: calc(100% - 10.25rem); - display: flex; - flex-direction: column; - // TODO: Theme - background: #2d2d2d; - color: #fff; - - &._open { - top: 10.25rem; - } -} - -.content { - flex: 1; - height: 100%; - display: flex; - flex-direction: column; - background: inherit; -} - -.toggle { - position: absolute; - top: -2.5rem; - height: 2.5rem; - width: 25rem; - max-width: 100vw; - left: 50%; - background: inherit; - color: inherit; - text-align: center; - font-size: 0; - transform: translateX(-50%); - border-top-left-radius: var(--tui-radius-xl); - border-top-right-radius: var(--tui-radius-xl); -} - -.icon { - @include transition(transform); - - :host._open & { - transform: rotate(180deg); - } -} - -.scrollbar { - margin-top: 1rem; -} - -.search { - width: 25rem; - margin: 6rem auto 0; -} - -.title { - margin: 4rem 0 1.25rem; - text-align: center; - text-transform: uppercase; - font: var(--tui-font-text-xl); -} - -.items { - display: flex; - gap: 2rem; - padding: 2rem; - align-items: center; - justify-content: center; - flex-wrap: wrap; -} diff --git a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.ts b/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.ts deleted file mode 100644 index 5f2cc0b78..000000000 --- a/web/projects/ui/src/app/apps/portal/components/drawer/drawer.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CommonModule } from '@angular/common' -import { - ChangeDetectionStrategy, - Component, - HostBinding, - inject, -} from '@angular/core' -import { FormsModule } from '@angular/forms' -import { RouterLink } from '@angular/router' -import { - TUI_DEFAULT_MATCHER, - TuiActiveZoneModule, - TuiFilterPipeModule, - TuiForModule, -} from '@taiga-ui/cdk' -import { - TuiScrollbarModule, - TuiTextfieldControllerModule, -} from '@taiga-ui/core' -import { TuiIconModule } from '@taiga-ui/experimental' -import { TuiInputModule } from '@taiga-ui/kit' -import { CardComponent } from '../card.component' -import { ServicesService } from '../../services/services.service' -import { toRouterLink } from '../../utils/to-router-link' -import { DrawerItemDirective } from './drawer-item.directive' -import { SYSTEM_UTILITIES } from '../../constants/system-utilities' -import { ToBadgePipe } from '../../pipes/to-badge' - -@Component({ - selector: 'app-drawer', - templateUrl: 'drawer.component.html', - styleUrls: ['drawer.component.scss'], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - CommonModule, - FormsModule, - RouterLink, - TuiScrollbarModule, - TuiActiveZoneModule, - TuiInputModule, - TuiTextfieldControllerModule, - TuiForModule, - TuiFilterPipeModule, - CardComponent, - DrawerItemDirective, - ToBadgePipe, - TuiIconModule, - ], -}) -export class DrawerComponent { - @HostBinding('class._open') - open = false - - search = '' - - readonly system = SYSTEM_UTILITIES - readonly services$ = inject(ServicesService) - - readonly bySearch = (item: any, search: string): boolean => - search.length < 2 || - TUI_DEFAULT_MATCHER(item.manifest?.title || item.value?.title || '', search) - - getLink(id: string): string { - return toRouterLink(id) - } -} diff --git a/web/projects/ui/src/app/apps/portal/components/header/header.component.ts b/web/projects/ui/src/app/apps/portal/components/header/header.component.ts index 3da3472d6..b12380702 100644 --- a/web/projects/ui/src/app/apps/portal/components/header/header.component.ts +++ b/web/projects/ui/src/app/apps/portal/components/header/header.component.ts @@ -18,7 +18,7 @@ import { BreadcrumbsService } from '../../services/breadcrumbs.service' @Component({ selector: 'header[appHeader]', template: ` - +
@for (item of breadcrumbs$ | async; track $index) { diff --git a/web/projects/ui/src/app/apps/portal/components/header/mobile.component.ts b/web/projects/ui/src/app/apps/portal/components/header/mobile.component.ts index e47815656..391fb6141 100644 --- a/web/projects/ui/src/app/apps/portal/components/header/mobile.component.ts +++ b/web/projects/ui/src/app/apps/portal/components/header/mobile.component.ts @@ -62,7 +62,7 @@ export class HeaderMobileComponent { get back() { return ( this.headerMobile?.[this.headerMobile?.length - 2]?.routerLink || - '/portal/desktop' + '/portal/dashboard' ) } } diff --git a/web/projects/ui/src/app/apps/portal/components/skeleton-list.component.ts b/web/projects/ui/src/app/apps/portal/components/skeleton-list.component.ts deleted file mode 100644 index b3fca5e06..000000000 --- a/web/projects/ui/src/app/apps/portal/components/skeleton-list.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, Input } from '@angular/core' -import { TuiRepeatTimesModule } from '@taiga-ui/cdk' - -@Component({ - selector: 'skeleton-list', - template: ` -
-
-
-
-
- `, - standalone: true, - imports: [TuiRepeatTimesModule], -}) -export class SkeletonListComponent { - @Input() rows = 3 - @Input() showAvatar = false -} diff --git a/web/projects/ui/src/app/apps/portal/constants/system-utilities.ts b/web/projects/ui/src/app/apps/portal/constants/system-utilities.ts index b71ee8e01..e29bc1059 100644 --- a/web/projects/ui/src/app/apps/portal/constants/system-utilities.ts +++ b/web/projects/ui/src/app/apps/portal/constants/system-utilities.ts @@ -24,10 +24,6 @@ export const SYSTEM_UTILITIES: Record = icon: 'tuiIconTool', title: 'Settings', }, - '/portal/system/snek': { - icon: 'assets/img/icon_transparent.png', - title: 'Snek', - }, '/portal/system/notifications': { icon: 'tuiIconBell', title: 'Notifications', diff --git a/web/projects/ui/src/app/apps/portal/routes/service/components/config-dep.component.ts b/web/projects/ui/src/app/apps/portal/modals/config-dep.component.ts similarity index 100% rename from web/projects/ui/src/app/apps/portal/routes/service/components/config-dep.component.ts rename to web/projects/ui/src/app/apps/portal/modals/config-dep.component.ts diff --git a/web/projects/ui/src/app/apps/portal/routes/service/modals/config.component.ts b/web/projects/ui/src/app/apps/portal/modals/config.component.ts similarity index 95% rename from web/projects/ui/src/app/apps/portal/routes/service/modals/config.component.ts rename to web/projects/ui/src/app/apps/portal/modals/config.component.ts index 43f874c47..d56e0e6f8 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/modals/config.component.ts +++ b/web/projects/ui/src/app/apps/portal/modals/config.component.ts @@ -20,6 +20,7 @@ import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { compare, Operation } from 'fast-json-patch' import { PatchDB } from 'patch-db-client' import { endWith, firstValueFrom, Subscription } from 'rxjs' +import { ConfigDepComponent } from 'src/app/apps/portal/modals/config-dep.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel, @@ -33,16 +34,16 @@ import { ActionButton, FormComponent, } from 'src/app/apps/portal/components/form.component' -import { PackageConfigData } from '../types/package-config-data' -import { ConfigDepComponent } from '../components/config-dep.component' +import { DependentInfo } from 'src/app/types/dependent-info' + +export interface PackageConfigData { + readonly pkgId: string + readonly dependentInfo?: DependentInfo +} @Component({ template: ` - + + /> No config options for {{ pkg.manifest.title }} diff --git a/web/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts b/web/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts deleted file mode 100644 index 39d1d91c7..000000000 --- a/web/projects/ui/src/app/apps/portal/pipes/to-navigation-item.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { NavigationItem } from '../types/navigation-item' -import { toNavigationItem } from '../utils/to-navigation-item' - -@Pipe({ - name: 'toNavigationItem', - standalone: true, -}) -export class ToNavigationItemPipe implements PipeTransform { - transform( - packages: Record, - id: string, - ): NavigationItem | null { - return id ? toNavigationItem(id, packages) : null - } -} diff --git a/web/projects/ui/src/app/apps/portal/portal.component.ts b/web/projects/ui/src/app/apps/portal/portal.component.ts index b609af43e..6592e2320 100644 --- a/web/projects/ui/src/app/apps/portal/portal.component.ts +++ b/web/projects/ui/src/app/apps/portal/portal.component.ts @@ -7,7 +7,6 @@ import { PatchDB } from 'patch-db-client' import { filter } from 'rxjs' import { DataModel } from 'src/app/services/patch-db/data-model' import { HeaderComponent } from './components/header/header.component' -import { DrawerComponent } from './components/drawer/drawer.component' import { BreadcrumbsService } from './services/breadcrumbs.service' @Component({ @@ -15,7 +14,6 @@ import { BreadcrumbsService } from './services/breadcrumbs.service' template: `
{{ name$ | async }}
- `, styles: [ ` @@ -32,7 +30,7 @@ import { BreadcrumbsService } from './services/breadcrumbs.service' `, ], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, RouterOutlet, HeaderComponent, DrawerComponent], + imports: [CommonModule, RouterOutlet, HeaderComponent], providers: [ // TODO: Move to global tuiDropdownOptionsProvider({ diff --git a/web/projects/ui/src/app/apps/portal/portal.routes.ts b/web/projects/ui/src/app/apps/portal/portal.routes.ts index 20db07d63..57f1a6476 100644 --- a/web/projects/ui/src/app/apps/portal/portal.routes.ts +++ b/web/projects/ui/src/app/apps/portal/portal.routes.ts @@ -7,15 +7,15 @@ const ROUTES: Routes = [ component: PortalComponent, children: [ { - redirectTo: 'desktop', + redirectTo: 'dashboard', pathMatch: 'full', path: '', }, { - path: 'desktop', + path: 'dashboard', loadComponent: () => - import('./routes/desktop/desktop.component').then( - m => m.DesktopComponent, + import('./routes/dashboard/dashboard.component').then( + m => m.DashboardComponent, ), }, { diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/controls.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/controls.component.ts new file mode 100644 index 000000000..ed037df29 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/controls.component.ts @@ -0,0 +1,100 @@ +import { AsyncPipe } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { TuiLetModule, tuiPure } from '@taiga-ui/cdk' +import { + TuiButtonModule, + tuiButtonOptionsProvider, +} from '@taiga-ui/experimental' +import { map, of } from 'rxjs' +import { UIComponent } from 'src/app/apps/portal/routes/dashboard/ui.component' +import { ActionsService } from 'src/app/apps/portal/services/actions.service' +import { DepErrorService } from 'src/app/services/dep-error.service' +import { + PackageDataEntry, + PackageMainStatus, +} from 'src/app/services/patch-db/data-model' + +@Component({ + standalone: true, + selector: 'fieldset[appControls]', + template: ` + @if (isRunning) { + + + + } @else { + + + + } + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiButtonModule, UIComponent, TuiLetModule, AsyncPipe], + providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })], +}) +export class ControlsComponent { + private readonly errors = inject(DepErrorService) + + @Input() + appControls!: PackageDataEntry + + readonly actions = inject(ActionsService) + + get isRunning(): boolean { + return ( + this.appControls.installed?.status.main.status === + PackageMainStatus.Running + ) + } + + get isConfigured(): boolean { + return !!this.appControls.installed?.status.configured + } + + @tuiPure + hasUnmet({ installed, manifest }: PackageDataEntry) { + return installed + ? this.errors.getPkgDepErrors$(manifest.id).pipe( + map(errors => + Object.keys(installed['current-dependencies']) + .filter(id => !!manifest.dependencies[id]) + .map(id => !!(errors[manifest.id] as any)?.[id]) // @TODO fix + .some(Boolean), + ), + ) + : of(false) + } +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/dashboard.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/dashboard.component.ts new file mode 100644 index 000000000..531112839 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/dashboard.component.ts @@ -0,0 +1,89 @@ +import { DatePipe } from '@angular/common' +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' +import { TuiIconModule } from '@taiga-ui/experimental' +import { map, timer } from 'rxjs' +import { MetricsComponent } from './metrics.component' +import { ServicesComponent } from './services.component' +import { UtilitiesComponent } from './utilities.component' + +@Component({ + standalone: true, + template: ` + + +

+ + Metrics +

+
+
+ +

+ + Utilities +

+
+
+ +

+ + Services +

+
+
+ `, + styles: ` + :host { + position: relative; + max-width: 64rem; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + margin: 2rem auto; + border: 0.375rem solid transparent; + } + + app-metrics, + app-utilities, + app-services { + position: relative; + clip-path: var(--clip-path); + backdrop-filter: blur(1rem); + font-size: 1rem; + } + + time { + position: absolute; + left: 22%; + font-weight: bold; + line-height: 1.75rem; + } + + h2 { + height: 2rem; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + padding: 0 2rem; + font-weight: bold; + font-size: 1rem; + + tui-icon { + font-size: 1rem; + } + } + `, + imports: [ + ServicesComponent, + MetricsComponent, + UtilitiesComponent, + TuiIconModule, + DatePipe, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DashboardComponent { + readonly date = toSignal(timer(0, 1000).pipe(map(() => new Date()))) +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/metrics.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/metrics.component.ts new file mode 100644 index 000000000..017a6322c --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/metrics.component.ts @@ -0,0 +1,42 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + standalone: true, + selector: 'app-metrics', + template: ` + +
TODO
+ `, + styles: ` + :host { + grid-column: 1/3; + + --clip-path: polygon( + 0 2rem, + 1.25rem 0, + 8.75rem 0, + calc(10rem + 0.1em) calc(2rem - 0.1em), + 11rem 2rem, + calc(65% - 0.2em) 2rem, + calc(65% + 1.25rem) 0, + calc(100% - 1.25rem) 0, + 100% 2rem, + 100% calc(100% - 2rem), + calc(100% - 1.25rem) 100%, + 10.5rem 100%, + calc(9.25rem - 0.1em) calc(100% - 2rem + 0.1em), + 1.25rem calc(100% - 2rem), + 0 calc(100% - 4rem) + ); + + section { + height: 80%; + display: flex; + align-items: center; + justify-content: center; + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MetricsComponent {} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/service.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/service.component.ts new file mode 100644 index 000000000..299578bc8 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/service.component.ts @@ -0,0 +1,79 @@ +import { AsyncPipe } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { RouterLink } from '@angular/router' +import { tuiPure } from '@taiga-ui/cdk' +import { ControlsComponent } from 'src/app/apps/portal/routes/dashboard/controls.component' +import { StatusComponent } from 'src/app/apps/portal/routes/dashboard/status.component' +import { ConnectionService } from 'src/app/services/connection.service' +import { PkgDependencyErrors } from 'src/app/services/dep-error.service' +import { + PackageDataEntry, + PackageState, +} from 'src/app/services/patch-db/data-model' +import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' + +@Component({ + standalone: true, + selector: 'tr[appService]', + template: ` + logo + + {{ appService.manifest.title }} + + {{ appService.manifest.version }} + + +
+ + `, + styles: ` + img { + height: 2rem; + width: 2rem; + border-radius: 100%; + } + + td { + padding: 0.5rem; + } + + a { + color: var(--tui-text-01); + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [RouterLink, AsyncPipe, StatusComponent, ControlsComponent], +}) +export class ServiceComponent { + @Input() + appService!: PackageDataEntry + + @Input() + appServiceError?: PkgDependencyErrors + + readonly connected$ = inject(ConnectionService).connected$ + + get routerLink() { + return `/portal/service/${this.appService.manifest.id}` + } + + get installed(): boolean { + return this.appService.state === PackageState.Installed + } + + @tuiPure + hasError(errors: PkgDependencyErrors = {}): boolean { + return Object.values(errors).some(Boolean) + } +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/services.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/services.component.ts new file mode 100644 index 000000000..8769ae101 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/services.component.ts @@ -0,0 +1,86 @@ +import { AsyncPipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { ServiceComponent } from 'src/app/apps/portal/routes/dashboard/service.component' +import { ServicesService } from 'src/app/apps/portal/services/services.service' +import { DepErrorService } from 'src/app/services/dep-error.service' + +@Component({ + standalone: true, + selector: 'app-services', + template: ` + + + + + + + + + + + + + @if (errors$ | async; as errors) { + @for (service of services$ | async; track $index) { + + } @empty { + + + + } + } + +
NameVersionStatusControls
No services installed
+ `, + styles: ` + :host { + grid-column: 1/4; + margin-top: -2rem; + + --clip-path: polygon( + 0 2rem, + 1.25rem 0, + 8.75rem 0, + calc(10rem + 0.1em) calc(2rem - 0.1em), + calc(100% - 1.25rem) 2rem, + 100% 4rem, + 100% calc(100% - 2rem), + calc(100% - 1.25rem) 100%, + 1.25rem 100%, + 0 calc(100% - 2rem) + ); + } + + table { + width: calc(100% - 4rem); + margin: 2rem; + } + + tr:not(:last-child) { + box-shadow: inset 0 -1px var(--tui-clear); + } + + th { + text-transform: uppercase; + color: var(--tui-text-02); + font: var(--tui-font-text-s); + font-weight: bold; + text-align: left; + padding: 0 0.5rem; + } + + td { + text-align: center; + padding: 1rem; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ServiceComponent, AsyncPipe], +}) +export class ServicesComponent { + readonly services$ = inject(ServicesService) + readonly errors$ = inject(DepErrorService).depErrors$ +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/status.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/status.component.ts new file mode 100644 index 000000000..d398385e9 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/status.component.ts @@ -0,0 +1,128 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { tuiPure } from '@taiga-ui/cdk' +import { TuiLoaderModule } from '@taiga-ui/core' +import { TuiIconModule } from '@taiga-ui/experimental' +import { + PackageDataEntry, + PackageState, +} from 'src/app/services/patch-db/data-model' +import { + HealthStatus, + PrimaryStatus, + renderPkgStatus, +} from 'src/app/services/pkg-status-rendering.service' +import { packageLoadingProgress } from 'src/app/util/package-loading-progress' + +@Component({ + standalone: true, + selector: 'td[appStatus]', + template: ` + @if (loading) { + + } @else { + @if (healthy) { + + } @else { + + } + } + {{ status }} + `, + styles: ` + :host { + display: flex; + align-items: center; + gap: 0.5rem; + height: 3rem; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiIconModule, TuiLoaderModule], +}) +export class StatusComponent { + @Input() + appStatus!: PackageDataEntry + + @Input() + appStatusError = false + + get healthy(): boolean { + const status = this.getStatus(this.appStatus) + + return ( + !this.appStatusError && // no deps error + !!this.appStatus.installed?.status.configured && // no config needed + status.primary !== PackageState.NeedsUpdate && // no update needed + status.health !== HealthStatus.Failure // no health issues + ) + } + + get loading(): boolean { + return ( + !!this.appStatus['install-progress'] || + this.color === 'var(--tui-info-fill)' + ) + } + + @tuiPure + getStatus(pkg: PackageDataEntry) { + return renderPkgStatus(pkg, {}) + } + + get status(): string { + if (this.appStatus['install-progress']) { + return `Installing... ${packageLoadingProgress(this.appStatus['install-progress'])?.totalProgress || 0}%` + } + + switch (this.getStatus(this.appStatus).primary) { + case PrimaryStatus.Running: + return 'Running' + case PrimaryStatus.Stopped: + return 'Stopped' + case PackageState.NeedsUpdate: + return 'Needs Update' + case PrimaryStatus.NeedsConfig: + return 'Needs Config' + case PrimaryStatus.Updating: + return 'Updating...' + case PrimaryStatus.Stopping: + return 'Stopping...' + case PrimaryStatus.Starting: + return 'Starting...' + case PrimaryStatus.BackingUp: + return 'Backing Up...' + case PrimaryStatus.Restarting: + return 'Restarting...' + case PrimaryStatus.Removing: + return 'Removing...' + case PrimaryStatus.Restoring: + return 'Restoring...' + default: + return 'Unknown' + } + } + + get color(): string { + if (this.appStatus['install-progress']) { + return 'var(--tui-info-fill)' + } + + switch (this.getStatus(this.appStatus).primary) { + case PrimaryStatus.Running: + return 'var(--tui-success-fill)' + case PackageState.NeedsUpdate: + case PrimaryStatus.NeedsConfig: + return 'var(--tui-warning-fill)' + case PrimaryStatus.Updating: + case PrimaryStatus.Stopping: + case PrimaryStatus.Starting: + case PrimaryStatus.BackingUp: + case PrimaryStatus.Restarting: + case PrimaryStatus.Removing: + case PrimaryStatus.Restoring: + return 'var(--tui-info-fill)' + default: + return 'var(--tui-text-02)' + } + } +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/ui.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/ui.component.ts new file mode 100644 index 000000000..265f799bf --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/ui.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { tuiPure } from '@taiga-ui/cdk' +import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { ConfigService } from 'src/app/services/config.service' +import { + InstalledPackageInfo, + InterfaceInfo, + PackageDataEntry, + PackageMainStatus, +} from 'src/app/services/patch-db/data-model' + +@Component({ + standalone: true, + selector: 'app-ui', + template: ` + @if (interfaces.length > 1) { + + + + + @for (interface of interfaces; track $index) { + + {{ interface.name }} + + } + + + + } @else { + + {{ interfaces[0]?.name }} + + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiButtonModule, TuiHostedDropdownModule, TuiDataListModule], +}) +export class UIComponent { + private readonly config = inject(ConfigService) + + @Input() + pkg!: PackageDataEntry + + get interfaces(): readonly InterfaceInfo[] { + return this.getInterfaces(this.pkg.installed) + } + + get isRunning(): boolean { + return this.pkg.installed?.status.main.status === PackageMainStatus.Running + } + + @tuiPure + getInterfaces(info?: InstalledPackageInfo): InterfaceInfo[] { + return info + ? Object.values(info.interfaceInfo).filter(({ type }) => type === 'ui') + : [] + } + + getHref(info?: InterfaceInfo): string | null { + return info && this.isRunning ? this.config.launchableAddress(info) : null + } +} diff --git a/web/projects/ui/src/app/apps/portal/routes/dashboard/utilities.component.ts b/web/projects/ui/src/app/apps/portal/routes/dashboard/utilities.component.ts new file mode 100644 index 000000000..d2f7b4539 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/routes/dashboard/utilities.component.ts @@ -0,0 +1,93 @@ +import { AsyncPipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { RouterLink } from '@angular/router' +import { + TuiBadgeNotificationModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { SYSTEM_UTILITIES } from 'src/app/apps/portal/constants/system-utilities' +import { BadgeService } from 'src/app/apps/portal/services/badge.service' + +@Component({ + standalone: true, + selector: 'app-utilities', + template: ` + + + `, + styles: ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + :host { + --clip-path: polygon( + 0 2rem, + 1.25rem 0, + 8.75rem 0, + calc(10rem + 0.1em) calc(2rem - 0.1em), + calc(100% - 1.25rem) 2rem, + 100% 4rem, + 100% calc(100% - 2rem), + calc(100% - 1.25rem) 100%, + 1.25rem 100%, + 0 calc(100% - 2rem) + ); + } + + .links { + display: grid; + grid-template: 1fr 1fr / 1fr 1fr 1fr; + gap: 0.75rem; + padding: 1.5rem; + font-size: min(0.75rem, 1.25vw); + } + + .link { + @include transition(background); + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + aspect-ratio: 1/1; + border-radius: 0.25rem; + border: 1px solid var(--tui-clear); + + tui-icon { + width: 50%; + height: 50%; + } + + tui-badge-notification { + position: absolute; + top: 10%; + right: 10%; + } + + &:hover { + background: var(--tui-clear); + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiIconModule, RouterLink, TuiBadgeNotificationModule, AsyncPipe], +}) +export class UtilitiesComponent { + private readonly badge = inject(BadgeService) + readonly items = Object.keys(SYSTEM_UTILITIES) + .filter(key => key !== '/portal/system/notifications') + .map(key => ({ + ...SYSTEM_UTILITIES[key], + routerLink: key, + notification$: this.badge.getCount(key), + })) +} diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/dektop-loading.service.ts b/web/projects/ui/src/app/apps/portal/routes/desktop/dektop-loading.service.ts deleted file mode 100644 index f0e50f0f2..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/dektop-loading.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { inject, Injectable } from '@angular/core' -import { PatchDB } from 'patch-db-client' -import { - endWith, - ignoreElements, - Observable, - shareReplay, - startWith, - take, - tap, -} from 'rxjs' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { DesktopService } from '../../services/desktop.service' - -/** - * This service loads initial values for desktop items - * and is used to show loading indicator. - */ -@Injectable({ - providedIn: 'root', -}) -export class DektopLoadingService extends Observable { - private readonly desktop = inject(DesktopService) - private readonly patch = inject(PatchDB) - private readonly loading = this.patch.watch$('ui', 'desktop').pipe( - take(1), - tap(items => (this.desktop.items = items.filter(Boolean))), - ignoreElements(), - startWith(true), - endWith(false), - shareReplay(1), - ) - - constructor() { - super(subscriber => this.loading.subscribe(subscriber)) - } -} diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop-item.directive.ts b/web/projects/ui/src/app/apps/portal/routes/desktop/desktop-item.directive.ts deleted file mode 100644 index f54fc39fa..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop-item.directive.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - Directive, - ElementRef, - HostBinding, - inject, - Input, - OnInit, -} from '@angular/core' -import { TuiTilesComponent } from '@taiga-ui/kit' - -/** - * This directive is responsible for creating empty placeholder - * on the desktop when item is dragged from the drawer - */ -@Directive({ - selector: '[desktopItem]', - standalone: true, -}) -export class DesktopItemDirective implements OnInit { - private readonly element: Element = inject(ElementRef).nativeElement - private readonly tiles = inject(TuiTilesComponent) - - @Input() - desktopItem = '' - - @HostBinding('class._empty') - get empty(): boolean { - return !this.desktopItem - } - - ngOnInit() { - if (this.empty) this.tiles.element = this.element - } -} diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html b/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html deleted file mode 100644 index 0945fda7e..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.html +++ /dev/null @@ -1,44 +0,0 @@ - - - -
- - - - - -
-
diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.scss b/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.scss deleted file mode 100644 index dd8bfe078..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.scss +++ /dev/null @@ -1,68 +0,0 @@ -@import '@taiga-ui/core/styles/taiga-ui-local'; - -:host { - display: flex; - align-items: center; - align-content: center; - justify-content: center; - height: 100%; - max-width: calc(100vw - 4rem); - margin: 0 auto; -} - -.loader { - height: 10rem; - width: 10rem; -} - -.fade { - @include scrollbar-hidden(); - - width: 100%; - height: calc(100% - 4rem); - display: flex; - flex-direction: column; - overflow: auto; -} - -.tiles { - width: 100%; - justify-content: center; - grid-template-columns: repeat(auto-fit, 12.5rem); - grid-auto-rows: min-content; - gap: 2rem; - margin: auto; - - &::after { - content: ''; - grid-column: 1; - height: 1rem; - order: 999; - } -} - -.remove { - @include transition(background); - position: fixed; - bottom: 0; - left: calc(50% - 3rem); - width: 6rem; - height: 6rem; - border-radius: 100%; - background: var(--tui-base-02); - z-index: 10; - - &:hover { - background: var(--tui-base-01); - } -} - -.item { - height: 5.5rem; - - &._dragged, - &._empty { - border-radius: var(--tui-radius-l); - box-shadow: inset 0 0 0 0.5rem var(--tui-clear-active); - } -} diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.ts b/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.ts deleted file mode 100644 index 589146a7e..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { CommonModule } from '@angular/common' -import { - Component, - ElementRef, - inject, - QueryList, - ViewChild, - ViewChildren, -} from '@angular/core' -import { RouterModule } from '@angular/router' -import { DragScrollerDirective } from '@start9labs/shared' -import { EMPTY_QUERY, TUI_PARENT_STOP } from '@taiga-ui/cdk' -import { - tuiFadeIn, - TuiLoaderModule, - tuiScaleIn, - TuiSvgModule, -} from '@taiga-ui/core' -import { TuiFadeModule } from '@taiga-ui/experimental' -import { - TuiTileComponent, - TuiTilesComponent, - TuiTilesModule, -} from '@taiga-ui/kit' -import { PatchDB } from 'patch-db-client' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { DesktopService } from '../../services/desktop.service' -import { DektopLoadingService } from './dektop-loading.service' -import { CardComponent } from '../../components/card.component' -import { DesktopItemDirective } from './desktop-item.directive' -import { ToNavigationItemPipe } from '../../pipes/to-navigation-item' -import { ToBadgePipe } from '../../pipes/to-badge' - -@Component({ - standalone: true, - templateUrl: 'desktop.component.html', - styleUrls: ['desktop.component.scss'], - animations: [TUI_PARENT_STOP, tuiScaleIn, tuiFadeIn], - imports: [ - CommonModule, - RouterModule, - CardComponent, - DesktopItemDirective, - TuiSvgModule, - TuiLoaderModule, - TuiTilesModule, - ToNavigationItemPipe, - TuiFadeModule, - DragScrollerDirective, - ToBadgePipe, - ], -}) -export class DesktopComponent { - @ViewChildren(TuiTileComponent, { read: ElementRef }) - private readonly tiles: QueryList = EMPTY_QUERY - - readonly desktop = inject(DesktopService) - readonly loading$ = inject(DektopLoadingService) - readonly packages$ = inject(PatchDB).watch$('package-data') - - @ViewChild(TuiTilesComponent) - readonly tile?: TuiTilesComponent - - onRemove() { - const element = this.tile?.element - const index = this.tiles - .toArray() - .map(({ nativeElement }) => nativeElement) - .indexOf(element) - - this.desktop.remove(this.desktop.items[index]) - } - - onReorder(order: Map) { - this.desktop.reorder(order) - } -} diff --git a/web/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts index 5a3582b31..f385cd0b3 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/components/actions.component.ts @@ -1,228 +1,94 @@ -import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { TuiButtonModule } from '@taiga-ui/experimental' -import { TUI_PROMPT } from '@taiga-ui/kit' -import { filter } from 'rxjs' import { - InterfaceInfo, + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { tuiPure } from '@taiga-ui/cdk' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info' +import { ActionsService } from 'src/app/apps/portal/services/actions.service' +import { + PackageDataEntry, PackageMainStatus, - PackagePlus, } from 'src/app/services/patch-db/data-model' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { FormDialogService } from 'src/app/services/form-dialog.service' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { ServiceConfigModal } from '../modals/config.component' -import { PackageConfigData } from '../types/package-config-data' @Component({ selector: 'service-actions', template: ` - + @if (isRunning) { + - + + } - + @if (isStopped && isConfigured) { + + } - + @if (!isConfigured) { + + } `, styles: [':host { display: flex; gap: 1rem }'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TuiButtonModule], + imports: [TuiButtonModule], }) export class ServiceActionsComponent { @Input({ required: true }) - service!: PackagePlus + service!: PackageDataEntry - constructor( - private readonly dialogs: TuiDialogService, - private readonly errorService: ErrorService, - private readonly loader: LoadingService, - private readonly embassyApi: ApiService, - private readonly formDialog: FormDialogService, - ) {} + @Input({ required: true }) + dependencies: readonly DependencyInfo[] = [] - private get id(): string { - return this.service.pkg.manifest.id - } - - get interfaceInfo(): Record { - return this.service.pkg.installed!['interfaceInfo'] - } + readonly actions = inject(ActionsService) get isConfigured(): boolean { - return this.service.pkg.installed!.status.configured + return this.service.installed!.status.configured } get isRunning(): boolean { return ( - this.service.pkg.installed?.status.main.status === - PackageMainStatus.Running + this.service.installed?.status.main.status === PackageMainStatus.Running ) } get isStopped(): boolean { return ( - this.service.pkg.installed?.status.main.status === - PackageMainStatus.Stopped + this.service.installed?.status.main.status === PackageMainStatus.Stopped ) } - presentModalConfig(): void { - this.formDialog.open(ServiceConfigModal, { - label: `${this.service.pkg.manifest.title} configuration`, - data: { pkgId: this.id }, - }) - } - - async tryStart(): Promise { - const pkg = this.service.pkg - if (Object.values(this.service.dependencies).some(dep => !!dep.errorText)) { - const depErrMsg = `${pkg.manifest.title} has unmet dependencies. It will not work as expected.` - const proceed = await this.presentAlertStart(depErrMsg) - - if (!proceed) return - } - - const alertMsg = pkg.manifest.alerts.start - - if (alertMsg) { - const proceed = await this.presentAlertStart(alertMsg) - - if (!proceed) return - } - - this.start() - } - - async tryStop(): Promise { - const { title, alerts } = this.service.pkg.manifest - - let content = alerts.stop || '' - if (hasCurrentDeps(this.service.pkg)) { - const depMessage = `Services that depend on ${title} will no longer work properly and may crash` - content = content ? `${content}.\n\n${depMessage}` : depMessage - } - - if (content) { - this.dialogs - .open(TUI_PROMPT, { - label: 'Warning', - size: 's', - data: { - content, - yes: 'Stop', - no: 'Cancel', - }, - }) - .pipe(filter(Boolean)) - .subscribe(() => this.stop()) - } else { - this.stop() - } - } - - async tryRestart(): Promise { - if (hasCurrentDeps(this.service.pkg)) { - this.dialogs - .open(TUI_PROMPT, { - label: 'Warning', - size: 's', - data: { - content: `Services that depend on ${this.service.pkg.manifest} may temporarily experiences issues`, - yes: 'Restart', - no: 'Cancel', - }, - }) - .pipe(filter(Boolean)) - .subscribe(() => this.restart()) - } else { - this.restart() - } - } - - private async start(): Promise { - const loader = this.loader.open(`Starting...`).subscribe() - - try { - await this.embassyApi.startPackage({ id: this.id }) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } - - private async stop(): Promise { - const loader = this.loader.open(`Stopping...`).subscribe() - - try { - await this.embassyApi.stopPackage({ id: this.id }) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } - - private async restart(): Promise { - const loader = this.loader.open(`Restarting...`).subscribe() - - try { - await this.embassyApi.restartPackage({ id: this.id }) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } - - private async presentAlertStart(content: string): Promise { - return new Promise(async resolve => { - this.dialogs - .open(TUI_PROMPT, { - label: 'Warning', - size: 's', - data: { - content, - yes: 'Continue', - no: 'Cancel', - }, - }) - .subscribe(response => resolve(response)) - }) + @tuiPure + hasUnmet(dependencies: readonly DependencyInfo[]): boolean { + return dependencies.some(dep => !!dep.errorText) } } diff --git a/web/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts index 45ded6d6d..81da0b63f 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/components/interface.component.ts @@ -1,4 +1,4 @@ -import { DOCUMENT, CommonModule } from '@angular/common' +import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, @@ -8,7 +8,6 @@ import { import { TuiSvgModule } from '@taiga-ui/core' import { TuiButtonModule } from '@taiga-ui/experimental' import { ConfigService } from 'src/app/services/config.service' -import { InterfaceInfo } from 'src/app/services/patch-db/data-model' import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe' @Component({ @@ -20,22 +19,23 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
{{ info.description }}
{{ info.typeDetail }}
- + [attr.href]="href" + (click.stop)="(0)" + > `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [TuiButtonModule, CommonModule, TuiSvgModule], }) export class ServiceInterfaceComponent { - private readonly document = inject(DOCUMENT) private readonly config = inject(ConfigService) @Input({ required: true, alias: 'serviceInterface' }) @@ -44,11 +44,7 @@ export class ServiceInterfaceComponent { @Input() disabled = false - launchUI(info: InterfaceInfo) { - this.document.defaultView?.open( - this.config.launchableAddress(info), - '_blank', - 'noreferrer', - ) + get href(): string | null { + return this.disabled ? null : this.config.launchableAddress(this.info) } } diff --git a/web/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts index db021af59..2fce9e78e 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/components/status.component.ts @@ -12,18 +12,17 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe' @Component({ selector: 'service-status', template: ` - + @if (installProgress) { + + Installing + + {{ installProgress | installProgress }} + + } @else { {{ connected ? rendering.display : 'Unknown' }} - - - - Installing - - {{ progress }} - - + } `, styles: [ ` diff --git a/web/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts b/web/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts index 12e35d2b8..33e5e06fd 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/pipes/to-menu.pipe.ts @@ -1,19 +1,21 @@ -import { inject, Pipe, PipeTransform, Type } from '@angular/core' +import { inject, Pipe, PipeTransform } from '@angular/core' import { Params } from '@angular/router' import { Manifest } from '@start9labs/marketplace' import { MarkdownComponent } from '@start9labs/shared' import { TuiDialogService } from '@taiga-ui/core' import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { from } from 'rxjs' +import { + PackageConfigData, + ServiceConfigModal, +} from 'src/app/apps/portal/modals/config.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { InstalledPackageInfo, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { ApiService } from 'src/app/services/api/embassy-api.service' -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 { ServiceCredentialsModal } from '../modals/credentials.component' export interface ServiceMenu { diff --git a/web/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts index 52e6371da..8d1e9beae 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/routes/actions.component.ts @@ -25,7 +25,6 @@ import { hasCurrentDeps } from 'src/app/util/has-deps' import { FormDialogService } from 'src/app/services/form-dialog.service' 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' @Component({ @@ -84,7 +83,6 @@ export class ServiceActionsRoute { private readonly router: Router, private readonly patch: PatchDB, private readonly formDialog: FormDialogService, - private readonly desktop: DesktopService, ) {} async handleAction(action: WithId) { @@ -162,8 +160,7 @@ export class ServiceActionsRoute { this.embassyApi .setDbValue(['ack-instructions', this.id], false) .catch(e => console.error('Failed to mark instructions as unseen', e)) - this.desktop.remove(this.id) - this.router.navigate(['portal', 'desktop']) + this.router.navigate(['./portal/dashboard']) } catch (e: any) { this.errorService.handleError(e) } finally { diff --git a/web/projects/ui/src/app/apps/portal/routes/service/routes/outlet.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/routes/outlet.component.ts index 6dcd7a26a..733f796f2 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/routes/outlet.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/routes/outlet.component.ts @@ -28,7 +28,7 @@ export class ServiceOutletComponent { tap(pkg => { // if package disappears, navigate to list page if (!pkg) { - this.router.navigate(['./portal/desktop']) + this.router.navigate(['./portal/dashboard']) } }), ) diff --git a/web/projects/ui/src/app/apps/portal/routes/service/routes/service.component.ts b/web/projects/ui/src/app/apps/portal/routes/service/routes/service.component.ts index 21f6ca148..152767ede 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/routes/service.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/routes/service.component.ts @@ -1,9 +1,17 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ActivatedRoute, NavigationExtras, Router } from '@angular/router' +import { Manifest } from '@start9labs/marketplace' import { getPkgId, isEmptyObject } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' -import { combineLatest, map } from 'rxjs' +import { combineLatest, map, switchMap } from 'rxjs' +import { ConnectionService } from 'src/app/services/connection.service' +import { + DependencyErrorType, + DepErrorService, + PkgDependencyErrors, +} from 'src/app/services/dep-error.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { DataModel, HealthCheckResult, @@ -16,31 +24,24 @@ import { PackageStatus, PrimaryRendering, PrimaryStatus, - StatusRendering, renderPkgStatus, + StatusRendering, } from 'src/app/services/pkg-status-rendering.service' -import { ConnectionService } from 'src/app/services/connection.service' +import { DependentInfo } from 'src/app/types/dependent-info' +import { ServiceActionsComponent } from '../components/actions.component' +import { ServiceAdditionalComponent } from '../components/additional.component' +import { ServiceDependenciesComponent } from '../components/dependencies.component' +import { ServiceHealthChecksComponent } from '../components/health-checks.component' +import { ServiceInterfacesComponent } from '../components/interfaces.component' +import { ServiceMenuComponent } from '../components/menu.component' 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 { - DepErrorService, - DependencyErrorType, - PkgDependencyErrors, -} from 'src/app/services/dep-error.service' + PackageConfigData, + ServiceConfigModal, +} from 'src/app/apps/portal/modals/config.component' +import { ProgressDataPipe } from '../pipes/progress-data.pipe' import { DependencyInfo } from '../types/dependency-info' -import { Manifest } from '@start9labs/marketplace' -import { toRouterLink } from '../../../utils/to-router-link' -import { PackageConfigData } from '../types/package-config-data' -import { ServiceConfigModal } from '../modals/config.component' -import { DependentInfo } from 'src/app/types/dependent-info' -import { FormDialogService } from 'src/app/services/form-dialog.service' const STATES = [ PackageState.Installing, @@ -50,41 +51,44 @@ const STATES = [ @Component({ template: ` - - - + @if (service$ | async; as service) { + @if (showProgress(service.pkg)) { + @if (service.pkg | progressData; as progress) {

Downloading

Validating

Unpacking

-
-
- - + } + } @else {

Status

- - - - - + } + + @if (isInstalled(service.pkg) && !isBackingUp(service.status)) { + + + @if (isRunning(service.status) && (health$ | async); as checks) { + + } + + @if (service.dependencies.length) { + + } + - -
-
+ } + } + } `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, @@ -105,16 +109,21 @@ const STATES = [ }) export class ServiceRoute { private readonly patch = inject(PatchDB) - private readonly pkgId = getPkgId(inject(ActivatedRoute)) + private readonly pkgId$ = inject(ActivatedRoute).paramMap.pipe( + map(params => params.get('pkgId')!), + ) private readonly depErrorService = inject(DepErrorService) private readonly router = inject(Router) private readonly formDialog = inject(FormDialogService) readonly connected$ = inject(ConnectionService).connected$ - readonly service$ = combineLatest([ - this.patch.watch$('package-data', this.pkgId), - this.depErrorService.getPkgDepErrors$(this.pkgId), - ]).pipe( + readonly service$ = this.pkgId$.pipe( + switchMap(pkgId => + combineLatest([ + this.patch.watch$('package-data', pkgId), + this.depErrorService.getPkgDepErrors$(pkgId), + ]), + ), map(([pkg, depErrors]) => { return { pkg, @@ -123,9 +132,12 @@ export class ServiceRoute { } }), ) - readonly health$ = this.patch - .watch$('package-data', this.pkgId, 'installed', 'status', 'main') - .pipe(map(toHealthCheck)) + readonly health$ = this.pkgId$.pipe( + switchMap(pkgId => + this.patch.watch$('package-data', pkgId, 'installed', 'status', 'main'), + ), + map(toHealthCheck), + ) getRendering({ primary }: PackageStatus): StatusRendering { return PrimaryRendering[primary] @@ -148,20 +160,16 @@ export class ServiceRoute { } private getDepInfo( - pkg: PackageDataEntry, + { installed, manifest }: PackageDataEntry, depErrors: PkgDependencyErrors, ): DependencyInfo[] { - const pkgInstalled = pkg.installed - - if (!pkgInstalled) return [] - - const pkgManifest = pkg.manifest - - return Object.keys(pkgInstalled['current-dependencies']) - .filter(depId => !!pkg.manifest.dependencies[depId]) - .map(depId => - this.getDepValues(pkgInstalled, pkgManifest, depId, depErrors), - ) + return installed + ? Object.keys(installed['current-dependencies']) + .filter(depId => !!manifest.dependencies[depId]) + .map(depId => + this.getDepValues(installed, manifest, depId, depErrors), + ) + : [] } private getDepValues( diff --git a/web/projects/ui/src/app/apps/portal/routes/service/service.module.ts b/web/projects/ui/src/app/apps/portal/routes/service/service.module.ts index fb2d6b8a1..af231e987 100644 --- a/web/projects/ui/src/app/apps/portal/routes/service/service.module.ts +++ b/web/projects/ui/src/app/apps/portal/routes/service/service.module.ts @@ -33,7 +33,7 @@ const ROUTES: Routes = [ { path: '', pathMatch: 'full', - redirectTo: '/portal/desktop', + redirectTo: '/portal/dashboard', }, ], }, diff --git a/web/projects/ui/src/app/apps/portal/routes/service/types/package-config-data.ts b/web/projects/ui/src/app/apps/portal/routes/service/types/package-config-data.ts deleted file mode 100644 index 3981773fc..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/service/types/package-config-data.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DependentInfo } from 'src/app/types/dependent-info' - -export interface PackageConfigData { - readonly pkgId: string - readonly dependentInfo?: DependentInfo -} diff --git a/web/projects/ui/src/app/apps/portal/routes/system/backups/services/restore.service.ts b/web/projects/ui/src/app/apps/portal/routes/system/backups/services/restore.service.ts index 72c37c877..449558eaf 100644 --- a/web/projects/ui/src/app/apps/portal/routes/system/backups/services/restore.service.ts +++ b/web/projects/ui/src/app/apps/portal/routes/system/backups/services/restore.service.ts @@ -58,7 +58,7 @@ export class BackupsRestoreService { ), ) .subscribe(() => { - this.router.navigate(['/portal/desktop']) + this.router.navigate(['/portal/dashboard']) }) } diff --git a/web/projects/ui/src/app/apps/portal/routes/system/settings/components/menu.component.ts b/web/projects/ui/src/app/apps/portal/routes/system/settings/components/menu.component.ts index 66a13c4d5..adc738603 100644 --- a/web/projects/ui/src/app/apps/portal/routes/system/settings/components/menu.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/system/settings/components/menu.component.ts @@ -17,6 +17,10 @@ import { SettingsUpdateComponent } from './update.component'

{{ cat.key }}

+
-
diff --git a/web/projects/ui/src/app/apps/portal/services/actions.service.ts b/web/projects/ui/src/app/apps/portal/services/actions.service.ts new file mode 100644 index 000000000..7272cf2d5 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/services/actions.service.ts @@ -0,0 +1,138 @@ +import { inject, Injectable } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs' +import { + PackageConfigData, + ServiceConfigModal, +} from 'src/app/apps/portal/modals/config.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { PackageDataEntry } from 'src/app/services/patch-db/data-model' +import { hasCurrentDeps } from 'src/app/util/has-deps' + +@Injectable({ + providedIn: 'root', +}) +export class ActionsService { + private readonly dialogs = inject(TuiDialogService) + private readonly errorService = inject(ErrorService) + private readonly loader = inject(LoadingService) + private readonly api = inject(ApiService) + private readonly formDialog = inject(FormDialogService) + + configure({ manifest }: PackageDataEntry): void { + this.formDialog.open(ServiceConfigModal, { + label: `${manifest.title} configuration`, + data: { pkgId: manifest.id }, + }) + } + + async start({ manifest }: PackageDataEntry, unmet: boolean): Promise { + const deps = `${manifest.title} has unmet dependencies. It will not work as expected.` + + if ( + (!unmet || (await this.alert(deps))) && + (!manifest.alerts.start || (await this.alert(manifest.alerts.start))) + ) { + this.doStart(manifest.id) + } + } + + stop(pkg: PackageDataEntry): void { + const { title, alerts } = pkg.manifest + + let content = alerts.stop || '' + + if (hasCurrentDeps(pkg)) { + const depMessage = `Services that depend on ${title} will no longer work properly and may crash` + content = content ? `${content}.\n\n${depMessage}` : depMessage + } + + if (content) { + this.dialogs + .open(TUI_PROMPT, getOptions(content, 'Stop')) + .pipe(filter(Boolean)) + .subscribe(() => this.doStop(pkg.manifest.id)) + } else { + this.doStop(pkg.manifest.id) + } + } + + restart(pkg: PackageDataEntry): void { + if (hasCurrentDeps(pkg)) { + this.dialogs + .open( + TUI_PROMPT, + getOptions( + `Services that depend on ${pkg.manifest} may temporarily experiences issues`, + 'Restart', + ), + ) + .pipe(filter(Boolean)) + .subscribe(() => this.doRestart(pkg.manifest.id)) + } else { + this.doRestart(pkg.manifest.id) + } + } + + private async doStart(id: string): Promise { + const loader = this.loader.open(`Starting...`).subscribe() + + try { + await this.api.startPackage({ id }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async doStop(id: string): Promise { + const loader = this.loader.open(`Stopping...`).subscribe() + + try { + await this.api.stopPackage({ id }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async doRestart(id: string): Promise { + const loader = this.loader.open(`Restarting...`).subscribe() + + try { + await this.api.restartPackage({ id }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private alert(content: string): Promise { + return firstValueFrom( + this.dialogs + .open(TUI_PROMPT, getOptions(content)) + .pipe(defaultIfEmpty(false)), + ) + } +} + +function getOptions( + content: string, + yes = 'Continue', +): Partial> { + return { + label: 'Warning', + size: 's', + data: { + content, + yes, + no: 'Cancel', + }, + } +} diff --git a/web/projects/ui/src/app/apps/portal/services/badge.service.ts b/web/projects/ui/src/app/apps/portal/services/badge.service.ts index e8ff3982f..2039e4db9 100644 --- a/web/projects/ui/src/app/apps/portal/services/badge.service.ts +++ b/web/projects/ui/src/app/apps/portal/services/badge.service.ts @@ -10,9 +10,11 @@ import { map, Observable, pairwise, + shareReplay, startWith, switchMap, } from 'rxjs' +import { EOSService } from 'src/app/services/eos.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { MarketplaceService } from 'src/app/services/marketplace.service' import { ConnectionService } from 'src/app/services/connection.service' @@ -23,6 +25,10 @@ import { ConnectionService } from 'src/app/services/connection.service' export class BadgeService { private readonly emver = inject(Emver) private readonly patch = inject(PatchDB) + private readonly settings$ = combineLatest([ + this.patch.watch$('server-info', 'ntp-synced'), + inject(EOSService).updateAvailable$, + ]).pipe(map(([synced, update]) => Number(!synced) + Number(update))) private readonly marketplace = inject( AbstractMarketplaceService, ) as MarketplaceService @@ -47,7 +53,7 @@ export class BadgeService { ), ) - private readonly updateCount$ = combineLatest([ + private readonly updates$ = combineLatest([ this.marketplace.getMarketplace$(true), this.local$, ]).pipe( @@ -65,12 +71,15 @@ export class BadgeService { new Set(), ).size, ), + shareReplay(1), ) getCount(id: string): Observable { switch (id) { case '/portal/system/updates': - return this.updateCount$ + return this.updates$ + case '/portal/system/settings': + return this.settings$ default: return EMPTY } diff --git a/web/projects/ui/src/app/apps/portal/services/desktop.service.ts b/web/projects/ui/src/app/apps/portal/services/desktop.service.ts deleted file mode 100644 index ac63cfbbd..000000000 --- a/web/projects/ui/src/app/apps/portal/services/desktop.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { inject, Injectable } from '@angular/core' -import { TuiAlertService } from '@taiga-ui/core' -import { ApiService } from 'src/app/services/api/embassy-api.service' - -@Injectable({ - providedIn: 'root', -}) -export class DesktopService { - private readonly alerts = inject(TuiAlertService) - private readonly api = inject(ApiService) - - order = new Map() - items: readonly string[] = [] - - add(id: string) { - if (this.items.includes(id)) return - - this.items = this.items.concat(id) - this.save(this.items) - } - - remove(id: string) { - if (!this.items.includes(id)) return - - this.items = this.items.filter(x => x !== id) - this.save(this.items) - } - - save(ids: readonly string[] = []) { - this.api - .setDbValue(['desktop'], Array.from(new Set(ids))) - .catch(() => - this.alerts - .open( - 'Desktop might be out of sync. Please refresh the page to fix it.', - { status: 'warning' }, - ) - .subscribe(), - ) - } - - reorder(order: Map) { - this.order = order - - const items: string[] = [...this.items] - - Array.from(this.order.entries()).forEach(([index, order]) => { - items[order] = this.items[index] - }) - - this.save(items) - } -} diff --git a/web/projects/ui/src/app/apps/portal/services/services.service.ts b/web/projects/ui/src/app/apps/portal/services/services.service.ts index 8afc6f43e..458e83227 100644 --- a/web/projects/ui/src/app/apps/portal/services/services.service.ts +++ b/web/projects/ui/src/app/apps/portal/services/services.service.ts @@ -13,15 +13,8 @@ export class ServicesService extends Observable { private readonly services$ = inject(PatchDB) .watch$('package-data') .pipe( - map(pkgs => Object.values(pkgs)), - startWith([]), - pairwise(), - filter(([prev, next]) => { - const length = next.length - return !length || prev.length !== length - }), - map(([_, pkgs]) => - pkgs.sort((a, b) => + map(pkgs => + Object.values(pkgs).sort((a, b) => b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase() ? -1 : 1, diff --git a/web/projects/ui/src/app/apps/portal/types/navigation-item.ts b/web/projects/ui/src/app/apps/portal/types/navigation-item.ts deleted file mode 100644 index ea912247c..000000000 --- a/web/projects/ui/src/app/apps/portal/types/navigation-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface NavigationItem { - readonly routerLink: string - readonly icon: string - readonly title: string -} diff --git a/web/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts b/web/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts index b33afe3a4..d4109d496 100644 --- a/web/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts +++ b/web/projects/ui/src/app/apps/portal/utils/to-navigation-item.ts @@ -1,8 +1,13 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { SYSTEM_UTILITIES } from '../constants/system-utilities' -import { NavigationItem } from '../types/navigation-item' import { toRouterLink } from './to-router-link' +export interface NavigationItem { + readonly routerLink: string + readonly icon: string + readonly title: string +} + export function toNavigationItem( id: string, packages: Record = {}, diff --git a/web/projects/ui/src/app/common/svg-definitions.component.ts b/web/projects/ui/src/app/common/svg-definitions.component.ts index 36d6462b9..b73909485 100644 --- a/web/projects/ui/src/app/common/svg-definitions.component.ts +++ b/web/projects/ui/src/app/common/svg-definitions.component.ts @@ -6,6 +6,20 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' template: ` + + + + + + + + + + + + + +