feat: implement top navigation (#2805)

* feat: implement top navigation

* chore: fix order
This commit is contained in:
Alex Inkin
2024-12-30 20:07:44 +04:00
committed by GitHub
parent 89ab67e067
commit 57e75e3614
42 changed files with 542 additions and 644 deletions

View File

@@ -1,13 +1,13 @@
<header>
<div class="title">
<store-icon
[class.tui-skeleton]="!registry"
[class.tui-skeleton_rounded]="!registry"
[tuiSkeleton]="!registry"
[style.border-radius.%]="!registry ? 100 : null"
size="60px"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
/>
<h1 [class.tui-skeleton]="!registry">
<h1 [tuiSkeleton]="!registry">
{{ registry?.info?.name || 'Unnamed Registry' }}
</h1>
<!-- change registry modal -->
@@ -31,19 +31,18 @@
>
<store-icon
size="42px"
[style.height]="'42px'"
[style.border-radius]="'100%'"
[style.height.px]="42"
[style.border-radius.%]="100"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
[class.tui-skeleton]="!registry"
[class.tui-skeleton_rounded]="!registry"
[tuiSkeleton]="!registry"
/>
<nav
*tuiSidebar="open; direction: 'right'; autoWidth: true"
class="nav-mobile-sidebar divide-bar"
>
<div class="nav-mobile-sidebar-top">
<h1 [class.tui-skeleton]="!registry">
<h1 [tuiSkeleton]="!registry">
{{ registry?.info?.name }}
</h1>
<button

View File

@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { SharedPipesModule } from '@start9labs/shared'
import { TuiSkeleton } from '@taiga-ui/kit'
import { MenuComponent } from './menu.component'
import { TuiLoader, TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core'
@@ -25,6 +26,7 @@ import { StoreIconComponentModule } from '../store-icon/store-icon.component.mod
TuiLet,
TuiAppearance,
TuiIcon,
TuiSkeleton,
],
declarations: [MenuComponent],
exports: [MenuComponent],

View File

@@ -19,6 +19,7 @@ import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
/>
</ng-template>
`,
styles: ':host { overflow: hidden; }',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StoreIconComponent {

View File

@@ -39,11 +39,11 @@ export class CategoriesComponent {
readonly categoryChange = new EventEmitter<string>()
readonly fallback: Record<string, T.Category> = {
a: { name: 'a', description: { short: 'a', long: 'a' } },
b: { name: 'a', description: { short: 'a', long: 'a' } },
c: { name: 'a', description: { short: 'a', long: 'a' } },
d: { name: 'a', description: { short: 'a', long: 'a' } },
e: { name: 'a', description: { short: 'a', long: 'a' } },
a: { name: '', description: { short: 'a', long: 'a' } },
b: { name: '', description: { short: 'a', long: 'a' } },
c: { name: '', description: { short: 'a', long: 'a' } },
d: { name: '', description: { short: 'a', long: 'a' } },
e: { name: '', description: { short: 'a', long: 'a' } },
}
switchCategory(category: string): void {

View File

@@ -9,7 +9,6 @@ tui-root {
@include transition(filter);
height: 100%;
font-family: 'Open Sans', sans-serif;
--tui-skeleton-radius: 1rem;
&.offline {
filter: saturate(0.75) contrast(0.85);

View File

@@ -1,93 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
HostBinding,
inject,
Input,
} from '@angular/core'
import {
TUI_ANIMATIONS_SPEED,
tuiFadeIn,
TuiIcon,
TuiTitle,
tuiToAnimationOptions,
tuiWidthCollapse,
} from '@taiga-ui/core'
import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
@Component({
standalone: true,
selector: 'a[headerBreadcrumb]',
template: `
@if (item.icon?.startsWith('@tui.')) {
<tui-icon [icon]="item.icon || ''" />
} @else if (item.icon) {
<img [style.width.rem]="2" [src]="item.icon" [alt]="item.title" />
}
<span tuiTitle>
{{ item.title }}
@if (item.subtitle) {
<span tuiSubtitle="">{{ item.subtitle }}</span>
}
</span>
<ng-content />
`,
styles: [
`
:host {
display: flex;
align-items: center;
gap: 1rem;
min-width: 1.25rem;
white-space: nowrap;
text-transform: capitalize;
--clip-path: polygon(
calc(100% - 1.75rem) 0%,
calc(100% - 0.875rem) 50%,
100% 100%,
0% 100%,
0.875rem 50%,
0% 0%
);
&:not(.active) {
--clip-path: polygon(
calc(100% - 1.75rem) 0%,
calc(100% - 0.875rem) 50%,
calc(100% - 1.75rem) 100%,
0% 100%,
0.875rem 50%,
0% 0%
);
}
& > * {
font-weight: bold;
gap: 0;
border-radius: 100%;
}
&::before,
&::after {
content: '';
margin: 0.5rem;
}
&::before {
margin: 0.25rem;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, TuiTitle],
animations: [tuiWidthCollapse, tuiFadeIn],
})
export class HeaderBreadcrumbComponent {
@Input({ required: true, alias: 'headerBreadcrumb' })
item!: Breadcrumb
@HostBinding('@tuiFadeIn')
@HostBinding('@tuiWidthCollapse')
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
}

View File

