refactor: fix multiple comments (#3013)

* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Alex Inkin
2025-08-30 00:37:34 +07:00
committed by GitHub
parent 8163db7ac3
commit ca39ffb9eb
29 changed files with 287 additions and 199 deletions

View File

@@ -36,7 +36,7 @@ import { MarketplaceItemComponent } from './item.component'
<marketplace-item <marketplace-item
[style.pointer-events]="'none'" [style.pointer-events]="'none'"
[data]="pkg().sdkVersion || 'Unknown'" [data]="pkg().sdkVersion || 'Unknown'"
label="SDK Version" label="SDK version"
icon="" icon=""
/> />
<!-- git hash --> <!-- git hash -->

View File

@@ -491,7 +491,7 @@ export default {
518: 'Verwerfen', 518: 'Verwerfen',
520: 'Update verfügbar', 520: 'Update verfügbar',
521: 'Um das Problem zu beheben, siehe', 521: 'Um das Problem zu beheben, siehe',
522: 'SDK Version', 522: 'SDK version',
523: 'Sicherungsbericht', 523: 'Sicherungsbericht',
524: 'Ausgewählte löschen', 524: 'Ausgewählte löschen',
525: 'Keine SSH-Schlüssel', 525: 'Keine SSH-Schlüssel',
@@ -577,4 +577,12 @@ export default {
610: 'Dynamisches DNS', 610: 'Dynamisches DNS',
611: 'Keine Service-Schnittstellen', 611: 'Keine Service-Schnittstellen',
612: 'Grund', 612: 'Grund',
613: 'Private Gateways für die StartOS-Benutzeroberfläche können nicht deaktiviert werden',
614: 'CA-Fingerabdruck',
615: 'DHCP-Server',
616: 'DHCP-Server können nicht bearbeitet werden',
617: 'Statisch',
618: 'Statische Server',
619: 'Warnung. StartOS verwendet derzeit das folgende Gateway für DNS',
620: 'Wenn Sie dieses Gateway für die Auflösung privater Domains verwenden möchten, legen Sie alternative statische DNS-Server mit dem obigen Formular fest.',
} satisfies i18n } satisfies i18n

View File

@@ -254,9 +254,9 @@ export const ENGLISH = {
'unknown %': 270, 'unknown %': 270,
'Not provided': 271, 'Not provided': 271,
'Links': 272, 'Links': 272,
'Git Hash': 273, 'Git hash': 273,
'License': 274, 'License': 274,
'Installed From': 275, 'Installed from': 275,
'Marketing': 278, 'Marketing': 278,
'Support': 279, 'Support': 279,
'Donations': 280, 'Donations': 280,
@@ -490,7 +490,7 @@ export const ENGLISH = {
'Dismiss': 518, // as in, dismiss or delete a task 'Dismiss': 518, // as in, dismiss or delete a task
'Update available': 520, 'Update available': 520,
'To resolve the issue, refer to': 521, 'To resolve the issue, refer to': 521,
'SDK Version': 522, 'SDK version': 522,
'Backup Report': 523, 'Backup Report': 523,
'Delete selected': 524, 'Delete selected': 524,
'No SSH keys': 525, 'No SSH keys': 525,
@@ -576,4 +576,12 @@ export const ENGLISH = {
'Dynamic DNS': 610, 'Dynamic DNS': 610,
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application '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 '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
'DHCP Servers': 615,
'Cannot edit DHCP servers': 616,
'Static': 617, // as in, unchanging
'Static Servers': 618, // as in, servers that do not change
'Warning. StartOS is currently using the following gateway for DNS': 619,
'If you intend to use this gateway for private domain resolution, set alternative static DNS servers using the form above.': 620,
} as const } as const

View File

@@ -577,4 +577,12 @@ export default {
610: 'DNS dinámico', 610: 'DNS dinámico',
611: 'Sin interfaces de servicio', 611: 'Sin interfaces de servicio',
612: 'Razón', 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',
615: 'Servidores DHCP',
616: 'No se pueden editar los servidores DHCP',
617: 'Estático',
618: 'Servidores estáticos',
619: 'Advertencia. StartOS está utilizando actualmente la siguiente puerta de enlace para DNS',
620: 'Si deseas usar esta puerta de enlace para la resolución de dominios privados, configura servidores DNS estáticos alternativos usando el formulario anterior.',
} satisfies i18n } satisfies i18n

View File

@@ -577,4 +577,12 @@ export default {
610: 'DNS dynamique', 610: 'DNS dynamique',
611: 'Aucune interface de service', 611: 'Aucune interface de service',
612: 'Raison', 612: 'Raison',
613: "Impossible de désactiver les passerelles privées pour l'interface utilisateur StartOS",
614: 'Empreinte de lAC',
615: 'Serveurs DHCP',
616: 'Impossible de modifier les serveurs DHCP',
617: 'Statique',
618: 'Serveurs statiques',
619: 'Avertissement. StartOS utilise actuellement la passerelle suivante pour le DNS',
620: 'Si vous souhaitez utiliser cette passerelle pour la résolution de domaines privés, définissez des serveurs DNS statiques alternatifs à laide du formulaire ci-dessus.',
} satisfies i18n } satisfies i18n

