Add developer tools panel (#3313)

This commit is contained in:
Paulus Schoutsen 2019-06-28 08:34:29 -07:00 committed by GitHub
parent 618d25ce48
commit 58e6be12af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 710 additions and 675 deletions

View File

@ -22,6 +22,7 @@ const BUILT_IN_PANEL_ICONS = [
"mailbox", // Mailbox "mailbox", // Mailbox
"tooltip-account", // Map "tooltip-account", // Map
"cart", // Shopping List "cart", // Shopping List
"hammer", // developer-tools
]; ];
// Given an icon name, load the SVG file // Given an icon name, load the SVG file

View File

@ -14,7 +14,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "./ha-icon"; import "./ha-icon";
import "../components/user/ha-user-badge"; import "../components/user/ha-user-badge";
import isComponentLoaded from "../common/config/is_component_loaded";
import { HomeAssistant, PanelInfo } from "../types"; import { HomeAssistant, PanelInfo } from "../types";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { DEFAULT_PANEL } from "../common/const"; import { DEFAULT_PANEL } from "../common/const";
@ -23,47 +22,64 @@ import {
ExternalConfig, ExternalConfig,
} from "../external_app/external_config"; } from "../external_app/external_config";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const computeUrl = (urlPath) => `/${urlPath}`; 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; const panels = hass.panels;
if (!panels) { if (!panels) {
return []; return [[], []];
} }
const sortValue = { const beforeSpacer: PanelInfo[] = [];
map: 1, const afterSpacer: PanelInfo[] = [];
logbook: 2,
history: 3,
};
const result: PanelInfo[] = Object.values(panels).filter(
(panel) => panel.title
);
result.sort((a, b) => { Object.values(panels).forEach((panel) => {
const aBuiltIn = a.component_name in sortValue; if (!panel.title) {
const bBuiltIn = b.component_name in sortValue; return;
if (aBuiltIn && bBuiltIn) {
return sortValue[a.component_name] - sortValue[b.component_name];
} }
if (aBuiltIn) { (SHOW_AFTER_SPACER.includes(panel.component_name)
return -1; ? afterSpacer
} : beforeSpacer
if (bBuiltIn) { ).push(panel);
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;
}); });
return result; beforeSpacer.sort(panelSorter);
afterSpacer.sort(panelSorter);
return [beforeSpacer, afterSpacer];
}; };
const renderPanel = (hass, panel) => html` const renderPanel = (hass, panel) => html`
@ -100,12 +116,7 @@ class HaSidebar extends LitElement {
return html``; return html``;
} }
const panels = computePanels(hass); const [beforeSpacer, afterSpacer] = computePanels(hass);
const configPanelIdx = panels.findIndex(
(panel) => panel.component_name === "config"
);
const configPanel =
configPanelIdx === -1 ? undefined : panels.splice(configPanelIdx, 1)[0];
return html` return html`
${this.expanded ${this.expanded
@ -137,69 +148,11 @@ class HaSidebar extends LitElement {
</paper-icon-item> </paper-icon-item>
</a> </a>
${panels.map((panel) => renderPanel(hass, panel))} ${beforeSpacer.map((panel) => renderPanel(hass, panel))}
<div class="spacer" disabled></div> <div class="spacer" disabled></div>
${this.expanded && hass.user && hass.user.is_admin ${afterSpacer.map((panel) => renderPanel(hass, panel))}
? html`
<div class="divider" disabled></div>
<div class="subheader" disabled>
${hass.localize("ui.sidebar.developer_tools")}
</div>
<div class="dev-tools" disabled>
<a href="/dev-service" tabindex="-1">
<paper-icon-button
icon="hass:remote"
alt="${hass.localize("panel.dev-services")}"
title="${hass.localize("panel.dev-services")}"
></paper-icon-button>
</a>
<a href="/dev-state" tabindex="-1">
<paper-icon-button
icon="hass:code-tags"
alt="${hass.localize("panel.dev-states")}"
title="${hass.localize("panel.dev-states")}"
></paper-icon-button>
</a>
<a href="/dev-event" tabindex="-1">
<paper-icon-button
icon="hass:radio-tower"
alt="${hass.localize("panel.dev-events")}"
title="${hass.localize("panel.dev-events")}"
></paper-icon-button>
</a>
<a href="/dev-template" tabindex="-1">
<paper-icon-button
icon="hass:file-xml"
alt="${hass.localize("panel.dev-templates")}"
title="${hass.localize("panel.dev-templates")}"
></paper-icon-button>
</a>
${isComponentLoaded(hass, "mqtt")
? html`
<a href="/dev-mqtt" tabindex="-1">
<paper-icon-button
icon="hass:altimeter"
alt="${hass.localize("panel.dev-mqtt")}"
title="${hass.localize("panel.dev-mqtt")}"
></paper-icon-button>
</a>
`
: html``}
<a href="/dev-info" tabindex="-1">
<paper-icon-button
icon="hass:information-outline"
alt="${hass.localize("panel.dev-info")}"
title="${hass.localize("panel.dev-info")}"
></paper-icon-button>
</a>
</div>
<div class="divider" disabled></div>
`
: ""}
${this._externalConfig && this._externalConfig.hasSettingsScreen ${this._externalConfig && this._externalConfig.hasSettingsScreen
? html` ? html`
<a <a
@ -223,7 +176,6 @@ class HaSidebar extends LitElement {
</a> </a>
` `
: ""} : ""}
${configPanel ? renderPanel(hass, configPanel) : ""}
${hass.user ${hass.user
? html` ? html`
<a <a
@ -280,7 +232,6 @@ class HaSidebar extends LitElement {
return ( return (
hass.panels !== oldHass.panels || hass.panels !== oldHass.panels ||
hass.panelUrl !== oldHass.panelUrl || hass.panelUrl !== oldHass.panelUrl ||
hass.config.components !== oldHass.config.components ||
hass.user !== oldHass.user || hass.user !== oldHass.user ||
hass.localize !== oldHass.localize hass.localize !== oldHass.localize
); );
@ -458,12 +409,6 @@ class HaSidebar extends LitElement {
pointer-events: none; pointer-events: none;
} }
.divider {
height: 1px;
background-color: var(--divider-color);
margin: 4px 0;
}
.subheader { .subheader {
color: var(--sidebar-text-color); color: var(--sidebar-text-color);
font-weight: 500; font-weight: 500;

View File

@ -9,7 +9,7 @@ import {
} from "./hass-router-page"; } from "./hass-router-page";
import { removeInitSkeleton } from "../util/init-skeleton"; import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_COMPONENTS = ["lovelace", "states"]; const CACHE_COMPONENTS = ["lovelace", "states", "developer-tools"];
const COMPONENTS = { const COMPONENTS = {
calendar: () => calendar: () =>
import(/* webpackChunkName: "panel-calendar" */ "../panels/calendar/ha-panel-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"), import(/* webpackChunkName: "panel-config" */ "../panels/config/ha-panel-config"),
custom: () => custom: () =>
import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"), import(/* webpackChunkName: "panel-custom" */ "../panels/custom/ha-panel-custom"),
"dev-event": () => "developer-tools": () =>
import(/* webpackChunkName: "panel-dev-event" */ "../panels/dev-event/ha-panel-dev-event"), import(/* webpackChunkName: "panel-developer-tools" */ "../panels/developer-tools/ha-panel-developer-tools"),
"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"),
lovelace: () => lovelace: () =>
import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"), import(/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"),
states: () => states: () =>
@ -52,14 +42,17 @@ const COMPONENTS = {
}; };
const getRoutes = (panels: Panels): RouterOptions => { const getRoutes = (panels: Panels): RouterOptions => {
const routes: { [route: string]: RouteOptions } = {}; const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => { Object.values(panels).forEach((panel) => {
routes[panel.url_path] = { const data: RouteOptions = {
load: COMPONENTS[panel.component_name],
tag: `ha-panel-${panel.component_name}`, tag: `ha-panel-${panel.component_name}`,
cache: CACHE_COMPONENTS.includes(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 { return {

View File

@ -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`
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>About</div>
</app-toolbar>
</app-header>
<div class="content">
<div class="about">
<p class="version">
<a href="https://www.home-assistant.io" target="_blank">
<img
src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
/>
</a>
<br />
Home Assistant<br />
${hass.config.version}
</p>
<p>
Path to configuration.yaml: ${hass.config.config_dir}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
>
Developed by a bunch of awesome people.
</a>
</p>
<p>
Published under the Apache 2.0 license<br />
Source:
<a
href="https://github.com/home-assistant/home-assistant"
target="_blank"
>server</a
>
&mdash;
<a
href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank"
>frontend-ui</a
>
</p>
<p>
Built using
<a href="https://www.python.org">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank"
>Polymer</a
>, Icons by
<a href="https://www.google.com/design/icons/" target="_blank"
>Google</a
>
and
<a href="https://MaterialDesignIcons.com" target="_blank"
>MaterialDesignIcons.com</a
>.
</p>
<p>
Frontend JavaScript version: ${JS_VERSION}
${customUiList.length > 0
? html`
<div>
Custom UIs:
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank">
${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<mwc-button @click="${this._toggleDefaultPage}" raised>
${defaultPageText}
</mwc-button>
</p>
</div>
<system-health-card .hass=${this.hass}></system-health-card>
<system-log-card .hass=${this.hass}></system-log-card>
<error-log-card .hass=${this.hass}></error-log-card>
</div>
</app-header-layout>
`;
}
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);

View File

@ -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;
}
}

View File

@ -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 "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; 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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-menu-button"; import "../../../resources/ha-style";
import "../../resources/ha-style";
import "./events-list"; import "./events-list";
import "./event-subscribe-card"; import "./event-subscribe-card";
import { EventsMixin } from "../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
@ -26,12 +22,10 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
-ms-user-select: initial; -ms-user-select: initial;
-webkit-user-select: initial; -webkit-user-select: initial;
-moz-user-select: initial; -moz-user-select: initial;
}
.content {
@apply --paper-font-body1; @apply --paper-font-body1;
padding: 16px; padding: 16px;
direction: ltr; direction: ltr;
display: block;
} }
.ha-form { .ha-form {
@ -49,45 +43,34 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
} }
</style> </style>
<app-header-layout has-scrolling-region> <div class$="[[computeFormClasses(narrow)]]">
<app-header slot="header" fixed> <div class="flex">
<app-toolbar> <p>Fire an event on the event bus.</p>
<ha-menu-button></ha-menu-button>
<div main-title>Events</div>
</app-toolbar>
</app-header>
<div class="content"> <div class="ha-form">
<div class$="[[computeFormClasses(narrow)]]"> <paper-input
<div class="flex"> label="Event Type"
<p>Fire an event on the event bus.</p> autofocus
required
<div class="ha-form"> value="{{eventType}}"
<paper-input ></paper-input>
label="Event Type" <paper-textarea
autofocus label="Event Data (JSON, optional)"
required value="{{eventData}}"
value="{{eventType}}" ></paper-textarea>
></paper-input> <mwc-button on-click="fireEvent" raised>Fire Event</mwc-button>
<paper-textarea
label="Event Data (JSON, optional)"
value="{{eventData}}"
></paper-textarea>
<mwc-button on-click="fireEvent" raised>Fire Event</mwc-button>
</div>
</div>
<div>
<div class="header">Available Events</div>
<events-list
on-event-selected="eventSelected"
hass="[[hass]]"
></events-list>
</div>
</div> </div>
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
</div> </div>
</app-header-layout>
<div>
<div class="header">Available Events</div>
<events-list
on-event-selected="eventSelected"
hass="[[hass]]"
></events-list>
</div>
</div>
<event-subscribe-card hass="[[hass]]"></event-subscribe-card>
`; `;
} }
@ -139,4 +122,4 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
} }
} }
customElements.define("ha-panel-dev-event", HaPanelDevEvent); customElements.define("developer-tools-event", HaPanelDevEvent);

View File

@ -10,10 +10,10 @@ import {
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { HassEvent } from "home-assistant-js-websocket"; import { HassEvent } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../../types";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../../polymer-types";
import "../../components/ha-card"; import "../../../components/ha-card";
import format_time from "../../common/datetime/format_time"; import format_time from "../../../common/datetime/format_time";
@customElement("event-subscribe-card") @customElement("event-subscribe-card")
class EventSubscribeCard extends LitElement { class EventSubscribeCard extends LitElement {

View File

@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin

View File

@ -0,0 +1,121 @@
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`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>Developer Tools</div>
</app-toolbar>
<paper-tabs
scrollable
attr-for-selected="page-name"
.selected=${page}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="info">
${this.hass.localize("panel.dev-info")}
</paper-tab>
<paper-tab page-name="event">
${this.hass.localize("panel.dev-events")}
</paper-tab>
${isComponentLoaded(this.hass, "mqtt")
? html`
<paper-tab page-name="mqtt">
${this.hass.localize("panel.dev-mqtt")}
</paper-tab>
`
: ""}
<paper-tab page-name="service">
${this.hass.localize("panel.dev-services")}
</paper-tab>
<paper-tab page-name="state">
${this.hass.localize("panel.dev-states")}
</paper-tab>
<paper-tab page-name="template">
${this.hass.localize("panel.dev-templates")}
</paper-tab>
</paper-tabs>
</app-header>
<developer-tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
</app-header-layout>
`;
}
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;
}
}

View File

@ -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`
<div class="about">
<p class="version">
<a href="https://www.home-assistant.io" target="_blank"
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt="Home Assistant logo"
/></a>
<br />
Home Assistant<br />
${hass.config.version}
</p>
<p>
Path to configuration.yaml: ${hass.config.config_dir}
</p>
<p class="develop">
<a
href="https://www.home-assistant.io/developers/credits/"
target="_blank"
>
Developed by a bunch of awesome people.
</a>
</p>
<p>
Published under the Apache 2.0 license<br />
Source:
<a
href="https://github.com/home-assistant/home-assistant"
target="_blank"
>server</a
>
&mdash;
<a
href="https://github.com/home-assistant/home-assistant-polymer"
target="_blank"
>frontend-ui</a
>
</p>
<p>
Built using
<a href="https://www.python.org">Python 3</a>,
<a href="https://www.polymer-project.org" target="_blank">Polymer</a>,
Icons by
<a href="https://www.google.com/design/icons/" target="_blank"
>Google</a
>
and
<a href="https://MaterialDesignIcons.com" target="_blank"
>MaterialDesignIcons.com</a
>.
</p>
<p>
Frontend JavaScript version: ${JS_VERSION}
${customUiList.length > 0
? html`
<div>
Custom UIs:
${customUiList.map(
(item) => html`
<div>
<a href="${item.url}" target="_blank"> ${item.name}</a>:
${item.version}
</div>
`
)}
</div>
`
: ""}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<mwc-button @click="${this._toggleDefaultPage}" raised>
${defaultPageText}
</mwc-button>
</p>
</div>
<system-health-card .hass=${this.hass}></system-health-card>
<system-log-card .hass=${this.hass}></system-log-card>
<error-log-card .hass=${this.hass}></error-log-card>
`;
}
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);

View File

@ -8,11 +8,11 @@ import {
} from "lit-element"; } from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; 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 { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
class DialogSystemLogDetail extends LitElement { class DialogSystemLogDetail extends LitElement {
private _params?: SystemLogDetailDialogParams; private _params?: SystemLogDetailDialogParams;

View File

@ -9,8 +9,8 @@ import {
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button"; import "@material/mwc-button";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../../types";
import { fetchErrorLog } from "../../data/error_log"; import { fetchErrorLog } from "../../../data/error_log";
class ErrorLogCard extends LitElement { class ErrorLogCard extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;

View File

@ -1,5 +1,5 @@
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { LoggedError } from "../../data/system_log"; import { LoggedError } from "../../../data/system_log";
declare global { declare global {
// for fire event // for fire event

View File

@ -7,13 +7,13 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-card"; import "../../../components/ha-card";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../../types";
import { import {
SystemHealthInfo, SystemHealthInfo,
fetchSystemHealthInfo, fetchSystemHealthInfo,
} from "../../data/system_health"; } from "../../../data/system_health";
const sortKeys = (a: string, b: string) => { const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") { if (a === "homeassistant") {

View File

@ -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-body";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import "../../components/ha-card"; import "../../../components/ha-card";
import "../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../../types";
import { LoggedError, fetchSystemLog } from "../../data/system_log"; import { LoggedError, fetchSystemLog } from "../../../data/system_log";
import formatDateTime from "../../common/datetime/format_date_time"; import formatDateTime from "../../../common/datetime/format_date_time";
import formatTime from "../../common/datetime/format_time"; import formatTime from "../../../common/datetime/format_time";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
const formatLogTime = (date, language: string) => { const formatLogTime = (date, language: string) => {

View File

@ -7,10 +7,10 @@ import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-card"; import "../../../components/ha-card";
import "../../components/ha-menu-button"; import "../../../components/ha-menu-button";
import "../../resources/ha-style"; import "../../../resources/ha-style";
import "../../util/app-localstorage-document"; import "../../../util/app-localstorage-document";
class HaPanelDevMqtt extends PolymerElement { class HaPanelDevMqtt extends PolymerElement {
static get template() { static get template() {
@ -86,4 +86,4 @@ class HaPanelDevMqtt extends PolymerElement {
} }
} }
customElements.define("ha-panel-dev-mqtt", HaPanelDevMqtt); customElements.define("developer-tools-mqtt", HaPanelDevMqtt);

View File

@ -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 "@material/mwc-button";
import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ENTITY_COMPONENT_DOMAINS } from "../../data/entity"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import "../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../components/ha-menu-button"; import "../../../components/ha-service-picker";
import "../../components/ha-service-picker"; import "../../../resources/ha-style";
import "../../resources/ha-style"; import "../../../util/app-localstorage-document";
import "../../util/app-localstorage-document";
const ERROR_SENTINEL = {}; const ERROR_SENTINEL = {};
class HaPanelDevService extends PolymerElement { class HaPanelDevService extends PolymerElement {
@ -22,9 +18,7 @@ class HaPanelDevService extends PolymerElement {
-ms-user-select: initial; -ms-user-select: initial;
-webkit-user-select: initial; -webkit-user-select: initial;
-moz-user-select: initial; -moz-user-select: initial;
} display: block;
.content {
padding: 16px; padding: 16px;
direction: ltr; direction: ltr;
} }
@ -81,100 +75,87 @@ class HaPanelDevService extends PolymerElement {
} }
</style> </style>
<app-header-layout has-scrolling-region> <app-localstorage-document
<app-header slot="header" fixed> key="panel-dev-service-state-domain-service"
<app-toolbar> data="{{domainService}}"
<ha-menu-button></ha-menu-button> >
<div main-title>Services</div> </app-localstorage-document>
</app-toolbar> <app-localstorage-document
</app-header> key="[[_computeServicedataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<app-localstorage-document <div class="content">
key="panel-dev-service-state-domain-service" <p>
data="{{domainService}}" The service dev tool allows you to call any available service in Home
> Assistant.
</app-localstorage-document> </p>
<app-localstorage-document
key="[[_computeServicedataKey(domainService)]]"
data="{{serviceData}}"
>
</app-localstorage-document>
<div class="content"> <div class="ha-form">
<p> <ha-service-picker
The service dev tool allows you to call any available service in hass="[[hass]]"
Home Assistant. value="{{domainService}}"
</p> ></ha-service-picker>
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
<div class="ha-form"> <ha-entity-picker
<ha-service-picker
hass="[[hass]]" hass="[[hass]]"
value="{{domainService}}" value="[[_computeEntityValue(parsedJSON)]]"
></ha-service-picker> on-change="_entityPicked"
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]"> disabled="[[!validJSON]]"
<ha-entity-picker domain-filter="[[_computeEntityDomainFilter(_domain)]]"
hass="[[hass]]" allow-custom-entity
value="[[_computeEntityValue(parsedJSON)]]" ></ha-entity-picker>
on-change="_entityPicked"
disabled="[[!validJSON]]"
domain-filter="[[_computeEntityDomainFilter(_domain)]]"
allow-custom-entity
></ha-entity-picker>
</template>
<paper-textarea
always-float-label
label="Service Data (JSON, optional)"
value="{{serviceData}}"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]"
>Call Service</mwc-button
>
<template is="dom-if" if="[[!validJSON]]">
<span class="error">Invalid JSON</span>
</template>
</div>
<template is="dom-if" if="[[!domainService]]">
<h1>Select a service to see the description</h1>
</template> </template>
<paper-textarea
<template is="dom-if" if="[[domainService]]"> always-float-label
<template is="dom-if" if="[[!_description]]"> label="Service Data (JSON, optional)"
<h1>No description is available</h1> value="{{serviceData}}"
</template> autocapitalize="none"
<template is="dom-if" if="[[_description]]"> autocomplete="off"
<h3>[[_description]]</h3> spellcheck="false"
></paper-textarea>
<table class="attributes"> <mwc-button on-click="_callService" raised disabled="[[!validJSON]]"
<tr> >Call Service</mwc-button
<th>Parameter</th> >
<th>Description</th> <template is="dom-if" if="[[!validJSON]]">
<th>Example</th> <span class="error">Invalid JSON</span>
</tr>
<template is="dom-if" if="[[!_attributes.length]]">
<tr>
<td colspan="3">This service takes no parameters.</td>
</tr>
</template>
<template
is="dom-repeat"
items="[[_attributes]]"
as="attribute"
>
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
</template> </template>
</div> </div>
</app-header-layout>
<template is="dom-if" if="[[!domainService]]">
<h1>Select a service to see the description</h1>
</template>
<template is="dom-if" if="[[domainService]]">
<template is="dom-if" if="[[!_description]]">
<h1>No description is available</h1>
</template>
<template is="dom-if" if="[[_description]]">
<h3>[[_description]]</h3>
<table class="attributes">
<tr>
<th>Parameter</th>
<th>Description</th>
<th>Example</th>
</tr>
<template is="dom-if" if="[[!_attributes.length]]">
<tr>
<td colspan="3">This service takes no parameters.</td>
</tr>
</template>
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
<tr>
<td><pre>[[attribute.key]]</pre></td>
<td>[[attribute.description]]</td>
<td>[[attribute.example]]</td>
</tr>
</template>
</table>
</template>
</template>
</div>
`; `;
} }
@ -304,4 +285,4 @@ class HaPanelDevService extends PolymerElement {
} }
} }
customElements.define("ha-panel-dev-service", HaPanelDevService); customElements.define("developer-tools-service", HaPanelDevService);

View File

@ -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 "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
@ -8,10 +5,9 @@ import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../components/ha-menu-button"; import "../../../resources/ha-style";
import "../../resources/ha-style"; import { EventsMixin } from "../../../mixins/events-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
@ -24,9 +20,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
-ms-user-select: initial; -ms-user-select: initial;
-webkit-user-select: initial; -webkit-user-select: initial;
-moz-user-select: initial; -moz-user-select: initial;
} display: block;
.content {
padding: 16px; padding: 16px;
direction: ltr; direction: ltr;
} }
@ -70,107 +64,96 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
</style> </style>
<app-header-layout has-scrolling-region> <div>
<app-header slot="header" fixed> <p>
<app-toolbar> Set the representation of a device within Home Assistant.<br />
<ha-menu-button></ha-menu-button> This will not communicate with the actual device.
<div main-title>States</div> </p>
</app-toolbar>
</app-header>
<div class="content"> <ha-entity-picker
<div> autofocus
<p> hass="[[hass]]"
Set the representation of a device within Home Assistant.<br /> value="{{_entityId}}"
This will not communicate with the actual device. allow-custom-entity
</p> ></ha-entity-picker>
<paper-input
label="State"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<paper-textarea
label="State attributes (JSON, optional)"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
value="{{_stateAttributes}}"
></paper-textarea>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button>
</div>
<ha-entity-picker <h1>Current entities</h1>
autofocus <table class="entities">
hass="[[hass]]" <tr>
value="{{_entityId}}" <th>Entity</th>
allow-custom-entity <th>State</th>
></ha-entity-picker> <th hidden$="[[narrow]]">
Attributes
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox>
</th>
</tr>
<tr>
<th>
<paper-input <paper-input
label="State" label="Filter entities"
required type="search"
autocapitalize="none" value="{{_entityFilter}}"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input> ></paper-input>
<paper-textarea </th>
label="State attributes (JSON, optional)" <th>
autocapitalize="none" <paper-input
autocomplete="off" label="Filter states"
spellcheck="false" type="search"
value="{{_stateAttributes}}" value="{{_stateFilter}}"
></paper-textarea> ></paper-input>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button> </th>
</div> <th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]">
<paper-input
<h1>Current entities</h1> label="Filter attributes"
<table class="entities"> type="search"
<tr> value="{{_attributeFilter}}"
<th>Entity</th> ></paper-input>
<th>State</th> </th>
<th hidden$="[[narrow]]"> </tr>
Attributes <tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
<paper-checkbox checked="{{_showAttributes}}"></paper-checkbox> <td colspan="3">No entities</td>
</th> </tr>
</tr> <template is="dom-repeat" items="[[_entities]]" as="entity">
<tr> <tr>
<th> <td>
<paper-input <paper-icon-button
label="Filter entities" on-click="entityMoreInfo"
type="search" icon="hass:open-in-new"
value="{{_entityFilter}}" alt="More Info"
></paper-input> title="More Info"
</th> >
<th> </paper-icon-button>
<paper-input <a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
label="Filter states" </td>
type="search" <td>[[entity.state]]</td>
value="{{_stateFilter}}" <template
></paper-input> is="dom-if"
</th> if="[[computeShowAttributes(narrow, _showAttributes)]]"
<th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]"> >
<paper-input <td>[[attributeString(entity)]]</td>
label="Filter attributes"
type="search"
value="{{_attributeFilter}}"
></paper-input>
</th>
</tr>
<tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
<td colspan="3">No entities</td>
</tr>
<template is="dom-repeat" items="[[_entities]]" as="entity">
<tr>
<td>
<paper-icon-button
on-click="entityMoreInfo"
icon="hass:open-in-new"
alt="More Info"
title="More Info"
>
</paper-icon-button>
<a href="#" on-click="entitySelected">[[entity.entity_id]]</a>
</td>
<td>[[entity.state]]</td>
<template
is="dom-if"
if="[[computeShowAttributes(narrow, _showAttributes)]]"
>
<td>[[attributeString(entity)]]</td>
</template>
</tr>
</template> </template>
</table> </tr>
</div> </template>
</app-header-layout> </table>
`; `;
} }
@ -351,4 +334,4 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
} }
customElements.define("ha-panel-dev-state", HaPanelDevState); customElements.define("developer-tools-state", HaPanelDevState);

View File

@ -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/paper-input/paper-textarea"; import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { timeOut } from "@polymer/polymer/lib/utils/async"; import { timeOut } from "@polymer/polymer/lib/utils/async";
@ -8,8 +5,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-menu-button"; import "../../../resources/ha-style";
import "../../resources/ha-style";
class HaPanelDevTemplate extends PolymerElement { class HaPanelDevTemplate extends PolymerElement {
static get template() { static get template() {
@ -67,52 +63,43 @@ class HaPanelDevTemplate extends PolymerElement {
} }
</style> </style>
<app-header-layout has-scrolling-region> <div class$="[[computeFormClasses(narrow)]]">
<app-header slot="header" fixed> <div class="edit-pane">
<app-toolbar> <p>
<ha-menu-button></ha-menu-button> Templates are rendered using the Jinja2 template engine with some
<div main-title>Templates</div> Home Assistant specific extensions.
</app-toolbar> </p>
</app-header> <ul>
<li>
<div class$="[[computeFormClasses(narrow)]]"> <a
<div class="edit-pane"> href="http://jinja.pocoo.org/docs/dev/templates/"
<p> target="_blank"
Templates are rendered using the Jinja2 template engine with some >Jinja2 template documentation</a
Home Assistant specific extensions. >
</p> </li>
<ul> <li>
<li> <a
<a href="https://home-assistant.io/docs/configuration/templating/"
href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank"
target="_blank" >Home Assistant template extensions</a
>Jinja2 template documentation</a >
> </li>
</li> </ul>
<li> <paper-textarea
<a label="Template editor"
href="https://home-assistant.io/docs/configuration/templating/" value="{{template}}"
target="_blank" autofocus
>Home Assistant template extensions</a ></paper-textarea>
>
</li>
</ul>
<paper-textarea
label="Template editor"
value="{{template}}"
autofocus
></paper-textarea>
</div>
<div class="render-pane">
<paper-spinner
class="render-spinner"
active="[[rendering]]"
></paper-spinner>
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
</div>
</div> </div>
</app-header-layout>
<div class="render-pane">
<paper-spinner
class="render-spinner"
active="[[rendering]]"
></paper-spinner>
<pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre>
</div>
</div>
`; `;
} }
@ -207,4 +194,4 @@ For loop example:
} }
} }
customElements.define("ha-panel-dev-template", HaPanelDevTemplate); customElements.define("developer-tools-template", HaPanelDevTemplate);