@@ -1,95 +0,0 @@
import { TuiIcon } from '@taiga-ui/core'
import { AsyncPipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, Observable, startWith } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { NetworkService } from 'src/app/services/network.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
standalone: true,
selector: 'header-connection',
template: `
<ng-content />
@if (connection$ | async; as connection) {
<!-- data-connection is used to display color indicator in the header through :has() -->
<tui-icon
[icon]="connection.icon"
[style.color]="connection.color"
[style.font-size.em]="1.5"
[attr.data-connection]="connection.status"
/>
{{ connection.message }}
}
`,
styles: [
`
:host {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 2rem;
}
:host-context(tui-root._mobile) {
display: none;
font-size: 1rem;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, AsyncPipe],
})
export class HeaderConnectionComponent {
readonly connection$: Observable<{
message: string
color: string
icon: string
status: string
}> = combineLatest([
inject(NetworkService),
inject(ConnectionService),
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo')
.pipe(startWith({ restarting: false, shuttingDown: false })),
]).pipe(
map(([network, websocket, status]) => {
if (!network)
return {
message: 'No Internet',
color: 'var(--tui-status-negative)',
icon: '@tui.cloud-off',
status: 'error',
}
if (!websocket)
return {
message: 'Connecting',
color: 'var(--tui-status-warning)',
icon: '@tui.cloud-off',
status: 'warning',
}
if (status.shuttingDown)
return {
message: 'Shutting Down',
color: 'var(--tui-status-neutral)',
icon: '@tui.power',
status: 'neutral',
}
if (status.restarting)
return {
message: 'Restarting',
color: 'var(--tui-status-neutral)',
icon: '@tui.power',
status: 'neutral',
}
return {
message: 'Connected',
color: 'var(--tui-status-positive)',
icon: '@tui.cloud',
status: 'success',
}
}),
)
}

View File

@@ -1,80 +0,0 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { TuiSidebar } from '@taiga-ui/addon-mobile'
import { TuiLet } from '@taiga-ui/cdk'
import {
TUI_ANIMATIONS_SPEED,
TuiButton,
tuiFadeIn,
tuiScaleIn,
tuiToAnimationOptions,
tuiWidthCollapse,
} from '@taiga-ui/core'
import { TuiBadgedContent, TuiBadgeNotification } from '@taiga-ui/kit'
import { SidebarDirective } from 'src/app/components/sidebar-host.component'
import { getMenu } from 'src/app/utils/system-utilities'
import { HeaderMenuComponent } from './menu.component'
@Component({
standalone: true,
selector: 'header-corner',
template: `
<ng-content />
@for (item of utils; track $index) {
@if (item.badge(); as badge) {
<tui-badged-content
[style.--tui-radius.%]="50"
[@tuiFadeIn]="animation"
[@tuiWidthCollapse]="animation"
[@tuiScaleIn]="animation"
>
<tui-badge-notification tuiSlot="top" size="s">
{{ badge }}
</tui-badge-notification>
<a
tuiIconButton
appearance="icon"
size="s"
[iconStart]="item.icon"
[routerLink]="item.routerLink"
[style.color]="'var(--tui-text-primary)'"
>
{{ item.name }}
</a>
</tui-badged-content>
}
}
<header-menu></header-menu>
`,
styles: [
`
:host {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.5rem 0 1.75rem;
--clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 1.75rem 100%);
}
:host-context(tui-root._mobile) tui-badged-content {
display: none;
}
`,
],
animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
HeaderMenuComponent,
SidebarDirective,
TuiBadgeNotification,
TuiBadgedContent,
TuiButton,
TuiLet,
TuiSidebar,
RouterLink,
],
})
export class HeaderCornerComponent {
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
readonly utils = getMenu()
}

View File

@@ -1,47 +1,34 @@
import { AsyncPipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
IsActiveMatchOptions,
RouterLink,
RouterLinkActive,
} from '@angular/router'
import { PatchDB } from 'patch-db-client'
import { HeaderConnectionComponent } from './connection.component'
import { HeaderHomeComponent } from './home.component'
import { HeaderCornerComponent } from './corner.component'
import { HeaderBreadcrumbComponent } from './breadcrumb.component'
import { HeaderSnekDirective } from './snek.directive'
import { HeaderMobileComponent } from './mobile.component'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { HeaderMenuComponent } from './menu.component'
import { HeaderMobileComponent } from './mobile.component'
import { HeaderNavigationComponent } from './navigation.component'
import { HeaderSnekDirective } from './snek.directive'
import { HeaderStatusComponent } from './status.component'
@Component({
selector: 'header[appHeader]',
template: `
<a headerHome routerLink="/portal/dashboard" routerLinkActive="active">
<div class="plaque"></div>
</a>
@for (item of breadcrumbs$ | async; track $index) {
<a
routerLinkActive="active"
[routerLink]="item.routerLink"
[routerLinkActiveOptions]="options"
[headerBreadcrumb]="item"
>
<div class="plaque"></div>
</a>
}
<div [style.flex]="1" [headerMobile]="breadcrumbs$ | async">
<div class="plaque"></div>
<header-navigation />
<div class="item item_center" [headerMobile]="breadcrumbs$ | async">
<img
[appSnek]="(snekScore$ | async) || 0"
[appSnek]="snekScore()"
class="snek"
alt="Play Snake"
src="assets/img/icons/snek.png"
/>
</div>
<header-connection><div class="plaque"></div></header-connection>
<header-corner><div class="plaque"></div></header-corner>
<header-status class="item item_connection" />
<header-menu class="item item_corner" />
`,
styles: [
`
@@ -49,89 +36,62 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
:host {
display: flex;
height: 3.5rem;
padding: var(--bumper);
--clip-path: polygon(
0% 0%,
calc(100% - 1.75rem) 0%,
100% 100%,
1.75rem 100%
);
height: 2.75rem;
border-radius: var(--bumper);
margin: var(--bumper);
overflow: hidden;
> * {
@include transition(all);
.item {
position: relative;
margin-left: -1.25rem;
backdrop-filter: blur(1rem);
clip-path: var(--clip-path);
}
> a:active,
> button:active {
backdrop-filter: blur(2rem) brightness(0.75) saturate(0.75);
}
&:has([data-connection='error']) {
--status: var(--tui-status-negative);
}
&:has([data-connection='warning']) {
--status: var(--tui-status-warning);
}
&:has([data-connection='neutral']) {
--status: var(--tui-status-neutral);
}
&:has([data-connection='success']) {
--status: var(--tui-status-positive);
}
}
header-connection .plaque::before {
box-shadow:
inset 0 1px rgba(255, 255, 255, 0.25),
inset 0 -0.25rem var(--tui-status-positive);
}
:host-context(tui-root._mobile) {
a {
display: none;
}
header-corner .plaque::before {
box-shadow:
inset 0 1px rgb(255 255 255 / 25%),
inset -0.375rem 0 var(--status);
}
}
.plaque {
@include transition(opacity);
position: absolute;
inset: 0;
z-index: -1;
filter: url(#round-corners);
opacity: 0.5;
.active & {
opacity: 0.75;
border-radius: inherit;
isolation: isolate;
&::before {
// TODO: Theme
background: #363636;
@include transition(all);
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
backdrop-filter: blur(1rem);
transform: skewX(30deg);
background: rgb(75 75 75 / 65%);
box-shadow: inset 0 1px rgb(255 255 255 / 25%);
z-index: -1;
}
&_center {
flex: 1;
}
&_connection::before {
box-shadow:
inset 0 1px rgba(255, 255, 255, 0.25),
inset 0 -0.75rem 0 -0.5rem var(--status);
}
&_corner {
margin-inline-start: var(--bumper);
&::before {
right: -2rem;
}
}
}
&::before {
@include transition(all);
content: '';
position: absolute;
inset: 0;
clip-path: var(--clip-path);
// TODO: Theme
background: #5f5f5f;
box-shadow: inset 0 1px rgb(255 255 255 / 25%);
&:has([data-status='error']) {
--status: var(--tui-status-negative);
}
&:has([data-status='warning']) {
--status: var(--tui-status-warning);
}
&:has([data-status='neutral']) {
--status: var(--tui-status-neutral);
}
&:has([data-status='success']) {
--status: var(--tui-status-positive);
}
}
@@ -147,6 +107,12 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
opacity: 1;
}
}
:host-context(tui-root._mobile) {
.item_center::before {
left: -2rem;
}
}
`,
],
standalone: true,
@@ -155,22 +121,24 @@ import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
RouterLink,
RouterLinkActive,
AsyncPipe,
HeaderConnectionComponent,
HeaderHomeComponent,
HeaderCornerComponent,
HeaderStatusComponent,
HeaderNavigationComponent,
HeaderSnekDirective,
HeaderBreadcrumbComponent,
HeaderMobileComponent,
HeaderMenuComponent,
],
})
export class HeaderComponent {
readonly options = OPTIONS
readonly breadcrumbs$ = inject(BreadcrumbsService)
readonly snekScore$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'gaming',
'snake',
'highScore',
readonly snekScore = toSignal(
inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'gaming',
'snake',
'highScore',
),
{ initialValue: 0 },
)
}

View File

@@ -1,46 +0,0 @@
import { TuiIcon } from '@taiga-ui/core'
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
standalone: true,
selector: 'a[headerHome]',
template: `
<ng-content />
<tui-icon icon="/assets/img/icons/home.svg" [style.font-size.rem]="2" />
`,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 2.5rem 0 1rem;
margin: 0 !important;
--clip-path: polygon(
calc(100% - 1.75rem) 0%,
calc(100% - 0.875rem) 50%,
calc(100% - 1.75rem) 100%,
0% 100%,
0% 0%
);
&.active {
--clip-path: polygon(
calc(100% - 1.75rem) 0%,
calc(100% - 0.875rem) 50%,
100% 100%,
0% 100%,
0% 0%
);
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon],
})
export class HeaderHomeComponent {}

View File

@@ -7,9 +7,10 @@ import {
TuiDropdown,
TuiIcon,
} from '@taiga-ui/core'
import { TuiBadgeNotification, TuiDataListDropdownManager } from '@taiga-ui/kit'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { AuthService } from 'src/app/services/auth.service'
import { RESOURCES } from 'src/app/utils/resources'
import { getMenu } from 'src/app/utils/system-utilities'
import { STATUS } from 'src/app/services/status.service'
import { ABOUT } from './about.component'
@Component({
@@ -22,65 +23,43 @@ import { ABOUT } from './about.component'
[(tuiDropdownOpen)]="open"
[tuiDropdownMaxHeight]="9999"
>
<img [style.max-width.%]="50" src="assets/img/icon.png" alt="StartOS" />
<img [style.max-width.%]="60" src="assets/img/icon.png" alt="StartOS" />
</button>
<ng-template #content>
<tui-data-list tuiDataListDropdownManager [style.width.rem]="13">
@for (link of utils; track $index) {
@if (status().status !== 'success') {
<div class="status">
<tui-icon [icon]="status().icon" />
{{ status().message }}
</div>
}
<tui-data-list [style.width.rem]="13">
<button tuiOption iconStart="@tui.info" (click)="about()">
About this server
</button>
<hr />
@for (link of links; track $index) {
<a
tuiOption
class="item"
target="_blank"
rel="noreferrer"
[iconStart]="link.icon"
[routerLink]="link.routerLink"
(click)="open = false"
[href]="link.href"
>
{{ link.name }}
@if (link.badge(); as badge) {
<tui-badge-notification>{{ badge }}</tui-badge-notification>
}
</a>
@if (!$index || $index === 3 || $index === 5) {
<hr />
}
}
<hr />
<button
<a
tuiOption
class="item"
tuiDropdownSided
iconStart="@tui.circle-help"
iconEnd="@tui.chevron-right"
[tuiDropdown]="dropdown"
[tuiDropdownOffset]="12"
[tuiDropdownManual]="false"
iconStart="@tui.wrench"
routerLink="/portal/system/settings"
(click)="open = false"
>
Resources
<ng-template #dropdown>
<tui-data-list>
<button
tuiOption
iconStart="@tui.info"
class="item"
(click)="about()"
>
About this server
</button>
<hr />
@for (link of links; track $index) {
<a
tuiOption
class="item"
target="_blank"
rel="noreferrer"
iconEnd="@tui.external-link"
[iconStart]="link.icon"
[href]="link.href"
>
{{ link.name }}
</a>
}
</tui-data-list>
</ng-template>
System Settings
</a>
<hr />
<button tuiOption iconStart="@tui.log-out" (click)="logout()">
Logout
</button>
</tui-data-list>
</ng-template>
@@ -88,42 +67,54 @@ import { ABOUT } from './about.component'
styles: [
`
:host {
margin: 0 -0.5rem;
padding-inline-start: 0.5rem;
&._open::before {
filter: brightness(1.2);
}
}
[tuiIconButton] {
height: calc(var(--tui-height-m) + 0.25rem);
width: calc(var(--tui-height-m) + 0.625rem);
.status {
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem 1rem 0.5rem;
opacity: 0.5;
}
.item {
[tuiOption] {
justify-content: flex-start;
gap: 0.5rem;
}
:host-context(tui-root._mobile) {
[tuiIconButton] {
box-shadow: inset -1.25rem 0 0 -1rem var(--status);
}
}
`,
],
host: { '[class._open]': 'open' },
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiDropdown,
TuiDataList,
TuiButton,
TuiIcon,
RouterLink,
TuiBadgeNotification,
TuiDropdown,
TuiDataListDropdownManager,
],
imports: [TuiDropdown, TuiDataList, TuiButton, TuiIcon, RouterLink],
})
export class HeaderMenuComponent {
private readonly api = inject(ApiService)
private readonly auth = inject(AuthService)
private readonly dialogs = inject(TuiDialogService)
open = false
readonly utils = getMenu()
readonly links = RESOURCES
readonly status = inject(STATUS)
about() {
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
}
logout() {
this.api.logout({}).catch(e => console.error('Failed to log out', e))
this.auth.setUnverified()
}
}

View File

@@ -14,11 +14,7 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
selector: '[headerMobile]',
template: `
@if (headerMobile && headerMobile.length > 1) {
<a
[routerLink]="back"
[style.padding.rem]="0.75"
[queryParams]="queryParams"
>
<a [routerLink]="back" [style.padding.rem]="0.75">
<tui-icon icon="@tui.arrow-left" />
</a>
}
@@ -39,20 +35,6 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
}
}
:host-context(tui-root._mobile) {
margin: 0;
--clip-path: polygon(
0% 0%,
calc(100% - 1.75rem) 0%,
100% 100%,
0% 100%
);
> * {
display: block;
}
}
.title {
@include text-overflow();
max-width: calc(100% - 5rem);
@@ -62,6 +44,12 @@ import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
margin-inline-start: 1rem;
}
}
:host-context(tui-root._mobile) {
> * {
display: block;
}
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -82,11 +70,7 @@ export class HeaderMobileComponent {
get back() {
return (
this.headerMobile?.[this.headerMobile?.length - 2]?.routerLink ||
'/portal/dashboard'
'/portal/services'
)
}
get queryParams() {
return this.back === '/portal/dashboard' ? { tab: 'utilities' } : null
}
}

View File

@@ -0,0 +1,175 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink, RouterLinkActive } from '@angular/router'
import {
TUI_ANIMATIONS_SPEED,
TuiButton,
tuiFadeIn,
TuiIcon,
tuiScaleIn,
tuiToAnimationOptions,
tuiWidthCollapse,
} from '@taiga-ui/core'
import { TuiBadgedContent, TuiBadgeNotification } from '@taiga-ui/kit'
import { getMenu } from 'src/app/utils/system-utilities'
@Component({
standalone: true,
selector: 'header-navigation',
template: `
@for (item of utils; track $index) {
<a
class="link"
routerLinkActive="link_active"
[routerLink]="item.routerLink"
>
<tui-badged-content
[style.--tui-radius.%]="50"
[@tuiFadeIn]="animation"
[@tuiWidthCollapse]="animation"
[@tuiScaleIn]="animation"
>
@if (item.badge(); as badge) {
<tui-badge-notification tuiSlot="top" size="s">
{{ badge }}
</tui-badge-notification>
}
<tui-icon [icon]="item.icon" />
</tui-badged-content>
<span>{{ item.name }}</span>
</a>
}
`,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
display: flex;
backdrop-filter: blur(1rem);
border-radius: inherit;
padding-inline-end: 0.75rem;
margin-inline-end: -0.4375rem;
isolation: isolate;
}
.link {
@include transition(all);
position: relative;
display: grid;
grid-template-columns: 1.5rem 0fr;
align-items: center;
padding: 0 0 0 1rem;
margin: 0;
border-radius: inherit;
color: var(--tui-text-secondary);
&:not(.link_active):hover tui-icon {
transform: scale(1.1);
}
&:not(.link_active):active tui-icon {
transform: scale(0.8);
}
&::before {
@include transition(all);
content: '';
position: absolute;
inset: 0;
transform: skewX(30deg);
background: rgb(75 75 75 / 65%);
box-shadow: inset 0 1px rgb(255 255 255 / 25%);
z-index: -1;
}
span {
@include transition(opacity);
position: relative;
overflow: hidden;
text-indent: 0.5rem;
opacity: 0;
}
&:hover,
&_active {
color: var(--tui-text-primary);
tui-icon {
color: var(--tui-text-primary);
}
}
&_active {
grid-template-columns: 1.5rem 1fr;
padding: 0 1rem;
margin: 0 var(--bumper);
+ .link::before {
border-top-left-radius: var(--bumper);
border-bottom-left-radius: var(--bumper);
}
&::before {
border-radius: var(--bumper);
filter: brightness(0.65);
}
span {
opacity: 1;
}
}
&:has(+ .link_active)::before {
border-top-right-radius: var(--bumper);
border-bottom-right-radius: var(--bumper);
}
&:has(~ .link_active) {
padding: 0 1rem 0 0;
}
&:first-child {
padding-inline-start: 1rem !important;
margin-inline-start: 0;
&::before {
left: -2rem;
}
}
&:last-child {
padding-inline-end: 1rem !important;
margin-inline-end: 0;
&::before {
border-top-right-radius: inherit;
border-bottom-right-radius: inherit;
}
}
}
tui-icon {
@include transition(transform);
color: var(--tui-text-secondary);
}
:host-context(tui-root._mobile) {
display: none;
}
`,
],
animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiBadgeNotification,
TuiBadgedContent,
TuiButton,
RouterLink,
TuiIcon,
RouterLinkActive,
],
})
export class HeaderNavigationComponent {
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
readonly utils = getMenu()
}

View File

@@ -0,0 +1,55 @@
import { AsyncPipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { TuiIcon } from '@taiga-ui/core'
import { STATUS } from 'src/app/services/status.service'
@Component({
standalone: true,
selector: 'header-status',
template: `
<span>
<!-- data-status is used to display color indicator in the header through :has() -->
<tui-icon
[icon]="status().icon"
[style.color]="status().color"
[style.font-size.em]="1.5"
[attr.data-status]="status().status"
/>
</span>
<span>{{ status().message }}</span>
`,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
:host {
@include transition(all);
display: grid;
grid-template-columns: 1.75rem 1fr;
align-items: center;
padding: 0 1rem;
margin-inline-start: var(--bumper);
&._connected {
grid-template-columns: 0fr 0fr;
padding: 0;
margin: 0;
}
> * {
overflow: hidden;
}
}
:host-context(tui-root._mobile) {
display: none;
}
`,
],
host: { '[class._connected]': 'status().status === "success"' },
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, AsyncPipe],
})
export class HeaderStatusComponent {
readonly status = inject(STATUS)
}

