mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Revamp URL form (#12060)
This commit is contained in:
parent
ab5df0fe6e
commit
1e929ae78a
4
src/common/string/is_ip_address.ts
Normal file
4
src/common/string/is_ip_address.ts
Normal file
@ -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);
|
@ -6,6 +6,7 @@ import { AutomationConfig } from "./automation";
|
|||||||
interface CloudStatusNotLoggedIn {
|
interface CloudStatusNotLoggedIn {
|
||||||
logged_in: false;
|
logged_in: false;
|
||||||
cloud: "disconnected" | "connecting" | "connected";
|
cloud: "disconnected" | "connecting" | "connected";
|
||||||
|
http_use_ssl: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoogleEntityConfig {
|
export interface GoogleEntityConfig {
|
||||||
@ -59,6 +60,7 @@ export interface CloudStatusLoggedIn {
|
|||||||
remote_connected: boolean;
|
remote_connected: boolean;
|
||||||
remote_certificate: undefined | CertificateInformation;
|
remote_certificate: undefined | CertificateInformation;
|
||||||
http_use_ssl: boolean;
|
http_use_ssl: boolean;
|
||||||
|
active_subscription: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
|
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@polymer/paper-input/paper-input";
|
import {
|
||||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
css,
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import "../../../components/ha-card";
|
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 { saveCoreConfig } from "../../../data/core";
|
||||||
import type { PolymerChangedEvent } from "../../../polymer-types";
|
import type { PolymerChangedEvent } from "../../../polymer-types";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||||
|
|
||||||
@customElement("ha-config-url-form")
|
@customElement("ha-config-url-form")
|
||||||
class ConfigUrlForm extends LitElement {
|
class ConfigUrlForm extends LitElement {
|
||||||
@ -20,18 +33,48 @@ class ConfigUrlForm extends LitElement {
|
|||||||
|
|
||||||
@state() private _internal_url?: string;
|
@state() private _internal_url?: string;
|
||||||
|
|
||||||
|
@state() private _cloudStatus?: CloudStatus | null;
|
||||||
|
|
||||||
|
@state() private _showCustomExternalUrl = false;
|
||||||
|
|
||||||
|
@state() private _showCustomInternalUrl = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const canEdit = ["storage", "default"].includes(
|
const canEdit = ["storage", "default"].includes(
|
||||||
this.hass.config.config_source
|
this.hass.config.config_source
|
||||||
);
|
);
|
||||||
const disabled = this._working || !canEdit;
|
const disabled = this._working || !canEdit;
|
||||||
|
|
||||||
if (!this.hass.userData?.showAdvanced) {
|
if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) {
|
||||||
return html``;
|
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`
|
return html`
|
||||||
<ha-card>
|
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${!canEdit
|
${!canEdit
|
||||||
? html`
|
? html`
|
||||||
@ -43,46 +86,147 @@ class ConfigUrlForm extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
${this.hass.localize("ui.panel.config.url.description")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${hasCloud
|
||||||
|
? html`
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.core.section.core.core_config.external_url"
|
"ui.panel.config.url.external_url_label"
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<ha-formfield
|
||||||
<paper-input
|
|
||||||
class="flex"
|
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.core.section.core.core_config.external_url"
|
"ui.panel.config.url.external_use_ha_cloud"
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
.disabled=${disabled}
|
||||||
|
.checked=${externalUrl === null}
|
||||||
|
@change=${this._toggleCloud}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${!this._showCustomExternalUrl
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<div class="row">
|
||||||
|
<div class="flex">
|
||||||
|
${hasCloud
|
||||||
|
? ""
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.url.external_url_label"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ha-textfield
|
||||||
|
class="flex"
|
||||||
name="external_url"
|
name="external_url"
|
||||||
type="url"
|
type="url"
|
||||||
.disabled=${disabled}
|
.disabled=${disabled}
|
||||||
.value=${this._externalUrlValue}
|
.value=${externalUrl || ""}
|
||||||
@value-changed=${this._handleChange}
|
@change=${this._handleChange}
|
||||||
|
placeholder="https://example.duckdns.org:8123"
|
||||||
>
|
>
|
||||||
</paper-input>
|
</ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
|
`}
|
||||||
|
${hasCloud || !isComponentLoaded(this.hass, "cloud")
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<div class="row">
|
||||||
|
<div class="flex"></div>
|
||||||
|
<a href="/config/cloud"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.url.external_get_ha_cloud"
|
||||||
|
)}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
${!this._showCustomExternalUrl && hasCloud
|
||||||
|
? html`
|
||||||
|
${remoteEnabled
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.url.ha_cloud_remote_not_enabled"
|
||||||
|
)}
|
||||||
|
<a href="/config/cloud" slot="action"
|
||||||
|
><mwc-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.url.enable_remote"
|
||||||
|
)}
|
||||||
|
></mwc-button
|
||||||
|
></a>
|
||||||
|
</ha-alert>
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
${this.hass.localize(
|
${this.hass.localize("ui.panel.config.url.internal_url_label")}
|
||||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<paper-input
|
|
||||||
class="flex"
|
<ha-formfield
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
"ui.panel.config.url.internal_url_automatic"
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${internalUrl === null}
|
||||||
|
@change=${this._toggleInternalAutomatic}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-formfield>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${!this._showCustomInternalUrl
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<div class="row">
|
||||||
|
<div class="flex"></div>
|
||||||
|
<ha-textfield
|
||||||
|
class="flex"
|
||||||
name="internal_url"
|
name="internal_url"
|
||||||
type="url"
|
type="url"
|
||||||
|
placeholder="http://<some IP address>:8123"
|
||||||
.disabled=${disabled}
|
.disabled=${disabled}
|
||||||
.value=${this._internalUrlValue}
|
.value=${internalUrl || ""}
|
||||||
@value-changed=${this._handleChange}
|
@change=${this._handleChange}
|
||||||
>
|
>
|
||||||
</paper-input>
|
</ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
|
`}
|
||||||
|
${
|
||||||
|
// 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`
|
||||||
|
<ha-alert
|
||||||
|
.alertType=${this._showCustomInternalUrl
|
||||||
|
? "info"
|
||||||
|
: "warning"}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.url.intenral_url_https_error_title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.url.internal_url_https_error_description"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||||
@ -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() {
|
private get _internalUrlValue() {
|
||||||
return this._internal_url !== undefined
|
return this._internal_url !== undefined
|
||||||
? this._internal_url
|
? this._internal_url
|
||||||
@ -107,9 +269,17 @@ class ConfigUrlForm extends LitElement {
|
|||||||
: this.hass.config.external_url;
|
: 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<string>) {
|
private _handleChange(ev: PolymerChangedEvent<string>) {
|
||||||
const target = ev.currentTarget as PaperInputElement;
|
const target = ev.currentTarget as HaTextField;
|
||||||
this[`_${target.name}`] = target.value;
|
this[`_${target.name}`] = target.value || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _save() {
|
private async _save() {
|
||||||
@ -117,8 +287,12 @@ class ConfigUrlForm extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
try {
|
try {
|
||||||
await saveCoreConfig(this.hass, {
|
await saveCoreConfig(this.hass, {
|
||||||
external_url: this._external_url || null,
|
external_url: this._showCustomExternalUrl
|
||||||
internal_url: this._internal_url || null,
|
? this._external_url || null
|
||||||
|
: null,
|
||||||
|
internal_url: this._showCustomInternalUrl
|
||||||
|
? this._internal_url || null
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = err.message || err;
|
this._error = err.message || err;
|
||||||
@ -129,11 +303,15 @@ class ConfigUrlForm extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
|
.description {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 0 -8px;
|
margin: 0 -8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
@ -154,6 +332,10 @@ class ConfigUrlForm extends LitElement {
|
|||||||
.card-actions {
|
.card-actions {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1355,13 +1355,24 @@
|
|||||||
"metric_example": "Celsius, kilograms",
|
"metric_example": "Celsius, kilograms",
|
||||||
"find_currency_value": "Find your value",
|
"find_currency_value": "Find your value",
|
||||||
"save_button": "Save",
|
"save_button": "Save",
|
||||||
"external_url": "External URL",
|
|
||||||
"internal_url": "Internal URL",
|
|
||||||
"currency": "Currency"
|
"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": {
|
"info": {
|
||||||
"caption": "Info",
|
"caption": "Info",
|
||||||
"copy_menu": "Copy menu",
|
"copy_menu": "Copy menu",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user