mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
feat: more refactors (#2844)
This commit is contained in:
@@ -1,16 +1,16 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
|
||||||
import {
|
import {
|
||||||
IsActiveMatchOptions,
|
ChangeDetectionStrategy,
|
||||||
RouterLink,
|
Component,
|
||||||
RouterLinkActive,
|
inject,
|
||||||
} from '@angular/router'
|
OnInit,
|
||||||
|
viewChild,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleService } from 'src/app/services/title.service'
|
||||||
import { HeaderMenuComponent } from './menu.component'
|
import { HeaderMenuComponent } from './menu.component'
|
||||||
import { HeaderMobileComponent } from './mobile.component'
|
|
||||||
import { HeaderNavigationComponent } from './navigation.component'
|
import { HeaderNavigationComponent } from './navigation.component'
|
||||||
import { HeaderSnekDirective } from './snek.directive'
|
import { HeaderSnekDirective } from './snek.directive'
|
||||||
import { HeaderStatusComponent } from './status.component'
|
import { HeaderStatusComponent } from './status.component'
|
||||||
@@ -19,7 +19,8 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
selector: 'header[appHeader]',
|
selector: 'header[appHeader]',
|
||||||
template: `
|
template: `
|
||||||
<header-navigation />
|
<header-navigation />
|
||||||
<div class="item item_center" [headerMobile]="breadcrumbs$ | async">
|
<div class="item item_center">
|
||||||
|
<div class="mobile"><ng-container #vcr /></div>
|
||||||
<img
|
<img
|
||||||
[appSnek]="snekScore()"
|
[appSnek]="snekScore()"
|
||||||
class="snek"
|
class="snek"
|
||||||
@@ -41,6 +42,10 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
margin: var(--bumper);
|
margin: var(--bumper);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
@@ -65,6 +70,7 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
|
|
||||||
&_center {
|
&_center {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_connection::before {
|
&_connection::before {
|
||||||
@@ -116,25 +122,36 @@ import { HeaderStatusComponent } from './status.component'
|
|||||||
.item_center::before {
|
.item_center::before {
|
||||||
left: -2rem;
|
left: -2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
font: var(--tui-font-text-l);
|
||||||
|
padding: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
::ng-deep > [tuiIconButton] {
|
||||||
|
margin-inline-start: -1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
RouterLink,
|
|
||||||
RouterLinkActive,
|
|
||||||
AsyncPipe,
|
|
||||||
HeaderStatusComponent,
|
HeaderStatusComponent,
|
||||||
HeaderNavigationComponent,
|
HeaderNavigationComponent,
|
||||||
HeaderSnekDirective,
|
HeaderSnekDirective,
|
||||||
HeaderMobileComponent,
|
|
||||||
HeaderMenuComponent,
|
HeaderMenuComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent implements OnInit {
|
||||||
readonly options = OPTIONS
|
private readonly title = inject(TitleService)
|
||||||
readonly breadcrumbs$ = inject(BreadcrumbsService)
|
|
||||||
|
readonly vcr = viewChild.required('vcr', { read: ViewContainerRef })
|
||||||
readonly snekScore = toSignal(
|
readonly snekScore = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB).watch$(
|
inject<PatchDB<DataModel>>(PatchDB).watch$(
|
||||||
'ui',
|
'ui',
|
||||||
@@ -144,11 +161,8 @@ export class HeaderComponent {
|
|||||||
),
|
),
|
||||||
{ initialValue: 0 },
|
{ initialValue: 0 },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const OPTIONS: IsActiveMatchOptions = {
|
ngOnInit() {
|
||||||
paths: 'exact',
|
this.title.register(this.vcr())
|
||||||
queryParams: 'ignored',
|
}
|
||||||
fragment: 'ignored',
|
|
||||||
matrixParams: 'ignored',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import {
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
TuiButton,
|
import { TuiButton, TuiDataList, TuiDropdown, TuiIcon } from '@taiga-ui/core'
|
||||||
TuiDataList,
|
|
||||||
TuiDialogService,
|
|
||||||
TuiDropdown,
|
|
||||||
TuiIcon,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
import { RESOURCES } from 'src/app/utils/resources'
|
|
||||||
import { STATUS } from 'src/app/services/status.service'
|
import { STATUS } from 'src/app/services/status.service'
|
||||||
|
import { RESOURCES } from 'src/app/utils/resources'
|
||||||
import { ABOUT } from './about.component'
|
import { ABOUT } from './about.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -102,7 +97,7 @@ import { ABOUT } from './about.component'
|
|||||||
export class HeaderMenuComponent {
|
export class HeaderMenuComponent {
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly auth = inject(AuthService)
|
private readonly auth = inject(AuthService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||||
|
|
||||||
open = false
|
open = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { TuiIcon } from '@taiga-ui/core'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
|
||||||
import { Breadcrumb } from 'src/app/services/breadcrumbs.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: '[headerMobile]',
|
|
||||||
template: `
|
|
||||||
@if (headerMobile && headerMobile.length > 1) {
|
|
||||||
<a [routerLink]="back" [style.padding.rem]="0.75">
|
|
||||||
<tui-icon icon="@tui.arrow-left" />
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
<span class="title">{{ title }}</span>
|
|
||||||
<ng-content />
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@include text-overflow();
|
|
||||||
max-width: calc(100% - 5rem);
|
|
||||||
text-transform: capitalize;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-inline-start: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
> * {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiIcon, RouterLink],
|
|
||||||
})
|
|
||||||
export class HeaderMobileComponent {
|
|
||||||
private readonly win = inject(WA_WINDOW)
|
|
||||||
|
|
||||||
@Input() headerMobile: readonly Breadcrumb[] | null = []
|
|
||||||
|
|
||||||
get title() {
|
|
||||||
return (
|
|
||||||
this.headerMobile?.[this.headerMobile?.length - 1]?.title ||
|
|
||||||
(this.win.location.search ? 'Utilities' : 'Services')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get back() {
|
|
||||||
return (
|
|
||||||
this.headerMobile?.[this.headerMobile?.length - 2]?.routerLink ||
|
|
||||||
'/portal/services'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router'
|
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||||
import { TuiSheetDialogService, TuiTabBar } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService, TuiTabBar } from '@taiga-ui/addon-mobile'
|
||||||
import { TuiDialogService, TuiIcon } from '@taiga-ui/core'
|
import { TuiIcon } from '@taiga-ui/core'
|
||||||
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
||||||
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
|
||||||
import { BadgeService } from 'src/app/services/badge.service'
|
import { BadgeService } from 'src/app/services/badge.service'
|
||||||
@@ -133,8 +133,7 @@ const FILTER = ['/portal/services', '/portal/settings', '/portal/marketplace']
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TabsComponent {
|
export class TabsComponent {
|
||||||
private readonly sheets = inject(TuiSheetDialogService)
|
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
|
||||||
private readonly links = viewChildren(RouterLinkActive)
|
private readonly links = viewChildren(RouterLinkActive)
|
||||||
|
|
||||||
index = 3
|
index = 3
|
||||||
@@ -154,7 +153,7 @@ export class TabsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
more(content: TemplateRef<any>) {
|
more(content: TemplateRef<any>) {
|
||||||
this.sheets.open(content, { label: 'Start OS' }).subscribe({
|
this.dialogs.open(content, { label: 'Start OS' }).subscribe({
|
||||||
complete: () => this.update(),
|
complete: () => this.update(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { TuiScrollbar } from '@taiga-ui/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
import { RouterOutlet } from '@angular/router'
|
||||||
import { NavigationEnd, Router, RouterOutlet } from '@angular/router'
|
import { TuiScrollbar } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter } from 'rxjs'
|
|
||||||
import { TabsComponent } from 'src/app/routes/portal/components/tabs.component'
|
import { TabsComponent } from 'src/app/routes/portal/components/tabs.component'
|
||||||
import { BreadcrumbsService } from 'src/app/services/breadcrumbs.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { HeaderComponent } from './components/header/header.component'
|
import { HeaderComponent } from './components/header/header.component'
|
||||||
|
|
||||||
@@ -48,15 +45,5 @@ import { HeaderComponent } from './components/header/header.component'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PortalComponent {
|
export class PortalComponent {
|
||||||
private readonly breadcrumbs = inject(BreadcrumbsService)
|
|
||||||
private readonly _ = inject(Router)
|
|
||||||
.events.pipe(
|
|
||||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
|
||||||
takeUntilDestroyed(),
|
|
||||||
)
|
|
||||||
.subscribe(e => {
|
|
||||||
this.breadcrumbs.update(e.url.replace('/portal/services/', ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
readonly name$ = inject<PatchDB<DataModel>>(PatchDB).watch$('ui', 'name')
|
readonly name$ = inject<PatchDB<DataModel>>(PatchDB).watch$('ui', 'name')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { TuiSelectModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy'
|
|||||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||||
import { RR } from 'src/app/services/api/api.types'
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Logs</ng-container>
|
||||||
<tui-select
|
<tui-select
|
||||||
tuiTextfieldAppearance="secondary"
|
tuiTextfieldAppearance="secondary"
|
||||||
tuiTextfieldSize="m"
|
tuiTextfieldSize="m"
|
||||||
@@ -59,6 +61,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
TuiSelectModule,
|
TuiSelectModule,
|
||||||
TuiTextfieldControllerModule,
|
TuiTextfieldControllerModule,
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SystemLogsComponent {
|
export default class SystemLogsComponent {
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { TuiScrollbar } from '@taiga-ui/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
AbstractCategoryService,
|
AbstractCategoryService,
|
||||||
FilterPackagesPipe,
|
FilterPackagesPipe,
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { tap, withLatestFrom } from 'rxjs'
|
import { TuiScrollbar } from '@taiga-ui/core'
|
||||||
import { MarketplaceNotificationComponent } from './components/notification.component'
|
|
||||||
import { MarketplaceMenuComponent } from './components/menu.component'
|
|
||||||
import { MarketplaceTileComponent } from './components/tile.component'
|
|
||||||
import { MarketplaceControlsComponent } from './components/controls.component'
|
|
||||||
import { MarketplacePreviewComponent } from './modals/preview.component'
|
|
||||||
import { MarketplaceSidebarsComponent } from './components/sidebars.component'
|
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { tap, withLatestFrom } from 'rxjs'
|
||||||
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
import { MarketplaceMenuComponent } from './components/menu.component'
|
||||||
|
import { MarketplaceNotificationComponent } from './components/notification.component'
|
||||||
|
import { MarketplaceSidebarsComponent } from './components/sidebars.component'
|
||||||
|
import { MarketplaceTileComponent } from './components/tile.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Marketplace</ng-container>
|
||||||
<marketplace-menu />
|
<marketplace-menu />
|
||||||
<tui-scrollbar>
|
<tui-scrollbar>
|
||||||
<div class="marketplace-content-wrapper">
|
<div class="marketplace-content-wrapper">
|
||||||
@@ -152,14 +152,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
MarketplaceTileComponent,
|
MarketplaceTileComponent,
|
||||||
MarketplaceMenuComponent,
|
MarketplaceMenuComponent,
|
||||||
MarketplaceNotificationComponent,
|
MarketplaceNotificationComponent,
|
||||||
MarketplaceControlsComponent,
|
|
||||||
MarketplacePreviewComponent,
|
|
||||||
MarketplaceSidebarsComponent,
|
MarketplaceSidebarsComponent,
|
||||||
TuiScrollbar,
|
TuiScrollbar,
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MarketplaceComponent {
|
export default class MarketplaceComponent {
|
||||||
private readonly categoryService = inject(AbstractCategoryService)
|
private readonly categoryService = inject(AbstractCategoryService)
|
||||||
private readonly marketplaceService = inject(MarketplaceService)
|
private readonly marketplaceService = inject(MarketplaceService)
|
||||||
private readonly router = inject(Router)
|
private readonly router = inject(Router)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ const MARKETPLACE_ROUTES: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadComponent: () =>
|
loadComponent: () => import('./marketplace.component'),
|
||||||
import('./marketplace.component').then(m => m.MarketplaceComponent),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { TuiProgress } from '@taiga-ui/kit'
|
import { TuiProgress } from '@taiga-ui/kit'
|
||||||
import { CpuComponent } from 'src/app/routes/portal/routes/metrics/cpu.component'
|
import { CpuComponent } from 'src/app/routes/portal/routes/metrics/cpu.component'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { TemperatureComponent } from 'src/app/routes/portal/routes/metrics/temperature.component'
|
import { TemperatureComponent } from 'src/app/routes/portal/routes/metrics/temperature.component'
|
||||||
import { MetricComponent } from 'src/app/routes/portal/routes/metrics/metric.component'
|
import { MetricComponent } from 'src/app/routes/portal/routes/metrics/metric.component'
|
||||||
import { MetricsService } from 'src/app/routes/portal/routes/metrics/metrics.service'
|
import { MetricsService } from 'src/app/routes/portal/routes/metrics/metrics.service'
|
||||||
@@ -12,6 +13,7 @@ import { TimeService } from 'src/app/services/time.service'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'app-metrics',
|
selector: 'app-metrics',
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Metrics</ng-container>
|
||||||
<section>
|
<section>
|
||||||
<app-metric class="wide" label="Storage" [style.max-height.%]="85">
|
<app-metric class="wide" label="Storage" [style.max-height.%]="85">
|
||||||
<progress
|
<progress
|
||||||
@@ -168,7 +170,7 @@ import { TimeService } from 'src/app/services/time.service'
|
|||||||
MetricComponent,
|
MetricComponent,
|
||||||
TemperatureComponent,
|
TemperatureComponent,
|
||||||
CpuComponent,
|
CpuComponent,
|
||||||
AsyncPipe,
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SystemMetricsComponent {
|
export default class SystemMetricsComponent {
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
|
|||||||
import { RR, ServerNotifications } from 'src/app/services/api/api.types'
|
import { RR, ServerNotifications } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { NotificationService } from 'src/app/services/notification.service'
|
import { NotificationService } from 'src/app/services/notification.service'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { NotificationsTableComponent } from './table.component'
|
import { NotificationsTableComponent } from './table.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Notifications</ng-container>
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
<button
|
<button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
@@ -54,7 +56,13 @@ import { NotificationsTableComponent } from './table.component'
|
|||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiDropdown, TuiButton, TuiDataList, NotificationsTableComponent],
|
imports: [
|
||||||
|
TuiDropdown,
|
||||||
|
TuiButton,
|
||||||
|
TuiDataList,
|
||||||
|
NotificationsTableComponent,
|
||||||
|
TitleDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export default class NotificationsComponent {
|
export default class NotificationsComponent {
|
||||||
readonly service = inject(NotificationService)
|
readonly service = inject(NotificationService)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TuiLineClamp, TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiCheckbox, TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
signal,
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import {
|
import {
|
||||||
ServerNotification,
|
ServerNotification,
|
||||||
ServerNotifications,
|
ServerNotifications,
|
||||||
@@ -76,13 +75,7 @@ import { NotificationItemComponent } from './item.component'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [FormsModule, TuiCheckbox, NotificationItemComponent, TuiSkeleton],
|
||||||
FormsModule,
|
|
||||||
TuiCheckbox,
|
|
||||||
TuiLineClamp,
|
|
||||||
NotificationItemComponent,
|
|
||||||
TuiSkeleton,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class NotificationsTableComponent implements OnChanges {
|
export class NotificationsTableComponent implements OnChanges {
|
||||||
@Input() notifications?: ServerNotifications
|
@Input() notifications?: ServerNotifications
|
||||||
|
|||||||
@@ -1,69 +1,99 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostListener,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { TuiTitle } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { TuiFade } from '@taiga-ui/kit'
|
import { TuiAvatar } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
|
||||||
import { ActionService } from 'src/app/services/action.service'
|
import { ActionService } from 'src/app/services/action.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'button[actionRequest]',
|
selector: 'tr[actionRequest]',
|
||||||
template: `
|
template: `
|
||||||
<span tuiTitle>
|
<td>
|
||||||
<strong tuiFade><ng-content /></strong>
|
<tui-avatar size="xs"><img [src]="pkg().icon" alt="" /></tui-avatar>
|
||||||
<span tuiSubtitle>
|
<span>{{ title() }}</span>
|
||||||
{{ actionRequest.reason || 'No reason provided' }}
|
</td>
|
||||||
</span>
|
<td>
|
||||||
</span>
|
@if (actionRequest().severity === 'critical') {
|
||||||
|
<strong [style.color]="'var(--tui-status-warning)'">Required</strong>
|
||||||
|
} @else {
|
||||||
|
<strong [style.color]="'var(--tui-status-info)'">Optional</strong>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
[style.color]="'var(--tui-text-secondary)'"
|
||||||
|
[style.grid-area]="'2 / span 2'"
|
||||||
|
>
|
||||||
|
{{ actionRequest().reason || 'No reason provided' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button tuiButton (click)="handle()">
|
||||||
|
{{ pkg().actions[actionRequest().actionId].name }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
td:first-child {
|
||||||
width: 100%;
|
white-space: nowrap;
|
||||||
margin: 0 -1rem;
|
max-width: 10rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
td:last-child {
|
||||||
white-space: nowrap;
|
text-align: right;
|
||||||
|
grid-area: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr min-content;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiTitle, TuiFade],
|
imports: [TuiButton, TuiAvatar],
|
||||||
hostDirectives: [TuiCell],
|
|
||||||
})
|
})
|
||||||
export class ServiceActionRequestComponent {
|
export class ServiceActionRequestComponent {
|
||||||
private readonly actionService = inject(ActionService)
|
private readonly actionService = inject(ActionService)
|
||||||
|
|
||||||
@Input({ required: true })
|
readonly actionRequest = input.required<T.ActionRequest>()
|
||||||
actionRequest!: T.ActionRequest
|
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||||
|
|
||||||
@Input({ required: true })
|
readonly pkg = computed(() => this.services()[this.actionRequest().packageId])
|
||||||
pkg!: PackageDataEntry
|
readonly title = computed(() => getManifest(this.pkg()).title)
|
||||||
|
|
||||||
@HostListener('click')
|
|
||||||
async handleAction() {
|
|
||||||
const { title } = getManifest(this.pkg)
|
|
||||||
const { actionId, packageId } = this.actionRequest
|
|
||||||
|
|
||||||
|
async handle() {
|
||||||
this.actionService.present({
|
this.actionService.present({
|
||||||
pkgInfo: {
|
pkgInfo: {
|
||||||
id: packageId,
|
id: this.actionRequest().packageId,
|
||||||
title,
|
title: this.title(),
|
||||||
mainStatus: this.pkg.status.main,
|
mainStatus: this.pkg().status.main,
|
||||||
icon: this.pkg.icon,
|
icon: this.pkg().icon,
|
||||||
},
|
},
|
||||||
actionInfo: {
|
actionInfo: {
|
||||||
id: actionId,
|
id: this.actionRequest().actionId,
|
||||||
metadata: this.pkg.actions[actionId],
|
metadata: this.pkg().actions[this.actionRequest().actionId],
|
||||||
},
|
},
|
||||||
requestInfo: this.actionRequest,
|
requestInfo: this.actionRequest(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,73 +4,57 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
input,
|
input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
|
||||||
import { ServiceActionRequestComponent } from './action-request.component'
|
import { ServiceActionRequestComponent } from './action-request.component'
|
||||||
|
import { ServicePlaceholderComponent } from './placeholder.component'
|
||||||
type ActionRequest = T.ActionRequest & {
|
|
||||||
actionName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'service-action-requests',
|
selector: 'service-action-requests',
|
||||||
template: `
|
template: `
|
||||||
@for (request of requests().critical; track $index) {
|
<header>Tasks</header>
|
||||||
<button [actionRequest]="request" [pkg]="pkg()">
|
<table tuiTable class="g-table">
|
||||||
{{ request.actionName }}
|
<thead>
|
||||||
<small class="g-warning">Required</small>
|
<tr>
|
||||||
</button>
|
<th tuiTh>Service</th>
|
||||||
}
|
<th tuiTh>Type</th>
|
||||||
@for (request of requests().important; track $index) {
|
<th tuiTh>Description</th>
|
||||||
<button [actionRequest]="request" [pkg]="pkg()">
|
<th tuiTh></th>
|
||||||
{{ request.actionName }}
|
</tr>
|
||||||
<small class="g-info">Requested</small>
|
</thead>
|
||||||
</button>
|
<tbody>
|
||||||
}
|
@for (item of requests(); track $index) {
|
||||||
@if (requests().critical.length + requests().important.length === 0) {
|
<tr [actionRequest]="item.request" [services]="services()"></tr>
|
||||||
<blockquote>No pending tasks</blockquote>
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@if (!requests().length) {
|
||||||
|
<service-placeholder icon="@tui.list-checks">
|
||||||
|
All tasks complete
|
||||||
|
</service-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
small {
|
:host {
|
||||||
margin-inline-start: 0.25rem;
|
grid-column: span 6;
|
||||||
padding-inline-start: 0.5rem;
|
|
||||||
box-shadow: inset 1px 0 var(--tui-border-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
text-align: center;
|
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
color: var(--tui-text-tertiary);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
host: { class: 'g-card' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [ServiceActionRequestComponent],
|
imports: [
|
||||||
|
TuiTable,
|
||||||
|
ServiceActionRequestComponent,
|
||||||
|
ServicePlaceholderComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ServiceActionRequestsComponent {
|
export class ServiceActionRequestsComponent {
|
||||||
readonly pkg = input.required<PackageDataEntry>()
|
readonly pkg = input.required<PackageDataEntry>()
|
||||||
readonly requests = computed(() => {
|
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||||
const { id } = getManifest(this.pkg())
|
|
||||||
const critical: ActionRequest[] = []
|
|
||||||
const important: ActionRequest[] = []
|
|
||||||
|
|
||||||
|
readonly requests = computed(() =>
|
||||||
Object.values(this.pkg().requestedActions)
|
Object.values(this.pkg().requestedActions)
|
||||||
.filter(r => r.active && r.request.packageId === id)
|
.filter(r => r.active)
|
||||||
.forEach(r => {
|
.sort((a, b) => a.request.severity.localeCompare(b.request.severity)),
|
||||||
const action = {
|
)
|
||||||
...r.request,
|
|
||||||
actionName: this.pkg().actions[r.request.actionId].name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r.request.severity === 'critical') {
|
|
||||||
critical.push(action)
|
|
||||||
} else {
|
|
||||||
important.push(action)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { critical, important }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,22 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
inline-size: 20rem;
|
||||||
|
max-inline-size: 100%;
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
inline-size: min-content;
|
||||||
|
|
||||||
|
[tuiButton] {
|
||||||
|
font-size: 0;
|
||||||
|
gap: 0;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
|||||||
import { TuiAvatar } from '@taiga-ui/kit'
|
import { TuiAvatar } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ServiceActionRequestsComponent } from './action-requests.component'
|
import { ServicePlaceholderComponent } from './placeholder.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'service-dependencies',
|
selector: 'service-dependencies',
|
||||||
@@ -24,33 +24,15 @@ import { ServiceActionRequestsComponent } from './action-requests.component'
|
|||||||
</span>
|
</span>
|
||||||
<tui-icon icon="@tui.arrow-right" />
|
<tui-icon icon="@tui.arrow-right" />
|
||||||
</a>
|
</a>
|
||||||
@if (services[d.key]; as service) {
|
|
||||||
<service-action-requests [pkg]="service" />
|
|
||||||
}
|
|
||||||
} @empty {
|
} @empty {
|
||||||
<blockquote>No dependencies</blockquote>
|
<service-placeholder icon="@tui.boxes">
|
||||||
|
No dependencies
|
||||||
|
</service-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
a {
|
:host {
|
||||||
margin: 0 -1rem;
|
grid-column: span 3;
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service-action-requests {
|
|
||||||
display: block;
|
|
||||||
padding: 1rem 0 0 2.375rem;
|
|
||||||
margin: -1rem 0 1rem 1.125rem;
|
|
||||||
box-shadow: inset 0.125rem 0 var(--tui-border-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
text-align: center;
|
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
color: var(--tui-text-tertiary);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
@@ -58,12 +40,12 @@ import { ServiceActionRequestsComponent } from './action-requests.component'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
KeyValuePipe,
|
KeyValuePipe,
|
||||||
|
RouterLink,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
TuiAvatar,
|
TuiAvatar,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
ServiceActionRequestsComponent,
|
|
||||||
RouterLink,
|
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
|
ServicePlaceholderComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceDependenciesComponent {
|
export class ServiceDependenciesComponent {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
grid-column: span 2;
|
grid-column: span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'service-health-check',
|
standalone: true,
|
||||||
|
selector: 'tr[healthCheck]',
|
||||||
template: `
|
template: `
|
||||||
@if (loading) {
|
<td>{{ healthCheck.name }}</td>
|
||||||
<tui-loader [tuiSkeleton]="!connected" [inheritColor]="!check.result" />
|
<td>
|
||||||
} @else {
|
<span>
|
||||||
<tui-icon
|
@if (loading) {
|
||||||
[icon]="icon"
|
<tui-loader size="m" />
|
||||||
[tuiSkeleton]="!connected"
|
} @else {
|
||||||
[style.color]="color"
|
<tui-icon [icon]="icon" [style.color]="color" />
|
||||||
/>
|
}
|
||||||
}
|
{{ message }}
|
||||||
<span tuiTitle>
|
|
||||||
<strong [tuiSkeleton]="!connected && 2">
|
|
||||||
{{ connected ? check.name : '' }}
|
|
||||||
</strong>
|
|
||||||
<span tuiSubtitle [tuiSkeleton]="!connected && 3" [style.color]="color">
|
|
||||||
{{ connected ? message : '' }}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</td>
|
||||||
`,
|
`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:first-letter {
|
span {
|
||||||
text-transform: uppercase;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
tui-loader {
|
:host-context(tui-root._mobile) {
|
||||||
width: 1.5rem;
|
display: flex;
|
||||||
height: 1.5rem;
|
flex-direction: column;
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
color: var(--tui-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
hostDirectives: [TuiCell],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
imports: [TuiLoader, TuiIcon],
|
||||||
imports: [TuiLoader, TuiIcon, TuiTitle, TuiSkeleton],
|
|
||||||
})
|
})
|
||||||
export class ServiceHealthCheckComponent {
|
export class ServiceHealthCheckComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
check!: T.NamedHealthCheckResult
|
healthCheck!: T.NamedHealthCheckResult
|
||||||
|
|
||||||
@Input()
|
|
||||||
connected = false
|
|
||||||
|
|
||||||
get loading(): boolean {
|
get loading(): boolean {
|
||||||
const { result } = this.check
|
const { result } = this.healthCheck
|
||||||
|
|
||||||
return !result || result === 'starting' || result === 'loading'
|
return !result || result === 'starting' || result === 'loading'
|
||||||
}
|
}
|
||||||
|
|
||||||
get icon(): string {
|
get icon(): string {
|
||||||
switch (this.check.result) {
|
switch (this.healthCheck.result) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return '@tui.check'
|
return '@tui.check'
|
||||||
case 'failure':
|
case 'failure':
|
||||||
@@ -67,7 +66,7 @@ export class ServiceHealthCheckComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get color(): string {
|
get color(): string {
|
||||||
switch (this.check.result) {
|
switch (this.healthCheck.result) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'var(--tui-status-positive)'
|
return 'var(--tui-status-positive)'
|
||||||
case 'failure':
|
case 'failure':
|
||||||
@@ -82,21 +81,21 @@ export class ServiceHealthCheckComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get message(): string {
|
get message(): string {
|
||||||
if (!this.check.result) {
|
if (!this.healthCheck.result) {
|
||||||
return 'Awaiting result...'
|
return 'Awaiting result...'
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.check.result) {
|
switch (this.healthCheck.result) {
|
||||||
case 'starting':
|
case 'starting':
|
||||||
return 'Starting...'
|
return 'Starting...'
|
||||||
case 'success':
|
case 'success':
|
||||||
return `Success: ${this.check.message}`
|
return `Success: ${this.healthCheck.message}`
|
||||||
case 'loading':
|
case 'loading':
|
||||||
case 'failure':
|
case 'failure':
|
||||||
return this.check.message
|
return this.healthCheck.message
|
||||||
// disabled
|
// disabled
|
||||||
default:
|
default:
|
||||||
return this.check.result
|
return this.healthCheck.result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { ServiceHealthCheckComponent } from 'src/app/routes/portal/routes/services/components/health-check.component'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ServiceHealthCheckComponent } from './health-check.component'
|
||||||
|
import { ServicePlaceholderComponent } from './placeholder.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'service-health-checks',
|
selector: 'service-health-checks',
|
||||||
template: `
|
template: `
|
||||||
<header>Health Checks</header>
|
<header>Health Checks</header>
|
||||||
@for (check of checks; track $index) {
|
<table tuiTable class="g-table">
|
||||||
<service-health-check
|
<thead>
|
||||||
[check]="check"
|
<tr>
|
||||||
[connected]="!!(connected$ | async)"
|
<th tuiTh>Name</th>
|
||||||
/>
|
<th tuiTh>Status</th>
|
||||||
} @empty {
|
</tr>
|
||||||
<blockquote>No health checks</blockquote>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (check of checks(); track $index) {
|
||||||
|
<tr [healthCheck]="check"></tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@if (!checks().length) {
|
||||||
|
<service-placeholder icon="@tui.heart-pulse">
|
||||||
|
No health checks
|
||||||
|
</service-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
blockquote {
|
:host {
|
||||||
text-align: center;
|
grid-column: span 3;
|
||||||
font: var(--tui-font-text-l);
|
|
||||||
color: var(--tui-text-tertiary);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [AsyncPipe, ServiceHealthCheckComponent],
|
imports: [ServiceHealthCheckComponent, ServicePlaceholderComponent, TuiTable],
|
||||||
})
|
})
|
||||||
export class ServiceHealthChecksComponent {
|
export class ServiceHealthChecksComponent {
|
||||||
@Input({ required: true })
|
readonly checks = input.required<readonly T.NamedHealthCheckResult[]>()
|
||||||
checks: readonly T.NamedHealthCheckResult[] = []
|
|
||||||
|
|
||||||
readonly connected$ = inject(ConnectionService)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ import { MappedInterface } from '../types/mapped-interface'
|
|||||||
<td>
|
<td>
|
||||||
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
||||||
</td>
|
</td>
|
||||||
<td class="g-secondary">{{ info.description }}</td>
|
<td class="g-secondary" [style.grid-area]="'2 / span 4'">
|
||||||
<td class="hosting">
|
{{ info.description }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
@if (info.public) {
|
@if (info.public) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
@@ -49,10 +51,10 @@ import { MappedInterface } from '../types/mapped-interface'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td [style.grid-area]="'span 2'">
|
||||||
@if (info.type === 'ui') {
|
@if (info.type === 'ui') {
|
||||||
<a
|
<a
|
||||||
tuiButton
|
tuiIconButton
|
||||||
appearance="action"
|
appearance="action"
|
||||||
iconStart="@tui.external-link"
|
iconStart="@tui.external-link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -76,20 +78,15 @@ import { MappedInterface } from '../types/mapped-interface'
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hosting {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
display: block;
|
display: grid;
|
||||||
padding: 0.5rem 0;
|
grid-template-columns: repeat(3, min-content) 1fr 2rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
display: inline-block;
|
padding: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.hosting {
|
|
||||||
font-size: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -27,23 +27,21 @@ import { ServiceInterfaceComponent } from './interface.component'
|
|||||||
<th tuiTh></th>
|
<th tuiTh></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@for (info of interfaces(); track $index) {
|
<tbody>
|
||||||
<tr
|
@for (info of interfaces(); track $index) {
|
||||||
serviceInterface
|
<tr
|
||||||
[info]="info"
|
serviceInterface
|
||||||
[pkg]="pkg()"
|
[info]="info"
|
||||||
[disabled]="disabled()"
|
[pkg]="pkg()"
|
||||||
></tr>
|
[disabled]="disabled()"
|
||||||
}
|
></tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
grid-column: span 2;
|
grid-column: span 4;
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
margin: 0 -0.5rem;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
|
import { TuiIcon } from '@taiga-ui/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'service-placeholder',
|
||||||
|
template: '<tui-icon [icon]="icon()" /><ng-content/>',
|
||||||
|
styles: `
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
font: var(--tui-font-text-l);
|
||||||
|
color: var(--tui-text-tertiary);
|
||||||
|
|
||||||
|
tui-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [TuiIcon],
|
||||||
|
})
|
||||||
|
export class ServicePlaceholderComponent {
|
||||||
|
readonly icon = input.required<string>()
|
||||||
|
}
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import {
|
import { TuiLoader } from '@taiga-ui/core'
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
HostBinding,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
|
||||||
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
import {
|
||||||
PrimaryRendering,
|
PrimaryRendering,
|
||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
StatusRendering,
|
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} from 'src/app/services/pkg-status-rendering.service'
|
||||||
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||||
|
|
||||||
@@ -18,20 +11,25 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
|||||||
selector: 'service-status',
|
selector: 'service-status',
|
||||||
template: `
|
template: `
|
||||||
<header>Status</header>
|
<header>Status</header>
|
||||||
<div [class]="class">
|
<div>
|
||||||
@if (installingInfo) {
|
@if (installingInfo) {
|
||||||
<strong>
|
<h3>
|
||||||
<tui-loader size="s" [inheritColor]="true" />
|
<tui-loader size="s" [inheritColor]="true" />
|
||||||
Installing
|
Installing
|
||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
{{ installingInfo.progress.overall | installingProgressString }}
|
{{ installingInfo.progress.overall | installingProgressString }}
|
||||||
</strong>
|
</h3>
|
||||||
} @else {
|
} @else {
|
||||||
<tui-icon [icon]="icon" [style.margin-bottom.rem]="0.25" />
|
<h3 [class]="class">
|
||||||
{{ connected ? rendering.display : 'Unknown' }}
|
{{ text }}
|
||||||
@if (rendering.showDots) {
|
@if (text === 'Action Required') {
|
||||||
<span class="loading-dots"></span>
|
<small>See below</small>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (rendering.showDots) {
|
||||||
|
<span class="loading-dots"></span>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
}
|
}
|
||||||
<ng-content />
|
<ng-content />
|
||||||
</div>
|
</div>
|
||||||
@@ -39,17 +37,29 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
|||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
display: grid;
|
grid-column: span 2;
|
||||||
grid-template-rows: min-content 1fr;
|
|
||||||
align-items: center;
|
|
||||||
font: var(--tui-font-heading-6);
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status {
|
h3 {
|
||||||
display: grid;
|
font: var(--tui-font-heading-4);
|
||||||
grid-template-rows: min-content 1fr 1fr;
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
font: var(--tui-font-text-l);
|
||||||
|
color: var(--tui-text-secondary);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
tui-loader {
|
tui-loader {
|
||||||
@@ -57,12 +67,24 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
margin: 0 0.25rem -0.125rem 0;
|
margin: 0 0.25rem -0.125rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
div {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [InstallingProgressDisplayPipe, TuiIcon, TuiLoader],
|
imports: [InstallingProgressDisplayPipe, TuiLoader],
|
||||||
})
|
})
|
||||||
export class ServiceStatusComponent {
|
export class ServiceStatusComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
@@ -74,6 +96,10 @@ export class ServiceStatusComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
connected = false
|
connected = false
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
return this.connected ? this.rendering.display : 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
get class(): string | null {
|
get class(): string | null {
|
||||||
if (!this.connected) return null
|
if (!this.connected) return null
|
||||||
|
|
||||||
@@ -94,21 +120,4 @@ export class ServiceStatusComponent {
|
|||||||
get rendering() {
|
get rendering() {
|
||||||
return PrimaryRendering[this.status]
|
return PrimaryRendering[this.status]
|
||||||
}
|
}
|
||||||
|
|
||||||
get icon(): string {
|
|
||||||
if (!this.connected) return '@tui.circle'
|
|
||||||
|
|
||||||
switch (this.rendering.color) {
|
|
||||||
case 'danger':
|
|
||||||
return '@tui.circle-x'
|
|
||||||
case 'warning':
|
|
||||||
return '@tui.circle-alert'
|
|
||||||
case 'success':
|
|
||||||
return '@tui.circle-check'
|
|
||||||
case 'primary':
|
|
||||||
return '@tui.circle-minus'
|
|
||||||
default:
|
|
||||||
return '@tui.circle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
|||||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
import { ServiceComponent } from './service.component'
|
import { ServiceComponent } from './service.component'
|
||||||
import { ServicesService } from './services.service'
|
import { ServicesService } from './services.service'
|
||||||
@@ -12,6 +13,7 @@ import { ServicesService } from './services.service'
|
|||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Services</ng-container>
|
||||||
<table tuiTable class="g-table" [(sorter)]="sorter">
|
<table tuiTable class="g-table" [(sorter)]="sorter">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -55,7 +57,7 @@ import { ServicesService } from './services.service'
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
imports: [ServiceComponent, ToManifestPipe, TuiTable],
|
imports: [ServiceComponent, ToManifestPipe, TuiTable, TitleDirective],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export default class DashboardComponent {
|
export default class DashboardComponent {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import ServiceMarkdownRoute from './markdown.component'
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -55,14 +55,6 @@ const OTHER = 'Other Custom Actions'
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiCell] {
|
|
||||||
margin: 0 -1rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: -0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-subpage' },
|
host: { class: 'g-subpage' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -1,53 +1,65 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import {
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
ChangeDetectionStrategy,
|
||||||
import { ActivatedRoute } from '@angular/router'
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { combineLatest, map } from 'rxjs'
|
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<app-interface
|
<ng-container *title>
|
||||||
*ngIf="interfacesWithAddresses$ | async as serviceInterface"
|
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
[packageId]="context.packageId"
|
{{ interface()?.name }}
|
||||||
[serviceInterface]="serviceInterface"
|
</ng-container>
|
||||||
/>
|
@if (interface(); as serviceInterface) {
|
||||||
|
<app-interface
|
||||||
|
[packageId]="pkgId"
|
||||||
|
[serviceInterface]="serviceInterface"
|
||||||
|
/>
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-subpage' },
|
host: { class: 'g-subpage' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, InterfaceComponent],
|
imports: [InterfaceComponent, RouterLink, TuiButton, TitleDirective],
|
||||||
})
|
})
|
||||||
export default class ServiceInterfaceRoute {
|
export default class ServiceInterfaceRoute {
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
readonly context = {
|
readonly pkgId = getPkgId()
|
||||||
packageId: getPkgId(),
|
readonly interfaceId = input('')
|
||||||
interfaceId:
|
|
||||||
inject(ActivatedRoute).snapshot.paramMap.get('interfaceId') || '',
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly interfacesWithAddresses$ = combineLatest([
|
readonly pkg = toSignal(
|
||||||
this.patch.watch$(
|
inject<PatchDB<DataModel>>(PatchDB).watch$('packageData', this.pkgId),
|
||||||
'packageData',
|
|
||||||
this.context.packageId,
|
|
||||||
'serviceInterfaces',
|
|
||||||
this.context.interfaceId,
|
|
||||||
),
|
|
||||||
this.patch.watch$('packageData', this.context.packageId, 'hosts'),
|
|
||||||
]).pipe(
|
|
||||||
map(([iFace, hosts]) => ({
|
|
||||||
...iFace,
|
|
||||||
addresses: getAddresses(
|
|
||||||
iFace,
|
|
||||||
hosts[iFace.addressInfo.hostId],
|
|
||||||
this.config,
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly interface = computed(() => {
|
||||||
|
const pkg = this.pkg()
|
||||||
|
const id = this.interfaceId()
|
||||||
|
|
||||||
|
if (!pkg || !id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serviceInterfaces, hosts } = pkg
|
||||||
|
const item = serviceInterfaces[this.interfaceId()]
|
||||||
|
const host = hosts[item.addressInfo.hostId]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
public: host.bindings[item.addressInfo.internalPort].net.public,
|
||||||
|
addresses: getAddresses(item, host, this.config),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||||
import { TuiAppearance, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
import { TuiAvatar, TuiFade } from '@taiga-ui/kit'
|
||||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
import { distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -25,6 +26,13 @@ const ICONS = {
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (service()) {
|
@if (service()) {
|
||||||
|
<div *title class="title">
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
<tui-avatar size="xs" [style.margin-inline-end.rem]="0.75">
|
||||||
|
<img alt="" [src]="service()?.icon" />
|
||||||
|
</tui-avatar>
|
||||||
|
<span tuiFade>{{ manifest()?.title }}</span>
|
||||||
|
</div>
|
||||||
<aside>
|
<aside>
|
||||||
<header tuiCell>
|
<header tuiCell>
|
||||||
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar>
|
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar>
|
||||||
@@ -58,6 +66,16 @@ const ICONS = {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-inline-start: -1rem;
|
||||||
|
|
||||||
|
&:not(:only-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
@@ -108,10 +126,13 @@ const ICONS = {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
padding: 0.125rem;
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
|
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: none;
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[tuiTitle] {
|
[tuiTitle] {
|
||||||
@@ -131,6 +152,8 @@ const ICONS = {
|
|||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiFade,
|
TuiFade,
|
||||||
|
TitleDirective,
|
||||||
|
TuiButton,
|
||||||
],
|
],
|
||||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,11 +46,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
<service-interfaces [pkg]="pkg()" [disabled]="status() !== 'running'" />
|
<service-interfaces [pkg]="pkg()" [disabled]="status() !== 'running'" />
|
||||||
<service-dependencies [pkg]="pkg()" [services]="services()" />
|
<service-dependencies [pkg]="pkg()" [services]="services()" />
|
||||||
<service-health-checks [checks]="health()" />
|
<service-health-checks [checks]="health()" />
|
||||||
|
<service-action-requests [pkg]="pkg()" [services]="services()" />
|
||||||
<section class="g-card">
|
|
||||||
<header>Tasks</header>
|
|
||||||
<service-action-requests [pkg]="pkg()" />
|
|
||||||
</section>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (installing()) {
|
@if (installing()) {
|
||||||
@@ -65,7 +61,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
grid-auto-rows: max-content;
|
grid-auto-rows: max-content;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
@@ -79,7 +75,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
grid-column: span 1 !important;
|
grid-column: span 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
UntypedFormGroup,
|
UntypedFormGroup,
|
||||||
} from '@angular/forms'
|
} from '@angular/forms'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { IST, inputSpec } from '@start9labs/start-sdk'
|
import { IST, inputSpec } from '@start9labs/start-sdk'
|
||||||
import { TuiButton, TuiDialogService } from '@taiga-ui/core'
|
import { TuiButton, TuiDialogService } from '@taiga-ui/core'
|
||||||
@@ -15,11 +16,16 @@ import { FormModule } from 'src/app/routes/portal/components/form/form.module'
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormService } from 'src/app/services/form.service'
|
import { FormService } from 'src/app/services/form.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { EmailInfoComponent } from './info.component'
|
import { EmailInfoComponent } from './info.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
Email
|
||||||
|
</ng-container>
|
||||||
<email-info />
|
<email-info />
|
||||||
<ng-container *ngIf="form$ | async as form">
|
<ng-container *ngIf="form$ | async as form">
|
||||||
<form [formGroup]="form" [style.text-align]="'right'">
|
<form [formGroup]="form" [style.text-align]="'right'">
|
||||||
@@ -31,7 +37,7 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
<button
|
<button
|
||||||
*ngIf="isSaved"
|
*ngIf="isSaved"
|
||||||
tuiButton
|
tuiButton
|
||||||
appearance="destructive"
|
appearance="secondary-destructive"
|
||||||
[style.margin-top.rem]="1"
|
[style.margin-top.rem]="1"
|
||||||
[style.margin-right.rem]="1"
|
[style.margin-right.rem]="1"
|
||||||
(click)="save(null)"
|
(click)="save(null)"
|
||||||
@@ -79,6 +85,8 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
TuiInputModule,
|
TuiInputModule,
|
||||||
EmailInfoComponent,
|
EmailInfoComponent,
|
||||||
|
RouterLink,
|
||||||
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsEmailComponent {
|
export class SettingsEmailComponent {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { Observable, map } from 'rxjs'
|
import { Observable, map } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
|
||||||
const iface: T.ServiceInterface = {
|
const iface: T.ServiceInterface = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -30,6 +33,10 @@ const iface: T.ServiceInterface = {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
Web Addresses
|
||||||
|
</ng-container>
|
||||||
<app-interface
|
<app-interface
|
||||||
*ngIf="ui$ | async as ui"
|
*ngIf="ui$ | async as ui"
|
||||||
[style.max-width.rem]="50"
|
[style.max-width.rem]="50"
|
||||||
@@ -38,7 +45,13 @@ const iface: T.ServiceInterface = {
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, InterfaceComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
InterfaceComponent,
|
||||||
|
RouterLink,
|
||||||
|
TuiButton,
|
||||||
|
TitleDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class StartOsUiComponent {
|
export class StartOsUiComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { TuiLet } from '@taiga-ui/cdk'
|
import { TuiLet } from '@taiga-ui/cdk'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
@@ -6,10 +7,15 @@ import { ErrorService, LoadingService } from '@start9labs/shared'
|
|||||||
import { from, map, merge, Observable, Subject } from 'rxjs'
|
import { from, map, merge, Observable, Subject } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { Session } from 'src/app/services/api/api.types'
|
import { Session } from 'src/app/services/api/api.types'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SSHTableComponent } from './table.component'
|
import { SSHTableComponent } from './table.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
Active Sessions
|
||||||
|
</ng-container>
|
||||||
<h3 class="g-title">Current session</h3>
|
<h3 class="g-title">Current session</h3>
|
||||||
<table
|
<table
|
||||||
class="g-table"
|
class="g-table"
|
||||||
@@ -36,7 +42,14 @@ import { SSHTableComponent } from './table.component'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiButton, SSHTableComponent, TuiLet],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiButton,
|
||||||
|
SSHTableComponent,
|
||||||
|
TuiLet,
|
||||||
|
RouterLink,
|
||||||
|
TitleDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SettingsSessionsComponent {
|
export class SettingsSessionsComponent {
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { ErrorService } from '@start9labs/shared'
|
import { ErrorService } from '@start9labs/shared'
|
||||||
import { catchError, defer, of } from 'rxjs'
|
import { catchError, defer, of } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SSHInfoComponent } from './info.component'
|
import { SSHInfoComponent } from './info.component'
|
||||||
import { SSHTableComponent } from './table.component'
|
import { SSHTableComponent } from './table.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
SSH
|
||||||
|
</ng-container>
|
||||||
<ssh-info />
|
<ssh-info />
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
Saved Keys
|
Saved Keys
|
||||||
@@ -25,7 +31,14 @@ import { SSHTableComponent } from './table.component'
|
|||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, TuiButton, SSHTableComponent, SSHInfoComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TuiButton,
|
||||||
|
SSHTableComponent,
|
||||||
|
SSHInfoComponent,
|
||||||
|
RouterLink,
|
||||||
|
TitleDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SettingsSSHComponent {
|
export class SettingsSSHComponent {
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared'
|
import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiAlertService,
|
TuiAlertService,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { WifiInfoComponent } from './info.component'
|
import { WifiInfoComponent } from './info.component'
|
||||||
import { WifiTableComponent } from './table.component'
|
import { WifiTableComponent } from './table.component'
|
||||||
import { parseWifi, WifiData, WiFiForm } from './utils'
|
import { parseWifi, WifiData, WiFiForm } from './utils'
|
||||||
@@ -32,6 +34,10 @@ import { wifiSpec } from './wifi.const'
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>
|
||||||
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||||
|
WiFi
|
||||||
|
</ng-container>
|
||||||
<wifi-info />
|
<wifi-info />
|
||||||
@if (status()?.interface) {
|
@if (status()?.interface) {
|
||||||
<h3 class="g-title">
|
<h3 class="g-title">
|
||||||
@@ -87,6 +93,8 @@ import { wifiSpec } from './wifi.const'
|
|||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
WifiInfoComponent,
|
WifiInfoComponent,
|
||||||
WifiTableComponent,
|
WifiTableComponent,
|
||||||
|
TitleDirective,
|
||||||
|
RouterLink,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SettingsWifiComponent {
|
export class SettingsWifiComponent {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { TuiIcon } from '@taiga-ui/core'
|
import { TuiIcon } from '@taiga-ui/core'
|
||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SettingsMenuComponent } from './components/menu.component'
|
import { SettingsMenuComponent } from './components/menu.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title><span>Settings</span></ng-container>
|
||||||
<a
|
<a
|
||||||
routerLink="/portal/settings"
|
routerLink="/portal/settings"
|
||||||
routerLinkActive="_current"
|
routerLinkActive="_current"
|
||||||
@@ -25,6 +27,7 @@ import { SettingsMenuComponent } from './components/menu.component'
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
span:not(:last-child),
|
||||||
settings-menu {
|
settings-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -39,6 +42,6 @@ import { SettingsMenuComponent } from './components/menu.component'
|
|||||||
host: { class: 'g-page' },
|
host: { class: 'g-page' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterModule, TuiIcon, SettingsMenuComponent],
|
imports: [RouterModule, TuiIcon, SettingsMenuComponent, TitleDirective],
|
||||||
})
|
})
|
||||||
export class SettingsComponent {}
|
export class SettingsComponent {}
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ import {
|
|||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { tuiIsString } from '@taiga-ui/cdk'
|
import { tuiIsString } from '@taiga-ui/cdk'
|
||||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
TuiAvatar,
|
TuiAvatar,
|
||||||
TuiFiles,
|
TuiFiles,
|
||||||
tuiInputFilesOptionsProvider,
|
tuiInputFilesOptionsProvider,
|
||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SideloadPackageComponent } from './package.component'
|
import { SideloadPackageComponent } from './package.component'
|
||||||
import { parseS9pk } from './sideload.utils'
|
import { parseS9pk } from './sideload.utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
<ng-container *title>Sideload</ng-container>
|
||||||
@if (file && package()) {
|
@if (file && package()) {
|
||||||
<sideload-package [package]="package()!" [file]="file!">
|
<sideload-package [package]="package()!" [file]="file!">
|
||||||
<button
|
<button
|
||||||
@@ -82,10 +84,10 @@ import { parseS9pk } from './sideload.utils'
|
|||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
TuiFiles,
|
TuiFiles,
|
||||||
TuiLink,
|
|
||||||
TuiAvatar,
|
TuiAvatar,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
SideloadPackageComponent,
|
SideloadPackageComponent,
|
||||||
|
TitleDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SideloadComponent {
|
export default class SideloadComponent {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const routes: Routes = [
|
|||||||
paramsInheritanceStrategy: 'always',
|
paramsInheritanceStrategy: 'always',
|
||||||
preloadingStrategy: PreloadAllModules,
|
preloadingStrategy: PreloadAllModules,
|
||||||
initialNavigation: 'disabled',
|
initialNavigation: 'disabled',
|
||||||
|
bindToComponentInputs: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export const mockPatchData: DataModel = {
|
|||||||
backupProgress: {},
|
backupProgress: {},
|
||||||
},
|
},
|
||||||
hostname: 'random-words',
|
hostname: 'random-words',
|
||||||
|
// @ts-ignore
|
||||||
host: {
|
host: {
|
||||||
bindings: {
|
bindings: {
|
||||||
80: {
|
80: {
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { inject, Injectable } from '@angular/core'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import {
|
|
||||||
DataModel,
|
|
||||||
PackageDataEntry,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities'
|
|
||||||
import { toRouterLink } from 'src/app/utils/to-router-link'
|
|
||||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
|
||||||
|
|
||||||
export interface Breadcrumb {
|
|
||||||
title: string
|
|
||||||
routerLink: string
|
|
||||||
subtitle?: string
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class BreadcrumbsService extends BehaviorSubject<readonly Breadcrumb[]> {
|
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super([])
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(page: string) {
|
|
||||||
const packages = await getAllPackages(this.patch)
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.next(toBreadcrumbs(page.split('?')[0], packages))
|
|
||||||
} catch (e) {
|
|
||||||
this.next([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toBreadcrumbs(
|
|
||||||
id: string,
|
|
||||||
packages: Record<string, PackageDataEntry> = {},
|
|
||||||
): Breadcrumb[] {
|
|
||||||
if (id.startsWith('/portal/') && !id.startsWith('/portal/services/')) {
|
|
||||||
const [page, ...path] = id.replace('/portal/', '').split('/')
|
|
||||||
const service = `/portal/${page}`
|
|
||||||
const { icon, title } = SYSTEM_UTILITIES[service]
|
|
||||||
const breadcrumbs: Breadcrumb[] = [
|
|
||||||
{
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
routerLink: toRouterLink(service),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (path.length) {
|
|
||||||
breadcrumbs.push({
|
|
||||||
title: path.join(': '),
|
|
||||||
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return breadcrumbs
|
|
||||||
}
|
|
||||||
|
|
||||||
const [service, ...path] = id.split('/')
|
|
||||||
const { title, version } = getManifest(packages[service])
|
|
||||||
const breadcrumbs: Breadcrumb[] = [
|
|
||||||
{
|
|
||||||
icon: packages[service].icon,
|
|
||||||
title,
|
|
||||||
subtitle: version,
|
|
||||||
routerLink: toRouterLink(service),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (path.length) {
|
|
||||||
breadcrumbs.push({
|
|
||||||
title: path.join(': '),
|
|
||||||
routerLink: breadcrumbs[0].routerLink + '/' + path.join('/'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return breadcrumbs
|
|
||||||
}
|
|
||||||
45
web/projects/ui/src/app/services/title.service.ts
Normal file
45
web/projects/ui/src/app/services/title.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
EmbeddedViewRef,
|
||||||
|
inject,
|
||||||
|
Injectable,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class TitleService {
|
||||||
|
private vcr?: ViewContainerRef
|
||||||
|
|
||||||
|
register(vcr: ViewContainerRef) {
|
||||||
|
this.vcr = vcr
|
||||||
|
}
|
||||||
|
|
||||||
|
add(template: TemplateRef<any>) {
|
||||||
|
return this.vcr?.createEmbeddedView(template)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'ng-template[title]',
|
||||||
|
providers: [tuiButtonOptionsProvider({ appearance: '' })],
|
||||||
|
})
|
||||||
|
export class TitleDirective implements OnInit, OnDestroy {
|
||||||
|
private readonly template = inject(TemplateRef)
|
||||||
|
private readonly service = inject(TitleService)
|
||||||
|
private view?: EmbeddedViewRef<any>
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.view = this.service.add(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.view?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,16 +69,14 @@ hr {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
|
|
||||||
tui-root._mobile & {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.g-card {
|
.g-card {
|
||||||
transition: all 300ms ease-in-out;
|
position: relative;
|
||||||
padding: 1.25rem 1.5rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 3.25rem 1rem 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: color-mix(in hsl, var(--start9-base-1) 50%, transparent);
|
background-color: color-mix(in hsl, var(--start9-base-1) 50%, transparent);
|
||||||
@@ -103,33 +101,38 @@ hr {
|
|||||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
inset 0 1px rgba(255, 255, 255, 0.15),
|
||||||
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
&:hover {
|
> [tuiCell] {
|
||||||
box-shadow:
|
margin: 0 -0.5rem;
|
||||||
0 0.375rem 0.5rem rgba(0, 0, 0, 0.25),
|
|
||||||
0 -0.125rem 0.25rem rgba(55, 155, 255, 0.08),
|
&:not(:last-child)::after {
|
||||||
0 0 0.5rem rgba(0, 0, 0, 0.3),
|
content: '';
|
||||||
inset 0 -0.125rem rgba(255, 255, 255, 0.03),
|
position: absolute;
|
||||||
inset 0 2px rgba(255, 255, 255, 0.1),
|
top: 100%;
|
||||||
inset 0 1px rgba(255, 255, 255, 0.15),
|
left: 1rem;
|
||||||
inset 0 0 1rem rgba(0, 0, 0, 0.25);
|
right: 1rem;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--tui-border-normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> [tuiCell]:not(:last-child)::after {
|
> table {
|
||||||
content: '';
|
margin: 0 -0.5rem;
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
td:empty,
|
||||||
left: 1rem;
|
th:empty {
|
||||||
right: 1rem;
|
display: none;
|
||||||
height: 1px;
|
}
|
||||||
background: var(--tui-border-normal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> header {
|
> header {
|
||||||
padding-bottom: 0.75rem;
|
position: absolute;
|
||||||
margin: -0.5rem 0 0.5rem;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
box-shadow: 0 -10rem 0 10rem var(--tui-background-neutral-1);
|
font: var(--tui-font-text-l);
|
||||||
font: var(--tui-font-heading-6);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +201,10 @@ hr {
|
|||||||
.g-table[tuiTable] {
|
.g-table[tuiTable] {
|
||||||
width: stretch;
|
width: stretch;
|
||||||
|
|
||||||
|
&:not(:has(tbody tr)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
tr:not(:last-child) {
|
tr:not(:last-child) {
|
||||||
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
box-shadow: inset 0 -1px var(--tui-background-neutral-1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user