View File

@@ -15,7 +15,11 @@ import { BadgeService } from 'src/app/services/badge.service'
import { RESOURCES } from 'src/app/utils/resources'
import { getMenu } from 'src/app/utils/system-utilities'
const FILTER = ['/portal/system/settings', '/portal/system/marketplace']
const FILTER = [
'/portal/services',
'/portal/system/settings',
'/portal/system/marketplace',
]
@Component({
standalone: true,
@@ -25,7 +29,7 @@ const FILTER = ['/portal/system/settings', '/portal/system/marketplace']
<a
tuiTabBarItem
icon="@tui.layout-grid"
routerLink="/portal/dashboard"
routerLink="/portal/services"
routerLinkActive
(isActiveChange)="update()"
>

View File

@@ -55,7 +55,7 @@ export class PortalComponent {
takeUntilDestroyed(),
)
.subscribe(e => {
this.breadcrumbs.update(e.url.replace('/portal/service/', ''))
this.breadcrumbs.update(e.url.replace('/portal/services/', ''))
})
readonly name$ = inject<PatchDB<DataModel>>(PatchDB).watch$('ui', 'name')

View File

@@ -7,19 +7,12 @@ const ROUTES: Routes = [
component: PortalComponent,
children: [
{
redirectTo: 'dashboard',
redirectTo: 'services',
pathMatch: 'full',
path: '',
},
{
path: 'dashboard',
loadComponent: () =>
import('./routes/dashboard/dashboard.component').then(
m => m.DashboardComponent,
),
},
{
path: 'service',
path: 'services',
loadChildren: () =>
import('./routes/service/service.module').then(m => m.ServiceModule),
},

View File

@@ -1,26 +1,27 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout'
@Component({
selector: 'service-health-check',
template: `
@if (loading) {
<tui-loader
[class.tui-skeleton]="!connected"
[inheritColor]="!check.result"
/>
<tui-loader [tuiSkeleton]="!connected" [inheritColor]="!check.result" />
} @else {
<tui-icon
[icon]="icon"
[class.tui-skeleton]="!connected"
[tuiSkeleton]="!connected"
[style.color]="color"
/>
}
<span tuiTitle>
<strong [class.tui-skeleton]="!connected">{{ check.name }}</strong>
<span tuiSubtitle [class.tui-skeleton]="!connected" [style.color]="color">
{{ message }}
<strong [tuiSkeleton]="!connected && 2">
{{ connected ? check.name : '' }}
</strong>
<span tuiSubtitle [tuiSkeleton]="!connected && 3" [style.color]="color">
{{ connected ? message : '' }}
</span>
</span>
`,
@@ -36,9 +37,10 @@ import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
}
`,
],
hostDirectives: [TuiCell],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiLoader, TuiIcon, TuiTitle],
imports: [TuiLoader, TuiIcon, TuiTitle, TuiSkeleton],
})
export class ServiceHealthCheckComponent {
@Input({ required: true })

View File

@@ -9,12 +9,12 @@ import {
import { TuiLet } from '@taiga-ui/cdk'
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
import { map } from 'rxjs'
import { UILaunchComponent } from 'src/app/routes/portal/routes/dashboard/ui.component'
import { ControlsService } from 'src/app/services/controls.service'
import { DepErrorService } from 'src/app/services/dep-error.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data'
import { UILaunchComponent } from './ui.component'
const RUNNING = ['running', 'starting', 'restarting']

View File

@@ -1,19 +1,14 @@
import { TuiIcon } from '@taiga-ui/core'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { TuiIcon } 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'
import { ServiceComponent } from './service.component'
import { ServicesService } from './services.service'
@Component({
standalone: true,
template: `
<h2>
<tui-icon icon="@tui.layout-grid" />
Services
</h2>
<div class="g-plaque"></div>
<table>
<thead>
<tr>
@@ -46,38 +41,8 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
position: relative;
max-width: 64rem;
margin: 0 auto;
clip-path: var(--clip-path);
backdrop-filter: blur(1rem);
font-size: 1rem;
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)
);
}
h2 {
height: 2rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0 2rem;
font-weight: bold;
font-size: 1rem;
tui-icon {
font-size: 1rem;
}
}
table {
@@ -105,15 +70,13 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
:host-context(tui-root._mobile) {
height: calc(100vh - 7.375rem);
--clip-path: none !important;
table {
width: 100%;
margin: 0;
}
thead,
h2 {
thead {
display: none;
}
}

View File

@@ -8,12 +8,12 @@ import {
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { tuiPure } from '@taiga-ui/cdk'
import { ControlsComponent } from 'src/app/routes/portal/routes/dashboard/controls.component'
import { StatusComponent } from 'src/app/routes/portal/routes/dashboard/status.component'
import { ConnectionService } from 'src/app/services/connection.service'
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { ControlsComponent } from './controls.component'
import { StatusComponent } from './status.component'
@Component({
standalone: true,
@@ -119,7 +119,7 @@ export class ServiceComponent implements OnChanges {
}
get routerLink() {
return `/portal/service/${this.manifest.id}`
return `/portal/services/${this.manifest.id}`
}
ngOnChanges() {

View File

@@ -1,4 +1,3 @@
import { TuiLoader, TuiIcon } from '@taiga-ui/core'
import {
ChangeDetectionStrategy,
Component,
@@ -6,9 +5,10 @@ import {
Input,
} from '@angular/core'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { InstallingProgressDisplayPipe } from '../service/pipes/install-progress.pipe'
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
@Component({
standalone: true,

View File

@@ -28,7 +28,7 @@ export class ServiceOutletComponent {
tap(pkg => {
// if package disappears, navigate to list page
if (!pkg) {
this.router.navigate(['./portal/dashboard'])
this.router.navigate(['./portal/services'])
}
}),
)

View File

@@ -33,7 +33,10 @@ const ROUTES: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: '/portal/dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component').then(
m => m.DashboardComponent,
),
},
],
},

View File

@@ -7,6 +7,7 @@ import {
} from '@angular/core'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core'
import { TuiSkeleton } from '@taiga-ui/kit'
import { UnknownDisk } from 'src/app/services/api/api.types'
@Component({
@@ -54,7 +55,7 @@ import { UnknownDisk } from 'src/app/services/api/api.types'
</tr>
} @else {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -109,7 +110,7 @@ import { UnknownDisk } from 'src/app/services/api/api.types'
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButton, UnitConversionPipesModule],
imports: [TuiButton, UnitConversionPipesModule, TuiSkeleton],
})
export class BackupsPhysicalComponent {
@Input()

View File

@@ -13,7 +13,7 @@ import {
TuiIcon,
TuiButton,
} from '@taiga-ui/core'
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
import { TuiConfirmData, TUI_CONFIRM, TuiSkeleton } from '@taiga-ui/kit'
import { filter, map, Subject, switchMap } from 'rxjs'
import { BackupTarget } from 'src/app/services/api/api.types'
import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
@@ -72,7 +72,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
} @else {
@for (i of ['', '']; track $index) {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -133,7 +133,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButton, GetBackupIconPipe, TuiIcon, KeyValuePipe],
imports: [TuiButton, GetBackupIconPipe, TuiIcon, KeyValuePipe, TuiSkeleton],
})
export class BackupsTargetsComponent {
private readonly dialogs = inject(TuiDialogService)

View File

@@ -2,6 +2,7 @@ import { TuiIcon } from '@taiga-ui/core'
import { DatePipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { TuiSkeleton } from '@taiga-ui/kit'
import { CronJob } from 'cron'
import { PatchDB } from 'patch-db-client'
import { from, map } from 'rxjs'
@@ -48,7 +49,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
} @else {
@for (row of ['', '']; track $index) {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -93,7 +94,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [GetBackupIconPipe, DatePipe, TuiIcon],
imports: [GetBackupIconPipe, DatePipe, TuiIcon, TuiSkeleton],
})
export class BackupsUpcomingComponent {
private readonly api = inject(ApiService)

View File

@@ -1,5 +1,5 @@
import { toSignal } from '@angular/core/rxjs-interop'
import { TuiCheckbox } from '@taiga-ui/kit'
import { TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit'
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
@@ -92,9 +92,7 @@ import { HasErrorPipe } from '../pipes/has-error.pipe'
} @else {
@for (row of ['', '']; track $index) {
<tr>
<td colspan="6">
<div class="tui-skeleton">Loading</div>
</td>
<td colspan="6"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -166,6 +164,7 @@ import { HasErrorPipe } from '../pipes/has-error.pipe'
HasErrorPipe,
GetBackupIconPipe,
TuiCheckbox,
TuiSkeleton,
],
})
export class BackupsHistoryModal {

View File

@@ -8,7 +8,7 @@ import {
TuiButton,
TuiNotification,
} from '@taiga-ui/core'
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
import { TuiConfirmData, TUI_CONFIRM, TuiSkeleton } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { BehaviorSubject, filter, from } from 'rxjs'
import { BackupJob } from 'src/app/services/api/api.types'
@@ -85,9 +85,7 @@ import { EDIT } from './edit.component'
} @else {
@for (i of ['', '']; track $index) {
<tr>
<td colspan="5">
<div class="tui-skeleton">Loading</div>
</td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -145,6 +143,7 @@ import { EDIT } from './edit.component'
TuiIcon,
ToHumanCronPipe,
GetBackupIconPipe,
TuiSkeleton,
],
})
export class BackupsJobsModal implements OnInit {

View File

@@ -68,7 +68,7 @@ export class BackupsRestoreService {
),
)
.subscribe(() => {
this.router.navigate(['/portal/dashboard'])
this.router.navigate(['/portal/services'])
})
}

View File

@@ -147,7 +147,7 @@ export class MarketplaceControlsComponent {
}
async showService() {
this.router.navigate(['/portal/service', this.pkg.id])
this.router.navigate(['/portal/services', this.pkg.id])
}
private async dryInstall(url: string) {

View File

@@ -1,4 +1,4 @@
import { TuiLineClamp, TuiCheckbox } from '@taiga-ui/kit'
import { TuiLineClamp, TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit'
import {
ChangeDetectionStrategy,
Component,
@@ -60,7 +60,7 @@ import { NotificationItemComponent } from './item.component'
} @else {
@for (row of ['', '']; track $index) {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -76,7 +76,13 @@ import { NotificationItemComponent } from './item.component'
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FormsModule, TuiCheckbox, TuiLineClamp, NotificationItemComponent],
imports: [
FormsModule,
TuiCheckbox,
TuiLineClamp,
NotificationItemComponent,
TuiSkeleton,
],
})
export class NotificationsTableComponent implements OnChanges {
@Input() notifications?: ServerNotifications

View File

@@ -13,7 +13,7 @@ import {
TuiDialogService,
TuiLink,
} from '@taiga-ui/core'
import { TUI_CONFIRM } from '@taiga-ui/kit'
import { TUI_CONFIRM, TuiSkeleton } from '@taiga-ui/kit'
import { filter } from 'rxjs'
import {
FormComponent,
@@ -78,7 +78,7 @@ import { Proxy } from 'src/app/services/patch-db/data-model'
<tr><td colspan="5">No proxies added</td></tr>
} @else {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -122,7 +122,7 @@ import { Proxy } from 'src/app/services/patch-db/data-model'
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiLink, TuiButton],
imports: [CommonModule, TuiLink, TuiButton, TuiSkeleton],
})
export class ProxiesTableComponent {
private readonly dialogs = inject(TuiDialogService)

View File

@@ -1,4 +1,4 @@
import { TuiCheckbox, TuiFade } from '@taiga-ui/kit'
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
@@ -63,7 +63,7 @@ import { PlatformInfoPipe } from './platform-info.pipe'
} @else {
@for (item of single ? [''] : ['', '']; track $index) {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -123,6 +123,7 @@ import { PlatformInfoPipe } from './platform-info.pipe'
TuiIcon,
TuiCheckbox,
TuiFade,
TuiSkeleton,
],
})
export class SSHTableComponent<T extends Session> implements OnChanges {

View File

@@ -8,7 +8,12 @@ import {
} from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogOptions, TuiDialogService, TuiButton } from '@taiga-ui/core'
import { TuiConfirmData, TuiFade, TUI_CONFIRM } from '@taiga-ui/kit'
import {
TuiConfirmData,
TuiFade,
TUI_CONFIRM,
TuiSkeleton,
} from '@taiga-ui/kit'
import { filter, take } from 'rxjs'
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
import { SSHKey } from 'src/app/services/api/api.types'
@@ -51,7 +56,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
} @else {
@for (i of ['', '']; track $index) {
<tr>
<td colspan="5"><div class="tui-skeleton">Loading</div></td>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
@@ -103,7 +108,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiButton, TuiFade],
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton],
})
export class SSHTableComponent {
private readonly loader = inject(LoadingService)

View File

@@ -60,7 +60,7 @@ import { SideloadService } from './sideload.service'
<a
tuiButton
appearance="tertiary-solid"
[routerLink]="'/portal/service/' + package.id"
[routerLink]="'/portal/services/' + package.id"
>
View installed
</a>

View File

@@ -0,0 +1,61 @@
import { inject, InjectionToken } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, startWith } from 'rxjs'
import { ConnectionService } from './connection.service'
import { NetworkService } from './network.service'
import { DataModel } from './patch-db/data-model'
export const STATUS = new InjectionToken('', {
factory: () =>
toSignal(
combineLatest({
network: inject(NetworkService),
websocket: inject(ConnectionService),
status: inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo')
.pipe(startWith({ restarting: false, shuttingDown: false })),
}).pipe(
map(({ network, websocket, status }) => {
if (!network) return OFFLINE
if (!websocket) return CONNECTING
if (status.shuttingDown) return SHUTTING_DOWN
if (status.restarting) return RESTARTING
return CONNECTED
}),
),
{ initialValue: CONNECTED },
),
})
const OFFLINE = {
message: 'No Internet',
color: 'var(--tui-status-negative)',
icon: '@tui.cloud-off',
status: 'error',
}
const CONNECTING = {
message: 'Connecting',
color: 'var(--tui-status-warning)',
icon: '@tui.cloud-off',
status: 'warning',
}
const SHUTTING_DOWN = {
message: 'Shutting Down',
color: 'var(--tui-status-neutral)',
icon: '@tui.power',
status: 'neutral',
}
const RESTARTING = {
message: 'Restarting',
color: 'var(--tui-status-neutral)',
icon: '@tui.power',
status: 'neutral',
}
const CONNECTED = {
message: 'Connected',
color: 'var(--tui-status-positive)',
icon: '@tui.cloud',
status: 'success',
}

