refactor: change service page to the new design

This commit is contained in:
waterplea
2024-05-12 16:53:01 +01:00
parent ec878defab
commit 7e1b433c17
27 changed files with 381 additions and 209 deletions

View File

@@ -10,7 +10,7 @@
& > * + * {
border-top-width: 1px;
border-bottom-width: 0px;
border-bottom-width: 0;
border-color: rgb(113 113 122);
}
}

View File

@@ -9,7 +9,7 @@
/* stylelint-disable order/order */
[tuiAppearance][data-appearance='secondary-warning'] {
background: var(--tui-warning-bg);
color: var(--tui-warning-fill);
color: var(--tui-text-01);
@include appearance-hover {
background: var(--tui-warning-bg-hover);
@@ -58,6 +58,7 @@
@include appearance-disabled {
background: #eaecee;
color: #333;
}
}
@@ -75,6 +76,7 @@
@include appearance-disabled {
background: #eaecee;
color: #333;
}
}
@@ -92,6 +94,7 @@
@include appearance-disabled {
background: #eaecee;
color: #333;
}
}
@@ -109,6 +112,7 @@
@include appearance-disabled {
background: #eaecee;
color: #333;
}
}
@@ -126,6 +130,7 @@
@include appearance-disabled {
background: #eaecee;
color: #333;
}
}

View File

