ha-frontend-cdce8p/src/panels/lovelace/ha-panel-lovelace.ts

321 lines
8.7 KiB
TypeScript

import "@material/mwc-button";
import {
fetchConfig,
LovelaceConfig,
saveConfig,
subscribeLovelaceUpdates,
WindowWithLovelaceProm,
} from "../../data/lovelace";
import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen";
import "./hui-root";
import { HomeAssistant, PanelInfo, Route } from "../../types";
import { Lovelace } from "./types";
import {
LitElement,
html,
PropertyValues,
TemplateResult,
property,
} from "lit-element";
import { showSaveDialog } from "./editor/show-save-config-dialog";
import { generateLovelaceConfigFromHass } from "./common/generate-lovelace-config";
import { showToast } from "../../util/toast";
interface LovelacePanelConfig {
mode: "yaml" | "storage";
}
let editorLoaded = false;
class LovelacePanel extends LitElement {
@property() public panel?: PanelInfo<LovelacePanelConfig>;
@property() public hass?: HomeAssistant;
@property() public narrow?: boolean;
@property() public route?: Route;
@property() private _columns?: number;
@property()
private _state?: "loading" | "loaded" | "error" | "yaml-editor" = "loading";
@property() private _errorMsg?: string;
@property() private lovelace?: Lovelace;
private mqls?: MediaQueryList[];
private _ignoreNextUpdateEvent = false;
constructor() {
super();
this._closeEditor = this._closeEditor.bind(this);
}
public render(): TemplateResult | void {
const state = this._state!;
if (state === "loaded") {
return html`
<hui-root
.hass="${this.hass}"
.lovelace="${this.lovelace}"
.route="${this.route}"
.columns="${this._columns}"
.narrow=${this.narrow}
@config-refresh="${this._forceFetchConfig}"
></hui-root>
`;
}
if (state === "error") {
return html`
<hass-error-screen
title="${this.hass!.localize("domain.lovelace")}"
.error="${this._errorMsg}"
>
<mwc-button on-click="_forceFetchConfig"
>${this.hass!.localize(
"ui.panel.lovelace.reload_lovelace"
)}</mwc-button
>
</hass-error-screen>
`;
}
if (state === "yaml-editor") {
return html`
<hui-editor
.hass="${this.hass}"
.lovelace="${this.lovelace}"
.closeEditor="${this._closeEditor}"
></hui-editor>
`;
}
return html`
<hass-loading-screen
rootnav
.hass=${this.hass}
.narrow=${this.narrow}
></hass-loading-screen>
`;
}
public updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("narrow")) {
this._updateColumns();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && this.hass!.dockedSidebar !== oldHass.dockedSidebar) {
this._updateColumns();
}
}
public firstUpdated() {
this._fetchConfig(false);
// we don't want to unsub as we want to stay informed of updates
subscribeLovelaceUpdates(this.hass!.connection, () =>
this._lovelaceChanged()
);
// reload lovelace on reconnect so we are sure we have the latest config
window.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._fetchConfig(false);
}
});
this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
mql.addListener(this._updateColumns);
return mql;
});
this._updateColumns();
}
public connectedCallback(): void {
super.connectedCallback();
if (
this.lovelace &&
this.hass &&
this.lovelace.language !== this.hass.language
) {
// language has been changed, rebuild UI
this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode);
} else if (this.lovelace && this.lovelace.mode === "generated") {
// When lovelace is generated, we re-generate each time a user goes
// to the states panel to make sure new entities are shown.
this._state = "loading";
this._regenerateConfig();
}
}
private async _regenerateConfig() {
const conf = await generateLovelaceConfigFromHass(this.hass!);
this._setLovelaceConfig(conf, "generated");
this._state = "loaded";
}
private _closeEditor() {
this._state = "loaded";
}
private _updateColumns() {
const matchColumns = this.mqls!.reduce(
(cols, mql) => cols + Number(mql.matches),
0
);
// Do -1 column if the menu is docked and open
this._columns = Math.max(
1,
matchColumns -
Number(!this.narrow && this.hass!.dockedSidebar === "docked")
);
}
private _lovelaceChanged() {
if (this._ignoreNextUpdateEvent) {
this._ignoreNextUpdateEvent = false;
return;
}
showToast(this, {
message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"),
action: {
action: () => this._fetchConfig(false),
text: this.hass!.localize("ui.panel.lovelace.changed_toast.refresh"),
},
duration: 0,
dismissable: false,
});
}
private _forceFetchConfig() {
this._fetchConfig(true);
}
private async _fetchConfig(forceDiskRefresh) {
let conf: LovelaceConfig;
let confMode: Lovelace["mode"] = this.panel!.config.mode;
let confProm: Promise<LovelaceConfig>;
const llWindow = window as WindowWithLovelaceProm;
// On first load, we speed up loading page by having LL promise ready
if (llWindow.llConfProm) {
confProm = llWindow.llConfProm;
llWindow.llConfProm = undefined;
} else {
// Refreshing a YAML config can trigger an update event. We will ignore
// all update events while fetching the config and for 2 seconds after the cnofig is back.
// We ignore because we already have the latest config.
if (this.lovelace && this.lovelace.mode === "yaml") {
this._ignoreNextUpdateEvent = true;
}
confProm = fetchConfig(this.hass!.connection, forceDiskRefresh);
}
try {
conf = await confProm;
} catch (err) {
if (err.code !== "config_not_found") {
// tslint:disable-next-line
console.log(err);
this._state = "error";
this._errorMsg = err.message;
return;
}
conf = await generateLovelaceConfigFromHass(this.hass!);
confMode = "generated";
} finally {
// Ignore updates for another 2 seconds.
if (this.lovelace && this.lovelace.mode === "yaml") {
setTimeout(() => {
this._ignoreNextUpdateEvent = false;
}, 2000);
}
}
this._state = "loaded";
this._setLovelaceConfig(conf, confMode);
}
private _checkLovelaceConfig(config: LovelaceConfig) {
// Somehow there can be badges with value null, we remove those
config.views.forEach((view) => {
if (view.badges) {
view.badges = view.badges.filter(Boolean);
}
});
}
private _setLovelaceConfig(config: LovelaceConfig, mode: Lovelace["mode"]) {
this._checkLovelaceConfig(config);
this.lovelace = {
config,
mode,
editMode: this.lovelace ? this.lovelace.editMode : false,
language: this.hass!.language,
enableFullEditMode: () => {
if (!editorLoaded) {
editorLoaded = true;
import(/* webpackChunkName: "lovelace-yaml-editor" */ "./hui-editor");
}
this._state = "yaml-editor";
},
setEditMode: (editMode: boolean) => {
if (!editMode || this.lovelace!.mode !== "generated") {
this._updateLovelace({ editMode });
return;
}
showSaveDialog(this, {
lovelace: this.lovelace!,
});
},
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
const { config: previousConfig, mode: previousMode } = this.lovelace!;
this._checkLovelaceConfig(newConfig);
try {
// Optimistic update
this._updateLovelace({
config: newConfig,
mode: "storage",
});
this._ignoreNextUpdateEvent = true;
await saveConfig(this.hass!, newConfig);
} catch (err) {
// tslint:disable-next-line
console.error(err);
// Rollback the optimistic update
this._updateLovelace({
config: previousConfig,
mode: previousMode,
});
throw err;
}
},
};
}
private _updateLovelace(props: Partial<Lovelace>) {
this.lovelace = {
...this.lovelace!,
...props,
};
}
}
customElements.define("ha-panel-lovelace", LovelacePanel);