mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
refactor: change navigation
Signed-off-by: waterplea <alexander@inkin.ru>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" height="1em" width="1em">
|
||||||
|
<g id="tuiIconCheckCircle" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<svg x="50%" y="50%" fill="none" height="1em" overflow="visible" viewBox="0 0 16 16" width="1em">
|
||||||
|
<svg
|
||||||
|
x="-8"
|
||||||
|
y="-8"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
vector-effect="non-scaling-stroke"
|
||||||
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"
|
||||||
|
/>
|
||||||
|
<path vector-effect="non-scaling-stroke" d="M15.5 9.5l-4.5 5-2.5-2.273" />
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 988 B |
@@ -178,26 +178,36 @@ tui-hint[data-appearance='onDark'] {
|
|||||||
|
|
||||||
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
backdrop-filter: blur(0.25rem);
|
backdrop-filter: blur(0.25rem);
|
||||||
box-shadow: 0 0.25rem 0.25rem rgb(0 0 0 / 25%);
|
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
|
// 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 {
|
tui-opt-group {
|
||||||
&::before {
|
&::before {
|
||||||
background: var(--tui-clear);
|
background: var(--tui-clear);
|
||||||
box-shadow:
|
height: 1px;
|
||||||
1rem 0 var(--tui-clear),
|
|
||||||
-1rem 0 var(--tui-clear);
|
|
||||||
padding-top: 0.25rem !important;
|
|
||||||
padding-bottom: 0 !important;
|
|
||||||
margin: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
display: none;
|
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 {
|
[tuiSidebar] > div.t-wrapper {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'
|
|||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||||
import { LoadingModule } from '@start9labs/shared'
|
import { LoadingModule } from '@start9labs/shared'
|
||||||
|
import { TuiSheetDialogModule } from '@taiga-ui/addon-mobile'
|
||||||
import {
|
import {
|
||||||
TuiAlertModule,
|
TuiAlertModule,
|
||||||
TuiDialogModule,
|
TuiDialogModule,
|
||||||
@@ -27,6 +28,7 @@ import { RoutingModule } from './routing.module'
|
|||||||
ToastContainerComponent,
|
ToastContainerComponent,
|
||||||
TuiRootModule,
|
TuiRootModule,
|
||||||
TuiDialogModule,
|
TuiDialogModule,
|
||||||
|
TuiSheetDialogModule,
|
||||||
TuiAlertModule,
|
TuiAlertModule,
|
||||||
TuiModeModule,
|
TuiModeModule,
|
||||||
TuiThemeNightModule,
|
TuiThemeNightModule,
|
||||||
|
|||||||
@@ -1,54 +1,52 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import {
|
import { RouterLink } from '@angular/router'
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
HostListener,
|
|
||||||
inject,
|
|
||||||
ViewChild,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { Router } from '@angular/router'
|
|
||||||
import { TuiSidebarModule } from '@taiga-ui/addon-mobile'
|
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 {
|
import {
|
||||||
TuiBadgedContentModule,
|
TuiBadgedContentModule,
|
||||||
TuiBadgeNotificationModule,
|
TuiBadgeNotificationModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
} from '@taiga-ui/experimental'
|
} 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 { 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({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'header-corner',
|
selector: 'header-corner',
|
||||||
template: `
|
template: `
|
||||||
<ng-content />
|
<ng-content />
|
||||||
<tui-badged-content
|
@for (item of utils; track $index) {
|
||||||
*tuiLet="notificationService.unreadCount$ | async as unread"
|
@if (item.badge(); as badge) {
|
||||||
[style.--tui-radius.%]="50"
|
<tui-badged-content
|
||||||
>
|
[style.--tui-radius.%]="50"
|
||||||
<tui-badge-notification *ngIf="unread" tuiSlot="top" size="s">
|
[@tuiFadeIn]="animation"
|
||||||
{{ unread }}
|
[@tuiWidthCollapse]="animation"
|
||||||
</tui-badge-notification>
|
[@tuiScaleIn]="animation"
|
||||||
<button
|
>
|
||||||
tuiIconButton
|
<tui-badge-notification tuiSlot="top" size="s">
|
||||||
iconLeft="tuiIconBellLarge"
|
{{ badge }}
|
||||||
appearance="icon"
|
</tui-badge-notification>
|
||||||
size="s"
|
<a
|
||||||
[style.color]="'var(--tui-text-01)'"
|
tuiIconButton
|
||||||
(click)="handleNotificationsClick(unread || 0)"
|
appearance="icon"
|
||||||
>
|
size="s"
|
||||||
Notifications
|
[iconLeft]="item.icon"
|
||||||
</button>
|
[routerLink]="item.routerLink"
|
||||||
</tui-badged-content>
|
[style.color]="'var(--tui-text-01)'"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</tui-badged-content>
|
||||||
|
}
|
||||||
|
}
|
||||||
<header-menu></header-menu>
|
<header-menu></header-menu>
|
||||||
<header-notifications
|
|
||||||
(onEmpty)="this.open$.next(false)"
|
|
||||||
*tuiSidebar="!!(open$ | async); direction: 'right'; autoWidth: true"
|
|
||||||
/>
|
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
@@ -65,48 +63,20 @@ import { NotificationService } from 'src/app/services/notification.service'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
|
animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderMenuComponent,
|
HeaderMenuComponent,
|
||||||
HeaderNotificationsComponent,
|
|
||||||
SidebarDirective,
|
SidebarDirective,
|
||||||
TuiBadgeNotificationModule,
|
TuiBadgeNotificationModule,
|
||||||
TuiBadgedContentModule,
|
TuiBadgedContentModule,
|
||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiLetModule,
|
TuiLetModule,
|
||||||
TuiSidebarModule,
|
TuiSidebarModule,
|
||||||
|
RouterLink,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HeaderCornerComponent {
|
export class HeaderCornerComponent {
|
||||||
private readonly router = inject(Router)
|
readonly animation = inject(TUI_ANIMATION_OPTIONS)
|
||||||
readonly notificationService = inject(NotificationService)
|
readonly utils = getMenu()
|
||||||
|
|
||||||
@ViewChild(HeaderNotificationsComponent, { read: ElementRef })
|
|
||||||
private readonly panel?: ElementRef<HTMLElement>
|
|
||||||
|
|
||||||
private readonly _ = this.router.events.subscribe(() => {
|
|
||||||
this.open$.next(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
readonly open$ = new Subject<boolean>()
|
|
||||||
|
|
||||||
@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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
|
|||||||
margin-left: -1.25rem;
|
margin-left: -1.25rem;
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: blur(1rem);
|
||||||
clip-path: var(--clip-path);
|
clip-path: var(--clip-path);
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
> a:active,
|
||||||
backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75);
|
> button:active {
|
||||||
}
|
backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has([data-connection='error']) {
|
&:has([data-connection='error']) {
|
||||||
|
|||||||
@@ -1,65 +1,86 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
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 {
|
import {
|
||||||
TuiDataListModule,
|
TuiDataListModule,
|
||||||
TuiDialogOptions,
|
|
||||||
TuiDialogService,
|
TuiDialogService,
|
||||||
|
TuiDropdownModule,
|
||||||
TuiHostedDropdownModule,
|
TuiHostedDropdownModule,
|
||||||
TuiSvgModule,
|
TuiSvgModule,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
|
import {
|
||||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
TuiBadgeNotificationModule,
|
||||||
import { filter } from 'rxjs'
|
TuiButtonModule,
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
TuiIconModule,
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
} 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 { ABOUT } from './about.component'
|
||||||
import { HeaderConnectionComponent } from './connection.component'
|
import { HeaderConnectionComponent } from './connection.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header-menu',
|
selector: 'header-menu',
|
||||||
template: `
|
template: `
|
||||||
<tui-hosted-dropdown [content]="content" [tuiDropdownMaxHeight]="9999">
|
<tui-hosted-dropdown
|
||||||
|
[content]="content"
|
||||||
|
[(open)]="open"
|
||||||
|
[tuiDropdownMaxHeight]="9999"
|
||||||
|
>
|
||||||
<button tuiIconButton appearance="">
|
<button tuiIconButton appearance="">
|
||||||
<img style="max-width: 62%" src="assets/img/icon.png" alt="StartOS" />
|
<img [style.max-width.%]="50" src="assets/img/icon.png" alt="StartOS" />
|
||||||
</button>
|
</button>
|
||||||
<ng-template #content>
|
<ng-template #content let-zone>
|
||||||
<tui-data-list>
|
<tui-data-list tuiDataListDropdownManager [tuiActiveZoneParent]="zone">
|
||||||
<header-connection class="status">
|
<header-connection class="status">
|
||||||
<h3 class="title">StartOS</h3>
|
<h3 class="title">StartOS</h3>
|
||||||
</header-connection>
|
</header-connection>
|
||||||
<button tuiOption class="item" (click)="about()">
|
@for (link of utils; track $index) {
|
||||||
<tui-icon icon="tuiIconInfo" />
|
<a
|
||||||
About this server
|
tuiOption
|
||||||
|
class="item"
|
||||||
|
[routerLink]="link.routerLink"
|
||||||
|
(click)="open = false"
|
||||||
|
>
|
||||||
|
<tui-icon [icon]="link.icon" />
|
||||||
|
{{ link.name }}
|
||||||
|
@if (link.badge(); as badge) {
|
||||||
|
<tui-badge-notification>{{ badge }}</tui-badge-notification>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
class="item"
|
||||||
|
tuiDropdownSided
|
||||||
|
[tuiDropdown]="dropdown"
|
||||||
|
[tuiDropdownOffset]="12"
|
||||||
|
[tuiDropdownManual]="false"
|
||||||
|
>
|
||||||
|
<tui-icon icon="tuiIconHelpCircle" />
|
||||||
|
Resources
|
||||||
|
<ng-template #dropdown>
|
||||||
|
<tui-data-list [tuiActiveZoneParent]="zone">
|
||||||
|
<button tuiOption class="item" (click)="about()">
|
||||||
|
<tui-icon icon="tuiIconInfo" />
|
||||||
|
About this server
|
||||||
|
</button>
|
||||||
|
@for (link of links; track $index) {
|
||||||
|
<a
|
||||||
|
tuiOption
|
||||||
|
class="item"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
[href]="link.href"
|
||||||
|
>
|
||||||
|
<tui-icon [icon]="link.icon" />
|
||||||
|
{{ link.name }}
|
||||||
|
<tui-icon class="external" icon="tuiIconExternalLink" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</tui-data-list>
|
||||||
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
<tui-opt-group>
|
|
||||||
@for (link of links; track $index) {
|
|
||||||
<a
|
|
||||||
tuiOption
|
|
||||||
class="item"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
[href]="link.href"
|
|
||||||
>
|
|
||||||
<tui-icon [icon]="link.icon" />
|
|
||||||
{{ link.name }}
|
|
||||||
<tui-icon class="external" icon="tuiIconArrowUpRight" />
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</tui-opt-group>
|
|
||||||
<tui-opt-group>
|
|
||||||
@for (item of system; track $index) {
|
|
||||||
<button tuiOption class="item" (click)="prompt(item.action)">
|
|
||||||
<tui-icon [icon]="item.icon" />
|
|
||||||
{{ item.action }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</tui-opt-group>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button tuiOption class="item" (click)="logout()">
|
|
||||||
<tui-icon icon="tuiIconLogOut" />
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
</tui-data-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</tui-hosted-dropdown>
|
</tui-hosted-dropdown>
|
||||||
@@ -70,9 +91,22 @@ import { HeaderConnectionComponent } from './connection.component'
|
|||||||
font-size: 1rem;
|
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 {
|
.item {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
::ng-deep tui-svg {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
@@ -80,7 +114,7 @@ import { HeaderConnectionComponent } from './connection.component'
|
|||||||
font-size: 0;
|
font-size: 0;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
width: 14rem;
|
width: 13rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -104,95 +138,22 @@ import { HeaderConnectionComponent } from './connection.component'
|
|||||||
TuiButtonModule,
|
TuiButtonModule,
|
||||||
TuiIconModule,
|
TuiIconModule,
|
||||||
HeaderConnectionComponent,
|
HeaderConnectionComponent,
|
||||||
|
RouterLink,
|
||||||
|
TuiBadgeNotificationModule,
|
||||||
|
TuiDropdownModule,
|
||||||
|
TuiDataListDropdownManagerModule,
|
||||||
|
TuiActiveZoneModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HeaderMenuComponent {
|
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)
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
|
||||||
readonly links = [
|
open = false
|
||||||
{
|
|
||||||
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 = [
|
readonly utils = getMenu()
|
||||||
{
|
readonly links = RESOURCES
|
||||||
icon: 'tuiIconRefreshCw',
|
|
||||||
action: 'Restart',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'tuiIconPower',
|
|
||||||
action: 'Shutdown',
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
about() {
|
about() {
|
||||||
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
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<TuiDialogOptions<TuiPromptData>> {
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: `
|
|
||||||
<tui-svg
|
|
||||||
style="align-self: flex-start; margin: 0.25rem 0;"
|
|
||||||
[style.color]="color"
|
|
||||||
[src]="icon"
|
|
||||||
></tui-svg>
|
|
||||||
<div tuiTitle>
|
|
||||||
<div tuiSubtitle><ng-content></ng-content></div>
|
|
||||||
<div [style.color]="color">
|
|
||||||
{{ notification.title }}
|
|
||||||
</div>
|
|
||||||
<tui-line-clamp
|
|
||||||
tuiSubtitle
|
|
||||||
style="pointer-events: none"
|
|
||||||
[linesLimit]="4"
|
|
||||||
[lineHeight]="16"
|
|
||||||
[content]="notification.message"
|
|
||||||
(overflownChange)="overflow = $event"
|
|
||||||
/>
|
|
||||||
<div style="display: flex; gap: 0.5rem; padding-top: 0.5rem;">
|
|
||||||
<button
|
|
||||||
*ngIf="notification.code === 1"
|
|
||||||
tuiButton
|
|
||||||
appearance="secondary"
|
|
||||||
size="xs"
|
|
||||||
(click)="service.viewReport(notification)"
|
|
||||||
>
|
|
||||||
View Report
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
*ngIf="overflow"
|
|
||||||
tuiButton
|
|
||||||
appearance="secondary"
|
|
||||||
size="xs"
|
|
||||||
(click)="service.viewFull(notification)"
|
|
||||||
>
|
|
||||||
View full
|
|
||||||
</button>
|
|
||||||
<ng-content select="a"></ng-content>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-content select="button"></ng-content>
|
|
||||||
`,
|
|
||||||
styles: [':host { box-shadow: 0 1px var(--tui-clear); }'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
TuiSvgModule,
|
|
||||||
TuiTitleModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiLineClampModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class HeaderNotificationComponent<T extends number> {
|
|
||||||
readonly service = inject(NotificationService)
|
|
||||||
|
|
||||||
@Input({ required: true }) notification!: ServerNotification<T>
|
|
||||||
|
|
||||||
overflow = false
|
|
||||||
|
|
||||||
get color(): string {
|
|
||||||
return this.service.getColor(this.notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
get icon(): string {
|
|
||||||
return this.service.getIcon(this.notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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: `
|
|
||||||
<ng-container *ngIf="notifications$ | async as notifications">
|
|
||||||
<h3 class="g-title" style="padding: 0 1rem">
|
|
||||||
Notifications
|
|
||||||
<a
|
|
||||||
*ngIf="notifications.length"
|
|
||||||
style="margin-left: auto; text-transform: none; font-size: 0.9rem; font-weight: 600;"
|
|
||||||
(click)="markAllSeen(notifications[0].id)"
|
|
||||||
>
|
|
||||||
Mark All Seen
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<tui-scrollbar *ngIf="packageData$ | async as packageData">
|
|
||||||
<header-notification
|
|
||||||
*ngFor="let not of notifications; let i = index"
|
|
||||||
tuiCell
|
|
||||||
[notification]="not"
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="not.packageId as pkgId">
|
|
||||||
{{
|
|
||||||
packageData[pkgId]
|
|
||||||
? (packageData[pkgId] | toManifest).title
|
|
||||||
: pkgId
|
|
||||||
}}
|
|
||||||
</ng-container>
|
|
||||||
<button
|
|
||||||
style="align-self: flex-start; flex-shrink: 0;"
|
|
||||||
tuiIconButton
|
|
||||||
appearance="icon"
|
|
||||||
iconLeft="tuiIconMinusCircle"
|
|
||||||
(click)="markSeen(notifications, not)"
|
|
||||||
></button>
|
|
||||||
<a
|
|
||||||
*ngIf="not.packageId && packageData[not.packageId]"
|
|
||||||
tuiButton
|
|
||||||
size="xs"
|
|
||||||
appearance="secondary"
|
|
||||||
[routerLink]="getLink(not.packageId || '')"
|
|
||||||
>
|
|
||||||
View Service
|
|
||||||
</a>
|
|
||||||
</header-notification>
|
|
||||||
</tui-scrollbar>
|
|
||||||
<a
|
|
||||||
style="margin: 2rem; text-align: center; font-size: 0.9rem; font-weight: 600;"
|
|
||||||
[routerLink]="'/portal/system/notifications'"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
</a>
|
|
||||||
</ng-container>
|
|
||||||
`,
|
|
||||||
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<DataModel>)
|
|
||||||
private readonly service = inject(NotificationService)
|
|
||||||
|
|
||||||
readonly packageData$ = this.patch.watch$('packageData').pipe(first())
|
|
||||||
|
|
||||||
readonly notifications$ = new Subject<ServerNotifications>()
|
|
||||||
|
|
||||||
@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<number>,
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +1,170 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import {
|
||||||
import { Component, inject } from '@angular/core'
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
TemplateRef,
|
||||||
|
viewChildren,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router'
|
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||||
import { TuiTabBarModule } from '@taiga-ui/addon-mobile'
|
import { TuiSheetDialogService, TuiTabBarModule } from '@taiga-ui/addon-mobile'
|
||||||
import { combineLatest, map, startWith } from 'rxjs'
|
import { TuiDialogService } from '@taiga-ui/core'
|
||||||
import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities'
|
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 { 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({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'app-tabs',
|
selector: 'app-tabs',
|
||||||
template: `
|
template: `
|
||||||
<nav tuiTabBar>
|
<nav tuiTabBar [(activeItemIndex)]="index">
|
||||||
<a
|
<a
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
icon="tuiIconGrid"
|
icon="tuiIconGrid"
|
||||||
routerLink="/portal/dashboard"
|
routerLink="/portal/dashboard"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[routerLinkActiveOptions]="{ exact: true }"
|
(isActiveChange)="update()"
|
||||||
>
|
>
|
||||||
Services
|
Services
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
icon="tuiIconActivity"
|
icon="tuiIconShoppingCart"
|
||||||
routerLink="/portal/system/metrics"
|
routerLink="/portal/system/marketplace"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
|
(isActiveChange)="update()"
|
||||||
>
|
>
|
||||||
Metrics
|
Marketplace
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
icon="tuiIconSettings"
|
icon="tuiIconSettings"
|
||||||
routerLink="/portal/dashboard"
|
routerLink="/portal/system/settings"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[routerLinkActiveOptions]="{ exact: true }"
|
[badge]="badge()"
|
||||||
[queryParams]="{ tab: 'utilities' }"
|
(isActiveChange)="update()"
|
||||||
[badge]="(utils$ | async) || 0"
|
|
||||||
>
|
>
|
||||||
Utilities
|
Settings
|
||||||
</a>
|
</a>
|
||||||
<a
|
<button
|
||||||
tuiTabBarItem
|
tuiTabBarItem
|
||||||
routerLinkActive
|
icon="tuiIconMoreHorizontal"
|
||||||
routerLink="/portal/system/notifications"
|
(click)="more(content)"
|
||||||
icon="tuiIconBell"
|
[badge]="all()"
|
||||||
[badge]="(notification$ | async) || 0"
|
|
||||||
>
|
>
|
||||||
Notifications
|
More
|
||||||
</a>
|
<ng-template #content let-observer>
|
||||||
|
@for (item of menu; track $index) {
|
||||||
|
<a
|
||||||
|
class="item"
|
||||||
|
routerLinkActive="item_active"
|
||||||
|
[routerLink]="item.routerLink"
|
||||||
|
(click)="observer.complete()"
|
||||||
|
>
|
||||||
|
<tui-icon [icon]="item.icon" />
|
||||||
|
{{ item.name }}
|
||||||
|
@if (item.badge(); as badge) {
|
||||||
|
<tui-badge-notification>{{ badge }}</tui-badge-notification>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<button class="item" (click)="about()">
|
||||||
|
<tui-icon icon="tuiIconInfo" />
|
||||||
|
About this server
|
||||||
|
</button>
|
||||||
|
@for (link of resources; track $index) {
|
||||||
|
<a class="item" target="_blank" rel="noreferrer" [href]="link.href">
|
||||||
|
<tui-icon [icon]="link.icon" />
|
||||||
|
{{ link.name }}
|
||||||
|
<tui-icon
|
||||||
|
icon="tuiIconExternalLink"
|
||||||
|
[style.margin-inline-start]="'auto'"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
|
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: none;
|
display: none;
|
||||||
|
backdrop-filter: blur(1rem);
|
||||||
// TODO: Theme
|
// TODO: Theme
|
||||||
--tui-elevation-01: #333;
|
--tui-elevation-01: #333;
|
||||||
--tui-base-01: #fff;
|
--tui-base-01: #fff;
|
||||||
--tui-base-04: var(--tui-clear);
|
--tui-base-04: var(--tui-clear);
|
||||||
--tui-error-fill: #f52222;
|
--tui-error-fill: #f52222;
|
||||||
backdrop-filter: blur(1rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiTabBar]::before {
|
[tuiTabBar]::before {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
@include clearbtn();
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: 0.75rem 0.25rem;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&_active {
|
||||||
|
color: var(--tui-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
imports: [AsyncPipe, RouterLink, RouterLinkActive, TuiTabBarModule],
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
RouterLinkActive,
|
||||||
|
TuiTabBarModule,
|
||||||
|
TuiBadgeNotificationModule,
|
||||||
|
TuiIconModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TabsComponent {
|
export class TabsComponent {
|
||||||
private readonly badge = inject(BadgeService)
|
private readonly sheets = inject(TuiSheetDialogService)
|
||||||
|
private readonly dialogs = inject(TuiDialogService)
|
||||||
|
private readonly links = viewChildren(RouterLinkActive)
|
||||||
|
|
||||||
readonly utils$ = combineLatest(
|
index = 3
|
||||||
Object.keys(SYSTEM_UTILITIES)
|
|
||||||
.filter(key => key !== '/portal/system/notifications')
|
readonly resources = RESOURCES
|
||||||
.map(key => this.badge.getCount(key).pipe(startWith(0))),
|
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
|
||||||
).pipe(map(values => values.reduce((acc, value) => acc + value, 0)))
|
readonly badge = toSignal(
|
||||||
readonly notification$ = inject(NotificationService).unreadCount$
|
inject(BadgeService).getCount('/portal/system/settings'),
|
||||||
|
{ initialValue: 0 },
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly all = computed(() =>
|
||||||
|
this.menu.reduce((acc, item) => acc + item.badge(), 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
about() {
|
||||||
|
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
more(content: TemplateRef<any>) {
|
||||||
|
this.sheets.open(content, { label: 'Start OS' }).subscribe({
|
||||||
|
complete: () => this.update(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const index = this.links().findIndex(link => link.isActive)
|
||||||
|
this.index = index === -1 ? 3 : index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
import {
|
import { NavigationEnd, Router, RouterOutlet } from '@angular/router'
|
||||||
ActivatedRoute,
|
|
||||||
NavigationEnd,
|
|
||||||
Router,
|
|
||||||
RouterOutlet,
|
|
||||||
} from '@angular/router'
|
|
||||||
import { TuiScrollbarModule } from '@taiga-ui/core'
|
import { TuiScrollbarModule } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter, map } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { TabsComponent } from 'src/app/routes/portal/components/tabs.component'
|
import { TabsComponent } from 'src/app/routes/portal/components/tabs.component'
|
||||||
import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
|
import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -19,7 +14,7 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
<header appHeader>{{ name$ | async }}</header>
|
<header appHeader>{{ name$ | async }}</header>
|
||||||
<main [attr.data-dashboard]="tab$ | async">
|
<main>
|
||||||
<tui-scrollbar [style.max-height.%]="100">
|
<tui-scrollbar [style.max-height.%]="100">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</tui-scrollbar>
|
</tui-scrollbar>
|
||||||
@@ -64,7 +59,4 @@ export class PortalComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
readonly name$ = inject(PatchDB<DataModel>).watch$('ui', 'name')
|
||||||
readonly tab$ = inject(ActivatedRoute).queryParams.pipe(
|
|
||||||
map(params => params['tab']),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,68 @@
|
|||||||
import { AsyncPipe, DatePipe } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { TuiIconModule } from '@taiga-ui/experimental'
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { map } from 'rxjs'
|
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
||||||
import { MetricsService } from 'src/app/services/metrics.service'
|
import { ServiceComponent } from 'src/app/routes/portal/routes/dashboard/service.component'
|
||||||
import { TimeService } from 'src/app/services/time.service'
|
import { ServicesService } from 'src/app/routes/portal/routes/dashboard/services.service'
|
||||||
import { MetricsComponent } from './metrics.component'
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
import { ServicesComponent } from './services.component'
|
|
||||||
import { UtilitiesComponent } from './utilities.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
<time>{{ date() | date: 'medium' }}</time>
|
<h2>
|
||||||
<app-metrics [metrics]="metrics$ | async">
|
<tui-icon icon="tuiIconGrid" />
|
||||||
<h2>
|
Services
|
||||||
<tui-icon icon="tuiIconActivity" />
|
</h2>
|
||||||
Metrics
|
<div class="g-plaque"></div>
|
||||||
</h2>
|
<table>
|
||||||
<div class="g-plaque"></div>
|
<thead>
|
||||||
</app-metrics>
|
<tr>
|
||||||
<app-utilities>
|
<th [style.width.rem]="3"></th>
|
||||||
<h2>
|
<th>Name</th>
|
||||||
<tui-icon icon="tuiIconSettings" />
|
<th>Version</th>
|
||||||
Utilities
|
<th [style.width.rem]="13">Status</th>
|
||||||
</h2>
|
<th [style.width.rem]="8" [style.text-indent.rem]="1">Controls</th>
|
||||||
<div class="g-plaque"></div>
|
</tr>
|
||||||
</app-utilities>
|
</thead>
|
||||||
<app-services>
|
<tbody>
|
||||||
<h2>
|
@for (pkg of services(); track $index) {
|
||||||
<tui-icon icon="tuiIconGrid" />
|
<tr
|
||||||
Services
|
appService
|
||||||
</h2>
|
[pkg]="pkg"
|
||||||
<div class="g-plaque"></div>
|
[depErrors]="errors()?.[(pkg | toManifest).id]"
|
||||||
</app-services>
|
></tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
{{ services() ? 'No services installed' : 'Loading...' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
height: calc(100vh - 6rem);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
display: grid;
|
margin: 0 auto;
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
margin: 2rem auto 0;
|
|
||||||
border: 0.375rem solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
app-metrics,
|
|
||||||
app-utilities,
|
|
||||||
app-services {
|
|
||||||
position: relative;
|
|
||||||
clip-path: var(--clip-path);
|
clip-path: var(--clip-path);
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: blur(1rem);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
overflow: hidden;
|
||||||
|
|
||||||
time {
|
--clip-path: polygon(
|
||||||
position: absolute;
|
0 2rem,
|
||||||
left: 22%;
|
1.25rem 0,
|
||||||
font-weight: bold;
|
8.75rem 0,
|
||||||
line-height: 1.75rem;
|
calc(10rem + 0.1em) calc(2rem - 0.1em),
|
||||||
text-shadow: 0 0 0.25rem #000;
|
calc(100% - 1.25rem) 2rem,
|
||||||
|
100% 4rem,
|
||||||
|
100% calc(100% - 2rem),
|
||||||
|
calc(100% - 1.25rem) 100%,
|
||||||
|
1.25rem 100%,
|
||||||
|
0 calc(100% - 2rem)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -81,59 +80,49 @@ import { UtilitiesComponent } from './utilities.component'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
height: calc(100vh - 7.375rem);
|
height: calc(100vh - 7.375rem);
|
||||||
display: block;
|
margin: 0 0.375rem;
|
||||||
margin: 0;
|
--clip-path: none !important;
|
||||||
border-top: 0;
|
|
||||||
border-bottom: 0;
|
|
||||||
|
|
||||||
app-metrics,
|
table {
|
||||||
app-utilities,
|
width: 100%;
|
||||||
app-services {
|
margin: 0;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
time,
|
thead,
|
||||||
h2 {
|
h2 {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile [data-dashboard='metrics']) {
|
|
||||||
app-metrics {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile [data-dashboard='utilities']) {
|
|
||||||
app-utilities {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile main:not([data-dashboard])) {
|
|
||||||
app-services {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
imports: [
|
imports: [TuiIconModule, ServiceComponent, ToManifestPipe],
|
||||||
ServicesComponent,
|
|
||||||
MetricsComponent,
|
|
||||||
UtilitiesComponent,
|
|
||||||
TuiIconModule,
|
|
||||||
DatePipe,
|
|
||||||
RouterLink,
|
|
||||||
AsyncPipe,
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class DashboardComponent {
|
export class DashboardComponent {
|
||||||
readonly metrics$ = inject(MetricsService)
|
readonly services = toSignal(inject(ServicesService))
|
||||||
readonly date = toSignal(
|
readonly errors = toSignal(inject(DepErrorService).depErrors$)
|
||||||
inject(TimeService).now$.pipe(map(({ now }) => new Date(now))),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
|
||||||
import { TuiProgressModule } from '@taiga-ui/kit'
|
|
||||||
import { CpuComponent } from 'src/app/routes/portal/routes/dashboard/cpu.component'
|
|
||||||
import { MetricComponent } from 'src/app/routes/portal/routes/dashboard/metric.component'
|
|
||||||
import { TemperatureComponent } from 'src/app/routes/portal/routes/dashboard/temperature.component'
|
|
||||||
import { Metrics } from 'src/app/services/api/api.types'
|
|
||||||
import { TimeService } from 'src/app/services/time.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: 'app-metrics',
|
|
||||||
template: `
|
|
||||||
<ng-content />
|
|
||||||
<section>
|
|
||||||
<app-metric class="wide" label="Storage" [style.max-height.%]="85">
|
|
||||||
<progress
|
|
||||||
tuiProgressBar
|
|
||||||
[max]="100"
|
|
||||||
[attr.value]="metrics?.disk?.percentageUsed?.value"
|
|
||||||
></progress>
|
|
||||||
<footer>
|
|
||||||
<div>
|
|
||||||
<span [attr.data-unit]="metrics?.disk?.used?.unit">
|
|
||||||
{{ getValue(metrics?.disk?.used?.value) }}
|
|
||||||
</span>
|
|
||||||
Used
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<span [attr.data-unit]="metrics?.disk?.available?.unit">
|
|
||||||
{{ getValue(metrics?.disk?.available?.value) }}
|
|
||||||
</span>
|
|
||||||
Available
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</app-metric>
|
|
||||||
<app-metric label="CPU">
|
|
||||||
<app-cpu [value]="cpu" />
|
|
||||||
</app-metric>
|
|
||||||
<app-metric label="Memory">
|
|
||||||
<label tuiProgressLabel>
|
|
||||||
<tui-progress-circle size="l" [max]="100" [value]="memory" />
|
|
||||||
{{ metrics?.memory?.percentageUsed?.value || ' - ' }}%
|
|
||||||
</label>
|
|
||||||
<footer>
|
|
||||||
<div>
|
|
||||||
<span [attr.data-unit]="metrics?.memory?.used?.unit">
|
|
||||||
{{ getValue(metrics?.memory?.used?.value) }}
|
|
||||||
</span>
|
|
||||||
Used
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<span [attr.data-unit]="metrics?.memory?.available?.unit">
|
|
||||||
{{ getValue(metrics?.memory?.available?.value) }}
|
|
||||||
</span>
|
|
||||||
Available
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</app-metric>
|
|
||||||
<aside>
|
|
||||||
<app-metric label="Uptime" [style.flex]="'unset'">
|
|
||||||
<label>
|
|
||||||
{{ uptime() }}
|
|
||||||
<div>Days : Hrs : Mins : Secs</div>
|
|
||||||
</label>
|
|
||||||
</app-metric>
|
|
||||||
<app-metric label="Temperature">
|
|
||||||
<app-temperature [value]="temperature" />
|
|
||||||
</app-metric>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
`,
|
|
||||||
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;
|
|
||||||
padding: 1rem 1.5rem 0.5rem;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: -1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: var(--tui-clear);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.375rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress {
|
|
||||||
height: 1.5rem;
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
border-radius: 0;
|
|
||||||
clip-path: none;
|
|
||||||
mask: linear-gradient(to right, #000 80%, transparent 80%);
|
|
||||||
mask-size: 5% 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
height: 100%;
|
|
||||||
width: 1px;
|
|
||||||
margin: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--tui-text-02);
|
|
||||||
font-size: 0.5rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--tui-text-01);
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: attr(data-unit);
|
|
||||||
font-size: 0.5rem;
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--tui-text-02);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
--clip-path: none !important;
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
section {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1rem -1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
order: -1;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
app-metric {
|
|
||||||
min-width: calc(50% - 0.5rem);
|
|
||||||
|
|
||||||
&.wide {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
TuiProgressModule,
|
|
||||||
MetricComponent,
|
|
||||||
TemperatureComponent,
|
|
||||||
CpuComponent,
|
|
||||||
AsyncPipe,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class MetricsComponent {
|
|
||||||
@Input({ required: true })
|
|
||||||
metrics: Metrics | null = null
|
|
||||||
|
|
||||||
readonly uptime = toSignal(inject(TimeService).uptime$)
|
|
||||||
|
|
||||||
get cpu(): number {
|
|
||||||
return Number(this.metrics?.cpu.percentageUsed.value || 0) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
get temperature(): number {
|
|
||||||
return Number(this.metrics?.general.temperature?.value || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
get memory(): number {
|
|
||||||
return Number(this.metrics?.memory?.percentageUsed?.value) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue(value?: string | null): number | string | undefined {
|
|
||||||
return value == null ? '-' : Number.parseInt(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { TuiScrollbarModule } from '@taiga-ui/core'
|
|
||||||
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
|
||||||
import { ServiceComponent } from 'src/app/routes/portal/routes/dashboard/service.component'
|
|
||||||
import { ServicesService } from 'src/app/routes/portal/routes/dashboard/services.service'
|
|
||||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: 'app-services',
|
|
||||||
template: `
|
|
||||||
<ng-content />
|
|
||||||
<tui-scrollbar [style.max-height.%]="100">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th [style.width.rem]="3"></th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Version</th>
|
|
||||||
<th [style.width.rem]="13">Status</th>
|
|
||||||
<th [style.width.rem]="8" [style.text-indent.rem]="1">Controls</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@if (errors$ | async; as errors) {
|
|
||||||
@for (pkg of services$ | async; track $index) {
|
|
||||||
<tr
|
|
||||||
appService
|
|
||||||
[pkg]="pkg"
|
|
||||||
[depErrors]="errors[(pkg | toManifest).id]"
|
|
||||||
></tr>
|
|
||||||
} @empty {
|
|
||||||
<tr>
|
|
||||||
<td colspan="5">No services installed</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</tui-scrollbar>
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
:host {
|
|
||||||
grid-column: 1/4;
|
|
||||||
margin-top: -2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
--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;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
--clip-path: none !important;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [ServiceComponent, AsyncPipe, ToManifestPipe, TuiScrollbarModule],
|
|
||||||
})
|
|
||||||
export class ServicesComponent {
|
|
||||||
readonly services$ = inject(ServicesService)
|
|
||||||
readonly errors$ = inject(DepErrorService).depErrors$
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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/utils/system-utilities'
|
|
||||||
import { BadgeService } from 'src/app/services/badge.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: 'app-utilities',
|
|
||||||
template: `
|
|
||||||
<ng-content />
|
|
||||||
<div class="links">
|
|
||||||
@for (item of items; track $index) {
|
|
||||||
<a class="link" [routerLink]="item.routerLink">
|
|
||||||
<tui-icon [icon]="item.icon" />
|
|
||||||
{{ item.title }}
|
|
||||||
@if (item.notification$ | async; as value) {
|
|
||||||
<tui-badge-notification>{{ value }}</tui-badge-notification>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
height: fit-content;
|
|
||||||
--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 {
|
|
||||||
width: 100%;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
--clip-path: none !important;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.links {
|
|
||||||
grid-template: 1fr 1fr/1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
font-size: 1rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiIconModule, RouterLink, TuiBadgeNotificationModule, AsyncPipe],
|
|
||||||
})
|
|
||||||
export class UtilitiesComponent {
|
|
||||||
private readonly badge = inject(BadgeService)
|
|
||||||
readonly items = Object.keys(SYSTEM_UTILITIES)
|
|
||||||
.filter(key => !SKIPPED.includes(key))
|
|
||||||
.map(key => ({
|
|
||||||
...SYSTEM_UTILITIES[key],
|
|
||||||
routerLink: key,
|
|
||||||
notification$: this.badge.getCount(key),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const SKIPPED = ['/portal/system/notifications', '/portal/system/metrics']
|
|
||||||
@@ -11,7 +11,7 @@ interface ActionItem {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: '[action]',
|
selector: '[action]',
|
||||||
template: `
|
template: `
|
||||||
<tui-icon [icon]="action.icon"></tui-icon>
|
<tui-icon [icon]="action.icon" />
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ action.name }}</strong>
|
<strong>{{ action.name }}</strong>
|
||||||
<div>{{ action.description }}</div>
|
<div>{{ action.description }}</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 0.5rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
Input,
|
Input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||||
|
import { TuiIconModule } from '@taiga-ui/experimental'
|
||||||
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
||||||
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||||
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -15,18 +17,20 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
|
|||||||
template: `
|
template: `
|
||||||
@if (installingInfo) {
|
@if (installingInfo) {
|
||||||
<strong>
|
<strong>
|
||||||
|
<tui-loader size="s" [inheritColor]="true" />
|
||||||
Installing
|
Installing
|
||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
{{ installingInfo.progress.overall | installingProgressString }}
|
{{ installingInfo.progress.overall | installingProgressString }}
|
||||||
</strong>
|
</strong>
|
||||||
} @else {
|
} @else {
|
||||||
|
<tui-icon [icon]="icon" [style.margin-bottom.rem]="0.25" />
|
||||||
{{ connected ? rendering.display : 'Unknown' }}
|
{{ connected ? rendering.display : 'Unknown' }}
|
||||||
|
@if (rendering.showDots) {
|
||||||
<span *ngIf="sigtermTimeout && (sigtermTimeout | durationToSeconds) > 30">
|
<span class="loading-dots"></span>
|
||||||
. This may take a while
|
}
|
||||||
</span>
|
@if (sigtermTimeout && (sigtermTimeout | durationToSeconds) > 30) {
|
||||||
|
<div>This may take a while</div>
|
||||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
@@ -36,7 +40,20 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
|
|||||||
font-size: x-large;
|
font-size: x-large;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
tui-loader {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin: 0 0.25rem -0.125rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
@@ -46,6 +63,8 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
InstallingProgressDisplayPipe,
|
InstallingProgressDisplayPipe,
|
||||||
UnitConversionPipesModule,
|
UnitConversionPipesModule,
|
||||||
|
TuiIconModule,
|
||||||
|
TuiLoaderModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceStatusComponent {
|
export class ServiceStatusComponent {
|
||||||
@@ -60,21 +79,38 @@ export class ServiceStatusComponent {
|
|||||||
|
|
||||||
@Input() sigtermTimeout?: string | null = null
|
@Input() sigtermTimeout?: string | null = null
|
||||||
|
|
||||||
@HostBinding('style.color')
|
@HostBinding('class')
|
||||||
get color(): string {
|
get class(): string | null {
|
||||||
if (!this.connected) return 'var(--tui-text-02)'
|
if (!this.connected) return null
|
||||||
|
|
||||||
switch (this.rendering.color) {
|
switch (this.rendering.color) {
|
||||||
case 'danger':
|
case 'danger':
|
||||||
return 'var(--tui-error-fill)'
|
return 'g-error'
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return 'var(--tui-warning-fill)'
|
return 'g-warning'
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'var(--tui-success-fill)'
|
return 'g-success'
|
||||||
case 'primary':
|
case 'primary':
|
||||||
return 'var(--tui-info-fill)'
|
return 'g-info'
|
||||||
default:
|
default:
|
||||||
return 'var(--tui-text-02)'
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon(): string {
|
||||||
|
if (!this.connected) return 'tuiIconCircle'
|
||||||
|
|
||||||
|
switch (this.rendering.color) {
|
||||||
|
case 'danger':
|
||||||
|
return 'tuiIconXCircle'
|
||||||
|
case 'warning':
|
||||||
|
return 'tuiIconAlertCircle'
|
||||||
|
case 'success':
|
||||||
|
return 'tuiIconCheckCircle'
|
||||||
|
case 'primary':
|
||||||
|
return 'tuiIconMinusCircle'
|
||||||
|
default:
|
||||||
|
return 'tuiIconCircle'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ import { DependencyInfo } from '../types/dependency-info'
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 1.5rem 0.5rem;
|
padding: 1rem 1.5rem 0.5rem;
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background: var(--tui-clear);
|
background: var(--tui-clear);
|
||||||
box-shadow: inset 0 7rem 0 -4rem var(--tui-clear);
|
box-shadow: inset 0 7rem 0 -4rem var(--tui-clear);
|
||||||
clip-path: polygon(0 1.5rem, 1.5rem 0, 100% 0, 100% 100%, 0 100%);
|
clip-path: polygon(0 1.5rem, 1.5rem 0, 100% 0, 100% 100%, 0 100%);
|
||||||
|
|||||||
@@ -1,17 +1,193 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { MetricsComponent } from 'src/app/routes/portal/routes/dashboard/metrics.component'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { MetricsService } from 'src/app/services/metrics.service'
|
import { TuiProgressModule } from '@taiga-ui/kit'
|
||||||
|
import { CpuComponent } from 'src/app/routes/portal/routes/system/metrics/cpu.component'
|
||||||
|
import { TemperatureComponent } from 'src/app/routes/portal/routes/system/metrics/temperature.component'
|
||||||
|
import { MetricComponent } from 'src/app/routes/portal/routes/system/metrics/metric.component'
|
||||||
|
import { MetricsService } from 'src/app/routes/portal/routes/system/metrics/metrics.service'
|
||||||
|
import { TimeService } from 'src/app/services/time.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-metrics',
|
||||||
template: `
|
template: `
|
||||||
<app-metrics [metrics]="metrics$ | async" />
|
<section>
|
||||||
|
<app-metric class="wide" label="Storage" [style.max-height.%]="85">
|
||||||
|
<progress
|
||||||
|
tuiProgressBar
|
||||||
|
[max]="100"
|
||||||
|
[attr.value]="metrics()?.disk?.percentageUsed?.value"
|
||||||
|
></progress>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<span [attr.data-unit]="metrics()?.disk?.used?.unit">
|
||||||
|
{{ getValue(metrics()?.disk?.used?.value) }}
|
||||||
|
</span>
|
||||||
|
Used
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<span [attr.data-unit]="metrics()?.disk?.available?.unit">
|
||||||
|
{{ getValue(metrics()?.disk?.available?.value) }}
|
||||||
|
</span>
|
||||||
|
Available
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</app-metric>
|
||||||
|
<app-metric label="CPU">
|
||||||
|
<app-cpu [value]="cpu" />
|
||||||
|
</app-metric>
|
||||||
|
<app-metric label="Memory">
|
||||||
|
<label tuiProgressLabel>
|
||||||
|
<tui-progress-circle size="l" [max]="100" [value]="memory" />
|
||||||
|
{{ metrics()?.memory?.percentageUsed?.value || ' - ' }}%
|
||||||
|
</label>
|
||||||
|
<footer>
|
||||||
|
<div>
|
||||||
|
<span [attr.data-unit]="metrics()?.memory?.used?.unit">
|
||||||
|
{{ getValue(metrics()?.memory?.used?.value) }}
|
||||||
|
</span>
|
||||||
|
Used
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<span [attr.data-unit]="metrics()?.memory?.available?.unit">
|
||||||
|
{{ getValue(metrics()?.memory?.available?.value) }}
|
||||||
|
</span>
|
||||||
|
Available
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</app-metric>
|
||||||
|
<aside>
|
||||||
|
<app-metric label="Uptime" [style.flex]="'unset'">
|
||||||
|
<label>
|
||||||
|
{{ uptime() }}
|
||||||
|
<div>Days : Hrs : Mins : Secs</div>
|
||||||
|
</label>
|
||||||
|
</app-metric>
|
||||||
|
<app-metric label="Temperature">
|
||||||
|
<app-temperature [value]="temperature" />
|
||||||
|
</app-metric>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--tui-clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 0;
|
||||||
|
clip-path: none;
|
||||||
|
mask: linear-gradient(to right, #000 80%, transparent 80%);
|
||||||
|
mask-size: 5% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
margin: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
font-size: 0.5rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--tui-text-01);
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-unit);
|
||||||
|
font-size: 0.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--tui-text-02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
section {
|
||||||
|
min-height: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -2rem -2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
order: -1;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-metric {
|
||||||
|
min-width: calc(50% - 0.5rem);
|
||||||
|
|
||||||
|
&.wide {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
imports: [
|
||||||
imports: [MetricsComponent, AsyncPipe],
|
TuiProgressModule,
|
||||||
|
MetricComponent,
|
||||||
|
TemperatureComponent,
|
||||||
|
CpuComponent,
|
||||||
|
AsyncPipe,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export default class SystemMetricsComponent {
|
export default class SystemMetricsComponent {
|
||||||
readonly metrics$ = inject(MetricsService)
|
readonly metrics = toSignal(inject(MetricsService))
|
||||||
|
readonly uptime = toSignal(inject(TimeService).uptime$)
|
||||||
|
|
||||||
|
get cpu(): number {
|
||||||
|
return Number(this.metrics()?.cpu.percentageUsed.value || 0) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
get temperature(): number {
|
||||||
|
return Number(this.metrics()?.general.temperature?.value || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
get memory(): number {
|
||||||
|
return Number(this.metrics()?.memory?.percentageUsed?.value) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(value?: string | null): number | string | undefined {
|
||||||
|
return value == null ? '-' : Number.parseInt(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,25 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { TuiAlertService, TuiDialogService } from '@taiga-ui/core'
|
import {
|
||||||
|
TuiAlertService,
|
||||||
|
TuiDialogOptions,
|
||||||
|
TuiDialogService,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
import * as argon2 from '@start9labs/argon2'
|
import * as argon2 from '@start9labs/argon2'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TUI_PROMPT, TuiCheckboxLabeledModule } from '@taiga-ui/kit'
|
import {
|
||||||
|
TUI_PROMPT,
|
||||||
|
TuiCheckboxLabeledModule,
|
||||||
|
TuiPromptData,
|
||||||
|
} from '@taiga-ui/kit'
|
||||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter, firstValueFrom, from, take } from 'rxjs'
|
import { filter, firstValueFrom, from, take } from 'rxjs'
|
||||||
import { switchMap } from 'rxjs/operators'
|
import { switchMap } from 'rxjs/operators'
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
||||||
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { ProxyService } from 'src/app/services/proxy.service'
|
import { ProxyService } from 'src/app/services/proxy.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { getServerInfo } from 'src/app/utils/get-server-info'
|
import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||||
@@ -35,6 +44,7 @@ export class SettingsService {
|
|||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly patch = inject(PatchDB<DataModel>)
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly auth = inject(AuthService)
|
||||||
private readonly isTor = inject(ConfigService).isTor()
|
private readonly isTor = inject(ConfigService).isTor()
|
||||||
|
|
||||||
wipe = false
|
wipe = false
|
||||||
@@ -100,6 +110,27 @@ export class SettingsService {
|
|||||||
icon: 'tuiIconMonitor',
|
icon: 'tuiIconMonitor',
|
||||||
routerLink: 'ui',
|
routerLink: 'ui',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Restart',
|
||||||
|
icon: 'tuiIconRefreshCw',
|
||||||
|
description: 'Restart Start OS server',
|
||||||
|
action: () => this.promptPower('Restart'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Shutdown',
|
||||||
|
icon: 'tuiIconPower',
|
||||||
|
description: 'Turn Start OS server off',
|
||||||
|
action: () => this.promptPower('Shutdown'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Logout',
|
||||||
|
icon: 'tuiIconLogOut',
|
||||||
|
description: 'Log off from Start OS',
|
||||||
|
action: () => {
|
||||||
|
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
|
this.auth.setUnverified()
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'Privacy and Security': [
|
'Privacy and Security': [
|
||||||
{
|
{
|
||||||
@@ -146,6 +177,25 @@ export class SettingsService {
|
|||||||
.subscribe(() => this.resetTor(this.wipe))
|
.subscribe(() => this.resetTor(this.wipe))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async promptPower(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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async resetTor(wipeState: boolean) {
|
private async resetTor(wipeState: boolean) {
|
||||||
const loader = this.loader.open('Resetting Tor...').subscribe()
|
const loader = this.loader.open('Resetting Tor...').subscribe()
|
||||||
|
|
||||||
@@ -294,3 +344,29 @@ class WipeComponent {
|
|||||||
readonly isTor = inject(ConfigService).isTor()
|
readonly isTor = inject(ConfigService).isTor()
|
||||||
readonly service = inject(SettingsService)
|
readonly service = inject(SettingsService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOptions(
|
||||||
|
operation: 'Restart' | 'Shutdown',
|
||||||
|
): Partial<TuiDialogOptions<TuiPromptData>> {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { EOSService } from 'src/app/services/eos.service'
|
||||||
|
import { NotificationService } from 'src/app/services/notification.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
@@ -24,6 +25,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class BadgeService {
|
export class BadgeService {
|
||||||
|
private readonly notifications = inject(NotificationService)
|
||||||
private readonly emver = inject(Emver)
|
private readonly emver = inject(Emver)
|
||||||
private readonly patch = inject(PatchDB<DataModel>)
|
private readonly patch = inject(PatchDB<DataModel>)
|
||||||
private readonly settings$ = combineLatest([
|
private readonly settings$ = combineLatest([
|
||||||
@@ -85,6 +87,8 @@ export class BadgeService {
|
|||||||
return this.updates$
|
return this.updates$
|
||||||
case '/portal/system/settings':
|
case '/portal/system/settings':
|
||||||
return this.settings$
|
return this.settings$
|
||||||
|
case '/portal/system/notifications':
|
||||||
|
return this.notifications.unreadCount$
|
||||||
default:
|
default:
|
||||||
return EMPTY
|
return EMPTY
|
||||||
}
|
}
|
||||||
|
|||||||
17
web/projects/ui/src/app/utils/resources.ts
Normal file
17
web/projects/ui/src/app/utils/resources.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const RESOURCES = [
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
|
import { inject } from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
import { BadgeService } from 'src/app/services/badge.service'
|
||||||
|
|
||||||
export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
||||||
{
|
{
|
||||||
|
'/portal/system/notifications': {
|
||||||
|
icon: 'tuiIconBell',
|
||||||
|
title: 'Notifications',
|
||||||
|
},
|
||||||
|
'/portal/system/marketplace': {
|
||||||
|
icon: 'tuiIconShoppingCart',
|
||||||
|
title: 'Marketplace',
|
||||||
|
},
|
||||||
|
'/portal/system/updates': {
|
||||||
|
icon: 'tuiIconGlobe',
|
||||||
|
title: 'Updates',
|
||||||
|
},
|
||||||
'/portal/system/backups': {
|
'/portal/system/backups': {
|
||||||
icon: 'tuiIconSave',
|
icon: 'tuiIconSave',
|
||||||
title: 'Backups',
|
title: 'Backups',
|
||||||
@@ -12,14 +28,6 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
|||||||
icon: 'tuiIconFileText',
|
icon: 'tuiIconFileText',
|
||||||
title: 'Logs',
|
title: 'Logs',
|
||||||
},
|
},
|
||||||
'/portal/system/marketplace': {
|
|
||||||
icon: 'tuiIconShoppingCart',
|
|
||||||
title: 'Marketplace',
|
|
||||||
},
|
|
||||||
'/portal/system/updates': {
|
|
||||||
icon: 'tuiIconGlobe',
|
|
||||||
title: 'Updates',
|
|
||||||
},
|
|
||||||
'/portal/system/sideload': {
|
'/portal/system/sideload': {
|
||||||
icon: 'tuiIconUpload',
|
icon: 'tuiIconUpload',
|
||||||
title: 'Sideload',
|
title: 'Sideload',
|
||||||
@@ -28,8 +36,15 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
|
|||||||
icon: 'tuiIconTool',
|
icon: 'tuiIconTool',
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
},
|
},
|
||||||
'/portal/system/notifications': {
|
|
||||||
icon: 'tuiIconBell',
|
|
||||||
title: 'Notifications',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMenu() {
|
||||||
|
const badge = inject(BadgeService)
|
||||||
|
|
||||||
|
return Object.keys(SYSTEM_UTILITIES).map(key => ({
|
||||||
|
name: SYSTEM_UTILITIES[key].title,
|
||||||
|
icon: SYSTEM_UTILITIES[key].icon,
|
||||||
|
routerLink: key,
|
||||||
|
badge: toSignal(badge.getCount(key), { initialValue: 0 }),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user