@@ -7,7 +7,7 @@
class="button"
[class.button_open]="open"
[style.border-radius.%]="100"
[appearance]="invalid ? 'secondary-destructive' : 'secondary'"
[appearance]="invalid ? 'danger-solid' : 'secondary'"
></button>
<ng-content></ng-content>
{{ spec.name }}

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiIconModule } from '@taiga-ui/experimental'
interface ActionItem {
readonly icon: string
@@ -10,7 +11,7 @@ interface ActionItem {
@Component({
selector: '[action]',
template: `
<tui-svg [src]="action.icon"></tui-svg>
<tui-icon [icon]="action.icon"></tui-icon>
<div>
<strong>{{ action.name }}</strong>
<div>{{ action.description }}</div>
@@ -18,7 +19,7 @@ interface ActionItem {
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiSvgModule],
imports: [TuiIconModule],
})
export class ServiceActionComponent {
@Input({ required: true })

View File

@@ -4,13 +4,16 @@ import {
inject,
Input,
} from '@angular/core'
import { Manifest } from '@startos'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiButtonModule,
tuiButtonOptionsProvider,
} from '@taiga-ui/experimental'
import { DependencyInfo } from 'src/app/routes/portal/routes/service/types/dependency-info'
import { ActionsService } from 'src/app/services/actions.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { Manifest } from '../../../../../../../../../../core/startos/bindings/Manifest'
@Component({
selector: 'service-actions',
@@ -18,7 +21,7 @@ import { Manifest } from '../../../../../../../../../../core/startos/bindings/Ma
@if (pkg.status.main.status === 'running') {
<button
tuiButton
appearance="secondary-destructive"
appearance="danger-solid"
iconLeft="tuiIconSquare"
(click)="actions.stop(manifest)"
>
@@ -27,7 +30,6 @@ import { Manifest } from '../../../../../../../../../../core/startos/bindings/Ma
<button
tuiButton
appearance="secondary"
iconLeft="tuiIconRotateCw"
(click)="actions.restart(manifest)"
>
@@ -56,10 +58,20 @@ import { Manifest } from '../../../../../../../../../../core/startos/bindings/Ma
</button>
}
`,
styles: [':host { display: flex; gap: 1rem }'],
styles: [
`
:host {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding-bottom: 1rem;
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButtonModule],
providers: [tuiButtonOptionsProvider({ size: 's' })],
})
export class ServiceActionsComponent {
@Input({ required: true })

View File

@@ -1,34 +0,0 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ToAdditionalPipe } from '../pipes/to-additional.pipe'
import { ServiceAdditionalItemComponent } from './additional-item.component'
@Component({
selector: 'service-additional',
template: `
<h3 class="g-title">Additional Info</h3>
<ng-container *ngFor="let additional of pkg | toAdditional">
<a
*ngIf="additional.description.startsWith('http'); else button"
class="g-action"
[additionalItem]="additional"
></a>
<ng-template #button>
<button
class="g-action"
[style.pointer-events]="!additional.icon ? 'none' : null"
[additionalItem]="additional"
(click)="additional.action?.()"
></button>
</ng-template>
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, ToAdditionalPipe, ServiceAdditionalItemComponent],
})
export class ServiceAdditionalComponent {
@Input({ required: true })
pkg!: PackageDataEntry
}

View File

@@ -0,0 +1,51 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { RouterLink } from '@angular/router'
import { TuiButtonModule } from '@taiga-ui/experimental'
@Component({
selector: 'service-backups',
template: `
<div>
<small>Last backup</small>
6 days ago
</div>
<div>
<small>Next backup</small>
Not scheduled
</div>
<div>
<a
tuiButton
iconLeft="tuiIconPlusSquare"
routerLink="/portal/system/backups"
size="s"
appearance="secondary-warning"
>
Manage
</a>
</div>
`,
styles: `
:host {
display: flex;
gap: 1rem;
flex-wrap: wrap;
white-space: nowrap;
> :last-child {
min-width: 100%;
padding-bottom: 1rem;
}
small {
display: block;
text-transform: uppercase;
color: var(--tui-text-02);
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButtonModule, RouterLink],
})
export class ServiceBackupsComponent {}

View File

@@ -1,22 +1,26 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { ServiceDependencyComponent } from './dependency.component'
import { DependencyInfo } from '../types/dependency-info'
import { ServiceDependencyComponent } from './dependency.component'
@Component({
selector: 'service-dependencies',
template: `
<h3 class="g-title">Dependencies</h3>
<button
*ngFor="let dep of dependencies"
class="g-action"
[serviceDependency]="dep"
(click)="dep.action()"
></button>
@for (dep of dependencies; track $index) {
<button
class="g-action"
[serviceDependency]="dep"
(click)="dep.action()"
></button>
}
@if (!dependencies.length) {
No dependencies
}
`,
styles: ':host { display: block; min-height: var(--tui-height-s) }',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, ServiceDependencyComponent],
imports: [ServiceDependencyComponent],
})
export class ServiceDependenciesComponent {
@Input({ required: true })

View File

@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { HealthCheckResult } from '@startos'
import { TuiLoaderModule, TuiSvgModule } from '@taiga-ui/core'
import { HealthCheckResult } from '../../../../../../../../../../core/startos/bindings/HealthCheckResult'
@Component({
selector: 'service-health-check',

View File

@@ -1,28 +1,33 @@
import { CommonModule } from '@angular/common'
import { AsyncPipe } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { HealthCheckResult } from '@startos'
import { ServiceHealthCheckComponent } from 'src/app/routes/portal/routes/service/components/health-check.component'
import { ConnectionService } from 'src/app/services/connection.service'
import { ServiceHealthCheckComponent } from './health-check.component'
import { HealthCheckResult } from '../../../../../../../../../../core/startos/bindings/HealthCheckResult'
@Component({
selector: 'service-health-checks',
template: `
<h3 class="g-title">Health Checks</h3>
<service-health-check
*ngFor="let check of checks"
class="g-action"
[check]="check"
[connected]="!!(connected$ | async)"
></service-health-check>
@for (check of checks; track $index) {
<service-health-check
class="g-action"
[check]="check"
[connected]="!!(connected$ | async)"
/>
}
@if (!checks.length) {
No health checks
}
`,
styles: ':host { display: block; min-height: var(--tui-height-s) }',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, ServiceHealthCheckComponent],
imports: [AsyncPipe, ServiceHealthCheckComponent],
})
export class ServiceHealthChecksComponent {
@Input({ required: true })

View File

@@ -5,35 +5,60 @@ import {
inject,
Input,
} from '@angular/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiLetModule } from '@taiga-ui/cdk'
import { TuiLoaderModule } from '@taiga-ui/core'
import { TuiButtonModule, TuiIconModule } from '@taiga-ui/experimental'
import { map, timer } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
@Component({
selector: 'a[serviceInterfaceListItem]',
template: `
<tui-svg [src]="info.icon" [style.color]="info.color"></tui-svg>
<div [style.flex]="1">
<strong>{{ info.name }}</strong>
<div>{{ info.description }}</div>
<div [style.color]="info.color">{{ info.typeDetail }}</div>
</div>
<a
*ngIf="info.type === 'ui'"
tuiIconButton
appearance="flat"
iconLeft="tuiIconExternalLinkLarge"
target="_blank"
rel="noreferrer"
[style.border-radius.%]="100"
[attr.href]="href"
(click.stop)="(0)"
></a>
<ng-container *tuiLet="healthCheck$ | async as check">
@if (check === null) {
<tui-loader />
} @else if (check === '') {
<tui-icon [icon]="info.icon" [style.color]="info.color" />
} @else {
<tui-icon icon="tuiIconXCircle" class="g-error" />
}
<div [style.flex]="1">
<strong>{{ info.name }}</strong>
<div>{{ info.description }}</div>
@if (check) {
<div class="g-error">
<b>Health check failed:</b>
{{ check }}
</div>
} @else {
<div [style.color]="info.color">{{ info.typeDetail }}</div>
}
</div>
@if (info.type === 'ui') {
<a
tuiIconButton
appearance="flat"
iconLeft="tuiIconExternalLinkLarge"
target="_blank"
rel="noreferrer"
title="Open"
[style.border-radius.%]="100"
[attr.href]="href"
(click.stop)="(0)"
></a>
}
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButtonModule, CommonModule, TuiSvgModule],
imports: [
CommonModule,
TuiButtonModule,
TuiLetModule,
TuiLoaderModule,
TuiIconModule,
],
})
export class ServiceInterfaceListItemComponent {
private readonly config = inject(ConfigService)
@@ -44,6 +69,11 @@ export class ServiceInterfaceListItemComponent {
@Input()
disabled = false
// TODO: Implement real health check
readonly healthCheck$ = timer(3000).pipe(
map(() => (Math.random() > 0.5 ? '' : 'You done f***d it up...')),
)
get href(): string | null {
return this.disabled ? null : this.config.launchableAddress(this.info)
}

View File

@@ -1,31 +1,25 @@
import { NgForOf } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterLink } from '@angular/router'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
import { InterfaceInfoPipe } from '../pipes/interface-info.pipe'
import { ServiceInterfaceListItemComponent } from './interface-list-item.component'
import { RouterLink } from '@angular/router'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'service-interface-list',
template: `
<h3 class="g-title">Service Interfaces</h3>
<a
*ngFor="let info of pkg | interfaceInfo"
class="g-action"
[serviceInterfaceListItem]="info"
[disabled]="status.primary !== 'running'"
[routerLink]="info.routerLink"
></a>
@for (info of pkg | interfaceInfo; track $index) {
<a
class="g-action"
[serviceInterfaceListItem]="info"
[disabled]="status.primary !== 'running'"
[routerLink]="info.routerLink"
></a>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NgForOf,
RouterLink,
InterfaceInfoPipe,
ServiceInterfaceListItemComponent,
],
imports: [RouterLink, InterfaceInfoPipe, ServiceInterfaceListItemComponent],
})
export class ServiceInterfaceListComponent {
@Input({ required: true })

View File

@@ -1,23 +1,23 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiIconModule } from '@taiga-ui/experimental'
import { ServiceMenu } from '../pipes/to-menu.pipe'
@Component({
selector: '[serviceMenuItem]',
template: `
<tui-svg [src]="menu.icon"></tui-svg>
<tui-icon [icon]="menu.icon" />
<div [style.flex]="1">
<strong>{{ menu.name }}</strong>
<div>
{{ menu.description }}
<ng-content></ng-content>
<ng-content />
</div>
</div>
<tui-svg src="tuiIconChevronRightLarge"></tui-svg>
<tui-icon icon="tuiIconChevronRight" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiSvgModule],
imports: [TuiIconModule],
})
export class ServiceMenuItemComponent {
@Input({ required: true, alias: 'serviceMenuItem' })

View File

@@ -7,7 +7,6 @@ import { RouterLink } from '@angular/router'
@Component({
selector: 'service-menu',
template: `
<h3 class="g-title">Menu</h3>
@for (menu of pkg | toMenu; track $index) {
@if (menu.routerLink) {
<a

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { Progress } from '@startos'
import { TuiProgressModule } from '@taiga-ui/kit'
import { InstallingProgressPipe } from '../pipes/install-progress.pipe'
import { Progress } from '../../../../../../../../../../core/startos/bindings/Progress'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
@Component({
selector: '[progress]',

View File

@@ -32,9 +32,11 @@ import { UnitConversionPipesModule } from '@start9labs/shared'
styles: [
`
:host {
font-size: x-large;
margin: 1em 0;
display: block;
font-size: x-large;
white-space: nowrap;
margin: auto 0;
height: 2.75rem;
}
`,
],

View File

@@ -1,7 +1,10 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiSvgModule } from '@taiga-ui/core'
import { AdditionalItem, FALLBACK_URL } from '../pipes/to-additional.pipe'
import {
AdditionalItem,
FALLBACK_URL,
} from 'src/app/routes/portal/routes/service/pipes/to-additional.pipe'
@Component({
selector: '[additionalItem]',

View File

@@ -0,0 +1,36 @@
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { TuiDialogOptions } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ToAdditionalPipe } from 'src/app/routes/portal/routes/service/pipes/to-additional.pipe'
import { ServiceAdditionalItemComponent } from './additional-item.component'
@Component({
selector: 'service-additional',
template: `
@for (additional of pkg | toAdditional; track $index) {
@if (additional.description.startsWith('http')) {
<a class="g-action" [additionalItem]="additional"></a>
} @else {
<button
class="g-action"
[style.pointer-events]="!additional.icon ? 'none' : null"
[additionalItem]="additional"
(click)="additional.action?.()"
></button>
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ToAdditionalPipe, ServiceAdditionalItemComponent],
})
export class ServiceAdditionalModal {
readonly pkg =
inject<TuiDialogOptions<PackageDataEntry>>(POLYMORPHEUS_CONTEXT).data
}

View File

@@ -24,17 +24,17 @@ export class InterfaceInfoPipe implements PipeTransform {
switch (val.type) {
case 'ui':
color = 'var(--tui-primary)'
icon = 'tuiIconMonitorLarge'
icon = 'tuiIconMonitor'
typeDetail = 'User Interface (UI)'
break
case 'p2p':
color = 'var(--tui-info-fill)'
icon = 'tuiIconUsersLarge'
icon = 'tuiIconUsers'
typeDetail = 'Peer-To-Peer Interface (P2P)'
break
case 'api':
color = 'var(--tui-support-09)'
icon = 'tuiIconTerminalLarge'
icon = 'tuiIconTerminal'
typeDetail = 'Application Program Interface (API)'
break
}

View File

@@ -1,12 +1,12 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { CopyService, MarkdownComponent } from '@start9labs/shared'
import { Manifest } from '@startos'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { CopyService, MarkdownComponent } from '@start9labs/shared'
import { from } from 'rxjs'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
import { Manifest } from '../../../../../../../../../../core/startos/bindings/Manifest'
export const FALLBACK_URL = 'Not provided'

View File

@@ -1,20 +1,21 @@
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Params } from '@angular/router'
import { MarkdownComponent } from '@start9labs/shared'
import { Manifest } from '@startos'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { from } from 'rxjs'
import {
PackageConfigData,
ConfigModal,
PackageConfigData,
} from 'src/app/routes/portal/modals/config.component'
import { ServiceAdditionalModal } from 'src/app/routes/portal/routes/service/modals/additional.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ProxyService } from 'src/app/services/proxy.service'
import { ServicePropertiesModal } from '../modals/properties.component'
import { getManifest } from 'src/app/utils/get-package-data'
import { Manifest } from '../../../../../../../../../../core/startos/bindings/Manifest'
import { ServicePropertiesModal } from 'src/app/routes/portal/routes/service/modals/properties.component'
export interface ServiceMenu {
icon: string
@@ -40,19 +41,19 @@ export class ToMenuPipe implements PipeTransform {
return [
{
icon: 'tuiIconListLarge',
icon: 'tuiIconList',
name: 'Instructions',
description: `Understand how to use ${manifest.title}`,
action: () => this.showInstructions(manifest),
},
{
icon: 'tuiIconSlidersLarge',
icon: 'tuiIconSliders',
name: 'Config',
description: `Customize ${manifest.title}`,
action: () => this.openConfig(manifest),
},
{
icon: 'tuiIconKeyLarge',
icon: 'tuiIconKey',
name: 'Properties',
description: `Runtime information, credentials, and other values of interest`,
action: () =>
@@ -64,13 +65,13 @@ export class ToMenuPipe implements PipeTransform {
.subscribe(),
},
{
icon: 'tuiIconZapLarge',
icon: 'tuiIconZap',
name: 'Actions',
description: `Uninstall and other commands specific to ${manifest.title}`,
routerLink: `actions`,
},
{
icon: 'tuiIconShieldLarge',
icon: 'tuiIconShield',
name: 'Outbound Proxy',
description: `Proxy all outbound traffic from ${manifest.title}`,
action: () =>
@@ -80,21 +81,33 @@ export class ToMenuPipe implements PipeTransform {
),
},
{
icon: 'tuiIconFileTextLarge',
icon: 'tuiIconFileText',
name: 'Logs',
description: `Raw, unfiltered logs`,
routerLink: 'logs',
},
{
icon: 'tuiIconInfo',
name: 'Additional Info',
description: `View package details`,
action: () =>
this.dialogs
.open(new PolymorpheusComponent(ServiceAdditionalModal), {
label: `Additional Info`,
data: pkg,
})
.subscribe(),
},
pkg.marketplaceUrl
? {
icon: 'tuiIconShoppingBagLarge',
icon: 'tuiIconShoppingBag',
name: 'Marketplace Listing',
description: `View ${manifest.title} on the Marketplace`,
routerLink: `/portal/system/marketplace`,
params: { url: pkg.marketplaceUrl, id: manifest.id },
}
: {
icon: 'tuiIconShoppingBagLarge',
icon: 'tuiIconShoppingBag',
name: 'Marketplace Listing',
description: `This package was not installed from the marketplace`,
},

View File

@@ -50,7 +50,7 @@ import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
[action]="{
name: action.name,
description: action.description,
icon: 'tuiIconPlayCircleLarge'
icon: 'tuiIconPlayCircle'
}"
(click)="handleAction(action)"
></button>
@@ -75,7 +75,7 @@ export class ServiceActionsRoute {
.pipe(filter(pkg => pkg.stateInfo.state === 'installed'))
readonly action = {
icon: 'tuiIconTrash2Large',
icon: 'tuiIconTrash2',
name: 'Uninstall',
description:
'This will uninstall the service from StartOS and delete all data permanently.',

View File

@@ -2,8 +2,15 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
import { isEmptyObject } from '@start9labs/shared'
import { HealthCheckResult, MainStatus, Manifest } from '@startos'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, switchMap } from 'rxjs'
import {
ConfigModal,
PackageConfigData,
} from 'src/app/routes/portal/modals/config.component'
import { ServiceBackupsComponent } from 'src/app/routes/portal/routes/service/components/backups.component'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
import { ConnectionService } from 'src/app/services/connection.service'
import {
DepErrorService,
@@ -21,87 +28,122 @@ import {
StatusRendering,
} from 'src/app/services/pkg-status-rendering.service'
import { DependentInfo } from 'src/app/types/dependent-info'
import { getManifest } from 'src/app/utils/get-package-data'
import { ServiceActionsComponent } from '../components/actions.component'
import { ServiceAdditionalComponent } from '../components/additional.component'
import { ServiceDependenciesComponent } from '../components/dependencies.component'
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
import { ServiceInterfaceListComponent } from '../components/interface-list.component'
import { ServiceMenuComponent } from '../components/menu.component'
import { ServiceProgressComponent } from '../components/progress.component'
import { ServiceStatusComponent } from '../components/status.component'
import {
PackageConfigData,
ConfigModal,
} from 'src/app/routes/portal/modals/config.component'
import { DependencyInfo } from '../types/dependency-info'
import { getManifest } from 'src/app/utils/get-package-data'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
import { Manifest } from '../../../../../../../../../../core/startos/bindings/Manifest'
import { HealthCheckResult } from '../../../../../../../../../../core/startos/bindings/HealthCheckResult'
import { MainStatus } from '../../../../../../../../../../core/startos/bindings/MainStatus'
@Component({
template: `
@if (service$ | async; as service) {
<h3 class="g-title">Status</h3>
<service-status
[connected]="!!(connected$ | async)"
[installingInfo]="service.pkg.stateInfo.installingInfo"
[rendering]="getRendering(service.status)"
[sigtermTimeout]="
service.pkg.status.main.status === 'stopping'
? service.pkg.status.main.timeout
: null
"
/>
@if (
service.pkg.stateInfo.state === 'installing' ||
service.pkg.stateInfo.state === 'updating' ||
service.pkg.stateInfo.state === 'restoring'
) {
<p
*ngFor="
let phase of service.pkg.stateInfo.installingInfo.progress.phases
<section [style.grid-column]="'span 3'">
<h3>Status</h3>
<service-status
[connected]="!!(connected$ | async)"
[installingInfo]="service.pkg.stateInfo.installingInfo"
[rendering]="getRendering(service.status)"
[sigtermTimeout]="
service.pkg.status.main.status === 'stopping'
? service.pkg.status.main.timeout
: null
"
[progress]="phase.progress"
>
{{ phase.name }}
</p>
} @else {
@if (
service.pkg.stateInfo.state === 'installed' &&
service.status.primary !== 'backingUp'
) {
@if (connected$ | async) {
<service-actions
[pkg]="service.pkg"
[dependencies]="service.dependencies"
/>
}
/>
@if (isInstalled(service) && (connected$ | async)) {
<service-actions
[pkg]="service.pkg"
[dependencies]="service.dependencies"
/>
}
</section>
@if (isInstalled(service)) {
<section [style.grid-column]="'span 3'">
<h3>Backups</h3>
<service-backups />
</section>
<section [style.grid-column]="'span 6'">
<h3>Metrics</h3>
TODO
</section>
<section [style.grid-column]="'span 4'" [style.align-self]="'start'">
<h3>Menu</h3>
<service-menu [pkg]="service.pkg" />
</section>
<div>
<section>
<h3>Health Checks</h3>
<service-health-checks [checks]="(health$ | async) || []" />
</section>
<section>
<h3>Dependencies</h3>
<service-dependencies [dependencies]="service.dependencies" />
</section>
</div>
<section [style.grid-column]="'span 4'" [style.align-self]="'start'">
<h3>Service Interfaces</h3>
<service-interface-list
[pkg]="service.pkg"
[status]="service.status"
/>
</section>
}
@if (
service.status.primary === 'running' && (health$ | async);
as checks
) {
<service-health-checks [checks]="checks" />
}
@if (service.dependencies.length) {
<service-dependencies [dependencies]="service.dependencies" />
}
<service-menu [pkg]="service.pkg" />
<service-additional [pkg]="service.pkg" />
@if (isInstalling(service.pkg.stateInfo.state)) {
@for (
item of service.pkg.stateInfo.installingInfo?.progress?.phases;
track $index
) {
<p [progress]="item.progress">{{ item.name }}</p>
}
}
}
`,
styles: `
:host {
display: grid;
grid-template-columns: repeat(12, 1fr);
flex-direction: column;
gap: 1rem;
margin: 1rem -1rem 0;
}
:host-context(tui-root._mobile) {
display: flex;
}
section {
display: flex;
flex-direction: column;
width: 100%;
padding: 1rem 1.5rem 0.5rem;
border-radius: 1rem;
background: var(--tui-clear);
box-shadow: inset 0 7rem 0 -4rem var(--tui-clear);
clip-path: polygon(0 1.5rem, 1.5rem 0, 100% 0, 100% 100%, 0 100%);
}
h3 {
margin-bottom: 1.25rem;
}
div {
display: flex;
flex-direction: column;
gap: inherit;
grid-column: span 4;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
@@ -113,7 +155,7 @@ import { MainStatus } from '../../../../../../../../../../core/startos/bindings/
ServiceHealthChecksComponent,
ServiceDependenciesComponent,
ServiceMenuComponent,
ServiceAdditionalComponent,
ServiceBackupsComponent,
InstallingProgressPipe,
],
})
@@ -134,13 +176,11 @@ export class ServiceRoute {
this.depErrorService.getPkgDepErrors$(pkgId),
]),
),
map(([pkg, depErrors]) => {
return {
pkg,
dependencies: this.getDepInfo(pkg, depErrors),
status: renderPkgStatus(pkg, depErrors),
}
}),
map(([pkg, depErrors]) => ({
pkg,
dependencies: this.getDepInfo(pkg, depErrors),
status: renderPkgStatus(pkg, depErrors),
})),
)
readonly health$ = this.pkgId$.pipe(
@@ -150,6 +190,16 @@ export class ServiceRoute {
map(toHealthCheck),
)
isInstalling(state: string): boolean {
return (
state === 'installing' || state === 'updating' || state === 'restoring'
)
}
isInstalled({ pkg, status }: any): boolean {
return pkg.stateInfo.state === 'installed' && status.primary !== 'backingUp'
}
getRendering({ primary }: PackageStatus): StatusRendering {
return PrimaryRendering[primary]
}

View File

@@ -26,7 +26,7 @@ import { GetBackupIconPipe } from '../pipes/get-backup-icon.pipe'
Past Events
<button
tuiButton
appearance="secondary-destructive"
appearance="danger-solid"
[disabled]="disabled"
(click)="delete()"
>

View File

@@ -2,20 +2,20 @@ import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
Input,
inject,
Input,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { Manifest } from '@startos'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiSvgModule } from '@taiga-ui/core'
import { TuiLineClampModule } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'
import { Observable, first } from 'rxjs'
import { first, Observable } from 'rxjs'
import { ServerNotification } from 'src/app/services/api/api.types'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { NotificationService } from 'src/app/services/notification.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { toRouterLink } from 'src/app/utils/to-router-link'
import { Manifest } from '../../../../../../../../../../core/startos/bindings/Manifest'
@Component({
selector: '[notificationItem]',

View File

@@ -1,19 +1,19 @@
import { inject, Injectable } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { Manifest } from '@startos'
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
import {
PackageConfigData,
ConfigModal,
PackageConfigData,
} from 'src/app/routes/portal/modals/config.component'
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 { hasCurrentDeps } from 'src/app/utils/has-deps'
import { getAllPackages } from 'src/app/utils/get-package-data'
import { PatchDB } from 'patch-db-client'
import { Manifest } from '../../../../../../core/startos/bindings/Manifest'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
@Injectable({
providedIn: 'root',

View File

@@ -23,7 +23,8 @@
"paths": {
/* These paths are relative to each app base folder */
"@start9labs/marketplace": ["../marketplace/index"],
"@start9labs/shared": ["../shared/src/public-api"]
"@start9labs/shared": ["../shared/src/public-api"],
"@startos": ["../../../core/startos/bindings/index"]
},
"typeRoots": ["node_modules/@types"],
"types": ["node"]