mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
feat: implement top navigation (#2805)
* feat: implement top navigation * chore: fix order
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MarketplaceConfig, sameUrl } from '@start9labs/shared'
|
||||
/>
|
||||
</ng-template>
|
||||
`,
|
||||
styles: ':host { overflow: hidden; }',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()"
|
||||
>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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,
|
||||
@@ -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'])
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -33,7 +33,10 @@ const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '/portal/dashboard',
|
||||
loadComponent: () =>
|
||||
import('./dashboard/dashboard.component').then(
|
||||
m => m.DashboardComponent,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BackupsRestoreService {
|
||||
),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['/portal/dashboard'])
|
||||
this.router.navigate(['/portal/services'])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
61
web/projects/ui/src/app/services/status.service.ts
Normal file
61
web/projects/ui/src/app/services/status.service.ts
Normal 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',
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function toRouterLink(id: string): string {
|
||||
return id.includes('/') ? id : `/portal/service/${id}`
|
||||
return id.includes('/') ? id : `/portal/services/${id}`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user