From f0ae9e21aec2214cdf4138dc0247b69820ecdaca Mon Sep 17 00:00:00 2001 From: waterplea Date: Tue, 28 May 2024 13:04:01 +0100 Subject: [PATCH] refactor: change navigation Signed-off-by: waterplea --- .../icons/tuiIconCheckCircleOutline.svg | 25 ++ web/projects/shared/styles/taiga.scss | 24 +- web/projects/ui/src/app/app.module.ts | 2 + .../components/header/corner.component.ts | 108 +++------ .../components/header/header.component.ts | 7 +- .../components/header/menu.component.ts | 211 +++++++--------- .../header/notification.component.ts | 85 ------- .../header/notifications.component.ts | 151 ------------ .../portal/components/tabs.component.ts | 150 +++++++++--- .../src/app/routes/portal/portal.component.ts | 14 +- .../routes/dashboard/dashboard.component.ts | 173 +++++++------ .../routes/dashboard/metrics.component.ts | 228 ------------------ .../routes/dashboard/services.component.ts | 106 -------- .../routes/dashboard/utilities.component.ts | 111 --------- .../service/components/action.component.ts | 2 +- .../service/components/actions.component.ts | 2 +- .../service/components/status.component.ts | 66 +++-- .../service/routes/service.component.ts | 2 +- .../metrics}/cpu.component.ts | 0 .../metrics}/metric.component.ts | 0 .../system/metrics/metrics.component.ts | 188 ++++++++++++++- .../routes/system/metrics}/metrics.service.ts | 0 .../metrics}/temperature.component.ts | 0 .../system/settings/settings.service.ts | 80 +++++- .../ui/src/app/services/badge.service.ts | 4 + web/projects/ui/src/app/utils/resources.ts | 17 ++ .../ui/src/app/utils/system-utilities.ts | 39 ++- 27 files changed, 737 insertions(+), 1058 deletions(-) create mode 100644 web/projects/shared/assets/taiga-ui/icons/tuiIconCheckCircleOutline.svg delete mode 100644 web/projects/ui/src/app/routes/portal/components/header/notification.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/header/notifications.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/metrics.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/services.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/dashboard/utilities.component.ts rename web/projects/ui/src/app/routes/portal/routes/{dashboard => system/metrics}/cpu.component.ts (100%) rename web/projects/ui/src/app/routes/portal/routes/{dashboard => system/metrics}/metric.component.ts (100%) rename web/projects/ui/src/app/{services => routes/portal/routes/system/metrics}/metrics.service.ts (100%) rename web/projects/ui/src/app/routes/portal/routes/{dashboard => system/metrics}/temperature.component.ts (100%) create mode 100644 web/projects/ui/src/app/utils/resources.ts diff --git a/web/projects/shared/assets/taiga-ui/icons/tuiIconCheckCircleOutline.svg b/web/projects/shared/assets/taiga-ui/icons/tuiIconCheckCircleOutline.svg new file mode 100644 index 000000000..3dab4ce82 --- /dev/null +++ b/web/projects/shared/assets/taiga-ui/icons/tuiIconCheckCircleOutline.svg @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index 394107f20..cb7087b33 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -178,26 +178,36 @@ tui-hint[data-appearance='onDark'] { tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] { border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(0.25rem); box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%); + border-radius: 0.325rem; // TODO: Replace --tui-elevation-02 when Taiga UI is updated - background: rgb(63 63 63 / 95%); + background: rgb(63 63 63 / 80%); tui-opt-group { &::before { background: var(--tui-clear); - box-shadow: - 1rem 0 var(--tui-clear), - -1rem 0 var(--tui-clear); - padding-top: 0.25rem !important; - padding-bottom: 0 !important; - margin: 0.25rem; + height: 1px; } &::after { display: none; } } + + [tuiOption] { + border-radius: 0.1875rem !important; + transition-property: background, box-shadow; + + &:focus, + &._with-dropdown { + box-shadow: + inset 0 -1px rgba(0, 0, 0, 0.3), + inset 0 1px rgba(255, 255, 255, 0.1), + inset 0 -3rem 4rem -2rem rgba(0, 0, 0, 0.3); + } + } } [tuiSidebar] > div.t-wrapper { diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index 94a7498be..f31c6e9ba 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { ServiceWorkerModule } from '@angular/service-worker' import { LoadingModule } from '@start9labs/shared' +import { TuiSheetDialogModule } from '@taiga-ui/addon-mobile' import { TuiAlertModule, TuiDialogModule, @@ -27,6 +28,7 @@ import { RoutingModule } from './routing.module' ToastContainerComponent, TuiRootModule, TuiDialogModule, + TuiSheetDialogModule, TuiAlertModule, TuiModeModule, TuiThemeNightModule, diff --git a/web/projects/ui/src/app/routes/portal/components/header/corner.component.ts b/web/projects/ui/src/app/routes/portal/components/header/corner.component.ts index a658fbbc5..8170f7834 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/corner.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/corner.component.ts @@ -1,54 +1,52 @@ -import { CommonModule } from '@angular/common' -import { - ChangeDetectionStrategy, - Component, - ElementRef, - HostListener, - inject, - ViewChild, -} from '@angular/core' -import { Router } from '@angular/router' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { RouterLink } from '@angular/router' import { TuiSidebarModule } from '@taiga-ui/addon-mobile' -import { tuiContainsOrAfter, tuiIsElement, TuiLetModule } from '@taiga-ui/cdk' +import { TuiLetModule } from '@taiga-ui/cdk' +import { + TUI_ANIMATION_OPTIONS, + tuiFadeIn, + tuiScaleIn, + tuiWidthCollapse, +} from '@taiga-ui/core' import { TuiBadgedContentModule, TuiBadgeNotificationModule, TuiButtonModule, } from '@taiga-ui/experimental' -import { Subject } from 'rxjs' -import { HeaderMenuComponent } from './menu.component' -import { HeaderNotificationsComponent } from './notifications.component' import { SidebarDirective } from 'src/app/components/sidebar-host.component' -import { NotificationService } from 'src/app/services/notification.service' +import { getMenu } from 'src/app/utils/system-utilities' +import { HeaderMenuComponent } from './menu.component' @Component({ standalone: true, selector: 'header-corner', template: ` - - - {{ unread }} - - - + @for (item of utils; track $index) { + @if (item.badge(); as badge) { + + + {{ badge }} + + + {{ item.name }} + + + } + } - `, styles: [ ` @@ -65,48 +63,20 @@ import { NotificationService } from 'src/app/services/notification.service' } `, ], + animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, HeaderMenuComponent, - HeaderNotificationsComponent, SidebarDirective, TuiBadgeNotificationModule, TuiBadgedContentModule, TuiButtonModule, TuiLetModule, TuiSidebarModule, + RouterLink, ], }) export class HeaderCornerComponent { - private readonly router = inject(Router) - readonly notificationService = inject(NotificationService) - - @ViewChild(HeaderNotificationsComponent, { read: ElementRef }) - private readonly panel?: ElementRef - - private readonly _ = this.router.events.subscribe(() => { - this.open$.next(false) - }) - - readonly open$ = new Subject() - - @HostListener('document:click.capture', ['$event.target']) - onClick(target: EventTarget | null) { - if ( - tuiIsElement(target) && - this.panel?.nativeElement && - !tuiContainsOrAfter(this.panel.nativeElement, target) - ) { - this.open$.next(false) - } - } - - handleNotificationsClick(unread: number) { - if (unread) { - this.open$.next(true) - } else { - this.router.navigateByUrl('/portal/system/notifications') - } - } + readonly animation = inject(TUI_ANIMATION_OPTIONS) + readonly utils = getMenu() } diff --git a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts index fd78ad4d2..444d08c59 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts @@ -64,10 +64,11 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service' margin-left: -1.25rem; backdrop-filter: blur(1rem); clip-path: var(--clip-path); + } - &:active { - backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75); - } + > a:active, + > button:active { + backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75); } &:has([data-connection='error']) { 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 d53b6931a..7c1fd86b3 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 @@ -1,65 +1,86 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { RouterLink } from '@angular/router' +import { TuiActiveZoneModule } from '@taiga-ui/cdk' import { TuiDataListModule, - TuiDialogOptions, TuiDialogService, + TuiDropdownModule, TuiHostedDropdownModule, TuiSvgModule, } from '@taiga-ui/core' -import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental' -import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' -import { filter } from 'rxjs' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { AuthService } from 'src/app/services/auth.service' +import { + TuiBadgeNotificationModule, + TuiButtonModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { TuiDataListDropdownManagerModule } from '@taiga-ui/kit' +import { RESOURCES } from 'src/app/utils/resources' +import { getMenu } from 'src/app/utils/system-utilities' import { ABOUT } from './about.component' import { HeaderConnectionComponent } from './connection.component' @Component({ selector: 'header-menu', template: ` - + - - + +

