Skip to content

Commit

Permalink
Add iframe strategy (#20068)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piitaya authored Mar 21, 2024
1 parent c30b9cd commit 90e9f79
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 6 deletions.
6 changes: 5 additions & 1 deletion src/panels/config/dashboard/dialog-new-dashboard.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,6 +25,10 @@ const STRATEGIES = [
type: "map",
iconPath: mdiMap,
},
{
type: "iframe",
iconPath: mdiWeb,
},
] as const satisfies Strategy[];

@customElement("ha-dialog-new-dashboard")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<
Expand All @@ -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> = {
Expand Down Expand Up @@ -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);
},
});
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
};
3 changes: 2 additions & 1 deletion src/panels/iframe/ha-panel-iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>
Expand Down
3 changes: 2 additions & 1 deletion src/panels/lovelace/cards/hui-iframe-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions src/panels/lovelace/strategies/get-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
};
Expand Down
38 changes: 38 additions & 0 deletions src/panels/lovelace/strategies/iframe/iframe-dashboard-strategy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 90e9f79

Please sign in to comment.