diff --git a/build-scripts/gulp/gen-icons.js b/build-scripts/gulp/gen-icons.js index bfa7d7cc77..11548794c4 100644 --- a/build-scripts/gulp/gen-icons.js +++ b/build-scripts/gulp/gen-icons.js @@ -22,6 +22,7 @@ const BUILT_IN_PANEL_ICONS = [ "mailbox", // Mailbox "tooltip-account", // Map "cart", // Shopping List + "hammer", // developer-tools ]; // Given an icon name, load the SVG file diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 22ce4caea1..0d030a5479 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -36,6 +36,7 @@ customElements.get("paper-icon-button").prototype._keyBindings = {}; class HassioMain extends ProvideHassLitMixin(HassRouterPage) { @property() public hass!: HomeAssistant; @property() public panel!: HassioPanelInfo; + @property() public narrow!: boolean; protected routerOptions: RouterOptions = { // Hass.io has a page with tabs, so we route all non-matching routes to it. @@ -108,6 +109,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { // As long as we have Polymer pages (el as PolymerElement).setProperties({ hass: this.hass, + narrow: this.narrow, supervisorInfo: this._supervisorInfo, hostInfo: this._hostInfo, hassInfo: this._hassInfo, @@ -115,6 +117,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { }); } else { el.hass = this.hass; + el.narrow = this.narrow; el.supervisorInfo = this._supervisorInfo; el.hostInfo = this._hostInfo; el.hassInfo = this._hassInfo; diff --git a/hassio/src/hassio-pages-with-tabs.ts b/hassio/src/hassio-pages-with-tabs.ts index adb43d373f..018fd2f4d3 100644 --- a/hassio/src/hassio-pages-with-tabs.ts +++ b/hassio/src/hassio-pages-with-tabs.ts @@ -34,6 +34,7 @@ const HAS_REFRESH_BUTTON = ["store", "snapshots"]; @customElement("hassio-pages-with-tabs") class HassioPagesWithTabs extends LitElement { @property() public hass!: HomeAssistant; + @property() public narrow!: boolean; @property() public route!: Route; @property() public supervisorInfo!: HassioSupervisorInfo; @property() public hostInfo!: HassioHostInfo; @@ -45,7 +46,11 @@ class HassioPagesWithTabs extends LitElement { - +
Hass.io
${HAS_REFRESH_BUTTON.includes(page) ? html` diff --git a/setup.py b/setup.py index 0be5e29558..32fae0f514 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190627.0", + version="20190630.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index afe0df84ac..8f87caca18 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -102,6 +102,7 @@ export class HaStateLabelBadge extends LitElement { switch (domain) { case "binary_sensor": case "device_tracker": + case "person": case "updater": case "sun": case "alarm_control_panel": diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts index 6c4d55adf7..1930eb608e 100644 --- a/src/components/ha-menu-button.ts +++ b/src/components/ha-menu-button.ts @@ -5,34 +5,113 @@ import { LitElement, html, customElement, + CSSResult, + css, } from "lit-element"; import { fireEvent } from "../common/dom/fire_event"; +import { HomeAssistant } from "../types"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { subscribeNotifications } from "../data/persistent_notification"; +import computeDomain from "../common/entity/compute_domain"; @customElement("ha-menu-button") class HaMenuButton extends LitElement { - @property({ type: Boolean }) - public hassio = false; + @property({ type: Boolean }) public hassio = false; + @property() public narrow!: boolean; + @property() public hass!: HomeAssistant; + @property() private _hasNotifications = false; + private _attachNotifOnConnect = false; + private _unsubNotifications?: UnsubscribeFunc; + + public connectedCallback() { + super.connectedCallback(); + if (this._attachNotifOnConnect) { + this._attachNotifOnConnect = false; + this._subscribeNotifications(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._unsubNotifications) { + this._attachNotifOnConnect = true; + this._unsubNotifications(); + this._unsubNotifications = undefined; + } + } protected render(): TemplateResult | void { + const hasNotifications = + this.narrow && + (this._hasNotifications || + Object.keys(this.hass.states).some( + (entityId) => computeDomain(entityId) === "configurator" + )); return html` + ${hasNotifications + ? html` +
+ ` + : ""} `; } - // We are not going to use ShadowDOM as we're rendering a single element - // without any CSS used. - protected createRenderRoot(): Element | ShadowRoot { - return this; + protected updated(changedProps) { + super.updated(changedProps); + + if (!changedProps.has("narrow")) { + return; + } + + this.style.visibility = this.narrow ? "initial" : "hidden"; + + if (!this.narrow) { + this._hasNotifications = false; + if (this._unsubNotifications) { + this._unsubNotifications(); + this._unsubNotifications = undefined; + } + return; + } + + this._subscribeNotifications(); + } + + private _subscribeNotifications() { + this._unsubNotifications = subscribeNotifications( + this.hass.connection, + (notifications) => { + this._hasNotifications = notifications.length > 0; + } + ); } private _toggleMenu(): void { fireEvent(this, "hass-toggle-menu"); } + + static get styles(): CSSResult { + return css` + :host { + position: relative; + } + .dot { + position: absolute; + background-color: var(--accent-color); + width: 12px; + height: 12px; + top: 8px; + right: 5px; + border-radius: 50%; + } + `; + } } declare global { diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 8bb0bcc131..341892caf2 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -14,7 +14,7 @@ import "@polymer/paper-listbox/paper-listbox"; import "./ha-icon"; import "../components/user/ha-user-badge"; -import isComponentLoaded from "../common/config/is_component_loaded"; +import "../components/ha-menu-button"; import { HomeAssistant, PanelInfo } from "../types"; import { fireEvent } from "../common/dom/fire_event"; import { DEFAULT_PANEL } from "../common/const"; @@ -22,48 +22,70 @@ import { getExternalConfig, ExternalConfig, } from "../external_app/external_config"; +import { + PersistentNotification, + subscribeNotifications, +} from "../data/persistent_notification"; +import computeDomain from "../common/entity/compute_domain"; + +const SHOW_AFTER_SPACER = ["config", "developer-tools"]; const computeUrl = (urlPath) => `/${urlPath}`; -const computePanels = (hass: HomeAssistant) => { +const SORT_VALUE = { + map: 1, + logbook: 2, + history: 3, + "developer-tools": 9, + configuration: 10, +}; + +const panelSorter = (a, b) => { + const aBuiltIn = a.component_name in SORT_VALUE; + const bBuiltIn = b.component_name in SORT_VALUE; + + if (aBuiltIn && bBuiltIn) { + return SORT_VALUE[a.component_name] - SORT_VALUE[b.component_name]; + } + if (aBuiltIn) { + return -1; + } + if (bBuiltIn) { + return 1; + } + // both not built in, sort by title + if (a.title! < b.title!) { + return -1; + } + if (a.title! > b.title!) { + return 1; + } + return 0; +}; + +const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => { const panels = hass.panels; if (!panels) { - return []; + return [[], []]; } - const sortValue = { - map: 1, - logbook: 2, - history: 3, - }; - const result: PanelInfo[] = Object.values(panels).filter( - (panel) => panel.title - ); + const beforeSpacer: PanelInfo[] = []; + const afterSpacer: PanelInfo[] = []; - result.sort((a, b) => { - const aBuiltIn = a.component_name in sortValue; - const bBuiltIn = b.component_name in sortValue; - - if (aBuiltIn && bBuiltIn) { - return sortValue[a.component_name] - sortValue[b.component_name]; + Object.values(panels).forEach((panel) => { + if (!panel.title) { + return; } - if (aBuiltIn) { - return -1; - } - if (bBuiltIn) { - return 1; - } - // both not built in, sort by title - if (a.title! < b.title!) { - return -1; - } - if (a.title! > b.title!) { - return 1; - } - return 0; + (SHOW_AFTER_SPACER.includes(panel.component_name) + ? afterSpacer + : beforeSpacer + ).push(panel); }); - return result; + beforeSpacer.sort(panelSorter); + afterSpacer.sort(panelSorter); + + return [beforeSpacer, afterSpacer]; }; const renderPanel = (hass, panel) => html` @@ -86,12 +108,15 @@ const renderPanel = (hass, panel) => html` * @appliesMixin LocalizeMixin */ class HaSidebar extends LitElement { - @property() public hass?: HomeAssistant; + @property() public hass!: HomeAssistant; + @property() public narrow!: boolean; + @property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean, reflect: true }) public expanded = false; @property() public _defaultPage?: string = localStorage.defaultPage || DEFAULT_PANEL; @property() private _externalConfig?: ExternalConfig; + @property() private _notifications?: PersistentNotification[]; protected render() { const hass = this.hass; @@ -100,29 +125,30 @@ class HaSidebar extends LitElement { return html``; } - const panels = computePanels(hass); - const configPanelIdx = panels.findIndex( - (panel) => panel.component_name === "config" - ); - const configPanel = - configPanelIdx === -1 ? undefined : panels.splice(configPanelIdx, 1)[0]; + const [beforeSpacer, afterSpacer] = computePanels(hass); + + let notificationCount = this._notifications + ? this._notifications.length + : 0; + for (const entityId in hass.states) { + if (computeDomain(entityId) === "configurator") { + notificationCount++; + } + } return html` - ${this.expanded - ? html` - -
Home Assistant
-
- ` - : html` - - `} + - ${panels.map((panel) => renderPanel(hass, panel))} + ${beforeSpacer.map((panel) => renderPanel(hass, panel))}
- ${this.expanded && hass.user && hass.user.is_admin - ? html` -
- -
- ${hass.localize("ui.sidebar.developer_tools")} -
- -
- - - - - - - - - - - - - ${isComponentLoaded(hass, "mqtt") - ? html` - - - - ` - : html``} - - - -
-
- ` - : ""} + ${afterSpacer.map((panel) => renderPanel(hass, panel))} ${this._externalConfig && this._externalConfig.hasSettingsScreen ? html` - ${hass.localize( - "ui.sidebar.external_app_configuration" - )} - - - ` - : ""} - ${configPanel ? renderPanel(hass, configPanel) : ""} - ${hass.user - ? html` - - - - - ${hass.user.name} + ${hass.localize("ui.sidebar.external_app_configuration")} ` - : html` - - - ${hass.localize("ui.sidebar.log_out")} - - `} + : ""} + +
+ + + + ${notificationCount > 0 + ? html` + + ${notificationCount} + + ` + : ""} + + ${hass.localize("ui.notification_drawer.title")} + + + + + + + + + ${hass.user ? hass.user.name : ""} + + +
`; } protected shouldUpdate(changedProps: PropertyValues): boolean { if ( - changedProps.has("_externalConfig") || changedProps.has("expanded") || - changedProps.has("alwaysExpand") + changedProps.has("narrow") || + changedProps.has("alwaysExpand") || + changedProps.has("_externalConfig") || + changedProps.has("_notifications") ) { return true; } @@ -280,9 +251,9 @@ class HaSidebar extends LitElement { return ( hass.panels !== oldHass.panels || hass.panelUrl !== oldHass.panelUrl || - hass.config.components !== oldHass.config.components || hass.user !== oldHass.user || - hass.localize !== oldHass.localize + hass.localize !== oldHass.localize || + hass.states !== oldHass.states ); } @@ -293,15 +264,15 @@ class HaSidebar extends LitElement { this._externalConfig = conf; }); } - this.shadowRoot!.querySelector("paper-listbox")!.addEventListener( - "mouseenter", - () => { - this.expanded = true; - } - ); + this.addEventListener("mouseenter", () => { + this.expanded = true; + }); this.addEventListener("mouseleave", () => { this._contract(); }); + subscribeNotifications(this.hass.connection, (notifications) => { + this._notifications = notifications; + }); } protected updated(changedProps) { @@ -315,17 +286,21 @@ class HaSidebar extends LitElement { this.expanded = this.alwaysExpand || false; } - private _handleLogOut() { - fireEvent(this, "hass-logout"); + private _handleShowNotificationDrawer() { + fireEvent(this, "hass-show-notifications"); } private _handleExternalAppConfiguration(ev: Event) { ev.preventDefault(); - this.hass!.auth.external!.fireMessage({ + this.hass.auth.external!.fireMessage({ type: "config_screen/show", }); } + private _toggleSidebar() { + fireEvent(this, "hass-toggle-menu"); + } + static get styles(): CSSResult { return css` :host { @@ -344,31 +319,41 @@ class HaSidebar extends LitElement { transition: width 0.2s ease-in; will-change: width; contain: strict; + transition-delay: 0.2s; } :host([expanded]) { width: 256px; } - .logo { - height: 65px; - box-sizing: border-box; - padding: 8px; + .menu { + height: 64px; + display: flex; + padding: 0 12px; border-bottom: 1px solid transparent; - } - .logo img { - width: 48px; - } - - app-toolbar { white-space: nowrap; font-weight: 400; color: var(--primary-text-color); border-bottom: 1px solid var(--divider-color); background-color: var(--primary-background-color); + font-size: 20px; + align-items: center; + } + :host([expanded]) .menu { + width: 256px; } - app-toolbar a { - color: var(--primary-text-color); + .menu paper-icon-button { + color: var(--sidebar-icon-color); + } + :host([expanded]) .menu paper-icon-button { + margin-right: 23px; + } + + .title { + display: none; + } + :host([expanded]) .title { + display: initial; } paper-listbox { @@ -435,35 +420,69 @@ class HaSidebar extends LitElement { color: var(--sidebar-selected-text-color); } - a .item-text { + paper-icon-item .item-text { display: none; } - :host([expanded]) a .item-text { + :host([expanded]) paper-icon-item .item-text { display: block; } - paper-icon-item.logout { - margin-top: 16px; + .divider { + bottom: 88px; + padding: 10px 0; } - paper-icon-item.profile { + .divider::before { + content: " "; + display: block; + height: 1px; + background-color: var(--divider-color); + } + + .notifications { + margin-top: 0; + margin-bottom: 0; + bottom: 48px; + cursor: pointer; + } + .profile { + bottom: 0; + } + .profile paper-icon-item { padding-left: 4px; } .profile .item-text { margin-left: 8px; } + .sticky-el { + position: sticky; + background-color: var( + --sidebar-background-color, + var(--primary-background-color) + ); + } + + .notification-badge { + position: absolute; + font-weight: 400; + bottom: 14px; + left: 26px; + border-radius: 50%; + background-color: var(--primary-color); + height: 20px; + line-height: 20px; + text-align: center; + padding: 0px 6px; + font-size: 0.65em; + color: var(--text-primary-color); + } + .spacer { flex: 1; pointer-events: none; } - .divider { - height: 1px; - background-color: var(--divider-color); - margin: 4px 0; - } - .subheader { color: var(--sidebar-text-color); font-weight: 500; diff --git a/src/data/persistent_notification.ts b/src/data/persistent_notification.ts new file mode 100644 index 0000000000..1bfc33bd34 --- /dev/null +++ b/src/data/persistent_notification.ts @@ -0,0 +1,43 @@ +import { + createCollection, + Connection, + HassEntity, +} from "home-assistant-js-websocket"; + +export interface PersitentNotificationEntity extends HassEntity { + notification_id?: string; + created_at?: string; + title?: string; + message?: string; +} + +export interface PersistentNotification { + created_at: string; + message: string; + notification_id: string; + title: string; + status: "read" | "unread"; +} + +const fetchNotifications = (conn) => + conn.sendMessagePromise({ + type: "persistent_notification/get", + }); + +const subscribeUpdates = (conn, store) => + conn.subscribeEvents( + () => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)), + "persistent_notifications_updated" + ); + +export const subscribeNotifications = ( + conn: Connection, + onChange: (notifications: PersistentNotification[]) => void +) => + createCollection( + "_ntf", + fetchNotifications, + subscribeUpdates, + conn, + onChange + ); diff --git a/src/data/ws-notifications.ts b/src/data/ws-notifications.ts deleted file mode 100644 index 261a32aac6..0000000000 --- a/src/data/ws-notifications.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createCollection, Connection } from "home-assistant-js-websocket"; - -const fetchNotifications = (conn) => - conn.sendMessagePromise({ - type: "persistent_notification/get", - }); - -const subscribeUpdates = (conn, store) => - conn.subscribeEvents( - () => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)), - "persistent_notifications_updated" - ); - -export const subscribeNotifications = ( - conn: Connection, - onChange: (notifications: Notification[]) => void -) => - createCollection( - "_ntf", - fetchNotifications, - subscribeUpdates, - conn, - onChange - ); diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index 0b92d5f95a..07544eeeb2 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -26,9 +26,20 @@ class MoreInfoCamera extends LitElement { @property() public hass?: HomeAssistant; @property() public stateObj?: CameraEntity; @property() private _cameraPrefs?: CameraPreferences; + @property() private _attached = false; + + public connectedCallback() { + super.connectedCallback(); + this._attached = true; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._attached = false; + } protected render(): TemplateResult | void { - if (!this.hass || !this.stateObj) { + if (!this._attached || !this.hass || !this.stateObj) { return html``; } diff --git a/src/panels/lovelace/components/notifications/hui-configurator-notification-item.ts b/src/dialogs/notifications/configurator-notification-item.ts similarity index 69% rename from src/panels/lovelace/components/notifications/hui-configurator-notification-item.ts rename to src/dialogs/notifications/configurator-notification-item.ts index d0394c46a2..e43695dc55 100644 --- a/src/panels/lovelace/components/notifications/hui-configurator-notification-item.ts +++ b/src/dialogs/notifications/configurator-notification-item.ts @@ -7,17 +7,17 @@ import { } from "lit-element"; import "@material/mwc-button"; -import "./hui-notification-item-template"; +import "./notification-item-template"; -import { HomeAssistant } from "../../../../types"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { HassNotification } from "./types"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import { PersitentNotificationEntity } from "../../data/persistent_notification"; -@customElement("hui-configurator-notification-item") +@customElement("configurator-notification-item") export class HuiConfiguratorNotificationItem extends LitElement { @property() public hass?: HomeAssistant; - @property() public notification?: HassNotification; + @property() public notification?: PersitentNotificationEntity; protected render(): TemplateResult | void { if (!this.hass || !this.notification) { @@ -25,7 +25,7 @@ export class HuiConfiguratorNotificationItem extends LitElement { } return html` - + ${this.hass.localize("domain.configurator")}
@@ -41,7 +41,7 @@ export class HuiConfiguratorNotificationItem extends LitElement { `state.configurator.${this.notification.state}` )} - + `; } @@ -54,6 +54,6 @@ export class HuiConfiguratorNotificationItem extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-configurator-notification-item": HuiConfiguratorNotificationItem; + "configurator-notification-item": HuiConfiguratorNotificationItem; } } diff --git a/src/panels/lovelace/components/notifications/hui-notification-drawer.js b/src/dialogs/notifications/notification-drawer.js similarity index 72% rename from src/panels/lovelace/components/notifications/hui-notification-drawer.js rename to src/dialogs/notifications/notification-drawer.js index a77d6c4165..f86ce08a4e 100644 --- a/src/panels/lovelace/components/notifications/hui-notification-drawer.js +++ b/src/dialogs/notifications/notification-drawer.js @@ -5,13 +5,14 @@ import "@polymer/app-layout/app-toolbar/app-toolbar"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "./hui-notification-item"; -import "../../../../components/ha-paper-icon-button-next"; - -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixin from "../../../../mixins/localize-mixin"; -import { computeRTL } from "../../../../common/util/compute_rtl"; +import "./notification-item"; +import "../../components/ha-paper-icon-button-next"; +import { EventsMixin } from "../../mixins/events-mixin"; +import LocalizeMixin from "../../mixins/localize-mixin"; +import { computeRTL } from "../../common/util/compute_rtl"; +import { subscribeNotifications } from "../../data/persistent_notification"; +import computeDomain from "../../common/entity/compute_domain"; /* * @appliesMixin EventsMixin * @appliesMixin LocalizeMixin @@ -129,7 +130,7 @@ export class HuiNotificationDrawer extends EventsMixin( @@ -160,6 +161,10 @@ export class HuiNotificationDrawer extends EventsMixin( reflectToAttribute: true, }, notifications: { + type: Array, + computed: "_computeNotifications(open, hass, _notificationsBackend)", + }, + _notificationsBackend: { type: Array, value: [], }, @@ -171,6 +176,16 @@ export class HuiNotificationDrawer extends EventsMixin( }; } + ready() { + super.ready(); + window.addEventListener("location-changed", () => { + // close drawer when we navigate away. + if (this.open) { + this.open = false; + } + }); + } + _closeDrawer(ev) { ev.stopPropagation(); this.open = false; @@ -188,17 +203,44 @@ export class HuiNotificationDrawer extends EventsMixin( this._openTimer = setTimeout(() => { this.classList.add("open"); }, 50); + this._unsubNotifications = subscribeNotifications( + this.hass.connection, + (notifications) => { + this._notificationsBackend = notifications; + } + ); } else { // Animate closed then hide this.classList.remove("open"); this._openTimer = setTimeout(() => { this.hidden = true; }, 250); + if (this._unsubNotifications) { + this._unsubNotifications(); + this._unsubNotifications = undefined; + } } } _computeRTL(hass) { return computeRTL(hass); } + + _computeNotifications(open, hass, notificationsBackend) { + if (!open) { + return []; + } + + const configuratorEntities = Object.keys(hass.states) + .filter((entityId) => computeDomain(entityId) === "configurator") + .map((entityId) => hass.states[entityId]); + + return notificationsBackend.concat(configuratorEntities); + } + + showDialog({ narrow }) { + this.open = true; + this.narrow = narrow; + } } -customElements.define("hui-notification-drawer", HuiNotificationDrawer); +customElements.define("notification-drawer", HuiNotificationDrawer); diff --git a/src/panels/lovelace/components/notifications/hui-notification-item-template.ts b/src/dialogs/notifications/notification-item-template.ts similarity index 89% rename from src/panels/lovelace/components/notifications/hui-notification-item-template.ts rename to src/dialogs/notifications/notification-item-template.ts index 601eaa2b1b..ec1aa7c639 100644 --- a/src/panels/lovelace/components/notifications/hui-notification-item-template.ts +++ b/src/dialogs/notifications/notification-item-template.ts @@ -7,9 +7,9 @@ import { CSSResult, } from "lit-element"; -import "../../../../components/ha-card"; +import "../../components/ha-card"; -@customElement("hui-notification-item-template") +@customElement("notification-item-template") export class HuiNotificationItemTemplate extends LitElement { protected render(): TemplateResult | void { return html` @@ -60,6 +60,6 @@ export class HuiNotificationItemTemplate extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-notification-item-template": HuiNotificationItemTemplate; + "notification-item-template": HuiNotificationItemTemplate; } } diff --git a/src/panels/lovelace/components/notifications/hui-notification-item.ts b/src/dialogs/notifications/notification-item.ts similarity index 56% rename from src/panels/lovelace/components/notifications/hui-notification-item.ts rename to src/dialogs/notifications/notification-item.ts index 6d16335a51..541b1b4121 100644 --- a/src/panels/lovelace/components/notifications/hui-notification-item.ts +++ b/src/dialogs/notifications/notification-item.ts @@ -6,18 +6,19 @@ import { TemplateResult, html, } from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; -import "./hui-configurator-notification-item"; -import "./hui-persistent-notification-item"; +import "./configurator-notification-item"; +import "./persistent-notification-item"; -import { HomeAssistant } from "../../../../types"; -import { HassNotification } from "./types"; +import { HomeAssistant } from "../../types"; +import { PersistentNotification } from "../../data/persistent_notification"; -@customElement("hui-notification-item") +@customElement("notification-item") export class HuiNotificationItem extends LitElement { @property() public hass?: HomeAssistant; - @property() public notification?: HassNotification; + @property() public notification?: HassEntity | PersistentNotification; protected shouldUpdate(changedProps: PropertyValues): boolean { if (!this.hass || !this.notification || changedProps.has("notification")) { @@ -32,24 +33,24 @@ export class HuiNotificationItem extends LitElement { return html``; } - return this.notification.entity_id + return "entity_id" in this.notification ? html` - + > ` : html` - + > `; } } declare global { interface HTMLElementTagNameMap { - "hui-notification-item": HuiNotificationItem; + "notification-item": HuiNotificationItem; } } diff --git a/src/panels/lovelace/components/notifications/hui-persistent-notification-item.ts b/src/dialogs/notifications/persistent-notification-item.ts similarity index 73% rename from src/panels/lovelace/components/notifications/hui-persistent-notification-item.ts rename to src/dialogs/notifications/persistent-notification-item.ts index c03ec6fbc6..b40a743203 100644 --- a/src/panels/lovelace/components/notifications/hui-persistent-notification-item.ts +++ b/src/dialogs/notifications/persistent-notification-item.ts @@ -10,18 +10,18 @@ import { import "@material/mwc-button"; import "@polymer/paper-tooltip/paper-tooltip"; -import "../../../../components/ha-relative-time"; -import "../../../../components/ha-markdown"; -import "./hui-notification-item-template"; +import "../../components/ha-relative-time"; +import "../../components/ha-markdown"; +import "./notification-item-template"; -import { HomeAssistant } from "../../../../types"; -import { HassNotification } from "./types"; +import { HomeAssistant } from "../../types"; +import { PersistentNotification } from "../../data/persistent_notification"; -@customElement("hui-persistent-notification-item") +@customElement("persistent-notification-item") export class HuiPersistentNotificationItem extends LitElement { @property() public hass?: HomeAssistant; - @property() public notification?: HassNotification; + @property() public notification?: PersistentNotification; protected render(): TemplateResult | void { if (!this.hass || !this.notification) { @@ -29,8 +29,10 @@ export class HuiPersistentNotificationItem extends LitElement { } return html` - - ${this._computeTitle(this.notification)} + + + ${this.notification.title || this.notification.notification_id} + @@ -54,7 +56,7 @@ export class HuiPersistentNotificationItem extends LitElement { "ui.card.persistent_notification.dismiss" )} - + `; } @@ -80,13 +82,9 @@ export class HuiPersistentNotificationItem extends LitElement { }); } - private _computeTitle(notification: HassNotification): string | undefined { - return notification.title || notification.notification_id; - } - private _computeTooltip( hass: HomeAssistant, - notification: HassNotification + notification: PersistentNotification ): string | undefined { if (!hass || !notification) { return undefined; @@ -105,6 +103,6 @@ export class HuiPersistentNotificationItem extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-persistent-notification-item": HuiPersistentNotificationItem; + "persistent-notification-item": HuiPersistentNotificationItem; } } diff --git a/src/dialogs/notifications/show-notification-drawer.ts b/src/dialogs/notifications/show-notification-drawer.ts new file mode 100644 index 0000000000..5d4902f64a --- /dev/null +++ b/src/dialogs/notifications/show-notification-drawer.ts @@ -0,0 +1,16 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface NotificationDrawerParams { + narrow: boolean; +} + +export const showNotificationDrawer = ( + element: HTMLElement, + dialogParams: NotificationDrawerParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "notification-drawer" as any, // Not in TS yet + dialogImport: () => import("./notification-drawer"), + dialogParams, + }); +}; diff --git a/src/layouts/hass-loading-screen.ts b/src/layouts/hass-loading-screen.ts index 28c06a7d16..c2ece403cc 100644 --- a/src/layouts/hass-loading-screen.ts +++ b/src/layouts/hass-loading-screen.ts @@ -12,17 +12,23 @@ import { import "../components/ha-menu-button"; import "../components/ha-paper-icon-button-arrow-prev"; import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; @customElement("hass-loading-screen") class HassLoadingScreen extends LitElement { @property({ type: Boolean }) public rootnav? = false; + @property() public hass?: HomeAssistant; + @property() public narrow?: boolean; protected render(): TemplateResult | void { return html` ${this.rootnav ? html` - + ` : html` - ${this.root - ? html` - - ` - : html` - - `} +
${this.header}
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index d2631cd825..480b8cc4f4 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -20,6 +20,7 @@ import { fireEvent } from "../common/dom/fire_event"; import { PolymerChangedEvent } from "../polymer-types"; // tslint:disable-next-line: no-duplicate-imports import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout"; +import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; const NON_SWIPABLE_PANELS = ["kiosk", "map"]; @@ -27,6 +28,7 @@ declare global { // for fire event interface HASSDomEvents { "hass-toggle-menu": undefined; + "hass-show-notifications": undefined; } } @@ -66,6 +68,7 @@ class HomeAssistantMain extends LitElement { > @@ -96,6 +99,12 @@ class HomeAssistantMain extends LitElement { setTimeout(() => this.appLayout.resetLayout()); } }); + + this.addEventListener("hass-show-notifications", () => { + showNotificationDrawer(this, { + narrow: this.narrow!, + }); + }); } protected updated(changedProps: PropertyValues) { diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index c1c0d7af39..d509aa136a 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -9,7 +9,7 @@ import { } from "./hass-router-page"; import { removeInitSkeleton } from "../util/init-skeleton"; -const CACHE_COMPONENTS = ["lovelace", "states"]; +const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"]; const COMPONENTS = { calendar: () => import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-calendar"), @@ -17,18 +17,8 @@ const COMPONENTS = { import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"), custom: () => import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"), - "dev-event": () => - import(/* webpackChunkName: "panel-dev-event" */ "../panels/dev-event/ha-panel-dev-event"), - "dev-info": () => - import(/* webpackChunkName: "panel-dev-info" */ "../panels/dev-info/ha-panel-dev-info"), - "dev-mqtt": () => - import(/* webpackChunkName: "panel-dev-mqtt" */ "../panels/dev-mqtt/ha-panel-dev-mqtt"), - "dev-service": () => - import(/* webpackChunkName: "panel-dev-service" */ "../panels/dev-service/ha-panel-dev-service"), - "dev-state": () => - import(/* webpackChunkName: "panel-dev-state" */ "../panels/dev-state/ha-panel-dev-state"), - "dev-template": () => - import(/* webpackChunkName: "panel-dev-template" */ "../panels/dev-template/ha-panel-dev-template"), + "developer-tools": () => + import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"), lovelace: () => import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"), states: () => @@ -52,14 +42,17 @@ const COMPONENTS = { }; const getRoutes = (panels: Panels): RouterOptions => { - const routes: { [route: string]: RouteOptions } = {}; + const routes: RouterOptions["routes"] = {}; Object.values(panels).forEach((panel) => { - routes[panel.url_path] = { - load: COMPONENTS[panel.component_name], + const data: RouteOptions = { tag: `ha-panel-${panel.component_name}`, cache: CACHE_COMPONENTS.includes(panel.component_name), }; + if (panel.component_name in COMPONENTS) { + data.load = COMPONENTS[panel.component_name]; + } + routes[panel.url_path] = data; }); return { @@ -93,6 +86,8 @@ class PartialPanelResolver extends HassRouterPage { protected createLoadingScreen() { const el = super.createLoadingScreen(); el.rootnav = true; + el.hass = this.hass; + el.narrow = this.narrow; return el; } diff --git a/src/panels/calendar/ha-panel-calendar.js b/src/panels/calendar/ha-panel-calendar.js index 682162b732..a22a502c5e 100644 --- a/src/panels/calendar/ha-panel-calendar.js +++ b/src/panels/calendar/ha-panel-calendar.js @@ -67,7 +67,10 @@ class HaPanelCalendar extends LocalizeMixin(PolymerElement) { - +
[[localize('panel.calendar')]]
diff --git a/src/panels/config/dashboard/ha-config-dashboard.js b/src/panels/config/dashboard/ha-config-dashboard.js index 0e31d22255..edd8f16daf 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.js +++ b/src/panels/config/dashboard/ha-config-dashboard.js @@ -47,7 +47,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) { - +
[[localize('panel.config')]]
@@ -125,6 +125,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) { static get properties() { return { hass: Object, + narrow: Boolean, isWide: Boolean, cloudStatus: Object, showAdvanced: Boolean, diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 2beebc7d06..14a719b93d 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -9,6 +9,7 @@ import { CoreFrontendUserData, getOptimisticFrontendUserDataCollection, } from "../../data/frontend"; +import { PolymerElement } from "@polymer/polymer"; declare global { // for fire event @@ -142,12 +143,29 @@ class HaPanelConfig extends HassRouterPage { } protected updatePageEl(el) { - el.route = this.routeTail; - el.hass = this.hass; - el.showAdvanced = !!(this._coreUserData && this._coreUserData.showAdvanced); - el.isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide; - el.narrow = this.narrow; - el.cloudStatus = this._cloudStatus; + const showAdvanced = !!( + this._coreUserData && this._coreUserData.showAdvanced + ); + const isWide = this.hass.dockedSidebar ? this._wideSidebar : this._wide; + + if ("setProperties" in el) { + // As long as we have Polymer panels + (el as PolymerElement).setProperties({ + route: this.routeTail, + hass: this.hass, + showAdvanced, + isWide, + narrow: this.narrow, + cloudStatus: this._cloudStatus, + }); + } else { + el.route = this.routeTail; + el.hass = this.hass; + el.showAdvanced = showAdvanced; + el.isWide = isWide; + el.narrow = this.narrow; + el.cloudStatus = this._cloudStatus; + } } private async _updateCloudStatus() { diff --git a/src/panels/dev-info/ha-panel-dev-info.ts b/src/panels/dev-info/ha-panel-dev-info.ts deleted file mode 100644 index 970030790f..0000000000 --- a/src/panels/dev-info/ha-panel-dev-info.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - LitElement, - html, - PropertyDeclarations, - CSSResult, - css, - TemplateResult, -} from "lit-element"; -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "../../components/ha-menu-button"; - -import { HomeAssistant } from "../../types"; -import { haStyle } from "../../resources/styles"; - -import "./system-log-card"; -import "./error-log-card"; -import "./system-health-card"; - -const JS_VERSION = __BUILD__; -const OPT_IN_PANEL = "states"; - -class HaPanelDevInfo extends LitElement { - public hass?: HomeAssistant; - - static get properties(): PropertyDeclarations { - return { - hass: {}, - }; - } - - protected render(): TemplateResult | void { - const hass = this.hass; - if (!hass) { - return html``; - } - const customUiList: Array<{ name: string; url: string; version: string }> = - (window as any).CUSTOM_UI_LIST || []; - - const nonDefaultLink = - localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states" - ? "/lovelace" - : "/states"; - - const nonDefaultLinkText = - localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states" - ? "Go to the Lovelace UI" - : "Go to the states UI"; - - const defaultPageText = `${ - localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set" - } ${OPT_IN_PANEL} as default page on this device`; - - return html` - - - - -
About
-
-
- -
-
-

- - Home Assistant logo - -
- Home Assistant
- ${hass.config.version} -

-

- Path to configuration.yaml: ${hass.config.config_dir} -

-

- - Developed by a bunch of awesome people. - -

-

- Published under the Apache 2.0 license
- Source: - server - — - frontend-ui -

-

- Built using - Python 3, - Polymer, Icons by - Google - and - MaterialDesignIcons.com. -

-

- Frontend JavaScript version: ${JS_VERSION} - ${customUiList.length > 0 - ? html` -

- Custom UIs: - ${customUiList.map( - (item) => html` -
- - ${item.name}: ${item.version} -
- ` - )} -
- ` - : ""} -

-

- ${nonDefaultLinkText}
- - ${defaultPageText} - -

-
- - - -
-
- `; - } - - protected firstUpdated(changedProps): void { - super.firstUpdated(changedProps); - - // Legacy custom UI can be slow to register, give them time. - const customUI = ((window as any).CUSTOM_UI_LIST || []).length; - setTimeout(() => { - if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) { - this.requestUpdate(); - } - }, 1000); - } - - protected _toggleDefaultPage(): void { - if (localStorage.defaultPage === OPT_IN_PANEL) { - delete localStorage.defaultPage; - } else { - localStorage.defaultPage = OPT_IN_PANEL; - } - this.requestUpdate(); - } - - static get styles(): CSSResult[] { - return [ - haStyle, - css` - :host { - -ms-user-select: initial; - -webkit-user-select: initial; - -moz-user-select: initial; - } - - .content { - padding: 16px 0px 16px 0; - direction: ltr; - } - - .about { - text-align: center; - line-height: 2em; - } - - .version { - @apply --paper-font-headline; - } - - .develop { - @apply --paper-font-subhead; - } - - .about a { - color: var(--dark-primary-color); - } - - system-health-card { - display: block; - max-width: 600px; - margin: 0 auto; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-panel-dev-info": HaPanelDevInfo; - } -} - -customElements.define("ha-panel-dev-info", HaPanelDevInfo); diff --git a/src/panels/dev-mqtt/ha-panel-dev-mqtt.js b/src/panels/dev-mqtt/ha-panel-dev-mqtt.js deleted file mode 100644 index 98bca44d17..0000000000 --- a/src/panels/dev-mqtt/ha-panel-dev-mqtt.js +++ /dev/null @@ -1,89 +0,0 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../components/ha-card"; -import "../../components/ha-menu-button"; -import "../../resources/ha-style"; -import "../../util/app-localstorage-document"; - -class HaPanelDevMqtt extends PolymerElement { - static get template() { - return html` - - - - - - -
MQTT
-
-
- - - - - - -
- -
- - - -
-
- Publish -
-
-
-
- `; - } - - static get properties() { - return { - hass: Object, - topic: String, - payload: String, - }; - } - - _publish() { - this.hass.callService("mqtt", "publish", { - topic: this.topic, - payload_template: this.payload, - }); - } -} - -customElements.define("ha-panel-dev-mqtt", HaPanelDevMqtt); diff --git a/src/panels/developer-tools/developer-tools-router.ts b/src/panels/developer-tools/developer-tools-router.ts new file mode 100644 index 0000000000..924c82232e --- /dev/null +++ b/src/panels/developer-tools/developer-tools-router.ts @@ -0,0 +1,68 @@ +import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; +import { customElement, property } from "lit-element"; +import { PolymerElement } from "@polymer/polymer"; +import { HomeAssistant } from "../../types"; + +@customElement("developer-tools-router") +class DeveloperToolsRouter extends HassRouterPage { + @property() public hass!: HomeAssistant; + @property() public narrow!: boolean; + + protected routerOptions: RouterOptions = { + // defaultPage: "info", + beforeRender: (page) => { + if (!page || page === "not_found") { + // If we can, we are going to restore the last visited page. + return this._currentPage ? this._currentPage : "info"; + } + return undefined; + }, + cacheAll: true, + showLoading: true, + routes: { + event: { + tag: "developer-tools-event", + load: () => import("./event/developer-tools-event"), + }, + info: { + tag: "developer-tools-info", + load: () => import("./info/developer-tools-info"), + }, + mqtt: { + tag: "developer-tools-mqtt", + load: () => import("./mqtt/developer-tools-mqtt"), + }, + service: { + tag: "developer-tools-service", + load: () => import("./service/developer-tools-service"), + }, + state: { + tag: "developer-tools-state", + load: () => import("./state/developer-tools-state"), + }, + template: { + tag: "developer-tools-template", + load: () => import("./template/developer-tools-template"), + }, + }, + }; + + protected updatePageEl(el) { + if ("setProperties" in el) { + // As long as we have Polymer pages + (el as PolymerElement).setProperties({ + hass: this.hass, + narrow: this.narrow, + }); + } else { + el.hass = this.hass; + el.narrow = this.narrow; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-router": DeveloperToolsRouter; + } +} diff --git a/src/panels/dev-event/ha-panel-dev-event.js b/src/panels/developer-tools/event/developer-tools-event.js similarity index 55% rename from src/panels/dev-event/ha-panel-dev-event.js rename to src/panels/developer-tools/event/developer-tools-event.js index d5cc9d9551..dd772af390 100644 --- a/src/panels/dev-event/ha-panel-dev-event.js +++ b/src/panels/developer-tools/event/developer-tools-event.js @@ -1,6 +1,3 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; @@ -8,11 +5,10 @@ import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/ha-menu-button"; -import "../../resources/ha-style"; +import "../../../resources/ha-style"; import "./events-list"; import "./event-subscribe-card"; -import { EventsMixin } from "../../mixins/events-mixin"; +import { EventsMixin } from "../../../mixins/events-mixin"; /* * @appliesMixin EventsMixin @@ -26,12 +22,10 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { -ms-user-select: initial; -webkit-user-select: initial; -moz-user-select: initial; - } - - .content { @apply --paper-font-body1; padding: 16px; direction: ltr; + display: block; } .ha-form { @@ -49,45 +43,34 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { } - - - - -
Events
-
-
+
+
+

Fire an event on the event bus.

-
-
-
-

Fire an event on the event bus.

- -
- - - Fire Event -
-
- -
-
Available Events
- -
+
+ + + Fire Event
-
- + +
+
Available Events
+ +
+
+ `; } @@ -139,4 +122,4 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { } } -customElements.define("ha-panel-dev-event", HaPanelDevEvent); +customElements.define("developer-tools-event", HaPanelDevEvent); diff --git a/src/panels/dev-event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts similarity index 93% rename from src/panels/dev-event/event-subscribe-card.ts rename to src/panels/developer-tools/event/event-subscribe-card.ts index cc0196b235..4b56290f86 100644 --- a/src/panels/dev-event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -10,10 +10,10 @@ import { import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; import { HassEvent } from "home-assistant-js-websocket"; -import { HomeAssistant } from "../../types"; -import { PolymerChangedEvent } from "../../polymer-types"; -import "../../components/ha-card"; -import format_time from "../../common/datetime/format_time"; +import { HomeAssistant } from "../../../types"; +import { PolymerChangedEvent } from "../../../polymer-types"; +import "../../../components/ha-card"; +import format_time from "../../../common/datetime/format_time"; @customElement("event-subscribe-card") class EventSubscribeCard extends LitElement { diff --git a/src/panels/dev-event/events-list.js b/src/panels/developer-tools/event/events-list.js similarity index 95% rename from src/panels/dev-event/events-list.js rename to src/panels/developer-tools/event/events-list.js index f27d9bd929..3ffba2e5f5 100644 --- a/src/panels/dev-event/events-list.js +++ b/src/panels/developer-tools/event/events-list.js @@ -1,7 +1,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { EventsMixin } from "../../mixins/events-mixin"; +import { EventsMixin } from "../../../mixins/events-mixin"; /* * @appliesMixin EventsMixin diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts new file mode 100644 index 0000000000..98a1d74e23 --- /dev/null +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -0,0 +1,124 @@ +import { + LitElement, + TemplateResult, + html, + CSSResultArray, + css, + customElement, + property, +} from "lit-element"; +import "@polymer/app-layout/app-header-layout/app-header-layout"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-tabs/paper-tab"; +import "@polymer/paper-tabs/paper-tabs"; + +import "../../components/ha-menu-button"; +import "../../resources/ha-style"; +import "./developer-tools-router"; + +import scrollToTarget from "../../common/dom/scroll-to-target"; + +import { haStyle } from "../../resources/styles"; +import { HomeAssistant, Route } from "../../types"; +import { navigate } from "../../common/navigate"; +import isComponentLoaded from "../../common/config/is_component_loaded"; + +@customElement("ha-panel-developer-tools") +class PanelDeveloperTools extends LitElement { + @property() public hass!: HomeAssistant; + @property() public route!: Route; + @property() public narrow!: boolean; + + protected render(): TemplateResult | void { + const page = this._page; + return html` + + + + +
Developer Tools
+
+ + + ${this.hass.localize("panel.dev-info")} + + + ${this.hass.localize("panel.dev-events")} + + ${isComponentLoaded(this.hass, "mqtt") + ? html` + + ${this.hass.localize("panel.dev-mqtt")} + + ` + : ""} + + ${this.hass.localize("panel.dev-services")} + + + ${this.hass.localize("panel.dev-states")} + + + ${this.hass.localize("panel.dev-templates")} + + +
+ +
+ `; + } + + private handlePageSelected(ev) { + const newPage = ev.detail.item.getAttribute("page-name"); + if (newPage !== this._page) { + navigate(this, `/developer-tools/${newPage}`); + } + + scrollToTarget( + this, + // @ts-ignore + this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget + ); + } + + private get _page() { + return this.route.path.substr(1); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + :host { + color: var(--primary-text-color); + --paper-card-header-color: var(--primary-text-color); + } + paper-tabs { + margin-left: 12px; + --paper-tabs-selection-bar-color: #fff; + text-transform: uppercase; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-developer-tools": PanelDeveloperTools; + } +} diff --git a/src/panels/developer-tools/info/developer-tools-info.ts b/src/panels/developer-tools/info/developer-tools-info.ts new file mode 100644 index 0000000000..6e248a97c4 --- /dev/null +++ b/src/panels/developer-tools/info/developer-tools-info.ts @@ -0,0 +1,194 @@ +import { + LitElement, + html, + CSSResult, + css, + TemplateResult, + property, +} from "lit-element"; + +import { HomeAssistant } from "../../../types"; +import { haStyle } from "../../../resources/styles"; + +import "./system-log-card"; +import "./error-log-card"; +import "./system-health-card"; + +const JS_VERSION = __BUILD__; +const OPT_IN_PANEL = "states"; + +class HaPanelDevInfo extends LitElement { + @property() public hass!: HomeAssistant; + + protected render(): TemplateResult | void { + const hass = this.hass; + const customUiList: Array<{ name: string; url: string; version: string }> = + (window as any).CUSTOM_UI_LIST || []; + + const nonDefaultLink = + localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states" + ? "/lovelace" + : "/states"; + + const nonDefaultLinkText = + localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states" + ? "Go to the Lovelace UI" + : "Go to the states UI"; + + const defaultPageText = `${ + localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set" + } ${OPT_IN_PANEL} as default page on this device`; + + return html` +
+

+ Home Assistant logo +
+ Home Assistant
+ ${hass.config.version} +

+

+ Path to configuration.yaml: ${hass.config.config_dir} +

+

+ + Developed by a bunch of awesome people. + +

+

+ Published under the Apache 2.0 license
+ Source: + server + — + frontend-ui +

+

+ Built using + Python 3, + Polymer, + Icons by + Google + and + MaterialDesignIcons.com. +

+

+ Frontend JavaScript version: ${JS_VERSION} + ${customUiList.length > 0 + ? html` +

+ Custom UIs: + ${customUiList.map( + (item) => html` +
+ ${item.name}: + ${item.version} +
+ ` + )} +
+ ` + : ""} +

+

+ ${nonDefaultLinkText}
+ + ${defaultPageText} + +

+
+ + + + `; + } + + protected firstUpdated(changedProps): void { + super.firstUpdated(changedProps); + + // Legacy custom UI can be slow to register, give them time. + const customUI = ((window as any).CUSTOM_UI_LIST || []).length; + setTimeout(() => { + if (((window as any).CUSTOM_UI_LIST || []).length !== customUI.length) { + this.requestUpdate(); + } + }, 1000); + } + + protected _toggleDefaultPage(): void { + if (localStorage.defaultPage === OPT_IN_PANEL) { + delete localStorage.defaultPage; + } else { + localStorage.defaultPage = OPT_IN_PANEL; + } + this.requestUpdate(); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + :host { + -ms-user-select: initial; + -webkit-user-select: initial; + -moz-user-select: initial; + } + + .content { + padding: 16px 0px 16px 0; + direction: ltr; + } + + .about { + text-align: center; + line-height: 2em; + } + + .version { + @apply --paper-font-headline; + } + + .develop { + @apply --paper-font-subhead; + } + + .about a { + color: var(--dark-primary-color); + } + + system-health-card { + display: block; + max-width: 600px; + margin: 0 auto; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-info": HaPanelDevInfo; + } +} + +customElements.define("developer-tools-info", HaPanelDevInfo); diff --git a/src/panels/dev-info/dialog-system-log-detail.ts b/src/panels/developer-tools/info/dialog-system-log-detail.ts similarity index 91% rename from src/panels/dev-info/dialog-system-log-detail.ts rename to src/panels/developer-tools/info/dialog-system-log-detail.ts index 6ec2299a1b..eba62d72ac 100644 --- a/src/panels/dev-info/dialog-system-log-detail.ts +++ b/src/panels/developer-tools/info/dialog-system-log-detail.ts @@ -8,11 +8,11 @@ import { } from "lit-element"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "../../components/dialog/ha-paper-dialog"; +import "../../../components/dialog/ha-paper-dialog"; import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; -import { PolymerChangedEvent } from "../../polymer-types"; -import { haStyleDialog } from "../../resources/styles"; +import { PolymerChangedEvent } from "../../../polymer-types"; +import { haStyleDialog } from "../../../resources/styles"; class DialogSystemLogDetail extends LitElement { private _params?: SystemLogDetailDialogParams; diff --git a/src/panels/dev-info/error-log-card.ts b/src/panels/developer-tools/info/error-log-card.ts similarity index 93% rename from src/panels/dev-info/error-log-card.ts rename to src/panels/developer-tools/info/error-log-card.ts index 46b4274492..9f2bfbd094 100644 --- a/src/panels/dev-info/error-log-card.ts +++ b/src/panels/developer-tools/info/error-log-card.ts @@ -9,8 +9,8 @@ import { import "@polymer/paper-icon-button/paper-icon-button"; import "@material/mwc-button"; -import { HomeAssistant } from "../../types"; -import { fetchErrorLog } from "../../data/error_log"; +import { HomeAssistant } from "../../../types"; +import { fetchErrorLog } from "../../../data/error_log"; class ErrorLogCard extends LitElement { public hass?: HomeAssistant; diff --git a/src/panels/dev-info/show-dialog-system-log-detail.ts b/src/panels/developer-tools/info/show-dialog-system-log-detail.ts similarity index 88% rename from src/panels/dev-info/show-dialog-system-log-detail.ts rename to src/panels/developer-tools/info/show-dialog-system-log-detail.ts index 3e1899495f..b1c378b5a5 100644 --- a/src/panels/dev-info/show-dialog-system-log-detail.ts +++ b/src/panels/developer-tools/info/show-dialog-system-log-detail.ts @@ -1,5 +1,5 @@ -import { fireEvent } from "../../common/dom/fire_event"; -import { LoggedError } from "../../data/system_log"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { LoggedError } from "../../../data/system_log"; declare global { // for fire event diff --git a/src/panels/dev-info/system-health-card.ts b/src/panels/developer-tools/info/system-health-card.ts similarity index 95% rename from src/panels/dev-info/system-health-card.ts rename to src/panels/developer-tools/info/system-health-card.ts index 82159ed847..82b819bc32 100644 --- a/src/panels/dev-info/system-health-card.ts +++ b/src/panels/developer-tools/info/system-health-card.ts @@ -7,13 +7,13 @@ import { TemplateResult, } from "lit-element"; import "@polymer/paper-spinner/paper-spinner"; -import "../../components/ha-card"; +import "../../../components/ha-card"; -import { HomeAssistant } from "../../types"; +import { HomeAssistant } from "../../../types"; import { SystemHealthInfo, fetchSystemHealthInfo, -} from "../../data/system_health"; +} from "../../../data/system_health"; const sortKeys = (a: string, b: string) => { if (a === "homeassistant") { diff --git a/src/panels/dev-info/system-log-card.ts b/src/panels/developer-tools/info/system-log-card.ts similarity index 91% rename from src/panels/dev-info/system-log-card.ts rename to src/panels/developer-tools/info/system-log-card.ts index 0d1391c1ee..e3800dcfec 100644 --- a/src/panels/dev-info/system-log-card.ts +++ b/src/panels/developer-tools/info/system-log-card.ts @@ -10,13 +10,13 @@ import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-spinner/paper-spinner"; -import "../../components/ha-card"; -import "../../components/buttons/ha-call-service-button"; -import "../../components/buttons/ha-progress-button"; -import { HomeAssistant } from "../../types"; -import { LoggedError, fetchSystemLog } from "../../data/system_log"; -import formatDateTime from "../../common/datetime/format_date_time"; -import formatTime from "../../common/datetime/format_time"; +import "../../../components/ha-card"; +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/buttons/ha-progress-button"; +import { HomeAssistant } from "../../../types"; +import { LoggedError, fetchSystemLog } from "../../../data/system_log"; +import formatDateTime from "../../../common/datetime/format_date_time"; +import formatTime from "../../../common/datetime/format_time"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; const formatLogTime = (date, language: string) => { diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.js b/src/panels/developer-tools/mqtt/developer-tools-mqtt.js new file mode 100644 index 0000000000..a2e80c0d94 --- /dev/null +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.js @@ -0,0 +1,74 @@ +import "@material/mwc-button"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-input/paper-textarea"; +import { html } from "@polymer/polymer/lib/utils/html-tag"; +import { PolymerElement } from "@polymer/polymer/polymer-element"; + +import "../../../components/ha-card"; +import "../../../resources/ha-style"; +import "../../../util/app-localstorage-document"; + +class HaPanelDevMqtt extends PolymerElement { + static get template() { + return html` + + + + + + + + +
+ + + +
+
+ Publish +
+
+ `; + } + + static get properties() { + return { + hass: Object, + topic: String, + payload: String, + }; + } + + _publish() { + this.hass.callService("mqtt", "publish", { + topic: this.topic, + payload_template: this.payload, + }); + } +} + +customElements.define("developer-tools-mqtt", HaPanelDevMqtt); diff --git a/src/panels/dev-service/ha-panel-dev-service.js b/src/panels/developer-tools/service/developer-tools-service.js similarity index 55% rename from src/panels/dev-service/ha-panel-dev-service.js rename to src/panels/developer-tools/service/developer-tools-service.js index b4b0be23fe..05b1e7e1bf 100644 --- a/src/panels/dev-service/ha-panel-dev-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -1,17 +1,13 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@material/mwc-button"; import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { ENTITY_COMPONENT_DOMAINS } from "../../data/entity"; -import "../../components/entity/ha-entity-picker"; -import "../../components/ha-menu-button"; -import "../../components/ha-service-picker"; -import "../../resources/ha-style"; -import "../../util/app-localstorage-document"; +import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; +import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-service-picker"; +import "../../../resources/ha-style"; +import "../../../util/app-localstorage-document"; const ERROR_SENTINEL = {}; class HaPanelDevService extends PolymerElement { @@ -22,9 +18,7 @@ class HaPanelDevService extends PolymerElement { -ms-user-select: initial; -webkit-user-select: initial; -moz-user-select: initial; - } - - .content { + display: block; padding: 16px; direction: ltr; } @@ -81,100 +75,87 @@ class HaPanelDevService extends PolymerElement { } - - - - -
Services
-
-
+ + + + - - - - +
+

+ The service dev tool allows you to call any available service in Home + Assistant. +

-
-

- The service dev tool allows you to call any available service in - Home Assistant. -

- -
- + +