Merge branch 'feat/preferred-port-design' of github.com:Start9Labs/start-os into sdk-comments

This commit is contained in:
Matt Hill
2026-02-24 10:28:00 -07:00
30 changed files with 9420 additions and 244 deletions

View File

@@ -305,21 +305,21 @@ export class PluginAddressesComponent {
if (!group.tableAction || !group.pluginPkgInfo) return
const iface = this.value()
const prefill: Record<string, unknown> = {}
if (!iface) return
if (iface) {
prefill['urlPluginMetadata'] = {
packageId: this.packageId() || null,
hostId: iface.addressInfo.hostId,
interfaceId: iface.id,
internalPort: iface.addressInfo.internalPort,
}
}
const { addressInfo } = iface
this.actionService.present({
pkgInfo: group.pluginPkgInfo,
actionInfo: group.tableAction,
prefill,
prefill: {
urlPluginMetadata: {
packageId: this.packageId() || null,
hostId: addressInfo.hostId,
interfaceId: iface.id,
internalPort: addressInfo.internalPort,
},
},
})
}
@@ -332,26 +332,30 @@ export class PluginAddressesComponent {
if (!group.pluginPkgInfo) return
const iface = this.value()
const prefill: Record<string, unknown> = {}
if (!iface) return
if (iface && address.hostnameInfo.metadata.kind === 'plugin') {
prefill['urlPluginMetadata'] = {
packageId: this.packageId() || null,
hostId: iface.addressInfo.hostId,
interfaceId: iface.id,
internalPort: iface.addressInfo.internalPort,
hostname: address.hostnameInfo.hostname,
port: address.hostnameInfo.port,
ssl: address.hostnameInfo.ssl,
public: address.hostnameInfo.public,
info: address.hostnameInfo.metadata.info,
}
}
const { hostnameInfo } = address
const { addressInfo } = iface
const hostMeta = hostnameInfo.metadata
if (hostMeta.kind !== 'plugin') return
this.actionService.present({
pkgInfo: group.pluginPkgInfo,
actionInfo: { id: actionId, metadata },
prefill,
prefill: {
urlPluginMetadata: {
packageId: this.packageId() || null,
hostId: addressInfo.hostId,
interfaceId: iface.id,
internalPort: addressInfo.internalPort,
hostname: hostnameInfo.hostname,
port: hostnameInfo.port,
ssl: hostnameInfo.ssl,
public: hostnameInfo.public,
info: hostMeta.info,
},
},
})
}
}

View File

@@ -154,20 +154,21 @@ export class ServiceTaskComponent {
}
async handle() {
const task = this.task()
const title = this.title()
const pkg = this.pkg()
const metadata = pkg?.actions[this.task().actionId]
const metadata = pkg?.actions[task.actionId]
if (title && pkg && metadata) {
this.actionService.present({
pkgInfo: {
id: this.task().packageId,
id: task.packageId,
title,
status: getInstalledBaseStatus(pkg.statusInfo),
icon: pkg.icon,
},
actionInfo: { id: this.task().actionId, metadata },
requestInfo: this.task(),
actionInfo: { id: task.actionId, metadata },
prefill: task.input?.value,
})
}
}

View File