View File

@@ -4,38 +4,42 @@ import { BadgeService } from 'src/app/services/badge.service'
export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
{
'/portal/system/notifications': {
icon: '@tui.bell',
title: 'Notifications',
'/portal/services': {
icon: '@tui.layout-grid',
title: 'Services',
},
'/portal/system/marketplace': {
icon: '@tui.shopping-cart',
title: 'Marketplace',
},
// '/portal/system/updates': {
// icon: '@tui.globe',
// title: 'Updates',
// },
'/portal/system/sideload': {
icon: '@tui.upload',
title: 'Sideload',
},
'/portal/system/logs': {
icon: '@tui.file-text',
title: 'Logs',
// '/portal/system/updates': {
// icon: '@tui.globe',
// title: 'Updates',
// },
'/portal/system/backups': {
icon: '@tui.save',
title: 'Backups',
},
'/portal/system/metrics': {
icon: '@tui.activity',
title: 'Metrics',
},
'/portal/system/backups': {
icon: '@tui.save',
title: 'Backups',
'/portal/system/logs': {
icon: '@tui.file-text',
title: 'Logs',
},
'/portal/system/settings': {
icon: '@tui.wrench',
title: 'Settings',
},
'/portal/system/notifications': {
icon: '@tui.bell',
title: 'Notifications',
},
}
export function getMenu() {

View File

@@ -1,3 +1,3 @@
export function toRouterLink(id: string): string {
return id.includes('/') ? id : `/portal/service/${id}`
return id.includes('/') ? id : `/portal/services/${id}`
}

View File

@@ -85,10 +85,6 @@ hr {
background: var(--tui-background-neutral-1);
font-weight: bold;
}
.tui-skeleton {
max-height: 0.5rem;
}
}
tui-root._mobile .g-table {