diff --git a/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts b/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts index 914b89528..d53b6931a 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts @@ -9,13 +9,10 @@ import { } from '@taiga-ui/core' import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental' import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' -import { PatchDB } from 'patch-db-client' import { filter } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' import { ABOUT } from './about.component' -import { getAllPackages } from 'src/app/utils/get-package-data' -import { DataModel } from 'src/app/services/patch-db/data-model' import { HeaderConnectionComponent } from './connection.component' @Component({ @@ -114,7 +111,6 @@ export class HeaderMenuComponent { private readonly errorService = inject(ErrorService) private readonly loader = inject(LoadingService) private readonly auth = inject(AuthService) - private readonly patch = inject(PatchDB) private readonly dialogs = inject(TuiDialogService) readonly links = [ @@ -136,10 +132,6 @@ export class HeaderMenuComponent { ] readonly system = [ - { - icon: 'tuiIconTool', - action: 'System Rebuild', - }, { icon: 'tuiIconRefreshCw', action: 'Restart', @@ -159,20 +151,17 @@ export class HeaderMenuComponent { this.auth.setUnverified() } - async prompt(action: keyof typeof METHODS) { - const minutes = - action === 'System Rebuild' - ? Object.keys(await getAllPackages(this.patch)).length * 2 - : '' - + async prompt(action: 'Restart' | 'Shutdown') { this.dialogs - .open(TUI_PROMPT, getOptions(action, minutes)) + .open(TUI_PROMPT, getOptions(action)) .pipe(filter(Boolean)) .subscribe(async () => { const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { - await this.api[METHODS[action]]({}) + await this.api[ + action === 'Restart' ? 'restartServer' : 'shutdownServer' + ]({}) } catch (e: any) { this.errorService.handleError(e) } finally { @@ -182,19 +171,11 @@ export class HeaderMenuComponent { } } -const METHODS = { - Restart: 'restartServer', - Shutdown: 'shutdownServer', - 'System Rebuild': 'systemRebuild', -} as const - function getOptions( - key: keyof typeof METHODS, - minutes: unknown, + operation: 'Restart' | 'Shutdown', ): Partial> { - switch (key) { - case 'Restart': - return { + return operation === 'Restart' + ? { label: 'Restart', size: 's', data: { @@ -204,8 +185,7 @@ function getOptions( no: 'Cancel', }, } - case 'Shutdown': - return { + : { label: 'Warning', size: 's', data: { @@ -215,15 +195,4 @@ function getOptions( no: 'Cancel', }, } - default: - return { - label: 'Warning', - size: 's', - data: { - content: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`, - yes: 'Rebuild', - no: 'Cancel', - }, - } - } } diff --git a/web/projects/ui/src/app/routes/portal/components/header/mobile.component.ts b/web/projects/ui/src/app/routes/portal/components/header/mobile.component.ts index dbd175640..1dc92b117 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/mobile.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/mobile.component.ts @@ -1,5 +1,11 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' import { RouterLink } from '@angular/router' +import { WINDOW } from '@ng-web-apis/common' import { TuiIconModule } from '@taiga-ui/experimental' import { Breadcrumb } from 'src/app/services/breadcrumbs.service' @@ -7,8 +13,12 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service' standalone: true, selector: '[headerMobile]', template: ` - @if (headerMobile?.length) { - + @if (headerMobile && headerMobile.length > 1) { + } @@ -46,6 +56,11 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service' .title { @include text-overflow(); max-width: calc(100% - 5rem); + text-transform: capitalize; + + &:first-child { + margin-inline-start: 1rem; + } } `, ], @@ -53,10 +68,15 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service' imports: [TuiIconModule, RouterLink], }) export class HeaderMobileComponent { + private readonly win = inject(WINDOW) + @Input() headerMobile: readonly Breadcrumb[] | null = [] get title() { - return this.headerMobile?.[this.headerMobile?.length - 1]?.title || '' + return ( + this.headerMobile?.[this.headerMobile?.length - 1]?.title || + (this.win.location.search ? 'Utilities' : 'Services') + ) } get back() { @@ -65,4 +85,8 @@ export class HeaderMobileComponent { '/portal/dashboard' ) } + + get queryParams() { + return this.back === '/portal/dashboard' ? { tab: 'utilities' } : null + } } diff --git a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts index 42f97f70a..e08d02562 100644 --- a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts @@ -24,10 +24,8 @@ import { NotificationService } from 'src/app/services/notification.service' Metrics @@ -58,7 +56,9 @@ import { NotificationService } from 'src/app/services/notification.service' display: none; // TODO: Theme --tui-elevation-01: #333; + --tui-base-01: #fff; --tui-base-04: var(--tui-clear); + --tui-error-fill: #f52222; backdrop-filter: blur(1rem); } diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/controls.component.ts index 026c33d88..307e023ad 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/controls.component.ts @@ -67,6 +67,7 @@ import { getManifest } from 'src/app/utils/get-package-data' :host { padding: 0; border: none; + cursor: default; } :host-context(tui-root._mobile) { diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts index a8495f5a5..46a5f7661 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts @@ -176,7 +176,8 @@ import { TimeService } from 'src/app/services/time.service' section { flex-wrap: wrap; - padding: 1rem; + padding: 0; + margin: 1rem -1rem; } aside { diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/service.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/service.component.ts index eff28f227..5f59d9fed 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/service.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/service.component.ts @@ -4,6 +4,7 @@ import { Component, inject, Input, + OnChanges, } from '@angular/core' import { RouterLink } from '@angular/router' import { tuiPure } from '@taiga-ui/cdk' @@ -19,7 +20,7 @@ import { getManifest } from 'src/app/utils/get-package-data' selector: 'tr[appService]', template: ` - logo + logo {{ manifest.title }} @@ -36,11 +37,25 @@ import { getManifest } from 'src/app/utils/get-package-data' appControls [disabled]="!installed || !(connected$ | async)" [pkg]="pkg" + (click.stop)="(0)" > `, styles: ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + :host { + @include transition(background); + clip-path: inset(0 round 0.5rem); + cursor: pointer; + + &:hover { + background: var(--tui-clear); + } + } + img { + display: block; height: 2rem; width: 2rem; border-radius: 100%; @@ -80,10 +95,13 @@ import { getManifest } from 'src/app/utils/get-package-data' } } `, + hostDirectives: [RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouterLink, AsyncPipe, StatusComponent, ControlsComponent], }) -export class ServiceComponent { +export class ServiceComponent implements OnChanges { + private readonly link = inject(RouterLink) + @Input() pkg!: PackageDataEntry @@ -104,6 +122,10 @@ export class ServiceComponent { return `/portal/service/${this.manifest.id}` } + ngOnChanges() { + this.link.routerLink = this.routerLink + } + @tuiPure hasError(errors: PkgDependencyErrors = {}): boolean { return Object.values(errors).some(Boolean) diff --git a/web/projects/ui/src/app/routes/portal/routes/dashboard/services.component.ts b/web/projects/ui/src/app/routes/portal/routes/dashboard/services.component.ts index 8925a0a86..32b447611 100644 --- a/web/projects/ui/src/app/routes/portal/routes/dashboard/services.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/dashboard/services.component.ts @@ -19,9 +19,7 @@ import { DepErrorService } from 'src/app/services/dep-error.service' Name Version Status - - Controls - + Controls diff --git a/web/projects/ui/src/app/routes/portal/routes/system/logs/logs.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/logs/logs.component.ts index c133c5187..029bdc47e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/logs/logs.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/logs/logs.component.ts @@ -9,8 +9,9 @@ import { LogsComponent } from '../../../components/logs/logs.component' @Component({ template: ` {{ subtitle }} @@ -50,7 +51,7 @@ import { LogsComponent } from '../../../components/logs/logs.component' } logs { - height: calc(100% - 4rem); + height: calc(100% - 5rem); } `, ], diff --git a/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts new file mode 100644 index 000000000..d45a7dbc4 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/system/metrics/metrics.component.ts @@ -0,0 +1,17 @@ +import { AsyncPipe } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { MetricsComponent } from 'src/app/routes/portal/routes/dashboard/metrics.component' +import { MetricsService } from 'src/app/services/metrics.service' + +@Component({ + template: ` + + `, + host: { class: 'g-page' }, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [MetricsComponent, AsyncPipe], +}) +export default class SystemMetricsComponent { + readonly metrics$ = inject(MetricsService) +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts index dddfacfe8..11e7dea59 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/notifications/item.component.ts @@ -8,7 +8,7 @@ import { import { RouterLink } from '@angular/router' import { T } from '@start9labs/start-sdk' import { tuiPure } from '@taiga-ui/cdk' -import { TuiSvgModule } from '@taiga-ui/core' +import { TuiLinkModule, TuiSvgModule } from '@taiga-ui/core' import { TuiLineClampModule } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { first, Observable } from 'rxjs' @@ -20,10 +20,10 @@ import { toRouterLink } from 'src/app/utils/to-router-link' @Component({ selector: '[notificationItem]', template: ` - + {{ notificationItem.createdAt | date: 'MMM d, y, h:mm a' }} - + {{ notificationItem.title }} @@ -43,20 +43,44 @@ import { toRouterLink } from 'src/app/utils/to-router-link' [content]="notificationItem.message" (overflownChange)="overflow = $event" /> - + `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, RouterLink, TuiLineClampModule, TuiSvgModule], + host: { + '[class._new]': '!notificationItem.read', + }, + styles: ` + :host._new { + background: var(--tui-clear); + } + + td { + padding: 0.25rem; + vertical-align: top; + } + `, + imports: [ + CommonModule, + RouterLink, + TuiLineClampModule, + TuiSvgModule, + TuiLinkModule, + ], }) export class NotificationItemComponent { private readonly patch = inject(PatchDB) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/notifications/notifications.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/notifications/notifications.component.ts index 6aa27b837..63265f5d6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/notifications/notifications.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/notifications/notifications.component.ts @@ -14,25 +14,24 @@ import { NotificationsTableComponent } from './table.component' template: `

- Notifications - - + - + Batch Action + - +

diff --git a/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts index 27f97aefd..e4c298f4e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/notifications/table.component.ts @@ -1,20 +1,18 @@ -import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input, OnChanges, } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { TuiCheckboxModule } from '@taiga-ui/experimental' +import { TuiLineClampModule } from '@taiga-ui/kit' +import { BehaviorSubject } from 'rxjs' import { ServerNotification, ServerNotifications, } from 'src/app/services/api/api.types' -import { TuiForModule } from '@taiga-ui/cdk' -import { BehaviorSubject } from 'rxjs' -import { TuiLineClampModule } from '@taiga-ui/kit' -import { FormsModule } from '@angular/forms' import { NotificationItemComponent } from './item.component' -import { TuiCheckboxModule } from '@taiga-ui/experimental' @Component({ selector: 'table[notifications]', @@ -39,39 +37,40 @@ import { TuiCheckboxModule } from '@taiga-ui/experimental' - - - - - - You have no notifications - - - - -
Loading
- -
+ @if (notifications) { + @for (notification of notifications; track $index) { + + + + } @empty { + + You have no notifications + + } + } @else { + @for (row of ['', '']; track $index) { + +
Loading
+ + } + } `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ - CommonModule, - TuiForModule, - TuiCheckboxModule, FormsModule, + TuiCheckboxModule, TuiLineClampModule, NotificationItemComponent, ], diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/button.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/button.component.ts index e53d7fdae..6ef8e9da2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/button.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/button.component.ts @@ -27,9 +27,16 @@ import { SettingBtn } from '../settings.types' `, - styles: [ - ':host:not(:last-child) { display: block; box-shadow: 0 1px var(--tui-clear); }', - ], + styles: ` + :host:not(:last-child) { + display: block; + box-shadow: 0 1px var(--tui-clear); + } + + button { + cursor: pointer; + } + `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, TuiIconModule, TuiTitleModule, RouterLink], diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/menu.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/menu.component.ts index 832e72135..d13a0e74f 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/menu.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/menu.component.ts @@ -51,7 +51,6 @@ import { SettingsUpdateComponent } from './update.component' display: flex; flex-direction: column; gap: 1rem; - padding-top: 1rem; } `, ], diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/update.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/update.component.ts index 411682708..20479b5a3 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/settings/components/update.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/components/update.component.ts @@ -47,10 +47,20 @@ import { UPDATE } from '../modals/update.component' `, - styles: [ - ':host { display: block; box-shadow: 0 1px var(--tui-clear); }', - '.small { width: 1rem; height: 1rem; }', - ], + styles: ` + :host { + display: block; + box-shadow: 0 1px var(--tui-clear); + } + + button { + cursor: pointer; + } + + .small { + font-size: 1rem; + } + `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, TuiIconModule, TuiTitleModule], diff --git a/web/projects/ui/src/app/routes/portal/routes/system/settings/settings.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/settings/settings.component.ts index 0a696d1df..2878646f2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/settings/settings.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/settings/settings.component.ts @@ -9,44 +9,29 @@ import { SettingsMenuComponent } from './components/menu.component' routerLink="/portal/system/settings" routerLinkActive="_current" [routerLinkActiveOptions]="{ exact: true }" - > - - Settings - - + > + `, styles: [ ` :host { + padding-top: 1rem; + ::ng-deep tui-notification { position: sticky; left: 0; } } - a { - position: sticky; - left: 0; - display: inline-flex; - align-items: center; - gap: 0.5rem; - margin: 1rem 0; - font-size: 1rem; - color: var(--tui-text-01); - } - - ._current { + a, + settings-menu { display: none; } - .page { - display: none; - } - - ._current + .page { + ._current + settings-menu { display: flex; - max-width: 45rem; + max-width: 30rem; margin: 0 auto; } `, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/system.module.ts b/web/projects/ui/src/app/routes/portal/routes/system/system.module.ts index 2d9e0171c..b225c171c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/system.module.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/system.module.ts @@ -48,6 +48,12 @@ const ROUTES: Routes = [ loadComponent: () => import('./updates/updates.component'), data: toNavigationItem('/portal/system/updates'), }, + { + title: systemTabResolver, + path: 'metrics', + loadComponent: () => import('./metrics/metrics.component'), + data: toNavigationItem('/portal/system/metrics'), + }, ] @NgModule({ imports: [RouterModule.forChild(ROUTES)] }) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/updates/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/updates/item.component.ts index 9003d1073..aba7e93d6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/updates/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/updates/item.component.ts @@ -43,7 +43,9 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps' template: `
- + + +
{{ marketplacePkg.manifest.title }}