@@ -23,7 +23,6 @@ import {
FormComponent,
} from 'src/app/routes/portal/components/form.component'
import { InvalidService } from 'src/app/routes/portal/components/form/containers/control.directive'
import { TaskInfoComponent } from 'src/app/routes/portal/modals/config-dep.component'
import { ActionService } from 'src/app/services/action.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -41,7 +40,6 @@ export type PackageActionData = {
id: string
metadata: T.ActionMetadata
}
requestInfo?: T.Task
prefill?: Record<string, unknown>
}
@@ -63,13 +61,6 @@ export type PackageActionData = {
</tui-notification>
}
@if (requestInfo) {
<task-info
[originalValue]="res.originalValue || {}"
[operations]="res.visibleOperations || []"
/>
}
<app-form
[spec]="res.spec"
[value]="res.originalValue || {}"
@@ -110,14 +101,7 @@ export type PackageActionData = {
}
}
`,
imports: [
TuiNotification,
TuiLoader,
TuiButton,
TaskInfoComponent,
FormComponent,
i18nPipe,
],
imports: [TuiNotification, TuiLoader, TuiButton, FormComponent, i18nPipe],
providers: [InvalidService],
})
export class ActionInputModal {
@@ -132,7 +116,7 @@ export class ActionInputModal {
readonly actionId = this.context.data.actionInfo.id
readonly warning = this.context.data.actionInfo.metadata.warning
readonly pkgInfo = this.context.data.pkgInfo
readonly requestInfo = this.context.data.requestInfo
readonly prefill = this.context.data.prefill
eventId: string | null = null
buttons: ActionButton<any>[] = [
@@ -148,7 +132,7 @@ export class ActionInputModal {
this.api.getActionInput({
packageId: this.pkgInfo.id,
actionId: this.actionId,
prefill: this.context.data.prefill ?? null,
prefill: this.prefill ?? null,
}),
).pipe(
map(res => {
@@ -156,12 +140,12 @@ export class ActionInputModal {
const originalValue = res.value || {}
this.eventId = res.eventId
const operations = this.requestInfo?.input
const operations = this.prefill
? compare(
JSON.parse(JSON.stringify(originalValue)),
utils.deepMerge(
JSON.parse(JSON.stringify(originalValue)),
this.requestInfo.input.value,
this.prefill,
) as object,
)
: null
@@ -170,11 +154,6 @@ export class ActionInputModal {
spec: res.spec,
originalValue,
operations,
visibleOperations:
operations?.filter(op => {
const key = op.path.split('/')[1]
return (res.spec[key!] as any)?.type !== 'hidden'
}) ?? null,
}
}),
catchError(e => {

View File

@@ -24,6 +24,7 @@ import {
LANGUAGE_TO_CODE,
LoadingService,
} from '@start9labs/shared'
import { WA_WINDOW } from '@ng-web-apis/common'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiAnimated } from '@taiga-ui/cdk'
import {
@@ -45,7 +46,7 @@ import {
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { filter } from 'rxjs'
import { filter, Subscription } 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'
@@ -94,6 +95,18 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
}
</button>
</div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.server" />
<span tuiTitle>
<strong>{{ 'Server Hostname' | i18n }}</strong>
<span tuiSubtitle>
{{ server.hostname }}
</span>
</span>
<button tuiButton (click)="onHostname()">
{{ 'Change' | i18n }}
</button>
</div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.app-window" />
<span tuiTitle>
@@ -272,6 +285,7 @@ export default class SystemGeneralComponent {
private readonly dialog = inject(DialogService)
private readonly i18n = inject(i18nPipe)
private readonly injector = inject(INJECTOR)
private readonly win = inject(WA_WINDOW)
count = 0
@@ -346,6 +360,82 @@ 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.',
},
})
.subscribe(hostname => {
if (this.win.location.hostname.endsWith('.local')) {
this.confirmHostnameChange(hostname, sub)
} else {
this.saveHostname(hostname, sub)
}
})
}
private confirmHostnameChange(hostname: string, promptSub: Subscription) {
this.dialog
.openConfirm({
label: 'Warning',
data: {
content:
'You are currently connected via your .local address. Changing the hostname will require you to switch to the new .local address.',
yes: 'Save',
no: 'Cancel',
},
})
.pipe(filter(Boolean))
.subscribe(() => this.saveHostname(hostname, promptSub, true))
}
private async saveHostname(
hostname: string,
promptSub: Subscription,
wasLocal = false,
) {
const loader = this.loader.open('Saving').subscribe()
try {
await this.api.setHostname({ hostname })
if (wasLocal) {
const { protocol, port } = this.win.location
const newUrl = `${protocol}//${hostname}.local${port ? ':' + port : ''}`
this.dialog
.openConfirm({
label: 'Hostname Changed',
data: {
content:
`${this.i18n.transform('Your server is now reachable at')} ${hostname}.local` as i18nKey,
yes: 'Open new address',
no: 'Dismiss',
},
})
.pipe(filter(Boolean))
.subscribe(() => this.win.open(newUrl, '_blank'))
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
promptSub.unsubscribe()
}
}
onTitle() {
const sub = this.dialog
.openPrompt<string>({

View File

@@ -27,7 +27,6 @@ export class ActionService {
private readonly formDialog = inject(FormDialogService)
async present(data: PackageActionData) {
data.prefill = data.prefill ?? data.requestInfo?.input?.value
const { pkgInfo, actionInfo } = data
if (actionInfo.metadata.hasInput) {

View File

@@ -121,6 +121,8 @@ export abstract class ApiService {
abstract toggleKiosk(enable: boolean): Promise<null>
abstract setHostname(params: { hostname: string }): Promise<null>
abstract setKeyboard(params: FullKeyboard): Promise<null>
abstract setLanguage(params: SetLanguageParams): Promise<null>

View File

@@ -255,6 +255,10 @@ export class LiveApiService extends ApiService {
})
}
async setHostname(params: { hostname: string }): Promise<null> {
return this.rpcRequest({ method: 'server.set-hostname', params })
}
async setKeyboard(params: FullKeyboard): Promise<null> {
return this.rpcRequest({ method: 'server.set-keyboard', params })
}

View File

@@ -447,6 +447,21 @@ export class MockApiService extends ApiService {
return null
}
async setHostname(params: { hostname: string }): Promise<null> {
await pauseFor(1000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/serverInfo/hostname',
value: params.hostname,
},
]
this.mockRevision(patch)
return null
}
async setKeyboard(params: FullKeyboard): Promise<null> {
await pauseFor(1000)