mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-12 20:26:54 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94bc9d8567 | |||
| 18268c67dc | |||
| 75a2331c87 | |||
| 40fa954542 |
@@ -78,7 +78,7 @@ class HaInputMulti extends LitElement {
|
||||
<div class="items">
|
||||
${repeat(
|
||||
this._items,
|
||||
(item, index) => `${item}-${index}`,
|
||||
(_item, index) => index,
|
||||
(item, index) => {
|
||||
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface HttpConfig {
|
||||
server_host?: string[];
|
||||
server_port?: number;
|
||||
ssl_certificate?: string;
|
||||
ssl_peer_certificate?: string;
|
||||
ssl_key?: string;
|
||||
cors_allowed_origins?: string[];
|
||||
use_x_forwarded_for?: boolean;
|
||||
trusted_proxies?: string[];
|
||||
use_x_frame_options?: boolean;
|
||||
ip_ban_enabled?: boolean;
|
||||
login_attempts_threshold?: number;
|
||||
ssl_profile?: "modern" | "intermediate";
|
||||
}
|
||||
|
||||
interface HttpConfigResponse {
|
||||
config: HttpConfig;
|
||||
}
|
||||
|
||||
export const fetchHttpConfig = (hass: HomeAssistant) =>
|
||||
hass.callWS<HttpConfigResponse>({ type: "http/config/get" });
|
||||
|
||||
export const saveHttpConfig = (hass: HomeAssistant, config: HttpConfig) =>
|
||||
hass.callWS<HttpConfigResponse>({
|
||||
type: "http/config/update",
|
||||
config,
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
|
||||
import type { HttpConfig } from "../../../data/http";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
const SCHEMA = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "server_port",
|
||||
required: true,
|
||||
selector: { number: { min: 1, max: 65535, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "server_host",
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
{
|
||||
name: "ssl_certificate",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "ssl_key",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "ssl_peer_certificate",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "ssl_profile",
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{
|
||||
value: "modern",
|
||||
label: localize(
|
||||
"ui.panel.config.network.http.ssl_profile_modern"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "intermediate",
|
||||
label: localize(
|
||||
"ui.panel.config.network.http.ssl_profile_intermediate"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cors_allowed_origins",
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
{
|
||||
name: "use_x_forwarded_for",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "trusted_proxies",
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
{
|
||||
name: "use_x_frame_options",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "ip_ban_enabled",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "login_attempts_threshold",
|
||||
required: true,
|
||||
selector: { number: { min: -1, max: 1000, mode: "box" } },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
@customElement("ha-config-http-form")
|
||||
class HaConfigHttpForm extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: HttpConfig;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _fieldErrors: Record<string, string> = {};
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state() private _saved = false;
|
||||
|
||||
protected override firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchConfig();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config && !this._error) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = SCHEMA(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.network.http.caption")}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
${this.hass.localize("ui.panel.config.network.http.description")}
|
||||
</p>
|
||||
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${this._saved
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="info"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.network.http.restart_required_title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.restart_required_description"
|
||||
)}
|
||||
<ha-button slot="action" @click=${this._restart}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.restart"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this._config
|
||||
? html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.error=${this._fieldErrors}
|
||||
.disabled=${this._saving}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._save} .disabled=${this._saving}>
|
||||
${this.hass.localize("ui.panel.config.network.http.save")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchConfig(): Promise<void> {
|
||||
try {
|
||||
const result = await fetchHttpConfig(this.hass);
|
||||
this._config = result.config;
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.panel.config.network.http.fields.${schema.name}`);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.panel.config.network.http.helpers.${schema.name}`) ||
|
||||
"";
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._config = ev.detail.value;
|
||||
this._saved = false;
|
||||
this._error = undefined;
|
||||
this._fieldErrors = {};
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const form = this.renderRoot.querySelector("ha-form");
|
||||
if (form && !form.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
this._error = undefined;
|
||||
this._fieldErrors = {};
|
||||
try {
|
||||
const result = await saveHttpConfig(this.hass, this._config);
|
||||
this._config = result.config;
|
||||
this._saved = true;
|
||||
} catch (err: any) {
|
||||
// voluptuous formats errors as "<message> @ data['<field>']".
|
||||
// If a field is identified, mark it inline; otherwise show a card-level
|
||||
// alert.
|
||||
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
|
||||
if (fieldMatch) {
|
||||
this._fieldErrors = { [fieldMatch[1]]: err.message };
|
||||
} else {
|
||||
this._error = err.message;
|
||||
}
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
await this.updateComplete;
|
||||
const haForm = this.renderRoot.querySelector("ha-form");
|
||||
await haForm?.updateComplete;
|
||||
// Inline field errors render inside ha-form's shadow root, so fall back to
|
||||
// it when no top-level alert is present.
|
||||
const target =
|
||||
this.renderRoot.querySelector<HTMLElement>("ha-alert") ??
|
||||
haForm?.shadowRoot?.querySelector<HTMLElement>("ha-alert");
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
|
||||
private _restart(): void {
|
||||
showRestartDialog(this);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.description {
|
||||
margin-top: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-http-form": HaConfigHttpForm;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "./ha-config-http-form";
|
||||
import "./ha-config-network";
|
||||
import "./ha-config-url-form";
|
||||
import "./supervisor-hostname";
|
||||
@@ -40,6 +41,7 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
<supervisor-network .hass=${this.hass}></supervisor-network>`
|
||||
: ""}
|
||||
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
|
||||
<ha-config-http-form .hass=${this.hass}></ha-config-http-form>
|
||||
<ha-config-network .hass=${this.hass}></ha-config-network>
|
||||
${NETWORK_BROWSERS.some((component) =>
|
||||
isComponentLoaded(this.hass.config, component)
|
||||
@@ -88,6 +90,7 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
supervisor-hostname,
|
||||
supervisor-network,
|
||||
ha-config-url-form,
|
||||
ha-config-http-form,
|
||||
ha-config-network,
|
||||
.discovery-card {
|
||||
display: block;
|
||||
|
||||
@@ -8251,6 +8251,44 @@
|
||||
"zeroconf": "Zeroconf browser",
|
||||
"zeroconf_info": "Show services discovered using mDNS. Does not include services unknown to Home Assistant."
|
||||
},
|
||||
"http": {
|
||||
"caption": "HTTP server",
|
||||
"description": "Configure how Home Assistant serves its web interface. Changes take effect after a restart.",
|
||||
"save": "Save",
|
||||
"restart": "Restart",
|
||||
"restart_required_title": "Restart required",
|
||||
"restart_required_description": "Restart Home Assistant to apply the new HTTP settings.",
|
||||
"ssl_profile_modern": "Modern",
|
||||
"ssl_profile_intermediate": "Intermediate",
|
||||
"fields": {
|
||||
"server_port": "Server port",
|
||||
"server_host": "Listen addresses",
|
||||
"ssl_certificate": "SSL certificate path",
|
||||
"ssl_key": "SSL key path",
|
||||
"ssl_peer_certificate": "SSL peer certificate path",
|
||||
"ssl_profile": "SSL profile",
|
||||
"cors_allowed_origins": "CORS allowed origins",
|
||||
"use_x_forwarded_for": "Trust X-Forwarded-For",
|
||||
"trusted_proxies": "Trusted proxies",
|
||||
"use_x_frame_options": "Send X-Frame-Options",
|
||||
"ip_ban_enabled": "Enable IP banning",
|
||||
"login_attempts_threshold": "Login attempts before ban"
|
||||
},
|
||||
"helpers": {
|
||||
"server_port": "The port Home Assistant listens on. Default is 8123.",
|
||||
"server_host": "IP addresses to bind to. Leave empty to listen on all interfaces.",
|
||||
"ssl_certificate": "Absolute path to your TLS certificate (for example, /ssl/fullchain.pem).",
|
||||
"ssl_key": "Absolute path to your TLS private key (for example, /ssl/privkey.pem).",
|
||||
"ssl_peer_certificate": "Absolute path to a client certificate Home Assistant should require for secure connections.",
|
||||
"ssl_profile": "Mozilla SSL profile. Use 'Intermediate' only if integrations have SSL handshake issues.",
|
||||
"cors_allowed_origins": "Origins that may make cross-origin requests. Include the scheme, for example https://example.com.",
|
||||
"use_x_forwarded_for": "Trust the X-Forwarded-For header behind a reverse proxy. Requires the trusted proxies list below.",
|
||||
"trusted_proxies": "Reverse-proxy IP addresses or CIDR networks allowed to set X-Forwarded-For. Use a network address, not a host.",
|
||||
"use_x_frame_options": "Send the X-Frame-Options header to help prevent clickjacking.",
|
||||
"ip_ban_enabled": "Automatically ban IP addresses after repeated failed logins.",
|
||||
"login_attempts_threshold": "Failed login attempts before an IP is banned. Set to -1 to disable automatic bans."
|
||||
}
|
||||
},
|
||||
"network_adapter": "Network adapter",
|
||||
"network_adapter_info": "Configure which network adapters integrations will use. A restart is required for these settings to apply.",
|
||||
"ip_information": "IP information",
|
||||
|
||||
Reference in New Issue
Block a user