mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feature/tor logs (#3077)
* add tor logs, rework services page, other small things * feat: sortable service table and mobile view --------- Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
@@ -88,6 +88,8 @@ export default {
|
||||
88: 'Aktionen',
|
||||
89: 'nicht empfohlen',
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
91: 'Installierte Dienste',
|
||||
92: 'Diagnosen für den Tor-Daemon auf diesem Server',
|
||||
96: 'Öffentliche Domain hinzufügen',
|
||||
97: 'Wird entfernt',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
|
||||
@@ -87,6 +87,8 @@ export const ENGLISH = {
|
||||
'Actions': 88, // as in, actions available to the user
|
||||
'not recommended': 89,
|
||||
'Root CA Trusted!': 90,
|
||||
'Installed services': 91, // as in, software services installed on this computer
|
||||
'Diagnostics for the Tor daemon on this server': 92,
|
||||
'Add public domain': 96,
|
||||
'Removing': 97,
|
||||
'Unsaved changes': 100,
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
88: 'Acciones',
|
||||
89: 'no recomendado',
|
||||
90: '¡CA raíz confiable!',
|
||||
91: 'Servicios instalados',
|
||||
92: 'Diagnósticos para el demonio Tor en este servidor',
|
||||
96: 'Agregar dominio público',
|
||||
97: 'Eliminando',
|
||||
100: 'Cambios no guardados',
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
88: 'Actions',
|
||||
89: 'non recommandé',
|
||||
90: 'Certificat racine approuvé !',
|
||||
91: 'Services installés',
|
||||
92: 'Diagnostics pour le service Tor sur ce serveur',
|
||||
96: 'Ajouter un domaine public',
|
||||
97: 'Suppression',
|
||||
100: 'Modifications non enregistrées',
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
88: 'Akcje',
|
||||
89: 'niezalecane',
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
91: 'Zainstalowane usługi',
|
||||
92: 'Diagnostyka demona Tor na tym serwerze',
|
||||
96: 'Dodaj domenę publiczną',
|
||||
97: 'Usuwanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
|
||||
@@ -3,7 +3,12 @@ import { TuiIcon } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-placeholder',
|
||||
template: '<tui-icon [icon]="icon()" /><ng-content/>',
|
||||
template: `
|
||||
@if (icon(); as icon) {
|
||||
<tui-icon [icon]="icon" />
|
||||
}
|
||||
<ng-content />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -26,5 +31,5 @@ import { TuiIcon } from '@taiga-ui/core'
|
||||
imports: [TuiIcon],
|
||||
})
|
||||
export class PlaceholderComponent {
|
||||
readonly icon = input.required<string>()
|
||||
readonly icon = input<string>()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
TuiComparator,
|
||||
TuiTable,
|
||||
TuiTableDirective,
|
||||
} from '@taiga-ui/addon-table'
|
||||
|
||||
@Component({
|
||||
selector: 'table[appTable]',
|
||||
@@ -8,7 +13,13 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
<tr>
|
||||
<ng-content select="th" />
|
||||
@for (header of appTable(); track $index) {
|
||||
<th>{{ header | i18n }}</th>
|
||||
<th
|
||||
tuiTh
|
||||
[requiredSort]="true"
|
||||
[sorter]="appTableSorters()[$index] || null"
|
||||
>
|
||||
{{ header | i18n }}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -22,9 +33,16 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-table' },
|
||||
hostDirectives: [
|
||||
{
|
||||
directive: TuiTableDirective,
|
||||
inputs: ['sorter'],
|
||||
},
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [i18nPipe],
|
||||
imports: [i18nPipe, TuiTable],
|
||||
})
|
||||
export class TableComponent {
|
||||
readonly appTable = input.required<ReadonlyArray<i18nKey | null>>()
|
||||
readonly appTableSorters = input<ReadonlyArray<TuiComparator<any> | null>>([])
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ export const ROUTES: Routes = [
|
||||
path: 'os',
|
||||
loadComponent: () => import('./routes/os.component'),
|
||||
},
|
||||
{
|
||||
path: 'tor',
|
||||
loadComponent: () => import('./routes/tor.component'),
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
|
||||
@@ -79,6 +79,12 @@ export default class SystemLogsComponent {
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
icon: '@tui.square-dashed-bottom-code',
|
||||
},
|
||||
{
|
||||
link: 'tor',
|
||||
title: 'Tor Logs',
|
||||
subtitle: 'Diagnostics for the Tor daemon on this server',
|
||||
icon: '@tui.target',
|
||||
},
|
||||
{
|
||||
link: 'kernel',
|
||||
title: 'Kernel Logs',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<logs-header [title]="'Tor Logs' | i18n">
|
||||
{{ 'Diagnostics for the Tor daemon on this server' | i18n }}
|
||||
</logs-header>
|
||||
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
||||
host: { class: 'g-page' },
|
||||
})
|
||||
export default class SystemTorComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followTorLogs(params)
|
||||
|
||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
||||
this.api.getTorLogs(params)
|
||||
}
|
||||
@@ -167,6 +167,9 @@ export default class MarketplaceComponent {
|
||||
takeUntilDestroyed(),
|
||||
tap(params => {
|
||||
const registry = params.get('registry')
|
||||
|
||||
this.categoryService.setQuery(params.get('search') || '')
|
||||
|
||||
if (!registry) {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
|
||||
@@ -49,16 +49,6 @@ import { NotificationsTableComponent } from './table.component'
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section {
|
||||
padding-block: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ServerNotification } from 'src/app/services/api/api.types'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { NotificationItemComponent } from './item.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { PlaceholderComponent } from '../../components/placeholder.component'
|
||||
|
||||
@Component({
|
||||
selector: '[notifications]',
|
||||
@@ -48,9 +49,9 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
</tr>
|
||||
} @empty {
|
||||
@if (notifications()) {
|
||||
<tr>
|
||||
<td colspan="4">{{ 'No notifications' | i18n }}</td>
|
||||
</tr>
|
||||
<app-placeholder icon="@tui.bell">
|
||||
{{ 'No notifications' | i18n }}
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
@for (i of ['', '']; track $index) {
|
||||
<tr>
|
||||
@@ -71,10 +72,6 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
td:only-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
input {
|
||||
position: absolute;
|
||||
@@ -97,6 +94,7 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
TuiSkeleton,
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
],
|
||||
})
|
||||
export class NotificationsTableComponent<T extends ServerNotification<number>>
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ToManifestPipe } from '../../../pipes/to-manifest'
|
||||
@let services = this.services();
|
||||
|
||||
@for (d of pkg().currentDependencies | keyvalue; track $index) {
|
||||
<!-- @TODO Alex Marketplace should use "search" query param to prefill search bar -->
|
||||
<a
|
||||
tuiCell
|
||||
[routerLink]="services[d.key] ? ['..', d.key] : ['/marketplace']"
|
||||
|
||||
@@ -86,7 +86,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
td:last-child {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
justify-content: end;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// import { AsyncPipe } from '@angular/common'
|
||||
// import {
|
||||
// ChangeDetectionStrategy,
|
||||
// Component,
|
||||
// computed,
|
||||
// inject,
|
||||
// input,
|
||||
// } from '@angular/core'
|
||||
// import { i18nPipe } from '@start9labs/shared'
|
||||
// import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
// import { map } from 'rxjs'
|
||||
// import { ControlsService } from 'src/app/services/controls.service'
|
||||
// import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
// import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
// import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
// import { getManifest } from 'src/app/utils/get-package-data'
|
||||
// import { UILaunchComponent } from './ui-launch.component'
|
||||
|
||||
// const RUNNING = ['running', 'starting', 'restarting']
|
||||
|
||||
// @Component({
|
||||
// selector: 'fieldset[appControls]',
|
||||
// template: `
|
||||
// <app-ui-launch [pkg]="pkg()" />
|
||||
// @if (running()) {
|
||||
// <button
|
||||
// tuiIconButton
|
||||
// iconStart="@tui.square"
|
||||
// (click)="controls.stop(manifest())"
|
||||
// >
|
||||
// {{ 'Stop' | i18n }}
|
||||
// </button>
|
||||
// } @else {
|
||||
// @let unmet = hasUnmet() | async;
|
||||
// <button
|
||||
// tuiIconButton
|
||||
// iconStart="@tui.play"
|
||||
// [disabled]="status().primary !== 'stopped'"
|
||||
// (click)="controls.start(manifest(), !!unmet)"
|
||||
// >
|
||||
// {{ 'Start' | i18n }}
|
||||
// </button>
|
||||
// }
|
||||
// `,
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// imports: [TuiButton, UILaunchComponent, AsyncPipe, i18nPipe],
|
||||
// providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
||||
// styles: `
|
||||
// :host {
|
||||
// padding: 0;
|
||||
// border: none;
|
||||
// cursor: default;
|
||||
// text-align: right;
|
||||
// }
|
||||
|
||||
// :host-context(tui-root._mobile) {
|
||||
// button {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// `,
|
||||
// })
|
||||
// export class ControlsComponent {
|
||||
// private readonly errors = inject(DepErrorService)
|
||||
|
||||
// readonly controls = inject(ControlsService)
|
||||
// readonly pkg = input.required<PackageDataEntry>()
|
||||
// readonly status = computed(() => renderPkgStatus(this.pkg()))
|
||||
// readonly running = computed(() => RUNNING.includes(this.status().primary))
|
||||
// readonly manifest = computed(() => getManifest(this.pkg()))
|
||||
// readonly hasUnmet = computed(() =>
|
||||
// this.errors.getPkgDepErrors$(this.manifest().id).pipe(
|
||||
// map(errors =>
|
||||
// Object.keys(this.pkg().currentDependencies)
|
||||
// .map(id => errors?.[id])
|
||||
// .some(Boolean),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
@@ -1,160 +1,61 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
viewChild,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
|
||||
import { TuiButton, TuiLoader } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map, shareReplay } from 'rxjs'
|
||||
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
import { ServiceComponent } from './service.component'
|
||||
import { ServicesTableComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>{{ 'Services' | i18n }}</ng-container>
|
||||
@if (!services()) {
|
||||
<tui-loader [style.height.%]="100" [textContent]="'Loading' | i18n" />
|
||||
} @else {
|
||||
@if (services()?.length) {
|
||||
<table tuiTable class="g-table" [(sorter)]="sorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th [style.width.rem]="3"></th>
|
||||
<th tuiTh [requiredSort]="true" [sorter]="name">
|
||||
{{ 'Name' | i18n }}
|
||||
</th>
|
||||
<th tuiTh [requiredSort]="true" [sorter]="status">
|
||||
{{ 'Status' | i18n }}
|
||||
</th>
|
||||
<th tuiTh>{{ 'Version' | i18n }}</th>
|
||||
<th
|
||||
tuiTh
|
||||
[requiredSort]="true"
|
||||
[sorter]="uptime"
|
||||
[style.width.rem]="10"
|
||||
>
|
||||
{{ 'Uptime' | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (pkg of services() | tuiTableSort; track $index) {
|
||||
<tr
|
||||
appService
|
||||
[pkg]="pkg"
|
||||
[depErrors]="errors()?.[(pkg | toManifest).id]"
|
||||
></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
} @else {
|
||||
<section>
|
||||
<div>
|
||||
{{ 'Welcome to' | i18n }}
|
||||
<span>StartOS</span>
|
||||
</div>
|
||||
<p>
|
||||
{{
|
||||
'To get started, visit the Marketplace and download your first service'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<a tuiButton routerLink="../marketplace">
|
||||
{{ 'View Marketplace' | i18n }}
|
||||
</a>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
<ng-container *title>{{ 'Installed services' | i18n }}</ng-container>
|
||||
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Installed services' | i18n }}
|
||||
</header>
|
||||
|
||||
<div #table [services]="services()"></div>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table {
|
||||
max-width: 60rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
div {
|
||||
font-size: min(12vw, 4rem);
|
||||
line-height: normal;
|
||||
header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.25em;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #ff4961;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-block-start: 1rem;
|
||||
.g-card {
|
||||
padding: 0;
|
||||
margin-top: -0.75rem;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-page' },
|
||||
imports: [
|
||||
ServiceComponent,
|
||||
ToManifestPipe,
|
||||
TuiTable,
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
RouterLink,
|
||||
],
|
||||
imports: [TitleDirective, i18nPipe, ServicesTableComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export default class DashboardComponent {
|
||||
readonly errors = toSignal(inject(DepErrorService).depErrors$)
|
||||
readonly services = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData')
|
||||
.pipe(
|
||||
map(pkgs => Object.values(pkgs).sort(byName)),
|
||||
map(pkgs => Object.values(pkgs)),
|
||||
shareReplay(1),
|
||||
),
|
||||
{ initialValue: null },
|
||||
)
|
||||
|
||||
readonly name: TuiComparator<PackageDataEntry> = byName
|
||||
|
||||
readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
|
||||
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
|
||||
|
||||
readonly uptime: TuiComparator<any> = (a, b) =>
|
||||
a.status.started || '' > a.status.started || '' ? -1 : 1
|
||||
|
||||
sorter = this.name
|
||||
}
|
||||
|
||||
function byName(a: PackageDataEntry, b: PackageDataEntry) {
|
||||
return getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
|
||||
? -1
|
||||
: 1
|
||||
protected _ = viewChild<ServicesTableComponent<any>>('table')
|
||||
}
|
||||
|
||||
@@ -18,18 +18,15 @@ import { StatusComponent } from './status.component'
|
||||
@Component({
|
||||
selector: 'tr[appService]',
|
||||
template: `
|
||||
<td [style.grid-area]="'1 / 1 / 4'">
|
||||
<td [style.width.rem]="3" [style.grid-area]="'1 / 1 / 4'">
|
||||
<img alt="logo" [src]="pkg.icon" />
|
||||
</td>
|
||||
<td class="title">
|
||||
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||
</td>
|
||||
<td
|
||||
appStatus
|
||||
[pkg]="pkg"
|
||||
[hasDepErrors]="hasError(depErrors)"
|
||||
[style.grid-area]="'3 / 2'"
|
||||
></td>
|
||||
<td [style.grid-area]="'3 / 2'">
|
||||
<app-status [pkg]="pkg" [hasDepErrors]="hasError(depErrors)" />
|
||||
</td>
|
||||
<td class="version">{{ manifest.version }}</td>
|
||||
<td class="uptime">
|
||||
@if (pkg.statusInfo.started; as started) {
|
||||
@@ -45,7 +42,6 @@ import { StatusComponent } from './status.component'
|
||||
|
||||
:host {
|
||||
@include taiga.transition(background);
|
||||
clip-path: inset(0 round 0.5rem);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@@ -76,10 +72,14 @@ import { StatusComponent } from './status.component'
|
||||
:host-context(tui-root._mobile) {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template: 1.25rem 1.5rem 1.5rem/4rem 1fr;
|
||||
grid-template: 1.25rem 2rem 1.5rem/4rem 1fr;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Component({
|
||||
selector: 'td[appStatus]',
|
||||
selector: 'app-status',
|
||||
template: `
|
||||
@if (error()) {
|
||||
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
|
||||
} @else if (loading()) {
|
||||
<tui-loader size="m" />
|
||||
<tui-loader size="s" />
|
||||
}
|
||||
|
||||
<b [style.color]="color()">{{ statusText() | i18n }}</b>
|
||||
@@ -33,13 +33,14 @@ import {
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 3rem;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
height: auto;
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
PackageDataEntry,
|
||||
StateInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ServiceComponent } from './service.component'
|
||||
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
|
||||
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ToManifestPipe } from '../../../pipes/to-manifest'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { PlaceholderComponent } from '../../../components/placeholder.component'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: '[services]',
|
||||
template: `
|
||||
@if (services()?.length === 0) {
|
||||
<app-placeholder>
|
||||
<h1 [style.margin-bottom]="0">
|
||||
{{ 'Welcome to' | i18n }}
|
||||
<span>StartOS!</span>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
{{
|
||||
'To get started, visit the Marketplace and download your first service'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
|
||||
<a
|
||||
style="margin: 1.5rem 0;"
|
||||
tuiButton
|
||||
size="m"
|
||||
iconStart="@tui.shopping-cart"
|
||||
routerLink="../marketplace"
|
||||
>
|
||||
{{ 'View Marketplace' | i18n }}
|
||||
</a>
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
<table
|
||||
[sorter]="name"
|
||||
[appTable]="[null, 'Name', 'Status', 'Version', 'Uptime']"
|
||||
[appTableSorters]="[null, name, status]"
|
||||
>
|
||||
@for (service of services() | tuiTableSort; track service) {
|
||||
<tr
|
||||
appService
|
||||
[pkg]="service"
|
||||
[depErrors]="errors()?.[(service | toManifest).id]"
|
||||
></tr>
|
||||
} @empty {
|
||||
@for (_ of ['', '']; track $index) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
FormsModule,
|
||||
TableComponent,
|
||||
ServiceComponent,
|
||||
ToManifestPipe,
|
||||
i18nPipe,
|
||||
TuiSkeleton,
|
||||
PlaceholderComponent,
|
||||
TuiButton,
|
||||
RouterLink,
|
||||
TuiTable,
|
||||
],
|
||||
})
|
||||
export class ServicesTableComponent<
|
||||
T extends T.PackageDataEntry & {
|
||||
stateInfo: StateInfo
|
||||
},
|
||||
> {
|
||||
readonly errors = toSignal(inject(DepErrorService).depErrors$)
|
||||
|
||||
readonly services = input.required<readonly T[] | null>()
|
||||
|
||||
readonly name: TuiComparator<PackageDataEntry> = byName
|
||||
|
||||
readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
|
||||
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
|
||||
}
|
||||
|
||||
function byName(a: PackageDataEntry, b: PackageDataEntry) {
|
||||
return getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
|
||||
? -1
|
||||
: 1
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// import {
|
||||
// ChangeDetectionStrategy,
|
||||
// Component,
|
||||
// inject,
|
||||
// Input,
|
||||
// DOCUMENT,
|
||||
// } from '@angular/core'
|
||||
// import { i18nPipe } from '@start9labs/shared'
|
||||
// import { T } from '@start9labs/start-sdk'
|
||||
// import { tuiPure } from '@taiga-ui/cdk'
|
||||
// import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
|
||||
// import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
// import { InterfaceService } from '../../../components/interfaces/interface.service'
|
||||
// import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
// @Component({
|
||||
// selector: 'app-ui-launch',
|
||||
// template: `
|
||||
// @if (interfaces.length > 1) {
|
||||
// <button
|
||||
// tuiIconButton
|
||||
// iconStart="@tui.external-link"
|
||||
// tuiDropdownOpen
|
||||
// [disabled]="!isRunning"
|
||||
// [tuiDropdown]="content"
|
||||
// >
|
||||
// {{ 'Open' | i18n }}
|
||||
// </button>
|
||||
// <ng-template #content>
|
||||
// <tui-data-list>
|
||||
// @for (interface of interfaces; track $index) {
|
||||
// <a
|
||||
// tuiOption
|
||||
// target="_blank"
|
||||
// rel="noreferrer"
|
||||
// [attr.href]="getHref(interface)"
|
||||
// >
|
||||
// {{ interface.name }}
|
||||
// </a>
|
||||
// }
|
||||
// </tui-data-list>
|
||||
// </ng-template>
|
||||
// } @else if (interfaces[0]) {
|
||||
// <button
|
||||
// tuiIconButton
|
||||
// iconStart="@tui.external-link"
|
||||
// [disabled]="!isRunning"
|
||||
// (click)="openUI(interfaces[0])"
|
||||
// >
|
||||
// {{ interfaces[0].name }}
|
||||
// </button>
|
||||
// }
|
||||
// `,
|
||||
// styles: `
|
||||
// :host-context(tui-root._mobile) *::before {
|
||||
// font-size: 1.5rem !important;
|
||||
// }
|
||||
// `,
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe],
|
||||
// })
|
||||
// export class UILaunchComponent {
|
||||
// private readonly interfaceService = inject(InterfaceService)
|
||||
// private readonly document = inject(DOCUMENT)
|
||||
|
||||
// @Input()
|
||||
// pkg!: PackageDataEntry
|
||||
|
||||
// get interfaces(): readonly T.ServiceInterface[] {
|
||||
// return this.getInterfaces(this.pkg)
|
||||
// }
|
||||
|
||||
// get isRunning(): boolean {
|
||||
// return getInstalledPrimaryStatus(this.pkg) === 'running'
|
||||
// }
|
||||
|
||||
// @tuiPure
|
||||
// getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
|
||||
// return pkg
|
||||
// ? Object.values(pkg.serviceInterfaces).filter(
|
||||
// i =>
|
||||
// i.type === 'ui' &&
|
||||
// (i.addressInfo.scheme === 'http' ||
|
||||
// i.addressInfo.sslScheme === 'https'),
|
||||
// )
|
||||
// : []
|
||||
// }
|
||||
|
||||
// getHref(ui: T.ServiceInterface): string {
|
||||
// const host = this.pkg.hosts[ui.addressInfo.hostId]
|
||||
// if (!host) return ''
|
||||
// return this.interfaceService.launchableAddress(ui, host)
|
||||
// }
|
||||
|
||||
// openUI(ui: T.ServiceInterface) {
|
||||
// this.document.defaultView?.open(this.getHref(ui), '_blank', 'noreferrer')
|
||||
// }
|
||||
// }
|
||||
@@ -73,14 +73,14 @@ export namespace RR {
|
||||
uptime: number // seconds
|
||||
}
|
||||
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & net.tor.logs
|
||||
export type GetServerLogsRes = FetchLogsRes
|
||||
|
||||
export type FollowServerLogsReq = {
|
||||
limit?: number // (optional) default is 50. Ignored if cursor provided
|
||||
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
|
||||
cursor?: string // the last known log. Websocket will return all logs since this log
|
||||
} // server.logs.follow & server.kernel-logs.follow
|
||||
} // server.logs.follow & server.kernel-logs.follow & net.tor.follow-logs
|
||||
export type FollowServerLogsRes = {
|
||||
startCursor: string
|
||||
guid: string
|
||||
|
||||
@@ -86,6 +86,8 @@ export abstract class ApiService {
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
@@ -94,6 +96,10 @@ export abstract class ApiService {
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
@@ -206,6 +206,10 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.logs', params })
|
||||
}
|
||||
|
||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.logs', params })
|
||||
}
|
||||
|
||||
async getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -218,6 +222,12 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.logs.follow', params })
|
||||
}
|
||||
|
||||
async followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
|
||||
}
|
||||
|
||||
async followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
|
||||
@@ -290,6 +290,17 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
const entries = this.randomLogs(params.limit)
|
||||
|
||||
return {
|
||||
entries,
|
||||
startCursor: 'start-cursor',
|
||||
endCursor: 'end-cursor',
|
||||
}
|
||||
}
|
||||
|
||||
async getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -313,6 +324,16 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
|
||||
@@ -222,6 +222,167 @@ export const mockPatchData: DataModel = {
|
||||
kiosk: true,
|
||||
},
|
||||
packageData: {
|
||||
lnd: {
|
||||
stateInfo: {
|
||||
state: 'installed',
|
||||
manifest: {
|
||||
...Mock.MockManifestLnd,
|
||||
version: '0.11.0:0.0.1',
|
||||
},
|
||||
},
|
||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||
icon: '/assets/img/service-icons/lnd.png',
|
||||
lastBackup: null,
|
||||
statusInfo: {
|
||||
desired: { main: 'stopped' },
|
||||
error: null,
|
||||
health: {},
|
||||
started: null,
|
||||
},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Config',
|
||||
description: 'LND needs configuration before starting',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
connect: {
|
||||
name: 'Connect',
|
||||
description: 'View LND connection details',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: 'Connecting',
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
grpc: {
|
||||
id: 'grpc',
|
||||
masked: false,
|
||||
name: 'GRPC',
|
||||
description:
|
||||
'Used by dependent services and client wallets for connecting to your node',
|
||||
type: 'api',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'grpc',
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
lndconnect: {
|
||||
id: 'lndconnect',
|
||||
masked: true,
|
||||
name: 'LND Connect',
|
||||
description:
|
||||
'Used by client wallets adhering to LND Connect protocol to connect to your node',
|
||||
type: 'api',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'lndconnect',
|
||||
suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand',
|
||||
},
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
'Used for connecting to other nodes on the Bitcoin network',
|
||||
type: 'p2p',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'rstuvw',
|
||||
internalPort: 8333,
|
||||
scheme: 'bitcoin',
|
||||
sslScheme: null,
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
currentDependencies: {
|
||||
bitcoind: {
|
||||
title: Mock.BitcoinDep.title,
|
||||
icon: Mock.BitcoinDep.icon,
|
||||
kind: 'running',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: Mock.ProxyDep.title,
|
||||
icon: Mock.ProxyDep.icon,
|
||||
kind: 'running',
|
||||
versionRange: '>2.0.0',
|
||||
healthChecks: [],
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
config: {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND needs configuration before starting',
|
||||
},
|
||||
},
|
||||
connect: {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'connect',
|
||||
severity: 'important',
|
||||
reason: 'View LND connection details',
|
||||
},
|
||||
},
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND likes BTC a certain way',
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoind/rpc': {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'rpc',
|
||||
severity: 'important',
|
||||
reason: `LND want's its own RPC credentials`,
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bitcoind: {
|
||||
stateInfo: {
|
||||
state: 'installed',
|
||||
@@ -511,166 +672,5 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
},
|
||||
lnd: {
|
||||
stateInfo: {
|
||||
state: 'installed',
|
||||
manifest: {
|
||||
...Mock.MockManifestLnd,
|
||||
version: '0.11.0:0.0.1',
|
||||
},
|
||||
},
|
||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||
icon: '/assets/img/service-icons/lnd.png',
|
||||
lastBackup: null,
|
||||
statusInfo: {
|
||||
desired: { main: 'stopped' },
|
||||
error: null,
|
||||
health: {},
|
||||
started: null,
|
||||
},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Config',
|
||||
description: 'LND needs configuration before starting',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
connect: {
|
||||
name: 'Connect',
|
||||
description: 'View LND connection details',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: 'Connecting',
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
grpc: {
|
||||
id: 'grpc',
|
||||
masked: false,
|
||||
name: 'GRPC',
|
||||
description:
|
||||
'Used by dependent services and client wallets for connecting to your node',
|
||||
type: 'api',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'grpc',
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
lndconnect: {
|
||||
id: 'lndconnect',
|
||||
masked: true,
|
||||
name: 'LND Connect',
|
||||
description:
|
||||
'Used by client wallets adhering to LND Connect protocol to connect to your node',
|
||||
type: 'api',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'qrstuv',
|
||||
internalPort: 10009,
|
||||
scheme: null,
|
||||
sslScheme: 'lndconnect',
|
||||
suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand',
|
||||
},
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
'Used for connecting to other nodes on the Bitcoin network',
|
||||
type: 'p2p',
|
||||
addressInfo: {
|
||||
username: null,
|
||||
hostId: 'rstuvw',
|
||||
internalPort: 8333,
|
||||
scheme: 'bitcoin',
|
||||
sslScheme: null,
|
||||
suffix: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
currentDependencies: {
|
||||
bitcoind: {
|
||||
title: Mock.BitcoinDep.title,
|
||||
icon: Mock.BitcoinDep.icon,
|
||||
kind: 'running',
|
||||
versionRange: '>=26.0.0',
|
||||
healthChecks: [],
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: Mock.ProxyDep.title,
|
||||
icon: Mock.ProxyDep.icon,
|
||||
kind: 'running',
|
||||
versionRange: '>2.0.0',
|
||||
healthChecks: [],
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
config: {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND needs configuration before starting',
|
||||
},
|
||||
},
|
||||
connect: {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'connect',
|
||||
severity: 'important',
|
||||
reason: 'View LND connection details',
|
||||
},
|
||||
},
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND likes BTC a certain way',
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoind/rpc': {
|
||||
active: true,
|
||||
task: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'rpc',
|
||||
severity: 'important',
|
||||
reason: `LND want's its own RPC credentials`,
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user