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