diff --git a/src/data/error_log.ts b/src/data/error_log.ts new file mode 100644 index 0000000000..b1bb4de170 --- /dev/null +++ b/src/data/error_log.ts @@ -0,0 +1,4 @@ +import { HomeAssistant } from "../types"; + +export const fetchErrorLog = (hass: HomeAssistant) => + hass.callApi("GET", "error_log"); diff --git a/src/data/system_log.ts b/src/data/system_log.ts new file mode 100644 index 0000000000..540bb04289 --- /dev/null +++ b/src/data/system_log.ts @@ -0,0 +1,13 @@ +import { HomeAssistant } from "../types"; + +export interface LoggedError { + message: string; + level: string; + source: string; + // unix timestamp in seconds + timestamp: number; + exception: string; +} + +export const fetchSystemLog = (hass: HomeAssistant) => + hass.callApi("GET", "error/all"); diff --git a/src/panels/dev-info/dialog-system-log-detail.ts b/src/panels/dev-info/dialog-system-log-detail.ts new file mode 100644 index 0000000000..10dcfdfff8 --- /dev/null +++ b/src/panels/dev-info/dialog-system-log-detail.ts @@ -0,0 +1,77 @@ +import { + LitElement, + html, + css, + PropertyDeclarations, + CSSResult, + TemplateResult, +} from "lit-element"; +import "@polymer/paper-dialog/paper-dialog"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; + +import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; +import { PolymerChangedEvent } from "../../polymer-types"; +import { haStyleDialog } from "../../resources/ha-style"; + +class DialogSystemLogDetail extends LitElement { + private _params?: SystemLogDetailDialogParams; + + static get properties(): PropertyDeclarations { + return { + _params: {}, + }; + } + + public async showDialog(params: SystemLogDetailDialogParams): Promise { + this._params = params; + await this.updateComplete; + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + const item = this._params.item; + + return html` + +

Log Details (${item.level})

+ +

${new Date(item.timestamp * 1000)}

+ ${item.message + ? html` +
${item.message}
+ ` + : html``} + ${item.exception + ? html` +
${item.exception}
+ ` + : html``} +
+
+ `; + } + + private _openedChanged(ev: PolymerChangedEvent): void { + if (!(ev.detail as any).value) { + this._params = undefined; + } + } + + static get styles(): CSSResult[] { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-system-log-detail": DialogSystemLogDetail; + } +} + +customElements.define("dialog-system-log-detail", DialogSystemLogDetail); diff --git a/src/panels/dev-info/error-log-card.ts b/src/panels/dev-info/error-log-card.ts new file mode 100644 index 0000000000..88e3208c7d --- /dev/null +++ b/src/panels/dev-info/error-log-card.ts @@ -0,0 +1,73 @@ +import { + LitElement, + html, + CSSResult, + css, + PropertyDeclarations, + TemplateResult, +} from "lit-element"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-button/paper-button"; + +import { HomeAssistant } from "../../types"; +import { fetchErrorLog } from "../../data/error_log"; + +class ErrorLogCard extends LitElement { + public hass?: HomeAssistant; + private _errorLog?: string; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + _errorLog: {}, + }; + } + + protected render(): TemplateResult | void { + return html` +

+ ${this._errorLog + ? html` + + ` + : html` + + Load Full Home Assistant Log + + `} +

+
${this._errorLog}
+ `; + } + + static get styles(): CSSResult { + return css` + .error-log-intro { + text-align: center; + margin: 16px; + } + + paper-icon-button { + float: right; + } + + .error-log { + @apply --paper-font-code) + clear: both; + white-space: pre-wrap; + margin: 16px; + } + `; + } + + private async _refreshErrorLog(): Promise { + this._errorLog = "Loading error log…"; + const log = await fetchErrorLog(this.hass!); + this._errorLog = log || "No errors have been reported."; + } +} + +customElements.define("error-log-card", ErrorLogCard); diff --git a/src/panels/dev-info/ha-loaded-components.js b/src/panels/dev-info/ha-loaded-components.js deleted file mode 100644 index 2cd617636c..0000000000 --- a/src/panels/dev-info/ha-loaded-components.js +++ /dev/null @@ -1,59 +0,0 @@ -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-dialog/paper-dialog"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../resources/ha-style"; - -import EventsMixin from "../../mixins/events-mixin"; - -/* - * @appliesMixin EventsMixin - */ -class HaLoadedComponents extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -

Loaded Components

- -

The following components are currently loaded:

-
    - -
-
-
- `; - } - - static get properties() { - return { - _hass: Object, - _components: Array, - - _opened: { - type: Boolean, - value: false, - }, - }; - } - - ready() { - super.ready(); - } - - showDialog({ hass }) { - this.hass = hass; - this._opened = true; - this._components = this.hass.config.components.sort(); - setTimeout(() => this.$.dialog.center(), 0); - } -} - -customElements.define("ha-loaded-components", HaLoadedComponents); diff --git a/src/panels/dev-info/ha-panel-dev-info.js b/src/panels/dev-info/ha-panel-dev-info.js deleted file mode 100644 index 91db9412ca..0000000000 --- a/src/panels/dev-info/ha-panel-dev-info.js +++ /dev/null @@ -1,416 +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 "@polymer/paper-card/paper-card"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-dialog/paper-dialog"; -import "@polymer/paper-icon-button/paper-icon-button"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-item/paper-item"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../components/buttons/ha-call-service-button"; -import "../../components/ha-menu-button"; -import "../../resources/ha-style"; - -import formatDateTime from "../../common/datetime/format_date_time"; -import formatTime from "../../common/datetime/format_time"; - -import EventsMixin from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; - -const OPT_IN_PANEL = "states"; -let registeredDialog = false; - -class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) { - static get template() { - return html` - - - - - - -
About
-
-
- -
-
-

