diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index f62a33fd7a..bda6105400 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -61,7 +61,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => { if (!name) { return; } - const newVal = ev.detail.value; + const newVal = (ev.target as any)?.value; if ((element.trigger[name] || "") === newVal) { return; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts index 4d90ec0219..fecd09535d 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts @@ -1,39 +1,133 @@ -import "@polymer/paper-input/paper-input"; -import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { WebhookTrigger } from "../../../../../data/automation"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-textfield"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { mdiContentCopy } from "@mdi/js"; +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { slugify } from "../../../../../common/string/slugify"; +import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; +import type { HaTextField } from "../../../../../components/ha-textfield"; +import { showToast } from "../../../../../util/toast"; +import { + WebhookTrigger, + AutomationConfig, +} from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-trigger-row"; +const DEFAULT_WEBHOOK_ID = ""; + @customElement("ha-automation-trigger-webhook") export class HaWebhookTrigger extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public trigger!: WebhookTrigger; + @state() private _config?: AutomationConfig; + + private _unsub?: UnsubscribeFunc; + public static get defaultConfig() { return { - webhook_id: "", + webhook_id: DEFAULT_WEBHOOK_ID, }; } + connectedCallback() { + super.connectedCallback(); + const details = { + callback: (config) => { + this._config = config; + }, + }; + fireEvent(this, "subscribe-automation-config", details); + this._unsub = (details as any).unsub; + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub(); + } + } + + private _generateWebhookId(): string { + // The webhook_id should be treated like a password. Generate a default + // value that would be hard for someone to guess. This generates a + // 144-bit random value. The output is a 24 character url-safe string. + const randomBytes = crypto.getRandomValues(new Uint8Array(18)); + const base64Str = btoa(String.fromCharCode(...randomBytes)); + const urlSafeId = base64Str.replace(/\+/g, "-").replace(/\//g, "_"); + + // Include the automation name to give the user context about what the + // webhook_id is used for. + const urlSafeAlias = slugify(this._config?.alias || "", "-"); + + return `${urlSafeAlias}-${urlSafeId}`; + } + + public willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (changedProperties.has("trigger")) { + if (this.trigger.webhook_id === DEFAULT_WEBHOOK_ID) { + this.trigger.webhook_id = this._generateWebhookId(); + } + } + } + protected render() { const { webhook_id: webhookId } = this.trigger; + return html` - + .helper=${this.hass.localize( + "ui.panel.config.automation.editor.triggers.type.webhook.webhook_id_helper" + )} + .iconTrailing=${true} + .value=${webhookId || ""} + @input=${this._valueChanged} + > + + `; } private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + private async _copyUrl(ev): Promise { + const inputElement = ev.target.parentElement as HaTextField; + const url = this.hass.hassUrl(`/api/webhook/${inputElement.value}`); + + await copyToClipboard(url); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + + static styles = css` + ha-textfield { + display: block; + } + + ha-textfield > ha-icon-button { + --mdc-icon-button-size: 24px; + --mdc-icon-size: 18px; + } + `; } declare global { diff --git a/src/translations/en.json b/src/translations/en.json index 6668462050..2583b4d528 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1678,8 +1678,10 @@ "seconds": "Seconds" }, "webhook": { + "copy_url": "Copy URL to Clipboard", "label": "Webhook", - "webhook_id": "Webhook ID" + "webhook_id": "Webhook ID", + "webhook_id_helper": "Treat this ID like a password: keep it secret, and make it hard to guess." }, "zone": { "label": "Zone",