diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index 9f4b9c19f..064ecc855 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -178,8 +178,9 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] { box-shadow: 1rem 0 var(--tui-clear), -1rem 0 var(--tui-clear); - padding-top: 0.375rem !important; + padding-top: 0.25rem !important; padding-bottom: 0 !important; + margin: 0.25rem; } &::after { diff --git a/web/projects/ui/src/app/apps/portal/components/actions.component.ts b/web/projects/ui/src/app/apps/portal/components/actions.component.ts new file mode 100644 index 000000000..9c6b1c484 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/components/actions.component.ts @@ -0,0 +1,64 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { TuiDataListModule } from '@taiga-ui/core' +import { TuiIconModule } from '@taiga-ui/experimental' + +export interface Action { + icon: string + label: string + action: () => void +} + +@Component({ + selector: 'app-actions', + template: ` + +

+ + + +
+ `, + styles: [ + ` + .title { + margin: 0; + padding: 0 0.5rem 0.25rem; + white-space: nowrap; + font: var(--tui-font-text-l); + font-weight: bold; + } + + .item { + justify-content: flex-start; + gap: 0.75rem; + } + + .icon { + opacity: var(--tui-disabled-opacity); + } + `, + ], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiDataListModule, CommonModule, TuiIconModule], +}) +export class ActionsComponent { + @Input() + actions: Record = {} + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.html b/web/projects/ui/src/app/apps/portal/components/actions/actions.component.html deleted file mode 100644 index 9be2e154e..000000000 --- a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.html +++ /dev/null @@ -1,17 +0,0 @@ - -

- - - -
diff --git a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.scss b/web/projects/ui/src/app/apps/portal/components/actions/actions.component.scss deleted file mode 100644 index 2cfd2d78a..000000000 --- a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.scss +++ /dev/null @@ -1,16 +0,0 @@ -.title { - margin: 0; - padding: 0 0.5rem 0.25rem; - white-space: nowrap; - font: var(--tui-font-text-l); - font-weight: bold; -} - -.item { - justify-content: flex-start; - gap: 0.75rem; -} - -.icon { - opacity: var(--tui-disabled-opacity); -} diff --git a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.ts b/web/projects/ui/src/app/apps/portal/components/actions/actions.component.ts deleted file mode 100644 index d560545a7..000000000 --- a/web/projects/ui/src/app/apps/portal/components/actions/actions.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { TuiDataListModule, TuiSvgModule } from '@taiga-ui/core' -import { CommonModule } from '@angular/common' - -export interface Action { - icon: string - label: string - action: () => void -} - -@Component({ - selector: 'app-actions', - templateUrl: './actions.component.html', - styleUrls: ['./actions.component.scss'], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiDataListModule, TuiSvgModule, CommonModule], -}) -export class ActionsComponent { - @Input() - actions: Record = {} - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/ui/src/app/apps/portal/components/card.component.ts b/web/projects/ui/src/app/apps/portal/components/card.component.ts new file mode 100644 index 000000000..ac89aca04 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/components/card.component.ts @@ -0,0 +1,158 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + HostListener, + inject, + Input, +} from '@angular/core' +import { + TuiBadgedContentModule, + TuiBadgeNotificationModule, + TuiButtonModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { RouterLink } from '@angular/router' +import { TickerModule } from '@start9labs/shared' +import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core' +import { NavigationService } from '../services/navigation.service' +import { Action, ActionsComponent } from './actions.component' +import { toRouterLink } from '../utils/to-router-link' + +@Component({ + selector: '[appCard]', + template: ` + + + @if (badge) { + + {{ badge }} + + } + @if (icon?.startsWith('tuiIcon')) { + + } @else { + + } + + + + @if (isService) { + + + + + + {{ title }} + + + + + } + `, + styles: [ + ` + :host { + display: flex; + height: 5.5rem; + width: 12.5rem; + border-radius: var(--tui-radius-l); + overflow: hidden; + box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%); + // TODO: Theme + background: rgb(111 109 109); + } + + .link { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; + gap: 0.25rem; + padding: 0 0.5rem; + font: var(--tui-font-text-m); + white-space: nowrap; + overflow: hidden; + } + + .icon { + width: 2.5rem; + height: 2.5rem; + border-radius: 100%; + color: var(--tui-text-01-night); + } + + .side { + width: 3rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%); + // TODO: Theme + background: #4b4a4a; + } + `, + ], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + RouterLink, + TuiButtonModule, + TuiHostedDropdownModule, + TuiDataListModule, + TuiIconModule, + TickerModule, + TuiBadgedContentModule, + TuiBadgeNotificationModule, + ActionsComponent, + ], +}) +export class CardComponent { + private readonly navigation = inject(NavigationService) + + @Input({ required: true }) + id!: string + + @Input({ required: true }) + icon!: string + + @Input({ required: true }) + title!: string + + @Input() + actions: Record = {} + + @Input() + badge: number | null = null + + get isService(): boolean { + return !this.id.includes('/') + } + + @HostListener('click') + onClick() { + const { id, icon, title } = this + const routerLink = toRouterLink(id) + + this.navigation.addTab({ icon, title, routerLink }) + } + + // Prevents Firefox from starting a native drag + @HostListener('pointerdown.prevent') + onDown() {} +} diff --git a/web/projects/ui/src/app/apps/portal/components/card/card.component.html b/web/projects/ui/src/app/apps/portal/components/card/card.component.html deleted file mode 100644 index 3c092b3f7..000000000 --- a/web/projects/ui/src/app/apps/portal/components/card/card.component.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - {{ badge }} - - - - - - - - - - - - - - {{ title }} - - - - diff --git a/web/projects/ui/src/app/apps/portal/components/card/card.component.scss b/web/projects/ui/src/app/apps/portal/components/card/card.component.scss deleted file mode 100644 index 8cddd4339..000000000 --- a/web/projects/ui/src/app/apps/portal/components/card/card.component.scss +++ /dev/null @@ -1,49 +0,0 @@ -:host { - display: flex; - height: 5.5rem; - width: 12.5rem; - border-radius: var(--tui-radius-l); - overflow: hidden; - box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%); - // TODO: Theme - background: rgb(111 109 109); -} - -.link { - display: flex; - flex: 1; - flex-direction: column; - align-items: center; - justify-content: center; - color: white; - gap: 0.25rem; - padding: 0 0.5rem; - font: var(--tui-font-text-m); - white-space: nowrap; - overflow: hidden; -} - -.icon { - width: 2.5rem; - height: 2.5rem; - border-radius: 100%; - color: var(--tui-text-01-night); -} - -tui-svg.icon { - transform: scale(1.5); -} - -.title { - max-width: 100%; -} - -.side { - width: 3rem; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%); - // TODO: Theme - background: #4b4a4a; -} diff --git a/web/projects/ui/src/app/apps/portal/components/card/card.component.ts b/web/projects/ui/src/app/apps/portal/components/card/card.component.ts deleted file mode 100644 index 4138aca3c..000000000 --- a/web/projects/ui/src/app/apps/portal/components/card/card.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { CommonModule } from '@angular/common' -import { - ChangeDetectionStrategy, - Component, - HostListener, - inject, - Input, -} from '@angular/core' -import { - TuiBadgedContentModule, - TuiBadgeNotificationModule, - TuiButtonModule, - TuiIconModule, -} from '@taiga-ui/experimental' -import { RouterLink } from '@angular/router' -import { TickerModule } from '@start9labs/shared' -import { TuiDataListModule, TuiHostedDropdownModule } from '@taiga-ui/core' -import { NavigationService } from '../../services/navigation.service' -import { Action, ActionsComponent } from '../actions/actions.component' -import { toRouterLink } from '../../utils/to-router-link' - -@Component({ - selector: '[appCard]', - templateUrl: 'card.component.html', - styleUrls: ['card.component.scss'], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - CommonModule, - RouterLink, - TuiButtonModule, - TuiHostedDropdownModule, - TuiDataListModule, - TuiIconModule, - TickerModule, - TuiBadgedContentModule, - TuiBadgeNotificationModule, - ActionsComponent, - ], -}) -export class CardComponent { - private readonly navigation = inject(NavigationService) - - @Input({ required: true }) - id!: string - - @Input({ required: true }) - icon!: string - - @Input({ required: true }) - title!: string - - @Input() - actions: Record = {} - - @Input() - badge: number | null = null - - get isService(): boolean { - return !this.id.includes('/') - } - - @HostListener('click') - onClick() { - const { id, icon, title } = this - const routerLink = toRouterLink(id) - - this.navigation.addTab({ icon, title, routerLink }) - } - - @HostListener('pointerdown.prevent') - onDown() { - // Prevents Firefox from starting a native drag - } -} 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 index 5b1a3c1f7..35a941eba 100644 --- 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 @@ -1,6 +1,6 @@

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 + }
- Nothing found
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 index 9bfca5632..5f2cc0b78 100644 --- 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 @@ -15,11 +15,11 @@ import { } from '@taiga-ui/cdk' import { TuiScrollbarModule, - TuiSvgModule, TuiTextfieldControllerModule, } from '@taiga-ui/core' +import { TuiIconModule } from '@taiga-ui/experimental' import { TuiInputModule } from '@taiga-ui/kit' -import { CardComponent } from '../card/card.component' +import { CardComponent } from '../card.component' import { ServicesService } from '../../services/services.service' import { toRouterLink } from '../../utils/to-router-link' import { DrawerItemDirective } from './drawer-item.directive' @@ -36,7 +36,6 @@ import { ToBadgePipe } from '../../pipes/to-badge' CommonModule, FormsModule, RouterLink, - TuiSvgModule, TuiScrollbarModule, TuiActiveZoneModule, TuiInputModule, @@ -46,6 +45,7 @@ import { ToBadgePipe } from '../../pipes/to-badge' CardComponent, DrawerItemDirective, ToBadgePipe, + TuiIconModule, ], }) export class DrawerComponent { diff --git a/web/projects/ui/src/app/apps/portal/components/header/header-connection.component.ts b/web/projects/ui/src/app/apps/portal/components/header/header-connection.component.ts new file mode 100644 index 000000000..30bf666c2 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/components/header/header-connection.component.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { TuiIconModule } from '@taiga-ui/experimental' +import { PatchDB } from 'patch-db-client' +import { combineLatest, map, Observable, startWith } from 'rxjs' +import { ConnectionService } from 'src/app/services/connection.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { AsyncPipe } from '@angular/common' + +@Component({ + standalone: true, + selector: 'header-connection', + template: ` + @if (connection$ | async; as connection) { + + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiIconModule, AsyncPipe], +}) +export class HeaderConnectionComponent { + readonly connection$: Observable<{ + message: string + color: string + icon: string + }> = combineLatest([ + inject(ConnectionService).networkConnected$, + inject(ConnectionService).websocketConnected$.pipe(startWith(false)), + inject(PatchDB) + .watch$('server-info', 'status-info') + .pipe(startWith({ restarting: false, 'shutting-down': false })), + ]).pipe( + map(([network, websocket, status]) => { + if (!network) + return { + message: 'No Internet', + color: 'var(--tui-error-fill)', + icon: 'tuiIconCloudOff', + } + if (!websocket) + return { + message: 'Connecting', + color: 'var(--tui-warning-fill)', + icon: 'tuiIconCloudOff', + } + if (status['shutting-down']) + return { + message: 'Shutting Down', + color: 'var(--tui-neutral-fill)', + icon: 'tuiIconPower', + } + if (status.restarting) + return { + message: 'Restarting', + color: 'var(--tui-neutral-fill)', + icon: 'tuiIconPower', + } + + return { + message: 'Connected', + color: 'var(--tui-success-fill)', + icon: 'tuiIconCloud', + } + }), + ) +} diff --git a/web/projects/ui/src/app/apps/portal/components/header/header-menu.component.ts b/web/projects/ui/src/app/apps/portal/components/header/header-menu.component.ts index f7154028c..253a3b0a7 100644 --- a/web/projects/ui/src/app/apps/portal/components/header/header-menu.component.ts +++ b/web/projects/ui/src/app/apps/portal/components/header/header-menu.component.ts @@ -1,24 +1,26 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' import { TuiDataListModule, + TuiDialogOptions, TuiDialogService, TuiHostedDropdownModule, TuiSvgModule, } from '@taiga-ui/core' -import { TuiButtonModule } from '@taiga-ui/experimental' +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/util/get-package-data' +import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ selector: 'header-menu', template: ` - + @@ -26,43 +28,35 @@ import { ABOUT } from './about.component'

StartOS

- - - + @for (link of links; track $index) { + + + {{ link.name }} + + + } - - - + @for (item of system; track $index) { + + } @@ -72,6 +66,10 @@ import { ABOUT } from './about.component' `, styles: [ ` + tui-icon { + font-size: 1rem; + } + .item { justify-content: flex-start; gap: 0.75rem; @@ -80,7 +78,6 @@ import { ABOUT } from './about.component' .title { margin: 0; padding: 0 0.5rem 0.25rem; - white-space: nowrap; font: var(--tui-font-text-l); font-weight: bold; } @@ -98,13 +95,50 @@ import { ABOUT } from './about.component' TuiDataListModule, TuiSvgModule, TuiButtonModule, + TuiIconModule, ], }) 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 patch = inject(PatchDB) 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', + }, + ] + + readonly system = [ + { + icon: 'tuiIconTool', + action: 'System Rebuild', + }, + { + icon: 'tuiIconRefreshCw', + action: 'Restart', + }, + { + icon: 'tuiIconPower', + action: 'Shutdown', + }, + ] as const + about() { this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe() } @@ -113,4 +147,72 @@ export class HeaderMenuComponent { this.api.logout({}).catch(e => console.error('Failed to log out', e)) this.auth.setUnverified() } + + async prompt(action: keyof typeof METHODS) { + const minutes = + action === 'System Rebuild' + ? Object.keys(await getAllPackages(this.patch)).length * 2 + : '' + + this.dialogs + .open(TUI_PROMPT, getOptions(action, minutes)) + .pipe(filter(Boolean)) + .subscribe(async () => { + const loader = this.loader.open(`Beginning ${action}...`).subscribe() + + try { + await this.api[METHODS[action]]({}) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + }) + } +} + +const METHODS = { + Restart: 'restartServer', + Shutdown: 'shutdownServer', + 'System Rebuild': 'systemRebuild', +} as const + +function getOptions( + key: keyof typeof METHODS, + minutes: unknown, +): Partial> { + switch (key) { + case 'Restart': + return { + 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', + }, + } + case 'Shutdown': + return { + 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', + }, + } + 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/apps/portal/components/header/header.component.ts b/web/projects/ui/src/app/apps/portal/components/header/header.component.ts index eb9e1fe8b..3e052c1a2 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 @@ -25,19 +25,13 @@ import { SidebarDirective } from '../../../../app/sidebar-host.component' import { HeaderMenuComponent } from './header-menu.component' import { HeaderNotificationsComponent } from './header-notifications.component' import { NotificationService } from '../../services/notification.service' +import { HeaderConnectionComponent } from './header-connection.component' @Component({ selector: 'header[appHeader]', template: ` - + + + + @for (tab of tabs$ | async; track tab) { + + @if (tab.icon.startsWith('tuiIcon')) { + + } @else { + + } + + + } + `, + styles: [ + ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + :host { + @include scrollbar-hidden; + + height: 3rem; + display: flex; + overflow: auto; + // TODO: Theme + background: rgb(97 95 95 / 84%); + } + + .tab { + position: relative; + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 7.5rem; + + &_active { + position: sticky; + left: 0; + right: 0; + z-index: 1; + // TODO: Theme + background: #373a3f; + } + } + + .icon { + width: 2rem; + height: 2rem; + border-radius: 100%; + color: var(--tui-base-08); + } + + .close { + position: absolute; + top: 0; + right: 0; + } + `, + ], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule], +}) +export class NavigationComponent { + private readonly router = inject(Router) + private readonly navigation = inject(NavigationService) + + readonly tabs$ = this.navigation.getTabs() + + removeTab(routerLink: string, active: boolean) { + this.navigation.removeTab(routerLink) + + if (active) this.router.navigate(['/portal/desktop']) + } +} diff --git a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html b/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html deleted file mode 100644 index a40a5c3c2..000000000 --- a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - diff --git a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss b/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss deleted file mode 100644 index ee4d6a05e..000000000 --- a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import '@taiga-ui/core/styles/taiga-ui-local'; - -:host { - @include scrollbar-hidden; - - height: 3rem; - display: flex; - // TODO: Theme - background: rgb(97 95 95 / 84%); - overflow: auto; -} - -.tab { - position: relative; - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: center; - width: 7.5rem; - - &_active { - position: sticky; - left: 0; - right: 0; - z-index: 1; - // TODO: Theme - background: #373a3f; - } -} - -.icon { - width: 2rem; - height: 2rem; - border-radius: 100%; - color: var(--tui-base-08); -} - -.close { - position: absolute; - top: 0; - right: 0; -} diff --git a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts b/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts deleted file mode 100644 index 18ad56ce7..000000000 --- a/web/projects/ui/src/app/apps/portal/components/navigation/navigation.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { Router, RouterModule } from '@angular/router' -import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental' -import { NavigationService } from '../../services/navigation.service' - -@Component({ - selector: 'nav[appNavigation]', - templateUrl: 'navigation.component.html', - styleUrls: ['navigation.component.scss'], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, RouterModule, TuiButtonModule, TuiIconModule], -}) -export class NavigationComponent { - private readonly router = inject(Router) - private readonly navigation = inject(NavigationService) - - readonly tabs$ = this.navigation.getTabs() - - removeTab(routerLink: string, active: boolean) { - this.navigation.removeTab(routerLink) - - if (active) this.router.navigate(['/portal/desktop']) - } -} diff --git a/web/projects/ui/src/app/apps/portal/portal.component.html b/web/projects/ui/src/app/apps/portal/portal.component.html deleted file mode 100644 index a66dfd1b1..000000000 --- a/web/projects/ui/src/app/apps/portal/portal.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
My server
- -
- -
- diff --git a/web/projects/ui/src/app/apps/portal/portal.component.scss b/web/projects/ui/src/app/apps/portal/portal.component.scss deleted file mode 100644 index 821a6ccf5..000000000 --- a/web/projects/ui/src/app/apps/portal/portal.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -:host { - // TODO: Theme - background: url(/assets/img/background_dark.jpeg); - background-size: cover; -} - -main { - flex: 1; - overflow: hidden; -} 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 7c57f5662..53bfe3d6a 100644 --- a/web/projects/ui/src/app/apps/portal/portal.component.ts +++ b/web/projects/ui/src/app/apps/portal/portal.component.ts @@ -1,10 +1,43 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { RouterOutlet } from '@angular/router' import { tuiDropdownOptionsProvider } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' +import { HeaderComponent } from './components/header/header.component' +import { NavigationComponent } from './components/navigation.component' +import { DrawerComponent } from './components/drawer/drawer.component' +import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ - templateUrl: 'portal.component.html', - styleUrls: ['portal.component.scss'], + standalone: true, + template: ` +
{{ name$ | async }}
+ +
+ + `, + styles: [ + ` + :host { + // TODO: Theme + background: url(/assets/img/background_dark.jpeg); + background-size: cover; + } + + main { + flex: 1; + overflow: hidden; + } + `, + ], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + RouterOutlet, + HeaderComponent, + NavigationComponent, + DrawerComponent, + ], providers: [ // TODO: Move to global tuiDropdownOptionsProvider({ @@ -12,4 +45,6 @@ import { tuiDropdownOptionsProvider } from '@taiga-ui/core' }), ], }) -export class PortalComponent {} +export class PortalComponent { + readonly name$ = inject(PatchDB).watch$('ui', 'name') +} diff --git a/web/projects/ui/src/app/apps/portal/portal.module.ts b/web/projects/ui/src/app/apps/portal/portal.module.ts deleted file mode 100644 index ef6abe620..000000000 --- a/web/projects/ui/src/app/apps/portal/portal.module.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { HeaderComponent } from './components/header/header.component' -import { PortalComponent } from './portal.component' -import { NavigationComponent } from './components/navigation/navigation.component' -import { DrawerComponent } from './components/drawer/drawer.component' - -const ROUTES: Routes = [ - { - path: '', - component: PortalComponent, - children: [ - { - redirectTo: 'desktop', - pathMatch: 'full', - path: '', - }, - { - path: 'desktop', - loadChildren: () => - import('./routes/desktop/desktop.module').then(m => m.DesktopModule), - }, - { - path: 'service', - loadChildren: () => - import('./routes/service/service.module').then(m => m.ServiceModule), - }, - { - path: 'system', - loadChildren: () => - import('./routes/system/system.module').then(m => m.SystemModule), - }, - ], - }, -] - -@NgModule({ - imports: [ - RouterModule.forChild(ROUTES), - HeaderComponent, - NavigationComponent, - DrawerComponent, - ], - declarations: [PortalComponent], - exports: [PortalComponent], -}) -export class PortalModule {} diff --git a/web/projects/ui/src/app/apps/portal/portal.routes.ts b/web/projects/ui/src/app/apps/portal/portal.routes.ts new file mode 100644 index 000000000..20db07d63 --- /dev/null +++ b/web/projects/ui/src/app/apps/portal/portal.routes.ts @@ -0,0 +1,35 @@ +import { Routes } from '@angular/router' +import { PortalComponent } from './portal.component' + +const ROUTES: Routes = [ + { + path: '', + component: PortalComponent, + children: [ + { + redirectTo: 'desktop', + pathMatch: 'full', + path: '', + }, + { + path: 'desktop', + loadComponent: () => + import('./routes/desktop/desktop.component').then( + m => m.DesktopComponent, + ), + }, + { + path: 'service', + loadChildren: () => + import('./routes/service/service.module').then(m => m.ServiceModule), + }, + { + path: 'system', + loadChildren: () => + import('./routes/system/system.module').then(m => m.SystemModule), + }, + ], + }, +] + +export default ROUTES 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 index 4ebcc0edf..f54fc39fa 100644 --- 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 @@ -4,7 +4,6 @@ import { HostBinding, inject, Input, - OnDestroy, OnInit, } from '@angular/core' import { TuiTilesComponent } from '@taiga-ui/kit' @@ -17,7 +16,7 @@ import { TuiTilesComponent } from '@taiga-ui/kit' selector: '[desktopItem]', standalone: true, }) -export class DesktopItemDirective implements OnInit, OnDestroy { +export class DesktopItemDirective implements OnInit { private readonly element: Element = inject(ElementRef).nativeElement private readonly tiles = inject(TuiTilesComponent) @@ -32,9 +31,4 @@ export class DesktopItemDirective implements OnInit, OnDestroy { ngOnInit() { if (this.empty) this.tiles.element = this.element } - - // TODO: Remove after Taiga UI updated to 3.40.0 - ngOnDestroy() { - if (this.tiles.element === this.element) this.tiles.element = null - } } 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 index 913d8d24f..589146a7e 100644 --- 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 @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common' import { Component, ElementRef, @@ -6,18 +7,48 @@ import { 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, tuiScaleIn } from '@taiga-ui/core' -import { TuiTileComponent, TuiTilesComponent } from '@taiga-ui/kit' +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 }) diff --git a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts b/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts deleted file mode 100644 index 34932972c..000000000 --- a/web/projects/ui/src/app/apps/portal/routes/desktop/desktop.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CommonModule } from '@angular/common' -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { DragScrollerDirective } from '@start9labs/shared' -import { TuiLoaderModule, TuiSvgModule } from '@taiga-ui/core' -import { TuiFadeModule } from '@taiga-ui/experimental' -import { TuiTilesModule } from '@taiga-ui/kit' -import { DesktopComponent } from './desktop.component' -import { CardComponent } from '../../components/card/card.component' -import { ToNavigationItemPipe } from '../../pipes/to-navigation-item' -import { ToBadgePipe } from '../../pipes/to-badge' -import { DesktopItemDirective } from './desktop-item.directive' - -const ROUTES: Routes = [ - { - path: '', - component: DesktopComponent, - }, -] - -@NgModule({ - imports: [ - CommonModule, - CardComponent, - DesktopItemDirective, - TuiSvgModule, - TuiLoaderModule, - TuiTilesModule, - ToNavigationItemPipe, - RouterModule.forChild(ROUTES), - TuiFadeModule, - DragScrollerDirective, - ToBadgePipe, - ], - declarations: [DesktopComponent], - exports: [DesktopComponent], -}) -export class DesktopModule {} 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 94f4885c3..1b4a19a18 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,5 +1,5 @@ import { inject, Pipe, PipeTransform, Type } from '@angular/core' -import { ActivatedRoute, Params, Router } from '@angular/router' +import { Params } from '@angular/router' import { Manifest } from '@start9labs/marketplace' import { MarkdownComponent } from '@start9labs/shared' import { TuiDialogService } from '@taiga-ui/core' @@ -33,8 +33,6 @@ export class ToMenuPipe implements PipeTransform { private readonly api = inject(ApiService) private readonly dialogs = inject(TuiDialogService) private readonly formDialog = inject(FormDialogService) - private readonly route = inject(ActivatedRoute) - private readonly router = inject(Router) private readonly proxyService = inject(ProxyService) transform({ manifest, installed }: PackageDataEntry): ServiceMenu[] { diff --git a/web/projects/ui/src/app/apps/portal/routes/system/settings/components/button.component.ts b/web/projects/ui/src/app/apps/portal/routes/system/settings/components/button.component.ts index 80fabf1f2..e53d7fdae 100644 --- a/web/projects/ui/src/app/apps/portal/routes/system/settings/components/button.component.ts +++ b/web/projects/ui/src/app/apps/portal/routes/system/settings/components/button.component.ts @@ -10,15 +10,6 @@ import { SettingBtn } from '../settings.types' - - - - `, styles: [ diff --git a/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.service.ts b/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.service.ts index a5dc9634c..e069ab3d3 100644 --- a/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.service.ts +++ b/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.service.ts @@ -111,26 +111,6 @@ export class SettingsService { routerLink: 'sessions', }, ], - Support: [ - { - title: 'User Manual', - description: 'Discover what StartOS can do', - icon: 'tuiIconMap', - href: 'https://docs.start9.com/0.3.5.x/user-manual', - }, - { - title: 'Contact Support', - description: 'Get help from the Start9 team and community', - icon: 'tuiIconMessageSquare', - href: 'https://start9.com/contact', - }, - { - title: 'Donate to Start9', - description: `Support StartOS development`, - icon: 'tuiIconDollarSign', - href: 'https://donate.start9.com', - }, - ], } private async setBrowserTab(): Promise { diff --git a/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.types.ts b/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.types.ts index 9f3474208..5ae9b4048 100644 --- a/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.types.ts +++ b/web/projects/ui/src/app/apps/portal/routes/system/settings/settings.types.ts @@ -6,7 +6,6 @@ export interface SettingBtn { description: string icon: string action?: Function - href?: string routerLink?: string } diff --git a/web/projects/ui/src/app/routing.module.ts b/web/projects/ui/src/app/routing.module.ts index f713d6fa8..be417216d 100644 --- a/web/projects/ui/src/app/routing.module.ts +++ b/web/projects/ui/src/app/routing.module.ts @@ -26,8 +26,7 @@ const routes: Routes = [ path: 'portal', canActivate: [AuthGuard], canActivateChild: [AuthGuard], - loadChildren: () => - import('./apps/portal/portal.module').then(m => m.PortalModule), + loadChildren: () => import('./apps/portal/portal.routes').then(m => m), }, { path: '',