mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Show local network URL used in Home Assistant URL settings (#22379)
* Show local network URL used in Home Assistant URL settings * full width alert * always display both urls and add copy buttons * remove mask button when editing * fix typo * type fix * update styling based on comments * fix bad copy/paste * Update src/components/ha-settings-row.ts Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> * Update src/panels/config/network/ha-config-url-form.ts Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> * Update src/panels/config/network/ha-config-url-form.ts Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> * PR comment * remove advanced flag * make the value handling logic clearer * move obfuscateUrl to a util function * PR comments --------- Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
parent
42f2341e06
commit
5f6396b187
@ -5,6 +5,8 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaSettingsRow extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public slim = false; // remove padding and min-height
|
||||
|
||||
@property({ type: Boolean, attribute: "three-line" })
|
||||
public threeLine = false;
|
||||
|
||||
@ -112,6 +114,14 @@ export class HaSettingsRow extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:host([slim]),
|
||||
:host([slim]) .content,
|
||||
:host([slim]) ::slotted(ha-switch) {
|
||||
padding: 0;
|
||||
}
|
||||
:host([slim]) .body {
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,12 @@ export interface NetworkConfig {
|
||||
configured_adapters: string[];
|
||||
}
|
||||
|
||||
export interface NetworkUrls {
|
||||
internal: string;
|
||||
external: string;
|
||||
cloud: string;
|
||||
}
|
||||
|
||||
export const getNetworkConfig = (hass: HomeAssistant) =>
|
||||
hass.callWS<NetworkConfig>({
|
||||
type: "network",
|
||||
@ -41,3 +47,8 @@ export const setNetworkConfig = (
|
||||
configured_adapters: configured_adapters,
|
||||
},
|
||||
});
|
||||
|
||||
export const getNetworkUrls = (hass: HomeAssistant) =>
|
||||
hass.callWS<NetworkUrls>({
|
||||
type: "network/url",
|
||||
});
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
import { obfuscateUrl } from "../../../../util/url";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
export class CloudRemotePref extends LitElement {
|
||||
@ -142,7 +143,7 @@ export class CloudRemotePref extends LitElement {
|
||||
<ha-textfield
|
||||
.value=${this._unmaskedUrl
|
||||
? `https://${remote_domain}`
|
||||
: "https://•••••••••••••••••.ui.nabu.casa"}
|
||||
: obfuscateUrl(`https://${remote_domain}`)}
|
||||
readonly
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
@ -153,7 +154,7 @@ export class CloudRemotePref extends LitElement {
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedUrl}
|
||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
||||
@ -165,9 +166,7 @@ export class CloudRemotePref extends LitElement {
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.copy_link"
|
||||
)}
|
||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiContentCopy, mdiEyeOff, mdiEye } from "@mdi/js";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { isIPAddress } from "../../../common/string/is_ip_address";
|
||||
import "../../../components/ha-alert";
|
||||
@ -18,7 +19,12 @@ import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import { getNetworkUrls, type NetworkUrls } from "../../../data/network";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../../types";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import { obfuscateUrl } from "../../../util/url";
|
||||
|
||||
@customElement("ha-config-url-form")
|
||||
class ConfigUrlForm extends LitElement {
|
||||
@ -28,9 +34,11 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
@state() private _working = false;
|
||||
|
||||
@state() private _external_url?: string;
|
||||
@state() private _urls?: NetworkUrls;
|
||||
|
||||
@state() private _internal_url?: string;
|
||||
@state() private _external_url: string = "";
|
||||
|
||||
@state() private _internal_url: string = "";
|
||||
|
||||
@state() private _cloudStatus?: CloudStatus | null;
|
||||
|
||||
@ -38,18 +46,29 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
@state() private _showCustomInternalUrl = false;
|
||||
|
||||
@state() private _unmaskedExternalUrl = false;
|
||||
|
||||
@state() private _unmaskedInternalUrl = false;
|
||||
|
||||
@state() private _cloudChecked = false;
|
||||
|
||||
protected render() {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._working || !canEdit;
|
||||
|
||||
if (!this.hass.userData?.showAdvanced || this._cloudStatus === undefined) {
|
||||
if (this._cloudStatus === undefined || this._urls === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const internalUrl = this._internalUrlValue;
|
||||
const externalUrl = this._externalUrlValue;
|
||||
const internalUrl = this._showCustomInternalUrl
|
||||
? this._internal_url
|
||||
: this._urls?.internal || "";
|
||||
const externalUrl = this._showCustomExternalUrl
|
||||
? this._external_url
|
||||
: (this._cloudChecked ? this._urls?.cloud : this._urls?.external) || "";
|
||||
|
||||
let hasCloud: boolean;
|
||||
let remoteEnabled: boolean;
|
||||
let httpUseHttps: boolean;
|
||||
@ -95,49 +114,61 @@ class ConfigUrlForm extends LitElement {
|
||||
|
||||
${hasCloud
|
||||
? html`
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</h4>
|
||||
<ha-settings-row slim>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.external_url_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.url.external_use_ha_cloud"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${disabled}
|
||||
.checked=${externalUrl === null}
|
||||
@change=${this._toggleCloud}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${disabled}
|
||||
.checked=${this._cloudChecked}
|
||||
@change=${this._toggleCloud}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: ""}
|
||||
${!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"
|
||||
type="url"
|
||||
.disabled=${disabled}
|
||||
.value=${externalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
placeholder="https://example.duckdns.org:8123"
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
name="external_url"
|
||||
type="url"
|
||||
placeholder="https://example.duckdns.org:8123"
|
||||
.value=${this._unmaskedExternalUrl ||
|
||||
(this._showCustomExternalUrl && canEdit)
|
||||
? externalUrl
|
||||
: obfuscateUrl(externalUrl)}
|
||||
@change=${this._handleChange}
|
||||
.disabled=${disabled || !this._showCustomExternalUrl}
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
${!this._showCustomExternalUrl || !canEdit
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedExternalUrl}
|
||||
.path=${this._unmaskedExternalUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button .url=${externalUrl} @click=${this._copyURL}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
${hasCloud || !isComponentLoaded(this.hass, "cloud")
|
||||
? ""
|
||||
: html`
|
||||
@ -180,40 +211,65 @@ class ConfigUrlForm extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize("ui.panel.config.url.internal_url_label")}
|
||||
</div>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
<h4>
|
||||
${this.hass.localize("ui.panel.config.url.internal_url_label")}
|
||||
</h4>
|
||||
<ha-settings-row slim>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.internal_url_automatic"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${internalUrl === null}
|
||||
@change=${this._toggleInternalAutomatic}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.url.internal_url_automatic_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${disabled}
|
||||
.checked=${!this._showCustomInternalUrl}
|
||||
@change=${this._toggleInternalAutomatic}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
|
||||
${!this._showCustomInternalUrl
|
||||
? ""
|
||||
: html`
|
||||
<div class="row">
|
||||
<div class="flex"></div>
|
||||
<ha-textfield
|
||||
class="flex"
|
||||
name="internal_url"
|
||||
type="url"
|
||||
placeholder="http://<some IP address>:8123"
|
||||
.disabled=${disabled}
|
||||
.value=${internalUrl || ""}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
</div>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
name="internal_url"
|
||||
type="url"
|
||||
placeholder=${this.hass.localize(
|
||||
"ui.panel.config.url.internal_url_placeholder"
|
||||
)}
|
||||
.value=${this._unmaskedInternalUrl ||
|
||||
(this._showCustomInternalUrl && canEdit)
|
||||
? internalUrl
|
||||
: obfuscateUrl(internalUrl)}
|
||||
@change=${this._handleChange}
|
||||
.disabled=${disabled || !this._showCustomInternalUrl}
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
${!this._showCustomInternalUrl || !canEdit
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedInternalUrl}
|
||||
.path=${this._unmaskedInternalUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button .url=${internalUrl} @click=${this._copyURL}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
${
|
||||
// If the user has configured a cert, show an error if
|
||||
httpUseHttps && // there is no internal url configured
|
||||
@ -253,46 +309,47 @@ 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) => {
|
||||
this._cloudStatus = cloudStatus;
|
||||
if (cloudStatus.logged_in) {
|
||||
this._showCustomExternalUrl = this._externalUrlValue !== null;
|
||||
} else {
|
||||
this._showCustomExternalUrl = true;
|
||||
}
|
||||
this._showCustomExternalUrl = !(
|
||||
this._cloudStatus.logged_in && !this.hass.config.external_url
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this._cloudStatus = null;
|
||||
this._showCustomExternalUrl = true;
|
||||
}
|
||||
this._fetchUrls();
|
||||
}
|
||||
|
||||
private get _internalUrlValue() {
|
||||
return this._internal_url !== undefined
|
||||
? this._internal_url
|
||||
: this.hass.config.internal_url;
|
||||
private _toggleCloud(ev: Event) {
|
||||
this._cloudChecked = (ev.currentTarget as HaSwitch).checked;
|
||||
this._showCustomExternalUrl = !this._cloudChecked;
|
||||
}
|
||||
|
||||
private get _externalUrlValue() {
|
||||
return this._external_url !== undefined
|
||||
? this._external_url
|
||||
: this.hass.config.external_url;
|
||||
private _toggleInternalAutomatic(ev: Event) {
|
||||
this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private _toggleCloud(ev) {
|
||||
this._showCustomExternalUrl = !ev.currentTarget.checked;
|
||||
private _toggleUnmaskedInternalUrl() {
|
||||
this._unmaskedInternalUrl = !this._unmaskedInternalUrl;
|
||||
}
|
||||
|
||||
private _toggleInternalAutomatic(ev) {
|
||||
this._showCustomInternalUrl = !ev.currentTarget.checked;
|
||||
private _toggleUnmaskedExternalUrl() {
|
||||
this._unmaskedExternalUrl = !this._unmaskedExternalUrl;
|
||||
}
|
||||
|
||||
private async _copyURL(ev) {
|
||||
const url = ev.currentTarget.url;
|
||||
await copyToClipboard(url);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChange(ev: ValueChangedEvent<string>) {
|
||||
const target = ev.currentTarget as HaTextField;
|
||||
this[`_${target.name}`] = target.value || null;
|
||||
this[`_${target.name}`] = target.value || "";
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@ -307,6 +364,7 @@ class ConfigUrlForm extends LitElement {
|
||||
? this._internal_url || null
|
||||
: null,
|
||||
});
|
||||
await this._fetchUrls();
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
} finally {
|
||||
@ -314,6 +372,19 @@ class ConfigUrlForm extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchUrls() {
|
||||
this._urls = await getNetworkUrls(this.hass);
|
||||
this._cloudChecked =
|
||||
this._urls?.cloud === this._urls?.external &&
|
||||
!this.hass.config.external_url;
|
||||
this._showCustomInternalUrl = !!this.hass.config.internal_url;
|
||||
this._showCustomExternalUrl = !(
|
||||
this._cloudStatus?.logged_in && !this.hass.config.external_url
|
||||
);
|
||||
this._internal_url = this._urls?.internal ?? "";
|
||||
this._external_url = this._urls?.external ?? "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.description {
|
||||
@ -351,6 +422,31 @@ class ConfigUrlForm extends LitElement {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.toggle-unmasked-url {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -1930,7 +1930,10 @@
|
||||
"multiselect": {
|
||||
"failed": "Failed to update {number} items."
|
||||
},
|
||||
"learn_more": "Learn more"
|
||||
"learn_more": "Learn more",
|
||||
"show_url": "Show full URL",
|
||||
"hide_url": "Hide URL",
|
||||
"copy_link": "Copy link"
|
||||
},
|
||||
"updates": {
|
||||
"caption": "Updates",
|
||||
@ -2398,7 +2401,9 @@
|
||||
"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."
|
||||
"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.",
|
||||
"internal_url_automatic_description": "Use the configured network settings",
|
||||
"internal_url_placeholder": "http://<some IP address>:8123"
|
||||
},
|
||||
"hardware": {
|
||||
"caption": "Hardware",
|
||||
@ -3915,9 +3920,6 @@
|
||||
"info": "Home Assistant Cloud provides a secure remote access to your instance while away from home. For more information on remote access and these settings visit our security documentation.",
|
||||
"info_instance_will_be_available": "Your instance will be available at your Nabu Casa URL.",
|
||||
"link_learn_how_it_works": "Learn how it works",
|
||||
"show_url": "Show full URL",
|
||||
"hide_url": "Hide URL",
|
||||
"copy_link": "Copy link",
|
||||
"security_options": "Security options",
|
||||
"external_activation": "Allow external activation of remote access",
|
||||
"external_activation_secondary": "If you disable remote access on this page, having this setting enabled allows you to reactivate it remotely via your Nabu Casa account.",
|
||||
|
9
src/util/url.ts
Normal file
9
src/util/url.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function obfuscateUrl(url: string) {
|
||||
if (url.endsWith(".ui.nabu.casa")) {
|
||||
return "https://•••••••••••••••••.ui.nabu.casa";
|
||||
}
|
||||
// hide any words that look like they might be a hostname or IP address
|
||||
return url.replace(/(?<=:\/\/)[\w-]+|(?<=\.)[\w-]+/g, (match) =>
|
||||
"•".repeat(match.length)
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user