mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
multiple bugs and better port forward ux
This commit is contained in:
@@ -701,4 +701,7 @@ export default {
|
||||
771: 'Spiel vorbei',
|
||||
772: 'Beliebige Taste drücken oder tippen zum Starten',
|
||||
773: 'Beliebige Taste drücken oder tippen zum Neustarten',
|
||||
774: 'Der Portstatus kann nicht ermittelt werden, solange der Dienst nicht läuft',
|
||||
775: 'Diese Adresse funktioniert nicht aus Ihrem lokalen Netzwerk aufgrund einer Router-Hairpinning-Einschränkung',
|
||||
776: 'Aktion nicht gefunden',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -701,4 +701,7 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Game Over': 771,
|
||||
'Press any key or tap to start': 772,
|
||||
'Press any key or tap to play again': 773,
|
||||
'Port status cannot be determined while service is not running': 774,
|
||||
'This address will not work from your local network due to a router hairpinning limitation': 775,
|
||||
'Action not found': 776,
|
||||
}
|
||||
|
||||
@@ -701,4 +701,7 @@ export default {
|
||||
771: 'Fin del juego',
|
||||
772: 'Pulsa cualquier tecla o toca para empezar',
|
||||
773: 'Pulsa cualquier tecla o toca para jugar de nuevo',
|
||||
774: 'El estado del puerto no se puede determinar mientras el servicio no está en ejecución',
|
||||
775: 'Esta dirección no funcionará desde tu red local debido a una limitación de hairpinning del router',
|
||||
776: 'Acción no encontrada',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -701,4 +701,7 @@ export default {
|
||||
771: 'Partie terminée',
|
||||
772: "Appuyez sur une touche ou touchez l'écran pour commencer",
|
||||
773: "Appuyez sur une touche ou touchez l'écran pour rejouer",
|
||||
774: "L'état du port ne peut pas être déterminé tant que le service n'est pas en cours d'exécution",
|
||||
775: "Cette adresse ne fonctionnera pas depuis votre réseau local en raison d'une limitation de hairpinning du routeur",
|
||||
776: 'Action introuvable',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -701,4 +701,7 @@ export default {
|
||||
771: 'Koniec gry',
|
||||
772: 'Naciśnij dowolny klawisz lub dotknij, aby rozpocząć',
|
||||
773: 'Naciśnij dowolny klawisz lub dotknij, aby zagrać ponownie',
|
||||
774: 'Status portu nie może być określony, gdy usługa nie jest uruchomiona',
|
||||
775: 'Ten adres nie będzie działać z Twojej sieci lokalnej z powodu ograniczenia hairpinning routera',
|
||||
776: 'Nie znaleziono akcji',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -287,9 +287,12 @@ export class AddressActionsComponent {
|
||||
}
|
||||
|
||||
showDnsValidation() {
|
||||
const port = this.address().hostnameInfo.port
|
||||
if (port === null) return
|
||||
this.domainHealth.showPublicDomainSetup(
|
||||
this.address().hostnameInfo.hostname,
|
||||
this.gatewayId(),
|
||||
port,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,8 @@ export class InterfaceAddressesComponent {
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}).map(f => f.toLocaleLowerCase()),
|
||||
...(iface.addSsl
|
||||
? {
|
||||
authority: ISB.Value.select({
|
||||
name: this.i18n.transform('Certificate Authority'),
|
||||
description: this.i18n.transform(
|
||||
@@ -179,6 +181,8 @@ export class InterfaceAddressesComponent {
|
||||
values: authorities,
|
||||
default: Object.keys(network.acme)[0] || 'local',
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -250,7 +254,13 @@ export class InterfaceAddressesComponent {
|
||||
await this.api.osUiAddPublicDomain(params)
|
||||
}
|
||||
|
||||
await this.domainHealth.checkPublicDomain(fqdn, gatewayId)
|
||||
const port = this.gatewayGroup().addresses.find(
|
||||
a => a.access === 'public' && a.hostnameInfo.port !== null,
|
||||
)?.hostnameInfo.port
|
||||
|
||||
if (port !== undefined && port !== null) {
|
||||
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, port)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
tuiSwitchOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
|
||||
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -28,7 +30,7 @@ export type DomainValidationData = {
|
||||
fqdn: string
|
||||
gateway: DnsGateway
|
||||
port: number
|
||||
initialResults?: { dnsPass: boolean; portPass: boolean }
|
||||
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null }
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -93,18 +95,12 @@ export type DomainValidationData = {
|
||||
{{ 'create this port forwarding rule' | i18n }}
|
||||
</p>
|
||||
|
||||
@let portRes = portResult();
|
||||
|
||||
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (portLoading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (portPass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (portPass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
<port-check-icon [result]="portRes" [loading]="portLoading()" />
|
||||
</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
@@ -121,6 +117,8 @@ export type DomainValidationData = {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<port-check-warnings [result]="portRes" />
|
||||
|
||||
@if (!isManualMode) {
|
||||
<footer class="g-buttons padding-top">
|
||||
<button
|
||||
@@ -217,6 +215,8 @@ export type DomainValidationData = {
|
||||
TuiButtonLoading,
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
PortCheckIconComponent,
|
||||
PortCheckWarningsComponent,
|
||||
],
|
||||
})
|
||||
export class DomainValidationComponent {
|
||||
@@ -234,11 +234,16 @@ export class DomainValidationComponent {
|
||||
readonly dnsLoading = signal(false)
|
||||
readonly portLoading = signal(false)
|
||||
readonly dnsPass = signal<boolean | undefined>(undefined)
|
||||
readonly portPass = signal<boolean | undefined>(undefined)
|
||||
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
|
||||
|
||||
readonly allPass = computed(
|
||||
() => this.dnsPass() === true && this.portPass() === true,
|
||||
readonly allPass = computed(() => {
|
||||
const result = this.portResult()
|
||||
return (
|
||||
this.dnsPass() === true &&
|
||||
!!result?.openInternally &&
|
||||
!!result?.openExternally
|
||||
)
|
||||
})
|
||||
|
||||
readonly isManualMode = !this.context.data.initialResults
|
||||
|
||||
@@ -246,7 +251,7 @@ export class DomainValidationComponent {
|
||||
const initial = this.context.data.initialResults
|
||||
if (initial) {
|
||||
this.dnsPass.set(initial.dnsPass)
|
||||
this.portPass.set(initial.portPass)
|
||||
if (initial.portResult) this.portResult.set(initial.portResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +280,7 @@ export class DomainValidationComponent {
|
||||
port: this.context.data.port,
|
||||
})
|
||||
|
||||
this.portPass.set(result.reachable)
|
||||
this.portResult.set(result)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { DialogService, ErrorService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -15,28 +16,36 @@ export class DomainHealthService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
async checkPublicDomain(fqdn: string, gatewayId: string): Promise<void> {
|
||||
async checkPublicDomain(
|
||||
fqdn: string,
|
||||
gatewayId: string,
|
||||
port: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const gateway = await this.getGatewayData(gatewayId)
|
||||
if (!gateway) return
|
||||
|
||||
const [dnsPass, portPass] = await Promise.all([
|
||||
const [dnsPass, portResult] = await Promise.all([
|
||||
this.api
|
||||
.queryDns({ fqdn })
|
||||
.then(ip => ip === gateway.ipInfo.wanIp)
|
||||
.catch(() => false),
|
||||
this.api
|
||||
.checkPort({ gateway: gatewayId, port: 443 })
|
||||
.then(r => r.reachable)
|
||||
.catch(() => false),
|
||||
.checkPort({ gateway: gatewayId, port })
|
||||
.catch((): null => null),
|
||||
])
|
||||
|
||||
if (!dnsPass || !portPass) {
|
||||
const portOk =
|
||||
!!portResult?.openInternally &&
|
||||
!!portResult?.openExternally &&
|
||||
!!portResult?.hairpinning
|
||||
|
||||
if (!dnsPass || !portOk) {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.openPublicDomainModal(fqdn, gateway, 443, {
|
||||
this.openPublicDomainModal(fqdn, gateway, port, {
|
||||
dnsPass,
|
||||
portPass,
|
||||
portResult,
|
||||
}),
|
||||
250,
|
||||
)
|
||||
@@ -66,12 +75,16 @@ export class DomainHealthService {
|
||||
}
|
||||
}
|
||||
|
||||
async showPublicDomainSetup(fqdn: string, gatewayId: string): Promise<void> {
|
||||
async showPublicDomainSetup(
|
||||
fqdn: string,
|
||||
gatewayId: string,
|
||||
port: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const gateway = await this.getGatewayData(gatewayId)
|
||||
if (!gateway) return
|
||||
|
||||
this.openPublicDomainModal(fqdn, gateway, 443)
|
||||
this.openPublicDomainModal(fqdn, gateway, port)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
@@ -82,14 +95,18 @@ export class DomainHealthService {
|
||||
const gateway = await this.getGatewayData(gatewayId)
|
||||
if (!gateway) return
|
||||
|
||||
const portPass = await this.api
|
||||
const portResult = await this.api
|
||||
.checkPort({ gateway: gatewayId, port })
|
||||
.then(r => r.reachable)
|
||||
.catch(() => false)
|
||||
.catch((): null => null)
|
||||
|
||||
if (!portPass) {
|
||||
const portOk =
|
||||
!!portResult?.openInternally &&
|
||||
!!portResult?.openExternally &&
|
||||
!!portResult?.hairpinning
|
||||
|
||||
if (!portOk) {
|
||||
setTimeout(
|
||||
() => this.openPortForwardModal(gateway, port, { portPass }),
|
||||
() => this.openPortForwardModal(gateway, port, { portResult }),
|
||||
250,
|
||||
)
|
||||
}
|
||||
@@ -133,7 +150,7 @@ export class DomainHealthService {
|
||||
fqdn: string,
|
||||
gateway: DnsGateway,
|
||||
port: number,
|
||||
initialResults?: { dnsPass: boolean; portPass: boolean },
|
||||
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null },
|
||||
) {
|
||||
this.dialog
|
||||
.openComponent(DOMAIN_VALIDATION, {
|
||||
@@ -147,7 +164,7 @@ export class DomainHealthService {
|
||||
private openPortForwardModal(
|
||||
gateway: DnsGateway,
|
||||
port: number,
|
||||
initialResults?: { portPass: boolean },
|
||||
initialResults?: { portResult: T.CheckPortRes | null },
|
||||
) {
|
||||
this.dialog
|
||||
.openComponent(PORT_FORWARD_VALIDATION, {
|
||||
|
||||
@@ -29,7 +29,9 @@ import { DomainHealthService } from './domain-health.service'
|
||||
tuiSwitch
|
||||
size="s"
|
||||
[showIcons]="false"
|
||||
[disabled]="toggling() || address.hostnameInfo.metadata.kind === 'mdns'"
|
||||
[disabled]="
|
||||
toggling() || address.hostnameInfo.metadata.kind === 'mdns'
|
||||
"
|
||||
[ngModel]="address.enabled"
|
||||
(ngModelChange)="onToggleEnabled()"
|
||||
/>
|
||||
@@ -207,10 +209,11 @@ export class InterfaceAddressItemComponent {
|
||||
|
||||
if (enabled) {
|
||||
const kind = addr.hostnameInfo.metadata.kind
|
||||
if (kind === 'public-domain') {
|
||||
if (kind === 'public-domain' && addr.hostnameInfo.port !== null) {
|
||||
await this.domainHealth.checkPublicDomain(
|
||||
addr.hostnameInfo.hostname,
|
||||
this.gatewayId(),
|
||||
addr.hostnameInfo.port,
|
||||
)
|
||||
} else if (kind === 'private-domain') {
|
||||
await this.domainHealth.checkPrivateDomain(this.gatewayId())
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { ErrorService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiButtonLoading } from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
|
||||
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DnsGateway } from './dns.component'
|
||||
@@ -15,7 +19,7 @@ import { DnsGateway } from './dns.component'
|
||||
export type PortForwardValidationData = {
|
||||
gateway: DnsGateway
|
||||
port: number
|
||||
initialResults?: { portPass: boolean }
|
||||
initialResults?: { portResult: T.CheckPortRes | null }
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -30,18 +34,12 @@ export type PortForwardValidationData = {
|
||||
{{ 'create this port forwarding rule' | i18n }}
|
||||
</p>
|
||||
|
||||
@let portRes = portResult();
|
||||
|
||||
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (loading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (pass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (pass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
<port-check-icon [result]="portRes" [loading]="loading()" />
|
||||
</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
@@ -53,19 +51,21 @@ export type PortForwardValidationData = {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<port-check-warnings [result]="portRes" />
|
||||
|
||||
@if (!isManualMode) {
|
||||
<footer class="g-buttons padding-top">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[disabled]="pass() === true"
|
||||
[disabled]="portOk()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Later' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="pass() !== true"
|
||||
[disabled]="!portOk()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Done' | i18n }}
|
||||
@@ -132,8 +132,8 @@ export type PortForwardValidationData = {
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
TuiButtonLoading,
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
PortCheckIconComponent,
|
||||
PortCheckWarningsComponent,
|
||||
],
|
||||
})
|
||||
export class PortForwardValidationComponent {
|
||||
@@ -144,14 +144,23 @@ export class PortForwardValidationComponent {
|
||||
injectContext<TuiDialogContext<void, PortForwardValidationData>>()
|
||||
|
||||
readonly loading = signal(false)
|
||||
readonly pass = signal<boolean | undefined>(undefined)
|
||||
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
|
||||
|
||||
readonly portOk = computed(() => {
|
||||
const result = this.portResult()
|
||||
return (
|
||||
!!result?.openInternally &&
|
||||
!!result?.openExternally &&
|
||||
!!result?.hairpinning
|
||||
)
|
||||
})
|
||||
|
||||
readonly isManualMode = !this.context.data.initialResults
|
||||
|
||||
constructor() {
|
||||
const initial = this.context.data.initialResults
|
||||
if (initial) {
|
||||
this.pass.set(initial.portPass)
|
||||
if (initial.portResult) this.portResult.set(initial.portResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +173,7 @@ export class PortForwardValidationComponent {
|
||||
port: this.context.data.port,
|
||||
})
|
||||
|
||||
this.pass.set(result.reachable)
|
||||
this.portResult.set(result)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'port-check-icon',
|
||||
template: `
|
||||
@if (loading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else {
|
||||
@let res = result();
|
||||
@if (res) {
|
||||
@if (!res.openInternally) {
|
||||
<tui-icon class="g-warning" icon="@tui.alert-triangle" />
|
||||
} @else if (!res.openExternally) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
}
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
tui-icon {
|
||||
font-size: 1.3rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiIcon, TuiLoader],
|
||||
})
|
||||
export class PortCheckIconComponent {
|
||||
readonly result = input<T.CheckPortRes>()
|
||||
readonly loading = input(false)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'port-check-warnings',
|
||||
template: `
|
||||
@let res = result();
|
||||
@if (res) {
|
||||
@if (!res.openInternally) {
|
||||
<p class="g-warning">
|
||||
{{
|
||||
'Port status cannot be determined while service is not running'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
@if (res.openExternally && !res.hairpinning) {
|
||||
<p class="g-warning">
|
||||
{{
|
||||
'This address will not work from your local network due to a router hairpinning limitation'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
p {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [i18nPipe],
|
||||
})
|
||||
export class PortCheckWarningsComponent {
|
||||
readonly result = input<T.CheckPortRes>()
|
||||
}
|
||||
@@ -19,7 +19,12 @@ import { ServiceTasksComponent } from 'src/app/routes/portal/routes/services/com
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getInstalledBaseStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import {
|
||||
ALLOWED_STATUSES,
|
||||
getInstalledBaseStatus,
|
||||
INACTIVE_STATUSES,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Component({
|
||||
@@ -49,6 +54,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
</td>
|
||||
<td class="g-secondary" [style.grid-row]="3">
|
||||
{{ task().reason || ('No reason provided' | i18n) }}
|
||||
@if (disabled()) {
|
||||
<div class="g-warning">{{ disabled() }}</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (task().severity !== 'critical') {
|
||||
@@ -66,7 +74,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
tuiIconButton
|
||||
iconStart="@tui.play"
|
||||
appearance="primary-success"
|
||||
[disabled]="!pkg()"
|
||||
[disabled]="!!disabled()"
|
||||
(click)="handle()"
|
||||
>
|
||||
{{ 'Run' | i18n }}
|
||||
@@ -113,7 +121,9 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { '[style.opacity]': 'pkg() ? null : "var(--tui-disabled-opacity)"' },
|
||||
host: {
|
||||
'[style.opacity]': '!disabled() ? null : "var(--tui-disabled-opacity)"',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiAvatar, i18nPipe, TuiFade],
|
||||
})
|
||||
@@ -124,6 +134,7 @@ export class ServiceTaskComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly tasks = inject(ServiceTasksComponent)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly task = input.required<T.Task & { replayId: string }>()
|
||||
readonly services = input.required<Record<string, PackageDataEntry>>()
|
||||
@@ -135,6 +146,28 @@ export class ServiceTaskComponent {
|
||||
() => this.tasks.pkg().currentDependencies[this.task().packageId],
|
||||
)
|
||||
|
||||
readonly disabled = computed(() => {
|
||||
const pkg = this.pkg()
|
||||
if (!pkg) return this.i18n.transform('Not installed')!
|
||||
|
||||
const action = pkg.actions[this.task().actionId]
|
||||
if (!action) return this.i18n.transform('Action not found')!
|
||||
|
||||
const status = renderPkgStatus(pkg).primary
|
||||
|
||||
if (INACTIVE_STATUSES.includes(status)) return status as string
|
||||
|
||||
if (!ALLOWED_STATUSES[action.allowedStatuses].has(status)) {
|
||||
return `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`
|
||||
}
|
||||
|
||||
if (typeof action.visibility === 'object') {
|
||||
return action.visibility.disabled
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
async dismiss() {
|
||||
const { packageId, replayId } = this.task()
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import { StandardActionsService } from 'src/app/services/standard-actions.servic
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
import {
|
||||
ALLOWED_STATUSES,
|
||||
INACTIVE_STATUSES,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
@@ -30,30 +32,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
const INACTIVE: PrimaryStatus[] = [
|
||||
'installing',
|
||||
'updating',
|
||||
'removing',
|
||||
'restoring',
|
||||
'backing-up',
|
||||
'error',
|
||||
]
|
||||
|
||||
const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
||||
'only-running': new Set(['running']),
|
||||
'only-stopped': new Set(['stopped']),
|
||||
any: new Set([
|
||||
'running',
|
||||
'stopped',
|
||||
'restarting',
|
||||
'restoring',
|
||||
'stopping',
|
||||
'starting',
|
||||
'backing-up',
|
||||
'task-required',
|
||||
]),
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (package(); as pkg) {
|
||||
@@ -286,6 +264,6 @@ export default class ServiceActionsRoute {
|
||||
}
|
||||
|
||||
protected readonly isInactive = computed(
|
||||
(pkg = this.package()) => !pkg || INACTIVE.includes(pkg.status),
|
||||
(pkg = this.package()) => !pkg || INACTIVE_STATUSES.includes(pkg.status),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ import {
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ErrorService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiButtonLoading } from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
|
||||
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
@@ -58,17 +61,13 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
|
||||
@for (iface of row.interfaces; track iface) {
|
||||
<div>{{ iface }}</div>
|
||||
}
|
||||
<port-check-warnings [result]="results()[i]" />
|
||||
</td>
|
||||
<td class="status">
|
||||
@if (loading()[i]) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (results()[i] === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (results()[i] === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
<port-check-icon
|
||||
[result]="results()[i]"
|
||||
[loading]="!!loading()[i]"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ row.externalPort }}</td>
|
||||
<td>{{ row.internalPort }}</td>
|
||||
@@ -103,11 +102,6 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tui-icon {
|
||||
font-size: 1.3rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 3.2rem;
|
||||
}
|
||||
@@ -145,8 +139,8 @@ function parseSocketAddr(s: string): { ip: string; port: number } {
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
PortCheckIconComponent,
|
||||
PortCheckWarningsComponent,
|
||||
TuiButtonLoading,
|
||||
],
|
||||
})
|
||||
@@ -159,7 +153,7 @@ export class PortForwardsModalComponent {
|
||||
injectContext<TuiDialogContext<void, PortForwardsModalData>>()
|
||||
|
||||
readonly loading = signal<Record<number, boolean>>({})
|
||||
readonly results = signal<Record<number, boolean>>({})
|
||||
readonly results = signal<Record<number, T.CheckPortRes>>({})
|
||||
|
||||
private readonly portForwards$ = combineLatest([
|
||||
this.patch.watch$('serverInfo', 'network', 'host', 'portForwards').pipe(
|
||||
@@ -254,7 +248,7 @@ export class PortForwardsModalComponent {
|
||||
port,
|
||||
})
|
||||
|
||||
this.results.update(r => ({ ...r, [index]: result.reachable }))
|
||||
this.results.update(r => ({ ...r, [index]: result }))
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace'
|
||||
import {
|
||||
FullKeyboard,
|
||||
pauseFor,
|
||||
RPCErrorDetails,
|
||||
SetLanguageParams,
|
||||
} from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
AddOperation,
|
||||
Dump,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
ReplaceOperation,
|
||||
Revision,
|
||||
} from 'patch-db-client'
|
||||
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import {
|
||||
DataModel,
|
||||
InstallingState,
|
||||
@@ -23,7 +26,9 @@ import {
|
||||
StateInfo,
|
||||
UpdatingState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace'
|
||||
import { toAuthorityUrl } from 'src/app/utils/acme'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { Mock } from './api.fixures'
|
||||
import {
|
||||
ActionRes,
|
||||
CheckDnsRes,
|
||||
@@ -44,13 +49,8 @@ import {
|
||||
ServerState,
|
||||
WebsocketConfig,
|
||||
} from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAuthorityUrl } from 'src/app/utils/acme'
|
||||
|
||||
import markdown from './md-sample.md'
|
||||
|
||||
@@ -521,7 +521,13 @@ export class MockApiService extends ApiService {
|
||||
async checkPort(params: T.CheckPortParams): Promise<T.CheckPortRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
return { ip: '0.0.0.0', port: params.port, reachable: false }
|
||||
return {
|
||||
ip: '0.0.0.0',
|
||||
port: params.port,
|
||||
openExternally: true,
|
||||
openInternally: false,
|
||||
hairpinning: true,
|
||||
}
|
||||
}
|
||||
|
||||
async checkDns(params: T.CheckDnsParams): Promise<CheckDnsRes> {
|
||||
|
||||
@@ -2,6 +2,30 @@ import { i18nKey } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export const INACTIVE_STATUSES: PrimaryStatus[] = [
|
||||
'installing',
|
||||
'updating',
|
||||
'removing',
|
||||
'restoring',
|
||||
'backing-up',
|
||||
'error',
|
||||
]
|
||||
|
||||
export const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
||||
'only-running': new Set(['running']),
|
||||
'only-stopped': new Set(['stopped']),
|
||||
any: new Set([
|
||||
'running',
|
||||
'stopped',
|
||||
'restarting',
|
||||
'restoring',
|
||||
'stopping',
|
||||
'starting',
|
||||
'backing-up',
|
||||
'task-required',
|
||||
]),
|
||||
}
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
health: T.HealthStatus | null
|
||||
|
||||
Reference in New Issue
Block a user