Show toast when Lovelace config was updated from a different place (#3218)

* Refresh other lovelace UI's when making a change

* Move to toast with refresh button

* Change to `hass-notification`

* Reload on reconnect

- Fix for duration = 0
- Reload on reconnect

* Listen to ready of connection

* Update src/managers/notification-manager.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* use showToast, listen connection-status,  noCancelOnOutsideClick -> option

* Remove unused import
This commit is contained in:
Bram Kragten 2019-05-27 23:54:14 +02:00 committed by Paulus Schoutsen
parent d10a0b3b6c
commit e595637a10
8 changed files with 152 additions and 53 deletions

View File

@ -1,4 +1,5 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Connection } from "home-assistant-js-websocket";
export interface LovelaceConfig { export interface LovelaceConfig {
title?: string; title?: string;
@ -76,3 +77,8 @@ export const saveConfig = (
type: "lovelace/config/save", type: "lovelace/config/save",
config, config,
}); });
export const subscribeLovelaceUpdates = (
conn: Connection,
onChange: () => void
) => conn.subscribeEvents(onChange, "lovelace_updated");

View File

@ -1,30 +1,95 @@
import {
LitElement,
query,
property,
TemplateResult,
html,
css,
CSSResult,
} from "lit-element";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-toast";
import { LitElement, query, property, TemplateResult, html } from "lit-element";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "@material/mwc-button";
import "../components/ha-toast";
// Typing // Typing
// tslint:disable-next-line: no-duplicate-imports // tslint:disable-next-line: no-duplicate-imports
import { HaToast } from "../components/ha-toast"; import { HaToast } from "../components/ha-toast";
export interface ShowToastParams { export interface ShowToastParams {
message: string; message: string;
action?: ToastActionParams;
duration?: number;
dismissable?: boolean;
}
export interface ToastActionParams {
action: () => void;
text: string;
} }
class NotificationManager extends LitElement { class NotificationManager extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() private _action?: ToastActionParams;
@property() private _noCancelOnOutsideClick: boolean = false;
@query("ha-toast") private _toast!: HaToast; @query("ha-toast") private _toast!: HaToast;
public showDialog({ message }: ShowToastParams) { public showDialog({
message,
action,
duration,
dismissable,
}: ShowToastParams) {
const toast = this._toast; const toast = this._toast;
toast.setAttribute("dir", computeRTL(this.hass) ? "rtl" : "ltr"); toast.setAttribute("dir", computeRTL(this.hass) ? "rtl" : "ltr");
toast.show(message); this._action = action || undefined;
this._noCancelOnOutsideClick =
dismissable === undefined ? false : !dismissable;
toast.hide();
toast.show({
text: message,
duration: duration === undefined ? 3000 : duration,
});
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-toast dir="[[_rtl]]" noCancelOnOutsideClick=${false}></ha-toast> <ha-toast .noCancelOnOutsideClick=${this._noCancelOnOutsideClick}>
${this._action
? html`
<mwc-button
.label=${this._action.text}
@click=${this.buttonClicked}
></mwc-button>
`
: ""}
</ha-toast>
`;
}
private buttonClicked() {
this._toast.hide();
if (this._action) {
this._action.action();
}
}
static get styles(): CSSResult {
return css`
mwc-button {
color: var(--primary-color);
font-weight: bold;
}
`; `;
} }
} }
customElements.define("notification-manager", NotificationManager); customElements.define("notification-manager", NotificationManager);
declare global {
// for fire event
interface HASSDomEvents {
"hass-notification": ShowToastParams;
}
}

View File

