mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
fix a few, more to go (#2869)
* fix a few, more to go * chore: comments (#2871) * chore: comments * chore: typo * chore: stricter typescript (#2872) * chore: comments * chore: stricter typescript --------- Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> * minor styling --------- Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
@@ -54,7 +54,6 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store-icon {
|
store-icon {
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,6 @@ export class LogsWindowComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onBottom(entries: readonly IntersectionObserverEntry[]) {
|
onBottom(entries: readonly IntersectionObserverEntry[]) {
|
||||||
this.scroll = entries[entries.length - 1].isIntersecting
|
this.scroll = !!entries[entries.length - 1]?.isIntersecting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ export function convertBytes(bytes: number): string {
|
|||||||
export class DurationToSecondsPipe implements PipeTransform {
|
export class DurationToSecondsPipe implements PipeTransform {
|
||||||
transform(duration?: string | null): number {
|
transform(duration?: string | null): number {
|
||||||
if (!duration) return 0
|
if (!duration) return 0
|
||||||
const [, num, , unit] =
|
|
||||||
duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/) || []
|
const regex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
|
||||||
return Number(num) * unitsToSeconds[unit]
|
const [, num, , unit] = duration.match(regex) || []
|
||||||
|
const multiplier = (unit && unitsToSeconds[unit]) || NaN
|
||||||
|
|
||||||
|
return unit ? Number(num) * multiplier : NaN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,13 +294,3 @@ a {
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
hr {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class AppComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.patch.watch$('ui').subscribe(({ name, language }) => {
|
this.patch.watch$('ui').subscribe(({ name, language }) => {
|
||||||
this.title.setTitle(name || 'StartOS')
|
this.title.setTitle(name || 'StartOS')
|
||||||
this.i18n.setLanguage(language)
|
this.i18n.setLanguage(language || 'english')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,6 @@ export function appInitializer(): () => void {
|
|||||||
auth.init()
|
auth.init()
|
||||||
localStorage.init()
|
localStorage.init()
|
||||||
router.initialNavigation()
|
router.initialNavigation()
|
||||||
i18n.setLanguage(i18n.language)
|
i18n.setLanguage(i18n.language || 'english')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class i18nService extends TuiLanguageSwitcherService {
|
|||||||
|
|
||||||
readonly loading = signal(false)
|
readonly loading = signal(false)
|
||||||
|
|
||||||
override setLanguage(language: TuiLanguageName): void {
|
override setLanguage(language: TuiLanguageName = 'english'): void {
|
||||||
if (this.language === language) {
|
if (this.language === language) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<section
|
<section
|
||||||
class="top"
|
class="top"
|
||||||
waIntersectionObserver
|
waIntersectionObserver
|
||||||
(waIntersectionObservee)="onTop($event[0].isIntersecting)"
|
(waIntersectionObservee)="onTop(!!$event[0]?.isIntersecting)"
|
||||||
>
|
>
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
<tui-loader textContent="Loading logs" />
|
<tui-loader textContent="Loading logs" />
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export class FormMultiselectComponent extends Control<
|
|||||||
|
|
||||||
private readonly isDisabled = (item: string) =>
|
private readonly isDisabled = (item: string) =>
|
||||||
Array.isArray(this.spec.disabled) &&
|
Array.isArray(this.spec.disabled) &&
|
||||||
this.spec.disabled.includes(this.inverted[item])
|
!!this.inverted[item] &&
|
||||||
|
this.spec.disabled.includes(this.inverted[item]!)
|
||||||
|
|
||||||
private readonly isExceedingLimit = (item: string) =>
|
private readonly isExceedingLimit = (item: string) =>
|
||||||
!!this.spec.maxLength &&
|
!!this.spec.maxLength &&
|
||||||
@@ -44,6 +45,6 @@ export class FormMultiselectComponent extends Control<
|
|||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
private memoize(value: null | readonly string[]): string[] {
|
private memoize(value: null | readonly string[]): string[] {
|
||||||
return value?.map(key => this.spec.values[key]) || []
|
return value?.map(key => this.spec.values[key] || '') || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
|||||||
|
|
||||||
readonly disabledItemHandler = (item: string) =>
|
readonly disabledItemHandler = (item: string) =>
|
||||||
Array.isArray(this.spec.disabled) &&
|
Array.isArray(this.spec.disabled) &&
|
||||||
this.spec.disabled.includes(this.inverted[item])
|
!!this.inverted[item] &&
|
||||||
|
this.spec.disabled.includes(this.inverted[item]!)
|
||||||
|
|
||||||
get disabled(): boolean {
|
get disabled(): boolean {
|
||||||
return typeof this.spec.disabled === 'string'
|
return typeof this.spec.disabled === 'string'
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
<tui-elastic-container class="g-form-group" formGroupName="value">
|
<tui-elastic-container class="g-form-group" formGroupName="value">
|
||||||
<form-group
|
<form-group
|
||||||
class="group"
|
class="group"
|
||||||
[spec]="(union && spec.variants[union].spec) || {}"
|
[spec]="(union && spec.variants[union]?.spec) || {}"
|
||||||
></form-group>
|
></form-group>
|
||||||
</tui-elastic-container>
|
</tui-elastic-container>
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ export class FormUnionComponent implements OnChanges {
|
|||||||
|
|
||||||
@tuiPure
|
@tuiPure
|
||||||
onUnion(union: string) {
|
onUnion(union: string) {
|
||||||
this.values[this.union] = this.form.control.controls['value'].value
|
this.values[this.union] = this.form.control.controls['value']?.value
|
||||||
this.form.control.setControl(
|
this.form.control.setControl(
|
||||||
'value',
|
'value',
|
||||||
this.formService.getFormGroup(
|
this.formService.getFormGroup(
|
||||||
union ? this.spec.variants[union].spec : {},
|
union ? this.spec.variants[union]?.spec || {} : {},
|
||||||
[],
|
[],
|
||||||
this.values[union],
|
this.values[union],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -71,22 +71,14 @@ type ClearnetForm = {
|
|||||||
Make {{ isPublic() ? 'private' : 'public' }}
|
Make {{ isPublic() ? 'private' : 'public' }}
|
||||||
</button>
|
</button>
|
||||||
@if (clearnet().length) {
|
@if (clearnet().length) {
|
||||||
<button
|
<button tuiButton iconStart="@tui.plus" (click)="add()">Add</button>
|
||||||
tuiButton
|
|
||||||
iconStart="@tui.plus"
|
|
||||||
[style.margin-inline-start]="'auto'"
|
|
||||||
(click)="add()"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
@if (clearnet().length) {
|
@if (clearnet().length) {
|
||||||
<table [appTable]="['Domain', 'ACME', 'URL', '']">
|
<table [appTable]="['ACME', 'URL', '']">
|
||||||
@for (address of clearnet(); track $index) {
|
@for (address of clearnet(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
<td [style.width.rem]="12">{{ address.acme | acme }}</td>
|
||||||
<td>{{ address.acme | acme }}</td>
|
|
||||||
<td>{{ address.url | mask }}</td>
|
<td>{{ address.url | mask }}</td>
|
||||||
<td [actions]="address.url">
|
<td [actions]="address.url">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ export function getAddresses(
|
|||||||
tor: AddressDetails[]
|
tor: AddressDetails[]
|
||||||
} {
|
} {
|
||||||
const addressInfo = serviceInterface.addressInfo
|
const addressInfo = serviceInterface.addressInfo
|
||||||
const hostnames = host.hostnameInfo[addressInfo.internalPort].filter(
|
const hostnames =
|
||||||
h =>
|
host.hostnameInfo[addressInfo.internalPort]?.filter(
|
||||||
config.isLocalhost() ||
|
h =>
|
||||||
h.kind !== 'ip' ||
|
config.isLocalhost() ||
|
||||||
h.hostname.kind !== 'ipv6' ||
|
h.kind !== 'ip' ||
|
||||||
!h.hostname.value.startsWith('fe80::'),
|
h.hostname.kind !== 'ipv6' ||
|
||||||
)
|
!h.hostname.value.startsWith('fe80::'),
|
||||||
|
) || []
|
||||||
|
|
||||||
if (config.isLocalhost()) {
|
if (config.isLocalhost()) {
|
||||||
const local = hostnames.find(
|
const local = hostnames.find(
|
||||||
@@ -67,11 +68,10 @@ export function getAddresses(
|
|||||||
addresses.forEach(url => {
|
addresses.forEach(url => {
|
||||||
if (h.kind === 'onion') {
|
if (h.kind === 'onion') {
|
||||||
tor.push({
|
tor.push({
|
||||||
label: `Tor${
|
label:
|
||||||
addresses.length > 1
|
addresses.length > 1
|
||||||
? ` (${new URL(url).protocol.replace(':', '').toUpperCase()})`
|
? new URL(url).protocol.replace(':', '').toUpperCase()
|
||||||
: ''
|
: '',
|
||||||
}`,
|
|
||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -79,14 +79,10 @@ export function getAddresses(
|
|||||||
|
|
||||||
if (h.public) {
|
if (h.public) {
|
||||||
clearnet.push({
|
clearnet.push({
|
||||||
label:
|
|
||||||
hostnameKind == 'domain'
|
|
||||||
? 'Domain'
|
|
||||||
: `${h.networkInterfaceId} (${hostnameKind})`,
|
|
||||||
url,
|
url,
|
||||||
acme:
|
acme:
|
||||||
hostnameKind == 'domain'
|
hostnameKind == 'domain'
|
||||||
? host.domains[h.hostname.domain]?.acme
|
? host.domains[h.hostname.domain]?.acme || null
|
||||||
: null, // @TODO Matt make sure this is handled correctly - looks like ACME settings aren't built yet anyway, but ACME settings aren't *available* for public IPs
|
: null, // @TODO Matt make sure this is handled correctly - looks like ACME settings aren't built yet anyway, but ACME settings aren't *available* for public IPs
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -128,7 +124,7 @@ export type MappedServiceInterface = T.ServiceInterface & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AddressDetails = {
|
export type AddressDetails = {
|
||||||
label: string
|
label?: string
|
||||||
url: string
|
url: string
|
||||||
acme?: string | null
|
acme?: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { MaskPipe } from './mask.pipe'
|
|||||||
<table [appTable]="['Network Interface', 'URL', '']">
|
<table [appTable]="['Network Interface', 'URL', '']">
|
||||||
@for (address of local(); track $index) {
|
@for (address of local(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
<td [style.width.rem]="12">{{ address.label }}</td>
|
||||||
<td>{{ address.url | mask }}</td>
|
<td>{{ address.url | mask }}</td>
|
||||||
<td [actions]="address.url"></td>
|
<td [actions]="address.url"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ import {
|
|||||||
TuiLink,
|
TuiLink,
|
||||||
TuiOption,
|
TuiOption,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit'
|
import {
|
||||||
|
TUI_CONFIRM,
|
||||||
|
TuiFade,
|
||||||
|
TuiFluidTypography,
|
||||||
|
TuiTooltip,
|
||||||
|
} from '@taiga-ui/kit'
|
||||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
FormComponent,
|
FormComponent,
|
||||||
@@ -69,8 +74,12 @@ type OnionForm = {
|
|||||||
<table [appTable]="['Protocol', 'URL', '']">
|
<table [appTable]="['Protocol', 'URL', '']">
|
||||||
@for (address of tor(); track $index) {
|
@for (address of tor(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td [style.width.rem]="15">{{ address.label }}</td>
|
<td [style.width.rem]="12">{{ address.label }}</td>
|
||||||
<td>{{ address.url | mask }}</td>
|
<td>
|
||||||
|
<div [tuiFluidTypography]="[0.625, 0.8125]" tuiFade>
|
||||||
|
{{ address.url | mask }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td [actions]="address.url">
|
<td [actions]="address.url">
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
@@ -99,6 +108,12 @@ type OnionForm = {
|
|||||||
</app-placeholder>
|
</app-placeholder>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
[tuiFade] {
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
imports: [
|
imports: [
|
||||||
TuiButton,
|
TuiButton,
|
||||||
@@ -111,6 +126,8 @@ type OnionForm = {
|
|||||||
PlaceholderComponent,
|
PlaceholderComponent,
|
||||||
MaskPipe,
|
MaskPipe,
|
||||||
InterfaceActionsComponent,
|
InterfaceActionsComponent,
|
||||||
|
TuiFade,
|
||||||
|
TuiFluidTypography,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class LogsFetchDirective {
|
|||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
readonly logsFetch = defer(() => this.observer).pipe(
|
readonly logsFetch = defer(() => this.observer).pipe(
|
||||||
filter(([{ isIntersecting }]) => isIntersecting && !this.component.scroll),
|
filter(([entry]) => !!entry?.isIntersecting && !this.component.scroll),
|
||||||
switchMap(() =>
|
switchMap(() =>
|
||||||
from(
|
from(
|
||||||
this.component.fetchLogs({
|
this.component.fetchLogs({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section
|
<section
|
||||||
class="top"
|
class="top"
|
||||||
waIntersectionObserver
|
waIntersectionObserver
|
||||||
(waIntersectionObservee)="onLoading($event[0].isIntersecting)"
|
(waIntersectionObservee)="onLoading(!!$event[0]?.isIntersecting)"
|
||||||
(logsFetch)="onPrevious($event)"
|
(logsFetch)="onPrevious($event)"
|
||||||
>
|
>
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
class="bottom"
|
class="bottom"
|
||||||
waIntersectionObserver
|
waIntersectionObserver
|
||||||
(waIntersectionObservee)="
|
(waIntersectionObservee)="
|
||||||
setScroll($event[$event.length - 1].isIntersecting)
|
setScroll(!!$event[$event.length - 1]?.isIntersecting)
|
||||||
"
|
"
|
||||||
></section>
|
></section>
|
||||||
</tui-scrollbar>
|
</tui-scrollbar>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function toNavigationItem(
|
|||||||
const item = SYSTEM_UTILITIES[id]
|
const item = SYSTEM_UTILITIES[id]
|
||||||
const routerLink = toRouterLink(id)
|
const routerLink = toRouterLink(id)
|
||||||
|
|
||||||
return SYSTEM_UTILITIES[id]
|
return item
|
||||||
? {
|
? {
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
@@ -94,7 +94,7 @@ function toNavigationItem(
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
icon: packages[id]?.icon,
|
icon: packages[id]?.icon,
|
||||||
title: getManifest(packages[id]).title,
|
title: getManifest(packages[id]!).title,
|
||||||
routerLink,
|
routerLink,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
signal,
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
TuiAppearance,
|
|
||||||
TuiButton,
|
|
||||||
TuiIcon,
|
|
||||||
TuiLink,
|
|
||||||
TuiTitle,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { TuiCardMedium } from '@taiga-ui/layout'
|
import { TuiCardMedium } from '@taiga-ui/layout'
|
||||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||||
import { RR } from 'src/app/services/api/api.types'
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
@@ -37,7 +31,7 @@ interface Log {
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
{{ logs[key].title }}
|
{{ logs[key]?.title }}
|
||||||
} @else {
|
} @else {
|
||||||
Logs
|
Logs
|
||||||
}
|
}
|
||||||
@@ -55,9 +49,9 @@ interface Log {
|
|||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
{{ logs[key].title }}
|
{{ logs[key]?.title }}
|
||||||
</strong>
|
</strong>
|
||||||
<p tuiSubtitle>{{ logs[key].subtitle }}</p>
|
<p tuiSubtitle>{{ logs[key]?.subtitle }}</p>
|
||||||
</header>
|
</header>
|
||||||
@for (log of logs | keyvalue; track $index) {
|
@for (log of logs | keyvalue; track $index) {
|
||||||
@if (log.key === current()) {
|
@if (log.key === current()) {
|
||||||
@@ -169,7 +163,6 @@ interface Log {
|
|||||||
TuiCardMedium,
|
TuiCardMedium,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
TuiLink,
|
|
||||||
TuiButton,
|
TuiButton,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
selector: 'tr[actionRequest]',
|
selector: 'tr[actionRequest]',
|
||||||
template: `
|
template: `
|
||||||
<td>
|
<td>
|
||||||
<tui-avatar size="xs"><img [src]="pkg().icon" alt="" /></tui-avatar>
|
<tui-avatar size="xs"><img [src]="pkg()?.icon" alt="" /></tui-avatar>
|
||||||
<span>{{ title() }}</span>
|
<span>{{ title() }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -35,7 +35,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button tuiButton (click)="handle()">
|
<button tuiButton (click)="handle()">
|
||||||
{{ pkg().actions[actionRequest().actionId].name }}
|
{{ pkg()?.actions?.[actionRequest().actionId]?.name }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`,
|
`,
|
||||||
@@ -79,19 +79,27 @@ export class ServiceActionRequestComponent {
|
|||||||
readonly services = input.required<Record<string, PackageDataEntry>>()
|
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||||
|
|
||||||
readonly pkg = computed(() => this.services()[this.actionRequest().packageId])
|
readonly pkg = computed(() => this.services()[this.actionRequest().packageId])
|
||||||
readonly title = computed(() => getManifest(this.pkg()).title)
|
readonly title = computed((pkg = this.pkg()) => pkg && getManifest(pkg).title)
|
||||||
|
|
||||||
async handle() {
|
async handle() {
|
||||||
|
const title = this.title()
|
||||||
|
const pkg = this.pkg()
|
||||||
|
const metadata = pkg?.actions[this.actionRequest().actionId]
|
||||||
|
|
||||||
|
if (!title || !pkg || !metadata) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.actionService.present({
|
this.actionService.present({
|
||||||
pkgInfo: {
|
pkgInfo: {
|
||||||
id: this.actionRequest().packageId,
|
id: this.actionRequest().packageId,
|
||||||
title: this.title(),
|
title,
|
||||||
mainStatus: this.pkg().status.main,
|
mainStatus: pkg.status.main,
|
||||||
icon: this.pkg().icon,
|
icon: pkg.icon,
|
||||||
},
|
},
|
||||||
actionInfo: {
|
actionInfo: {
|
||||||
id: this.actionRequest().actionId,
|
id: this.actionRequest().actionId,
|
||||||
metadata: this.pkg().actions[this.actionRequest().actionId],
|
metadata,
|
||||||
},
|
},
|
||||||
requestInfo: this.actionRequest(),
|
requestInfo: this.actionRequest(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { ServiceActionRequestComponent } from './action-request.component'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
|
min-height: 12rem;
|
||||||
grid-column: span 6;
|
grid-column: span 6;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -50,7 +51,13 @@ export class ServiceActionRequestsComponent {
|
|||||||
|
|
||||||
readonly requests = computed(() =>
|
readonly requests = computed(() =>
|
||||||
Object.values(this.pkg().requestedActions)
|
Object.values(this.pkg().requestedActions)
|
||||||
.filter(r => r.active)
|
// @TODO Alex uncomment filter line below to produce infinite loop on service details page when dependency not installed. This means the page is infinitely trying to re-render
|
||||||
|
// .filter(r => r.active)
|
||||||
|
.filter(
|
||||||
|
r =>
|
||||||
|
this.services()[r.request.packageId]?.actions[r.request.actionId] &&
|
||||||
|
r.active,
|
||||||
|
)
|
||||||
.sort((a, b) => a.request.severity.localeCompare(b.request.severity)),
|
.sort((a, b) => a.request.severity.localeCompare(b.request.severity)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'service-controls',
|
selector: 'service-controls',
|
||||||
template: `
|
template: `
|
||||||
@if (['running', 'starting', 'restarting'].includes(status)) {
|
@if (status && ['running', 'starting', 'restarting'].includes(status)) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
appearance="secondary-destructive"
|
appearance="primary-destructive"
|
||||||
iconStart="@tui.square"
|
iconStart="@tui.square"
|
||||||
(click)="controls.stop(manifest())"
|
(click)="controls.stop(manifest())"
|
||||||
>
|
>
|
||||||
@@ -41,7 +41,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
iconStart="@tui.play"
|
iconStart="@tui.play"
|
||||||
(click)="controls.start(manifest(), !!hasUnmet)"
|
(click)="controls.start(manifest(), !!hasUnmet())"
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
@@ -50,11 +50,12 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 18rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-inline-size: 100%;
|
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
|
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
@@ -62,6 +63,11 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[tuiButton] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -85,12 +91,13 @@ export class ServiceControlsComponent {
|
|||||||
pkg!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
status!: PrimaryStatus
|
status?: PrimaryStatus
|
||||||
|
|
||||||
readonly manifest = computed(() => getManifest(this.pkg))
|
readonly manifest = computed(() => getManifest(this.pkg))
|
||||||
|
|
||||||
readonly controls = inject(ControlsService)
|
readonly controls = inject(ControlsService)
|
||||||
|
|
||||||
|
// @TODO Alex observable in signal?
|
||||||
readonly hasUnmet = computed(() =>
|
readonly hasUnmet = computed(() =>
|
||||||
this.errors.getPkgDepErrors$(this.manifest().id).pipe(
|
this.errors.getPkgDepErrors$(this.manifest().id).pipe(
|
||||||
map(errors =>
|
map(errors =>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
|||||||
import { TuiAvatar } from '@taiga-ui/kit'
|
import { TuiAvatar } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
|
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -20,6 +21,11 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
<tui-avatar><img alt="" [src]="d.value.icon" /></tui-avatar>
|
<tui-avatar><img alt="" [src]="d.value.icon" /></tui-avatar>
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
{{ d.value.title }}
|
{{ d.value.title }}
|
||||||
|
@if (getError(d.key); as error) {
|
||||||
|
<span tuiSubtitle class="g-warning">{{ error }}</span>
|
||||||
|
} @else {
|
||||||
|
<span tuiSubtitle class="g-positive">Satisfied</span>
|
||||||
|
}
|
||||||
<span tuiSubtitle>{{ d.value.versionRange }}</span>
|
<span tuiSubtitle>{{ d.value.versionRange }}</span>
|
||||||
</span>
|
</span>
|
||||||
<tui-icon icon="@tui.arrow-right" />
|
<tui-icon icon="@tui.arrow-right" />
|
||||||
@@ -30,6 +36,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
|
min-height: 12rem;
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -52,4 +59,32 @@ export class ServiceDependenciesComponent {
|
|||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
services: Record<string, PackageDataEntry> = {}
|
services: Record<string, PackageDataEntry> = {}
|
||||||
|
|
||||||
|
@Input({ required: true })
|
||||||
|
errors: PkgDependencyErrors = {}
|
||||||
|
|
||||||
|
getError(id: string): string {
|
||||||
|
const depError = this.errors[id]
|
||||||
|
|
||||||
|
if (!depError) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (depError.type) {
|
||||||
|
case 'notInstalled':
|
||||||
|
return 'Not installed'
|
||||||
|
case 'incorrectVersion':
|
||||||
|
return 'Incorrect version'
|
||||||
|
case 'notRunning':
|
||||||
|
return 'Not running'
|
||||||
|
case 'actionRequired':
|
||||||
|
return 'Action required'
|
||||||
|
case 'healthChecksFailed':
|
||||||
|
return 'Required health check not passing'
|
||||||
|
case 'transitive':
|
||||||
|
return 'Dependency has a dependency issue'
|
||||||
|
default:
|
||||||
|
return 'Unknown error'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { ServiceHealthCheckComponent } from './health-check.component'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
|
min-height: 12rem;
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class ServiceInterfacesComponent {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
public: !!host?.bindings[value.addressInfo.internalPort].net.public,
|
public: !!host?.bindings[value.addressInfo.internalPort]?.net.public,
|
||||||
addresses: host ? getAddresses(value, host, this.config) : {},
|
addresses: host ? getAddresses(value, host, this.config) : {},
|
||||||
routerLink: `./interface/${id}`,
|
routerLink: `./interface/${id}`,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
<small>See below</small>
|
<small>See below</small>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (rendering.showDots) {
|
@if (rendering?.showDots) {
|
||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
font: var(--tui-font-heading-4);
|
font: var(--tui-font-heading-4);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -76,6 +77,10 @@ import {
|
|||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -89,7 +94,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class ServiceStatusComponent {
|
export class ServiceStatusComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
status!: PrimaryStatus
|
status?: PrimaryStatus
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
installingInfo?: InstallingInfo
|
installingInfo?: InstallingInfo
|
||||||
@@ -98,13 +103,13 @@ export class ServiceStatusComponent {
|
|||||||
connected = false
|
connected = false
|
||||||
|
|
||||||
get text() {
|
get text() {
|
||||||
return this.connected ? this.rendering.display : 'Unknown'
|
return this.connected ? this.rendering?.display : 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
get class(): string | null {
|
get class(): string | null {
|
||||||
if (!this.connected) return null
|
if (!this.connected) return null
|
||||||
|
|
||||||
switch (this.rendering.color) {
|
switch (this.rendering?.color) {
|
||||||
case 'danger':
|
case 'danger':
|
||||||
return 'g-negative'
|
return 'g-negative'
|
||||||
case 'warning':
|
case 'warning':
|
||||||
@@ -119,7 +124,7 @@ export class ServiceStatusComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get rendering() {
|
get rendering() {
|
||||||
return PrimaryRendering[this.status]
|
return this.status && PrimaryRendering[this.status]
|
||||||
}
|
}
|
||||||
|
|
||||||
getText(progress: T.Progress): string {
|
getText(progress: T.Progress): string {
|
||||||
|
|||||||
@@ -30,15 +30,6 @@ const RUNNING = ['running', 'starting', 'restarting']
|
|||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
iconStart="@tui.rotate-cw"
|
|
||||||
[disabled]="status().primary !== 'running'"
|
|
||||||
(click)="controls.restart(manifest())"
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<button
|
||||||
*tuiLet="hasUnmet() | async as hasUnmet"
|
*tuiLet="hasUnmet() | async as hasUnmet"
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export class ActionInputModal {
|
|||||||
.filter(
|
.filter(
|
||||||
id =>
|
id =>
|
||||||
id !== this.pkgInfo.id &&
|
id !== this.pkgInfo.id &&
|
||||||
Object.values(packages[id].requestedActions).some(
|
Object.values(packages[id]!.requestedActions).some(
|
||||||
({ request, active }) =>
|
({ request, active }) =>
|
||||||
!active &&
|
!active &&
|
||||||
request.severity === 'critical' &&
|
request.severity === 'critical' &&
|
||||||
@@ -201,7 +201,7 @@ export class ActionInputModal {
|
|||||||
const message =
|
const message =
|
||||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
||||||
const content = `${message}${breakages.map(
|
const content = `${message}${breakages.map(
|
||||||
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
|
id => `<li><b>${getManifest(packages[id]!).title}</b></li>`,
|
||||||
)}</ul>`
|
)}</ul>`
|
||||||
const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }
|
const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }
|
||||||
|
|
||||||
|
|||||||
@@ -14,39 +14,44 @@ import { map } from 'rxjs'
|
|||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
import {
|
import {
|
||||||
|
AdditionalItem,
|
||||||
FALLBACK_URL,
|
FALLBACK_URL,
|
||||||
ServiceAdditionalItemComponent,
|
ServiceAdditionalItemComponent,
|
||||||
} from '../components/additional-item.component'
|
} from '../components/additional-item.component'
|
||||||
|
import { KeyValuePipe } from '@angular/common'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<section class="g-card">
|
@for (group of items() | keyvalue; track $index) {
|
||||||
@for (additional of items(); track $index) {
|
<section class="g-card">
|
||||||
@if (additional.description.startsWith('http')) {
|
<header>{{ group.key }}</header>
|
||||||
<a tuiCell [additionalItem]="additional"></a>
|
@for (additional of group.value; track $index) {
|
||||||
} @else {
|
@if (additional.description.startsWith('http')) {
|
||||||
<button
|
<a tuiCell [additionalItem]="additional"></a>
|
||||||
tuiCell
|
} @else {
|
||||||
[style.pointer-events]="!additional.icon ? 'none' : null"
|
<button
|
||||||
[additionalItem]="additional"
|
tuiCell
|
||||||
(click)="additional.action?.()"
|
[style.pointer-events]="!additional.icon ? 'none' : null"
|
||||||
></button>
|
[additionalItem]="additional"
|
||||||
|
(click)="additional.action?.()"
|
||||||
|
></button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
</section>
|
||||||
</section>
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
section {
|
section {
|
||||||
|
max-width: 42rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 36rem;
|
margin-bottom: 2rem;
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
host: { class: 'g-subpage' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
host: { class: 'g-subpage' },
|
imports: [ServiceAdditionalItemComponent, TuiCell, KeyValuePipe],
|
||||||
imports: [ServiceAdditionalItemComponent, TuiCell],
|
|
||||||
})
|
})
|
||||||
export default class ServiceAboutRoute {
|
export default class ServiceAboutRoute {
|
||||||
private readonly copyService = inject(CopyService)
|
private readonly copyService = inject(CopyService)
|
||||||
@@ -55,56 +60,62 @@ export default class ServiceAboutRoute {
|
|||||||
{ label: 'License', size: 'l' },
|
{ label: 'License', size: 'l' },
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly items = toSignal(
|
readonly items = toSignal<Record<string, AdditionalItem[]>>(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
inject<PatchDB<DataModel>>(PatchDB)
|
||||||
.watch$('packageData', getPkgId())
|
.watch$('packageData', getPkgId())
|
||||||
.pipe(
|
.pipe(
|
||||||
map(pkg => {
|
map(pkg => {
|
||||||
const manifest = getManifest(pkg)
|
const manifest = getManifest(pkg)
|
||||||
|
|
||||||
return [
|
return {
|
||||||
{
|
General: [
|
||||||
name: 'Version',
|
{
|
||||||
description: manifest.version,
|
name: 'Version',
|
||||||
},
|
description: manifest.version,
|
||||||
{
|
icon: '@tui.copy',
|
||||||
name: 'Git Hash',
|
action: () => this.copyService.copy(manifest.version),
|
||||||
description: manifest.gitHash || 'Unknown',
|
},
|
||||||
icon: manifest.gitHash ? '@tui.copy' : '',
|
{
|
||||||
action: () =>
|
name: 'Git Hash',
|
||||||
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
description: manifest.gitHash || 'Unknown',
|
||||||
},
|
icon: manifest.gitHash ? '@tui.copy' : '',
|
||||||
{
|
action: () =>
|
||||||
name: 'License',
|
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
||||||
description: manifest.license,
|
},
|
||||||
icon: '@tui.chevron-right',
|
{
|
||||||
action: () => this.markdown.subscribe(),
|
name: 'License',
|
||||||
},
|
description: manifest.license,
|
||||||
{
|
icon: '@tui.chevron-right',
|
||||||
name: 'Website',
|
action: () => this.markdown.subscribe(),
|
||||||
description: manifest.marketingSite || FALLBACK_URL,
|
},
|
||||||
},
|
],
|
||||||
{
|
Links: [
|
||||||
name: 'Donation Link',
|
{
|
||||||
description: manifest.donationUrl || FALLBACK_URL,
|
name: 'Installed From',
|
||||||
},
|
description: pkg.registry || FALLBACK_URL,
|
||||||
{
|
},
|
||||||
name: 'Source Repository',
|
{
|
||||||
description: manifest.upstreamRepo,
|
name: 'Service Repository',
|
||||||
},
|
description: manifest.upstreamRepo,
|
||||||
{
|
},
|
||||||
name: 'Support Site',
|
{
|
||||||
description: manifest.supportSite || FALLBACK_URL,
|
name: 'Package Repository',
|
||||||
},
|
description: manifest.wrapperRepo,
|
||||||
{
|
},
|
||||||
name: 'Registry',
|
{
|
||||||
description: pkg.registry || FALLBACK_URL,
|
name: 'Marketing Site',
|
||||||
},
|
description: manifest.marketingSite || FALLBACK_URL,
|
||||||
{
|
},
|
||||||
name: 'Binary Source',
|
{
|
||||||
description: manifest.wrapperRepo,
|
name: 'Support Site',
|
||||||
},
|
description: manifest.supportSite || FALLBACK_URL,
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
name: 'Donation Link',
|
||||||
|
description: manifest.donationUrl || FALLBACK_URL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,24 +12,11 @@ import { StandardActionsService } from 'src/app/services/standard-actions.servic
|
|||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
import { ServiceActionComponent } from '../components/action.component'
|
import { ServiceActionComponent } from '../components/action.component'
|
||||||
|
|
||||||
const OTHER = 'Other Custom Actions'
|
const OTHER = 'Custom Actions'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (package(); as pkg) {
|
@if (package(); as pkg) {
|
||||||
<section class="g-card">
|
|
||||||
<header>Standard Actions</header>
|
|
||||||
<button
|
|
||||||
tuiCell
|
|
||||||
[action]="rebuild"
|
|
||||||
(click)="service.rebuild(pkg.manifest.id)"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
tuiCell
|
|
||||||
[action]="uninstall"
|
|
||||||
(click)="service.uninstall(pkg.manifest)"
|
|
||||||
></button>
|
|
||||||
</section>
|
|
||||||
@for (group of pkg.actions | keyvalue; track $index) {
|
@for (group of pkg.actions | keyvalue; track $index) {
|
||||||
@if (group.value.length) {
|
@if (group.value.length) {
|
||||||
<section class="g-card">
|
<section class="g-card">
|
||||||
@@ -46,6 +33,19 @@ const OTHER = 'Other Custom Actions'
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
<section class="g-card">
|
||||||
|
<header>Standard Actions</header>
|
||||||
|
<button
|
||||||
|
tuiCell
|
||||||
|
[action]="rebuild"
|
||||||
|
(click)="service.rebuild(pkg.manifest.id)"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
tuiCell
|
||||||
|
[action]="uninstall"
|
||||||
|
(click)="service.uninstall(pkg.manifest)"
|
||||||
|
></button>
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -80,8 +80,8 @@ export default class ServiceActionsRoute {
|
|||||||
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
||||||
>(
|
>(
|
||||||
(acc, [id]) => {
|
(acc, [id]) => {
|
||||||
const action = { id, ...pkg.actions[id] }
|
const action = { id, ...pkg.actions[id]! }
|
||||||
const group = pkg.actions[id].group || OTHER
|
const group = pkg.actions[id]?.group || OTHER
|
||||||
const current = acc[group] || []
|
const current = acc[group] || []
|
||||||
|
|
||||||
return { ...acc, [group]: current.concat(action) }
|
return { ...acc, [group]: current.concat(action) }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { NgTemplateOutlet } from '@angular/common'
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -11,7 +10,7 @@ import { RouterLink } from '@angular/router'
|
|||||||
import { getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
import { TuiItem } from '@taiga-ui/cdk'
|
import { TuiItem } from '@taiga-ui/cdk'
|
||||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||||
import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
|
import { TuiBreadcrumbs } from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||||
@@ -59,8 +58,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
TuiBreadcrumbs,
|
TuiBreadcrumbs,
|
||||||
TuiItem,
|
TuiItem,
|
||||||
TuiLink,
|
TuiLink,
|
||||||
TuiBadge,
|
|
||||||
NgTemplateOutlet,
|
|
||||||
InterfaceStatusComponent,
|
InterfaceStatusComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -84,11 +81,16 @@ export default class ServiceInterfaceRoute {
|
|||||||
|
|
||||||
const { serviceInterfaces, hosts } = pkg
|
const { serviceInterfaces, hosts } = pkg
|
||||||
const item = serviceInterfaces[this.interfaceId()]
|
const item = serviceInterfaces[this.interfaceId()]
|
||||||
const host = hosts[item.addressInfo.hostId]
|
const key = item?.addressInfo.hostId || ''
|
||||||
|
const host = hosts[key]
|
||||||
|
|
||||||
|
if (!host || !item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
public: host.bindings[item.addressInfo.internalPort].net.public,
|
public: !!host?.bindings[item.addressInfo.internalPort]?.net.public,
|
||||||
addresses: getAddresses(item, host, this.config),
|
addresses: getAddresses(item, host, this.config),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { ActivatedRoute } from '@angular/router'
|
|||||||
import { isEmptyObject } from '@start9labs/shared'
|
import { isEmptyObject } from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { map } from 'rxjs'
|
import { map, of } from 'rxjs'
|
||||||
import { UptimeComponent } from 'src/app/routes/portal/components/uptime.component'
|
import { UptimeComponent } from 'src/app/routes/portal/components/uptime.component'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
@@ -31,31 +32,36 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
template: `
|
template: `
|
||||||
<service-status
|
<service-status
|
||||||
[connected]="!!connected()"
|
[connected]="!!connected()"
|
||||||
[installingInfo]="pkg().stateInfo.installingInfo"
|
[installingInfo]="pkg()?.stateInfo?.installingInfo"
|
||||||
[status]="status()"
|
[status]="status()"
|
||||||
>
|
>
|
||||||
@if ($any(pkg().status).started; as started) {
|
@if ($any(pkg()?.status)?.started; as started) {
|
||||||
<p class="g-secondary" [appUptime]="started"></p>
|
<p class="g-secondary" [appUptime]="started"></p>
|
||||||
}
|
}
|
||||||
@if (installed() && connected()) {
|
@if (installed() && connected() && pkg(); as pkg) {
|
||||||
<service-controls [pkg]="pkg()" [status]="status()" />
|
<service-controls [pkg]="pkg" [status]="status()" />
|
||||||
}
|
}
|
||||||
</service-status>
|
</service-status>
|
||||||
|
|
||||||
@if (installed()) {
|
@if (installed() && pkg(); as pkg) {
|
||||||
@if (pkg().status.main === 'error') {
|
@if (pkg.status.main === 'error') {
|
||||||
<service-error [pkg]="pkg()" />
|
<service-error [pkg]="pkg" />
|
||||||
|
}
|
||||||
|
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||||
|
@if (errors() | async; as errors) {
|
||||||
|
<service-dependencies
|
||||||
|
[pkg]="pkg"
|
||||||
|
[services]="services()"
|
||||||
|
[errors]="errors"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<service-interfaces [pkg]="pkg()" [disabled]="status() !== 'running'" />
|
|
||||||
<service-dependencies [pkg]="pkg()" [services]="services()" />
|
|
||||||
<service-health-checks [checks]="health()" />
|
<service-health-checks [checks]="health()" />
|
||||||
<service-action-requests [pkg]="pkg()" [services]="services()" />
|
<service-action-requests [pkg]="pkg" [services]="services() || {}" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (installing()) {
|
@if (installing() && pkg(); as pkg) {
|
||||||
@for (
|
@for (
|
||||||
item of pkg().stateInfo.installingInfo?.progress?.phases;
|
item of pkg.stateInfo.installingInfo?.progress?.phases;
|
||||||
track $index
|
track $index
|
||||||
) {
|
) {
|
||||||
<p [progress]="item.progress">{{ item.name }}</p>
|
<p [progress]="item.progress">{{ item.name }}</p>
|
||||||
@@ -100,6 +106,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceRoute {
|
export class ServiceRoute {
|
||||||
|
private readonly errorService = inject(DepErrorService)
|
||||||
protected readonly connected = toSignal(inject(ConnectionService))
|
protected readonly connected = toSignal(inject(ConnectionService))
|
||||||
|
|
||||||
protected readonly id = toSignal(
|
protected readonly id = toSignal(
|
||||||
@@ -111,10 +118,14 @@ export class ServiceRoute {
|
|||||||
{ initialValue: {} as Record<string, PackageDataEntry> },
|
{ initialValue: {} as Record<string, PackageDataEntry> },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
protected readonly errors = computed((id = this.id()) =>
|
||||||
|
id ? this.errorService.getPkgDepErrors$(id) : of({}),
|
||||||
|
)
|
||||||
|
|
||||||
protected readonly pkg = computed(() => this.services()[this.id() || ''])
|
protected readonly pkg = computed(() => this.services()[this.id() || ''])
|
||||||
|
|
||||||
protected readonly health = computed(() =>
|
protected readonly health = computed((pkg = this.pkg()) =>
|
||||||
this.pkg() ? toHealthCheck(this.pkg().status) : [],
|
pkg ? toHealthCheck(pkg.status) : [],
|
||||||
)
|
)
|
||||||
|
|
||||||
protected readonly status = computed((pkg = this.pkg()) =>
|
protected readonly status = computed((pkg = this.pkg()) =>
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ export default class SystemAcmeComponent {
|
|||||||
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||||
map(acme =>
|
map(acme =>
|
||||||
Object.keys(acme).map(url => {
|
Object.keys(acme).map(url => {
|
||||||
const contact = acme[url].contact.map(mailto =>
|
const contact =
|
||||||
mailto.replace('mailto:', ''),
|
acme[url]?.contact.map(mailto => mailto.replace('mailto:', '')) ||
|
||||||
)
|
[]
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
contact,
|
contact,
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export class BackupsBackupComponent {
|
|||||||
// existing backup
|
// existing backup
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
argon2.verify(entry.startOs[id].passwordHash!, password)
|
argon2.verify(entry.startOs[id]?.passwordHash!, password)
|
||||||
await this.createBackup(password)
|
await this.createBackup(password)
|
||||||
} catch {
|
} catch {
|
||||||
this.oldPassword(password)
|
this.oldPassword(password)
|
||||||
@@ -156,7 +156,7 @@ export class BackupsBackupComponent {
|
|||||||
|
|
||||||
private async oldPassword(password: string) {
|
private async oldPassword(password: string) {
|
||||||
const { id } = await getServerInfo(this.patch)
|
const { id } = await getServerInfo(this.patch)
|
||||||
const { passwordHash } = this.context.data.entry.startOs[id]
|
const { passwordHash = '' } = this.context.data.entry.startOs[id] || {}
|
||||||
|
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open<string>(PROMPT, PASSWORD_OPTIONS)
|
.open<string>(PROMPT, PASSWORD_OPTIONS)
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ export class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasThisBackup({ startOs }: BackupTarget, id: string): boolean {
|
hasThisBackup({ startOs }: BackupTarget, id: string): boolean {
|
||||||
|
const item = startOs[id]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
startOs[id] &&
|
!!item &&
|
||||||
Version.parse(startOs[id].version).compare(Version.parse('0.3.6')) !==
|
Version.parse(item.version).compare(Version.parse('0.3.6')) !== 'less'
|
||||||
'less'
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import { ActivatedRoute } from '@angular/router'
|
|||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { ISB } from '@start9labs/start-sdk'
|
import { ISB } from '@start9labs/start-sdk'
|
||||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
import { TuiAlertService, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiAlertService, TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||||
import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit'
|
import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
|
||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
@@ -232,7 +231,7 @@ export class BackupNetworkComponent {
|
|||||||
...value,
|
...value,
|
||||||
})
|
})
|
||||||
|
|
||||||
target.entry = Object.values(res)[0]
|
target.entry = Object.values(res)[0]!
|
||||||
this.service.cifs.update(cifs => [...cifs])
|
this.service.cifs.update(cifs => [...cifs])
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -273,7 +272,13 @@ export class BackupNetworkComponent {
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [[id, entry]] = Object.entries(await this.api.addBackupTarget(v))
|
const [item] = Object.entries(await this.api.addBackupTarget(v))
|
||||||
|
const [id, entry] = item || []
|
||||||
|
|
||||||
|
if (!id || !entry) {
|
||||||
|
throw 'Invalid response from server'
|
||||||
|
}
|
||||||
|
|
||||||
const hasAnyBackup = this.service.hasAnyBackup(entry)
|
const hasAnyBackup = this.service.hasAnyBackup(entry)
|
||||||
const added = { id, entry, hasAnyBackup }
|
const added = { id, entry, hasAnyBackup }
|
||||||
this.service.cifs.update(cifs => [added, ...cifs])
|
this.service.cifs.update(cifs => [added, ...cifs])
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ export class BackupsRecoverComponent {
|
|||||||
|
|
||||||
return Object.keys(backups)
|
return Object.keys(backups)
|
||||||
.map(id => ({
|
.map(id => ({
|
||||||
...backups[id],
|
...backups[id]!,
|
||||||
id,
|
id,
|
||||||
installed: !!packageData[id],
|
installed: !!packageData[id],
|
||||||
checked: false,
|
checked: false,
|
||||||
newerOs:
|
newerOs:
|
||||||
Version.parse(backups[id].osVersion).compare(
|
Version.parse(backups[id]?.osVersion || '').compare(
|
||||||
Version.parse(this.config.version),
|
Version.parse(this.config.version),
|
||||||
) === 'greater',
|
) === 'greater',
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ export class SnekComponent implements AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
handleTouchStart(evt: TouchEvent) {
|
handleTouchStart(evt: TouchEvent) {
|
||||||
const firstTouch = this.getTouches(evt)[0]
|
const firstTouch = this.getTouches(evt)[0]
|
||||||
this.xDown = firstTouch.clientX
|
this.xDown = firstTouch?.clientX
|
||||||
this.yDown = firstTouch.clientY
|
this.yDown = firstTouch?.clientY
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTouchMove(evt: TouchEvent) {
|
handleTouchMove(evt: TouchEvent) {
|
||||||
@@ -151,8 +151,8 @@ export class SnekComponent implements AfterViewInit, OnDestroy {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var xUp = evt.touches[0].clientX
|
var xUp = evt.touches[0]?.clientX || 0
|
||||||
var yUp = evt.touches[0].clientY
|
var yUp = evt.touches[0]?.clientY || 0
|
||||||
|
|
||||||
var xDiff = this.xDown - xUp
|
var xDiff = this.xDown - xUp
|
||||||
var yDiff = this.yDown - yUp
|
var yDiff = this.yDown - yUp
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { EOSService } from 'src/app/services/eos.service'
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<h2 style="margin-top: 0">StartOS {{ versions[0].version }}</h2>
|
<h2 style="margin-top: 0">StartOS {{ versions[0]?.version }}</h2>
|
||||||
<h3 style="color: var(--tui-text-secondary); font-weight: normal">
|
<h3 style="color: var(--tui-text-secondary); font-weight: normal">
|
||||||
Release Notes
|
Release Notes
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default class StartOsUiComponent {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map(host => ({
|
map(host => ({
|
||||||
...iface,
|
...iface,
|
||||||
public: host.bindings[iface.addressInfo.internalPort].net.public,
|
public: !!host.bindings[iface.addressInfo.internalPort]?.net.public,
|
||||||
addresses: getAddresses(iface, host, this.config),
|
addresses: getAddresses(iface, host, this.config),
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -70,7 +70,13 @@ export default class SystemSessionsComponent {
|
|||||||
private readonly sessions$ = from(this.api.getSessions({}))
|
private readonly sessions$ = from(this.api.getSessions({}))
|
||||||
private readonly local$ = new Subject<readonly SessionWithId[]>()
|
private readonly local$ = new Subject<readonly SessionWithId[]>()
|
||||||
|
|
||||||
readonly current$ = this.sessions$.pipe(map(s => [s.sessions[s.current]]))
|
readonly current$ = this.sessions$.pipe(
|
||||||
|
map(s => {
|
||||||
|
const current = s.sessions[s.current]
|
||||||
|
|
||||||
|
return current ? [current] : []
|
||||||
|
}),
|
||||||
|
)
|
||||||
readonly other$: Observable<readonly SessionWithId[]> = merge(
|
readonly other$: Observable<readonly SessionWithId[]> = merge(
|
||||||
this.local$,
|
this.local$,
|
||||||
this.sessions$.pipe(
|
this.sessions$.pipe(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
StoreIconComponentModule,
|
StoreIconComponentModule,
|
||||||
StoreIdentity,
|
StoreIdentity,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { TuiTable } from '@taiga-ui/addon-table'
|
|
||||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||||
import { TuiButton, TuiNotification, TuiTitle } from '@taiga-ui/core'
|
import { TuiButton, TuiNotification, TuiTitle } from '@taiga-ui/core'
|
||||||
import {
|
import {
|
||||||
@@ -88,7 +87,10 @@ interface UpdatesData {
|
|||||||
</aside>
|
</aside>
|
||||||
<section class="g-subpage">
|
<section class="g-subpage">
|
||||||
@if (data()?.errors?.includes(current()?.url || '')) {
|
@if (data()?.errors?.includes(current()?.url || '')) {
|
||||||
<tui-notification appearance="negative">
|
<tui-notification
|
||||||
|
appearance="negative"
|
||||||
|
[style.margin-block-end.rem]="1"
|
||||||
|
>
|
||||||
Request Failed
|
Request Failed
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
}
|
}
|
||||||
@@ -222,7 +224,9 @@ export default class UpdatesComponent {
|
|||||||
combineLatest({
|
combineLatest({
|
||||||
hosts: this.marketplaceService
|
hosts: this.marketplaceService
|
||||||
.getKnownHosts$(true)
|
.getKnownHosts$(true)
|
||||||
.pipe(tap(([store]) => !this.isMobile && this.current.set(store))),
|
.pipe(
|
||||||
|
tap(([store]) => !this.isMobile && store && this.current.set(store)),
|
||||||
|
),
|
||||||
marketplace: this.marketplaceService.marketplace$,
|
marketplace: this.marketplaceService.marketplace$,
|
||||||
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
|
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
|
||||||
.watch$('packageData')
|
.watch$('packageData')
|
||||||
|
|||||||
@@ -501,9 +501,9 @@ export class MockApiService extends ApiService {
|
|||||||
): Promise<GetPackageRes> {
|
): Promise<GetPackageRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
if (!versionRange || versionRange === '=*') {
|
if (!versionRange || versionRange === '=*') {
|
||||||
return Mock.RegistryPackages[id]
|
return Mock.RegistryPackages[id]!
|
||||||
} else {
|
} else {
|
||||||
return Mock.OtherPackageVersions[id][versionRange]
|
return Mock.OtherPackageVersions[id]![versionRange]!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,7 +870,7 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
this.mockRevision([
|
this.mockRevision([
|
||||||
{
|
{
|
||||||
...appPatch[0],
|
...appPatch[0]!,
|
||||||
value: 'stopped',
|
value: 'stopped',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -1055,18 +1055,18 @@ export class MockApiService extends ApiService {
|
|||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/packageData/${params.id}`,
|
path: `/packageData/${params.id}`,
|
||||||
value: {
|
value: {
|
||||||
...Mock.LocalPkgs[params.id],
|
...Mock.LocalPkgs[params.id]!,
|
||||||
stateInfo: {
|
stateInfo: {
|
||||||
// if installing
|
// if installing
|
||||||
// state: 'installing',
|
// state: 'installing',
|
||||||
|
|
||||||
// if updating
|
// if updating
|
||||||
state: 'updating',
|
state: 'updating',
|
||||||
manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
|
manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!,
|
||||||
|
|
||||||
// both
|
// both
|
||||||
installingInfo: {
|
installingInfo: {
|
||||||
newManifest: Mock.LocalPkgs[params.id].stateInfo.manifest,
|
newManifest: Mock.LocalPkgs[params.id]?.stateInfo.manifest!,
|
||||||
progress: PROGRESS,
|
progress: PROGRESS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1123,11 +1123,11 @@ export class MockApiService extends ApiService {
|
|||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/packageData/${id}`,
|
path: `/packageData/${id}`,
|
||||||
value: {
|
value: {
|
||||||
...Mock.LocalPkgs[id],
|
...Mock.LocalPkgs[id]!,
|
||||||
stateInfo: {
|
stateInfo: {
|
||||||
state: 'restoring',
|
state: 'restoring',
|
||||||
installingInfo: {
|
installingInfo: {
|
||||||
newManifest: Mock.LocalPkgs[id].stateInfo.manifest!,
|
newManifest: Mock.LocalPkgs[id]?.stateInfo.manifest!,
|
||||||
progress: PROGRESS,
|
progress: PROGRESS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1640,7 +1640,7 @@ export class MockApiService extends ApiService {
|
|||||||
) {
|
) {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
progress.phases[i].progress = true
|
progress.phases[i]!.progress = true
|
||||||
|
|
||||||
if (
|
if (
|
||||||
progress.overall &&
|
progress.overall &&
|
||||||
@@ -1671,7 +1671,7 @@ export class MockApiService extends ApiService {
|
|||||||
if (phase.progress.done === phase.progress.total) {
|
if (phase.progress.done === phase.progress.total) {
|
||||||
await pauseFor(250)
|
await pauseFor(250)
|
||||||
|
|
||||||
progress.phases[i].progress = true
|
progress.phases[i]!.progress = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1777,7 +1777,7 @@ export class MockApiService extends ApiService {
|
|||||||
path: `/packageData/${id}/stateInfo`,
|
path: `/packageData/${id}/stateInfo`,
|
||||||
value: {
|
value: {
|
||||||
state: 'installed',
|
state: 'installed',
|
||||||
manifest: Mock.LocalPkgs[id].stateInfo.manifest,
|
manifest: Mock.LocalPkgs[id]?.stateInfo.manifest!,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export class BadgeService {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
!curr[id] ||
|
!curr[id] ||
|
||||||
(p.stateInfo.installingInfo && !curr[id].stateInfo.installingInfo)
|
(p.stateInfo.installingInfo &&
|
||||||
|
!curr[id]?.stateInfo.installingInfo)
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -70,7 +71,7 @@ export class BadgeService {
|
|||||||
local[id] &&
|
local[id] &&
|
||||||
this.exver.compareExver(
|
this.exver.compareExver(
|
||||||
version,
|
version,
|
||||||
getManifest(local[id]).version,
|
getManifest(local[id]!).version,
|
||||||
) === 1
|
) === 1
|
||||||
? result.add(id)
|
? result.add(id)
|
||||||
: result,
|
: result,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export class ConfigService {
|
|||||||
(this.hostname.startsWith('192.168.') ||
|
(this.hostname.startsWith('192.168.') ||
|
||||||
this.hostname.startsWith('10.') ||
|
this.hostname.startsWith('10.') ||
|
||||||
(this.hostname.startsWith('172.') &&
|
(this.hostname.startsWith('172.') &&
|
||||||
!![this.hostname.split('.').map(Number)[1]].filter(
|
!![this.hostname.split('.').map(Number)[1] || NaN].filter(
|
||||||
n => n >= 16 && n < 32,
|
n => n >= 16 && n < 32,
|
||||||
).length))
|
).length))
|
||||||
}
|
}
|
||||||
@@ -107,13 +107,14 @@ export class ConfigService {
|
|||||||
if (!host) return ''
|
if (!host) return ''
|
||||||
|
|
||||||
let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort]
|
let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort]
|
||||||
hostnameInfo = hostnameInfo.filter(
|
hostnameInfo =
|
||||||
h =>
|
hostnameInfo?.filter(
|
||||||
this.isLocalhost() ||
|
h =>
|
||||||
h.kind !== 'ip' ||
|
this.isLocalhost() ||
|
||||||
h.hostname.kind !== 'ipv6' ||
|
h.kind !== 'ip' ||
|
||||||
!h.hostname.value.startsWith('fe80::'),
|
h.hostname.kind !== 'ipv6' ||
|
||||||
)
|
!h.hostname.value.startsWith('fe80::'),
|
||||||
|
) || []
|
||||||
if (this.isLocalhost()) {
|
if (this.isLocalhost()) {
|
||||||
const local = hostnameInfo.find(
|
const local = hostnameInfo.find(
|
||||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { inject, Injectable } from '@angular/core'
|
|||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||||
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
|
import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { defaultIfEmpty, filter, firstValueFrom } from 'rxjs'
|
import {
|
||||||
|
defaultIfEmpty,
|
||||||
|
defer,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
of,
|
||||||
|
switchMap,
|
||||||
|
} from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getAllPackages } from 'src/app/utils/get-package-data'
|
import { getAllPackages } from 'src/app/utils/get-package-data'
|
||||||
@@ -20,53 +27,16 @@ export class ControlsService {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
|
||||||
async start(manifest: T.Manifest, unmet: boolean): Promise<void> {
|
async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
|
||||||
const deps = `${manifest.title} has unmet dependencies. It will not work as expected.`
|
const deps = `${title} has unmet dependencies. It will not work as expected.`
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!unmet || (await this.alert(deps))) &&
|
(unmet && !(await this.alert(deps))) ||
|
||||||
(!manifest.alerts.start || (await this.alert(manifest.alerts.start)))
|
(alerts.start && !(await this.alert(alerts.start)))
|
||||||
) {
|
) {
|
||||||
this.doStart(manifest.id)
|
return
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop({ id, title, alerts }: T.Manifest): Promise<void> {
|
|
||||||
let content = alerts.stop || ''
|
|
||||||
|
|
||||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
|
||||||
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
|
|
||||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content) {
|
|
||||||
this.dialogs
|
|
||||||
.open(TUI_CONFIRM, getOptions(content, 'Stop'))
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.doStop(id))
|
|
||||||
} else {
|
|
||||||
this.doStop(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async restart({ id, title }: T.Manifest): Promise<void> {
|
|
||||||
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
|
||||||
this.dialogs
|
|
||||||
.open(
|
|
||||||
TUI_CONFIRM,
|
|
||||||
getOptions(
|
|
||||||
`Services that depend on ${title} may temporarily experiences issues`,
|
|
||||||
'Restart',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.doRestart(id))
|
|
||||||
} else {
|
|
||||||
this.doRestart(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async doStart(id: string): Promise<void> {
|
|
||||||
const loader = this.loader.open(`Starting...`).subscribe()
|
const loader = this.loader.open(`Starting...`).subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -78,28 +48,55 @@ export class ControlsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doStop(id: string): Promise<void> {
|
async stop({ id, title, alerts }: T.Manifest) {
|
||||||
const loader = this.loader.open(`Stopping...`).subscribe()
|
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
|
||||||
|
let content = alerts.stop || ''
|
||||||
|
|
||||||
try {
|
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||||
await this.api.stopPackage({ id })
|
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer(() =>
|
||||||
|
content
|
||||||
|
? this.dialogs
|
||||||
|
.open(TUI_CONFIRM, getOptions(content, 'Stop'))
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
: of(null),
|
||||||
|
).subscribe(async () => {
|
||||||
|
const loader = this.loader.open(`Stopping...`).subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.stopPackage({ id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doRestart(id: string): Promise<void> {
|
async restart({ id, title }: T.Manifest) {
|
||||||
const loader = this.loader.open(`Restarting...`).subscribe()
|
const packages = await getAllPackages(this.patch)
|
||||||
|
const options = getOptions(
|
||||||
|
`Services that depend on ${title} may temporarily experiences issues`,
|
||||||
|
'Restart',
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
defer(() =>
|
||||||
await this.api.restartPackage({ id })
|
hasCurrentDeps(id, packages)
|
||||||
} catch (e: any) {
|
? this.dialogs.open(TUI_CONFIRM, options).pipe(filter(Boolean))
|
||||||
this.errorService.handleError(e)
|
: of(null),
|
||||||
} finally {
|
).subscribe(async () => {
|
||||||
loader.unsubscribe()
|
const loader = this.loader.open(`Restarting...`).subscribe()
|
||||||
}
|
|
||||||
|
try {
|
||||||
|
await this.api.restartPackage({ id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private alert(content: string): Promise<boolean> {
|
private alert(content: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class DepErrorService {
|
|||||||
): PkgDependencyErrors {
|
): PkgDependencyErrors {
|
||||||
const pkg = pkgs[pkgId]
|
const pkg = pkgs[pkgId]
|
||||||
|
|
||||||
if (!isInstalled(pkg)) return {}
|
if (!pkg || !isInstalled(pkg)) return {}
|
||||||
|
|
||||||
return currentDeps(pkgs, pkgId).reduce(
|
return currentDeps(pkgs, pkgId).reduce(
|
||||||
(innerErrors, depId): PkgDependencyErrors => ({
|
(innerErrors, depId): PkgDependencyErrors => ({
|
||||||
@@ -88,17 +88,14 @@ export class DepErrorService {
|
|||||||
|
|
||||||
const currentDep = pkg.currentDependencies[depId]
|
const currentDep = pkg.currentDependencies[depId]
|
||||||
const depManifest = dep.stateInfo.manifest
|
const depManifest = dep.stateInfo.manifest
|
||||||
|
const expected = currentDep?.versionRange || ''
|
||||||
|
|
||||||
// incorrect version
|
// incorrect version
|
||||||
if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) {
|
if (!this.exver.satisfies(depManifest.version, expected)) {
|
||||||
if (
|
if (depManifest.satisfies.some(v => !this.exver.satisfies(v, expected))) {
|
||||||
depManifest.satisfies.some(
|
|
||||||
v => !this.exver.satisfies(v, currentDep.versionRange),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
|
expected,
|
||||||
type: 'incorrectVersion',
|
type: 'incorrectVersion',
|
||||||
expected: currentDep.versionRange,
|
|
||||||
received: depManifest.version,
|
received: depManifest.version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,10 +125,10 @@ export class DepErrorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// health check failure
|
// health check failure
|
||||||
if (depStatus === 'running' && currentDep.kind === 'running') {
|
if (depStatus === 'running' && currentDep?.kind === 'running') {
|
||||||
for (let id of currentDep.healthChecks) {
|
for (let id of currentDep.healthChecks) {
|
||||||
const check = dep.status.health[id]
|
const check = dep.status.health[id]
|
||||||
if (check?.result !== 'success') {
|
if (check && check?.result !== 'success') {
|
||||||
return {
|
return {
|
||||||
type: 'healthChecksFailed',
|
type: 'healthChecksFailed',
|
||||||
check,
|
check,
|
||||||
@@ -142,7 +139,7 @@ export class DepErrorService {
|
|||||||
|
|
||||||
// transitive
|
// transitive
|
||||||
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
|
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
|
||||||
Object.values(outerErrors[transitiveId]).some(err => !!err),
|
Object.values(outerErrors[transitiveId] || {}).some(err => !!err),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (transitiveError) {
|
if (transitiveError) {
|
||||||
|
|||||||
@@ -42,9 +42,12 @@ export class FormService {
|
|||||||
const selected = valid ? value?.selection : spec.default
|
const selected = valid ? value?.selection : spec.default
|
||||||
const selection = this.getUnionSelectSpec(spec, selected)
|
const selection = this.getUnionSelectSpec(spec, selected)
|
||||||
const group = this.getFormGroup({ selection })
|
const group = this.getFormGroup({ selection })
|
||||||
const control = selected ? spec.variants[selected].spec : {}
|
const control = selected ? spec.variants[selected]?.spec : {}
|
||||||
|
|
||||||
group.setControl('value', this.getFormGroup(control, [], value?.value))
|
group.setControl(
|
||||||
|
'value',
|
||||||
|
this.getFormGroup(control || {}, [], value?.value),
|
||||||
|
)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
@@ -410,7 +413,12 @@ function listObjEquals(
|
|||||||
if (!uniqueBy) {
|
if (!uniqueBy) {
|
||||||
return false
|
return false
|
||||||
} else if (typeof uniqueBy === 'string') {
|
} else if (typeof uniqueBy === 'string') {
|
||||||
return uniqueByEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
const uniqueBySpec = spec.spec[uniqueBy]
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!uniqueBySpec &&
|
||||||
|
uniqueByEquals(uniqueBySpec, val1[uniqueBy], val2[uniqueBy])
|
||||||
|
)
|
||||||
} else if ('any' in uniqueBy) {
|
} else if ('any' in uniqueBy) {
|
||||||
for (let unique of uniqueBy.any) {
|
for (let unique of uniqueBy.any) {
|
||||||
if (listObjEquals(unique, spec, val1, val2)) {
|
if (listObjEquals(unique, spec, val1, val2)) {
|
||||||
@@ -522,8 +530,12 @@ export function convertValuesRecursive(
|
|||||||
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
|
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
|
||||||
} else if (valueSpec.type === 'union') {
|
} else if (valueSpec.type === 'union') {
|
||||||
const formGr = group.get(key) as UntypedFormGroup
|
const formGr = group.get(key) as UntypedFormGroup
|
||||||
const spec = valueSpec.variants[formGr.controls['selection'].value].spec
|
const value = formGr.controls['selection']?.value
|
||||||
convertValuesRecursive(spec, formGr)
|
const spec = !!value && valueSpec.variants[value]?.spec
|
||||||
|
|
||||||
|
if (spec) {
|
||||||
|
convertValuesRecursive(spec, formGr)
|
||||||
|
}
|
||||||
} else if (valueSpec.type === 'list') {
|
} else if (valueSpec.type === 'list') {
|
||||||
const formArr = group.get(key) as UntypedFormArray
|
const formArr = group.get(key) as UntypedFormArray
|
||||||
const { controls } = formArr
|
const { controls } = formArr
|
||||||
|
|||||||
@@ -249,20 +249,23 @@ export class MarketplaceService {
|
|||||||
flavor: string | null,
|
flavor: string | null,
|
||||||
pkgInfo: GetPackageRes,
|
pkgInfo: GetPackageRes,
|
||||||
): MarketplacePkg {
|
): MarketplacePkg {
|
||||||
version =
|
const ver =
|
||||||
version ||
|
version ||
|
||||||
Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) ||
|
Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) ||
|
||||||
null
|
null
|
||||||
|
const best = ver && pkgInfo.best[ver]
|
||||||
|
|
||||||
return !version || !pkgInfo.best[version]
|
if (!best) {
|
||||||
? ({} as MarketplacePkg)
|
return {} as MarketplacePkg
|
||||||
: {
|
}
|
||||||
id,
|
|
||||||
version,
|
return {
|
||||||
flavor,
|
id,
|
||||||
...pkgInfo,
|
flavor,
|
||||||
...pkgInfo.best[version],
|
version: ver || '',
|
||||||
}
|
...pkgInfo,
|
||||||
|
...best,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestErrors$(): Observable<string[]> {
|
getRequestErrors$(): Observable<string[]> {
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ export function toAcmeUrl(name: string): string {
|
|||||||
return knownACME.find(acme => acme.name === name)?.url || name
|
return knownACME.find(acme => acme.name === name)?.url || name
|
||||||
}
|
}
|
||||||
|
|
||||||
export const knownACME: {
|
export const knownACME = [
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
}[] = [
|
|
||||||
{
|
{
|
||||||
name: `Let's Encrypt`,
|
name: `Let's Encrypt`,
|
||||||
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||||
@@ -18,4 +15,4 @@ export const knownACME: {
|
|||||||
name: `Let's Encrypt (Staging)`,
|
name: `Let's Encrypt (Staging)`,
|
||||||
url: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
url: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
||||||
},
|
},
|
||||||
]
|
] as const
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ export function dryUpdate(
|
|||||||
Object.keys(pkg.currentDependencies || {}).some(
|
Object.keys(pkg.currentDependencies || {}).some(
|
||||||
pkgId => pkgId === id,
|
pkgId => pkgId === id,
|
||||||
) &&
|
) &&
|
||||||
!exver.satisfies(version, pkg.currentDependencies[id].versionRange),
|
!exver.satisfies(
|
||||||
|
version,
|
||||||
|
pkg.currentDependencies[id]?.versionRange || '',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map(pkg => getManifest(pkg).title)
|
.map(pkg => getManifest(pkg).title)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export function getMenu() {
|
|||||||
const badge = inject(BadgeService)
|
const badge = inject(BadgeService)
|
||||||
|
|
||||||
return Object.keys(SYSTEM_UTILITIES).map(key => ({
|
return Object.keys(SYSTEM_UTILITIES).map(key => ({
|
||||||
name: SYSTEM_UTILITIES[key].title,
|
name: SYSTEM_UTILITIES[key]?.title || '',
|
||||||
icon: SYSTEM_UTILITIES[key].icon,
|
icon: SYSTEM_UTILITIES[key]?.icon || '',
|
||||||
routerLink: key,
|
routerLink: key,
|
||||||
badge: toSignal(badge.getCount(key), { initialValue: 0 }),
|
badge: toSignal(badge.getCount(key), { initialValue: 0 }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ hr {
|
|||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.g-aside {
|
.g-aside {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user