mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
fix: finish porting minor changes to major (#2799)
This commit is contained in:
@@ -36,14 +36,14 @@ import { BackupReport } from 'src/app/services/api/api.types'
|
||||
})
|
||||
export class BackupsReportModal {
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<void, { report: BackupReport; timestamp: string }>>(
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
)
|
||||
inject<
|
||||
TuiDialogContext<void, { content: BackupReport; timestamp: string }>
|
||||
>(POLYMORPHEUS_CONTEXT)
|
||||
|
||||
readonly system = this.getSystem()
|
||||
|
||||
get report(): BackupReport {
|
||||
return this.context.data.report
|
||||
return this.context.data.content
|
||||
}
|
||||
|
||||
get timestamp(): string {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: '[registry]',
|
||||
template: `
|
||||
<store-icon
|
||||
[url]="registry.url"
|
||||
[marketplace]="marketplace"
|
||||
size="40px"
|
||||
></store-icon>
|
||||
<div tuiTitle>
|
||||
{{ registry.name }}
|
||||
<div tuiSubtitle>{{ registry.url }}</div>
|
||||
</div>
|
||||
<tui-icon
|
||||
*ngIf="registry.selected; else content"
|
||||
icon="@tui.check"
|
||||
[style.color]="'var(--tui-positive)'"
|
||||
/>
|
||||
<ng-template #content><ng-content></ng-content></ng-template>
|
||||
`,
|
||||
styles: [':host { border-radius: 0.25rem; width: stretch; }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, StoreIconComponent, TuiIconModule, TuiTitleModule],
|
||||
})
|
||||
export class MarketplaceRegistryComponent {
|
||||
readonly marketplace = inject(ConfigService).marketplace
|
||||
|
||||
@Input()
|
||||
registry!: { url: string; selected: boolean; name?: string }
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { sameUrl } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'store-icon',
|
||||
template: `
|
||||
<img
|
||||
*ngIf="icon; else noIcon"
|
||||
[style.border-radius.%]="100"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Marketplace Icon"
|
||||
/>
|
||||
<ng-template #noIcon>
|
||||
<img
|
||||
[style.max-width]="size || '100%'"
|
||||
[style.border-radius]="0"
|
||||
src="assets/img/storefront-outline.png"
|
||||
alt="Marketplace Icon"
|
||||
/>
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf],
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
@Input()
|
||||
url = ''
|
||||
@Input()
|
||||
size?: string
|
||||
@Input()
|
||||
marketplace!: any
|
||||
|
||||
get icon() {
|
||||
const { start9, community } = this.marketplace
|
||||
|
||||
if (sameUrl(this.url, start9)) {
|
||||
return 'assets/img/icon_transparent.png'
|
||||
} else if (sameUrl(this.url, community)) {
|
||||
return 'assets/img/community-store.png'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<ng-container *ngIf="actionRequests.critical.length">
|
||||
<ion-item-divider>Required Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.critical"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="actionRequests.important.length">
|
||||
<ion-item-divider>Requested Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.important"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="play-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
@@ -1,16 +0,0 @@
|
||||
.light {
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
color: var(--ion-color-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dependency {
|
||||
display: inline-flex;
|
||||
img {
|
||||
max-width: 16px;
|
||||
margin: 0 2px 0 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { getDepDetails } from 'src/app/util/dep-info'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-action-requests',
|
||||
templateUrl: './app-show-action-requests.component.html',
|
||||
styleUrls: ['./app-show-action-requests.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowActionRequestsComponent {
|
||||
@Input()
|
||||
allPkgs!: Record<string, T.PackageDataEntry>
|
||||
|
||||
@Input()
|
||||
pkg!: T.PackageDataEntry
|
||||
|
||||
@Input()
|
||||
manifest!: T.Manifest
|
||||
|
||||
get actionRequests() {
|
||||
const critical: (T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
})[] = []
|
||||
const important: (T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
})[] = []
|
||||
|
||||
Object.values(this.pkg.requestedActions)
|
||||
.filter(r => r.active)
|
||||
.forEach(r => {
|
||||
const self = r.request.packageId === this.manifest.id
|
||||
const toReturn = {
|
||||
...r.request,
|
||||
actionName: self
|
||||
? this.pkg.actions[r.request.actionId].name
|
||||
: this.allPkgs[r.request.packageId]?.actions[r.request.actionId]
|
||||
.name || 'Unknown Action',
|
||||
dependency: self
|
||||
? null
|
||||
: getDepDetails(this.pkg, this.allPkgs, r.request.packageId),
|
||||
}
|
||||
|
||||
if (r.request.severity === 'critical') {
|
||||
critical.push(toReturn)
|
||||
} else {
|
||||
important.push(toReturn)
|
||||
}
|
||||
})
|
||||
|
||||
return { critical, important }
|
||||
}
|
||||
|
||||
constructor(private readonly actionService: ActionService) {}
|
||||
|
||||
async handleAction(request: T.ActionRequest) {
|
||||
const self = request.packageId === this.manifest.id
|
||||
this.actionService.present({
|
||||
pkgInfo: {
|
||||
id: request.packageId,
|
||||
title: self
|
||||
? this.manifest.title
|
||||
: getDepDetails(this.pkg, this.allPkgs, request.packageId).title,
|
||||
mainStatus: self
|
||||
? this.pkg.status.main
|
||||
: this.allPkgs[request.packageId].status.main,
|
||||
icon: self
|
||||
? this.pkg.icon
|
||||
: getDepDetails(this.pkg, this.allPkgs, request.packageId).icon,
|
||||
},
|
||||
actionInfo: {
|
||||
id: request.actionId,
|
||||
metadata:
|
||||
request.packageId === this.manifest.id
|
||||
? this.pkg.actions[request.actionId]
|
||||
: this.allPkgs[request.packageId].actions[request.actionId],
|
||||
},
|
||||
requestInfo: {
|
||||
request,
|
||||
dependentId:
|
||||
request.packageId === this.manifest.id ? undefined : this.manifest.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<ion-item-divider>Message</ion-item-divider>
|
||||
<div class="code-block ion-margin">
|
||||
<code>
|
||||
<ion-text color="warning">{{ error.message }}</ion-text>
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<ion-item-divider>Actions</ion-item-divider>
|
||||
<div class="ion-margin">
|
||||
<p>
|
||||
<b>Rebuild Container</b>
|
||||
is harmless action that and only takes a few seconds to complete. It will
|
||||
likely resolve this issue.
|
||||
<b>Uninstall Service</b>
|
||||
is a dangerous action that will remove the service from StartOS and wipe all
|
||||
its data.
|
||||
</p>
|
||||
<ion-button class="ion-margin-end" (click)="rebuild()">
|
||||
Rebuild Container
|
||||
</ion-button>
|
||||
<ion-button (click)="tryUninstall()" color="danger">
|
||||
Uninstall Service
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="error.debug">
|
||||
<ion-item-divider>Full Stack Trace</ion-item-divider>
|
||||
<div class="code-block ion-margin">
|
||||
<code>{{ error.message }}</code>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-error',
|
||||
templateUrl: 'app-show-error.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowErrorComponent {
|
||||
@Input()
|
||||
manifest!: T.Manifest
|
||||
|
||||
@Input()
|
||||
error!: T.MainStatus & { main: 'error' }
|
||||
|
||||
constructor(
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly standardActionsService: StandardActionsService,
|
||||
) {}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
const success = await copyToClipboard(text)
|
||||
const message = success
|
||||
? 'Copied to clipboard!'
|
||||
: 'Failed to copy to clipboard.'
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async rebuild() {
|
||||
return this.standardActionsService.rebuild(this.manifest.id)
|
||||
}
|
||||
|
||||
async tryUninstall() {
|
||||
return this.standardActionsService.tryUninstall(this.manifest)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div
|
||||
*ngIf="!caTrusted; else trusted"
|
||||
tuiCardLarge
|
||||
tuiSurface="elevated"
|
||||
tuiSurface="floating"
|
||||
class="card"
|
||||
>
|
||||
<tui-icon icon="@tui.lock" [style.font-size.rem]="4" />
|
||||
@@ -72,7 +72,7 @@
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
(click)="launchHttps()"
|
||||
[disabled]="caTrusted"
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template #trusted>
|
||||
<div tuiCardLarge tuiSurface="elevated" class="card">
|
||||
<div tuiCardLarge tuiSurface="floating" class="card">
|
||||
<tui-icon
|
||||
icon="@tui.shield"
|
||||
tuiAppearance="icon-success"
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface FormContext<T> {
|
||||
<button
|
||||
*ngIf="button.handler; else link"
|
||||
tuiButton
|
||||
[appearance]="last ? 'primary' : 'flat'"
|
||||
[appearance]="last ? 'primary' : 'flat-grayscale'"
|
||||
[type]="last ? 'submit' : 'button'"
|
||||
(click)="onClick(button.handler)"
|
||||
>
|
||||
@@ -60,7 +60,7 @@ export interface FormContext<T> {
|
||||
<ng-template #link>
|
||||
<a
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
[routerLink]="button.link"
|
||||
(click)="close()"
|
||||
>
|
||||
|
||||
@@ -22,6 +22,6 @@
|
||||
[(ngModel)]="value"
|
||||
(click.stop)="(0)"
|
||||
/>
|
||||
<tui-icon icon="@tui.paint" tuiAppearance="icon" class="icon" />
|
||||
<tui-icon icon="@tui.paint-bucket" tuiAppearance="icon" class="icon" />
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
position: absolute;
|
||||
height: 0.3rem;
|
||||
width: 1.4rem;
|
||||
bottom: 0.125rem;
|
||||
bottom: -0.25rem;
|
||||
background: currentColor;
|
||||
border-radius: 0.125rem;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
size="s"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
OnDestroy,
|
||||
} from '@angular/core'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { TuiDialogContext, TuiButton } from '@taiga-ui/core'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -44,8 +44,7 @@ import { TuiDialogContext, TuiButton } from '@taiga-ui/core'
|
||||
})
|
||||
export class HeaderSnekComponent implements AfterViewInit, OnDestroy {
|
||||
private readonly document = inject(DOCUMENT)
|
||||
private readonly dialog =
|
||||
inject<TuiDialogContext<number, number>>(POLYMORPHEUS_CONTEXT)
|
||||
private readonly dialog = injectContext<TuiDialogContext<number, number>>()
|
||||
|
||||
highScore: number = this.dialog.data
|
||||
score = 0
|
||||
|
||||
@@ -25,7 +25,7 @@ import { AddressDetails } from './interface.utils'
|
||||
*ngIf="network$ | async as network"
|
||||
clearnetAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="elevated"
|
||||
tuiSurface="floating"
|
||||
[network]="network"
|
||||
[addresses]="serviceInterface.addresses.clearnet"
|
||||
>
|
||||
@@ -46,7 +46,7 @@ import { AddressDetails } from './interface.utils'
|
||||
<app-address-group
|
||||
torAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="elevated"
|
||||
tuiSurface="floating"
|
||||
[addresses]="serviceInterface.addresses.tor"
|
||||
>
|
||||
<em>
|
||||
@@ -66,7 +66,7 @@ import { AddressDetails } from './interface.utils'
|
||||
<app-address-group
|
||||
localAddresses
|
||||
tuiCardLarge="compact"
|
||||
tuiSurface="elevated"
|
||||
tuiSurface="floating"
|
||||
[addresses]="serviceInterface.addresses.local"
|
||||
>
|
||||
<em>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<footer class="footer">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.circle-arrow-down"
|
||||
(click)="setScroll(true); scrollToBottom()"
|
||||
>
|
||||
@@ -57,7 +57,7 @@
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.download"
|
||||
[logsDownload]="fetchLogs"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
@Component({
|
||||
@@ -11,6 +11,5 @@ import { QrCodeModule } from 'ng-qrcode'
|
||||
imports: [QrCodeModule],
|
||||
})
|
||||
export class QRModal {
|
||||
readonly context =
|
||||
inject<TuiDialogContext<void, string>>(POLYMORPHEUS_CONTEXT)
|
||||
readonly context = injectContext<TuiDialogContext<void, string>>()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { map } from 'rxjs'
|
||||
import { UILaunchComponent } from 'src/app/routes/portal/routes/dashboard/ui.component'
|
||||
import { ActionsService } from 'src/app/services/actions.service'
|
||||
import { ControlsService } from 'src/app/services/controls.service'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
@@ -26,7 +26,7 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.square"
|
||||
(click)="actions.stop(manifest())"
|
||||
(click)="controls.stop(manifest())"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
@@ -35,7 +35,7 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
tuiIconButton
|
||||
iconStart="@tui.rotate-cw"
|
||||
[disabled]="status().primary !== 'running'"
|
||||
(click)="actions.restart(manifest())"
|
||||
(click)="controls.restart(manifest())"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
@@ -45,18 +45,10 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
tuiIconButton
|
||||
iconStart="@tui.play"
|
||||
[disabled]="status().primary !== 'stopped'"
|
||||
(click)="actions.start(manifest(), !!hasUnmet)"
|
||||
(click)="controls.start(manifest(), !!hasUnmet)"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.wrench"
|
||||
(click)="actions.configure(manifest())"
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
}
|
||||
|
||||
<app-ui-launch [pkg]="pkg()" />
|
||||
@@ -80,10 +72,9 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
})
|
||||
export class ControlsComponent {
|
||||
private readonly errors = inject(DepErrorService)
|
||||
readonly actions = inject(ActionsService)
|
||||
|
||||
readonly controls = inject(ControlsService)
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
|
||||
readonly status = computed(() => renderPkgStatus(this.pkg()))
|
||||
readonly running = computed(() => RUNNING.includes(this.status().primary))
|
||||
readonly manifest = computed(() => getManifest(this.pkg()))
|
||||
|
||||
@@ -21,7 +21,7 @@ import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th [style.width.rem]="13">Status</th>
|
||||
<th [style.width.rem]="8" [style.text-indent.rem]="1">Controls</th>
|
||||
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">Controls</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -58,14 +58,7 @@ export class StatusComponent {
|
||||
hasDepErrors = false
|
||||
|
||||
get healthy(): boolean {
|
||||
const status = this.getStatus(this.pkg)
|
||||
|
||||
return (
|
||||
!this.hasDepErrors && // no deps error
|
||||
// @TODO Matt how do we handle this now?
|
||||
// !!this.pkg.status.configured && // no config needed
|
||||
status.health !== 'failure' // no health issues
|
||||
)
|
||||
return !this.hasDepErrors && this.getStatus(this.pkg).health !== 'failure'
|
||||
}
|
||||
|
||||
get loading(): boolean {
|
||||
@@ -87,9 +80,8 @@ export class StatusComponent {
|
||||
return 'Running'
|
||||
case 'stopped':
|
||||
return 'Stopped'
|
||||
// @TODO Matt just dropping this?
|
||||
// case 'needsConfig':
|
||||
// return 'Needs Config'
|
||||
case 'actionRequired':
|
||||
return 'Action Required'
|
||||
case 'updating':
|
||||
return 'Updating...'
|
||||
case 'stopping':
|
||||
@@ -113,9 +105,8 @@ export class StatusComponent {
|
||||
switch (this.getStatus(this.pkg).primary) {
|
||||
case 'running':
|
||||
return 'var(--tui-status-positive)'
|
||||
// @TODO Matt just dropping this?
|
||||
// case 'needsConfig':
|
||||
// return 'var(--tui-status-warning)'
|
||||
case 'actionRequired':
|
||||
return 'var(--tui-status-warning)'
|
||||
case 'installing':
|
||||
case 'updating':
|
||||
case 'stopping':
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getDepDetails } from 'src/app/utils/dep-info'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
export type ActionRequest = T.ActionRequest & {
|
||||
actionName: string
|
||||
dependency: {
|
||||
title: string
|
||||
icon: string
|
||||
} | null
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'button[actionRequest]',
|
||||
template: `
|
||||
<tui-icon class="g-warning" [icon]="icon" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ actionRequest.actionName }}</strong>
|
||||
@if (actionRequest.dependency) {
|
||||
<span tuiSubtitle>
|
||||
<strong>Service:</strong>
|
||||
<img
|
||||
alt=""
|
||||
[src]="actionRequest.dependency.icon"
|
||||
[style.width.rem]="1"
|
||||
/>
|
||||
{{ actionRequest.dependency.title }}
|
||||
</span>
|
||||
}
|
||||
<span tuiSubtitle>
|
||||
{{ actionRequest.reason || 'no reason provided' }}
|
||||
</span>
|
||||
</span>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceActionRequestComponent {
|
||||
private readonly actionService = inject(ActionService)
|
||||
|
||||
@Input({ required: true })
|
||||
actionRequest!: ActionRequest
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
allPkgs!: Record<string, PackageDataEntry>
|
||||
|
||||
get icon(): string {
|
||||
return this.actionRequest.severity === 'critical'
|
||||
? '@tui.triangle-alert'
|
||||
: '@tui.play'
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
async handleAction() {
|
||||
const { id, title } = getManifest(this.pkg)
|
||||
const { actionId, packageId } = this.actionRequest
|
||||
const details = getDepDetails(this.pkg, this.allPkgs, packageId)
|
||||
const self = packageId === id
|
||||
|
||||
this.actionService.present({
|
||||
pkgInfo: {
|
||||
id: packageId,
|
||||
title: self ? title : details.title,
|
||||
mainStatus: self
|
||||
? this.pkg.status.main
|
||||
: this.allPkgs[packageId].status.main,
|
||||
icon: self ? this.pkg.icon : details.icon,
|
||||
},
|
||||
actionInfo: {
|
||||
id: actionId,
|
||||
metadata: self
|
||||
? this.pkg.actions[actionId]
|
||||
: this.allPkgs[packageId].actions[actionId],
|
||||
},
|
||||
requestInfo: {
|
||||
request: this.actionRequest,
|
||||
dependentId: self ? undefined : id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,41 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
|
||||
interface ActionItem {
|
||||
readonly icon: string
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly icon?: string
|
||||
readonly visibility?: T.ActionVisibility
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: '[action]',
|
||||
template: `
|
||||
<tui-icon [icon]="action.icon" />
|
||||
<div>
|
||||
<tui-icon [icon]="action.icon || '@tui.circle-play'" />
|
||||
<div tuiTitle>
|
||||
<strong>{{ action.name }}</strong>
|
||||
<div>{{ action.description }}</div>
|
||||
<div tuiSubtitle>{{ action.description }}</div>
|
||||
@if (disabled) {
|
||||
<div tuiSubtitle class="g-warning">{{ disabled }}</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon],
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
host: {
|
||||
'[disabled]': '!!disabled',
|
||||
},
|
||||
})
|
||||
export class ServiceActionComponent {
|
||||
@Input({ required: true })
|
||||
action!: ActionItem
|
||||
|
||||
get disabled() {
|
||||
return (
|
||||
typeof this.action.visibility === 'object' &&
|
||||
this.action.visibility.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { T } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { DependencyInfo } from 'src/app/routes/portal/routes/service/types/dependency-info'
|
||||
import { ActionsService } from 'src/app/services/actions.service'
|
||||
import { ControlsService } from '../../../../../services/controls.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PackageStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
@@ -49,17 +49,6 @@ const STOPPABLE = ['running', 'starting', 'restarting']
|
||||
Start
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (canConfigure) {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary-warning"
|
||||
iconStart="@tui.wrench"
|
||||
(click)="actions.configure(manifest)"
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
@@ -84,7 +73,7 @@ export class ServiceActionsComponent {
|
||||
status: PackageStatus
|
||||
}
|
||||
|
||||
readonly actions = inject(ActionsService)
|
||||
readonly actions = inject(ControlsService)
|
||||
|
||||
get manifest(): T.Manifest {
|
||||
return getManifest(this.service.pkg)
|
||||
@@ -95,19 +84,13 @@ export class ServiceActionsComponent {
|
||||
}
|
||||
|
||||
get canStart(): boolean {
|
||||
return this.service.status.primary === 'stopped' && !this.canConfigure
|
||||
return this.service.status.primary === 'stopped'
|
||||
}
|
||||
|
||||
get canRestart(): boolean {
|
||||
return this.service.status.primary === 'running'
|
||||
}
|
||||
|
||||
get canConfigure(): boolean {
|
||||
// @TODO Matt should we just drop this?
|
||||
// return !this.service.pkg.status.configured
|
||||
return false
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
hasUnmet(dependencies: readonly DependencyInfo[]): boolean {
|
||||
return dependencies.some(dep => !!dep.errorText)
|
||||
|
||||
@@ -6,14 +6,8 @@ import { ServiceDependencyComponent } from './dependency.component'
|
||||
selector: 'service-dependencies',
|
||||
template: `
|
||||
@for (dep of dependencies; track $index) {
|
||||
<button
|
||||
class="g-action"
|
||||
[serviceDependency]="dep"
|
||||
(click)="dep.action()"
|
||||
></button>
|
||||
}
|
||||
|
||||
@if (!dependencies.length) {
|
||||
<button [serviceDependency]="dep"></button>
|
||||
} @empty {
|
||||
No dependencies
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceDependency]',
|
||||
template: `
|
||||
<img [src]="dep.icon" alt="" />
|
||||
<span [style.flex]="1">
|
||||
<span tuiTitle>
|
||||
<strong>
|
||||
@if (dep.errorText) {
|
||||
<tui-icon icon="@tui.triangle-alert" [style.color]="color" />
|
||||
}
|
||||
{{ dep.title }}
|
||||
</strong>
|
||||
<div>{{ dep.version }}</div>
|
||||
<div [style.color]="color">{{ dep.errorText || 'Satisfied' }}</div>
|
||||
<span tuiSubtitle>{{ dep.version }}</span>
|
||||
<span tuiSubtitle="" [style.color]="color">
|
||||
{{ dep.errorText || 'Satisfied' }}
|
||||
</span>
|
||||
</span>
|
||||
@if (dep.actionText) {
|
||||
<div>
|
||||
<span>
|
||||
{{ dep.actionText }}
|
||||
<tui-icon icon="@tui.arrow-right" />
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
@@ -38,7 +41,11 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon],
|
||||
host: {
|
||||
'(click)': 'dep.action()',
|
||||
},
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceDependencyComponent {
|
||||
@Input({ required: true, alias: 'serviceDependency' })
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { copyToClipboard } from '@start9labs/shared'
|
||||
import {
|
||||
TuiAlertService,
|
||||
TuiButton,
|
||||
TuiDialogService,
|
||||
TuiIcon,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiLineClamp, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'service-error',
|
||||
template: `
|
||||
<tui-line-clamp
|
||||
style="pointer-events: none; margin: 1rem 0 -1rem; color: var(--tui-status-negative);"
|
||||
[linesLimit]="2"
|
||||
[content]="error?.message"
|
||||
(overflownChange)="overflow = $event"
|
||||
/>
|
||||
<h4 class="g-title">
|
||||
<span [style.display]="'flex'">
|
||||
Actions
|
||||
<tui-icon [style.margin-left.rem]="0.25" [tuiTooltip]="hint" />
|
||||
</span>
|
||||
</h4>
|
||||
<ng-template #hint>
|
||||
<div>
|
||||
<b>Rebuild Container</b>
|
||||
is harmless action that and only takes a few seconds to complete. It
|
||||
will likely resolve this issue.
|
||||
</div>
|
||||
<b>Uninstall Service</b>
|
||||
is a dangerous action that will remove the service from StartOS and wipe
|
||||
all its data.
|
||||
</ng-template>
|
||||
<p style="display: flex; flex-wrap: wrap; gap: 1rem">
|
||||
<button tuiButton (click)="rebuild()">Rebuild Container</button>
|
||||
<button tuiButton appearance="negative" (click)="uninstall()">
|
||||
Uninstall Service
|
||||
</button>
|
||||
@if (overflow) {
|
||||
<button tuiButton appearance="secondary-grayscale" (click)="show()">
|
||||
View full message
|
||||
</button>
|
||||
}
|
||||
</p>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiIcon, TuiTooltip, TuiLineClamp],
|
||||
})
|
||||
export class ServiceErrorComponent {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly service = inject(StandardActionsService)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
overflow = false
|
||||
|
||||
get error() {
|
||||
return this.pkg.status.main === 'error' ? this.pkg.status : null
|
||||
}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
const success = await copyToClipboard(text)
|
||||
|
||||
this.alerts
|
||||
.open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.', {
|
||||
appearance: success ? 'positive' : 'negative',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
rebuild() {
|
||||
this.service.rebuild(getManifest(this.pkg).id)
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
this.service.uninstall(getManifest(this.pkg))
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialogs
|
||||
.open(this.error?.message, { label: 'Service error' })
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'service-health-check',
|
||||
@@ -17,12 +17,12 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
[style.color]="color"
|
||||
/>
|
||||
}
|
||||
<div>
|
||||
<span tuiTitle>
|
||||
<strong [class.tui-skeleton]="!connected">{{ check.name }}</strong>
|
||||
<div [class.tui-skeleton]="!connected" [style.color]="color">
|
||||
<span tuiSubtitle [class.tui-skeleton]="!connected" [style.color]="color">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
@@ -38,7 +38,7 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiLoader, TuiIcon],
|
||||
imports: [TuiLoader, TuiIcon, TuiTitle],
|
||||
})
|
||||
export class ServiceHealthCheckComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ConnectionService } from 'src/app/services/connection.service'
|
||||
template: `
|
||||
@for (check of checks; track $index) {
|
||||
<service-health-check
|
||||
class="g-action"
|
||||
[check]="check"
|
||||
[connected]="!!(connected$ | async)"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiLoader, TuiIcon, TuiButton } from '@taiga-ui/core'
|
||||
import { TuiLoader, TuiIcon, TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { map, timer } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
@@ -23,26 +24,31 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
||||
} @else {
|
||||
<tui-icon icon="@tui.circle-x" class="g-error" />
|
||||
}
|
||||
<div [style.flex]="1">
|
||||
<span tuiTitle>
|
||||
<strong>{{ info.name }}</strong>
|
||||
<div>{{ info.description }}</div>
|
||||
<span tuiSubtitle>{{ info.description }}</span>
|
||||
@if (check) {
|
||||
<div class="g-error">
|
||||
<b>Health check failed:</b>
|
||||
{{ check }}
|
||||
</div>
|
||||
<span tuiSubtitle class="g-error">
|
||||
<span>
|
||||
<b>Health check failed:</b>
|
||||
{{ check }}
|
||||
</span>
|
||||
</span>
|
||||
} @else {
|
||||
<div [style.color]="info.color">{{ info.typeDetail }}</div>
|
||||
<span tuiSubtitle [style.color]="info.color">
|
||||
{{ info.typeDetail }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
@if (info.type === 'ui') {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.external-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Open"
|
||||
size="m"
|
||||
[style.border-radius.%]="100"
|
||||
[attr.href]="href"
|
||||
(click.stop)="(0)"
|
||||
@@ -52,7 +58,8 @@ import { ExtendedInterfaceInfo } from '../pipes/interface-info.pipe'
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiButton, TuiLet, TuiLoader, TuiIcon],
|
||||
imports: [CommonModule, TuiButton, TuiLet, TuiLoader, TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceInterfaceListItemComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
@@ -73,7 +80,7 @@ export class ServiceInterfaceListItemComponent {
|
||||
|
||||
get href(): string | null {
|
||||
return this.disabled
|
||||
? null
|
||||
? 'null'
|
||||
: this.config.launchableAddress(this.info, this.pkg.hosts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ServiceInterfaceListItemComponent } from './interface-list-item.compone
|
||||
template: `
|
||||
@for (info of pkg | interfaceInfo; track $index) {
|
||||
<a
|
||||
class="g-action"
|
||||
serviceInterfaceListItem
|
||||
[info]="info"
|
||||
[pkg]="pkg"
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { ServiceMenu } from '../pipes/to-menu.pipe'
|
||||
|
||||
@Component({
|
||||
selector: '[serviceMenuItem]',
|
||||
template: `
|
||||
<tui-icon [icon]="menu.icon" />
|
||||
<div [style.flex]="1">
|
||||
<span tuiTitle [style.flex]="1">
|
||||
<strong>{{ menu.name }}</strong>
|
||||
<div>
|
||||
<span tuiSubtitle>
|
||||
{{ menu.description }}
|
||||
<ng-content />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<ng-content />
|
||||
</span>
|
||||
<tui-icon icon="@tui.chevron-right" />
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon],
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
hostDirectives: [TuiCell],
|
||||
})
|
||||
export class ServiceMenuItemComponent {
|
||||
@Input({ required: true, alias: 'serviceMenuItem' })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ToMenuPipe } from '../pipes/to-menu.pipe'
|
||||
import { ServiceMenuItemComponent } from './menu-item.component'
|
||||
import { RouterLink } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'service-menu',
|
||||
@@ -10,19 +10,16 @@ import { RouterLink } from '@angular/router'
|
||||
@for (menu of pkg | toMenu; track $index) {
|
||||
@if (menu.routerLink) {
|
||||
<a
|
||||
class="g-action"
|
||||
[serviceMenuItem]="menu"
|
||||
[routerLink]="menu.routerLink"
|
||||
[queryParams]="menu.params || {}"
|
||||
></a>
|
||||
} @else {
|
||||
<button
|
||||
class="g-action"
|
||||
[serviceMenuItem]="menu"
|
||||
(click)="menu.action?.()"
|
||||
>
|
||||
<button [serviceMenuItem]="menu" (click)="menu.action?.()">
|
||||
@if (menu.name === 'Outbound Proxy') {
|
||||
<div [style.color]="color">{{ pkg.outboundProxy || 'None' }}</div>
|
||||
<div tuiSubtitle [style.color]="color">
|
||||
{{ pkg.outboundProxy || 'None' }}
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pip
|
||||
selector: '[progress]',
|
||||
template: `
|
||||
<ng-content />
|
||||
@if (progress | installingProgress; as decimal) {
|
||||
: {{ decimal * 100 }}%
|
||||
@if (progress | installingProgress; as percent) {
|
||||
: {{ percent }}%
|
||||
<progress
|
||||
tuiProgressBar
|
||||
size="xs"
|
||||
@@ -17,7 +17,7 @@ import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pip
|
||||
? 'var(--tui-text-positive)'
|
||||
: 'var(--tui-text-action)'
|
||||
"
|
||||
[value]="decimal * 100"
|
||||
[value]="percent / 100"
|
||||
></progress>
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLabel, TuiTitle } from '@taiga-ui/core'
|
||||
import { mask } from 'src/app/utils/mask'
|
||||
|
||||
@Component({
|
||||
selector: 'service-property',
|
||||
template: `
|
||||
<label [style.flex]="1" tuiTitle>
|
||||
<span tuiSubtitle>{{ label }}</span>
|
||||
{{ masked ? mask : value }}
|
||||
</label>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
>
|
||||
Toggle
|
||||
</button>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat"
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(value)"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
box-shadow: 0 1px var(--tui-background-neutral-1);
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiButton, TuiLabel, TuiTitle],
|
||||
})
|
||||
export class ServicePropertyComponent {
|
||||
@Input()
|
||||
label = ''
|
||||
|
||||
@Input()
|
||||
value = ''
|
||||
|
||||
masked = true
|
||||
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
get mask(): string {
|
||||
return mask(this.value, 64)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { getErrorMessage } from '@start9labs/shared'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiLoader,
|
||||
TuiNotification,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import * as json from 'fast-json-patch'
|
||||
import { compare } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs'
|
||||
import {
|
||||
ActionButton,
|
||||
FormComponent,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { ActionRequestInfoComponent } from 'src/app/routes/portal/modals/config-dep.component'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||
import { InvalidService } from '../../../components/form/invalid.service'
|
||||
|
||||
export type PackageActionData = {
|
||||
pkgInfo: {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
mainStatus: T.MainStatus['main']
|
||||
}
|
||||
actionInfo: {
|
||||
id: string
|
||||
metadata: T.ActionMetadata
|
||||
}
|
||||
requestInfo?: {
|
||||
dependentId?: string
|
||||
request: T.ActionRequest
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div class="service-title">
|
||||
<img [src]="pkgInfo.icon" alt="" />
|
||||
<h4>{{ pkgInfo.title }}</h4>
|
||||
</div>
|
||||
@if (res$ | async; as res) {
|
||||
@if (error) {
|
||||
<tui-notification appearance="negative">
|
||||
<div [innerHTML]="error"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
|
||||
@if (warning) {
|
||||
<tui-notification appearance="warning">
|
||||
<div [innerHTML]="warning"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
|
||||
@if (requestInfo) {
|
||||
<action-request-info
|
||||
[originalValue]="res.originalValue || {}"
|
||||
[operations]="res.operations || []"
|
||||
/>
|
||||
}
|
||||
|
||||
<app-form
|
||||
[spec]="res.spec"
|
||||
[value]="res.originalValue || {}"
|
||||
[buttons]="buttons"
|
||||
[operations]="res.operations || []"
|
||||
>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat-grayscale"
|
||||
type="reset"
|
||||
[style.margin-right]="'auto'"
|
||||
>
|
||||
Reset Defaults
|
||||
</button>
|
||||
</app-form>
|
||||
} @else {
|
||||
<tui-loader size="l" textContent="loading" />
|
||||
}
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
.service-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.4rem;
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
TuiNotification,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
ActionRequestInfoComponent,
|
||||
FormComponent,
|
||||
],
|
||||
providers: [InvalidService],
|
||||
})
|
||||
export class ActionInputModal {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly actionService = inject(ActionService)
|
||||
private readonly context =
|
||||
injectContext<TuiDialogContext<void, PackageActionData>>()
|
||||
|
||||
readonly actionId = this.context.data.actionInfo.id
|
||||
readonly warning = this.context.data.actionInfo.metadata.warning
|
||||
readonly pkgInfo = this.context.data.pkgInfo
|
||||
readonly requestInfo = this.context.data.requestInfo
|
||||
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
text: 'Submit',
|
||||
handler: value => this.execute(value),
|
||||
},
|
||||
]
|
||||
|
||||
error = ''
|
||||
|
||||
res$ = defer(() =>
|
||||
this.api.getActionInput({
|
||||
packageId: this.pkgInfo.id,
|
||||
actionId: this.actionId,
|
||||
}),
|
||||
).pipe(
|
||||
map(res => {
|
||||
const originalValue = res.value || {}
|
||||
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.request.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
utils.deepMerge(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.request.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
this.error = String(getErrorMessage(e))
|
||||
return EMPTY
|
||||
}),
|
||||
)
|
||||
|
||||
async execute(input: object) {
|
||||
if (await this.checkConflicts(input)) {
|
||||
return this.actionService.execute(this.pkgInfo.id, this.actionId, input)
|
||||
}
|
||||
}
|
||||
|
||||
private async checkConflicts(input: object): Promise<boolean> {
|
||||
const packages = await getAllPackages(this.patch)
|
||||
|
||||
const breakages = Object.keys(packages)
|
||||
.filter(
|
||||
id =>
|
||||
id !== this.pkgInfo.id &&
|
||||
Object.values(packages[id].requestedActions).some(
|
||||
({ request, active }) =>
|
||||
!active &&
|
||||
request.severity === 'critical' &&
|
||||
request.packageId === this.pkgInfo.id &&
|
||||
request.actionId === this.actionId &&
|
||||
request.when?.condition === 'input-not-matches' &&
|
||||
request.input &&
|
||||
json
|
||||
.compare(input, request.input)
|
||||
.some(op => op.op === 'add' || op.op === 'replace'),
|
||||
),
|
||||
)
|
||||
.map(id => id)
|
||||
|
||||
if (!breakages.length) return true
|
||||
|
||||
const message =
|
||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
||||
const content = `${message}${breakages.map(
|
||||
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
|
||||
)}</ul>`
|
||||
const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }
|
||||
|
||||
return firstValueFrom(
|
||||
this.dialogs.open<boolean>(TUI_CONFIRM, { data }).pipe(endWith(false)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import { ToAdditionalPipe } from 'src/app/routes/portal/routes/service/pipes/to-additional.pipe'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { ServiceAdditionalItemComponent } from './additional-item.component'
|
||||
|
||||
@Component({
|
||||
@@ -31,6 +26,5 @@ import { ServiceAdditionalItemComponent } from './additional-item.component'
|
||||
imports: [ToAdditionalPipe, ServiceAdditionalItemComponent],
|
||||
})
|
||||
export class ServiceAdditionalModal {
|
||||
readonly pkg =
|
||||
inject<TuiDialogOptions<PackageDataEntry>>(POLYMORPHEUS_CONTEXT).data
|
||||
readonly pkg = injectContext<TuiDialogOptions<PackageDataEntry>>().data
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { TuiLoader, TuiButton } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ServicePropertyComponent } from '../components/property.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (loading$ | async) {
|
||||
<tui-loader />
|
||||
} @else {
|
||||
@for (prop of properties | keyvalue: asIsOrder; track prop) {
|
||||
<service-property [label]="prop.key" [value]="prop.value" />
|
||||
} @empty {
|
||||
No properties
|
||||
}
|
||||
}
|
||||
<button tuiButton iconStart="@tui.refresh-cw" (click)="refresh()">
|
||||
Refresh
|
||||
</button>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
button {
|
||||
float: right;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiButton, ServicePropertyComponent, TuiLoader],
|
||||
})
|
||||
export class ServicePropertiesModal {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
readonly id = inject<{ data: string }>(POLYMORPHEUS_CONTEXT).data
|
||||
readonly loading$ = new BehaviorSubject(true)
|
||||
|
||||
properties: Record<string, string> = {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.getProperties()
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.getProperties()
|
||||
}
|
||||
|
||||
private async getProperties(): Promise<void> {
|
||||
this.loading$.next(true)
|
||||
|
||||
try {
|
||||
// @TODO Matt this needs complete rework, right?
|
||||
// this.properties = await this.api.getPackageProperties({ id: this.id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading$.next(false)
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { WithId } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'groupActions',
|
||||
standalone: true,
|
||||
})
|
||||
export class GroupActionsPipe implements PipeTransform {
|
||||
transform(
|
||||
actions: PackageDataEntry['actions'],
|
||||
): Array<Array<WithId<T.ActionMetadata>>> | null {
|
||||
if (!actions) return null
|
||||
|
||||
const noGroup = 'noGroup'
|
||||
const grouped = Object.entries(actions).reduce<
|
||||
Record<string, WithId<T.ActionMetadata>[]>
|
||||
>((groups, [id, action]) => {
|
||||
const actionWithId = { id, ...action }
|
||||
const groupKey = action.group || noGroup
|
||||
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = [actionWithId]
|
||||
} else {
|
||||
groups[groupKey].push(actionWithId)
|
||||
}
|
||||
|
||||
return groups
|
||||
}, {})
|
||||
|
||||
return Object.values(grouped).map(group =>
|
||||
group.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getDepDetails } from 'src/app/utils/dep-info'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ActionRequest } from '../components/action-request.component'
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'toActionRequests',
|
||||
})
|
||||
export class ToActionRequestsPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry, packages: Record<string, PackageDataEntry>) {
|
||||
const { id } = getManifest(pkg)
|
||||
const critical: ActionRequest[] = []
|
||||
const important: ActionRequest[] = []
|
||||
|
||||
Object.values(pkg.requestedActions)
|
||||
.filter(r => r.active)
|
||||
.forEach(r => {
|
||||
const self = r.request.packageId === id
|
||||
const toReturn = {
|
||||
...r.request,
|
||||
actionName: self
|
||||
? pkg.actions[r.request.actionId].name
|
||||
: packages[r.request.packageId]?.actions[r.request.actionId].name ||
|
||||
'Unknown Action',
|
||||
dependency: self
|
||||
? null
|
||||
: getDepDetails(pkg, packages, r.request.packageId),
|
||||
}
|
||||
|
||||
if (r.request.severity === 'critical') {
|
||||
critical.push(toReturn)
|
||||
} else {
|
||||
important.push(toReturn)
|
||||
}
|
||||
})
|
||||
|
||||
return { critical, important }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { CopyService, MarkdownComponent } from '@start9labs/shared'
|
||||
import { CopyService, MARKDOWN } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { from } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
@@ -70,7 +69,7 @@ export class ToAdditionalPipe implements PipeTransform {
|
||||
|
||||
private showLicense({ id, version }: T.Manifest) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
.open(MARKDOWN, {
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: {
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Params } from '@angular/router'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import { MARKDOWN } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { from } from 'rxjs'
|
||||
// @TODO Alex implement config
|
||||
// import {
|
||||
// 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 { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ServicePropertiesModal } from 'src/app/routes/portal/routes/service/modals/properties.component'
|
||||
|
||||
export interface ServiceMenu {
|
||||
icon: string
|
||||
@@ -47,24 +41,6 @@ export class ToMenuPipe implements PipeTransform {
|
||||
description: `Understand how to use ${manifest.title}`,
|
||||
action: () => this.showInstructions(manifest),
|
||||
},
|
||||
{
|
||||
icon: '@tui.sliders-vertical',
|
||||
name: 'Config',
|
||||
description: `Customize ${manifest.title}`,
|
||||
action: () => this.openConfig(manifest),
|
||||
},
|
||||
{
|
||||
icon: '@tui.key',
|
||||
name: 'Properties',
|
||||
description: `Runtime information, credentials, and other values of interest`,
|
||||
action: () =>
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(ServicePropertiesModal), {
|
||||
label: `${manifest.title} credentials`,
|
||||
data: manifest.id,
|
||||
})
|
||||
.subscribe(),
|
||||
},
|
||||
{
|
||||
icon: '@tui.zap',
|
||||
name: 'Actions',
|
||||
@@ -121,7 +97,7 @@ export class ToMenuPipe implements PipeTransform {
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
.open(MARKDOWN, {
|
||||
label: `${title} instructions`,
|
||||
size: 'l',
|
||||
data: {
|
||||
@@ -130,11 +106,4 @@ export class ToMenuPipe implements PipeTransform {
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private openConfig({ title, id }: T.Manifest) {
|
||||
// this.formDialog.open<PackageConfigData>(ConfigModal, {
|
||||
// label: `${title} configuration`,
|
||||
// data: { pkgId: id },
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,215 +1,99 @@
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
isEmptyObject,
|
||||
WithId,
|
||||
ErrorService,
|
||||
LoadingService,
|
||||
getPkgId,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, switchMap, timer } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
import { ActionSuccessPage } from '../modals/action-success/action-success.page'
|
||||
import { GroupActionsPipe } from '../pipes/group-actions.pipe'
|
||||
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (pkg$ | async; as pkg) {
|
||||
@if (package(); as pkg) {
|
||||
<section>
|
||||
<h3 class="g-title">Standard Actions</h3>
|
||||
<button
|
||||
class="g-action"
|
||||
[action]="action"
|
||||
(click)="tryUninstall(pkg)"
|
||||
[action]="rebuild"
|
||||
(click)="service.rebuild(pkg.manifest.id)"
|
||||
></button>
|
||||
<button
|
||||
class="g-action"
|
||||
[action]="uninstall"
|
||||
(click)="service.uninstall(pkg.manifest)"
|
||||
></button>
|
||||
</section>
|
||||
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
|
||||
<h3 *ngIf="actionGroups.length" class="g-title">
|
||||
Actions for {{ (pkg | toManifest).title }}
|
||||
</h3>
|
||||
<div *ngFor="let group of actionGroups">
|
||||
@if (pkg.actions.length) {
|
||||
<h3 class="g-title">Actions for {{ pkg.manifest.title }}</h3>
|
||||
}
|
||||
@for (action of pkg.actions; track $index) {
|
||||
@if (action.visibility !== 'hidden') {
|
||||
<button
|
||||
*ngFor="let action of group"
|
||||
class="g-action"
|
||||
[action]="{
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
icon: '@tui.circle-play',
|
||||
}"
|
||||
(click)="handleAction(action)"
|
||||
[action]="action"
|
||||
(click)="
|
||||
handleAction(pkg.mainStatus, pkg.icon, pkg.manifest, action)
|
||||
"
|
||||
></button>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ServiceActionComponent,
|
||||
GroupActionsPipe,
|
||||
ToManifestPipe,
|
||||
],
|
||||
imports: [ServiceActionComponent],
|
||||
})
|
||||
export class ServiceActionsRoute {
|
||||
private readonly id = getPkgId(inject(ActivatedRoute))
|
||||
private readonly actions = inject(ActionService)
|
||||
|
||||
readonly pkg$ = this.patch
|
||||
.watch$('packageData', this.id)
|
||||
.pipe(filter(pkg => pkg.stateInfo.state === 'installed'))
|
||||
readonly service = inject(StandardActionsService)
|
||||
readonly package = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData', getPkgId())
|
||||
.pipe(
|
||||
filter(pkg => pkg.stateInfo.state === 'installed'),
|
||||
map(pkg => ({
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.keys(pkg.actions).map(id => ({
|
||||
id,
|
||||
...pkg.actions[id],
|
||||
})),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
readonly action = {
|
||||
icon: '@tui.trash-2',
|
||||
name: 'Uninstall',
|
||||
description:
|
||||
'This will uninstall the service from StartOS and delete all data permanently.',
|
||||
}
|
||||
readonly rebuild = REBUILD
|
||||
readonly uninstall = UNINSTALL
|
||||
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly router: Router,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
async handleAction(action: WithId<T.ActionMetadata>) {
|
||||
// @TODO Matt this needs complete rework, right?
|
||||
// if (action.disabled) {
|
||||
// this.dialogs
|
||||
// .open(action.disabled, {
|
||||
// label: 'Forbidden',
|
||||
// size: 's',
|
||||
// })
|
||||
// .subscribe()
|
||||
// } else {
|
||||
// if (action.input && !isEmptyObject(action.input)) {
|
||||
// this.formDialog.open(FormComponent, {
|
||||
// label: action.name,
|
||||
// data: {
|
||||
// spec: action.input,
|
||||
// buttons: [
|
||||
// {
|
||||
// text: 'Execute',
|
||||
// handler: async (value: any) =>
|
||||
// this.executeAction(action.id, value),
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// })
|
||||
// } else {
|
||||
// this.dialogs
|
||||
// .open(TUI_CONFIRM, {
|
||||
// label: 'Confirm',
|
||||
// size: 's',
|
||||
// data: {
|
||||
// content: `Are you sure you want to execute action "${
|
||||
// action.name
|
||||
// }"? ${action.warning || ''}`,
|
||||
// yes: 'Execute',
|
||||
// no: 'Cancel',
|
||||
// },
|
||||
// })
|
||||
// .pipe(filter(Boolean))
|
||||
// .subscribe(() => this.executeAction(action.id))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
||||
const { title, alerts, id } = getManifest(pkg)
|
||||
|
||||
let content =
|
||||
alerts.uninstall ||
|
||||
`Uninstalling ${title} will permanently delete its data`
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content,
|
||||
yes: 'Uninstall',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.uninstall())
|
||||
}
|
||||
|
||||
private async uninstall() {
|
||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.uninstallPackage({ id: this.id })
|
||||
this.embassyApi
|
||||
.setDbValue<boolean>(['ack-instructions', this.id], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.router.navigate(['./portal/dashboard'])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(
|
||||
actionId: string,
|
||||
input?: object,
|
||||
): Promise<boolean> {
|
||||
const loader = this.loader.open('Executing action...').subscribe()
|
||||
|
||||
try {
|
||||
// @TODO Matt this needs complete rework, right?
|
||||
// const data = await this.embassyApi.executePackageAction({
|
||||
// id: this.id,
|
||||
// actionId,
|
||||
// input,
|
||||
// })
|
||||
|
||||
timer(500)
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
this.dialogs.open(new PolymorpheusComponent(ActionSuccessPage), {
|
||||
label: 'Execution Complete',
|
||||
// data,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
handleAction(
|
||||
mainStatus: T.MainStatus['main'],
|
||||
icon: string,
|
||||
manifest: T.Manifest,
|
||||
action: T.ActionMetadata & { id: string },
|
||||
) {
|
||||
this.actions.present({
|
||||
pkgInfo: { id: manifest.id, title: manifest.title, icon, mainStatus },
|
||||
actionInfo: { id: action.id, metadata: action },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const REBUILD = {
|
||||
icon: '@tui.wrench',
|
||||
name: 'Rebuild Service',
|
||||
description:
|
||||
'Rebuilds the service container. It is harmless and only takes a few seconds to complete, but it should only be necessary if a StartOS bug is preventing dependencies, interfaces, or actions from synchronizing.',
|
||||
}
|
||||
|
||||
const UNINSTALL = {
|
||||
icon: '@tui.trash-2',
|
||||
name: 'Uninstall',
|
||||
description:
|
||||
'Uninstalls this service from StartOS and delete all data permanently.',
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ import { getMultihostAddresses } from '../../../components/interfaces/interface.
|
||||
imports: [CommonModule, InterfaceComponent],
|
||||
})
|
||||
export class ServiceInterfaceRoute {
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
readonly context = {
|
||||
packageId: getPkgId(this.route),
|
||||
interfaceId: this.route.snapshot.paramMap.get('interfaceId') || '',
|
||||
packageId: getPkgId(),
|
||||
interfaceId:
|
||||
inject(ActivatedRoute).snapshot.paramMap.get('interfaceId') || '',
|
||||
}
|
||||
|
||||
readonly interfacesWithAddresses$ = combineLatest([
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
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'
|
||||
|
||||
@Component({
|
||||
template: '<logs [fetchLogs]="fetch" [followLogs]="follow" [context]="id" />',
|
||||
@@ -15,7 +14,7 @@ import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.compon
|
||||
export class ServiceLogsRoute {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly id = getPkgId(inject(ActivatedRoute))
|
||||
readonly id = getPkgId()
|
||||
|
||||
readonly follow = async (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followPackageLogs({ id: this.id, ...params })
|
||||
|
||||
@@ -5,11 +5,6 @@ import { isEmptyObject } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, switchMap } from 'rxjs'
|
||||
// @TODO Alex implement config
|
||||
// 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'
|
||||
@@ -30,13 +25,16 @@ import {
|
||||
} 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 { ServiceActionRequestComponent } from '../components/action-request.component'
|
||||
import { ServiceActionsComponent } from '../components/actions.component'
|
||||
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
||||
import { ServiceErrorComponent } from '../components/error.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 { ToActionRequestsPipe } from '../pipes/to-action-requests.pipe'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
|
||||
@Component({
|
||||
@@ -61,10 +59,17 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
<service-backups [pkg]="service.pkg" />
|
||||
</section>
|
||||
|
||||
<section [style.grid-column]="'span 6'">
|
||||
<h3>Metrics</h3>
|
||||
TODO
|
||||
</section>
|
||||
@if (service.pkg.status.main === 'error') {
|
||||
<section class="error">
|
||||
<h3>Error</h3>
|
||||
<service-error [pkg]="service.pkg" />
|
||||
</section>
|
||||
} @else {
|
||||
<section [style.grid-column]="'span 6'">
|
||||
<h3>Metrics</h3>
|
||||
TODO
|
||||
</section>
|
||||
}
|
||||
|
||||
<section [style.grid-column]="'span 4'" [style.align-self]="'start'">
|
||||
<h3>Menu</h3>
|
||||
@@ -72,6 +77,34 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
</section>
|
||||
|
||||
<div>
|
||||
@if (service.pkg | toActionRequests: service.allPkgs; as requests) {
|
||||
@if (requests.critical.length) {
|
||||
<section>
|
||||
<h3>Required Actions</h3>
|
||||
@for (request of requests.critical; track $index) {
|
||||
<button
|
||||
[actionRequest]="request"
|
||||
[pkg]="service.pkg"
|
||||
[allPkgs]="service.allPkgs"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (requests.important.length) {
|
||||
<section>
|
||||
<h3>Requested Actions</h3>
|
||||
@for (request of requests.important; track $index) {
|
||||
<button
|
||||
[actionRequest]="request"
|
||||
[pkg]="service.pkg"
|
||||
[allPkgs]="service.allPkgs"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
<section>
|
||||
<h3>Health Checks</h3>
|
||||
<service-health-checks [checks]="(health$ | async) || []" />
|
||||
@@ -124,6 +157,24 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
background: var(--tui-background-neutral-1);
|
||||
box-shadow: inset 0 7rem 0 -4rem var(--tui-background-neutral-1);
|
||||
clip-path: polygon(0 1.5rem, 1.5rem 0, 100% 0, 100% 100%, 0 100%);
|
||||
|
||||
&.error {
|
||||
box-shadow: inset 0 7rem 0 -4rem var(--tui-status-negative-pale);
|
||||
grid-column: span 6;
|
||||
|
||||
h3 {
|
||||
color: var(--tui-status-negative);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep [tuiCell] {
|
||||
width: stretch;
|
||||
margin: 0 -1rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
box-shadow: 0 0.51rem 0 -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -153,6 +204,9 @@ import { DependencyInfo } from '../types/dependency-info'
|
||||
ServiceDependenciesComponent,
|
||||
ServiceMenuComponent,
|
||||
ServiceBackupsComponent,
|
||||
ServiceActionRequestComponent,
|
||||
ServiceErrorComponent,
|
||||
ToActionRequestsPipe,
|
||||
InstallingProgressPipe,
|
||||
],
|
||||
})
|
||||
@@ -169,15 +223,20 @@ export class ServiceRoute {
|
||||
readonly service$ = this.pkgId$.pipe(
|
||||
switchMap(pkgId =>
|
||||
combineLatest([
|
||||
this.patch.watch$('packageData', pkgId),
|
||||
this.patch.watch$('packageData'),
|
||||
this.depErrorService.getPkgDepErrors$(pkgId),
|
||||
]),
|
||||
]).pipe(
|
||||
map(([allPkgs, depErrors]) => {
|
||||
const pkg = allPkgs[pkgId]
|
||||
return {
|
||||
allPkgs,
|
||||
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(
|
||||
@@ -263,11 +322,8 @@ export class ServiceRoute {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkg, pkgManifest, 'update', depId)
|
||||
// @TODO Matt do we just remove this case?
|
||||
// } else if (depError.type === 'configUnsatisfied') {
|
||||
// errorText = 'Config not satisfied'
|
||||
// fixText = 'Auto config'
|
||||
// fixAction = () => this.fixDep(pkg, pkgManifest, 'configure', depId)
|
||||
} else if (depError.type === 'actionRequired') {
|
||||
errorText = 'Action Required (see above)'
|
||||
} else if (depError.type === 'notRunning') {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
|
||||
@@ -51,7 +51,7 @@ interface Package {
|
||||
}
|
||||
</div>
|
||||
<footer class="g-buttons">
|
||||
<button tuiButton appearance="flat" (click)="toggleSelectAll()">
|
||||
<button tuiButton appearance="flat-grayscale" (click)="toggleSelectAll()">
|
||||
Toggle all
|
||||
</button>
|
||||
<button tuiButton [disabled]="!hasSelection" (click)="done()">
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
TuiWrapperModule,
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
} from '@taiga-ui/legacy'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogContext, TuiDialogService, TuiButton } from '@taiga-ui/core'
|
||||
import { TuiButton, TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiBadge, TuiSwitch } from '@taiga-ui/kit'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
TuiWrapperModule,
|
||||
} from '@taiga-ui/legacy'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { from, map } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { BackupJob, BackupTarget } from 'src/app/services/api/api.types'
|
||||
import { TARGET, TARGET_CREATE } from './target.component'
|
||||
import { BACKUP, BACKUP_OPTIONS } from './backup.component'
|
||||
import { BackupJobBuilder } from '../utils/job-builder'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ToHumanCronPipe } from '../pipes/to-human-cron.pipe'
|
||||
import { BackupJobBuilder } from '../utils/job-builder'
|
||||
import { BACKUP, BACKUP_OPTIONS } from './backup.component'
|
||||
import { TARGET, TARGET_CREATE } from './target.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -113,7 +110,7 @@ export class BackupsEditModal {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<BackupJob, BackupJobBuilder>>(POLYMORPHEUS_CONTEXT)
|
||||
injectContext<TuiDialogContext<BackupJob, BackupJobBuilder>>()
|
||||
|
||||
readonly target = toSignal(
|
||||
from(this.api.getBackupTargets({})).pipe(map(({ saved }) => saved)),
|
||||
|
||||
@@ -223,13 +223,13 @@ export class BackupsHistoryModal {
|
||||
}
|
||||
}
|
||||
|
||||
showReport(run: BackupRun) {
|
||||
showReport({ report, completedAt }: BackupRun) {
|
||||
this.dialogs
|
||||
.open(REPORT, {
|
||||
label: 'Backup Report',
|
||||
data: {
|
||||
report: run.report,
|
||||
timestamp: run.completedAt,
|
||||
content: report,
|
||||
timestamp: completedAt,
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
@@ -5,10 +5,7 @@ import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiMapperPipe } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiDialogContext, TuiGroup } from '@taiga-ui/core'
|
||||
import { TuiBlock, TuiCheckbox } from '@taiga-ui/kit'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { take } from 'rxjs'
|
||||
import { PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
@@ -79,7 +76,7 @@ export class BackupsRecoverModal {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly context =
|
||||
inject<TuiDialogContext<void, RecoverData>>(POLYMORPHEUS_CONTEXT)
|
||||
injectContext<TuiDialogContext<void, RecoverData>>()
|
||||
|
||||
readonly packageData$ = inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData')
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerComponent, StartOSDiskInfo } from '@start9labs/shared'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
|
||||
interface Data {
|
||||
servers: StartOSDiskInfo[]
|
||||
@@ -23,8 +20,7 @@ interface Data {
|
||||
imports: [ServerComponent],
|
||||
})
|
||||
export class ServersComponent {
|
||||
readonly context =
|
||||
inject<TuiDialogContext<StartOSDiskInfo, Data>>(POLYMORPHEUS_CONTEXT)
|
||||
readonly context = injectContext<TuiDialogContext<StartOSDiskInfo, Data>>()
|
||||
}
|
||||
|
||||
export const SERVERS = new PolymorpheusComponent(ServersComponent)
|
||||
|
||||
@@ -15,10 +15,7 @@ import {
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BackupTarget } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -83,9 +80,9 @@ export class BackupsTargetModal {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
readonly context =
|
||||
inject<
|
||||
injectContext<
|
||||
TuiDialogContext<BackupTarget & { id: string }, { type: BackupType }>
|
||||
>(POLYMORPHEUS_CONTEXT)
|
||||
>()
|
||||
|
||||
readonly loading = signal(true)
|
||||
readonly text =
|
||||
|
||||
@@ -76,7 +76,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="outline"
|
||||
appearance="outline-grayscale"
|
||||
(click)="showService()"
|
||||
>
|
||||
View Installed
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
MarketplaceRegistryComponent,
|
||||
StoreIconComponentModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
ErrorService,
|
||||
LoadingService,
|
||||
@@ -9,30 +12,24 @@ import {
|
||||
toUrl,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
StoreIconComponentModule,
|
||||
MarketplaceRegistryComponent,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiIcon,
|
||||
TuiTitle,
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
PolymorpheusComponent,
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
} from '@taiga-ui/polymorpheus'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
|
||||
import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -100,7 +97,7 @@ export class MarketplaceRegistryModal {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
private readonly context = inject<TuiDialogContext>(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context = injectContext<TuiDialogContext>()
|
||||
private readonly route = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
|
||||
|
||||
@@ -46,12 +46,12 @@ import { toRouterLink } from 'src/app/utils/to-router-link'
|
||||
(overflownChange)="overflow = $event"
|
||||
/>
|
||||
@if (overflow) {
|
||||
<button tuiLink (click)="service.viewFull(notificationItem)">
|
||||
<button tuiLink (click)="service.viewModal(notificationItem, true)">
|
||||
View Full
|
||||
</button>
|
||||
}
|
||||
@if (notificationItem.code === 1) {
|
||||
<button tuiLink (click)="service.viewReport(notificationItem)">
|
||||
@if (notificationItem.code === 1 || notificationItem.code === 2) {
|
||||
<button tuiLink (click)="service.viewModal(notificationItem)">
|
||||
View Report
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -15,44 +15,68 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiAlertService, TuiButton } from '@taiga-ui/core'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiProgressBar } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { combineLatest, filter, firstValueFrom, map } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
|
||||
import { SideloadService } from './sideload.service'
|
||||
|
||||
@Component({
|
||||
selector: 'sideload-package',
|
||||
template: `
|
||||
<div class="outer-container">
|
||||
<ng-content />
|
||||
<marketplace-package-hero
|
||||
*tuiLet="button$ | async as button"
|
||||
[pkg]="package"
|
||||
>
|
||||
<div class="inner-container">
|
||||
@if (button !== null && button !== 'Install') {
|
||||
<a
|
||||
tuiButton
|
||||
appearance="tertiary-solid"
|
||||
[routerLink]="'/portal/service/' + package.id"
|
||||
>
|
||||
View installed
|
||||
</a>
|
||||
}
|
||||
@if (button) {
|
||||
<button tuiButton (click)="upload()">{{ button }}</button>
|
||||
}
|
||||
</div>
|
||||
</marketplace-package-hero>
|
||||
<!-- @TODO Matt do we want this here? How do we turn s9pk into MarketplacePkg? -->
|
||||
<!-- <marketplace-about [pkg]="package" />-->
|
||||
<!-- @if (!(package.dependencyMetadata | empty)) {-->
|
||||
<!-- <marketplace-dependencies [pkg]="package" (open)="open($event)" />-->
|
||||
<!-- }-->
|
||||
<!-- <marketplace-additional [pkg]="package" />-->
|
||||
@if (progress$ | async; as progress) {
|
||||
@for (phase of progress.phases; track $index) {
|
||||
<p>
|
||||
{{ phase.name }}
|
||||
@if (phase.progress | installingProgress; as progress) {
|
||||
: {{ progress }}%
|
||||
}
|
||||
</p>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
size="xs"
|
||||
[style.color]="
|
||||
phase.progress === true
|
||||
? 'var(--tui-text-positive)'
|
||||
: 'var(--tui-text-action)'
|
||||
"
|
||||
[attr.value]="(phase.progress | installingProgress) / 100 || null"
|
||||
></progress>
|
||||
}
|
||||
} @else {
|
||||
<marketplace-package-hero
|
||||
*tuiLet="button$ | async as button"
|
||||
[pkg]="package"
|
||||
>
|
||||
<div class="inner-container">
|
||||
@if (button !== null && button !== 'Install') {
|
||||
<a
|
||||
tuiButton
|
||||
appearance="tertiary-solid"
|
||||
[routerLink]="'/portal/service/' + package.id"
|
||||
>
|
||||
View installed
|
||||
</a>
|
||||
}
|
||||
@if (button) {
|
||||
<button tuiButton (click)="upload()">{{ button }}</button>
|
||||
}
|
||||
</div>
|
||||
</marketplace-package-hero>
|
||||
<!-- @TODO Matt do we want this here? How do we turn s9pk into MarketplacePkg? -->
|
||||
<!-- <marketplace-about [pkg]="package" />-->
|
||||
<!-- @if (!(package.dependencyMetadata | empty)) {-->
|
||||
<!-- <marketplace-dependencies [pkg]="package" (open)="open($event)" />-->
|
||||
<!-- }-->
|
||||
<!-- <marketplace-additional [pkg]="package" />-->
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
@@ -87,6 +111,8 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
TuiLet,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
InstallingProgressPipe,
|
||||
TuiProgressBar,
|
||||
],
|
||||
})
|
||||
export class SideloadPackageComponent {
|
||||
@@ -94,9 +120,10 @@ export class SideloadPackageComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly sideloadService = inject(SideloadService)
|
||||
|
||||
readonly progress$ = this.sideloadService.progress$
|
||||
readonly button$ = combineLatest([
|
||||
inject(ClientStorageService).showDevTools$,
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
@@ -133,17 +160,14 @@ export class SideloadPackageComponent {
|
||||
file!: File
|
||||
|
||||
async upload() {
|
||||
const loader = this.loader.open('Uploading package').subscribe()
|
||||
const loader = this.loader.open('Starting upload').subscribe()
|
||||
|
||||
try {
|
||||
const { upload } = await this.api.sideloadPackage()
|
||||
const { upload, progress } = await this.api.sideloadPackage()
|
||||
|
||||
await this.api.uploadPackage(upload, this.file).catch(console.error)
|
||||
await this.router.navigate(['/portal/service', this.package.id])
|
||||
|
||||
this.alerts
|
||||
.open('Package uploaded successfully', { appearance: 'positive' })
|
||||
.subscribe()
|
||||
this.sideloadService.followProgress(progress)
|
||||
this.api.uploadPackage(upload, this.file).catch(console.error)
|
||||
await firstValueFrom(this.progress$.pipe(filter(Boolean)))
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { endWith, ReplaySubject, shareReplay, Subject, switchMap } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -7,25 +7,16 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SideloadService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly guid$ = new Subject<string>()
|
||||
|
||||
readonly websocketConnected$ = new ReplaySubject()
|
||||
|
||||
readonly progress$ = this.guid$.pipe(
|
||||
switchMap(guid =>
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
openObserver: {
|
||||
next: () => this.websocketConnected$.next(''),
|
||||
},
|
||||
})
|
||||
.pipe(endWith(null)),
|
||||
this.api.openWebsocket$<T.FullProgress>(guid).pipe(endWith(null)),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(private readonly api: ApiService) {}
|
||||
|
||||
followProgress(guid: string) {
|
||||
this.guid$.next(guid)
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { filter } from 'rxjs'
|
||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
ActionInputModal,
|
||||
PackageActionData,
|
||||
} from '../modals/action-input.component'
|
||||
} from 'src/app/routes/portal/routes/service/modals/action-input.component'
|
||||
import { ActionSuccessPage } from 'src/app/routes/portal/routes/service/modals/action-success/action-success.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
|
||||
const allowedStatuses = {
|
||||
'only-running': new Set(['running']),
|
||||
@@ -98,11 +98,7 @@ export class ActionService {
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
packageId: string,
|
||||
actionId: string,
|
||||
input?: object,
|
||||
): Promise<boolean> {
|
||||
async execute(packageId: string, actionId: string, input?: object) {
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
|
||||
try {
|
||||
@@ -112,7 +108,7 @@ export class ActionService {
|
||||
input: input || null,
|
||||
})
|
||||
|
||||
if (!res) return true
|
||||
if (!res) return
|
||||
|
||||
if (res.result) {
|
||||
this.dialogs
|
||||
@@ -124,10 +120,8 @@ export class ActionService {
|
||||
} else if (res.message) {
|
||||
this.dialogs.open(res.message, { label: res.title }).subscribe()
|
||||
}
|
||||
return true // needed to dismiss original modal/alert
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false // don't dismiss original modal/alert
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
|
||||
@@ -5,13 +5,7 @@ import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
|
||||
// @TODO Alex implement config
|
||||
// import {
|
||||
// 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 { getAllPackages } from 'src/app/utils/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
@@ -19,21 +13,13 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ActionsService {
|
||||
export class ControlsService {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
configure(manifest: T.Manifest): void {
|
||||
// this.formDialog.open<PackageConfigData>(ConfigModal, {
|
||||
// label: `${manifest.title} configuration`,
|
||||
// data: { pkgId: manifest.id },
|
||||
// })
|
||||
}
|
||||
|
||||
async start(manifest: T.Manifest, unmet: boolean): Promise<void> {
|
||||
const deps = `${manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { ErrorService, MARKDOWN } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { firstValueFrom, merge, shareReplay, Subject } from 'rxjs'
|
||||
import { REPORT } from 'src/app/components/report.component'
|
||||
import {
|
||||
ServerNotification,
|
||||
ServerNotifications,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { REPORT } from 'src/app/components/report.component'
|
||||
import { firstValueFrom, merge, shareReplay, Subject } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -89,19 +89,19 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
viewFull(notification: ServerNotification<number>) {
|
||||
this.dialogs
|
||||
.open(notification.message, { label: notification.title })
|
||||
.subscribe()
|
||||
}
|
||||
viewModal(
|
||||
{ data, createdAt, code, title, message }: ServerNotification<number>,
|
||||
full = false,
|
||||
) {
|
||||
const label = full || code === 2 ? title : 'Backup Report'
|
||||
const content = code === 1 ? REPORT : MARKDOWN
|
||||
|
||||
viewReport(notification: ServerNotification<number>) {
|
||||
this.dialogs
|
||||
.open(REPORT, {
|
||||
label: 'Backup Report',
|
||||
.open(full ? message : content, {
|
||||
label,
|
||||
data: {
|
||||
report: notification.data,
|
||||
timestamp: notification.createdAt,
|
||||
content: data,
|
||||
timestamp: createdAt,
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { hasCurrentDeps } from '../util/has-deps'
|
||||
import { getAllPackages } from '../util/get-package-data'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter } from 'rxjs'
|
||||
import { getAllPackages } from '../utils/get-package-data'
|
||||
import { hasCurrentDeps } from '../utils/has-deps'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StandardActionsService {
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly navCtrl: NavController,
|
||||
) {}
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
async rebuild(id: string) {
|
||||
const loader = this.loader.open(`Rebuilding Container...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.rebuildPackage({ id })
|
||||
this.navCtrl.navigateBack('/services/' + id)
|
||||
await this.router.navigate(['portal', 'services', id])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -34,48 +35,38 @@ export class StandardActionsService {
|
||||
}
|
||||
}
|
||||
|
||||
async tryUninstall(manifest: T.Manifest): Promise<void> {
|
||||
const { id, title, alerts } = manifest
|
||||
|
||||
let message =
|
||||
async uninstall({ id, title, alerts }: T.Manifest): Promise<void> {
|
||||
let content =
|
||||
alerts.uninstall ||
|
||||
`Uninstalling ${title} will permanently delete its data`
|
||||
|
||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||
message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content,
|
||||
yes: 'Uninstall',
|
||||
no: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Uninstall',
|
||||
handler: () => {
|
||||
this.uninstall(id)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.doUninstall(id))
|
||||
}
|
||||
|
||||
private async uninstall(id: string) {
|
||||
private async doUninstall(id: string) {
|
||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.api.uninstallPackage({ id })
|
||||
this.api
|
||||
await this.api
|
||||
.setDbValue<boolean>(['ackInstructions', id], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
await this.router.navigate(['portal'])
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function isRemoving(
|
||||
}
|
||||
|
||||
export function isInstalling(
|
||||
pkg: PackageDataEntry,
|
||||
pkg: T.PackageDataEntry,
|
||||
): pkg is PackageDataEntry<InstallingState> {
|
||||
return pkg.stateInfo.state === 'installing'
|
||||
}
|
||||
|
||||
@@ -205,20 +205,20 @@ button.g-action {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.g-success.g-success {
|
||||
color: var(--tui-status-positive);
|
||||
.g-success {
|
||||
color: var(--tui-status-positive) !important;
|
||||
}
|
||||
|
||||
.g-warning.g-warning {
|
||||
color: var(--tui-status-warning);
|
||||
.g-warning {
|
||||
color: var(--tui-status-warning) !important;
|
||||
}
|
||||
|
||||
.g-error.g-error {
|
||||
color: var(--tui-status-negative);
|
||||
color: var(--tui-status-negative) !important;
|
||||
}
|
||||
|
||||
.g-info.g-info {
|
||||
color: var(--tui-status-info);
|
||||
.g-info {
|
||||
color: var(--tui-status-info) !important;
|
||||
}
|
||||
|
||||
ng-component {
|
||||
|
||||
Reference in New Issue
Block a user