@ -1,6 +1,11 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { fetchConfig, LovelaceConfig, saveConfig } from "../../data/lovelace"; import {
fetchConfig,
LovelaceConfig,
saveConfig,
subscribeLovelaceUpdates,
} from "../../data/lovelace";
import "../../layouts/hass-loading-screen"; import "../../layouts/hass-loading-screen";
import "../../layouts/hass-error-screen"; import "../../layouts/hass-error-screen";
import "./hui-root"; import "./hui-root";
@ -15,6 +20,7 @@ import {
} from "lit-element"; } from "lit-element";
import { showSaveDialog } from "./editor/show-save-config-dialog"; import { showSaveDialog } from "./editor/show-save-config-dialog";
import { generateLovelaceConfig } from "./common/generate-lovelace-config"; import { generateLovelaceConfig } from "./common/generate-lovelace-config";
import { showToast } from "../../util/toast";
interface LovelacePanelConfig { interface LovelacePanelConfig {
mode: "yaml" | "storage"; mode: "yaml" | "storage";
@ -42,6 +48,8 @@ class LovelacePanel extends LitElement {
private mqls?: MediaQueryList[]; private mqls?: MediaQueryList[];
private _saving: boolean = false;
constructor() { constructor() {
super(); super();
this._closeEditor = this._closeEditor.bind(this); this._closeEditor = this._closeEditor.bind(this);
@ -66,7 +74,11 @@ class LovelacePanel extends LitElement {
if (state === "error") { if (state === "error") {
return html` return html`
<hass-error-screen title="Lovelace" .error="${this._errorMsg}"> <hass-error-screen title="Lovelace" .error="${this._errorMsg}">
<mwc-button on-click="_forceFetchConfig">Reload Lovelace</mwc-button> <mwc-button on-click="_forceFetchConfig"
>${this.hass!.localize(
"ui.panel.lovelace.reload_lovelace"
)}</mwc-button
>
</hass-error-screen> </hass-error-screen>
`; `;
} }
@ -107,6 +119,16 @@ class LovelacePanel extends LitElement {
public firstUpdated() { public firstUpdated() {
this._fetchConfig(false); 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._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => { this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`); const mql = matchMedia(`(min-width: ${width}px)`);
@ -155,6 +177,22 @@ class LovelacePanel extends LitElement {
); );
} }
private _lovelaceChanged() {
if (this._saving) {
this._saving = false;
} else {
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() { private _forceFetchConfig() {
this._fetchConfig(true); this._fetchConfig(true);
} }
@ -211,6 +249,7 @@ class LovelacePanel extends LitElement {
config: newConfig, config: newConfig,
mode: "storage", mode: "storage",
}); });
this._saving = true;
await saveConfig(this.hass!, newConfig); await saveConfig(this.hass!, newConfig);
} catch (err) { } catch (err) {
// tslint:disable-next-line // tslint:disable-next-line

View File

@ -31,9 +31,6 @@ declare global {
"iron-resize": undefined; "iron-resize": undefined;
"config-refresh": undefined; "config-refresh": undefined;
"ha-refresh-cloud-status": undefined; "ha-refresh-cloud-status": undefined;
"hass-notification": {
message: string;
};
"hass-api-called": { "hass-api-called": {
success: boolean; success: boolean;
response: unknown; response: unknown;

View File

@ -1,37 +1,31 @@
import { Constructor, LitElement } from "lit-element"; import { Constructor, LitElement } from "lit-element";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
import { HaToast } from "../components/ha-toast"; import { showToast } from "../util/toast";
import { computeRTL } from "../common/util/compute_rtl";
export default (superClass: Constructor<LitElement & HassBaseEl>) => export default (superClass: Constructor<LitElement & HassBaseEl>) =>
class extends superClass { class extends superClass {
private _discToast?: HaToast;
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
// Need to load in advance because when disconnected, can't dynamically load code. // Need to load in advance because when disconnected, can't dynamically load code.
import(/* webpackChunkName: "ha-toast" */ "../components/ha-toast"); import(/* webpackChunkName: "notification-manager" */ "../managers/notification-manager");
} }
protected hassReconnected() { protected hassReconnected() {
super.hassReconnected(); super.hassReconnected();
if (this._discToast) {
this._discToast.opened = false; showToast(this, {
} message: "",
duration: 1,
});
} }
protected hassDisconnected() { protected hassDisconnected() {
super.hassDisconnected(); super.hassDisconnected();
if (!this._discToast) {
const el = document.createElement("ha-toast"); showToast(this, {
el.duration = 0; message: this.hass!.localize("ui.notification_toast.connection_lost"),
this._discToast = el; duration: 0,
this.shadowRoot!.appendChild(el as any); dismissable: false,
} });
this._discToast.dir = computeRTL(this.hass!);
this._discToast.text = this.hass!.localize(
"ui.notification_toast.connection_lost"
);
this._discToast.opened = true;
} }
}; };

View File

@ -999,7 +999,12 @@
"warning": { "warning": {
"entity_not_found": "Entity not available: {entity}", "entity_not_found": "Entity not available: {entity}",
"entity_non_numeric": "Entity is non-numeric: {entity}" "entity_non_numeric": "Entity is non-numeric: {entity}"
} },
"changed_toast": {
"message": "The Lovelace config was updated, would you like to refresh?",
"refresh": "Refresh"
},
"reload_lovelace": "Reload Lovelace"
}, },
"mailbox": { "mailbox": {
"empty": "You do not have any messages", "empty": "You do not have any messages",

View File

@ -1,3 +1,6 @@
import { HassElement } from "../state/hass-element";
import { showToast } from "./toast";
export const registerServiceWorker = (notifyUpdate = true) => { export const registerServiceWorker = (notifyUpdate = true) => {
if ( if (
!("serviceWorker" in navigator) || !("serviceWorker" in navigator) ||
@ -20,9 +23,19 @@ export const registerServiceWorker = (notifyUpdate = true) => {
!__DEMO__ !__DEMO__
) { ) {
// Notify users here of a new frontend being available. // Notify users here of a new frontend being available.
import(/* webpackChunkName: "show-new-frontend-toast" */ "./show-new-frontend-toast").then( const haElement = window.document.querySelector(
(mod) => mod.default(installingWorker) "home-assistant, ha-onboarding"
); )! as HassElement;
showToast(haElement, {
message: "A new version of the frontend is available.",
action: {
action: () =>
installingWorker.postMessage({ type: "skipWaiting" }),
text: "reload",
},
duration: 0,
dismissable: false,
});
} }
}); });
}); });

View File

@ -1,20 +0,0 @@
import "@material/mwc-button";
import "../components/ha-toast";
export default (installingWorker) => {
const toast = document.createElement("ha-toast");
toast.opened = true;
toast.text = "A new version of the frontend is available.";
toast.duration = 0;
const button = document.createElement("mwc-button");
button.addEventListener("click", () =>
installingWorker.postMessage({ type: "skipWaiting" })
);
button.style.color = "var(--primary-color)";
button.style.fontWeight = "bold";
button.label = "reload";
toast.appendChild(button);
document.body.appendChild(toast);
};