mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge branch 'feat/preferred-port-design' of github.com:Start9Labs/start-os into feat/preferred-port-design
This commit is contained in:
@@ -176,8 +176,6 @@ pub struct AttachParams {
|
||||
pub guid: InternedString,
|
||||
#[ts(optional)]
|
||||
pub kiosk: Option<bool>,
|
||||
pub name: Option<InternedString>,
|
||||
pub hostname: Option<InternedString>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
@@ -187,8 +185,6 @@ pub async fn attach(
|
||||
password,
|
||||
guid: disk_guid,
|
||||
kiosk,
|
||||
name,
|
||||
hostname,
|
||||
}: AttachParams,
|
||||
) -> Result<SetupProgress, Error> {
|
||||
let setup_ctx = ctx.clone();
|
||||
@@ -242,10 +238,8 @@ pub async fn attach(
|
||||
}
|
||||
disk_phase.complete();
|
||||
|
||||
let hostname = ServerHostnameInfo::new_opt(name, hostname)?;
|
||||
|
||||
let (account, net_ctrl) =
|
||||
setup_init(&setup_ctx, password, kiosk, hostname, init_phases).await?;
|
||||
setup_init(&setup_ctx, password, kiosk, None, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&setup_ctx.webserver,
|
||||
|
||||
@@ -5,6 +5,4 @@ export type AttachParams = {
|
||||
password: EncryptedWire | null
|
||||
guid: string
|
||||
kiosk?: boolean
|
||||
name: string | null
|
||||
hostname: string | null
|
||||
}
|
||||
|
||||
@@ -10,15 +10,14 @@ import {
|
||||
} from '@angular/forms'
|
||||
import {
|
||||
ErrorService,
|
||||
generateHostname,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
normalizeHostname,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiAutoFocus, TuiMapperPipe, TuiValidator } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiError,
|
||||
TuiHint,
|
||||
TuiIcon,
|
||||
TuiTextfield,
|
||||
TuiTitle,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
import {
|
||||
TuiFieldErrorPipe,
|
||||
TuiPassword,
|
||||
TuiTooltip,
|
||||
tuiValidationErrorsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
|
||||
@@ -48,29 +46,16 @@ import { StateService } from '../services/state.service'
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
@if (isFresh) {
|
||||
<tui-textfield>
|
||||
<label tuiLabel>{{ 'Server Hostname' | i18n }}</label>
|
||||
<input tuiTextfield tuiAutoFocus formControlName="hostname" />
|
||||
<span class="local-suffix">.local</span>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
iconStart="@tui.refresh-cw"
|
||||
size="xs"
|
||||
[tuiHint]="'Randomize' | i18n"
|
||||
(click)="randomizeHostname()"
|
||||
></button>
|
||||
<tui-icon
|
||||
[tuiTooltip]="
|
||||
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.'
|
||||
| i18n
|
||||
"
|
||||
/>
|
||||
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
|
||||
<input tuiTextfield tuiAutoFocus formControlName="name" />
|
||||
</tui-textfield>
|
||||
<tui-error
|
||||
formControlName="hostname"
|
||||
formControlName="name"
|
||||
[error]="[] | tuiFieldError | async"
|
||||
/>
|
||||
@if (form.controls.name.value?.trim()) {
|
||||
<p class="hostname-preview">{{ derivedHostname }}.local</p>
|
||||
}
|
||||
}
|
||||
|
||||
<tui-textfield [style.margin-top.rem]="isFresh ? 1 : 0">
|
||||
@@ -134,8 +119,10 @@ import { StateService } from '../services/state.service'
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
.local-suffix {
|
||||
.hostname-preview {
|
||||
color: var(--tui-text-secondary);
|
||||
font: var(--tui-font-text-s);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
@@ -160,8 +147,6 @@ import { StateService } from '../services/state.service'
|
||||
TuiMapperPipe,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
TuiHint,
|
||||
TuiTooltip,
|
||||
i18nPipe,
|
||||
],
|
||||
providers: [
|
||||
@@ -170,7 +155,6 @@ import { StateService } from '../services/state.service'
|
||||
minlength: 'Must be 12 characters or greater',
|
||||
maxlength: 'Must be 64 character or less',
|
||||
match: 'Passwords do not match',
|
||||
pattern: 'Only lowercase letters, numbers, and hyphens allowed',
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -181,7 +165,7 @@ export default class PasswordPage {
|
||||
private readonly stateService = inject(StateService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
// Fresh install requires password and hostname
|
||||
// Fresh install requires password and name
|
||||
readonly isFresh = this.stateService.setupType === 'fresh'
|
||||
|
||||
readonly form = new FormGroup({
|
||||
@@ -191,10 +175,7 @@ export default class PasswordPage {
|
||||
Validators.maxLength(64),
|
||||
]),
|
||||
confirm: new FormControl(''),
|
||||
hostname: new FormControl(generateHostname(), [
|
||||
Validators.required,
|
||||
Validators.pattern(/^[a-z0-9][a-z0-9-]*$/),
|
||||
]),
|
||||
name: new FormControl('', [Validators.required]),
|
||||
})
|
||||
|
||||
readonly validator = (value: string) => (control: AbstractControl) =>
|
||||
@@ -202,8 +183,8 @@ export default class PasswordPage {
|
||||
? null
|
||||
: { match: this.i18n.transform('Passwords do not match') }
|
||||
|
||||
randomizeHostname() {
|
||||
this.form.controls.hostname.setValue(generateHostname())
|
||||
get derivedHostname(): string {
|
||||
return normalizeHostname(this.form.controls.name.value || '')
|
||||
}
|
||||
|
||||
async skip() {
|
||||
@@ -217,14 +198,15 @@ export default class PasswordPage {
|
||||
|
||||
private async executeSetup(password: string | null) {
|
||||
const loader = this.loader.open('Starting setup').subscribe()
|
||||
const hostname = this.form.controls.hostname.value || generateHostname()
|
||||
const name = this.form.controls.name.value || ''
|
||||
const hostname = normalizeHostname(name)
|
||||
|
||||
try {
|
||||
if (this.stateService.setupType === 'attach') {
|
||||
await this.stateService.attachDrive(password, hostname)
|
||||
await this.stateService.attachDrive(password)
|
||||
} else {
|
||||
// fresh, restore, or transfer - all use execute
|
||||
await this.stateService.executeSetup(password, hostname)
|
||||
await this.stateService.executeSetup(password, name, hostname)
|
||||
}
|
||||
|
||||
await this.router.navigate(['/loading'])
|
||||
|
||||
@@ -48,11 +48,10 @@ export class StateService {
|
||||
/**
|
||||
* Called for attach flow (existing data drive)
|
||||
*/
|
||||
async attachDrive(password: string | null, hostname: string): Promise<void> {
|
||||
async attachDrive(password: string | null): Promise<void> {
|
||||
await this.api.attach({
|
||||
guid: this.dataDriveGuid,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
hostname,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +59,11 @@ export class StateService {
|
||||
* Called for fresh, restore, and transfer flows
|
||||
* Password is required for fresh, optional for restore/transfer
|
||||
*/
|
||||
async executeSetup(password: string | null, hostname: string): Promise<void> {
|
||||
async executeSetup(
|
||||
password: string | null,
|
||||
name: string,
|
||||
hostname: string,
|
||||
): Promise<void> {
|
||||
let recoverySource: T.RecoverySource<T.EncryptedWire> | null = null
|
||||
|
||||
if (this.recoverySource) {
|
||||
@@ -81,6 +84,7 @@ export class StateService {
|
||||
guid: this.dataDriveGuid,
|
||||
// @ts-expect-error TODO: backend should make password optional for restore/transfer
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
name,
|
||||
hostname,
|
||||
recoverySource,
|
||||
})
|
||||
|
||||
@@ -691,12 +691,9 @@ export default {
|
||||
755: 'Schnittstelle(n)',
|
||||
756: 'Keine Portweiterleitungsregeln',
|
||||
757: 'Portweiterleitungsregeln am Gateway erforderlich',
|
||||
758: 'Server-Hostname',
|
||||
760: 'Dieser Wert wird als Hostname Ihres Servers und mDNS-Adresse im LAN verwendet. Nur Kleinbuchstaben, Zahlen und Bindestriche sind erlaubt.',
|
||||
761: 'Zufällig generieren',
|
||||
762: 'Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt',
|
||||
763: 'Sie sind derzeit über Ihre .local-Adresse verbunden. Das Ändern des Hostnamens erfordert einen Wechsel zur neuen .local-Adresse.',
|
||||
764: 'Hostname geändert',
|
||||
765: 'Neue Adresse öffnen',
|
||||
766: 'Ihr Server ist jetzt erreichbar unter',
|
||||
767: 'Servername',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -691,12 +691,9 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Interface(s)': 755,
|
||||
'No port forwarding rules': 756,
|
||||
'Port forwarding rules required on gateway': 757,
|
||||
'Server Hostname': 758,
|
||||
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.': 760,
|
||||
'Randomize': 761,
|
||||
'Only lowercase letters, numbers, and hyphens allowed': 762,
|
||||
'You are currently connected via your .local address. Changing the hostname will require you to switch to the new .local address.': 763,
|
||||
'Hostname Changed': 764,
|
||||
'Open new address': 765,
|
||||
'Your server is now reachable at': 766,
|
||||
'Server Name': 767,
|
||||
}
|
||||
|
||||
@@ -691,12 +691,9 @@ export default {
|
||||
755: 'Interfaz/Interfaces',
|
||||
756: 'Sin reglas de redirección de puertos',
|
||||
757: 'Reglas de redirección de puertos requeridas en la puerta de enlace',
|
||||
758: 'Nombre de host del servidor',
|
||||
760: 'Este valor se usará como el nombre de host de su servidor y la dirección mDNS en la LAN. Solo se permiten letras minúsculas, números y guiones.',
|
||||
761: 'Aleatorizar',
|
||||
762: 'Solo se permiten letras minúsculas, números y guiones',
|
||||
763: 'Actualmente está conectado a través de su dirección .local. Cambiar el nombre de host requerirá que cambie a la nueva dirección .local.',
|
||||
764: 'Nombre de host cambiado',
|
||||
765: 'Abrir nueva dirección',
|
||||
766: 'Su servidor ahora es accesible en',
|
||||
767: 'Nombre del servidor',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -691,12 +691,9 @@ export default {
|
||||
755: 'Interface(s)',
|
||||
756: 'Aucune règle de redirection de port',
|
||||
757: 'Règles de redirection de ports requises sur la passerelle',
|
||||
758: "Nom d'hôte du serveur",
|
||||
760: "Cette valeur sera utilisée comme nom d'hôte de votre serveur et adresse mDNS sur le LAN. Seules les lettres minuscules, les chiffres et les tirets sont autorisés.",
|
||||
761: 'Générer aléatoirement',
|
||||
762: 'Seules les lettres minuscules, les chiffres et les tirets sont autorisés',
|
||||
763: "Vous êtes actuellement connecté via votre adresse .local. Changer le nom d'hôte nécessitera de passer à la nouvelle adresse .local.",
|
||||
764: "Nom d'hôte modifié",
|
||||
765: 'Ouvrir la nouvelle adresse',
|
||||
766: 'Votre serveur est maintenant accessible à',
|
||||
767: 'Nom du serveur',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -691,12 +691,9 @@ export default {
|
||||
755: 'Interfejs(y)',
|
||||
756: 'Brak reguł przekierowania portów',
|
||||
757: 'Reguły przekierowania portów wymagane na bramce',
|
||||
758: 'Nazwa hosta serwera',
|
||||
760: 'Ta wartość będzie używana jako nazwa hosta serwera i adres mDNS w sieci LAN. Dozwolone są tylko małe litery, cyfry i myślniki.',
|
||||
761: 'Losuj',
|
||||
762: 'Dozwolone są tylko małe litery, cyfry i myślniki',
|
||||
763: 'Jesteś obecnie połączony przez adres .local. Zmiana nazwy hosta będzie wymagać przełączenia na nowy adres .local.',
|
||||
764: 'Nazwa hosta zmieniona',
|
||||
765: 'Otwórz nowy adres',
|
||||
766: 'Twój serwer jest teraz dostępny pod adresem',
|
||||
767: 'Nazwa serwera',
|
||||
} satisfies i18n
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@ export class PortalComponent {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
||||
readonly name = toSignal(this.patch.watch$('serverInfo', 'name'))
|
||||
readonly update = toSignal(inject(OSService).updating$)
|
||||
readonly bar = signal(true)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import {
|
||||
DialogService,
|
||||
@@ -30,9 +29,7 @@ import { TuiAnimated } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
tuiFadeIn,
|
||||
TuiIcon,
|
||||
tuiScaleIn,
|
||||
TuiTextfield,
|
||||
TuiTitle,
|
||||
} from '@taiga-ui/core'
|
||||
@@ -46,7 +43,7 @@ import {
|
||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { OSService } from 'src/app/services/os.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
@@ -54,6 +51,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { UPDATE } from './update.component'
|
||||
import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
import { ServerNameDialog } from './server-name.dialog'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -98,25 +96,16 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.server" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Server Hostname' | i18n }}</strong>
|
||||
<strong>{{ 'Server Name' | i18n }}</strong>
|
||||
<span tuiSubtitle>
|
||||
{{ server.hostname }}
|
||||
{{ server.name }}
|
||||
</span>
|
||||
<span tuiSubtitle>{{ server.hostname }}.local</span>
|
||||
</span>
|
||||
<button tuiButton (click)="onHostname()">
|
||||
<button tuiButton (click)="onName()">
|
||||
{{ 'Change' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.app-window" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Browser tab title' | i18n }}</strong>
|
||||
<span tuiSubtitle>
|
||||
{{ 'Customize the name appearing in your browser tab' | i18n }}
|
||||
</span>
|
||||
</span>
|
||||
<button tuiButton (click)="onTitle()">{{ 'Change' | i18n }}</button>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.languages" />
|
||||
<span tuiTitle>
|
||||
@@ -276,7 +265,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
],
|
||||
})
|
||||
export default class SystemGeneralComponent {
|
||||
private readonly title = inject(Title)
|
||||
private readonly dialogs = inject(TuiResponsiveDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
@@ -290,7 +278,6 @@ export default class SystemGeneralComponent {
|
||||
count = 0
|
||||
|
||||
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
||||
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
||||
readonly score = toSignal(this.patch.watch$('ui', 'snakeHighScore'))
|
||||
readonly os = inject(OSService)
|
||||
readonly i18nService = inject(i18nService)
|
||||
@@ -360,33 +347,35 @@ export default class SystemGeneralComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onHostname() {
|
||||
const sub = this.dialog
|
||||
.openPrompt<string>({
|
||||
label: 'Server Hostname',
|
||||
data: {
|
||||
label: 'Hostname' as i18nKey,
|
||||
message:
|
||||
'This value will be used as your server hostname and mDNS address on the LAN. Only lowercase letters, numbers, and hyphens are allowed.',
|
||||
placeholder: 'start9' as i18nKey,
|
||||
required: true,
|
||||
buttonText: 'Save',
|
||||
initialValue: this.server()?.hostname || '',
|
||||
pattern: '^[a-z0-9][a-z0-9-]*$',
|
||||
patternError:
|
||||
'Hostname must contain only lowercase letters, numbers, and hyphens, and cannot start with a hyphen.',
|
||||
onName() {
|
||||
const server = this.server()
|
||||
if (!server) return
|
||||
|
||||
this.dialog
|
||||
.openComponent<{ name: string; hostname: string } | null>(
|
||||
new PolymorpheusComponent(ServerNameDialog, this.injector),
|
||||
{
|
||||
label: 'Server Name',
|
||||
size: 's',
|
||||
data: { initialName: server.name },
|
||||
},
|
||||
})
|
||||
.subscribe(hostname => {
|
||||
)
|
||||
.pipe(
|
||||
filter(
|
||||
(result): result is { name: string; hostname: string } =>
|
||||
result !== null,
|
||||
),
|
||||
)
|
||||
.subscribe(result => {
|
||||
if (this.win.location.hostname.endsWith('.local')) {
|
||||
this.confirmHostnameChange(hostname, sub)
|
||||
this.confirmNameChange(result)
|
||||
} else {
|
||||
this.saveHostname(hostname, sub)
|
||||
this.saveName(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private confirmHostnameChange(hostname: string, promptSub: Subscription) {
|
||||
private confirmNameChange(result: { name: string; hostname: string }) {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Warning',
|
||||
@@ -398,18 +387,17 @@ export default class SystemGeneralComponent {
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.saveHostname(hostname, promptSub, true))
|
||||
.subscribe(() => this.saveName(result, true))
|
||||
}
|
||||
|
||||
private async saveHostname(
|
||||
hostname: string,
|
||||
promptSub: Subscription,
|
||||
private async saveName(
|
||||
{ name, hostname }: { name: string; hostname: string },
|
||||
wasLocal = false,
|
||||
) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setHostname({ hostname })
|
||||
await this.api.setHostname({ name, hostname })
|
||||
|
||||
if (wasLocal) {
|
||||
const { protocol, port } = this.win.location
|
||||
@@ -432,40 +420,9 @@ export default class SystemGeneralComponent {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
promptSub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
onTitle() {
|
||||
const sub = this.dialog
|
||||
.openPrompt<string>({
|
||||
label: 'Browser tab title',
|
||||
data: {
|
||||
label: 'Device Name',
|
||||
message:
|
||||
'This value will be displayed as the title of your browser tab.',
|
||||
placeholder: 'StartOS' as i18nKey,
|
||||
required: false,
|
||||
buttonText: 'Save',
|
||||
initialValue: this.name(),
|
||||
},
|
||||
})
|
||||
.subscribe(async name => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
const title = `${name || 'StartOS'} — ${this.i18n.transform('System')}`
|
||||
|
||||
try {
|
||||
await this.api.setDbValue(['name'], name || null)
|
||||
this.title.setTitle(title)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
sub.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async onRepair() {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe, normalizeHostname } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiTextfield } from '@taiga-ui/core'
|
||||
import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-textfield>
|
||||
<label tuiLabel>{{ 'Server Name' | i18n }}</label>
|
||||
<input tuiTextfield [(ngModel)]="name" />
|
||||
</tui-textfield>
|
||||
@if (name.trim()) {
|
||||
<p class="hostname-preview">{{ normalizeHostname(name) }}.local</p>
|
||||
}
|
||||
<footer>
|
||||
<button tuiButton appearance="secondary" (click)="cancel()">
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button tuiButton [disabled]="!name.trim()" (click)="confirm()">
|
||||
{{ 'Save' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
styles: `
|
||||
.hostname-preview {
|
||||
color: var(--tui-text-secondary);
|
||||
font: var(--tui-font-text-s);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`,
|
||||
imports: [FormsModule, TuiButton, TuiTextfield, i18nPipe],
|
||||
})
|
||||
export class ServerNameDialog {
|
||||
private readonly context =
|
||||
injectContext<
|
||||
TuiDialogContext<
|
||||
{ name: string; hostname: string } | null,
|
||||
{ initialName: string }
|
||||
>
|
||||
>()
|
||||
|
||||
name = this.context.data.initialName
|
||||
readonly normalizeHostname = normalizeHostname
|
||||
|
||||
cancel() {
|
||||
this.context.completeWith(null)
|
||||
}
|
||||
|
||||
confirm() {
|
||||
const name = this.name.trim()
|
||||
this.context.completeWith({
|
||||
name,
|
||||
hostname: normalizeHostname(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,7 @@ import UpdatesComponent from './updates.component'
|
||||
|
||||
&:last-child {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&[colspan]:only-child {
|
||||
|
||||
@@ -121,7 +121,7 @@ export abstract class ApiService {
|
||||
|
||||
abstract toggleKiosk(enable: boolean): Promise<null>
|
||||
|
||||
abstract setHostname(params: { hostname: string }): Promise<null>
|
||||
abstract setHostname(params: T.SetServerHostnameParams): Promise<null>
|
||||
|
||||
abstract setKeyboard(params: FullKeyboard): Promise<null>
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setHostname(params: { hostname: string }): Promise<null> {
|
||||
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
||||
return this.rpcRequest({ method: 'server.set-hostname', params })
|
||||
}
|
||||
|
||||
|
||||
@@ -447,10 +447,15 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async setHostname(params: { hostname: string }): Promise<null> {
|
||||
async setHostname(params: T.SetServerHostnameParams): Promise<null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/name',
|
||||
value: params.name,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/hostname',
|
||||
|
||||
@@ -232,6 +232,7 @@ export const mockPatchData: DataModel = {
|
||||
shuttingDown: false,
|
||||
backupProgress: null,
|
||||
},
|
||||
name: 'Random Words',
|
||||
hostname: 'random-words',
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
caFingerprint: '63:2B:11:99:44:40:17:DF:37:FC:C3:DF:0F:3D:15',
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function titleResolver({
|
||||
let route = inject(i18nPipe).transform(data['title'])
|
||||
|
||||
const patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
const title = await firstValueFrom(patch.watch$('ui', 'name'))
|
||||
const title = await firstValueFrom(patch.watch$('serverInfo', 'name'))
|
||||
const id = params['pkgId']
|
||||
|
||||
if (id) {
|
||||
|
||||
Reference in New Issue
Block a user