mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Misc (#3087)
* help ios downlaod .crt and add begin add masked for addresses * only require and show CA for public domain if addSsl * fix type and revert i18n const * feat: add address masking and adjust design (#3088) * feat: add address masking and adjust design * update lockfile * chore: move eye button to actions * chore: refresh notifications and handle action error * static width for health check name --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * hide certificate authorities tab * alpha.17 * add waiting health check status * remove "on" from waiting message * reject on abort in `.watch` * id migration: nostr -> nostr-rs-relay * health check waiting state * use interface type for launch button * better wording for masked * cleaner * sdk improvements * fix type error * fix notification badge issue --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
1169
web/package-lock.json
generated
1169
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.16",
|
||||
"version": "0.4.0-alpha.17",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"https://registry.start9.com/": "Start9 Registry",
|
||||
"https://community-registry.start9.com/": "Community Registry",
|
||||
"https://beta-registry.start9.com/": "Start9 Beta Registry",
|
||||
"https://community-beta-registry.start9.com/": "Community Beta Registry"
|
||||
"https://community-beta-registry.start9.com/": "Community Beta Registry",
|
||||
"https://alpha-registry-x.start9.com/": "Start9 Alpha Registry"
|
||||
},
|
||||
"startosRegistry": "https://beta-registry.start9.com/",
|
||||
"snakeHighScore": 0
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
i18nPipe,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -29,7 +28,6 @@ import { MenuComponent } from './menu.component'
|
||||
TuiButton,
|
||||
CategoriesModule,
|
||||
StoreIconComponentModule,
|
||||
TuiLet,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiSkeleton,
|
||||
|
||||
@@ -2,18 +2,11 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { ItemComponent } from './item.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ItemComponent],
|
||||
exports: [ItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedPipesModule,
|
||||
TickerComponent,
|
||||
TuiLet,
|
||||
],
|
||||
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent],
|
||||
})
|
||||
export class ItemModule {}
|
||||
|
||||
@@ -146,7 +146,9 @@ export default class SuccessPage implements AfterViewInit {
|
||||
.getElementById('cert')
|
||||
?.setAttribute(
|
||||
'href',
|
||||
`data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`,
|
||||
URL.createObjectURL(
|
||||
new Blob([this.cert!], { type: 'application/octet-stream' }),
|
||||
),
|
||||
)
|
||||
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, ElementRef, inject, input } from '@angular/core'
|
||||
import { Component, ElementRef, inject } from '@angular/core'
|
||||
import {
|
||||
INTERSECTION_ROOT,
|
||||
WA_INTERSECTION_ROOT,
|
||||
WaIntersectionObserver,
|
||||
} from '@ng-web-apis/intersection-observer'
|
||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||
@@ -36,12 +36,7 @@ import { SetupLogsService } from '../../services/setup-logs.service'
|
||||
NgDompurifyPipe,
|
||||
TuiScrollbar,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
providers: [{ provide: WA_INTERSECTION_ROOT, useExisting: ElementRef }],
|
||||
})
|
||||
export class LogsWindowComponent {
|
||||
readonly logs$ = inject(SetupLogsService)
|
||||
|
||||
@@ -40,7 +40,9 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</tui-textfield>
|
||||
<footer class="g-buttons">
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Directive, inject, DOCUMENT } from '@angular/core'
|
||||
import { Directive, DOCUMENT, inject } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
MutationObserverService,
|
||||
provideMutationObserverInit,
|
||||
WaMutationObserverService,
|
||||
} from '@ng-web-apis/mutation-observer'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
|
||||
@Directive({
|
||||
selector: '[safeLinks]',
|
||||
providers: [
|
||||
MutationObserverService,
|
||||
provideMutationObserverInit({
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}),
|
||||
WaMutationObserverService,
|
||||
provideMutationObserverInit({ childList: true, subtree: true }),
|
||||
],
|
||||
})
|
||||
export class SafeLinksDirective {
|
||||
private readonly doc = inject(DOCUMENT)
|
||||
private readonly el = tuiInjectElement()
|
||||
private readonly sub = inject(MutationObserverService)
|
||||
private readonly sub = inject(WaMutationObserverService)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
Array.from(this.doc.links)
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
91: 'Installierte Dienste',
|
||||
92: 'Diagnosen für den Tor-Daemon auf diesem Server',
|
||||
93: 'Fingerabdruck kopieren',
|
||||
94: 'Warten',
|
||||
95: 'Warten auf',
|
||||
96: 'Öffentliche Domain hinzufügen',
|
||||
97: 'Wird entfernt',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Keine Service-Schnittstellen',
|
||||
612: 'Grund',
|
||||
613: 'Private Gateways für die StartOS-Benutzeroberfläche können nicht deaktiviert werden',
|
||||
614: 'CA-Fingerabdruck',
|
||||
614: 'Root-CA',
|
||||
615: 'DHCP-Server',
|
||||
616: 'DHCP-Server können nicht bearbeitet werden',
|
||||
617: 'Statisch',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Eine andere Version auswählen',
|
||||
626: 'Hochladen',
|
||||
627: 'UI öffnen',
|
||||
628: 'In Zwischenablage kopiert',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -89,6 +89,9 @@ export const ENGLISH = {
|
||||
'Root CA Trusted!': 90,
|
||||
'Installed services': 91, // as in, software services installed on this computer
|
||||
'Diagnostics for the Tor daemon on this server': 92,
|
||||
'Copy fingerprint': 93, // as in the fingerprint of a root certificate authority
|
||||
'Waiting': 94,
|
||||
'Waiting on': 95, // as in "awaiting"
|
||||
'Add public domain': 96,
|
||||
'Removing': 97,
|
||||
'Unsaved changes': 100,
|
||||
@@ -577,7 +580,7 @@ export const ENGLISH = {
|
||||
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
|
||||
'Reason': 612, // as in, an explanation for something
|
||||
'Cannot disable private gateways for StartOS UI': 613,
|
||||
'CA fingerprint': 614, // as in, the unique, fixed-length digital identifier generated from a certificate's data using a cryptographic hash function
|
||||
'Root CA': 614, // as in, the unique, fixed-length digital identifier generated from a certificate's data using a cryptographic hash function
|
||||
'DHCP Servers': 615,
|
||||
'Cannot edit DHCP servers': 616,
|
||||
'Static': 617, // as in, unchanging
|
||||
@@ -591,4 +594,5 @@ export const ENGLISH = {
|
||||
'Select another version': 625,
|
||||
'Upload': 626, // as in, upload a file
|
||||
'Open UI': 627, // as in, upload a file
|
||||
'Copied to clipboard': 628,
|
||||
} as const
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: '¡CA raíz confiable!',
|
||||
91: 'Servicios instalados',
|
||||
92: 'Diagnósticos para el demonio Tor en este servidor',
|
||||
93: 'Copiar huella digital',
|
||||
94: 'Esperando',
|
||||
95: 'En espera de',
|
||||
96: 'Agregar dominio público',
|
||||
97: 'Eliminando',
|
||||
100: 'Cambios no guardados',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Sin interfaces de servicio',
|
||||
612: 'Razón',
|
||||
613: 'No se pueden deshabilitar las puertas de enlace privadas para la interfaz de usuario de StartOS',
|
||||
614: 'Huella digital de la CA',
|
||||
614: 'CA raíz',
|
||||
615: 'Servidores DHCP',
|
||||
616: 'No se pueden editar los servidores DHCP',
|
||||
617: 'Estático',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Seleccionar otra versión',
|
||||
626: 'Subir',
|
||||
627: 'Abrir UI',
|
||||
628: 'Copiado al portapapeles',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Certificat racine approuvé !',
|
||||
91: 'Services installés',
|
||||
92: 'Diagnostics pour le service Tor sur ce serveur',
|
||||
93: 'Copier l’empreinte',
|
||||
94: 'En attente',
|
||||
95: 'En attente de',
|
||||
96: 'Ajouter un domaine public',
|
||||
97: 'Suppression',
|
||||
100: 'Modifications non enregistrées',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Aucune interface de service',
|
||||
612: 'Raison',
|
||||
613: "Impossible de désactiver les passerelles privées pour l'interface utilisateur StartOS",
|
||||
614: 'Empreinte de l’AC',
|
||||
614: 'CA racine',
|
||||
615: 'Serveurs DHCP',
|
||||
616: 'Impossible de modifier les serveurs DHCP',
|
||||
617: 'Statique',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Sélectionner une autre version',
|
||||
626: 'Téléverser',
|
||||
627: 'Ouvrir UI',
|
||||
628: 'Copié dans le presse-papiers',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
91: 'Zainstalowane usługi',
|
||||
92: 'Diagnostyka demona Tor na tym serwerze',
|
||||
93: 'Kopiuj odcisk palca',
|
||||
94: 'Oczekiwanie',
|
||||
95: 'Oczekiwanie na',
|
||||
96: 'Dodaj domenę publiczną',
|
||||
97: 'Usuwanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Brak interfejsów usług',
|
||||
612: 'Powód',
|
||||
613: 'Nie można wyłączyć prywatnych bram dla interfejsu użytkownika StartOS',
|
||||
614: 'Odcisk palca CA',
|
||||
614: 'głównego CA',
|
||||
615: 'Serwery DHCP',
|
||||
616: 'Nie można edytować serwerów DHCP',
|
||||
617: 'Statyczny',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Wybierz inną wersję',
|
||||
626: 'Prześlij',
|
||||
627: 'Otwórz UI',
|
||||
628: 'Skopiowano do schowka',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, signal } from '@angular/core'
|
||||
import { tuiCreateToken, tuiProvide } from '@taiga-ui/cdk'
|
||||
import { forwardRef, InjectionToken, signal } from '@angular/core'
|
||||
import { tuiProvide } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiLanguageName,
|
||||
tuiLanguageSwitcher,
|
||||
@@ -11,12 +11,19 @@ import { i18nService } from './i18n.service'
|
||||
export type i18nKey = keyof typeof ENGLISH
|
||||
export type i18n = Record<(typeof ENGLISH)[i18nKey], string>
|
||||
|
||||
export const I18N = tuiCreateToken(signal<i18n | null>(null))
|
||||
export const I18N_LOADER =
|
||||
tuiCreateToken<(lang: TuiLanguageName) => Promise<i18n>>()
|
||||
export const I18N_STORAGE = tuiCreateToken<
|
||||
export const I18N = new InjectionToken('', {
|
||||
factory: () => signal<i18n | null>(null),
|
||||
})
|
||||
|
||||
export const I18N_LOADER = new InjectionToken<
|
||||
(lang: TuiLanguageName) => Promise<i18n>
|
||||
>('')
|
||||
|
||||
export const I18N_STORAGE = new InjectionToken<
|
||||
(lang: TuiLanguageName) => Promise<void>
|
||||
>(() => Promise.resolve())
|
||||
>('', {
|
||||
factory: () => () => Promise.resolve(),
|
||||
})
|
||||
|
||||
export const I18N_PROVIDERS = [
|
||||
tuiLanguageSwitcher(async (language: TuiLanguageName): Promise<unknown> => {
|
||||
|
||||
@@ -50,7 +50,6 @@ export * from './tokens/relative-url'
|
||||
|
||||
export * from './util/base-64'
|
||||
export * from './util/convert-ansi'
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/format-progress'
|
||||
export * from './util/get-new-entries'
|
||||
export * from './util/get-pkg-id'
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import { copyToClipboard } from '../util/copy-to-clipboard'
|
||||
|
||||
import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CopyService {
|
||||
private readonly clipboard = inject(Clipboard)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
|
||||
async copy(text: string) {
|
||||
const success = await copyToClipboard(text)
|
||||
const success = this.clipboard.copy(text)
|
||||
const message = success ? 'Copied to clipboard' : 'Failed'
|
||||
const appearance = success ? 'positive' : 'negative'
|
||||
|
||||
this.alerts
|
||||
.open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.')
|
||||
.subscribe()
|
||||
this.alerts.open(this.i18n.transform(message), { appearance }).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export class DownloadHTMLService {
|
||||
const elem = this.document.createElement('a')
|
||||
elem.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(html),
|
||||
URL.createObjectURL(
|
||||
new Blob([html], { type: 'application/octet-stream' }),
|
||||
),
|
||||
)
|
||||
elem.setAttribute('download', filename)
|
||||
elem.style.display = 'none'
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
HttpAngularOptions,
|
||||
HttpOptions,
|
||||
LocalHttpResponse,
|
||||
Method,
|
||||
} from '../types/http.types'
|
||||
import { RPCResponse, RPCOptions } from '../types/rpc.types'
|
||||
import { RELATIVE_URL } from '../tokens/relative-url'
|
||||
@@ -42,7 +41,7 @@ export class HttpService {
|
||||
const { method, headers, params, timeout } = opts
|
||||
|
||||
return this.httpRequest<RPCResponse<T>>({
|
||||
method: Method.POST,
|
||||
method: 'POST',
|
||||
url: fullUrl || this.relativeUrl,
|
||||
headers,
|
||||
body: { method, params },
|
||||
@@ -73,7 +72,7 @@ export class HttpService {
|
||||
}
|
||||
|
||||
let req: Observable<LocalHttpResponse<T>>
|
||||
if (method === Method.GET) {
|
||||
if (method === 'GET') {
|
||||
req = this.http.get(url, options as any) as any
|
||||
} else {
|
||||
req = this.http.post(url, body, options as any) as any
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http'
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
}
|
||||
export type Method = 'GET' | 'POST'
|
||||
|
||||
type ParamPrimitive = string | number | boolean
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export async function copyToClipboard(str: string): Promise<boolean> {
|
||||
if (window.isSecureContext) {
|
||||
return navigator.clipboard
|
||||
.writeText(str)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
const el = document.createElement('textarea')
|
||||
el.value = str
|
||||
el.setAttribute('readonly', '')
|
||||
el.style.position = 'absolute'
|
||||
el.style.left = '-9999px'
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
const didCopy = document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
return didCopy
|
||||
}
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { DialogService, ErrorService } from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { tuiMarkControlAsTouchedAndValidate, TuiValidator } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAlertService,
|
||||
|
||||
@@ -10,11 +10,11 @@ export class AuthService {
|
||||
private readonly storage = inject(WA_LOCAL_STORAGE)
|
||||
private readonly effect = effect(() => {
|
||||
if (this.authenticated()) {
|
||||
this.storage.setItem(KEY, JSON.stringify(true))
|
||||
this.storage?.setItem(KEY, JSON.stringify(true))
|
||||
} else {
|
||||
this.storage.removeItem(KEY)
|
||||
this.storage?.removeItem(KEY)
|
||||
}
|
||||
})
|
||||
|
||||
readonly authenticated = signal(Boolean(this.storage.getItem(KEY)))
|
||||
readonly authenticated = signal(Boolean(this.storage?.getItem(KEY)))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_DATE_FORMAT,
|
||||
TUI_DIALOGS_CLOSE,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, of, pairwise } from 'rxjs'
|
||||
import { filter, identity, of, pairwise } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
PATCH_CACHE,
|
||||
@@ -133,4 +134,10 @@ export const APP_PROVIDERS = [
|
||||
provide: VERSION,
|
||||
useFactory: () => inject(ConfigService).version,
|
||||
},
|
||||
tuiObfuscateOptionsProvider({
|
||||
recipes: {
|
||||
mask: ({ length }) => '•'.repeat(length),
|
||||
none: identity,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
forwardRef,
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
@@ -15,17 +15,13 @@ import {
|
||||
} from '@angular/forms'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiAnimated } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
TuiButton,
|
||||
TuiError,
|
||||
tuiFadeIn,
|
||||
tuiHeightCollapse,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
tuiParentStop,
|
||||
TuiTextfield,
|
||||
tuiToAnimationOptions,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiFieldErrorPipe, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
@@ -57,40 +53,40 @@ import { FormObjectComponent } from './object.component'
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@for (item of array.control.controls; track item) {
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
(remove)="removeAt($index)"
|
||||
/>
|
||||
}
|
||||
<div tuiAnimated class="control">
|
||||
<div>
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="array"
|
||||
tuiTextfieldSize="m"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
(remove)="removeAt($index)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@@ -145,11 +141,23 @@ import { FormObjectComponent } from './object.component'
|
||||
}
|
||||
|
||||
.control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
display: grid;
|
||||
|
||||
form-control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.tui-enter,
|
||||
&.tui-leave {
|
||||
animation-name: tuiFade, tuiCollapse;
|
||||
}
|
||||
}
|
||||
`,
|
||||
animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop],
|
||||
hostDirectives: [ControlDirective],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
@@ -166,14 +174,13 @@ import { FormObjectComponent } from './object.component'
|
||||
MustachePipe,
|
||||
FormControlComponent,
|
||||
forwardRef(() => FormObjectComponent),
|
||||
TuiAnimated,
|
||||
],
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly order = ERRORS
|
||||
readonly array = inject(FormArrayName)
|
||||
readonly open = new Map<AbstractControl, boolean>()
|
||||
@@ -181,6 +188,7 @@ export class FormArrayComponent {
|
||||
private warned = false
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly destroyRef = inject(DestroyRef)
|
||||
private readonly cdr = inject(ChangeDetectorRef)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
get canAdd(): boolean {
|
||||
@@ -226,5 +234,6 @@ export class FormArrayComponent {
|
||||
private addItem() {
|
||||
this.array.control.insert(0, this.formService.getListItem(this.spec))
|
||||
this.open.set(this.array.control.at(0), true)
|
||||
this.cdr.markForCheck()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure, TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import { TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import { TuiElasticContainer } from '@taiga-ui/kit'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
import { FormControlComponent } from './control.component'
|
||||
import { FormGroupComponent } from './group.component'
|
||||
|
||||
@@ -73,7 +72,6 @@ export class FormUnionComponent implements OnChanges {
|
||||
}
|
||||
|
||||
// OTHER?
|
||||
@tuiPure
|
||||
onUnion(union: string) {
|
||||
this.spec.others = this.spec.others || {}
|
||||
this.spec.others[this.union] = this.form.control.controls['value']?.value
|
||||
@@ -84,9 +82,7 @@ export class FormUnionComponent implements OnChanges {
|
||||
[],
|
||||
this.spec.others[union],
|
||||
),
|
||||
{
|
||||
emitEvent: false,
|
||||
},
|
||||
{ emitEvent: false },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TUI_LAST_DAY,
|
||||
TuiDay,
|
||||
TuiMapperPipe,
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
@@ -44,7 +43,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[ngModel]="getTime(value)"
|
||||
[ngModel]="value | tuiMapper: getTime"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@@ -128,7 +127,6 @@ export class FormDatetimeComponent extends Control<
|
||||
readonly min = TUI_FIRST_DAY
|
||||
readonly max = TUI_LAST_DAY
|
||||
|
||||
@tuiPure
|
||||
getTime(value: string | null) {
|
||||
return value ? TuiTime.fromString(value) : null
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import { invert } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { TuiChevron, TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-multiselect',
|
||||
template: `
|
||||
<tui-textfield multi [disabledItemHandler]="disabledItemHandler">
|
||||
<tui-textfield multi tuiChevron [disabledItemHandler]="disabledItemHandler">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>{{ spec.name }}</label>
|
||||
}
|
||||
@@ -43,6 +42,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
TuiChevron,
|
||||
],
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IST } from '@start9labs/start-sdk'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiDataList, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiDataListWrapper,
|
||||
TuiChevron,
|
||||
TuiFluidTypography,
|
||||
tuiFluidTypographyOptionsProvider,
|
||||
TuiSelect,
|
||||
@@ -19,6 +19,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
selector: 'form-select',
|
||||
template: `
|
||||
<tui-textfield
|
||||
tuiChevron
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
(tuiActiveZoneChange)="!$event && control.onTouched()"
|
||||
@@ -76,6 +77,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
TuiChevron,
|
||||
],
|
||||
})
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
@@ -53,7 +54,9 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
size="xs"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -63,6 +66,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
size="xs"
|
||||
title="Remove"
|
||||
class="remove"
|
||||
(pointerdown.prevent)="(0)"
|
||||
(click)="remove()"
|
||||
></button>
|
||||
@if (spec | hint; as hint) {
|
||||
@@ -76,7 +80,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:host-context(form-array > form-control > :host) .remove {
|
||||
:host-context(form-control.array > :host) .remove {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
@@ -87,6 +91,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiButton, TuiHint, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
@@ -35,14 +35,27 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
</div>
|
||||
<div tuiCell>
|
||||
<div tuiTitle>
|
||||
<strong>{{ 'CA fingerprint' | i18n }}</strong>
|
||||
<strong>{{ 'Root CA' | i18n }}</strong>
|
||||
<div tuiSubtitle tuiFade>{{ server.caFingerprint }}</div>
|
||||
</div>
|
||||
<a
|
||||
tuiIconButton
|
||||
download
|
||||
appearance="icon"
|
||||
iconStart="@tui.download"
|
||||
href="/static/local-root-ca.crt"
|
||||
[tuiHint]="'Download' | i18n"
|
||||
tuiHintDirection="bottom"
|
||||
>
|
||||
{{ 'Download' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(server.caFingerprint)"
|
||||
[tuiHint]="'Copy fingerprint' | i18n"
|
||||
tuiHintDirection="bottom"
|
||||
>
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
@@ -65,7 +78,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
`,
|
||||
styles: '[tuiCell] { padding-inline: 0; white-space: nowrap }',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiTitle, TuiButton, TuiCell, i18nPipe, TuiFade],
|
||||
imports: [TuiTitle, TuiButton, TuiCell, i18nPipe, TuiFade, TuiHint],
|
||||
})
|
||||
export class AboutComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
tuiFadeIn,
|
||||
TuiHint,
|
||||
TuiIcon,
|
||||
tuiScaleIn,
|
||||
tuiToAnimationOptions,
|
||||
tuiWidthCollapse,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiHint, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiBadgedContent, TuiBadgeNotification } from '@taiga-ui/kit'
|
||||
import { getMenu } from 'src/app/utils/system-utilities'
|
||||
|
||||
@@ -27,12 +19,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
[class.link_system]="item.routerLink === 'system'"
|
||||
[tuiHint]="rla.isActive ? '' : (item.name | i18n)"
|
||||
>
|
||||
<tui-badged-content
|
||||
[style.--tui-radius.%]="50"
|
||||
[@tuiFadeIn]="animation"
|
||||
[@tuiWidthCollapse]="animation"
|
||||
[@tuiScaleIn]="animation"
|
||||
>
|
||||
<tui-badged-content [style.--tui-radius.%]="50">
|
||||
@if (item.badge(); as badge) {
|
||||
<tui-badge-notification tuiSlot="top" size="s">
|
||||
{{ badge }}
|
||||
@@ -175,7 +162,6 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiBadgeNotification,
|
||||
@@ -188,6 +174,5 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
],
|
||||
})
|
||||
export class HeaderNavigationComponent {
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly utils = getMenu()
|
||||
}
|
||||
|
||||
@@ -3,36 +3,39 @@ import {
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
CopyService,
|
||||
DialogService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
} from '@start9labs/shared'
|
||||
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiButton,
|
||||
tuiButtonOptionsProvider,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiIcon,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
import { InterfaceAddressItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
template: `
|
||||
<div class="desktop">
|
||||
<button tuiIconButton appearance="flat-grayscale" (click)="viewDetails()">
|
||||
{{ 'Address details' | i18n }}
|
||||
<tui-icon class="info" icon="@tui.info" background="@tui.info-filled" />
|
||||
</button>
|
||||
@if (interface.value()?.type === 'ui') {
|
||||
@if (interface.address().masked) {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
[iconStart]="
|
||||
interface.currentlyMasked() ? '@tui.eye' : '@tui.eye-off'
|
||||
"
|
||||
(click)="interface.currentlyMasked.set(!interface.currentlyMasked())"
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (interface.address().ui) {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
@@ -67,15 +70,12 @@ import { InterfaceComponent } from '../interface.component'
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[tuiAppearanceState]="open() ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
>
|
||||
{{ 'Actions' | i18n }}
|
||||
<tui-data-list *tuiTextfieldDropdown="let close">
|
||||
<button tuiOption new iconStart="@tui.info" (click)="viewDetails()">
|
||||
{{ 'Address details' | i18n }}
|
||||
</button>
|
||||
@if (interface.value()?.type === 'ui') {
|
||||
<tui-data-list *tuiTextfieldDropdown (click)="open.set(false)">
|
||||
@if (interface.address().ui) {
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
@@ -87,6 +87,18 @@ import { InterfaceComponent } from '../interface.component'
|
||||
{{ 'Open' | i18n }}
|
||||
</a>
|
||||
}
|
||||
@if (interface.address().masked) {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.eye"
|
||||
(click)="
|
||||
interface.currentlyMasked.set(!interface.currentlyMasked())
|
||||
"
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button tuiOption new iconStart="@tui.qr-code" (click)="showQR()">
|
||||
{{ 'Show QR' | i18n }}
|
||||
</button>
|
||||
@@ -94,7 +106,7 @@ import { InterfaceComponent } from '../interface.component'
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(href()); close()"
|
||||
(click)="copyService.copy(href())"
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
</button>
|
||||
@@ -105,24 +117,12 @@ import { InterfaceComponent } from '../interface.component'
|
||||
styles: `
|
||||
:host {
|
||||
text-align: right;
|
||||
grid-area: 1 / 2 / 4 / 3;
|
||||
grid-area: 1/4/4/4;
|
||||
width: fit-content;
|
||||
place-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host-context(.uncommon-hidden) .desktop {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--tui-status-info);
|
||||
|
||||
&::after {
|
||||
mask-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
@@ -136,15 +136,19 @@ import { InterfaceComponent } from '../interface.component'
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tbody.uncommon-hidden) {
|
||||
.desktop {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
i18nPipe,
|
||||
TuiTextfield,
|
||||
TuiIcon,
|
||||
],
|
||||
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe, TuiTextfield],
|
||||
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -152,27 +156,13 @@ export class AddressActionsComponent {
|
||||
readonly isMobile = inject(TUI_IS_MOBILE)
|
||||
readonly dialog = inject(DialogService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
readonly interface = inject(InterfaceAddressItemComponent)
|
||||
readonly open = signal(false)
|
||||
|
||||
readonly href = input.required<string>()
|
||||
readonly bullets = input.required<string[]>()
|
||||
readonly disabled = input.required<boolean>()
|
||||
|
||||
open = false
|
||||
|
||||
viewDetails() {
|
||||
this.dialog
|
||||
.openAlert(
|
||||
`<ul>${this.bullets()
|
||||
.map(b => `<li>${b}</li>`)
|
||||
.join('')}</ul>` as i18nKey,
|
||||
{
|
||||
label: 'About this address' as i18nKey,
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
showQR() {
|
||||
this.dialog
|
||||
.openComponent(new PolymorpheusComponent(QRModal), {
|
||||
|
||||
@@ -15,12 +15,13 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
<header>{{ 'Addresses' | i18n }}</header>
|
||||
<tui-elastic-container>
|
||||
<table [appTable]="['Type', 'Access', 'Gateway', 'URL', null]">
|
||||
<th [style.width.rem]="2"></th>
|
||||
@for (address of addresses()?.common; track $index) {
|
||||
<tr [address]="address" [isRunning]="isRunning()"></tr>
|
||||
} @empty {
|
||||
@if (addresses()) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="6">
|
||||
<app-placeholder icon="@tui.list-x">
|
||||
{{ 'No addresses' | i18n }}
|
||||
</app-placeholder>
|
||||
@@ -39,7 +40,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
<tbody [class.uncommon-hidden]="!uncommon">
|
||||
@if (addresses()?.uncommon?.length && uncommon) {
|
||||
<tr [style.background]="'var(--tui-background-neutral-1)'">
|
||||
<td colspan="5"></td>
|
||||
<td colspan="6"></td>
|
||||
</tr>
|
||||
}
|
||||
@for (address of addresses()?.uncommon; track $index) {
|
||||
@@ -66,6 +67,20 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
</tui-elastic-container>
|
||||
`,
|
||||
styles: `
|
||||
:host ::ng-deep {
|
||||
th:nth-child(2) {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
th:nth-child(3) {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
th:nth-child(4) {
|
||||
width: 17rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g-table:has(caption) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
@@ -76,6 +91,13 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
[tuiButton] {
|
||||
border-radius: var(--tui-radius-xs);
|
||||
margin-block-end: 0.75rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiObfuscatePipe } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { DisplayAddress } from '../interface.service'
|
||||
import { AddressActionsComponent } from './actions.component'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[address]',
|
||||
template: `
|
||||
@if (address(); as address) {
|
||||
<td [style.padding-inline-end]="0">
|
||||
<div class="wrapper">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
(click)="viewDetails()"
|
||||
>
|
||||
{{ 'Address details' | i18n }}
|
||||
<tui-icon
|
||||
class="info"
|
||||
icon="@tui.info"
|
||||
background="@tui.info-filled"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="wrapper">{{ address.type }}</div>
|
||||
</td>
|
||||
@@ -26,13 +51,18 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td [style.order]="-1">
|
||||
<td [style.grid-area]="'1 / 1 / 1 / 3'">
|
||||
<div class="wrapper" [title]="address.gatewayName">
|
||||
{{ address.gatewayName || '-' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="wrapper" [title]="address.url">{{ address.url }}</div>
|
||||
<td [style.grid-area]="'3 / 1 / 3 / 3'">
|
||||
<div
|
||||
class="wrapper"
|
||||
[title]="address.masked && currentlyMasked() ? '' : address.url"
|
||||
>
|
||||
{{ address.url | tuiObfuscate: recipe() }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
actions
|
||||
@@ -46,20 +76,30 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
styles: `
|
||||
:host {
|
||||
white-space: nowrap;
|
||||
grid-template-columns: fit-content(10rem) 1fr 2rem 2rem;
|
||||
|
||||
td:last-child {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--tui-status-info);
|
||||
|
||||
&::after {
|
||||
mask-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.uncommon-hidden) {
|
||||
.wrapper {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
td {
|
||||
padding-block: 0;
|
||||
td,
|
||||
& {
|
||||
padding-block: 0 !important;
|
||||
border: hidden;
|
||||
}
|
||||
}
|
||||
@@ -76,23 +116,50 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
:host-context(tui-root._mobile) {
|
||||
td {
|
||||
width: auto !important;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
display: none;
|
||||
grid-area: 1 / 3 / 4 / 3;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-primary);
|
||||
padding-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [i18nPipe, AddressActionsComponent, TuiBadge],
|
||||
imports: [
|
||||
i18nPipe,
|
||||
AddressActionsComponent,
|
||||
TuiBadge,
|
||||
TuiObfuscatePipe,
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceAddressItemComponent {
|
||||
private readonly dialogs = inject(DialogService)
|
||||
|
||||
readonly address = input.required<DisplayAddress>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
readonly currentlyMasked = signal(true)
|
||||
readonly recipe = computed(() =>
|
||||
this.address()?.masked && this.currentlyMasked() ? 'mask' : 'none',
|
||||
)
|
||||
|
||||
viewDetails() {
|
||||
this.dialogs
|
||||
.openAlert(
|
||||
`<ul>${this.address()
|
||||
.bullets.map(b => `<li>${b}</li>`)
|
||||
.join('')}</ul>` as i18nKey,
|
||||
{ label: 'About this address' as i18nKey },
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
template: `
|
||||
<div>
|
||||
<section [gateways]="value()?.gateways"></section>
|
||||
<section [publicDomains]="value()?.publicDomains"></section>
|
||||
<section
|
||||
[publicDomains]="value()?.publicDomains"
|
||||
[addSsl]="value()?.addSsl || false"
|
||||
></section>
|
||||
<section [torDomains]="value()?.torDomains"></section>
|
||||
<section [privateDomains]="value()?.privateDomains"></section>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ type AddressWithInfo = {
|
||||
info: T.HostnameInfo
|
||||
gateway?: GatewayPlus
|
||||
showSsl: boolean
|
||||
masked: boolean
|
||||
ui: boolean
|
||||
}
|
||||
|
||||
function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
||||
@@ -130,6 +132,9 @@ export class InterfaceService {
|
||||
|
||||
if (!hostnamesInfos.length) return addresses
|
||||
|
||||
const masked = serviceInterface.masked
|
||||
const ui = serviceInterface.type === 'ui'
|
||||
|
||||
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(
|
||||
h => {
|
||||
const { url, sslUrl } = utils.addressHostToUrl(
|
||||
@@ -143,7 +148,14 @@ export class InterfaceService {
|
||||
: undefined
|
||||
const res = []
|
||||
if (url) {
|
||||
res.push({ url, info, gateway, showSsl: false })
|
||||
res.push({
|
||||
url,
|
||||
info,
|
||||
gateway,
|
||||
showSsl: false,
|
||||
masked,
|
||||
ui,
|
||||
})
|
||||
}
|
||||
if (sslUrl) {
|
||||
res.push({
|
||||
@@ -151,6 +163,8 @@ export class InterfaceService {
|
||||
info,
|
||||
gateway,
|
||||
showSsl: !!url,
|
||||
masked,
|
||||
ui,
|
||||
})
|
||||
}
|
||||
return res
|
||||
@@ -328,7 +342,7 @@ export class InterfaceService {
|
||||
}
|
||||
|
||||
private toDisplayAddress(
|
||||
{ info, url, gateway, showSsl }: AddressWithInfo,
|
||||
{ info, url, gateway, showSsl, masked, ui }: AddressWithInfo,
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
@@ -509,6 +523,8 @@ export class InterfaceService {
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
masked,
|
||||
ui,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -522,6 +538,7 @@ export type MappedServiceInterface = T.ServiceInterface & {
|
||||
common: DisplayAddress[]
|
||||
uncommon: DisplayAddress[]
|
||||
}
|
||||
addSsl: boolean
|
||||
}
|
||||
|
||||
export type InterfaceGateway = GatewayPlus & {
|
||||
@@ -534,4 +551,6 @@ export type DisplayAddress = {
|
||||
gatewayName: string | null
|
||||
url: string
|
||||
bullets: i18nKey[]
|
||||
masked: boolean
|
||||
ui: boolean
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import { InterfaceComponent } from './interface.component'
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove(domain)"
|
||||
[disabled]="!privateDomains()"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -31,7 +31,8 @@ import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="service.add()"
|
||||
(click)="service.add(addSsl())"
|
||||
[disabled]="!publicDomains()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
@@ -44,7 +45,7 @@ import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
} @else {
|
||||
<table [appTable]="['Domain', 'Gateway', 'Certificate Authority', null]">
|
||||
@for (domain of publicDomains(); track $index) {
|
||||
<tr [publicDomain]="domain"></tr>
|
||||
<tr [publicDomain]="domain" [addSsl]="addSsl()"></tr>
|
||||
} @empty {
|
||||
@for (_ of [0]; track $index) {
|
||||
<tr>
|
||||
@@ -79,4 +80,6 @@ export class PublicDomainsComponent {
|
||||
readonly service = inject(PublicDomainService)
|
||||
|
||||
readonly publicDomains = input.required<readonly PublicDomain[] | undefined>()
|
||||
|
||||
readonly addSsl = input.required<boolean>()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ import { toAuthorityName } from 'src/app/utils/acme'
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="service.edit(publicDomain())"
|
||||
(click)="service.edit(publicDomain(), addSsl())"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
@@ -107,8 +107,11 @@ export class PublicDomainsItemComponent {
|
||||
open = false
|
||||
|
||||
readonly publicDomain = input.required<PublicDomain>()
|
||||
readonly addSsl = input.required<boolean>()
|
||||
|
||||
readonly authority = computed(() => toAuthorityName(this.publicDomain().acme))
|
||||
readonly authority = computed(() =>
|
||||
toAuthorityName(this.publicDomain().acme, this.addSsl()),
|
||||
)
|
||||
readonly dnsMessage = computed<i18nKey>(
|
||||
() =>
|
||||
`Create one of the DNS records below to cause ${this.publicDomain().fqdn} to resolve to ${this.publicDomain().gateway?.ipInfo.wanIp}` as i18nKey,
|
||||
|
||||
@@ -58,7 +58,7 @@ export class PublicDomainService {
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
async add(addSsl: boolean) {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: this.i18n.transform('Domain'),
|
||||
@@ -69,7 +69,10 @@ export class PublicDomainService {
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}).map(f => f.toLocaleLowerCase()),
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
...this.gatewaySpec(),
|
||||
...(addSsl
|
||||
? this.authoritySpec()
|
||||
: ({} as ReturnType<typeof this.authoritySpec>)),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -87,9 +90,12 @@ export class PublicDomainService {
|
||||
})
|
||||
}
|
||||
|
||||
async edit(domain: PublicDomain) {
|
||||
async edit(domain: PublicDomain, addSsl: boolean) {
|
||||
const editSpec = ISB.InputSpec.of({
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
...this.gatewaySpec(),
|
||||
...(addSsl
|
||||
? this.authoritySpec()
|
||||
: ({} as ReturnType<typeof this.authoritySpec>)),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -155,7 +161,7 @@ export class PublicDomainService {
|
||||
private async save(
|
||||
fqdn: string,
|
||||
gatewayId: string,
|
||||
authority: 'local' | string,
|
||||
authority?: 'local' | string,
|
||||
) {
|
||||
const gateway = this.data()!.gateways.find(g => g.id === gatewayId)!
|
||||
|
||||
@@ -163,7 +169,7 @@ export class PublicDomainService {
|
||||
const params = {
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
acme: authority === 'local' ? null : authority,
|
||||
acme: !authority || authority === 'local' ? null : authority,
|
||||
}
|
||||
try {
|
||||
let ip: string | null
|
||||
@@ -225,7 +231,7 @@ export class PublicDomainService {
|
||||
}
|
||||
}
|
||||
|
||||
private gatewayAndAuthoritySpec() {
|
||||
private gatewaySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
const gateways = data.gateways.filter(
|
||||
@@ -251,6 +257,13 @@ export class PublicDomainService {
|
||||
.filter(g => !g.ipInfo.wanIp || utils.CGNAT.contains(g.ipInfo.wanIp))
|
||||
.map(g => g.id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
private authoritySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
return {
|
||||
authority: ISB.Value.select({
|
||||
name: this.i18n.transform('Certificate Authority'),
|
||||
description: this.i18n.transform(
|
||||
|
||||
@@ -50,6 +50,7 @@ type OnionForm = {
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
[disabled]="!torDomains()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, ElementRef, Input, ViewChild } from '@angular/core'
|
||||
import {
|
||||
INTERSECTION_ROOT,
|
||||
WA_INTERSECTION_ROOT,
|
||||
WaIntersectionObserver,
|
||||
} from '@ng-web-apis/intersection-observer'
|
||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||
@@ -31,12 +31,7 @@ import { LogsPipe } from './logs.pipe'
|
||||
LogsPipe,
|
||||
i18nPipe,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
providers: [{ provide: WA_INTERSECTION_ROOT, useExisting: ElementRef }],
|
||||
})
|
||||
export class LogsComponent {
|
||||
@ViewChild('bottom')
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
ErrorService,
|
||||
@@ -14,12 +15,15 @@ import {
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { distinctUntilChanged, skip } from 'rxjs/operators'
|
||||
import {
|
||||
RR,
|
||||
ServerNotification,
|
||||
ServerNotifications,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { BadgeService } from 'src/app/services/badge.service'
|
||||
import { NotificationService } from 'src/app/services/notification.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { NotificationsTableComponent } from './table.component'
|
||||
@@ -36,13 +40,13 @@ import { NotificationsTableComponent } from './table.component'
|
||||
iconStart="@tui.trash"
|
||||
appearance="primary-destructive"
|
||||
[style.margin]="'0 0.5rem 0 auto'"
|
||||
[disabled]="!tableNotifications()?.selected()?.length"
|
||||
[disabled]="!table()?.selected()?.length"
|
||||
(click)="remove(notifications() || [])"
|
||||
>
|
||||
{{ 'Delete selected' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<div #table [notifications]="notifications()"></div>
|
||||
<div [notifications]="notifications()"></div>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
@@ -64,20 +68,26 @@ export default class NotificationsComponent implements OnInit {
|
||||
readonly errorService = inject(ErrorService)
|
||||
readonly notifications = signal<ServerNotifications | null>(null)
|
||||
|
||||
protected tableNotifications =
|
||||
viewChild<NotificationsTableComponent<ServerNotification<number>>>('table')
|
||||
protected readonly table = viewChild<
|
||||
NotificationsTableComponent<ServerNotification<number>>
|
||||
>(NotificationsTableComponent)
|
||||
|
||||
protected readonly badge = inject(BadgeService)
|
||||
.getCount('notifications')
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(Boolean),
|
||||
skip(1),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(() => this.init())
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
|
||||
|
||||
if (isEmptyObject(params)) {
|
||||
this.getMore({}).then(() => {
|
||||
const latest = this.notifications()?.at(0)
|
||||
if (latest) {
|
||||
this.service.markSeenAll(latest.id)
|
||||
}
|
||||
})
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -93,7 +103,7 @@ export default class NotificationsComponent implements OnInit {
|
||||
|
||||
async remove(all: ServerNotifications) {
|
||||
const ids =
|
||||
this.tableNotifications()
|
||||
this.table()
|
||||
?.selected()
|
||||
.map(n => n.id) || []
|
||||
const loader = this.loader.open('Deleting').subscribe()
|
||||
@@ -107,4 +117,13 @@ export default class NotificationsComponent implements OnInit {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.getMore({}).then(() => {
|
||||
const latest = this.notifications()?.at(0)
|
||||
if (latest) {
|
||||
this.service.markSeenAll(latest.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
Input,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -11,15 +12,15 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
@Component({
|
||||
selector: 'tr[healthCheck]',
|
||||
template: `
|
||||
<td>{{ healthCheck.name }}</td>
|
||||
<td class="name">{{ healthCheck().name }}</td>
|
||||
<td>
|
||||
<span>
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<tui-loader size="m" />
|
||||
} @else {
|
||||
<tui-icon [icon]="icon" [style.color]="color" />
|
||||
<tui-icon [icon]="icon()" [style.color]="color()" />
|
||||
}
|
||||
{{ message }}
|
||||
{{ message() }}
|
||||
</span>
|
||||
</td>
|
||||
`,
|
||||
@@ -30,17 +31,25 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
td:first-child {
|
||||
font-weight: bold;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
td {
|
||||
width: 100%;
|
||||
|
||||
td:last-child {
|
||||
color: var(--tui-text-secondary);
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -48,19 +57,17 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
imports: [TuiLoader, TuiIcon],
|
||||
})
|
||||
export class ServiceHealthCheckComponent {
|
||||
@Input({ required: true })
|
||||
healthCheck!: T.NamedHealthCheckResult
|
||||
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
get loading(): boolean {
|
||||
const { result } = this.healthCheck
|
||||
readonly healthCheck = input.required<T.NamedHealthCheckResult>()
|
||||
|
||||
return !result || result === 'starting' || result === 'loading'
|
||||
}
|
||||
readonly loading = computed(
|
||||
({ result } = this.healthCheck()) =>
|
||||
!result || ['starting', 'loading', 'waiting'].includes(result),
|
||||
)
|
||||
|
||||
get icon(): string {
|
||||
switch (this.healthCheck.result) {
|
||||
readonly icon = computed(() => {
|
||||
switch (this.healthCheck().result) {
|
||||
case 'success':
|
||||
return '@tui.check'
|
||||
case 'failure':
|
||||
@@ -68,10 +75,10 @@ export class ServiceHealthCheckComponent {
|
||||
default:
|
||||
return '@tui.minus'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get color(): string {
|
||||
switch (this.healthCheck.result) {
|
||||
readonly color = computed(() => {
|
||||
switch (this.healthCheck().result) {
|
||||
case 'success':
|
||||
return 'var(--tui-status-positive)'
|
||||
case 'failure':
|
||||
@@ -79,26 +86,30 @@ export class ServiceHealthCheckComponent {
|
||||
case 'starting':
|
||||
case 'loading':
|
||||
return 'var(--tui-background-accent-1)'
|
||||
// disabled
|
||||
// disabled and waiting
|
||||
default:
|
||||
return 'var(--tui-text-secondary)'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get message(): string {
|
||||
if (!this.healthCheck.result) {
|
||||
readonly message = computed(({ result, message } = this.healthCheck()) => {
|
||||
if (!result) {
|
||||
return this.i18n.transform('Awaiting result')!
|
||||
}
|
||||
|
||||
switch (this.healthCheck.result) {
|
||||
switch (result) {
|
||||
case 'starting':
|
||||
return this.i18n.transform('Starting')!
|
||||
case 'success':
|
||||
return `${this.i18n.transform('Success')}: ${this.healthCheck.message || 'health check passing'}`
|
||||
return `${this.i18n.transform('Success')}: ${message || 'health check passing'}`
|
||||
case 'waiting':
|
||||
return message
|
||||
? `${this.i18n.transform('Waiting on')} ${message}...`
|
||||
: `${this.i18n.transform('Waiting')}...`
|
||||
case 'loading':
|
||||
case 'failure':
|
||||
case 'disabled':
|
||||
return this.healthCheck.message || this.healthCheck.result
|
||||
return message || result
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { ServiceUptimeComponent } from 'src/app/routes/portal/routes/services/components/uptime.component'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
@@ -19,17 +16,17 @@ import { StatusComponent } from './status.component'
|
||||
selector: 'tr[appService]',
|
||||
template: `
|
||||
<td [style.width.rem]="3" [style.grid-area]="'1 / 1 / 4'">
|
||||
<img alt="logo" [src]="pkg.icon" />
|
||||
<img alt="logo" [src]="pkg().icon" />
|
||||
</td>
|
||||
<td class="title">
|
||||
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||
<a [routerLink]="'/services/' + manifest().id">{{ manifest().title }}</a>
|
||||
</td>
|
||||
<td class="status" [style.grid-area]="'3 / 2'">
|
||||
<app-status [pkg]="pkg" [hasDepErrors]="hasError(depErrors)" />
|
||||
<app-status [pkg]="pkg()" [hasDepErrors]="hasError()" />
|
||||
</td>
|
||||
<td class="version">{{ manifest.version }}</td>
|
||||
<td class="version">{{ manifest().version }}</td>
|
||||
<td class="uptime">
|
||||
@if (pkg.statusInfo.started; as started) {
|
||||
@if (pkg().statusInfo.started; as started) {
|
||||
<span>{{ 'Uptime' | i18n }}:</span>
|
||||
<service-uptime [started]="started" />
|
||||
} @else {
|
||||
@@ -143,39 +140,15 @@ import { StatusComponent } from './status.component'
|
||||
}
|
||||
}
|
||||
`,
|
||||
hostDirectives: [RouterLink],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, StatusComponent, ServiceUptimeComponent, i18nPipe],
|
||||
})
|
||||
export class ServiceComponent implements OnChanges {
|
||||
private readonly link = inject(RouterLink)
|
||||
export class ServiceComponent {
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly depErrors = input<PkgDependencyErrors>({})
|
||||
|
||||
@Input()
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
depErrors?: PkgDependencyErrors
|
||||
|
||||
readonly connected$ = inject(ConnectionService)
|
||||
|
||||
get installed(): boolean {
|
||||
return this.pkg.stateInfo.state === 'installed'
|
||||
}
|
||||
|
||||
get manifest() {
|
||||
return getManifest(this.pkg)
|
||||
}
|
||||
|
||||
get routerLink() {
|
||||
return `/services/${this.manifest.id}`
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.link.routerLink = this.routerLink
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
hasError(errors: PkgDependencyErrors = {}): boolean {
|
||||
return Object.values(errors).some(Boolean)
|
||||
}
|
||||
readonly manifest = computed(() => getManifest(this.pkg()))
|
||||
readonly hasError = computed(() =>
|
||||
Object.values(this.depErrors()).some(Boolean),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,9 @@ import { RouterLink } from '@angular/router'
|
||||
@for (service of services() | tuiTableSort; track $index) {
|
||||
<tr
|
||||
appService
|
||||
[routerLink]="'/services/' + (service | toManifest)?.id"
|
||||
[pkg]="service"
|
||||
[depErrors]="errors()?.[(service | toManifest).id]"
|
||||
[depErrors]="errors()?.[(service | toManifest).id] || {}"
|
||||
></tr>
|
||||
} @empty {
|
||||
@for (_ of ['', '']; track $index) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, inject, signal } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
DialogService,
|
||||
getErrorMessage,
|
||||
@@ -17,7 +17,7 @@ import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import * as json from 'fast-json-patch'
|
||||
import { compare } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs'
|
||||
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
||||
import {
|
||||
ActionButton,
|
||||
FormComponent,
|
||||
@@ -50,13 +50,12 @@ export type PackageActionData = {
|
||||
<img [src]="pkgInfo.icon" alt="" />
|
||||
<h4>{{ pkgInfo.title }}</h4>
|
||||
</div>
|
||||
@if (res$ | async; as res) {
|
||||
@if (error) {
|
||||
<tui-notification appearance="negative">
|
||||
<div [innerHTML]="error"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<tui-notification appearance="negative">
|
||||
<div [innerHTML]="error()"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
@if (res(); as res) {
|
||||
@if (warning) {
|
||||
<tui-notification appearance="warning">
|
||||
<div [innerHTML]="warning"></div>
|
||||
@@ -85,7 +84,7 @@ export type PackageActionData = {
|
||||
{{ 'Reset defaults' | i18n }}
|
||||
</button>
|
||||
</app-form>
|
||||
} @else {
|
||||
} @else if (!error()) {
|
||||
<tui-loader size="l" textContent="loading" />
|
||||
}
|
||||
`,
|
||||
@@ -111,7 +110,6 @@ export type PackageActionData = {
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
TuiNotification,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
@@ -143,38 +141,39 @@ export class ActionInputModal {
|
||||
},
|
||||
]
|
||||
|
||||
error = ''
|
||||
readonly error = signal('')
|
||||
readonly res = toSignal(
|
||||
from(
|
||||
this.api.getActionInput({
|
||||
packageId: this.pkgInfo.id,
|
||||
actionId: this.actionId,
|
||||
}),
|
||||
).pipe(
|
||||
map(res => {
|
||||
console.warn('MAP', res)
|
||||
const originalValue = res.value || {}
|
||||
this.eventId = res.eventId
|
||||
|
||||
res$ = defer(() =>
|
||||
this.api.getActionInput({
|
||||
packageId: this.pkgInfo.id,
|
||||
actionId: this.actionId,
|
||||
}),
|
||||
).pipe(
|
||||
map(res => {
|
||||
console.warn('MAP', res)
|
||||
const originalValue = res.value || {}
|
||||
this.eventId = res.eventId
|
||||
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
utils.deepMerge(
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
console.error('catchError', e)
|
||||
this.error = String(getErrorMessage(e))
|
||||
return EMPTY
|
||||
}),
|
||||
utils.deepMerge(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
console.error('catchError', e)
|
||||
this.error.set(String(getErrorMessage(e)))
|
||||
return EMPTY
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
async execute(input: object) {
|
||||
|
||||
@@ -2,13 +2,12 @@ import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTextfield, TuiTextfieldDirective } from '@taiga-ui/core'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTextfield } from '@taiga-ui/core'
|
||||
import { QrCodeComponent } from 'ng-qrcode'
|
||||
import { SingleResult } from './types'
|
||||
|
||||
@@ -48,7 +47,7 @@ import { SingleResult } from './types'
|
||||
tabindex="-1"
|
||||
iconStart="@tui.copy"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="copy()"
|
||||
(click)="copy.copy(single.value)"
|
||||
>
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
@@ -96,25 +95,10 @@ import { SingleResult } from './types'
|
||||
],
|
||||
})
|
||||
export class ActionSuccessSingleComponent {
|
||||
@ViewChild(TuiTextfieldDirective, { read: ElementRef })
|
||||
private readonly input!: ElementRef<HTMLInputElement>
|
||||
readonly copy = inject(CopyService)
|
||||
|
||||
@Input()
|
||||
single!: SingleResult
|
||||
|
||||
masked = true
|
||||
|
||||
copy() {
|
||||
const el = this.input.nativeElement
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
el.type = 'text'
|
||||
el.focus()
|
||||
el.select()
|
||||
el.ownerDocument.execCommand('copy')
|
||||
el.type = this.masked && this.single.masked ? 'password' : 'text'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ export default class ServiceAboutRoute {
|
||||
private readonly markdown = inject(DialogService).openComponent(MARKDOWN, {
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: from(inject(ApiService).getStaticInstalled(this.pkgId, 'LICENSE.md')),
|
||||
data: from(
|
||||
inject(ApiService).getStatic(
|
||||
`/s9pk/installed/${this.pkgId}.s9pk/LICENSE.md`,
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
readonly groups = toSignal<{ header: i18nKey; items: AdditionalItem[] }[]>(
|
||||
|
||||
@@ -142,6 +142,7 @@ export default class ServiceInterfaceRoute {
|
||||
torDomains: host.onions,
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
privateDomains: host.privateDomains,
|
||||
addSsl: !!binding?.options.addSsl,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
path="/user-manual/backup-create.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
} @else {
|
||||
@@ -83,7 +82,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
path="/user-manual/backup-restore.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
}
|
||||
@@ -127,7 +125,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
fragment="#network-folder"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</section>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import { TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||
import { TuiAnimated, TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -168,7 +168,7 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
</button>
|
||||
</div>
|
||||
@if (count > 4) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale" @tuiScaleIn @tuiFadeIn>
|
||||
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
||||
<tui-icon icon="@tui.briefcase-medical" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Disk Repair' | i18n }}</strong>
|
||||
@@ -209,10 +209,14 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
[tuiCell] {
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
[tuiAnimated].tui-enter,
|
||||
[tuiAnimated].tui-leave {
|
||||
animation-name: tuiFade, tuiScale;
|
||||
}
|
||||
`,
|
||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [tuiScaleIn, tuiFadeIn],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
@@ -231,6 +235,7 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
SnekDirective,
|
||||
TuiBadge,
|
||||
TuiBadgeNotification,
|
||||
TuiAnimated,
|
||||
],
|
||||
})
|
||||
export default class SystemGeneralComponent {
|
||||
|
||||
@@ -103,6 +103,7 @@ export default class StartOsUiComponent {
|
||||
torDomains: network.host.onions,
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
privateDomains: network.host.privateDomains,
|
||||
addSsl: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export const SYSTEM_MENU = [
|
||||
item: 'Gateways',
|
||||
link: 'gateways',
|
||||
},
|
||||
{
|
||||
icon: '@tui.award',
|
||||
item: 'Certificate Authorities',
|
||||
link: 'authorities',
|
||||
},
|
||||
// {
|
||||
// icon: '@tui.award',
|
||||
// item: 'Certificate Authorities',
|
||||
// link: 'authorities',
|
||||
// },
|
||||
{
|
||||
icon: '@tui.globe',
|
||||
item: 'DNS' as i18nKey,
|
||||
|
||||
@@ -71,12 +71,12 @@ export default [
|
||||
path: 'gateways',
|
||||
loadComponent: () => import('./routes/gateways/gateways.component'),
|
||||
},
|
||||
{
|
||||
path: 'authorities',
|
||||
title: titleResolver,
|
||||
loadComponent: () =>
|
||||
import('./routes/authorities/authorities.component'),
|
||||
},
|
||||
// {
|
||||
// path: 'authorities',
|
||||
// title: titleResolver,
|
||||
// loadComponent: () =>
|
||||
// import('./routes/authorities/authorities.component'),
|
||||
// },
|
||||
{
|
||||
path: 'dns',
|
||||
title: titleResolver,
|
||||
|
||||
@@ -385,7 +385,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -420,7 +420,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -465,7 +465,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -500,7 +500,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -547,7 +547,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.5',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -595,7 +595,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.4',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -647,7 +647,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -682,7 +682,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -727,7 +727,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -775,7 +775,7 @@ export namespace Mock {
|
||||
marketingSite: '',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: PROXY_ICON,
|
||||
sourceVersion: null,
|
||||
|
||||
@@ -15,10 +15,7 @@ export abstract class ApiService {
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string>
|
||||
|
||||
abstract getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string>
|
||||
abstract getStatic(url: string): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Method,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
@@ -37,7 +36,7 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
async uploadFile(guid: string, body: Blob): Promise<void> {
|
||||
await this.httpRequest({
|
||||
method: Method.POST,
|
||||
method: 'POST',
|
||||
body,
|
||||
url: `/rest/rpc/${guid}`,
|
||||
timeout: 0,
|
||||
@@ -53,7 +52,7 @@ export class LiveApiService extends ApiService {
|
||||
const encodedUrl = encodeURIComponent(pkg.s9pk.url)
|
||||
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
method: 'GET',
|
||||
url: `/s9pk/proxy/${encodedUrl}/${path}`,
|
||||
params: {
|
||||
rootSighash: pkg.s9pk.commitment.rootSighash,
|
||||
@@ -63,13 +62,10 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string> {
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
url: `/s9pk/installed/${id}.s9pk/${path}`,
|
||||
method: 'GET',
|
||||
url,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,10 +83,7 @@ export class MockApiService extends ApiService {
|
||||
return markdown
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string> {
|
||||
async getStatic(url: string): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
}
|
||||
@@ -1125,8 +1122,8 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
result: 'success',
|
||||
message: null,
|
||||
result: 'waiting',
|
||||
message: 'Chain State',
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
|
||||
@@ -163,16 +163,3 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
showDots: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {
|
||||
warning: { display: 'Issue', color: 'warning' },
|
||||
satisfied: { display: 'Satisfied', color: 'success' },
|
||||
}
|
||||
|
||||
export const HealthRendering: Record<T.HealthStatus, StatusRendering> = {
|
||||
failure: { display: 'Failure', color: 'danger' },
|
||||
starting: { display: 'Starting', color: 'primary' },
|
||||
loading: { display: 'Loading', color: 'primary' },
|
||||
success: { display: 'Healthy', color: 'success' },
|
||||
disabled: { display: 'Disabled', color: 'dark' },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export function toAuthorityName(url: string | null): string | 'Local Root CA' {
|
||||
return (
|
||||
knownAuthorities.find(ca => ca.url === url)?.name || url || 'Local Root CA'
|
||||
)
|
||||
export function toAuthorityName(
|
||||
url: string | null,
|
||||
addSsl = true,
|
||||
): string | 'Local Root CA' | '-' {
|
||||
if (url) {
|
||||
return knownAuthorities.find(ca => ca.url === url)?.name || url
|
||||
} else {
|
||||
return addSsl ? 'Local Root CA' : '-'
|
||||
}
|
||||
}
|
||||
|
||||
export function toAuthorityUrl(name: string): string {
|
||||
|
||||
Reference in New Issue
Block a user