Add iframe strategy (#20068)

* Add iframe strategy with editor

* Unify sandbox parameters

* Update translations

* Remove title from editor

* Add editor when creating iframe strategy

* Update src/translations/en.json
This commit is contained in:
Paul Bottein 2024-03-21 13:44:49 +01:00 committed by GitHub
parent c30b9cdfcf
commit 90e9f79841
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 320 additions and 6 deletions

View File

@ -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")

View File

@ -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`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
)
)}
>
<div>
<hui-dashboard-strategy-element-editor
.hass=${this.hass}
.lovelace=${this._params.config}
.value=${this._data}
@config-changed=${this._handleConfigChanged}
dialogInitialFocus
></hui-dashboard-strategy-element-editor>
</div>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.next")}
</ha-button>
</ha-dialog>
`;
}
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;
}
}

View File

@ -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<DataTableItem> = {
@ -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<void> {
showDashboardDetailDialog(this, {
dashboard,

View File

@ -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<unknown>;
}
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,
});
};

View File

@ -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"
></iframe>
</hass-subpage>

View File

@ -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 {
<iframe
title=${ifDefined(this._config.title)}
src=${this._config.url}
sandbox="${sandbox_user_params} allow-forms allow-modals allow-popups allow-pointer-lock allow-same-origin allow-scripts"
.sandbox=${`${sandbox_user_params} ${IFRAME_SANDBOX}`}
allow="fullscreen"
></iframe>
</div>

View File

@ -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`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
const data = ev.detail.value;
fireEvent(this, "config-changed", { config: data });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
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;
}
}

View File

@ -25,12 +25,14 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
"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: {},
};

View File

@ -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<LovelaceConfig> {
return {
title: config.title,
views: [
{
strategy: config,
},
],
};
}
public static async getConfigElement(): Promise<LovelaceStrategyEditor> {
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;
}
}

View File

@ -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<LovelaceViewConfig> {
return {
type: "panel",
title: config.title,
cards: [
{
type: "iframe",
url: config.url,
} as IframeCardConfig,
],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"iframe-view-strategy": IframeViewStrategy;
}
}

View File

@ -9,6 +9,7 @@ export type LovelaceStrategy<T = any> = {
generate(config: LovelaceStrategyConfig, hass: HomeAssistant): Promise<T>;
getConfigElement?: () => LovelaceStrategyEditor;
noEditor?: boolean;
configRequired?: boolean;
};
export interface LovelaceDashboardStrategy

View File

@ -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": {

2
src/util/iframe.ts Normal file
View File

@ -0,0 +1,2 @@
export const IFRAME_SANDBOX =
"allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts allow-modals allow-downloads";