mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Add Cloud Webhook management (#2102)
* Add Cloud Webhook support * Lint * Tweak text * Rename it to cloudhook * Fix final type * fix type * Catch null
This commit is contained in:
parent
8ad5280501
commit
07b65f37db
19
src/data/cloud.ts
Normal file
19
src/data/cloud.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface CloudWebhook {
|
||||
webhook_id: string;
|
||||
cloudhook_id: string;
|
||||
cloudhook_url: string;
|
||||
}
|
||||
|
||||
export const createCloudhook = (hass: HomeAssistant, webhookId: string) =>
|
||||
hass.callWS<CloudWebhook>({
|
||||
type: "cloud/cloudhook/create",
|
||||
webhook_id: webhookId,
|
||||
});
|
||||
|
||||
export const deleteCloudhook = (hass: HomeAssistant, webhookId: string) =>
|
||||
hass.callWS({
|
||||
type: "cloud/cloudhook/delete",
|
||||
webhook_id: webhookId,
|
||||
});
|
12
src/data/webhook.ts
Normal file
12
src/data/webhook.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface Webhook {
|
||||
webhook_id: string;
|
||||
domain: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const fetchWebhooks = (hass: HomeAssistant): Promise<Webhook[]> =>
|
||||
hass.callWS({
|
||||
type: "webhook/list",
|
||||
});
|
@ -24,6 +24,10 @@ export class CloudAlexaPref extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.cloudStatus) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const enabled = this.cloudStatus!.prefs.alexa_enabled;
|
||||
|
||||
return html`
|
||||
|
@ -25,7 +25,11 @@ export class CloudGooglePref extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { google_enabled, google_allow_unlock } = this.cloudStatus!.prefs;
|
||||
if (!this.cloudStatus) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const { google_enabled, google_allow_unlock } = this.cloudStatus.prefs;
|
||||
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
|
142
src/panels/config/cloud/cloud-webhook-manage-dialog.ts
Normal file
142
src/panels/config/cloud/cloud-webhook-manage-dialog.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
|
||||
// tslint:disable-next-line
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
|
||||
import { buttonLink } from "../../../resources/ha-style";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { WebhookDialogParams } from "./types";
|
||||
|
||||
const inputLabel = "Public URL – Click to copy to clipboard";
|
||||
|
||||
export class CloudWebhookManageDialog extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
private _params?: WebhookDialogParams;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_params: {},
|
||||
};
|
||||
}
|
||||
|
||||
public async showDialog(params: WebhookDialogParams) {
|
||||
this._params = params;
|
||||
// Wait till dialog is rendered.
|
||||
await this.updateComplete;
|
||||
this._dialog.open();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const { webhook, cloudhook } = this._params;
|
||||
const docsUrl =
|
||||
webhook.domain === "automation"
|
||||
? "https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger"
|
||||
: `https://www.home-assistant.io/components/${webhook.domain}/`;
|
||||
return html`
|
||||
${this._renderStyle()}
|
||||
<paper-dialog with-backdrop>
|
||||
<h2>Webhook for ${webhook.name}</h2>
|
||||
<div>
|
||||
<p>The webhook is available at the following url:</p>
|
||||
<paper-input
|
||||
label="${inputLabel}"
|
||||
value="${cloudhook.cloudhook_url}"
|
||||
@click="${this._copyClipboard}"
|
||||
@blur="${this._restoreLabel}"
|
||||
></paper-input>
|
||||
<p>
|
||||
If you no longer want to use this webhook, you can
|
||||
<button class="link" @click="${this._disableWebhook}">
|
||||
disable it</button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="paper-dialog-buttons">
|
||||
<a href="${docsUrl}" target="_blank"
|
||||
><paper-button>VIEW DOCUMENTATION</paper-button></a
|
||||
>
|
||||
<paper-button @click="${this._closeDialog}">CLOSE</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _dialog(): PaperDialogElement {
|
||||
return this.shadowRoot!.querySelector("paper-dialog")!;
|
||||
}
|
||||
|
||||
private get _paperInput(): PaperInputElement {
|
||||
return this.shadowRoot!.querySelector("paper-input")!;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
private async _disableWebhook() {
|
||||
if (!confirm("Are you sure you want to disable this webhook?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._params!.disableHook();
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _copyClipboard(ev: FocusEvent) {
|
||||
// paper-input -> iron-input -> input
|
||||
const paperInput = ev.currentTarget as PaperInputElement;
|
||||
const input = (paperInput.inputElement as any)
|
||||
.inputElement as HTMLInputElement;
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
paperInput.label = "COPIED TO CLIPBOARD";
|
||||
} catch (err) {
|
||||
// Copying failed. Oh no
|
||||
}
|
||||
}
|
||||
|
||||
private _restoreLabel() {
|
||||
this._paperInput.label = inputLabel;
|
||||
}
|
||||
|
||||
private _renderStyle() {
|
||||
return html`
|
||||
<style>
|
||||
paper-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
paper-input {
|
||||
margin-top: -8px;
|
||||
}
|
||||
${buttonLink} button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"cloud-webhook-manage-dialog": CloudWebhookManageDialog;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cloud-webhook-manage-dialog", CloudWebhookManageDialog);
|
234
src/panels/config/cloud/cloud-webhooks.ts
Normal file
234
src/panels/config/cloud/cloud-webhooks.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "@polymer/lit-element";
|
||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import "../../../components/ha-card";
|
||||
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import { HomeAssistant, WebhookError } from "../../../types";
|
||||
import { WebhookDialogParams, CloudStatusLoggedIn } from "./types";
|
||||
import { Webhook, fetchWebhooks } from "../../../data/webhook";
|
||||
import {
|
||||
createCloudhook,
|
||||
deleteCloudhook,
|
||||
CloudWebhook,
|
||||
} from "../../../data/cloud";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"manage-cloud-webhook": WebhookDialogParams;
|
||||
}
|
||||
}
|
||||
|
||||
export class CloudWebhooks extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public cloudStatus?: CloudStatusLoggedIn;
|
||||
private _cloudHooks?: { [webhookId: string]: CloudWebhook };
|
||||
private _localHooks?: Webhook[];
|
||||
private _progress: string[];
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
cloudStatus: {},
|
||||
_cloudHooks: {},
|
||||
_localHooks: {},
|
||||
_progress: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._progress = [];
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.renderStyle()}
|
||||
<ha-card header="Webhooks">
|
||||
<div class="body">
|
||||
Anything that is configured to be triggered by a webhook can be given
|
||||
a publicly accessible URL to allow you to send data back to Home
|
||||
Assistant from anywhere, without exposing your instance to the
|
||||
internet.
|
||||
</div>
|
||||
|
||||
${this._renderBody()}
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://www.nabucasa.com/config/webhooks" target="_blank">
|
||||
Learn more about creating webhook-powered automations.
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("cloudStatus") && this.cloudStatus) {
|
||||
this._cloudHooks = this.cloudStatus.prefs.cloudhooks || {};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBody() {
|
||||
if (!this.cloudStatus || !this._localHooks || !this._cloudHooks) {
|
||||
return html`
|
||||
<div class="loading">Loading…</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return this._localHooks.map(
|
||||
(entry) => html`
|
||||
<div class="webhook" .entry="${entry}">
|
||||
<paper-item-body two-line>
|
||||
<div>
|
||||
${entry.name}
|
||||
${
|
||||
entry.domain === entry.name.toLowerCase()
|
||||
? ""
|
||||
: ` (${entry.domain})`
|
||||
}
|
||||
</div>
|
||||
<div secondary>${entry.webhook_id}</div>
|
||||
</paper-item-body>
|
||||
${
|
||||
this._progress.includes(entry.webhook_id)
|
||||
? html`
|
||||
<div class="progress">
|
||||
<paper-spinner active></paper-spinner>
|
||||
</div>
|
||||
`
|
||||
: this._cloudHooks![entry.webhook_id]
|
||||
? html`
|
||||
<paper-button @click="${this._handleManageButton}"
|
||||
>Manage</paper-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<paper-toggle-button
|
||||
@click="${this._enableWebhook}"
|
||||
></paper-toggle-button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
private _showDialog(webhookId: string) {
|
||||
const webhook = this._localHooks!.find(
|
||||
(ent) => ent.webhook_id === webhookId
|
||||
);
|
||||
const cloudhook = this._cloudHooks![webhookId];
|
||||
const params: WebhookDialogParams = {
|
||||
webhook: webhook!,
|
||||
cloudhook,
|
||||
disableHook: () => this._disableWebhook(webhookId),
|
||||
};
|
||||
fireEvent(this, "manage-cloud-webhook", params);
|
||||
}
|
||||
|
||||
private _handleManageButton(ev: MouseEvent) {
|
||||
const entry = (ev.currentTarget as any).parentElement.entry as Webhook;
|
||||
this._showDialog(entry.webhook_id);
|
||||
}
|
||||
|
||||
private async _enableWebhook(ev: MouseEvent) {
|
||||
const entry = (ev.currentTarget as any).parentElement.entry;
|
||||
this._progress = [...this._progress, entry.webhook_id];
|
||||
let updatedWebhook;
|
||||
|
||||
try {
|
||||
updatedWebhook = await createCloudhook(this.hass!, entry.webhook_id);
|
||||
} catch (err) {
|
||||
alert((err as WebhookError).message);
|
||||
return;
|
||||
} finally {
|
||||
this._progress = this._progress.filter((wid) => wid !== entry.webhook_id);
|
||||
}
|
||||
|
||||
this._cloudHooks = {
|
||||
...this._cloudHooks,
|
||||
[entry.webhook_id]: updatedWebhook,
|
||||
};
|
||||
|
||||
// Only open dialog if we're not also enabling others, otherwise it's confusing
|
||||
if (this._progress.length === 0) {
|
||||
this._showDialog(entry.webhook_id);
|
||||
}
|
||||
}
|
||||
|
||||
private async _disableWebhook(webhookId: string) {
|
||||
this._progress = [...this._progress, webhookId];
|
||||
try {
|
||||
await deleteCloudhook(this.hass!, webhookId!);
|
||||
} catch (err) {
|
||||
alert(`Failed to disable webhook: ${(err as WebhookError).message}`);
|
||||
return;
|
||||
} finally {
|
||||
this._progress = this._progress.filter((wid) => wid !== webhookId);
|
||||
}
|
||||
|
||||
// Remove cloud related parts from entry.
|
||||
const { [webhookId]: disabledHook, ...newHooks } = this._cloudHooks!;
|
||||
this._cloudHooks = newHooks;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._localHooks = await fetchWebhooks(this.hass!);
|
||||
}
|
||||
|
||||
private renderStyle() {
|
||||
return html`
|
||||
<style>
|
||||
.body {
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
.loading {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.webhook {
|
||||
display: flex;
|
||||
padding: 4px 16px;
|
||||
}
|
||||
.progress {
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
paper-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.footer {
|
||||
padding: 16px;
|
||||
}
|
||||
.footer a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"cloud-webhooks": CloudWebhooks;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cloud-webhooks", CloudWebhooks);
|
@ -10,15 +10,19 @@ import "../../../layouts/hass-subpage";
|
||||
import "../../../resources/ha-style";
|
||||
|
||||
import "../ha-config-section";
|
||||
import "./cloud-webhooks";
|
||||
|
||||
import formatDateTime from "../../../common/datetime/format_date_time";
|
||||
import EventsMixin from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
import { fetchSubscriptionInfo } from "./data";
|
||||
import "./cloud-alexa-pref";
|
||||
import "./cloud-google-pref";
|
||||
|
||||
let registeredWebhookDialog = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
@ -129,6 +133,11 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
></cloud-google-pref>
|
||||
|
||||
<cloud-webhooks
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
></cloud-webhooks>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
@ -152,9 +161,26 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
this._fetchSubscriptionInfo();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!registeredWebhookDialog) {
|
||||
registeredWebhookDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "manage-cloud-webhook",
|
||||
dialogTag: "cloud-webhook-manage-dialog",
|
||||
dialogImport: () => import("./cloud-webhook-manage-dialog"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchSubscriptionInfo() {
|
||||
this._subscription = await fetchSubscriptionInfo(this.hass);
|
||||
if (this._subscription.provider && this.cloudStatus.cloud !== "connected") {
|
||||
if (
|
||||
this._subscription.provider &&
|
||||
this.cloudStatus &&
|
||||
this.cloudStatus.cloud !== "connected"
|
||||
) {
|
||||
this.fire("ha-refresh-cloud-status");
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { CloudWebhook } from "../../../data/cloud";
|
||||
import { Webhook } from "../../../data/webhook";
|
||||
|
||||
export interface EntityFilter {
|
||||
include_domains: string[];
|
||||
include_entities: string[];
|
||||
@ -19,6 +22,7 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||
google_enabled: boolean;
|
||||
alexa_enabled: boolean;
|
||||
google_allow_unlock: boolean;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
};
|
||||
};
|
||||
|
||||
@ -27,3 +31,9 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
||||
export interface SubscriptionInfo {
|
||||
human_description: string;
|
||||
}
|
||||
|
||||
export interface WebhookDialogParams {
|
||||
webhook: Webhook;
|
||||
cloudhook: CloudWebhook;
|
||||
disableHook: () => void;
|
||||
}
|
||||
|
@ -1,6 +1,19 @@
|
||||
import "@polymer/paper-styles/paper-styles";
|
||||
import "@polymer/polymer/polymer-legacy";
|
||||
|
||||
export const buttonLink = `
|
||||
button.link {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
|
||||
@ -162,16 +175,7 @@ documentContainer.innerHTML = `<custom-style>
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
button.link {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
${buttonLink}
|
||||
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
|
@ -15,6 +15,11 @@ declare global {
|
||||
var __VERSION__: string;
|
||||
}
|
||||
|
||||
export interface WebhookError {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Credential {
|
||||
auth_provider_type: string;
|
||||
auth_provider_id: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user