-
- Home Assistant
- [[hass.config.version]] -

-

- Path to configuration.yaml: [[hass.config.config_dir]] -
[[loadedComponents.length]] Loaded Components -

-

- - 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: [[jsVersion]] - -

-

- [[_nonDefaultLinkText()]] -

[[_defaultPageText()]]
-
- -
- - - - -
-

- - -

-
[[errorLog]]
-
-
- - -

Log Details ([[selectedItem.level]])

- -

[[fullTimeStamp(selectedItem.timestamp)]]

- - -
-
- `; - } - - static get properties() { - return { - hass: Object, - - narrow: { - type: Boolean, - value: false, - }, - - showMenu: { - type: Boolean, - value: false, - }, - - errorLog: { - type: String, - value: "", - }, - - updating: { - type: Boolean, - value: true, - }, - - items: { - type: Array, - value: [], - }, - - selectedItem: Object, - - jsVersion: { - type: String, - value: __BUILD__, - }, - - customUiList: { - type: Array, - value: window.CUSTOM_UI_LIST || [], - }, - - loadedComponents: { - type: Array, - value: [], - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-service-called", (ev) => - this.serviceCalled(ev) - ); - // Fix for overlay showing on top of dialog. - this.$.showlog.addEventListener("iron-overlay-opened", (ev) => { - if (ev.target.withBackdrop) { - ev.target.parentNode.insertBefore(ev.target.backdropElement, ev.target); - } - }); - } - - serviceCalled(ev) { - // Check if this is for us - if (ev.detail.success && ev.detail.domain === "system_log") { - // Do the right thing depending on service - if (ev.detail.service === "clear") { - this.items = []; - } - } - } - - connectedCallback() { - super.connectedCallback(); - this.$.scrollable.dialogElement = this.$.showlog; - this._fetchData(); - this.loadedComponents = this.hass.config.components; - - if (!registeredDialog) { - registeredDialog = true; - this.fire("register-dialog", { - dialogShowEvent: "show-loaded-components", - dialogTag: "ha-loaded-components", - dialogImport: () => - import(/* webpackChunkName: "ha-loaded-components" */ "./ha-loaded-components"), - }); - } - - if (!window.CUSTOM_UI_LIST) { - // Give custom UI an opportunity to load. - setTimeout(() => { - this.customUiList = window.CUSTOM_UI_LIST || []; - }, 1000); - } else { - this.customUiList = window.CUSTOM_UI_LIST; - } - } - - refreshErrorLog(ev) { - if (ev) ev.preventDefault(); - - this.errorLog = "Loading error log…"; - - this.hass.callApi("GET", "error_log").then((log) => { - this.errorLog = log || "No errors have been reported."; - }); - } - - fullTimeStamp(date) { - return new Date(date * 1000); - } - - formatTime(date) { - const today = new Date().setHours(0, 0, 0, 0); - const dateTime = new Date(date * 1000); - const dateTimeDay = new Date(date * 1000).setHours(0, 0, 0, 0); - - return dateTimeDay < today - ? formatDateTime(dateTime, this.hass.language) - : formatTime(dateTime, this.hass.language); - } - - openLog(event) { - this.selectedItem = event.model.item; - this.$.showlog.open(); - } - - _fetchData() { - this.updating = true; - this.hass.callApi("get", "error/all").then((items) => { - this.items = items; - this.updating = false; - }); - } - - _nonDefaultLink() { - if ( - localStorage.defaultPage === OPT_IN_PANEL && - OPT_IN_PANEL === "states" - ) { - return "/lovelace"; - } - return "/states"; - } - - _nonDefaultLinkText() { - if ( - localStorage.defaultPage === OPT_IN_PANEL && - OPT_IN_PANEL === "states" - ) { - return "Go to the Lovelace UI"; - } - return "Go to the states UI"; - } - - _defaultPageText() { - return `>> ${ - localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set" - } ${OPT_IN_PANEL} as default page on this device <<`; - } - - _toggleDefaultPage() { - if (localStorage.defaultPage === OPT_IN_PANEL) { - delete localStorage.defaultPage; - } else { - localStorage.defaultPage = OPT_IN_PANEL; - } - this.$.love.innerText = this._defaultPageText(); - } - - _showComponents() { - this.fire("show-loaded-components", { - hass: this.hass, - }); - } -} - -customElements.define("ha-panel-dev-info", HaPanelDevInfo); diff --git a/src/panels/dev-info/ha-panel-dev-info.ts b/src/panels/dev-info/ha-panel-dev-info.ts new file mode 100644 index 0000000000..5de2a09317 --- /dev/null +++ b/src/panels/dev-info/ha-panel-dev-info.ts @@ -0,0 +1,215 @@ +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/ha-style"; + +import "./system-log-card"; +import "./error-log-card"; + +const JS_VERSION = __BUILD__; +const OPT_IN_PANEL = "states"; + +class HaPanelDevInfo extends LitElement { + public hass?: HomeAssistant; + public narrow?: boolean; + public showMenu?: boolean; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + narrow: {}, + showMenu: {}, + }; + } + + 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
+ ${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); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-dev-info": HaPanelDevInfo; + } +} + +customElements.define("ha-panel-dev-info", HaPanelDevInfo); diff --git a/src/panels/dev-info/show-dialog-system-log-detail.ts b/src/panels/dev-info/show-dialog-system-log-detail.ts new file mode 100644 index 0000000000..3e1899495f --- /dev/null +++ b/src/panels/dev-info/show-dialog-system-log-detail.ts @@ -0,0 +1,36 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { LoggedError } from "../../data/system_log"; + +declare global { + // for fire event + interface HASSDomEvents { + "show-dialog-system-log-detail": SystemLogDetailDialogParams; + } +} + +let registeredDialog = false; +const dialogShowEvent = "show-dialog-system-log-detail"; +const dialogTag = "dialog-system-log-detail"; + +export interface SystemLogDetailDialogParams { + item: LoggedError; +} + +const registerDialog = (element: HTMLElement) => + fireEvent(element, "register-dialog", { + dialogShowEvent, + dialogTag, + dialogImport: () => + import(/* webpackChunkName: "system-log-detail-dialog" */ "./dialog-system-log-detail"), + }); + +export const showSystemLogDetailDialog = ( + element: HTMLElement, + systemLogDetailParams: SystemLogDetailDialogParams +): void => { + if (!registeredDialog) { + registeredDialog = true; + registerDialog(element); + } + fireEvent(element, dialogShowEvent, systemLogDetailParams); +}; diff --git a/src/panels/dev-info/system-log-card.ts b/src/panels/dev-info/system-log-card.ts new file mode 100644 index 0000000000..11104a5dc5 --- /dev/null +++ b/src/panels/dev-info/system-log-card.ts @@ -0,0 +1,148 @@ +import { + LitElement, + html, + CSSResult, + css, + PropertyDeclarations, + TemplateResult, +} from "lit-element"; +import "@polymer/paper-card/paper-card"; +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/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) => { + const today = new Date().setHours(0, 0, 0, 0); + const dateTime = new Date(date * 1000); + const dateTimeDay = new Date(date * 1000).setHours(0, 0, 0, 0); + + return dateTimeDay < today + ? formatDateTime(dateTime, language) + : formatTime(dateTime, language); +}; + +class SystemLogCard extends LitElement { + public hass?: HomeAssistant; + private _items?: LoggedError[]; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + _items: {}, + }; + } + + protected render(): TemplateResult | void { + return html` +
+ + ${this._items === undefined + ? html` +
+ +
+ ` + : html` + ${this._items.length === 0 + ? html` +
There are no new issues!
+ ` + : this._items.map( + (item) => html` + + +
+ ${item.message} +
+
+ ${formatLogTime( + item.timestamp, + this.hass!.language + )} + ${item.source} (${item.level}) +
+
+
+ ` + )} + +
+ Clear + Refresh +
+ `} +
+
+ `; + } + + protected firstUpdated(changedProps): void { + super.firstUpdated(changedProps); + this._fetchData(); + this.addEventListener("hass-service-called", (ev) => + this.serviceCalled(ev) + ); + } + + protected serviceCalled(ev): void { + // Check if this is for us + if (ev.detail.success && ev.detail.domain === "system_log") { + // Do the right thing depending on service + if (ev.detail.service === "clear") { + this._items = []; + } + } + } + + private async _fetchData(): Promise { + this._items = undefined; + this._items = await fetchSystemLog(this.hass!); + } + + private _openLog(ev: Event): void { + const item = (ev.currentTarget as any).logItem; + showSystemLogDetailDialog(this, { item }); + } + + static get styles(): CSSResult { + return css` + paper-card { + display: block; + padding-top: 16px; + } + + paper-item { + cursor: pointer; + } + + .system-log-intro { + margin: 16px; + border-top: 1px solid var(--light-primary-color); + padding-top: 16px; + } + + .loading-container { + @apply --layout-vertical; + @apply --layout-center-center; + height: 100px; + } + `; + } +} + +customElements.define("system-log-card", SystemLogCard); diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts index 90658007bb..58ac1051c6 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts @@ -11,6 +11,7 @@ import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; import { moveCard } from "../config-util"; import { MoveCardViewDialogParams } from "./show-move-card-view-dialog"; +import { PolymerChangedEvent } from "../../../../polymer-types"; export class HuiDialogMoveCardView extends LitElement { private _params?: MoveCardViewDialogParams; @@ -91,7 +92,7 @@ export class HuiDialogMoveCardView extends LitElement { this._dialog.close(); } - private _openedChanged(ev: MouseEvent) { + private _openedChanged(ev: PolymerChangedEvent): void { if (!(ev.detail as any).value) { this._params = undefined; } diff --git a/src/polymer-types.ts b/src/polymer-types.ts index 693c2db105..c7692329a0 100644 --- a/src/polymer-types.ts +++ b/src/polymer-types.ts @@ -1,6 +1,12 @@ // Force file to be a module to augment global scope. export {}; +export interface PolymerChangedEvent extends Event { + detail: { + value: T; + }; +} + declare global { // for fire event interface HASSDomEvents {