From 04cf316cd2fa7325ccaa0222c0bd993a45d9eddd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 12 Mar 2024 12:12:07 +0100 Subject: [PATCH 1/6] Add iframe strategy with editor --- .../config/dashboard/dialog-new-dashboard.ts | 6 +- .../hui-iframe-dashboard-strategy-editor.ts | 82 +++++++++++++++++++ .../lovelace/strategies/get-strategy.ts | 2 + .../iframe/iframe-dashboard-strategy.ts | 36 ++++++++ .../strategies/iframe/iframe-view-strategy.ts | 34 ++++++++ src/translations/en.json | 8 ++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts create mode 100644 src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts create mode 100644 src/panels/lovelace/strategies/iframe/iframe-view-strategy.ts diff --git a/src/panels/config/dashboard/dialog-new-dashboard.ts b/src/panels/config/dashboard/dialog-new-dashboard.ts index 10b65cee0c9f..e6a71ddb9844 100644 --- a/src/panels/config/dashboard/dialog-new-dashboard.ts +++ b/src/panels/config/dashboard/dialog-new-dashboard.ts @@ -1,5 +1,5 @@ import "@material/mwc-list/mwc-list"; -import { mdiMap, mdiPencilOutline, mdiShape } from "@mdi/js"; +import { mdiMap, mdiPencilOutline, mdiShape, mdiWeb } from "@mdi/js"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -25,6 +25,10 @@ const STRATEGIES = [ type: "map", iconPath: mdiMap, }, + { + type: "iframe", + iconPath: mdiWeb, + }, ] as const satisfies Strategy[]; @customElement("ha-dialog-new-dashboard") diff --git a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts new file mode 100644 index 000000000000..cf15d763414a --- /dev/null +++ b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts @@ -0,0 +1,82 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import { IframeDashboardStrategyConfig } from "../../strategies/iframe/iframe-dashboard-strategy"; +import { LovelaceStrategyEditor } from "../../strategies/types"; + +const SCHEMA = [ + { + name: "title", + selector: { + text: {}, + }, + }, + { + name: "url", + selector: { + text: { + type: "url", + }, + }, + }, +] as const satisfies readonly HaFormSchema[]; + +@customElement("hui-iframe-dashboard-strategy-editor") +export class HuiIframeDashboarStrategyEditor + extends LitElement + implements LovelaceStrategyEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() + private _config?: IframeDashboardStrategyConfig; + + public setConfig(config: IframeDashboardStrategyConfig): void { + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const data = ev.detail.value; + fireEvent(this, "config-changed", { config: data }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => { + switch (schema.name) { + case "title": + case "url": + return this.hass?.localize( + `ui.panel.lovelace.editor.strategy.iframe.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-iframe-dashboard-strategy-editor": HuiIframeDashboarStrategyEditor; + } +} diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts index 5613d2311101..4c363ef97a58 100644 --- a/src/panels/lovelace/strategies/get-strategy.ts +++ b/src/panels/lovelace/strategies/get-strategy.ts @@ -25,12 +25,14 @@ const STRATEGIES: Record> = { "original-states": () => import("./original-states/original-states-dashboard-strategy"), map: () => import("./map/map-dashboard-strategy"), + iframe: () => import("./iframe/iframe-dashboard-strategy"), }, view: { "original-states": () => import("./original-states/original-states-view-strategy"), energy: () => import("../../energy/strategies/energy-view-strategy"), map: () => import("./map/map-view-strategy"), + iframe: () => import("./iframe/iframe-view-strategy"), }, section: {}, }; diff --git a/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts b/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts new file mode 100644 index 000000000000..9b3bd3125a67 --- /dev/null +++ b/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts @@ -0,0 +1,36 @@ +import { ReactiveElement } from "lit"; +import { customElement } from "lit/decorators"; +import { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { LovelaceStrategyEditor } from "../types"; +import { IframeViewStrategyConfig } from "./iframe-view-strategy"; + +export type IframeDashboardStrategyConfig = IframeViewStrategyConfig; + +@customElement("iframe-dashboard-strategy") +export class IframeDashboardStrategy extends ReactiveElement { + static async generate( + config: IframeDashboardStrategyConfig + ): Promise { + return { + title: config.title, + views: [ + { + strategy: config, + }, + ], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../../editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor" + ); + return document.createElement("hui-iframe-dashboard-strategy-editor"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "iframe-dashboard-strategy": IframeDashboardStrategy; + } +} diff --git a/src/panels/lovelace/strategies/iframe/iframe-view-strategy.ts b/src/panels/lovelace/strategies/iframe/iframe-view-strategy.ts new file mode 100644 index 000000000000..043188b7a5c8 --- /dev/null +++ b/src/panels/lovelace/strategies/iframe/iframe-view-strategy.ts @@ -0,0 +1,34 @@ +import { ReactiveElement } from "lit"; +import { customElement } from "lit/decorators"; +import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; +import { IframeCardConfig } from "../../cards/types"; + +export type IframeViewStrategyConfig = { + type: "iframe"; + url: string; + title?: string; +}; + +@customElement("iframe-view-strategy") +export class IframeViewStrategy extends ReactiveElement { + static async generate( + config: IframeViewStrategyConfig + ): Promise { + return { + type: "panel", + title: config.title, + cards: [ + { + type: "iframe", + url: config.url, + } as IframeCardConfig, + ], + }; + } +} + +declare global { + interface HTMLElementTagNameMap { + "iframe-view-strategy": IframeViewStrategy; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index c016bba08a05..e968734a7306 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2212,6 +2212,10 @@ "map": { "title": "[%key:panel::map%]", "description": "Display people and your devices on a map" + }, + "iframe": { + "title": "Web page", + "description": "Integrate a web page into your dashboard" } } }, @@ -5780,6 +5784,10 @@ "areas": "Areas", "hide_entities_without_area": "Hide entities without area", "hide_energy": "Hide energy" + }, + "iframe": { + "title": "Title", + "url": "URL" } }, "view": { From 149f427b826f37546aa17f19582634501b82e889 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 12 Mar 2024 12:17:49 +0100 Subject: [PATCH 2/6] Unify sandbox parameters --- src/panels/iframe/ha-panel-iframe.ts | 3 ++- src/panels/lovelace/cards/hui-iframe-card.ts | 3 ++- src/util/iframe.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/util/iframe.ts diff --git a/src/panels/iframe/ha-panel-iframe.ts b/src/panels/iframe/ha-panel-iframe.ts index 72b7e96d10ab..60d4f99742e1 100644 --- a/src/panels/iframe/ha-panel-iframe.ts +++ b/src/panels/iframe/ha-panel-iframe.ts @@ -4,6 +4,7 @@ import { ifDefined } from "lit/directives/if-defined"; import "../../layouts/hass-error-screen"; import "../../layouts/hass-subpage"; import { HomeAssistant, PanelInfo } from "../../types"; +import { IFRAME_SANDBOX } from "../../util/iframe"; @customElement("ha-panel-iframe") class HaPanelIframe extends LitElement { @@ -40,7 +41,7 @@ class HaPanelIframe extends LitElement { this.panel.title === null ? undefined : this.panel.title )} src=${this.panel.config.url} - sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts allow-modals allow-downloads" + .sandbox=${IFRAME_SANDBOX} allow="fullscreen" > diff --git a/src/panels/lovelace/cards/hui-iframe-card.ts b/src/panels/lovelace/cards/hui-iframe-card.ts index d81de3dd88f1..c64cb2bf243e 100644 --- a/src/panels/lovelace/cards/hui-iframe-card.ts +++ b/src/panels/lovelace/cards/hui-iframe-card.ts @@ -6,6 +6,7 @@ import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import "../../../components/ha-alert"; import "../../../components/ha-card"; import type { HomeAssistant } from "../../../types"; +import { IFRAME_SANDBOX } from "../../../util/iframe"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { IframeCardConfig } from "./types"; @@ -96,7 +97,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard { diff --git a/src/util/iframe.ts b/src/util/iframe.ts new file mode 100644 index 000000000000..467935996c50 --- /dev/null +++ b/src/util/iframe.ts @@ -0,0 +1,2 @@ +export const IFRAME_SANDBOX = + "allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts allow-modals allow-downloads"; From a3659246e404df592b0b8feeb060c97dddc149ed Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 12 Mar 2024 12:19:20 +0100 Subject: [PATCH 3/6] Update translations --- src/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index e968734a7306..cba89ad05cba 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2214,8 +2214,8 @@ "description": "Display people and your devices on a map" }, "iframe": { - "title": "Web page", - "description": "Integrate a web page into your dashboard" + "title": "Webpage", + "description": "Integrate a webpage into Home Assistant." } } }, From 97810c0ccc0cafa0169ed0f155876b18d9332099 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 14 Mar 2024 17:34:58 +0100 Subject: [PATCH 4/6] Remove title from editor --- .../hui-iframe-dashboard-strategy-editor.ts | 7 ------- src/translations/en.json | 1 - 2 files changed, 8 deletions(-) diff --git a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts index cf15d763414a..32d012b0ce3e 100644 --- a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts +++ b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts @@ -11,12 +11,6 @@ import { IframeDashboardStrategyConfig } from "../../strategies/iframe/iframe-da import { LovelaceStrategyEditor } from "../../strategies/types"; const SCHEMA = [ - { - name: "title", - selector: { - text: {}, - }, - }, { name: "url", selector: { @@ -64,7 +58,6 @@ export class HuiIframeDashboarStrategyEditor private _computeLabelCallback = (schema: SchemaUnion) => { switch (schema.name) { - case "title": case "url": return this.hass?.localize( `ui.panel.lovelace.editor.strategy.iframe.${schema.name}` diff --git a/src/translations/en.json b/src/translations/en.json index cba89ad05cba..eece05c61ba6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5786,7 +5786,6 @@ "hide_energy": "Hide energy" }, "iframe": { - "title": "Title", "url": "URL" } }, From d1eef9e71b167d786403c7d6ceee55cf3912d494 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 14 Mar 2024 18:13:51 +0100 Subject: [PATCH 5/6] Add editor when creating iframe strategy --- ...g-lovelace-dashboard-configure-strategy.ts | 101 ++++++++++++++++++ .../ha-config-lovelace-dashboards.ts | 33 +++++- ...g-lovelace-dashboard-configure-strategy.ts | 21 ++++ .../iframe/iframe-dashboard-strategy.ts | 2 + src/panels/lovelace/strategies/types.ts | 1 + 5 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-configure-strategy.ts create mode 100644 src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-configure-strategy.ts diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-configure-strategy.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-configure-strategy.ts new file mode 100644 index 000000000000..2509438daae7 --- /dev/null +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-configure-strategy.ts @@ -0,0 +1,101 @@ +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-form/ha-form"; +import { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor"; +import { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy"; + +@customElement("dialog-lovelace-dashboard-configure-strategy") +export class DialogLovelaceDashboardDetail extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: LovelaceDashboardConfigureStrategyDialogParams; + + @state() private _submitting = false; + + @state() private _data?: LovelaceStrategyConfig; + + public showDialog( + params: LovelaceDashboardConfigureStrategyDialogParams + ): void { + this._params = params; + this._data = params.config.strategy; + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params || !this._data) { + return nothing; + } + + return html` + +
+ +
+ + + ${this.hass.localize("ui.common.next")} + +
+ `; + } + + private _handleConfigChanged(ev: CustomEvent): void { + this._data = ev.detail.config; + } + + private async _save() { + if (!this._data) { + return; + } + this._submitting = true; + await this._params!.saveConfig({ + ...this._params!.config, + strategy: this._data, + }); + this._submitting = false; + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardDetail; + } +} diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index ad04b2b36857..9e901fa23685 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -24,7 +24,8 @@ import "../../../../components/ha-icon-button"; import "../../../../components/ha-svg-icon"; import { LovelacePanelConfig } from "../../../../data/lovelace"; import { - LovelaceConfig, + LovelaceRawConfig, + isStrategyDashboard, saveConfig, } from "../../../../data/lovelace/config/types"; import { @@ -39,8 +40,10 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog- import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../../types"; +import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy"; import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard"; import { lovelaceTabs } from "../ha-config-lovelace"; +import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; type DataTableItem = Pick< @@ -64,6 +67,12 @@ export class HaConfigLovelaceDashboards extends LitElement { @state() private _dashboards: LovelaceDashboard[] = []; + public willUpdate() { + if (!this.hasUpdated) { + this.hass.loadFragmentTranslation("lovelace"); + } + } + private _columns = memoize( (narrow: boolean, _language, dashboards): DataTableColumnContainer => { const columns: DataTableColumnContainer = { @@ -339,7 +348,25 @@ export class HaConfigLovelaceDashboards extends LitElement { private async _addDashboard() { showNewDashboardDialog(this, { - selectConfig: (config) => { + selectConfig: async (config) => { + if (config && isStrategyDashboard(config)) { + const strategyType = config.strategy.type; + const strategyClass = await getLovelaceStrategy( + "dashboard", + strategyType + ); + + if (strategyClass.configRequired) { + showDashboardConfigureStrategyDialog(this, { + config: config, + saveConfig: async (updatedConfig) => { + this._openDetailDialog(undefined, undefined, updatedConfig); + }, + }); + return; + } + } + this._openDetailDialog(undefined, undefined, config); }, }); @@ -348,7 +375,7 @@ export class HaConfigLovelaceDashboards extends LitElement { private async _openDetailDialog( dashboard?: LovelaceDashboard, urlPath?: string, - defaultConfig?: LovelaceConfig + defaultConfig?: LovelaceRawConfig ): Promise { showDashboardDetailDialog(this, { dashboard, diff --git a/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-configure-strategy.ts b/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-configure-strategy.ts new file mode 100644 index 000000000000..51f131e7b303 --- /dev/null +++ b/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-configure-strategy.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import { LovelaceDashboardStrategyConfig } from "../../../../data/lovelace/config/types"; + +export interface LovelaceDashboardConfigureStrategyDialogParams { + config: LovelaceDashboardStrategyConfig; + saveConfig: (values: LovelaceDashboardStrategyConfig) => Promise; +} + +export const loadDashboardConfigureStrategyDialog = () => + import("./dialog-lovelace-dashboard-configure-strategy"); + +export const showDashboardConfigureStrategyDialog = ( + element: HTMLElement, + dialogParams: LovelaceDashboardConfigureStrategyDialogParams +) => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-lovelace-dashboard-configure-strategy", + dialogImport: loadDashboardConfigureStrategyDialog, + dialogParams, + }); +}; diff --git a/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts b/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts index 9b3bd3125a67..471823af8ce7 100644 --- a/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts @@ -27,6 +27,8 @@ export class IframeDashboardStrategy extends ReactiveElement { ); return document.createElement("hui-iframe-dashboard-strategy-editor"); } + + static configRequired = true; } declare global { diff --git a/src/panels/lovelace/strategies/types.ts b/src/panels/lovelace/strategies/types.ts index ac3a07de3738..2dd6006a93d4 100644 --- a/src/panels/lovelace/strategies/types.ts +++ b/src/panels/lovelace/strategies/types.ts @@ -9,6 +9,7 @@ export type LovelaceStrategy = { generate(config: LovelaceStrategyConfig, hass: HomeAssistant): Promise; getConfigElement?: () => LovelaceStrategyEditor; noEditor?: boolean; + configRequired?: boolean; }; export interface LovelaceDashboardStrategy From c01ad33cfbaedebb0eecb82d80582146324cdb7b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 13:56:07 +0100 Subject: [PATCH 6/6] Update src/translations/en.json --- src/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index eece05c61ba6..6ca56eb1a9d8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2215,7 +2215,7 @@ }, "iframe": { "title": "Webpage", - "description": "Integrate a webpage into Home Assistant." + "description": "Integrate a webpage as a dashboard." } } },