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
[style.pointer-events]="'none'"
[data]="pkg().sdkVersion || 'Unknown'"
label="SDK Version"
label="SDK version"
icon=""
/>
<!-- git hash -->

View File

@@ -491,7 +491,7 @@ export default {
518: 'Verwerfen',
520: 'Update verfügbar',
521: 'Um das Problem zu beheben, siehe',
522: 'SDK Version',
522: 'SDK version',
523: 'Sicherungsbericht',
524: 'Ausgewählte löschen',
525: 'Keine SSH-Schlüssel',
@@ -577,4 +577,12 @@ export default {
610: 'Dynamisches DNS',
611: 'Keine Service-Schnittstellen',
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

View File

@@ -254,9 +254,9 @@ export const ENGLISH = {
'unknown %': 270,
'Not provided': 271,
'Links': 272,
'Git Hash': 273,
'Git hash': 273,
'License': 274,
'Installed From': 275,
'Installed from': 275,
'Marketing': 278,
'Support': 279,
'Donations': 280,
@@ -490,7 +490,7 @@ export const ENGLISH = {
'Dismiss': 518, // as in, dismiss or delete a task
'Update available': 520,
'To resolve the issue, refer to': 521,
'SDK Version': 522,
'SDK version': 522,
'Backup Report': 523,
'Delete selected': 524,
'No SSH keys': 525,
@@ -576,4 +576,12 @@ export const ENGLISH = {
'Dynamic DNS': 610,
'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
'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

View File

@@ -577,4 +577,12 @@ export default {
610: 'DNS dinámico',
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',
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

View File

@@ -577,4 +577,12 @@ export default {
610: 'DNS dynamique',
611: 'Aucune interface de service',
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

View File

@@ -577,4 +577,12 @@ export default {
610: 'Dynamiczny DNS',
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',
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

View File

@@ -88,13 +88,6 @@
--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'] {
color: var(--tui-text-primary-on-accent-1);
background: var(--tui-status-positive);
@@ -108,8 +101,7 @@
}
@include taiga.appearance-disabled {
background: var(--tui-status-neutral);
color: #333;
opacity: var(--tui-disabled-opacity);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,22 @@
import { ChangeDetectionStrategy, Component, inject } 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 { 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 { TitleDirective } from 'src/app/services/title.service'
import { getManifest } from 'src/app/utils/get-package-data'
import { ServiceComponent } from './service.component'
import { ServicesService } from './services.service'
import { i18nPipe } from '@start9labs/shared'
@Component({
template: `
@@ -124,13 +129,17 @@ import { i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class DashboardComponent {
readonly services = toSignal(inject(ServicesService))
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) =>
getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1
: 1
readonly name: TuiComparator<PackageDataEntry> = byName
readonly status: TuiComparator<PackageDataEntry> = (a, b) =>
getInstalledPrimaryStatus(b) > getInstalledPrimaryStatus(a) ? -1 : 1
@@ -140,3 +149,9 @@ export default class DashboardComponent {
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',
items: [
{
name: 'Title',
value: manifest.title,
},
{
name: 'ID' as i18nKey,
value: manifest.id,
},
{
name: 'Version',
value: manifest.version,
@@ -82,18 +90,14 @@ export default class ServiceAboutRoute {
action: () => this.copyService.copy(manifest.version),
},
{
name: 'Installed From',
value: pkg.registry || NOT_PROVIDED,
},
{
name: 'Git Hash',
name: 'Git hash',
value: manifest.gitHash || '-',
icon: manifest.gitHash ? '@tui.copy' : '',
action: () =>
manifest.gitHash && this.copyService.copy(manifest.gitHash),
},
{
name: 'SDK Version',
name: 'SDK version',
value: manifest.sdkVersion || '-',
icon: manifest.sdkVersion ? '@tui.copy' : '',
action: () =>
@@ -106,6 +110,10 @@ export default class ServiceAboutRoute {
icon: '@tui.chevron-right',
action: () => this.markdown.subscribe(),
},
{
name: 'Installed from',
value: pkg.registry || NOT_PROVIDED,
},
],
},
{
@@ -124,10 +132,6 @@ export default class ServiceAboutRoute {
{
header: 'Links',
items: [
{
name: 'Marketing',
value: manifest.marketingSite || NOT_PROVIDED,
},
{
name: 'Documentation',
value: manifest.docsUrl || NOT_PROVIDED,
@@ -136,6 +140,10 @@ export default class ServiceAboutRoute {
name: 'Support',
value: manifest.supportSite || NOT_PROVIDED,
},
{
name: 'Marketing',
value: manifest.marketingSite || NOT_PROVIDED,
},
{
name: 'Donations',
value: manifest.donationUrl || NOT_PROVIDED,

View File

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

View File

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

View File

@@ -51,14 +51,8 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
<form-group [spec]="d.spec" />
@if (d.warn.length; as length) {
<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>
@for (warn of d.warn; track $index) {
<p>{{ warn }}</p>
}
<footer>
@@ -122,20 +116,20 @@ export default class SystemDnsComponent {
name: 'DHCP',
spec: ISB.InputSpec.of({
servers: ISB.Value.dynamicText(() => ({
name: 'DHCP Servers',
name: this.i18n.transform('DHCP Servers'),
default: null,
required: true,
disabled: 'Cannot edit DHCP servers',
disabled: this.i18n.transform('Cannot edit DHCP servers'),
})),
}),
},
static: {
name: 'Static',
name: this.i18n.transform('Static'),
spec: ISB.InputSpec.of({
servers: ISB.Value.list(
ISB.List.text(
{
name: 'Static Servers',
name: this.i18n.transform('Static Servers'),
minLength: 1,
maxLength: 3,
},
@@ -157,13 +151,12 @@ export default class SystemDnsComponent {
const spec = await configBuilderToSpec(this.dnsSpec)
const dhcpServers = { servers: dns.dhcpServers.join(', ') }
const staticServers = { servers: dns.staticServers || [] }
const current: (typeof this.dnsSpec._TYPE)['strategy'] =
dns.staticServers
? {
selection: 'static',
value: staticServers,
value: { servers: dns.staticServers || [] },
other: {
dhcp: dhcpServers,
},
@@ -175,19 +168,29 @@ export default class SystemDnsComponent {
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 {
spec,
form,
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),
warn,
}
}),
),

View File

@@ -24,7 +24,9 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
@Component({
template: `
<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 }}
</ng-container>
<header tuiHeader>

View File

@@ -17,7 +17,9 @@ import { SessionsTableComponent } from './table.component'
@Component({
template: `
<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 }}
</ng-container>

View File

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

View File

@@ -146,7 +146,7 @@ export class MarketplaceService {
}
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(
map(([info, packages]) => ({ info, packages, url })),
catchError(e => {

View File

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