View File

@@ -577,4 +577,12 @@ export default {
610: 'Dynamiczny DNS', 610: 'Dynamiczny DNS',
611: 'Brak interfejsów usług', 611: 'Brak interfejsów usług',
612: 'Powód', 612: 'Powód',
613: 'Nie można wyłączyć prywatnych bram dla interfejsu użytkownika StartOS',
614: 'Odcisk palca CA',
615: 'Serwery DHCP',
616: 'Nie można edytować serwerów DHCP',
617: 'Statyczny',
618: 'Serwery statyczne',
619: 'Ostrzeżenie. StartOS obecnie używa następującej bramy do DNS',
620: 'Jeśli zamierzasz używać tej bramy do rozwiązywania domen prywatnych, ustaw alternatywne statyczne serwery DNS za pomocą powyższego formularza.',
} satisfies i18n } satisfies i18n

View File

@@ -88,13 +88,6 @@
--start9-base-5: rgba(60, 62, 64, 1); --start9-base-5: rgba(60, 62, 64, 1);
} }
[tuiAppearance][data-appearance^='primary']:not([tuiCheckbox]._readonly) {
@include taiga.appearance-disabled {
background: var(--tui-status-neutral);
color: #333;
}
}
[tuiAppearance][data-appearance='primary-success'] { [tuiAppearance][data-appearance='primary-success'] {
color: var(--tui-text-primary-on-accent-1); color: var(--tui-text-primary-on-accent-1);
background: var(--tui-status-positive); background: var(--tui-status-positive);
@@ -108,8 +101,7 @@
} }
@include taiga.appearance-disabled { @include taiga.appearance-disabled {
background: var(--tui-status-neutral); opacity: var(--tui-disabled-opacity);
color: #333;
} }
} }

View File

@@ -21,7 +21,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
</div> </div>
<div tuiCell> <div tuiCell>
<div tuiTitle> <div tuiTitle>
<strong>Git Hash</strong> <strong>{{ 'Git hash' | i18n }}</strong>
<div tuiSubtitle tuiFade>{{ gitHash }}</div> <div tuiSubtitle tuiFade>{{ gitHash }}</div>
</div> </div>
<button <button
@@ -35,7 +35,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
</div> </div>
<div tuiCell> <div tuiCell>
<div tuiTitle> <div tuiTitle>
<strong>CA fingerprint</strong> <strong>{{ 'CA fingerprint' | i18n }}</strong>
<div tuiSubtitle tuiFade>{{ server.caFingerprint }}</div> <div tuiSubtitle tuiFade>{{ server.caFingerprint }}</div>
</div> </div>
<button <button

View File

