From 1e929ae78a098a7a188f4eb949264be342932da7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 14:14:25 -0700 Subject: [PATCH] Revamp URL form (#12060) --- src/common/string/is_ip_address.ts | 4 + src/data/cloud.ts | 2 + src/panels/config/core/ha-config-url-form.ts | 260 ++++++++++++++++--- src/translations/en.json | 15 +- 4 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 src/common/string/is_ip_address.ts diff --git a/src/common/string/is_ip_address.ts b/src/common/string/is_ip_address.ts new file mode 100644 index 0000000000..8f0277176a --- /dev/null +++ b/src/common/string/is_ip_address.ts @@ -0,0 +1,4 @@ +const regexp = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + +export const isIPAddress = (input: string): boolean => regexp.test(input); diff --git a/src/data/cloud.ts b/src/data/cloud.ts index e8add4f4ef..49a4244a1f 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation"; interface CloudStatusNotLoggedIn { logged_in: false; cloud: "disconnected" | "connecting" | "connected"; + http_use_ssl: boolean; } export interface GoogleEntityConfig { @@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn { remote_connected: boolean; remote_certificate: undefined | CertificateInformation; http_use_ssl: boolean; + active_subscription: boolean; } export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn; diff --git a/src/panels/config/core/ha-config-url-form.ts b/src/panels/config/core/ha-config-url-form.ts index df25744fea..e74546ce7b 100644 --- a/src/panels/config/core/ha-config-url-form.ts +++ b/src/panels/config/core/ha-config-url-form.ts @@ -1,12 +1,25 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; +import "../../../components/ha-switch"; +import "../../../components/ha-alert"; +import "../../../components/ha-formfield"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { CloudStatus, fetchCloudStatus } from "../../../data/cloud"; import { saveCoreConfig } from "../../../data/core"; import type { PolymerChangedEvent } from "../../../polymer-types"; import type { HomeAssistant } from "../../../types"; +import { isIPAddress } from "../../../common/string/is_ip_address"; @customElement("ha-config-url-form") class ConfigUrlForm extends LitElement { @@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement { @state() private _internal_url?: string; + @state() private _cloudStatus?: CloudStatus | null; + + @state() private _showCustomExternalUrl = false; + + @state() private _showCustomInternalUrl = false; + protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); const disabled = this._working || !canEdit; - if (!this.hass.userData?.showAdvanced) { + if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) { return html``; } + const internalUrl = this._internalUrlValue; + const externalUrl = this._externalUrlValue; + let hasCloud: boolean; + let remoteEnabled: boolean; + let httpUseHttps: boolean; + + if (this._cloudStatus === null) { + hasCloud = false; + remoteEnabled = false; + httpUseHttps = false; + } else { + httpUseHttps = this._cloudStatus.http_use_ssl; + + if (this._cloudStatus.logged_in) { + hasCloud = true; + remoteEnabled = + this._cloudStatus.active_subscription && + this._cloudStatus.prefs.remote_enabled; + } else { + hasCloud = false; + remoteEnabled = false; + } + } + return html` - +
${!canEdit ? html` @@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement { ` : ""} ${this._error ? html`
${this._error}
` : ""} -
-
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.external_url" - )} -
- - +
+ ${this.hass.localize("ui.panel.config.url.description")}
+ ${hasCloud + ? html` +
+
+ ${this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + + +
+ ` + : ""} + ${!this._showCustomExternalUrl + ? "" + : html` +
+
+ ${hasCloud + ? "" + : this.hass.localize( + "ui.panel.config.url.external_url_label" + )} +
+ + +
+ `} + ${hasCloud || !isComponentLoaded(this.hass, "cloud") + ? "" + : html` + + `} + ${!this._showCustomExternalUrl && hasCloud + ? html` + ${remoteEnabled + ? "" + : html` + + ${this.hass.localize( + "ui.panel.config.url.ha_cloud_remote_not_enabled" + )} + + + `} + ` + : ""} +
- ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.internal_url" - )} + ${this.hass.localize("ui.panel.config.url.internal_url_label")}
- - + +
+ + ${!this._showCustomInternalUrl + ? "" + : html` +
+
+ + +
+ `} + ${ + // If the user has configured a cert, show an error if + httpUseHttps && // there is no internal url configured + (!internalUrl || + // the internal url does not start with https + !internalUrl.startsWith("https://") || + // the internal url points at an IP address + isIPAddress(new URL(internalUrl).hostname)) + ? html` + + ${this.hass.localize( + "ui.panel.config.url.internal_url_https_error_description" + )} + + ` + : "" + }
@@ -95,6 +239,24 @@ class ConfigUrlForm extends LitElement { `; } + protected override firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + this._showCustomInternalUrl = this._internalUrlValue !== null; + + if (isComponentLoaded(this.hass, "cloud")) { + fetchCloudStatus(this.hass).then((cloudStatus) => { + if (cloudStatus.logged_in) { + this._cloudStatus = cloudStatus; + this._showCustomExternalUrl = this._externalUrlValue !== null; + } + }); + } else { + this._cloudStatus = null; + this._showCustomExternalUrl = true; + } + } + private get _internalUrlValue() { return this._internal_url !== undefined ? this._internal_url @@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement { : this.hass.config.external_url; } + private _toggleCloud(ev) { + this._showCustomExternalUrl = !ev.currentTarget.checked; + } + + private _toggleInternalAutomatic(ev) { + this._showCustomInternalUrl = !ev.currentTarget.checked; + } + private _handleChange(ev: PolymerChangedEvent) { - const target = ev.currentTarget as PaperInputElement; - this[`_${target.name}`] = target.value; + const target = ev.currentTarget as HaTextField; + this[`_${target.name}`] = target.value || null; } private async _save() { @@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement { this._error = undefined; try { await saveCoreConfig(this.hass, { - external_url: this._external_url || null, - internal_url: this._internal_url || null, + external_url: this._showCustomExternalUrl + ? this._external_url || null + : null, + internal_url: this._showCustomInternalUrl + ? this._internal_url || null + : null, }); } catch (err: any) { this._error = err.message || err; @@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement { static get styles(): CSSResultGroup { return css` + .description { + margin-bottom: 1em; + } .row { display: flex; flex-direction: row; margin: 0 -8px; align-items: center; + padding: 8px 0; } .secondary { @@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement { .card-actions { text-align: right; } + + a { + color: var(--primary-color); + } `; } } diff --git a/src/translations/en.json b/src/translations/en.json index 6880894b7f..45661e7f8e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1355,13 +1355,24 @@ "metric_example": "Celsius, kilograms", "find_currency_value": "Find your value", "save_button": "Save", - "external_url": "External URL", - "internal_url": "Internal URL", "currency": "Currency" } } } }, + "url": { + "caption": "Home Assistant URL", + "description": "Configure what website addresses Home Assistant should share with other devices when they need to fetch data from Home Assistant (eg. to play text-to-speech or other hosted media).", + "internal_url_label": "Local Network", + "external_url_label": "Internet", + "external_use_ha_cloud": "Use Home Assistant Cloud", + "external_get_ha_cloud": "Access from anywhere using Home Assistant Cloud", + "ha_cloud_remote_not_enabled": "Your Home Assistant Cloud remote connection is currently not enabled.", + "enable_remote": "[%key:ui::common::enable%]", + "internal_url_automatic": "Automatic", + "internal_url_https_error_title": "Invalid local network URL", + "internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate." + }, "info": { "caption": "Info", "copy_menu": "Copy menu",