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/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/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/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..32d012b0ce3e
--- /dev/null
+++ b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-iframe-dashboard-strategy-editor.ts
@@ -0,0 +1,75 @@
+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: "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 "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..471823af8ce7
--- /dev/null
+++ b/src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts
@@ -0,0 +1,38 @@
+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");
+ }
+
+ static configRequired = true;
+}
+
+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/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
diff --git a/src/translations/en.json b/src/translations/en.json
index c5fd82a529a6..03bf3a621e22 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": "Webpage",
+ "description": "Integrate a webpage as a dashboard."
}
}
},
@@ -5785,6 +5789,9 @@
"areas": "Areas",
"hide_entities_without_area": "Hide entities without area",
"hide_energy": "Hide energy"
+ },
+ "iframe": {
+ "url": "URL"
}
},
"view": {
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";