@@ -5,8 +5,8 @@ import {
input, input,
inject, inject,
} from '@angular/core' } from '@angular/core'
import { TuiTitle } from '@taiga-ui/core' import { TuiIcon, TuiTitle } from '@taiga-ui/core'
import { TuiSkeleton, TuiSwitch } from '@taiga-ui/kit' import { TuiSkeleton, TuiSwitch, TuiTooltip } from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { i18nPipe, LoadingService, ErrorService } from '@start9labs/shared' import { i18nPipe, LoadingService, ErrorService } from '@start9labs/shared'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell } from '@taiga-ui/layout'
@@ -19,8 +19,15 @@ import { InterfaceComponent } from './interface.component'
template: ` template: `
<header>{{ 'Gateways' | i18n }}</header> <header>{{ 'Gateways' | i18n }}</header>
@for (gateway of gateways(); track $index) { @for (gateway of gateways(); track $index) {
<label tuiCell="s"> <label tuiCell="s" [style.background]="">
<span tuiTitle>{{ gateway.ipInfo.name }}</span> <span tuiTitle [style.opacity]="1">{{ gateway.ipInfo.name }}</span>
@if (!interface.packageId() && !gateway.public) {
<tui-icon
[tuiTooltip]="
'Cannot disable private gateways for StartOS UI' | i18n
"
/>
}
<input <input
type="checkbox" type="checkbox"
tuiSwitch tuiSwitch
@@ -47,6 +54,10 @@ import { InterfaceComponent } from './interface.component'
background: transparent; background: transparent;
} }
} }
[tuiCell]:has([tuiTooltip]) {
background: none !important;
}
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -58,6 +69,8 @@ import { InterfaceComponent } from './interface.component'
TuiCell, TuiCell,
TuiTitle, TuiTitle,
TuiSkeleton, TuiSkeleton,
TuiIcon,
TuiTooltip,
], ],
}) })
export class InterfaceGatewaysComponent { export class InterfaceGatewaysComponent {

View File

@@ -52,7 +52,7 @@ import { MarketplaceSidebarService } from '../services/sidebar.service'
:host { :host {
cursor: pointer; cursor: pointer;
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both; animation: animateIn 400ms calc(var(--animation-order) * 50ms) both;
} }
tui-drawer { tui-drawer {

View File

@@ -31,7 +31,7 @@ import { i18nPipe } from '@start9labs/shared'
styles: ` styles: `
:host { :host {
min-height: 12rem; min-height: 12rem;
grid-column: span 3; grid-column: span 4;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },

View File

@@ -3,48 +3,48 @@ import {
Component, Component,
inject, inject,
Input, Input,
DOCUMENT,
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiButton } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit' import { TuiBadge } from '@taiga-ui/kit'
import { ConfigService } from 'src/app/services/config.service' import { InterfaceService } from 'src/app/routes/portal/components/interfaces/interface.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { InterfaceService } from '../../../components/interfaces/interface.service'
@Component({ @Component({
selector: 'tr[serviceInterface]', selector: 'tr[serviceInterface]',
template: ` template: `
<td> <td><ng-content /></td>
<strong>{{ info.name }}</strong>
</td>
<td> <td>
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge> <tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
</td> </td>
<td class="g-secondary" [style.grid-area]="'2 / span 4'"> <td class="g-secondary" [style.grid-area]="'2 / 1 / 2 / 3'">
{{ info.description }} {{ info.description }}
</td> </td>
<td> <td>
@if (info.type === 'ui') { @if (info.type === 'ui') {
<button <a
tuiIconButton tuiIconButton
iconStart="@tui.external-link" iconStart="@tui.external-link"
appearance="flat-grayscale" appearance="flat-grayscale"
[disabled]="disabled" target="_blank"
(click)="openUI()" rel="noopener noreferrer"
></button> [attr.href]="disabled ? null : href"
(click.stop)="(0)"
></a>
} }
<a
tuiIconButton
iconStart="@tui.settings"
appearance="flat-grayscale"
[routerLink]="info.routerLink"
></a>
</td> </td>
`, `,
styles: ` styles: `
strong { :host {
clip-path: inset(0 round 0.75rem);
cursor: pointer;
&:hover {
background: var(--tui-background-neutral-1);
}
}
td:first-child {
white-space: nowrap; white-space: nowrap;
} }
@@ -58,7 +58,7 @@ import { InterfaceService } from '../../../components/interfaces/interface.servi
} }
td:last-child { td:last-child {
grid-area: 3 / span 4; grid-area: 1 / 3 / span 2 / 3;
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;
flex-direction: row-reverse; flex-direction: row-reverse;
@@ -68,7 +68,7 @@ import { InterfaceService } from '../../../components/interfaces/interface.servi
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
display: grid; display: grid;
grid-template-columns: repeat(3, min-content) 1fr; grid-template-columns: min-content;
align-items: center; align-items: center;
padding: 1rem 0.5rem; padding: 1rem 0.5rem;
gap: 0.5rem; gap: 0.5rem;
@@ -80,16 +80,13 @@ import { InterfaceService } from '../../../components/interfaces/interface.servi
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiBadge, RouterLink], imports: [TuiButton, TuiBadge],
}) })
export class ServiceInterfaceItemComponent { export class ServiceInterfaceItemComponent {
private readonly interfaceService = inject(InterfaceService) private readonly interfaceService = inject(InterfaceService)
private readonly document = inject(DOCUMENT)
@Input({ required: true }) @Input({ required: true })
info!: T.ServiceInterface & { info!: T.ServiceInterface
routerLink: string
}
@Input({ required: true }) @Input({ required: true })
pkg!: PackageDataEntry pkg!: PackageDataEntry
@@ -110,11 +107,9 @@ export class ServiceInterfaceItemComponent {
get href() { get href() {
const host = this.pkg.hosts[this.info.addressInfo.hostId] const host = this.pkg.hosts[this.info.addressInfo.hostId]
if (!host) return ''
return this.interfaceService.launchableAddress(this.info, host)
}
openUI() { return host
this.document.defaultView?.open(this.href, '_blank', 'noreferrer') ? this.interfaceService.launchableAddress(this.info, host)
: null
} }
} }

View File

@@ -4,6 +4,7 @@ import {
computed, computed,
input, input,
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table' import { TuiTable } from '@taiga-ui/addon-table'
import { tuiDefaultSort } from '@taiga-ui/cdk' import { tuiDefaultSort } from '@taiga-ui/cdk'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -27,11 +28,17 @@ import { PlaceholderComponent } from '../../../components/placeholder.component'
<tbody> <tbody>
@for (info of interfaces(); track $index) { @for (info of interfaces(); track $index) {
<tr <tr
tabindex="-1"
serviceInterface serviceInterface
[info]="info" [info]="info"
[pkg]="pkg()" [pkg]="pkg()"
[disabled]="disabled()" [disabled]="disabled()"
></tr> [routerLink]="info.routerLink"
>
<a [routerLink]="info.routerLink">
<strong>{{ info.name }}</strong>
</a>
</tr>
} @empty { } @empty {
<app-placeholder icon="@tui.monitor-x"> <app-placeholder icon="@tui.monitor-x">
{{ 'No service interfaces' | i18n }} {{ 'No service interfaces' | i18n }}
@@ -42,7 +49,7 @@ import { PlaceholderComponent } from '../../../components/placeholder.component'
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 6; grid-column: span 7;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
@@ -52,6 +59,7 @@ import { PlaceholderComponent } from '../../../components/placeholder.component'
TuiTable, TuiTable,
i18nPipe, i18nPipe,
PlaceholderComponent, PlaceholderComponent,
RouterLink,
], ],
}) })
export class ServiceInterfacesComponent { export class ServiceInterfacesComponent {

View File

@@ -50,7 +50,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
size="m" size="m"
[max]="100" [max]="100"
[class.g-positive]="phase.progress === true" [class.g-positive]="phase.progress === true"
[value]="isIndeterminate(phase.progress) ? undefined : percent" [attr.value]="isIndeterminate(phase.progress) ? undefined : percent"
></progress> ></progress>
</div> </div>
} }

View File

@@ -43,7 +43,7 @@ import {
`, `,
styles: ` styles: `
:host { :host {
grid-column: span 2; grid-column: span 3;
min-height: 12rem; min-height: 12rem;
} }
@@ -78,14 +78,14 @@ import {
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
:host { :host {
min-height: 0; min-height: 0;
} }
div { div {
display: grid; display: grid;
grid-template-columns: 1fr max-content; grid-template-columns: 1fr max-content;
padding: 0.5rem 0; padding: 0.5rem 0;
} }
h3 { h3 {
text-align: left; text-align: left;

View File

@@ -23,35 +23,26 @@ import { getManifest } from 'src/app/utils/get-package-data'
@Component({ @Component({
selector: 'tr[task]', selector: 'tr[task]',
template: ` template: `
<td tuiFade> <td tuiFade class="row">
<tui-avatar size="xs"><img [src]="pkg()?.icon" alt="" /></tui-avatar> <tui-avatar size="xs"><img [src]="pkg()?.icon" alt="" /></tui-avatar>
<span>{{ pkgTitle() }}</span> <span>{{ pkgTitle() }}</span>
</td> </td>
<td> <td [style.grid-row]="2">
{{ pkg()?.actions?.[task().actionId]?.name }} <strong>{{ pkg()?.actions?.[task().actionId]?.name }}</strong>
</td> </td>
<td> <td class="row">
@if (task().severity === 'critical') { @if (task().severity === 'critical') {
<strong [style.color]="'var(--tui-status-warning)'"> <strong class="g-warning">{{ 'Required' | i18n }}</strong>
{{ 'Required' | i18n }}
</strong>
} @else if (task().severity === 'important') { } @else if (task().severity === 'important') {
<strong [style.color]="'var(--tui-status-info)'"> <strong class="g-info">{{ 'Recommended' | i18n }}</strong>
{{ 'Recommended' | i18n }}
</strong>
} @else { } @else {
<strong> <strong>{{ 'Optional' | i18n }}</strong>
{{ 'Optional' | i18n }}
</strong>
} }
</td> </td>
<td <td class="g-secondary" [style.grid-row]="3">
[style.color]="'var(--tui-text-secondary)'"
[style.grid-area]="'2 / span 4'"
>
{{ task().reason || ('No reason provided' | i18n) }} {{ task().reason || ('No reason provided' | i18n) }}
</td> </td>
<td> <td [style.grid-area]="'2 / 2 / 4'">
@if (task().severity !== 'critical') { @if (task().severity !== 'critical') {
<button <button
tuiIconButton tuiIconButton
@@ -76,24 +67,25 @@ import { getManifest } from 'src/app/utils/get-package-data'
} }
td:last-child { td:last-child {
grid-area: 3 / span 4;
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;
flex-direction: row-reverse; justify-content: end;
justify-content: flex-end;
gap: 0.5rem;
} }
span { span {
margin-inline-start: 0.5rem; margin-inline-start: 0.5rem;
line-height: 1.5rem;
vertical-align: middle; vertical-align: middle;
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
display: grid; display: grid;
align-items: center; grid-template-columns: 1fr min-content;
padding: 1rem 0rem 1rem 0.5rem; padding: 1rem 0.5rem;
gap: 0.5rem;
.row {
margin-bottom: 1rem;
}
td { td {
display: flex; display: flex;

View File

@@ -39,7 +39,7 @@ import { i18nPipe } from '@start9labs/shared'
styles: ` styles: `
:host { :host {
min-height: 12rem; min-height: 12rem;
grid-column: span 6; grid-column: span 10;
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },

View File

@@ -32,7 +32,7 @@ import { distinctUntilChanged } from 'rxjs/operators'
styles: [ styles: [
` `
:host { :host {
grid-column: span 4; grid-column: span 3;
} }
h3 { h3 {
@@ -55,11 +55,13 @@ import { distinctUntilChanged } from 'rxjs/operators'
text-transform: uppercase; text-transform: uppercase;
color: var(--tui-text-secondary); color: var(--tui-text-secondary);
font: var(--tui-font-text-ui-xs); font: var(--tui-font-text-ui-xs);
font-size: min(4cqw, 0.75rem);
} }
label { label {
display: block; display: block;
font-size: min(6vw, 2.5rem); font-size: min(6vw, 2.5rem);
font-size: min(20cqw, 2.5rem);
margin: 1rem 0; margin: 1rem 0;
color: var(--tui-text-primary); color: var(--tui-text-primary);
} }

View File

@@ -1,17 +1,22 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table' import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
import { TuiButton, TuiLoader } from '@taiga-ui/core' 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 { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
import { DepErrorService } from 'src/app/services/dep-error.service' import { DepErrorService } from 'src/app/services/dep-error.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
import { ServiceComponent } from './service.component' import { ServiceComponent } from './service.component'
import { ServicesService } from './services.service'
import { i18nPipe } from '@start9labs/shared'
@Component({ @Component({
template: ` template: `
@@ -124,13 +129,17 @@ import { i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export default class DashboardComponent { export default class DashboardComponent {
readonly services = toSignal(inject(ServicesService))
readonly errors = toSignal(inject(DepErrorService).depErrors$) readonly errors = toSignal(inject(DepErrorService).depErrors$)
readonly services = toSignal(
inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs => Object.values(pkgs).sort(byName)),
shareReplay(1),
),
)
readonly name: TuiComparator<PackageDataEntry> = (a, b) => readonly name: TuiComparator<PackageDataEntry> = byName
getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1
: 1
readonly status: TuiComparator<PackageDataEntry> = (a, b) => readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1 getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
@@ -140,3 +149,9 @@ export default class DashboardComponent {
sorter = this.name sorter = this.name
} }
function byName(a: PackageDataEntry, b: PackageDataEntry) {
return getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1
: 1
}

View File

@@ -1,31 +0,0 @@
import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { map, Observable, shareReplay } from 'rxjs'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
@Injectable({
providedIn: 'root',
})
export class ServicesService extends Observable<readonly PackageDataEntry[]> {
private readonly services$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs =>
Object.values(pkgs).sort((a, b) =>
getManifest(b).title.toLowerCase() >
getManifest(a).title.toLowerCase()
? -1
: 1,
),
),
shareReplay(1),
)
constructor() {
super(subscriber => this.services$.subscribe(subscriber))
}
}

View File

@@ -75,6 +75,14 @@ export default class ServiceAboutRoute {
{ {
header: 'General', header: 'General',
items: [ items: [
{
name: 'Title',
value: manifest.title,
},
{
name: 'ID' as i18nKey,
value: manifest.id,
},
{ {
name: 'Version', name: 'Version',
value: manifest.version, value: manifest.version,
@@ -82,18 +90,14 @@ export default class ServiceAboutRoute {
action: () => this.copyService.copy(manifest.version), action: () => this.copyService.copy(manifest.version),
}, },
{ {
name: 'Installed From', name: 'Git hash',
value: pkg.registry || NOT_PROVIDED,
},
{
name: 'Git Hash',
value: manifest.gitHash || '-', value: manifest.gitHash || '-',
icon: manifest.gitHash ? '@tui.copy' : '', icon: manifest.gitHash ? '@tui.copy' : '',
action: () => action: () =>
manifest.gitHash && this.copyService.copy(manifest.gitHash), manifest.gitHash && this.copyService.copy(manifest.gitHash),
}, },
{ {
name: 'SDK Version', name: 'SDK version',
value: manifest.sdkVersion || '-', value: manifest.sdkVersion || '-',
icon: manifest.sdkVersion ? '@tui.copy' : '', icon: manifest.sdkVersion ? '@tui.copy' : '',
action: () => action: () =>
@@ -106,6 +110,10 @@ export default class ServiceAboutRoute {
icon: '@tui.chevron-right', icon: '@tui.chevron-right',
action: () => this.markdown.subscribe(), action: () => this.markdown.subscribe(),
}, },
{
name: 'Installed from',
value: pkg.registry || NOT_PROVIDED,
},
], ],
}, },
{ {
@@ -124,10 +132,6 @@ export default class ServiceAboutRoute {
{ {
header: 'Links', header: 'Links',
items: [ items: [
{
name: 'Marketing',
value: manifest.marketingSite || NOT_PROVIDED,
},
{ {
name: 'Documentation', name: 'Documentation',
value: manifest.docsUrl || NOT_PROVIDED, value: manifest.docsUrl || NOT_PROVIDED,
@@ -136,6 +140,10 @@ export default class ServiceAboutRoute {
name: 'Support', name: 'Support',
value: manifest.supportSite || NOT_PROVIDED, value: manifest.supportSite || NOT_PROVIDED,
}, },
{
name: 'Marketing',
value: manifest.marketingSite || NOT_PROVIDED,
},
{ {
name: 'Donations', name: 'Donations',
value: manifest.donationUrl || NOT_PROVIDED, value: manifest.donationUrl || NOT_PROVIDED,

View File

@@ -31,10 +31,16 @@ const INACTIVE: PrimaryStatus[] = [
@Component({ @Component({
template: ` template: `
@if (service()) { @if (service()) {
<div *title class="title"> <div
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> *title
<div routerLink="./" class="m-header"> class="title"
<tui-avatar size="xs" [style.margin-inline-end.rem]="0.75"> [style.--background]="'url(' + service()?.icon + ')'"
>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
<div routerLink="./">
<tui-avatar size="xs" [style.margin]="'0 0.75rem 0.125rem 0'">
<img alt="" [src]="service()?.icon" /> <img alt="" [src]="service()?.icon" />
</tui-avatar> </tui-avatar>
<span tuiFade>{{ manifest()?.title }}</span> <span tuiFade>{{ manifest()?.title }}</span>
@@ -45,33 +51,52 @@ const INACTIVE: PrimaryStatus[] = [
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar> <tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar>
<span tuiTitle> <span tuiTitle>
<strong tuiFade>{{ manifest()?.title }}</strong> <strong tuiFade>{{ manifest()?.title }}</strong>
<span tuiSubtitle [style.textTransform]="'none'"> <span tuiSubtitle>{{ manifest()?.version }}</span>
{{ manifest()?.version }}
</span>
</span> </span>
</header> </header>
<nav [attr.inert]="isInactive() ? '' : null"> <nav [attr.inert]="isInactive() ? '' : null">
@for (item of nav; track $index) { @for (item of nav; track $index) {
<a @if (item.title === 'Documentation') {
tuiCell <a
tuiAppearance="action-grayscale" tuiCell
routerLinkActive="active" tuiAppearance="action-grayscale"
[routerLinkActiveOptions]="{ exact: true }" [href]="manifest()?.docsUrl"
[routerLink]="item.title === 'dashboard' ? './' : item.title" target="_blank"
> noreferrer
<tui-icon [icon]="item.icon" /> >
<span tuiTitle>{{ item.title | i18n }}</span> <tui-icon [icon]="item.icon" />
@if (item.title === 'dashboard') { <span tuiTitle>
<a routerLink="interface" routerLinkActive="active"></a> <span>
} {{ item.title | i18n }}
</a> </span>
</span>
<tui-icon icon="@tui.external-link" [style.font-size.rem]="1" />
</a>
} @else {
<a
tuiCell
tuiAppearance="action-grayscale"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
[routerLink]="item.title === 'dashboard' ? './' : item.title"
>
<tui-icon [icon]="item.icon" />
<span tuiTitle>{{ item.title | i18n }}</span>
@if (item.title === 'dashboard') {
<a routerLink="interface" routerLinkActive="active"></a>
}
</a>
}
} }
</nav> </nav>
</aside> </aside>
} }
<router-outlet /> <router-outlet />
`, `,
host: { class: 'g-page' }, host: {
class: 'g-page',
'[style.--background]': '"url(" + service()?.icon + ")"',
},
styles: ` styles: `
:host { :host {
display: flex; display: flex;
@@ -88,9 +113,31 @@ const INACTIVE: PrimaryStatus[] = [
} }
} }
[tuiSubtitle] {
text-transform: lowercase;
}
header { header {
margin: 0 -0.5rem; margin: -0.5rem -0.5rem 0;
padding-top: 1rem;
border-radius: 0;
cursor: pointer; cursor: pointer;
box-shadow: 0 -1px rgba(255, 255, 255, 0.1);
}
header::before,
.title::before {
content: '';
position: absolute;
inset: 0;
background: var(--background);
background-size: 1px;
mask: linear-gradient(to bottom, black, transparent);
opacity: 0.2;
}
.title::before {
mask: linear-gradient(to bottom right, black, transparent);
} }
nav[inert] a:not(:first-child) { nav[inert] a:not(:first-child) {
@@ -110,11 +157,6 @@ const INACTIVE: PrimaryStatus[] = [
} }
} }
.m-header {
cursor: pointer;
display: flex;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
flex-direction: column; flex-direction: column;
padding: 0; padding: 0;
@@ -150,7 +192,8 @@ const INACTIVE: PrimaryStatus[] = [
box-shadow: none; box-shadow: none;
} }
[tuiTitle] { [tuiTitle],
tui-icon:last-child {
display: none; display: none;
} }
} }
@@ -181,6 +224,7 @@ export class ServiceOutletComponent {
{ title: 'actions', icon: '@tui.clapperboard' }, { title: 'actions', icon: '@tui.clapperboard' },
{ title: 'logs', icon: '@tui.logs' }, { title: 'logs', icon: '@tui.logs' },
{ title: 'about', icon: '@tui.info' }, { title: 'about', icon: '@tui.info' },
{ title: 'Documentation', icon: '@tui.book-open-text' },
] ]
protected readonly service = toSignal( protected readonly service = toSignal(

View File

@@ -50,11 +50,11 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
</service-status> </service-status>
@if (status() !== 'backingUp') { @if (status() !== 'backingUp') {
<service-health-checks [checks]="health()" />
<service-uptime <service-uptime
class="g-card" class="g-card"
[started]="$any(pkg.status)?.started" [started]="$any(pkg.status)?.started"
/> />
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
@if (errors() | async; as errors) { @if (errors() | async; as errors) {
<service-dependencies <service-dependencies
@@ -63,8 +63,8 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
[errors]="errors" [errors]="errors"
/> />
} }
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
<service-health-checks [checks]="health()" />
<service-tasks <service-tasks
#tasks="elementRef" #tasks="elementRef"
tuiElement tuiElement
@@ -109,7 +109,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
:host { :host {
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(10, 1fr);
grid-auto-rows: max-content; grid-auto-rows: max-content;
gap: 1rem; gap: 1rem;
} }

View File

@@ -51,14 +51,8 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
<form-group [spec]="d.spec" /> <form-group [spec]="d.spec" />
@if (d.warn.length; as length) { @for (warn of d.warn; track $index) {
<p> <p>{{ warn }}</p>
Warning. StartOS is currently using {{ d.warn.join(', ') }} for DNS.
Therefore, {{ length > 1 ? 'they' : 'it' }} cannot use StartOS for
DNS. This is circular. If you want to use StartOS as the DNS server
for {{ d.warn.join(', ') }} for private domain resolution, you must
set custom DNS servers above.
</p>
} }
<footer> <footer>
@@ -122,20 +116,20 @@ export default class SystemDnsComponent {
name: 'DHCP', name: 'DHCP',
spec: ISB.InputSpec.of({ spec: ISB.InputSpec.of({
servers: ISB.Value.dynamicText(() => ({ servers: ISB.Value.dynamicText(() => ({
name: 'DHCP Servers', name: this.i18n.transform('DHCP Servers'),
default: null, default: null,
required: true, required: true,
disabled: 'Cannot edit DHCP servers', disabled: this.i18n.transform('Cannot edit DHCP servers'),
})), })),
}), }),
}, },
static: { static: {
name: 'Static', name: this.i18n.transform('Static'),
spec: ISB.InputSpec.of({ spec: ISB.InputSpec.of({
servers: ISB.Value.list( servers: ISB.Value.list(
ISB.List.text( ISB.List.text(
{ {
name: 'Static Servers', name: this.i18n.transform('Static Servers'),
minLength: 1, minLength: 1,
maxLength: 3, maxLength: 3,
}, },
@@ -157,13 +151,12 @@ export default class SystemDnsComponent {
const spec = await configBuilderToSpec(this.dnsSpec) const spec = await configBuilderToSpec(this.dnsSpec)
const dhcpServers = { servers: dns.dhcpServers.join(', ') } const dhcpServers = { servers: dns.dhcpServers.join(', ') }
const staticServers = { servers: dns.staticServers || [] }
const current: (typeof this.dnsSpec._TYPE)['strategy'] = const current: (typeof this.dnsSpec._TYPE)['strategy'] =
dns.staticServers dns.staticServers
? { ? {
selection: 'static', selection: 'static',
value: staticServers, value: { servers: dns.staticServers || [] },
other: { other: {
dhcp: dhcpServers, dhcp: dhcpServers,
}, },
@@ -175,19 +168,29 @@ export default class SystemDnsComponent {
const form = this.formService.createForm(spec, { strategy: current }) const form = this.formService.createForm(spec, { strategy: current })
let warn: string[] = []
if (
Object.values(pkgs).some(p =>
Object.values(p.hosts).some(h => h?.privateDomains.length),
)
) {
Object.values(gateways)
.filter(g =>
(dns.staticServers || dns.dhcpServers).some(d =>
g.ipInfo?.lanIp.includes(d),
),
)
.map(
g =>
`${this.i18n.transform('Warning. StartOS is currently using the following gateway for DNS')}: ${g.ipInfo!.name}. ${this.i18n.transform('If you intend to use this gateway for private domain resolution, set alternative static DNS servers using the form above.')}`,
)
}
return { return {
spec, spec,
form, form,
warn: warn,
(Object.values(pkgs).some(p =>
Object.values(p.hosts).some(h => h?.privateDomains.length),
) ||
[]) &&
Object.values(gateways)
.filter(g =>
dns.dhcpServers.some(d => g.ipInfo?.lanIp.includes(d)),
)
.map(g => g.ipInfo?.name),
} }
}), }),
), ),

View File

@@ -24,7 +24,9 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{ 'Change Password' | i18n }} {{ 'Change Password' | i18n }}
</ng-container> </ng-container>
<header tuiHeader> <header tuiHeader>

View File

@@ -17,7 +17,9 @@ import { SessionsTableComponent } from './table.component'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{ 'Active Sessions' | i18n }} {{ 'Active Sessions' | i18n }}
</ng-container> </ng-container>

View File

@@ -42,7 +42,9 @@ import { wifiSpec } from './wifi.const'
@Component({ @Component({
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
WiFi WiFi
</ng-container> </ng-container>
<header tuiHeader> <header tuiHeader>

View File

@@ -146,7 +146,7 @@ export class MarketplaceService {
} }
private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> { private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {
console.warn('FETCHING REGISTRY: ', url) console.log('FETCHING REGISTRY: ', url)
return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe( return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe(
map(([info, packages]) => ({ info, packages, url })), map(([info, packages]) => ({ info, packages, url })),
catchError(e => { catchError(e => {

View File

@@ -86,6 +86,7 @@ hr {
} }
.g-card { .g-card {
container: card / inline-size;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;