StartOS

- + @for (link of links; track $index) { + + + {{ link.name }} + + + } +
+
- - @for (link of links; track $index) { - - - {{ link.name }} - - - } - - - @for (item of system; track $index) { - - } - - - -
@@ -70,9 +91,22 @@ import { HeaderConnectionComponent } from './connection.component' font-size: 1rem; } + tui-hosted-dropdown { + margin: 0 -0.5rem; + + [tuiIconButton] { + height: calc(var(--tui-height-m) + 0.375rem); + width: calc(var(--tui-height-m) + 0.625rem); + } + } + .item { justify-content: flex-start; gap: 0.75rem; + + ::ng-deep tui-svg { + margin-left: auto; + } } .status { @@ -80,7 +114,7 @@ import { HeaderConnectionComponent } from './connection.component' font-size: 0; padding: 0 0.5rem; height: 2rem; - width: 14rem; + width: 13rem; } .title { @@ -104,95 +138,22 @@ import { HeaderConnectionComponent } from './connection.component' TuiButtonModule, TuiIconModule, HeaderConnectionComponent, + RouterLink, + TuiBadgeNotificationModule, + TuiDropdownModule, + TuiDataListDropdownManagerModule, + TuiActiveZoneModule, ], }) export class HeaderMenuComponent { - private readonly api = inject(ApiService) - private readonly errorService = inject(ErrorService) - private readonly loader = inject(LoadingService) - private readonly auth = inject(AuthService) private readonly dialogs = inject(TuiDialogService) - readonly links = [ - { - name: 'User Manual', - icon: 'tuiIconBookOpen', - href: 'https://docs.start9.com/0.3.5.x/user-manual', - }, - { - name: 'Contact Support', - icon: 'tuiIconHeadphones', - href: 'https://start9.com/contact', - }, - { - name: 'Donate to Start9', - icon: 'tuiIconDollarSign', - href: 'https://donate.start9.com', - }, - ] + open = false - readonly system = [ - { - icon: 'tuiIconRefreshCw', - action: 'Restart', - }, - { - icon: 'tuiIconPower', - action: 'Shutdown', - }, - ] as const + readonly utils = getMenu() + readonly links = RESOURCES about() { this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe() } - - logout() { - this.api.logout({}).catch(e => console.error('Failed to log out', e)) - this.auth.setUnverified() - } - - async prompt(action: 'Restart' | 'Shutdown') { - this.dialogs - .open(TUI_PROMPT, getOptions(action)) - .pipe(filter(Boolean)) - .subscribe(async () => { - const loader = this.loader.open(`Beginning ${action}...`).subscribe() - - try { - await this.api[ - action === 'Restart' ? 'restartServer' : 'shutdownServer' - ]({}) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - }) - } -} - -function getOptions( - operation: 'Restart' | 'Shutdown', -): Partial> { - return operation === 'Restart' - ? { - label: 'Restart', - size: 's', - data: { - content: - 'Are you sure you want to restart your server? It can take several minutes to come back online.', - yes: 'Restart', - no: 'Cancel', - }, - } - : { - label: 'Warning', - size: 's', - data: { - content: - 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in', - yes: 'Shutdown', - no: 'Cancel', - }, - } } diff --git a/web/projects/ui/src/app/routes/portal/components/header/notification.component.ts b/web/projects/ui/src/app/routes/portal/components/header/notification.component.ts deleted file mode 100644 index d6300c405..000000000 --- a/web/projects/ui/src/app/routes/portal/components/header/notification.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CommonModule } from '@angular/common' -import { - ChangeDetectionStrategy, - Component, - inject, - Input, -} from '@angular/core' -import { TuiSvgModule } from '@taiga-ui/core' -import { TuiButtonModule, TuiTitleModule } from '@taiga-ui/experimental' -import { TuiLineClampModule } from '@taiga-ui/kit' -import { ServerNotification } from 'src/app/services/api/api.types' -import { NotificationService } from 'src/app/services/notification.service' - -@Component({ - selector: 'header-notification', - template: ` - -
-
-
- {{ notification.title }} -
- -
- - - - -
-
- - `, - styles: [':host { box-shadow: 0 1px var(--tui-clear); }'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - CommonModule, - TuiSvgModule, - TuiTitleModule, - TuiButtonModule, - TuiLineClampModule, - ], -}) -export class HeaderNotificationComponent { - readonly service = inject(NotificationService) - - @Input({ required: true }) notification!: ServerNotification - - overflow = false - - get color(): string { - return this.service.getColor(this.notification) - } - - get icon(): string { - return this.service.getIcon(this.notification) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/header/notifications.component.ts b/web/projects/ui/src/app/routes/portal/components/header/notifications.component.ts deleted file mode 100644 index a5033200d..000000000 --- a/web/projects/ui/src/app/routes/portal/components/header/notifications.component.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { CommonModule } from '@angular/common' -import { - ChangeDetectionStrategy, - Component, - Output, - inject, - EventEmitter, -} from '@angular/core' -import { RouterLink } from '@angular/router' -import { TuiForModule } from '@taiga-ui/cdk' -import { TuiScrollbarModule } from '@taiga-ui/core' -import { - TuiAvatarStackModule, - TuiButtonModule, - TuiCellModule, - TuiTitleModule, -} from '@taiga-ui/experimental' -import { PatchDB } from 'patch-db-client' -import { Subject, first, tap } from 'rxjs' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { HeaderNotificationComponent } from './notification.component' -import { toRouterLink } from 'src/app/utils/to-router-link' -import { - ServerNotification, - ServerNotifications, -} from 'src/app/services/api/api.types' -import { NotificationService } from 'src/app/services/notification.service' -import { ToManifestPipe } from '../../pipes/to-manifest' - -@Component({ - selector: 'header-notifications', - template: ` - -

- Notifications - - Mark All Seen - -

- - - - {{ - packageData[pkgId] - ? (packageData[pkgId] | toManifest).title - : pkgId - }} - - - - View Service - - - - - View All - -
- `, - styles: [ - ` - :host { - display: flex; - flex-direction: column; - height: 100%; - width: 22rem; - max-width: 80vw; - } - `, - ], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - CommonModule, - RouterLink, - TuiForModule, - TuiScrollbarModule, - TuiButtonModule, - HeaderNotificationComponent, - TuiCellModule, - TuiAvatarStackModule, - TuiTitleModule, - ToManifestPipe, - ], -}) -export class HeaderNotificationsComponent { - private readonly patch = inject(PatchDB) - private readonly service = inject(NotificationService) - - readonly packageData$ = this.patch.watch$('packageData').pipe(first()) - - readonly notifications$ = new Subject() - - @Output() onEmpty = new EventEmitter() - - ngAfterViewInit() { - this.patch - .watch$('serverInfo', 'unreadNotifications', 'recent') - .pipe( - tap(recent => this.notifications$.next(recent)), - first(), - ) - .subscribe() - } - - markSeen( - current: ServerNotifications, - notification: ServerNotification, - ) { - this.notifications$.next(current.filter(c => c.id !== notification.id)) - - if (current.length === 1) this.onEmpty.emit() - - this.service.markSeen([notification]) - } - - markAllSeen(latestId: number) { - this.notifications$.next([]) - - this.service.markSeenAll(latestId) - - this.onEmpty.emit() - } - - getLink(id: string) { - return toRouterLink(id) - } -} 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 e08d02562..55ba80774 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 @@ -1,84 +1,170 @@ -import { AsyncPipe } from '@angular/common' -import { Component, inject } from '@angular/core' +import { + Component, + computed, + inject, + TemplateRef, + viewChildren, +} from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' import { RouterLink, RouterLinkActive } from '@angular/router' -import { TuiTabBarModule } from '@taiga-ui/addon-mobile' -import { combineLatest, map, startWith } from 'rxjs' -import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities' +import { TuiSheetDialogService, TuiTabBarModule } from '@taiga-ui/addon-mobile' +import { TuiDialogService } from '@taiga-ui/core' +import { + TuiBadgeNotificationModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { ABOUT } from 'src/app/routes/portal/components/header/about.component' import { BadgeService } from 'src/app/services/badge.service' -import { NotificationService } from 'src/app/services/notification.service' +import { RESOURCES } from 'src/app/utils/resources' +import { getMenu } from 'src/app/utils/system-utilities' + +const FILTER = ['/portal/system/settings', '/portal/system/marketplace'] @Component({ standalone: true, selector: 'app-tabs', template: ` -