mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
implement server name
This commit is contained in:
@@ -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