mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
commit
3be4b9d79b
@ -8,7 +8,7 @@ trigger:
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.3-3.7-alpine3.10'
|
||||
value: '1.10.1-3.7-alpine3.11'
|
||||
- name: versionNode
|
||||
value: '12.1'
|
||||
- group: twine
|
||||
|
BIN
gallery/public/images/frenck.jpg
Normal file
BIN
gallery/public/images/frenck.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
gallery/public/images/netflix.jpg
Normal file
BIN
gallery/public/images/netflix.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -34,6 +34,7 @@ export const createMediaPlayerEntities = () => [
|
||||
media_content_type: "movie",
|
||||
media_title: "Epic sax guy 10 hours",
|
||||
app_name: "YouTube",
|
||||
entity_picture: "/images/frenck.jpg",
|
||||
supported_features: 33,
|
||||
}),
|
||||
getEntity("media_player", "living_room", "playing", {
|
||||
@ -42,6 +43,7 @@ export const createMediaPlayerEntities = () => [
|
||||
media_title: "Chapter 1",
|
||||
media_series_title: "House of Cards",
|
||||
app_name: "Netflix",
|
||||
entity_picture: "/images/netflix.jpg",
|
||||
supported_features: 1,
|
||||
}),
|
||||
getEntity("media_player", "sonos_idle", "idle", {
|
||||
|
@ -93,7 +93,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
|
@ -107,7 +107,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title="${this.addon.name} ${this.addon
|
||||
.last_version} is available"
|
||||
.version_latest} is available"
|
||||
.description="You are currently running version ${this.addon
|
||||
.version}"
|
||||
icon="hassio:arrow-up-bold-circle"
|
||||
@ -179,7 +179,7 @@ class HassioAddonInfo extends LitElement {
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
${this.addon.last_version}
|
||||
${this.addon.version_latest}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
@ -636,7 +636,7 @@ class HassioAddonInfo extends LitElement {
|
||||
this.addon &&
|
||||
!this.addon.detached &&
|
||||
this.addon.version &&
|
||||
this.addon.version !== this.addon.last_version
|
||||
this.addon.version !== this.addon.version_latest
|
||||
);
|
||||
}
|
||||
|
||||
@ -661,8 +661,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
private get _computeCannotIngressSidebar(): boolean {
|
||||
return (
|
||||
!this.addon.ingress ||
|
||||
!atLeastVersion(this.hass.connection.haVersion, 0, 92)
|
||||
!this.addon.ingress || !atLeastVersion(this.hass.config.version, 0, 92)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ class HassioAddons extends LitElement {
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
|
@ -40,8 +40,8 @@ export class HassioUpdate extends LitElement {
|
||||
].filter((value) => {
|
||||
return (
|
||||
!!value &&
|
||||
(value.last_version
|
||||
? value.version !== value.last_version
|
||||
(value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: false)
|
||||
@ -68,26 +68,26 @@ export class HassioUpdate extends LitElement {
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
this.hassInfo.version,
|
||||
this.hassInfo.last_version,
|
||||
this.hassInfo.version_latest,
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.hassInfo.last_version.includes("b") ? "rc" : "www"
|
||||
this.hassInfo.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`,
|
||||
"hassio:home-assistant"
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.last_version,
|
||||
this.supervisorInfo.version_latest,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
"hassio/hassos/update",
|
||||
"hassio/os/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
)
|
||||
: ""}
|
||||
|
@ -87,8 +87,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme,
|
||||
true
|
||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
||||
);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
|
@ -100,7 +100,7 @@ class HassioHostInfo extends LitElement {
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/hassos/config/sync"
|
||||
path="hassio/os/config/sync"
|
||||
title="Load HassOS configs or updates from USB"
|
||||
>Import from USB</ha-call-api-button
|
||||
>
|
||||
@ -108,9 +108,7 @@ class HassioHostInfo extends LitElement {
|
||||
: ""}
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
path="hassio/hassos/update"
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/os/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
`
|
||||
|
@ -41,7 +41,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latest version</td>
|
||||
<td>${this.supervisorInfo.last_version}</td>
|
||||
<td>${this.supervisorInfo.version_latest}</td>
|
||||
</tr>
|
||||
${this.supervisorInfo.channel !== "stable"
|
||||
? html`
|
||||
@ -63,7 +63,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
|
||||
>Reload</ha-call-api-button
|
||||
>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.last_version
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200318.1",
|
||||
version="20200330.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { derivedStyles } from "../../resources/styles";
|
||||
import { HomeAssistant, Theme } from "../../types";
|
||||
|
||||
interface ProcessedTheme {
|
||||
keys: { [key: string]: "" };
|
||||
styles: { [key: string]: string };
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): string | null => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
@ -15,67 +21,82 @@ const hexToRgb = (hex: string): string | null => {
|
||||
: null;
|
||||
};
|
||||
|
||||
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
|
||||
|
||||
/**
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS Theme information
|
||||
* localTheme: selected theme.
|
||||
* updateMeta: boolean if we should update the theme-color meta element.
|
||||
* selectedTheme: selected theme.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes,
|
||||
localTheme,
|
||||
updateMeta = false
|
||||
themes: HomeAssistant["themes"],
|
||||
selectedTheme?: string
|
||||
) => {
|
||||
if (!element._themes) {
|
||||
element._themes = {};
|
||||
}
|
||||
let themeName = themes.default_theme;
|
||||
if (localTheme === "default" || (localTheme && themes.themes[localTheme])) {
|
||||
themeName = localTheme;
|
||||
}
|
||||
const styles = { ...element._themes };
|
||||
if (themeName !== "default") {
|
||||
const theme = { ...derivedStyles, ...themes.themes[themeName] };
|
||||
Object.keys(theme).forEach((key) => {
|
||||
const prefixedKey = `--${key}`;
|
||||
element._themes[prefixedKey] = "";
|
||||
styles[prefixedKey] = theme[key];
|
||||
if (key.startsWith("rgb")) {
|
||||
return;
|
||||
}
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
return;
|
||||
}
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
element._themes[prefixedRgbKey] = "";
|
||||
const rgbValue = hexToRgb(theme[key]);
|
||||
if (rgbValue !== null) {
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (element.updateStyles) {
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
const newTheme = selectedTheme
|
||||
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
|
||||
: undefined;
|
||||
|
||||
if (!updateMeta) {
|
||||
if (!element._themes && !newTheme) {
|
||||
// No styles to reset, and no styles to set
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = document.querySelector("meta[name=theme-color]");
|
||||
if (meta) {
|
||||
if (!meta.hasAttribute("default-content")) {
|
||||
meta.setAttribute("default-content", meta.getAttribute("content")!);
|
||||
}
|
||||
const themeColor =
|
||||
styles["--primary-color"] || meta.getAttribute("default-content");
|
||||
meta.setAttribute("content", themeColor);
|
||||
// Add previous set keys to reset them, and new theme
|
||||
const styles = { ...element._themes, ...newTheme?.styles };
|
||||
element._themes = newTheme?.keys;
|
||||
|
||||
// Set and/or reset styles
|
||||
if (element.updateStyles) {
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// Implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
};
|
||||
|
||||
const processTheme = (
|
||||
themeName: string,
|
||||
themes: HomeAssistant["themes"]
|
||||
): ProcessedTheme | undefined => {
|
||||
if (!themes.themes[themeName]) {
|
||||
return;
|
||||
}
|
||||
const theme: Theme = {
|
||||
...derivedStyles,
|
||||
...themes.themes[themeName],
|
||||
};
|
||||
const styles = {};
|
||||
const keys = {};
|
||||
for (const key of Object.keys(theme)) {
|
||||
const prefixedKey = `--${key}`;
|
||||
const value = theme[key];
|
||||
styles[prefixedKey] = value;
|
||||
keys[prefixedKey] = "";
|
||||
|
||||
// Try to create a rgb value for this key if it is a hex color
|
||||
if (!value.startsWith("#")) {
|
||||
// Not a hex color
|
||||
continue;
|
||||
}
|
||||
const rgbKey = `rgb-${key}`;
|
||||
if (theme[rgbKey] !== undefined) {
|
||||
// Theme has it's own rgb value
|
||||
continue;
|
||||
}
|
||||
const rgbValue = hexToRgb(value);
|
||||
if (rgbValue !== null) {
|
||||
const prefixedRgbKey = `--${rgbKey}`;
|
||||
styles[prefixedRgbKey] = rgbValue;
|
||||
keys[prefixedRgbKey] = "";
|
||||
}
|
||||
}
|
||||
PROCESSED_THEMES[themeName] = { styles, keys };
|
||||
return { styles, keys };
|
||||
};
|
||||
|
||||
export const invalidateThemeCache = () => {
|
||||
PROCESSED_THEMES = {};
|
||||
};
|
||||
|
@ -4,17 +4,58 @@ import { domainIcon } from "./domain_icon";
|
||||
|
||||
export const coverIcon = (state: HassEntity): string => {
|
||||
const open = state.state !== "closed";
|
||||
|
||||
switch (state.attributes.device_class) {
|
||||
case "garage":
|
||||
return open ? "hass:garage-open" : "hass:garage";
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:garage";
|
||||
default:
|
||||
return "hass:garage-open";
|
||||
}
|
||||
case "gate":
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
case "closing":
|
||||
return "hass:gate-arrow-right";
|
||||
case "closed":
|
||||
return "hass:gate";
|
||||
default:
|
||||
return "hass:gate-open";
|
||||
}
|
||||
case "door":
|
||||
return open ? "hass:door-open" : "hass:door-closed";
|
||||
case "damper":
|
||||
return open ? "hass:circle" : "hass:circle-slice-8";
|
||||
case "shutter":
|
||||
return open ? "hass:window-shutter-open" : "hass:window-shutter";
|
||||
case "blind":
|
||||
return open ? "hass:blinds-open" : "hass:blinds";
|
||||
case "curtain":
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:blinds";
|
||||
default:
|
||||
return "hass:blinds-open";
|
||||
}
|
||||
case "window":
|
||||
return open ? "hass:window-open" : "hass:window-closed";
|
||||
switch (state.state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
default:
|
||||
return domainIcon("cover", state.state);
|
||||
}
|
||||
|
@ -77,7 +77,16 @@ export const domainIcon = (domain: string, state?: string): string => {
|
||||
: "hass:checkbox-marked-circle";
|
||||
|
||||
case "cover":
|
||||
return state === "closed" ? "hass:window-closed" : "hass:window-open";
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
|
||||
case "lock":
|
||||
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
|
||||
|
@ -69,9 +69,10 @@ export interface DataTableSortColumnData {
|
||||
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric" | "icon";
|
||||
type?: "numeric" | "icon" | "icon-button";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
}
|
||||
|
||||
@ -227,10 +228,13 @@ export class HaDataTable extends LitElement {
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
@ -241,9 +245,8 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: String(
|
||||
column.width
|
||||
),
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth || "",
|
||||
})
|
||||
: ""}
|
||||
role="columnheader"
|
||||
@ -318,10 +321,13 @@ export class HaDataTable extends LitElement {
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
column.type === "icon"
|
||||
),
|
||||
"mdc-data-table__cell--icon-button": Boolean(
|
||||
column.type === "icon-button"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
})}"
|
||||
@ -329,7 +335,10 @@ export class HaDataTable extends LitElement {
|
||||
? styleMap({
|
||||
[column.grows
|
||||
? "minWidth"
|
||||
: "width"]: String(column.width),
|
||||
: "width"]: column.width,
|
||||
maxWidth: column.maxWidth
|
||||
? column.maxWidth
|
||||
: "",
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
@ -532,6 +541,7 @@ export class HaDataTable extends LitElement {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell.mdc-data-table__cell--icon {
|
||||
@ -544,7 +554,7 @@ export class HaDataTable extends LitElement {
|
||||
padding-left: 16px;
|
||||
/* @noflip */
|
||||
padding-right: 0;
|
||||
width: 40px;
|
||||
width: 56px;
|
||||
}
|
||||
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
|
||||
.mdc-data-table__header-cell--checkbox[dir="rtl"],
|
||||
@ -591,7 +601,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__header-cell--icon,
|
||||
.mdc-data-table__cell--icon {
|
||||
width: 24px;
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
|
||||
@ -610,6 +620,28 @@ export class HaDataTable extends LitElement {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
width: 56px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button:first-child,
|
||||
.mdc-data-table__cell--icon-button:first-child {
|
||||
width: 64px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button:last-child,
|
||||
.mdc-data-table__cell--icon-button:last-child {
|
||||
width: 64px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon-button a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@ -695,6 +727,9 @@ export class HaDataTable extends LitElement {
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.scroller {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
@ -246,7 +246,7 @@ class HaEntityPicker extends LitElement {
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
padding: 0px 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
|
@ -19,7 +19,7 @@ class HaCoverControls extends PolymerElement {
|
||||
<div class="state">
|
||||
<paper-icon-button
|
||||
aria-label="Open cover"
|
||||
icon="hass:arrow-up"
|
||||
icon="[[computeOpenIcon(stateObj)]]"
|
||||
on-click="onOpenTap"
|
||||
invisible$="[[!entityObj.supportsOpen]]"
|
||||
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
|
||||
@ -32,7 +32,7 @@ class HaCoverControls extends PolymerElement {
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
aria-label="Close cover"
|
||||
icon="hass:arrow-down"
|
||||
icon="[[computeCloseIcon(stateObj)]]"
|
||||
on-click="onCloseTap"
|
||||
invisible$="[[!entityObj.supportsClose]]"
|
||||
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
|
||||
@ -60,6 +60,26 @@ class HaCoverControls extends PolymerElement {
|
||||
return new CoverEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computeOpenIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "gate":
|
||||
return "hass:arrow-expand-horizontal";
|
||||
default:
|
||||
return "hass:arrow-up";
|
||||
}
|
||||
}
|
||||
|
||||
computeCloseIcon(stateObj) {
|
||||
switch (stateObj.attributes.device_class) {
|
||||
case "awning":
|
||||
case "gate":
|
||||
return "hass:arrow-collapse-horizontal";
|
||||
default:
|
||||
return "hass:arrow-down";
|
||||
}
|
||||
}
|
||||
|
||||
computeOpenDisabled(stateObj, entityObj) {
|
||||
var assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
|
||||
|
@ -32,7 +32,7 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { compare } from "../common/string/compare";
|
||||
import { getDefaultPanelUrlPath, getDefaultPanel } from "../data/panel";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
|
||||
@ -87,10 +87,8 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
const defaultPage = getDefaultPanelUrlPath();
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title || panel.url_path === defaultPage) {
|
||||
if (!panel.title || panel.url_path === hass.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
@ -143,7 +141,7 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanel(hass.panels);
|
||||
const defaultPanel = getDefaultPanel(hass);
|
||||
|
||||
return html`
|
||||
<div class="menu">
|
||||
@ -297,7 +295,8 @@ class HaSidebar extends LitElement {
|
||||
hass.panelUrl !== oldHass.panelUrl ||
|
||||
hass.user !== oldHass.user ||
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.states !== oldHass.states
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
);
|
||||
}
|
||||
|
||||
@ -530,6 +529,7 @@ class HaSidebar extends LitElement {
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
background: none;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -12,9 +12,9 @@ export const callAlarmAction = (
|
||||
| "arm_night"
|
||||
| "arm_custom_bypass"
|
||||
| "disarm",
|
||||
code: string
|
||||
code?: string
|
||||
) => {
|
||||
hass!.callService("alarm_control_panel", "alarm_" + action, {
|
||||
hass!.callService("alarm_control_panel", `alarm_${action}`, {
|
||||
entity_id: entity,
|
||||
code,
|
||||
});
|
||||
|
@ -25,3 +25,16 @@ export const fetchAuthProviders = () =>
|
||||
fetch("/auth/providers", {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
export const createAuthForUser = async (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
username: string,
|
||||
password: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/auth_provider/homeassistant/create",
|
||||
user_id: userId,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
@ -53,6 +53,9 @@ export const fallbackDeviceName = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
|
||||
devices.filter((device) => device.area_id === areaId);
|
||||
|
||||
export const updateDeviceRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string,
|
||||
|
@ -27,6 +27,16 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
new_entity_id?: string;
|
||||
}
|
||||
|
||||
export const findBatteryEntity = (
|
||||
hass: HomeAssistant,
|
||||
entities: EntityRegistryEntry[]
|
||||
): EntityRegistryEntry | undefined =>
|
||||
entities.find(
|
||||
(entity) =>
|
||||
hass.states[entity.entity_id] &&
|
||||
hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||
);
|
||||
|
||||
export const computeEntityRegistryName = (
|
||||
hass: HomeAssistant,
|
||||
entry: EntityRegistryEntry
|
||||
|
1
src/data/graph.ts
Normal file
1
src/data/graph.ts
Normal file
@ -0,0 +1 @@
|
||||
export const strokeWidth = 5;
|
@ -29,7 +29,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
arch: "armhf" | "aarch64" | "i386" | "amd64";
|
||||
machine: any;
|
||||
homeassistant: string;
|
||||
last_version: string;
|
||||
version_latest: string;
|
||||
boot: "auto" | "manual";
|
||||
build: boolean;
|
||||
options: object;
|
||||
|
@ -23,7 +23,7 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
|
||||
"GET",
|
||||
"hassio/hassos/info"
|
||||
"hassio/os/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
|
||||
"GET",
|
||||
"hassio/homeassistant/info"
|
||||
"hassio/core/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -17,3 +17,12 @@ export const subscribeMQTTTopic = (
|
||||
topic,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeMQTTDeviceEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "mqtt/device/remove",
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
@ -1,13 +1,20 @@
|
||||
import { HomeAssistant, PanelInfo } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
const DEFAULT_PANEL = "lovelace";
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
export const getDefaultPanelUrlPath = () =>
|
||||
localStorage.defaultPage || DEFAULT_PANEL;
|
||||
export const getStorageDefaultPanelUrlPath = () =>
|
||||
localStorage.defaultPanel
|
||||
? JSON.parse(localStorage.defaultPanel)
|
||||
: DEFAULT_PANEL;
|
||||
|
||||
export const getDefaultPanel = (panels: HomeAssistant["panels"]) =>
|
||||
panels[localStorage.defaultPage] || panels[DEFAULT_PANEL];
|
||||
export const setDefaultPanel = (element: HTMLElement, urlPath: string) => {
|
||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant) =>
|
||||
hass.panels[hass.defaultPanel];
|
||||
|
||||
export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
if (!hass.panels) {
|
||||
@ -22,14 +29,13 @@ export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.url_path === "lovelace") {
|
||||
return hass.localize("panel.states");
|
||||
}
|
||||
|
||||
if (panel.url_path === "profile") {
|
||||
return hass.localize("panel.profile");
|
||||
}
|
||||
|
||||
return (
|
||||
hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
// default panel
|
||||
(hass.panels[localStorage.defaultPage] || hass.panels[DEFAULT_PANEL]).title!
|
||||
);
|
||||
return hass.localize(`panel.${panel.title}`) || panel.title || undefined;
|
||||
};
|
||||
|
@ -5,6 +5,8 @@ export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
|
||||
export const SYSTEM_GROUP_ID_USER = "system-users";
|
||||
export const SYSTEM_GROUP_ID_READ_ONLY = "system-read-only";
|
||||
|
||||
export const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -15,7 +17,7 @@ export interface User {
|
||||
credentials: Credential[];
|
||||
}
|
||||
|
||||
interface UpdateUserParams {
|
||||
export interface UpdateUserParams {
|
||||
name?: User["name"];
|
||||
group_ids?: User["group_ids"];
|
||||
}
|
||||
@ -25,10 +27,16 @@ export const fetchUsers = async (hass: HomeAssistant) =>
|
||||
type: "config/auth/list",
|
||||
});
|
||||
|
||||
export const createUser = async (hass: HomeAssistant, name: string) =>
|
||||
export const createUser = async (
|
||||
hass: HomeAssistant,
|
||||
name: string,
|
||||
// tslint:disable-next-line: variable-name
|
||||
group_ids?: User["group_ids"]
|
||||
) =>
|
||||
hass.callWS<{ user: User }>({
|
||||
type: "config/auth/create",
|
||||
name,
|
||||
group_ids,
|
||||
});
|
||||
|
||||
export const updateUser = async (
|
||||
|
@ -130,7 +130,11 @@ class DataEntryFlowDialog extends LitElement {
|
||||
>
|
||||
${this._loading || (this._step === null && this._handlers === undefined)
|
||||
? html`
|
||||
<step-flow-loading></step-flow-loading>
|
||||
<step-flow-loading
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.loading_first_time"
|
||||
)}
|
||||
></step-flow-loading>
|
||||
`
|
||||
: this._step === undefined
|
||||
? // When we are going to next step, we render 1 round of empty
|
||||
|
@ -5,14 +5,22 @@ import {
|
||||
css,
|
||||
customElement,
|
||||
CSSResult,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
|
||||
@customElement("step-flow-loading")
|
||||
class StepFlowLoading extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="init-spinner">
|
||||
${this.label
|
||||
? html`
|
||||
<div>${this.label}</div>
|
||||
`
|
||||
: ""}
|
||||
<paper-spinner-lite active></paper-spinner-lite>
|
||||
</div>
|
||||
`;
|
||||
|
@ -142,6 +142,9 @@ class DialogBox extends LitElement {
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TemplateResult } from "lit-html";
|
||||
|
||||
interface BaseDialogParams {
|
||||
confirmText?: string;
|
||||
text?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
@ -92,12 +92,12 @@ window.hassConnection.then(({ conn }) => {
|
||||
subscribeFrontendUserData(conn, "core", noop);
|
||||
|
||||
if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) {
|
||||
(window as WindowWithLovelaceProm).llConfProm = fetchConfig(
|
||||
conn,
|
||||
null,
|
||||
false
|
||||
);
|
||||
(window as WindowWithLovelaceProm).llResProm = fetchResources(conn);
|
||||
const llWindow = window as WindowWithLovelaceProm;
|
||||
llWindow.llConfProm = fetchConfig(conn, null, false);
|
||||
llWindow.llConfProm.catch(() => {
|
||||
// Ignore it, it is handled by Lovelace panel.
|
||||
});
|
||||
llWindow.llResProm = fetchResources(conn);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ export const demoConfig: HassConfig = {
|
||||
temperature: "°C",
|
||||
volume: "L",
|
||||
},
|
||||
components: ["notify.html5", "history"],
|
||||
components: ["notify.html5", "history", "shopping_list"],
|
||||
time_zone: "America/Los_Angeles",
|
||||
config_dir: "/config",
|
||||
version: "DEMO",
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import {
|
||||
applyThemesOnElement,
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
|
||||
import { demoConfig } from "./demo_config";
|
||||
import { demoServices } from "./demo_services";
|
||||
@ -8,6 +11,7 @@ import { HomeAssistant } from "../types";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { getLocalLanguage } from "../util/hass-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
|
||||
const ensureArray = <T>(val: T | T[]): T[] =>
|
||||
Array.isArray(val) ? val : [val];
|
||||
@ -169,6 +173,7 @@ export const provideHass = (
|
||||
name: "Demo User",
|
||||
},
|
||||
panelUrl: "lovelace",
|
||||
defaultPanel: DEFAULT_PANEL,
|
||||
|
||||
language: localLanguage,
|
||||
selectedLanguage: localLanguage,
|
||||
@ -224,6 +229,7 @@ export const provideHass = (
|
||||
(eventListeners[event] || []).forEach((fn) => fn(event));
|
||||
},
|
||||
mockTheme(theme) {
|
||||
invalidateThemeCache();
|
||||
hass().updateHass({
|
||||
selectedTheme: theme ? "mock" : "default",
|
||||
themes: {
|
||||
@ -237,8 +243,7 @@ export const provideHass = (
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
selectedTheme,
|
||||
true
|
||||
selectedTheme as string
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -88,6 +88,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${this.tabs}
|
||||
>
|
||||
<div slot="toolbar-icon"><slot name="toolbar-icon"></slot></div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
|
@ -10,7 +10,7 @@ import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import { Route, HomeAssistant } from "../types";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
|
||||
export class HomeAssistantAppEl extends HassElement {
|
||||
@property() private _route?: Route;
|
||||
@ -86,7 +86,7 @@ export class HomeAssistantAppEl extends HassElement {
|
||||
this._route === undefined &&
|
||||
(route.path === "" || route.path === "/")
|
||||
) {
|
||||
navigate(window, `/${getDefaultPanelUrlPath()}`, true);
|
||||
navigate(window, `/${getStorageDefaultPanelUrlPath()}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -109,9 +109,9 @@ class DialogAreaDetail extends LitElement {
|
||||
name: this._name.trim(),
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry(values);
|
||||
await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
await this._params!.createEntry!(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
@ -124,7 +124,7 @@ class DialogAreaDetail extends LitElement {
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
|
397
src/panels/config/areas/ha-config-area-page.ts
Normal file
397
src/panels/config/areas/ha-config-area-page.ts
Normal file
@ -0,0 +1,397 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../components/dialog/ha-paper-dialog";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
devicesInArea,
|
||||
computeDeviceName,
|
||||
} from "../../../data/device_registry";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { RelatedResult, findRelated } from "../../../data/search";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public areaId!: string;
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public showAdvanced!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _related?: RelatedResult;
|
||||
|
||||
private _area = memoizeOne((areaId: string, areas: AreaRegistryEntry[]):
|
||||
| AreaRegistryEntry
|
||||
| undefined => areas.find((area) => area.area_id === areaId));
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(areaId: string, devices: DeviceRegistryEntry[]): DeviceRegistryEntry[] =>
|
||||
devicesInArea(devices, areaId)
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("areaId")) {
|
||||
this._findRelated();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const area = this._area(this.areaId, this.areas);
|
||||
|
||||
if (!area) {
|
||||
return html`
|
||||
<hass-error-screen
|
||||
error="${this.hass.localize("ui.panel.config.areas.area_not_found")}"
|
||||
></hass-error-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
const devices = this._devices(this.areaId, this.devices);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
>
|
||||
${this.narrow
|
||||
? html`
|
||||
<span slot="header">
|
||||
${area.name}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:settings"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
></paper-icon-button>
|
||||
|
||||
<div class="container">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div class="fullwidth">
|
||||
<h1>${area.name}</h1>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="column">
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>${devices.length
|
||||
? devices.map(
|
||||
(device) =>
|
||||
html`
|
||||
<a href="/config/devices/device/${device.id}">
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.no_devices"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "automation")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)}
|
||||
>${this._related?.automation?.length
|
||||
? this._related.automation.map((automation) => {
|
||||
const state = this.hass.states[automation];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/automation/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)}
|
||||
>${this._related?.scene?.length
|
||||
? this._related.scene.map((scene) => {
|
||||
const state = this.hass.states[scene];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/scene/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass, "script")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)}
|
||||
>${this._related?.script?.length
|
||||
? this._related.script.map((script) => {
|
||||
const state = this.hass.states[script];
|
||||
return state
|
||||
? html`
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/script/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
|
||||
private _showSettings(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
updateEntry: async (values) =>
|
||||
updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-headline_-_font-size);
|
||||
font-weight: var(--paper-font-headline_-_font-weight);
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: auto;
|
||||
max-width: 1000px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.column {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-area-page": HaConfigAreaPage;
|
||||
}
|
||||
}
|
200
src/panels/config/areas/ha-config-areas-dashboard.ts
Normal file
200
src/panels/config/areas/ha-config-areas-dashboard.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import {
|
||||
devicesInArea,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areas: AreaRegistryEntry[], devices: DeviceRegistryEntry[]) => {
|
||||
return areas.map((area) => {
|
||||
return {
|
||||
...area,
|
||||
devices: devicesInArea(devices, area.area_id).length,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.area"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
devices: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.data_table.devices"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.integrations}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._areas(this.areas, this.devices)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.no_areas"
|
||||
)}
|
||||
id="area_id"
|
||||
>
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:help-circle"
|
||||
@click=${this._showHelp}
|
||||
></paper-icon-button>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_area"
|
||||
)}"
|
||||
@click=${this._createArea}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
private _createArea() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.areas.caption"),
|
||||
text: html`
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction2")}
|
||||
</p>
|
||||
<a href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.integrations_page"
|
||||
)}
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const areaId = ev.detail.id;
|
||||
navigate(this, `/config/areas/area/${areaId}`);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) =>
|
||||
createAreaRegistryEntry(this.hass!, values),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
hass-loading-screen {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab.rtl {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
ha-fab[is-wide].rtl {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,226 +1,120 @@
|
||||
import "./ha-config-areas-dashboard";
|
||||
import "./ha-config-area-page";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
deleteAreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
AreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
showAreaRegistryDetailDialog,
|
||||
loadAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../layouts/hass-router-page";
|
||||
import { property, customElement, PropertyValues } from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-areas")
|
||||
export class HaConfigAreas extends LitElement {
|
||||
class HaConfigAreas extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public isWide?: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
private _unsubAreas?: UnsubscribeFunc;
|
||||
@property() public isWide!: boolean;
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ha-config-areas-dashboard",
|
||||
cache: true,
|
||||
},
|
||||
area: {
|
||||
tag: "ha-config-area-page",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@property() private _configEntries: ConfigEntry[] = [];
|
||||
@property() private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
||||
@property() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _unsubs?: UnsubscribeFunc[];
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubAreas) {
|
||||
this._unsubAreas();
|
||||
if (this._unsubs) {
|
||||
while (this._unsubs.length) {
|
||||
this._unsubs.pop()!();
|
||||
}
|
||||
this._unsubs = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._areas === undefined) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.integrations}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">
|
||||
${this.hass.localize("ui.panel.config.areas.picker.header")}
|
||||
</span>
|
||||
<span slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.areas.picker.introduction")}
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.introduction2"
|
||||
)}
|
||||
</p>
|
||||
<a href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.integrations_page"
|
||||
)}
|
||||
</a>
|
||||
</span>
|
||||
<ha-card>
|
||||
${this._areas.map((entry) => {
|
||||
return html`
|
||||
<paper-item @click=${this._openEditEntry} .entry=${entry}>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
${this._areas.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
${this.hass.localize("ui.panel.config.areas.no_areas")}
|
||||
<mwc-button @click=${this._createArea}>
|
||||
${this.hass.localize("ui.panel.config.areas.create_area")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize("ui.panel.config.areas.create_area")}"
|
||||
@click=${this._createArea}
|
||||
class="${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
})}"
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubAreas) {
|
||||
this._unsubAreas = subscribeAreaRegistry(
|
||||
this.hass.connection,
|
||||
(areas) => {
|
||||
this._areas = areas;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _createArea() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _openEditEntry(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
private _openDialog(entry?: AreaRegistryEntry) {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) =>
|
||||
createAreaRegistryEntry(this.hass!, values),
|
||||
updateEntry: async (values) =>
|
||||
updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.areas.delete.confirmation_text"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
this.addEventListener("hass-reload-entries", () => {
|
||||
this._loadData();
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
hass-loading-screen {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 16px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubs && changedProps.has("hass")) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab.rtl {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
|
||||
ha-fab[is-wide].rtl {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
if (this._currentPage === "area") {
|
||||
pageEl.areaId = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.devices = this._deviceRegistryEntries;
|
||||
pageEl.areas = this._areas;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries.sort((conf1, conf2) =>
|
||||
compare(conf1.title, conf2.title)
|
||||
);
|
||||
});
|
||||
if (this._unsubs) {
|
||||
return;
|
||||
}
|
||||
this._unsubs = [
|
||||
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||
this._deviceRegistryEntries = entries;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-areas": HaConfigAreas;
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import {
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
createEntry: (values: AreaRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry: (
|
||||
createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (
|
||||
updates: Partial<AreaRegistryEntryMutableParams>
|
||||
) => Promise<unknown>;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
removeEntry?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadAreaRegistryDetailDialog = () =>
|
||||
|
@ -7,18 +7,13 @@ import {
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
||||
import "../ha-config-section";
|
||||
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@ -27,12 +22,16 @@ import {
|
||||
AutomationEntity,
|
||||
showAutomationEditor,
|
||||
AutomationConfig,
|
||||
triggerAutomation,
|
||||
} from "../../../data/automation";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showThingtalkDialog } from "./show-dialog-thingtalk";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ha-automation-picker")
|
||||
class HaAutomationPicker extends LitElement {
|
||||
@ -42,139 +41,152 @@ class HaAutomationPicker extends LitElement {
|
||||
@property() public route!: Route;
|
||||
@property() public automations!: AutomationEntity[];
|
||||
|
||||
private _automations = memoizeOne((automations: AutomationEntity[]) => {
|
||||
return automations.map((automation) => {
|
||||
return {
|
||||
...automation,
|
||||
name: computeStateName(automation),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
toggle: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_toggle, automation) =>
|
||||
html`
|
||||
<ha-entity-toggle
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
></ha-entity-toggle>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, automation: any) => html`
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${automation.attributes.last_triggered
|
||||
? formatDateTime(
|
||||
new Date(automation.attributes.last_triggered),
|
||||
this.hass.language
|
||||
)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.execute = {
|
||||
title: "",
|
||||
template: (_info, automation) => html`
|
||||
<mwc-button .automation=${automation} @click=${this._execute}>
|
||||
${this.hass.localize("ui.card.automation.trigger")}
|
||||
</mwc-button>
|
||||
`,
|
||||
};
|
||||
}
|
||||
columns.info = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, automation) => html`
|
||||
<paper-icon-button
|
||||
.automation=${automation}
|
||||
@click=${this._showInfo}
|
||||
icon="hass:information-outline"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_info_automation"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, automation: any) => html`
|
||||
<a
|
||||
href=${ifDefined(
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${automation.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-icon-button
|
||||
.icon=${automation.attributes.id
|
||||
? "hass:pencil"
|
||||
: "hass:pencil-off"}
|
||||
.disabled=${!automation.attributes.id}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_info_automation"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
${!automation.attributes.id
|
||||
? html`
|
||||
<paper-tooltip position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.only_editable"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._automations(this.automations)}
|
||||
id="entity_id"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.no_automations"
|
||||
)}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.header")}
|
||||
</div>
|
||||
<div slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.introduction"
|
||||
)}
|
||||
<p>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/automation/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.learn_more"
|
||||
)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-card
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.pick_automation"
|
||||
)}
|
||||
>
|
||||
${this.automations.length === 0
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.no_automations"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: this.automations.map(
|
||||
(automation) => html`
|
||||
|
||||
<div class='automation'>
|
||||
<ha-entity-toggle
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
></ha-entity-toggle>
|
||||
|
||||
<paper-item-body two-line>
|
||||
<div>${computeStateName(automation)}</div>
|
||||
<div secondary>
|
||||
${this.hass.localize(
|
||||
"ui.card.automation.last_triggered"
|
||||
)}: ${
|
||||
automation.attributes.last_triggered
|
||||
? formatDateTime(
|
||||
new Date(automation.attributes.last_triggered),
|
||||
this.hass.language
|
||||
)
|
||||
: this.hass.localize("ui.components.relative_time.never")
|
||||
}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<div class='actions'>
|
||||
<paper-icon-button
|
||||
.automation=${automation}
|
||||
@click=${this._showInfo}
|
||||
icon="hass:information-outline"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_info_automation"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${automation.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-icon-button
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.edit_automation"
|
||||
)}"
|
||||
icon="hass:pencil"
|
||||
.disabled=${!automation.attributes.id}
|
||||
></paper-icon-button>
|
||||
${
|
||||
!automation.attributes.id
|
||||
? html`
|
||||
<paper-tooltip position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.only_editable"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
<div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.add_automation"
|
||||
)}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
@click=${this._createNew}
|
||||
></ha-fab>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.add_automation"
|
||||
)}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
@click=${this._createNew}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showInfo(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.currentTarget.automation.entity_id;
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private _execute(ev) {
|
||||
const entityId = ev.currentTarget.automation.entity_id;
|
||||
triggerAutomation(this.hass, entityId);
|
||||
}
|
||||
|
||||
private _createNew() {
|
||||
if (!isComponentLoaded(this.hass, "cloud")) {
|
||||
showAutomationEditor(this);
|
||||
@ -190,33 +202,6 @@ class HaAutomationPicker extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.automation {
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
|
||||
.automation a[href] {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-entity-toggle {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
@ -242,10 +227,6 @@ class HaAutomationPicker extends LitElement {
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { DeviceRegistryEntry } from "../../../../data/device_registry";
|
||||
import { removeMQTTDeviceEntry } from "../../../../data/mqtt";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
|
||||
@customElement("ha-device-card-mqtt")
|
||||
export class HaDeviceCardMqtt extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-button class="warning" @click="${this._confirmDeleteEntry}">
|
||||
${this.hass.localize("ui.panel.config.devices.delete")}
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _confirmDeleteEntry(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeMQTTDeviceEntry(this.hass!, this.device.id);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return haStyle;
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
} from "../../../../data/device_registry";
|
||||
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { AreaRegistryEntry } from "../../../../data/area_registry";
|
||||
|
||||
@customElement("ha-device-card")
|
||||
export class HaDeviceCard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="info">
|
||||
${this.device.model
|
||||
? html`
|
||||
<div class="model">${this.device.model}</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.manufacturer
|
||||
? html`
|
||||
<div class="manuf">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.manuf",
|
||||
"manufacturer",
|
||||
this.device.manufacturer
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.area_id
|
||||
? html`
|
||||
<div class="area">
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.area",
|
||||
"area",
|
||||
this._computeArea(this.areas, this.device)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.via_device_id
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.via"
|
||||
)}
|
||||
<span class="hub"
|
||||
>${this._computeDeviceName(
|
||||
this.devices,
|
||||
this.device.via_device_id
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.sw_version
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.firmware",
|
||||
"version",
|
||||
this.device.sw_version
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
}
|
||||
|
||||
private _computeArea(areas, device) {
|
||||
if (!areas || !device || !device.area_id) {
|
||||
return "No Area";
|
||||
}
|
||||
// +1 because of "No Area" entry
|
||||
return areas.find((area) => area.area_id === device.area_id).name;
|
||||
}
|
||||
|
||||
private _computeDeviceName(devices, deviceId) {
|
||||
const device = devices.find((dev) => dev.id === deviceId);
|
||||
return device
|
||||
? computeDeviceName(device, this.hass)
|
||||
: `(${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device_unavailable"
|
||||
)})`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
flex: 1 0 100%;
|
||||
padding-bottom: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.device {
|
||||
width: 30%;
|
||||
}
|
||||
.area {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.extra-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.manuf,
|
||||
.entity-id,
|
||||
.model {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -166,6 +166,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-icon {
|
||||
width: 40px;
|
||||
}
|
||||
@ -182,6 +185,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
#entities > * {
|
||||
margin: 8px 16px 8px 8px;
|
||||
}
|
||||
#entities > paper-icon-item {
|
||||
margin: 0;
|
||||
}
|
||||
paper-icon-item {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
|
118
src/panels/config/devices/device-detail/ha-device-info-card.ts
Normal file
118
src/panels/config/devices/device-detail/ha-device-info-card.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
} from "../../../../data/device_registry";
|
||||
import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { AreaRegistryEntry } from "../../../../data/area_registry";
|
||||
|
||||
@customElement("ha-device-info-card")
|
||||
export class HaDeviceCard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public device!: DeviceRegistryEntry;
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="Device info">
|
||||
<div class="card-content">
|
||||
${this.device.model
|
||||
? html`
|
||||
<div class="model">${this.device.model}</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.manufacturer
|
||||
? html`
|
||||
<div class="manuf">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.manuf",
|
||||
"manufacturer",
|
||||
this.device.manufacturer
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.via_device_id
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.via"
|
||||
)}
|
||||
<span class="hub"
|
||||
>${this._computeDeviceName(
|
||||
this.devices,
|
||||
this.device.via_device_id
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.device.sw_version
|
||||
? html`
|
||||
<div class="extra-info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.firmware",
|
||||
"version",
|
||||
this.device.sw_version
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
}
|
||||
|
||||
private _computeDeviceName(devices, deviceId) {
|
||||
const device = devices.find((dev) => dev.id === deviceId);
|
||||
return device
|
||||
? computeDeviceName(device, this.hass)
|
||||
: `(${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device_unavailable"
|
||||
)})`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
flex: 1 0 100%;
|
||||
padding-bottom: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.device {
|
||||
width: 30%;
|
||||
}
|
||||
.area {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.extra-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.manuf,
|
||||
.entity-id,
|
||||
.model {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -15,13 +15,15 @@ import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../ha-config-section";
|
||||
|
||||
import "./device-detail/ha-device-card";
|
||||
import "./device-detail/ha-device-info-card";
|
||||
import "./device-detail/ha-device-card-mqtt";
|
||||
import "./device-detail/ha-device-entities-card";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
findBatteryEntity,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
@ -40,9 +42,9 @@ import { createValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { RelatedResult, findRelated } from "../../../data/search";
|
||||
import { SceneEntities, showSceneEditor } from "../../../data/scene";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string;
|
||||
@ -70,6 +72,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
devices ? devices.find((device) => device.id === deviceId) : undefined
|
||||
);
|
||||
|
||||
private _integrations = memoizeOne(
|
||||
(device: DeviceRegistryEntry, entries: ConfigEntry[]): string[] =>
|
||||
entries
|
||||
.filter((entry) => device.config_entries.includes(entry.entry_id))
|
||||
.map((entry) => entry.domain)
|
||||
);
|
||||
|
||||
private _entities = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
@ -88,6 +97,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)
|
||||
);
|
||||
|
||||
private _batteryEntity = memoizeOne((entities: EntityRegistryEntry[]):
|
||||
| EntityRegistryEntry
|
||||
| undefined => findBatteryEntity(this.hass, entities));
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
@ -113,7 +126,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const integrations = this._integrations(device, this.entries);
|
||||
const entities = this._entities(this.deviceId, this.entities);
|
||||
const batteryEntity = this._batteryEntity(entities);
|
||||
const batteryState = batteryEntity
|
||||
? this.hass.states[batteryEntity.entity_id]
|
||||
: undefined;
|
||||
const areaName = this._computeAreaName(this.areas, device);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@ -139,22 +158,70 @@ export class HaConfigDevicePage extends LitElement {
|
||||
></paper-icon-button>
|
||||
|
||||
<div class="container">
|
||||
<div class="left">
|
||||
<div class="device-info">
|
||||
${
|
||||
this.narrow
|
||||
? ""
|
||||
: html`
|
||||
<div class="header fullwidth">
|
||||
${
|
||||
this.narrow
|
||||
? ""
|
||||
: html`
|
||||
<div>
|
||||
<h1>${computeDeviceName(device, this.hass)}</h1>
|
||||
`
|
||||
}
|
||||
<ha-device-card
|
||||
${areaName
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.area",
|
||||
"area",
|
||||
areaName
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="header-right">
|
||||
${
|
||||
batteryState
|
||||
? html`
|
||||
<div class="battery">
|
||||
${batteryState.state}%
|
||||
<ha-state-icon
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${batteryState}
|
||||
></ha-state-icon>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<img
|
||||
src="https://brands.home-assistant.io/${
|
||||
integrations[0]
|
||||
}/logo.png"
|
||||
srcset="
|
||||
https://brands.home-assistant.io/${
|
||||
integrations[0]
|
||||
}/logo@2x.png 2x
|
||||
"
|
||||
referrerpolicy="no-referrer"
|
||||
@load=${this._onImageLoad}
|
||||
@error=${this._onImageError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<ha-device-info-card
|
||||
.hass=${this.hass}
|
||||
.areas=${this.areas}
|
||||
.devices=${this.devices}
|
||||
.device=${device}
|
||||
></ha-device-card>
|
||||
</div>
|
||||
>
|
||||
${
|
||||
integrations.includes("mqtt")
|
||||
? html`
|
||||
<ha-device-card-mqtt
|
||||
.hass=${this.hass}
|
||||
.device=${device}
|
||||
></ha-device-card-mqtt>
|
||||
`
|
||||
: html``
|
||||
}
|
||||
</ha-device-info-card>
|
||||
|
||||
${
|
||||
entities.length
|
||||
@ -168,32 +235,46 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: html``
|
||||
}
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="column">
|
||||
${
|
||||
isComponentLoaded(this.hass, "automation")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)}
|
||||
>${this._related?.automation?.length
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations"
|
||||
)}
|
||||
<paper-icon-button
|
||||
@click=${this._showAutomationDialog}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create"
|
||||
)}
|
||||
icon="hass:plus-circle"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
${this._related?.automation?.length
|
||||
? this._related.automation.map((automation) => {
|
||||
const state = this.hass.states[automation];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<paper-item
|
||||
.automation=${state}
|
||||
@click=${this._openAutomation}
|
||||
.disabled=${!state.attributes.id}
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/automation/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item-body>
|
||||
${state.attributes.friendly_name ||
|
||||
automation}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
<paper-item
|
||||
.automation=${state}
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
@ -214,13 +295,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._showAutomationDialog}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""
|
||||
@ -230,58 +304,72 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${
|
||||
isComponentLoaded(this.hass, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)}
|
||||
>${this._related?.scene?.length
|
||||
? this._related.scene.map((scene) => {
|
||||
const state = this.hass.states[scene];
|
||||
return state
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes"
|
||||
)}
|
||||
${
|
||||
entities.length
|
||||
? html`
|
||||
<div>
|
||||
<paper-item
|
||||
.scene=${state}
|
||||
@click=${this._openScene}
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${state.attributes.friendly_name ||
|
||||
scene}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<paper-icon-button
|
||||
@click=${this._createScene}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create"
|
||||
)}
|
||||
icon="hass:plus-circle"
|
||||
></paper-icon-button>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
${entities.length
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._createScene}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
||||
${
|
||||
this._related?.scene?.length
|
||||
? this._related.scene.map((scene) => {
|
||||
const state = this.hass.states[scene];
|
||||
return state
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/scene/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item
|
||||
.scene=${state}
|
||||
.disabled=${!state.attributes.id}
|
||||
>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
${!state.attributes.id
|
||||
? html`
|
||||
<paper-tooltip
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
: html`
|
||||
<paper-item class="no-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}</paper-item
|
||||
>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
</ha-card>
|
||||
`
|
||||
: ""
|
||||
@ -289,25 +377,38 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${
|
||||
isComponentLoaded(this.hass, "script")
|
||||
? html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)}
|
||||
>${this._related?.script?.length
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts"
|
||||
)}
|
||||
<paper-icon-button
|
||||
@click=${this._showScriptDialog}
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.create"
|
||||
)}
|
||||
icon="hass:plus-circle"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
${this._related?.script?.length
|
||||
? this._related.script.map((script) => {
|
||||
const state = this.hass.states[script];
|
||||
return state
|
||||
? html`
|
||||
<paper-item
|
||||
.script=${script}
|
||||
@click=${this._openScript}
|
||||
<a
|
||||
href=${ifDefined(
|
||||
state.attributes.id
|
||||
? `/config/script/edit/${state.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-item-body>
|
||||
${state.attributes.friendly_name ||
|
||||
script}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
<paper-item .script=${script}>
|
||||
<paper-item-body>
|
||||
${computeStateName(state)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
})
|
||||
@ -318,19 +419,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
)}</paper-item
|
||||
>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._showScriptDialog}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage> `;
|
||||
@ -344,6 +437,21 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return state ? computeStateName(state) : null;
|
||||
}
|
||||
|
||||
private _computeAreaName(areas, device): string | undefined {
|
||||
if (!areas || !device || !device.area_id) {
|
||||
return undefined;
|
||||
}
|
||||
return areas.find((area) => area.area_id === device.area_id).name;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.display = "inline-block";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.display = "none";
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, "device", this.deviceId);
|
||||
}
|
||||
@ -358,25 +466,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openScene(ev: Event) {
|
||||
const state = (ev.currentTarget as any).scene;
|
||||
if (state.attributes.id) {
|
||||
navigate(this, `/config/scene/edit/${state.attributes.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _openScript(ev: Event) {
|
||||
const script = (ev.currentTarget as any).script;
|
||||
navigate(this, `/config/script/edit/${script}`);
|
||||
}
|
||||
|
||||
private _openAutomation(ev: Event) {
|
||||
const state = (ev.currentTarget as any).automation;
|
||||
if (state.attributes.id) {
|
||||
navigate(this, `/config/automation/edit/${state.attributes.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _showScriptDialog() {
|
||||
showDeviceAutomationDialog(this, { deviceId: this.deviceId, script: true });
|
||||
}
|
||||
@ -459,6 +548,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-header paper-icon-button {
|
||||
margin-right: -8px;
|
||||
color: var(--primary-color);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
padding: 16px;
|
||||
}
|
||||
@ -467,7 +568,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
@ -479,46 +580,60 @@ export class HaConfigDevicePage extends LitElement {
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.left,
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.column,
|
||||
.fullwidth {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 33.33%;
|
||||
padding-bottom: 0;
|
||||
.column {
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 66.66%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 50%;
|
||||
.header-right {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.header-right img {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-right:first-child {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-right > *:not(:first-child) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.battery {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) .left,
|
||||
:host([narrow]) .right,
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) .container > *:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -530,6 +645,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
paper-item.no-link {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ import {
|
||||
computeDeviceName,
|
||||
DeviceEntityLookup,
|
||||
} from "../../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
findBatteryEntity,
|
||||
} from "../../../data/entity_registry";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@ -130,25 +133,38 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: DataTableRowData) => {
|
||||
const battery = device.battery_entity
|
||||
? this.hass.states[device.battery_entity]
|
||||
: undefined;
|
||||
// Have to work on a nice layout for mobile
|
||||
return html`
|
||||
${name}<br />
|
||||
${device.area} | ${device.integration}<br />
|
||||
${battery && !isNaN(battery.state as any)
|
||||
? html`
|
||||
${battery.state}%
|
||||
<ha-state-icon
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${battery}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: ""}
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${device.area} | ${device.integration}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
battery_entity: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.battery"
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "90px",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
: undefined;
|
||||
return battery
|
||||
? html`
|
||||
${isNaN(battery.state as any) ? "-" : battery.state}%
|
||||
<ha-state-icon
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${battery}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: html`
|
||||
-
|
||||
`;
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
@ -198,7 +214,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "60px",
|
||||
width: "15%",
|
||||
maxWidth: "90px",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
@ -246,12 +263,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
deviceId: string,
|
||||
deviceEntityLookup: DeviceEntityLookup
|
||||
): string | undefined {
|
||||
const batteryEntity = (deviceEntityLookup[deviceId] || []).find(
|
||||
(entity) =>
|
||||
this.hass.states[entity.entity_id] &&
|
||||
this.hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||
const batteryEntity = findBatteryEntity(
|
||||
this.hass,
|
||||
deviceEntityLookup[deviceId] || []
|
||||
);
|
||||
|
||||
return batteryEntity ? batteryEntity.entity_id : undefined;
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,10 @@ import {
|
||||
computeDeviceName,
|
||||
DeviceEntityLookup,
|
||||
} from "../../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
findBatteryEntity,
|
||||
} from "../../../data/entity_registry";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@ -204,7 +207,8 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "60px",
|
||||
width: "15%",
|
||||
maxWidth: "90px",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
@ -250,12 +254,10 @@ export class HaDevicesDataTable extends LitElement {
|
||||
deviceId: string,
|
||||
deviceEntityLookup: DeviceEntityLookup
|
||||
): string | undefined {
|
||||
const batteryEntity = (deviceEntityLookup[deviceId] || []).find(
|
||||
(entity) =>
|
||||
this.hass.states[entity.entity_id] &&
|
||||
this.hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||
const batteryEntity = findBatteryEntity(
|
||||
this.hass,
|
||||
deviceEntityLookup[deviceId] || []
|
||||
);
|
||||
|
||||
return batteryEntity ? batteryEntity.entity_id : undefined;
|
||||
}
|
||||
|
||||
|
@ -132,19 +132,16 @@ class HaInputNumberForm extends LitElement {
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
</div>
|
||||
${this._mode === "slider"
|
||||
? html`
|
||||
<paper-input
|
||||
.value=${this._step}
|
||||
.configValue=${"step"}
|
||||
type="number"
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_number.step"
|
||||
)}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
<paper-input
|
||||
.value=${this._step}
|
||||
.configValue=${"step"}
|
||||
type="number"
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_number.step"
|
||||
)}
|
||||
></paper-input>
|
||||
|
||||
<paper-input
|
||||
.value=${this._unit_of_measurement}
|
||||
.configValue=${"unit_of_measurement"}
|
||||
|
@ -344,9 +344,6 @@ export class HaConfigManagerDashboard extends LitElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
mwc-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { PolymerChangedEvent } from "../../../../polymer-types";
|
||||
import { HaSwitch } from "../../../../components/ha-switch";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { setDefaultPanel, DEFAULT_PANEL } from "../../../../data/panel";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
@ -57,6 +58,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const defaultPanelUrlPath = this.hass.defaultPanel;
|
||||
const urlInvalid =
|
||||
this._params.urlPath !== "lovelace" &&
|
||||
!/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+$/.test(this._urlPath);
|
||||
@ -169,12 +171,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
slot="secondaryAction"
|
||||
@click=${this._toggleDefault}
|
||||
.disabled=${this._params.urlPath === "lovelace" &&
|
||||
(!localStorage.defaultPage ||
|
||||
localStorage.defaultPage === "lovelace")}
|
||||
defaultPanelUrlPath === "lovelace"}
|
||||
>
|
||||
${this._params.urlPath === localStorage.defaultPage ||
|
||||
(this._params.urlPath === "lovelace" &&
|
||||
!localStorage.defaultPage)
|
||||
${this._params.urlPath === defaultPanelUrlPath
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.remove_default"
|
||||
)
|
||||
@ -244,12 +243,10 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
if (urlPath === localStorage.defaultPage) {
|
||||
delete localStorage.defaultPage;
|
||||
} else {
|
||||
localStorage.defaultPage = urlPath;
|
||||
}
|
||||
location.reload();
|
||||
setDefaultPanel(
|
||||
this,
|
||||
urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
|
@ -184,8 +184,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
const defaultMode = (this.hass.panels?.lovelace
|
||||
?.config as LovelacePanelConfig).mode;
|
||||
const isDefault =
|
||||
!localStorage.defaultPage || localStorage.defaultPage === "lovelace";
|
||||
const defaultUrlPath = this.hass.defaultPanel;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
return [
|
||||
{
|
||||
icon: "hass:view-dashboard",
|
||||
@ -201,7 +201,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: localStorage.defaultPage === dashboard.url_path,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
@ -1,30 +1,28 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResultArray,
|
||||
css,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
|
||||
import "../ha-config-section";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { activateScene, SceneEntity } from "../../../data/scene";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { SceneEntity, activateScene } from "../../../data/scene";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@customElement("ha-scene-dashboard")
|
||||
@ -35,108 +33,131 @@ class HaSceneDashboard extends LitElement {
|
||||
@property() public route!: Route;
|
||||
@property() public scenes!: SceneEntity[];
|
||||
|
||||
private _scenes = memoizeOne((scenes: SceneEntity[]) => {
|
||||
return scenes.map((scene) => {
|
||||
return {
|
||||
...scene,
|
||||
name: computeStateName(scene),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(_language): DataTableColumnContainer => {
|
||||
return {
|
||||
activate: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_toggle, scene) =>
|
||||
html`
|
||||
<paper-icon-button
|
||||
.scene=${scene}
|
||||
icon="hass:play"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.activate_scene"
|
||||
)}"
|
||||
@click=${(ev: Event) => this._activateScene(ev)}
|
||||
></paper-icon-button>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
info: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, scene) => html`
|
||||
<paper-icon-button
|
||||
.scene=${scene}
|
||||
@click=${this._showInfo}
|
||||
icon="hass:information-outline"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.show_info_scene"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
`,
|
||||
},
|
||||
edit: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, scene: any) => html`
|
||||
<a
|
||||
href=${ifDefined(
|
||||
scene.attributes.id
|
||||
? `/config/scene/edit/${scene.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-icon-button
|
||||
.icon=${scene.attributes.id ? "hass:pencil" : "hass:pencil-off"}
|
||||
.disabled=${!scene.attributes.id}
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.edit_scene"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
${!scene.attributes.id
|
||||
? html`
|
||||
<paper-tooltip position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.only_editable"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
.data=${this._scenes(this.scenes)}
|
||||
id="entity_id"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_scenes"
|
||||
)}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.scene.picker.header")}
|
||||
</div>
|
||||
<div slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.scene.picker.introduction")}
|
||||
<p>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scene/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-card
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.pick_scene"
|
||||
)}
|
||||
>
|
||||
${this.scenes.length === 0
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_scenes"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: this.scenes.map(
|
||||
(scene) => html`
|
||||
<div class="scene">
|
||||
<paper-icon-button
|
||||
.scene=${scene}
|
||||
icon="hass:play"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.activate_scene"
|
||||
)}"
|
||||
@click=${this._activateScene}
|
||||
></paper-icon-button>
|
||||
<paper-item-body two-line>
|
||||
<div>${computeStateName(scene)}</div>
|
||||
</paper-item-body>
|
||||
<div class="actions">
|
||||
<a
|
||||
href=${ifDefined(
|
||||
scene.attributes.id
|
||||
? `/config/scene/edit/${scene.attributes.id}`
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<paper-icon-button
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.edit_scene"
|
||||
)}"
|
||||
icon="hass:pencil"
|
||||
.disabled=${!scene.attributes.id}
|
||||
></paper-icon-button>
|
||||
${!scene.attributes.id
|
||||
? html`
|
||||
<paper-tooltip position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.only_editable"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
<a href="/config/scene/edit/new">
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.add_scene"
|
||||
)}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-fab>
|
||||
</a>
|
||||
</hass-tabs-subpage>
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:help-circle"
|
||||
@click=${this._showHelp}
|
||||
></paper-icon-button>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<a href="/config/scene/edit/new">
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title=${this.hass.localize("ui.panel.config.scene.picker.add_scene")}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-fab>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showInfo(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.currentTarget.scene.entity_id;
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private async _activateScene(ev) {
|
||||
ev.stopPropagation();
|
||||
const scene = ev.target.scene as SceneEntity;
|
||||
await activateScene(this.hass, scene.entity_id);
|
||||
showToast(this, {
|
||||
@ -149,38 +170,28 @@ class HaSceneDashboard extends LitElement {
|
||||
forwardHaptic("light");
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.scene.picker.header"),
|
||||
text: html`
|
||||
${this.hass.localize("ui.panel.config.scene.picker.introduction")}
|
||||
<p>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scene/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
|
||||
</a>
|
||||
</p>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.scene {
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
|
||||
.scene > *:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.scene a[href] {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
|
@ -1,31 +1,28 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
CSSResultArray,
|
||||
css,
|
||||
TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
|
||||
import "../ha-config-section";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import { triggerScript } from "../../../data/script";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { triggerScript } from "../../../data/script";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-script-picker")
|
||||
class HaScriptPicker extends LitElement {
|
||||
@ -35,91 +32,123 @@ class HaScriptPicker extends LitElement {
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
|
||||
private _scripts = memoizeOne((scripts: HassEntity[]) => {
|
||||
return scripts.map((script) => {
|
||||
return {
|
||||
...script,
|
||||
name: computeStateName(script),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(_language): DataTableColumnContainer => {
|
||||
return {
|
||||
activate: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_toggle, script) =>
|
||||
html`
|
||||
<paper-icon-button
|
||||
.script=${script}
|
||||
icon="hass:play"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.activate_script"
|
||||
)}"
|
||||
@click=${(ev: Event) => this._runScript(ev)}
|
||||
></paper-icon-button>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.script.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, script: any) => html`
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${script.attributes.last_triggered
|
||||
? formatDateTime(
|
||||
new Date(script.attributes.last_triggered),
|
||||
this.hass.language
|
||||
)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
info: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, script) => html`
|
||||
<paper-icon-button
|
||||
.script=${script}
|
||||
@click=${this._showInfo}
|
||||
icon="hass:information-outline"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.show_info"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
`,
|
||||
},
|
||||
edit: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_info, script: any) => html`
|
||||
<a href="/config/script/edit/${script.entity_id}">
|
||||
<paper-icon-button
|
||||
icon="hass:pencil"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.edit_script"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
.data=${this._scripts(this.scripts)}
|
||||
id="entity_id"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.script.picker.no_scripts"
|
||||
)}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.script.picker.header")}
|
||||
</div>
|
||||
<div slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.script.picker.introduction")}
|
||||
<p>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scripts/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.learn_more"
|
||||
)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-card>
|
||||
${this.scripts.length === 0
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.no_scripts"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: this.scripts.map(
|
||||
(script) => html`
|
||||
<div class="script">
|
||||
<paper-icon-button
|
||||
.script=${script}
|
||||
icon="hass:play"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.trigger_script"
|
||||
)}"
|
||||
@click=${this._runScript}
|
||||
></paper-icon-button>
|
||||
<paper-item-body two-line>
|
||||
<div>${computeStateName(script)}</div>
|
||||
</paper-item-body>
|
||||
<div class="actions">
|
||||
<a href=${`/config/script/edit/${script.entity_id}`}>
|
||||
<paper-icon-button
|
||||
icon="hass:pencil"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.edit_script"
|
||||
)}"
|
||||
></paper-icon-button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
||||
<a href="/config/script/new">
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.add_script"
|
||||
)}"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-fab>
|
||||
</a>
|
||||
</hass-tabs-subpage>
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:help-circle"
|
||||
@click=${this._showHelp}
|
||||
></paper-icon-button>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<a href="/config/script/new">
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
title="${this.hass.localize(
|
||||
"ui.panel.config.script.picker.add_script"
|
||||
)}"
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-fab>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _runScript(ev) {
|
||||
ev.stopPropagation();
|
||||
const script = ev.currentTarget.script as HassEntity;
|
||||
await triggerScript(this.hass, script.entity_id);
|
||||
showToast(this, {
|
||||
@ -131,38 +160,34 @@ class HaScriptPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _showInfo(ev) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.currentTarget.script.entity_id;
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.script.caption"),
|
||||
text: html`
|
||||
${this.hass.localize("ui.panel.config.script.picker.introduction")}
|
||||
<p>
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scripts/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.script.picker.learn_more")}
|
||||
</a>
|
||||
</p>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.script {
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
|
||||
.script > *:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.script a[href],
|
||||
paper-icon-button {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
@ -187,10 +212,6 @@ class HaScriptPicker extends LitElement {
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
242
src/panels/config/users/dialog-add-user.ts
Normal file
242
src/panels/config/users/dialog-add-user.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../resources/ha-style";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import {
|
||||
User,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
GROUPS,
|
||||
createUser,
|
||||
deleteUser,
|
||||
} from "../../../data/user";
|
||||
import { createAuthForUser } from "../../../data/auth";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _loading = false;
|
||||
// Error message when can't talk to server etc
|
||||
@property() private _error?: string;
|
||||
@property() private _params?: AddUserDialogParams;
|
||||
@property() private _name?: string;
|
||||
@property() private _username?: string;
|
||||
@property() private _password?: string;
|
||||
@property() private _group?: string;
|
||||
|
||||
public showDialog(params: AddUserDialogParams) {
|
||||
this._params = params;
|
||||
this._name = "";
|
||||
this._username = "";
|
||||
this._password = "";
|
||||
this._group = SYSTEM_GROUP_ID_USER;
|
||||
this._error = undefined;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._createUser(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this._close}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.hass.localize("ui.panel.config.users.add_user.caption")}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<paper-input
|
||||
class="name"
|
||||
.label=${this.hass.localize("ui.panel.config.users.add_user.name")}
|
||||
.value=${this._name}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="on"
|
||||
error-message="Required"
|
||||
@value-changed=${this._nameChanged}
|
||||
@blur=${this._maybePopulateUsername}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="username"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.username"
|
||||
)}
|
||||
.value=${this._username}
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="none"
|
||||
@value-changed=${this._usernameChanged}
|
||||
error-message="Required"
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password"
|
||||
)}
|
||||
type="password"
|
||||
.value=${this._password}
|
||||
required
|
||||
auto-validate
|
||||
@value-changed=${this._passwordChanged}
|
||||
error-message="Required"
|
||||
></paper-input>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._group}
|
||||
@iron-select=${this._handleGroupChange}
|
||||
attr-for-selected="group-id"
|
||||
>
|
||||
${GROUPS.map(
|
||||
(groupId) => html`
|
||||
<paper-item group-id=${groupId}>
|
||||
${this.hass.localize(`groups.${groupId}`)}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
${this._group === SYSTEM_GROUP_ID_USER
|
||||
? html`
|
||||
<br />
|
||||
The users group is a work in progress. The user will be unable
|
||||
to administer the instance via the UI. We're still auditing all
|
||||
management API endpoints to ensure that they correctly limit
|
||||
access to administrators.
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click="${this._close}"
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${this._loading
|
||||
? html`
|
||||
<div slot="primaryAction" class="submit-spinner">
|
||||
<paper-spinner active></paper-spinner>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._name || !this._username || !this._password}
|
||||
@click=${this._createUser}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.users.add_user.create")}
|
||||
</mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
private _maybePopulateUsername() {
|
||||
if (this._username || !this._name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = this._name.split(" ");
|
||||
|
||||
if (parts.length) {
|
||||
this._username = parts[0].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private _nameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private _usernameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._username = ev.detail.value;
|
||||
}
|
||||
|
||||
private _passwordChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._password = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleGroupChange(ev): Promise<void> {
|
||||
this._group = ev.detail.item.getAttribute("group-id");
|
||||
}
|
||||
|
||||
private async _createUser(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
this._error = "";
|
||||
|
||||
let user: User;
|
||||
try {
|
||||
const userResponse = await createUser(this.hass, this._name, [
|
||||
this._group!,
|
||||
]);
|
||||
user = userResponse.user;
|
||||
} catch (err) {
|
||||
this._loading = false;
|
||||
this._error = err.code;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createAuthForUser(
|
||||
this.hass,
|
||||
user.id,
|
||||
this._username,
|
||||
this._password
|
||||
);
|
||||
} catch (err) {
|
||||
await deleteUser(this.hass, user.id);
|
||||
this._loading = false;
|
||||
this._error = err.code;
|
||||
return;
|
||||
}
|
||||
|
||||
this._params!.userAddedCallback(user);
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-add-user": DialogAddUser;
|
||||
}
|
||||
}
|
209
src/panels/config/users/dialog-user-detail.ts
Normal file
209
src/panels/config/users/dialog-user-detail.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/user/ha-user-picker";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { UserDetailDialogParams } from "./show-dialog-user-detail";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import { GROUPS, SYSTEM_GROUP_ID_USER } from "../../../data/user";
|
||||
|
||||
@customElement("dialog-user-detail")
|
||||
class DialogUserDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _name!: string;
|
||||
@property() private _group?: string;
|
||||
@property() private _error?: string;
|
||||
@property() private _params?: UserDetailDialogParams;
|
||||
@property() private _submitting: boolean = false;
|
||||
|
||||
public async showDialog(params: UserDetailDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._name = params.entry.name || "";
|
||||
this._group = params.entry.group_ids[0];
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const user = this._params.entry;
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closing=${this._close}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(this.hass, user.name)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
.value=${this._name}
|
||||
@value-changed=${this._nameChanged}
|
||||
label="${this.hass!.localize("ui.panel.config.user.editor.name")}"
|
||||
></paper-input>
|
||||
<ha-paper-dropdown-menu
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.group")}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._group}
|
||||
@iron-select=${this._handleGroupChange}
|
||||
attr-for-selected="group-id"
|
||||
>
|
||||
${GROUPS.map(
|
||||
(groupId) => html`
|
||||
<paper-item group-id=${groupId}>
|
||||
${this.hass.localize(`groups.${groupId}`)}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-paper-dropdown-menu>
|
||||
${this._group === SYSTEM_GROUP_ID_USER
|
||||
? html`
|
||||
<br />
|
||||
The users group is a work in progress. The user will be unable
|
||||
to administer the instance via the UI. We're still auditing
|
||||
all management API endpoints to ensure that they correctly
|
||||
limit access to administrators.
|
||||
`
|
||||
: ""}
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize("ui.panel.config.users.editor.id")}
|
||||
</td>
|
||||
<td>${user.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize("ui.panel.config.users.editor.owner")}
|
||||
</td>
|
||||
<td>${user.is_owner}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize("ui.panel.config.users.editor.active")}
|
||||
</td>
|
||||
<td>${user.is_active}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated"
|
||||
)}
|
||||
</td>
|
||||
<td>${user.system_generated}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondaryAction">
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${this._submitting || user.system_generated}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</mwc-button>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<paper-tooltip position="right"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_users_not_removable"
|
||||
)}</paper-tooltip
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._name}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _nameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleGroupChange(ev): Promise<void> {
|
||||
this._group = ev.detail.item.getAttribute("group-id");
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await this._params!.updateEntry({
|
||||
name: this._name.trim(),
|
||||
group_ids: [this._group!],
|
||||
});
|
||||
this._close();
|
||||
} catch (err) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-user-detail": DialogUserDetail;
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaUserPicker extends EventsMixin(
|
||||
NavigateMixin(LocalizeMixin(PolymerElement))
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab[rtl][is-wide] {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 16px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-tabs-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
route="[[route]]"
|
||||
back-path="/config"
|
||||
tabs="[[_computeTabs()]]"
|
||||
>
|
||||
<ha-card>
|
||||
<template is="dom-repeat" items="[[users]]" as="user">
|
||||
<a href="[[_computeUrl(user)]]">
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
<div>[[_withDefault(user.name, 'Unnamed User')]]</div>
|
||||
<div secondary="">
|
||||
[[_computeGroup(localize, user)]]
|
||||
<template is="dom-if" if="[[user.system_generated]]">
|
||||
-
|
||||
[[localize('ui.panel.config.users.picker.system_generated')]]
|
||||
</template>
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</ha-card>
|
||||
|
||||
<ha-fab
|
||||
is-wide$="[[isWide]]"
|
||||
narrow$="[[narrow]]"
|
||||
icon="hass:plus"
|
||||
title="[[localize('ui.panel.config.users.picker.add_user')]]"
|
||||
on-click="_addUser"
|
||||
rtl$="[[rtl]]"
|
||||
></ha-fab>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
users: Array,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
route: Object,
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
this.fire("register-dialog", {
|
||||
dialogShowEvent: "show-add-user",
|
||||
dialogTag: "ha-dialog-add-user",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "ha-dialog-add-user" */ "./ha-dialog-add-user"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_withDefault(value, defaultValue) {
|
||||
return value || defaultValue;
|
||||
}
|
||||
|
||||
_computeUrl(user) {
|
||||
return `/config/users/${user.id}`;
|
||||
}
|
||||
|
||||
_computeGroup(localize, user) {
|
||||
return localize(`groups.${user.group_ids[0]}`);
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
|
||||
_computeTabs() {
|
||||
return configSections.persons;
|
||||
}
|
||||
|
||||
_addUser() {
|
||||
this.fire("show-add-user", {
|
||||
hass: this.hass,
|
||||
dialogClosedCallback: async ({ userId }) => {
|
||||
this.fire("reload-users");
|
||||
if (userId) this.navigate(`/config/users/${userId}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-user-picker", HaUserPicker);
|
@ -1,109 +0,0 @@
|
||||
import "@polymer/app-route/app-route";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
|
||||
import "./ha-config-user-picker";
|
||||
import "./ha-user-editor";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { fetchUsers } from "../../../data/user";
|
||||
|
||||
/*
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaConfigUsers extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route
|
||||
route="[[route]]"
|
||||
pattern="/:user"
|
||||
data="{{_routeData}}"
|
||||
></app-route>
|
||||
|
||||
<template is="dom-if" if='[[_equals(_routeData.user, "picker")]]'>
|
||||
<ha-config-user-picker
|
||||
hass="[[hass]]"
|
||||
users="[[_users]]"
|
||||
is-wide="[[isWide]]"
|
||||
narrow="[[narrow]]"
|
||||
route="[[route]]"
|
||||
></ha-config-user-picker>
|
||||
</template>
|
||||
<template
|
||||
is="dom-if"
|
||||
if='[[!_equals(_routeData.user, "picker")]]'
|
||||
restamp
|
||||
>
|
||||
<ha-user-editor
|
||||
hass="[[hass]]"
|
||||
user="[[_computeUser(_users, _routeData.user)]]"
|
||||
narrow="[[narrow]]"
|
||||
route="[[route]]"
|
||||
></ha-user-editor>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
route: {
|
||||
type: Object,
|
||||
observer: "_checkRoute",
|
||||
},
|
||||
_routeData: Object,
|
||||
_user: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
_users: {
|
||||
type: Array,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this._loadData();
|
||||
this.addEventListener("reload-users", () => this._loadData());
|
||||
}
|
||||
|
||||
_handlePickUser(ev) {
|
||||
this._user = ev.detail.user;
|
||||
}
|
||||
|
||||
_checkRoute(route) {
|
||||
// prevent list getting under toolbar
|
||||
fireEvent(this, "iron-resize");
|
||||
|
||||
this._debouncer = Debouncer.debounce(
|
||||
this._debouncer,
|
||||
timeOut.after(0),
|
||||
() => {
|
||||
if (route.path === "") {
|
||||
this.navigate(`${route.prefix}/picker`, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_computeUser(users, userId) {
|
||||
return users && users.filter((u) => u.id === userId)[0];
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
async _loadData() {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-users", HaConfigUsers);
|
192
src/panels/config/users/ha-config-users.ts
Normal file
192
src/panels/config/users/ha-config-users.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
LitElement,
|
||||
property,
|
||||
css,
|
||||
PropertyValues,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { html } from "lit-html";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { User, fetchUsers, updateUser, deleteUser } from "../../../data/user";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import { showUserDetailDialog } from "./show-dialog-user-detail";
|
||||
import { showAddUserDialog } from "./show-dialog-add-user";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("ha-config-users")
|
||||
export class HaConfigUsers extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public _users: User[] = [];
|
||||
@property() public isWide!: boolean;
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(_language): DataTableColumnContainer => {
|
||||
return {
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name) => html`
|
||||
${name ||
|
||||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user")}
|
||||
`,
|
||||
},
|
||||
group_ids: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.picker.headers.group"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
template: (groupIds) => html`
|
||||
${this.hass.localize(`groups.${groupIds[0]}`)}
|
||||
`,
|
||||
},
|
||||
system_generated: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.picker.headers.system"
|
||||
),
|
||||
type: "icon",
|
||||
width: "10%",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
template: (generated) => html`
|
||||
${generated
|
||||
? html`
|
||||
<ha-icon icon="hass:check-circle-outline"></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._fetchUsers();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
.data=${this._users}
|
||||
@row-click=${this._editUser}
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
?is-wide=${this.isWide}
|
||||
?narrow=${this.narrow}
|
||||
icon="hass:plus"
|
||||
.title=${this.hass.localize("ui.panel.config.users.picker.add_user")}
|
||||
@click=${this._addUser}
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchUsers() {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
}
|
||||
|
||||
private _editUser(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const id = ev.detail.id;
|
||||
const entry = this._users.find((user) => user.id === id);
|
||||
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
showUserDetailDialog(this, {
|
||||
entry,
|
||||
updateEntry: async (values) => {
|
||||
const updated = await updateUser(this.hass!, entry!.id, values);
|
||||
this._users = this._users!.map((ent) =>
|
||||
ent === entry ? updated.user : ent
|
||||
);
|
||||
},
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.users.editor.confirm_user_deletion",
|
||||
"name",
|
||||
entry.name
|
||||
),
|
||||
dismissText: this.hass!.localize("ui.common.no"),
|
||||
confirmText: this.hass!.localize("ui.common.yes"),
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteUser(this.hass!, entry!.id);
|
||||
this._users = this._users!.filter((ent) => ent !== entry);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _addUser() {
|
||||
showAddUserDialog(this, {
|
||||
userAddedCallback: async (user: User) => {
|
||||
if (user) {
|
||||
this._users = { ...this._users, ...user };
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
ha-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
ha-fab[narrow] {
|
||||
bottom: 84px;
|
||||
}
|
||||
ha-fab[rtl][is-wide] {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/dialog/ha-paper-dialog";
|
||||
import "../../../resources/ha-style";
|
||||
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
ha-paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
.username {
|
||||
margin-top: -8px;
|
||||
}
|
||||
</style>
|
||||
<ha-paper-dialog
|
||||
id="dialog"
|
||||
with-backdrop
|
||||
opened="{{_opened}}"
|
||||
on-opened-changed="_openedChanged"
|
||||
>
|
||||
<h2>[[localize('ui.panel.config.users.add_user.caption')]]</h2>
|
||||
<div>
|
||||
<template is="dom-if" if="[[_errorMsg]]">
|
||||
<div class="error">[[_errorMsg]]</div>
|
||||
</template>
|
||||
<paper-input
|
||||
class="name"
|
||||
label="[[localize('ui.panel.config.users.add_user.name')]]"
|
||||
value="{{_name}}"
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="on"
|
||||
error-message="Required"
|
||||
on-blur="_maybePopulateUsername"
|
||||
></paper-input>
|
||||
<paper-input
|
||||
class="username"
|
||||
label="[[localize('ui.panel.config.users.add_user.username')]]"
|
||||
value="{{_username}}"
|
||||
required
|
||||
auto-validate
|
||||
autocapitalize="none"
|
||||
error-message="Required"
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label="[[localize('ui.panel.config.users.add_user.password')]]"
|
||||
type="password"
|
||||
value="{{_password}}"
|
||||
required
|
||||
auto-validate
|
||||
error-message="Required"
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<template is="dom-if" if="[[_loading]]">
|
||||
<div class="submit-spinner">
|
||||
<paper-spinner active></paper-spinner>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_loading]]">
|
||||
<mwc-button on-click="_createUser"
|
||||
>[[localize('ui.panel.config.users.add_user.create')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: Object,
|
||||
_dialogClosedCallback: Function,
|
||||
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// Error message when can't talk to server etc
|
||||
_errorMsg: String,
|
||||
|
||||
_opened: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_name: String,
|
||||
_username: String,
|
||||
_password: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._createUser(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDialog({ hass, dialogClosedCallback }) {
|
||||
this.hass = hass;
|
||||
this._dialogClosedCallback = dialogClosedCallback;
|
||||
this._loading = false;
|
||||
this._opened = true;
|
||||
setTimeout(() => this.shadowRoot.querySelector("paper-input").focus(), 0);
|
||||
}
|
||||
|
||||
_maybePopulateUsername() {
|
||||
if (this._username) return;
|
||||
|
||||
const parts = this._name.split(" ");
|
||||
|
||||
if (parts.length) {
|
||||
this._username = parts[0].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
async _createUser(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) return;
|
||||
|
||||
this._loading = true;
|
||||
this._errorMsg = null;
|
||||
|
||||
let userId;
|
||||
|
||||
try {
|
||||
const userResponse = await this.hass.callWS({
|
||||
type: "config/auth/create",
|
||||
name: this._name,
|
||||
});
|
||||
userId = userResponse.user.id;
|
||||
} catch (err) {
|
||||
this._loading = false;
|
||||
this._errorMsg = err.code;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.hass.callWS({
|
||||
type: "config/auth_provider/homeassistant/create",
|
||||
user_id: userId,
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
});
|
||||
} catch (err) {
|
||||
this._loading = false;
|
||||
this._errorMsg = err.code;
|
||||
await this.hass.callWS({
|
||||
type: "config/auth/delete",
|
||||
user_id: userId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogDone(userId);
|
||||
}
|
||||
|
||||
_dialogDone(userId) {
|
||||
this._dialogClosedCallback({ userId });
|
||||
|
||||
this.setProperties({
|
||||
_errorMsg: null,
|
||||
_username: "",
|
||||
_password: "",
|
||||
_dialogClosedCallback: null,
|
||||
_opened: false,
|
||||
});
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_openedChanged(ev) {
|
||||
// Closed dialog by clicking on the overlay
|
||||
// Check against dialogClosedCallback to make sure we didn't change
|
||||
// programmatically
|
||||
if (this._dialogClosedCallback && !ev.detail.value) {
|
||||
this._dialogDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-dialog-add-user", HaDialogAddUser);
|
@ -1,257 +0,0 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
customElement,
|
||||
CSSResultArray,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import "@material/mwc-button";
|
||||
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import "../../../components/ha-card";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import {
|
||||
User,
|
||||
deleteUser,
|
||||
updateUser,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
} from "../../../data/user";
|
||||
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"reload-users": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN];
|
||||
|
||||
@customElement("ha-user-editor")
|
||||
class HaUserEditor extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public user?: User;
|
||||
@property() public narrow?: boolean;
|
||||
@property() public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hass = this.hass;
|
||||
const user = this.user;
|
||||
if (!hass || !user) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.persons}
|
||||
>
|
||||
<ha-card .header=${this._name}>
|
||||
<table class="card-content">
|
||||
<tr>
|
||||
<td>${hass.localize("ui.panel.config.users.editor.id")}</td>
|
||||
<td>${user.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${hass.localize("ui.panel.config.users.editor.owner")}</td>
|
||||
<td>${user.is_owner}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${hass.localize("ui.panel.config.users.editor.group")}</td>
|
||||
<td>
|
||||
<select
|
||||
@change=${this._handleGroupChange}
|
||||
.value=${until(
|
||||
this.updateComplete.then(() => user.group_ids[0])
|
||||
)}
|
||||
>
|
||||
${GROUPS.map(
|
||||
(groupId) => html`
|
||||
<option value=${groupId}>
|
||||
${hass.localize(`groups.${groupId}`)}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
${user.group_ids[0] === SYSTEM_GROUP_ID_USER
|
||||
? html`
|
||||
<tr>
|
||||
<td colspan="2" class="user-experiment">
|
||||
The users group is a work in progress. The user will be
|
||||
unable to administer the instance via the UI. We're still
|
||||
auditing all management API endpoints to ensure that they
|
||||
correctly limit access to administrators.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<tr>
|
||||
<td>${hass.localize("ui.panel.config.users.editor.active")}</td>
|
||||
<td>${user.is_active}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
${hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated"
|
||||
)}
|
||||
</td>
|
||||
<td>${user.system_generated}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._handlePromptRenameUser}>
|
||||
${hass.localize("ui.panel.config.users.editor.rename_user")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._promptDeleteUser}
|
||||
.disabled=${user.system_generated}
|
||||
>
|
||||
${hass.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</mwc-button>
|
||||
${user.system_generated
|
||||
? html`
|
||||
${hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_users_not_removable"
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _name() {
|
||||
return (
|
||||
this.user &&
|
||||
(this.user.name ||
|
||||
this.hass!.localize("ui.panel.config.users.editor.unnamed_user"))
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleRenameUser(newName?: string) {
|
||||
if (newName === null || newName === this.user!.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUser(this.hass!, this.user!.id, {
|
||||
name: newName,
|
||||
});
|
||||
fireEvent(this, "reload-users");
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: `${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.user_rename_failed"
|
||||
)} ${err.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlePromptRenameUser(ev): Promise<void> {
|
||||
ev.currentTarget.blur();
|
||||
showPromptDialog(this, {
|
||||
title: this.hass!.localize("ui.panel.config.users.editor.enter_new_name"),
|
||||
defaultValue: this.user!.name,
|
||||
inputLabel: this.hass!.localize("ui.panel.config.users.add_user.name"),
|
||||
confirm: (text) => this._handleRenameUser(text),
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleGroupChange(ev): Promise<void> {
|
||||
const selectEl = ev.currentTarget as HTMLSelectElement;
|
||||
const newGroup = selectEl.value;
|
||||
try {
|
||||
await updateUser(this.hass!, this.user!.id, {
|
||||
group_ids: [newGroup],
|
||||
});
|
||||
showSaveSuccessToast(this, this.hass!);
|
||||
fireEvent(this, "reload-users");
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: `${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.group_update_failed"
|
||||
)} ${err.message}`,
|
||||
});
|
||||
selectEl.value = this.user!.group_ids[0];
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteUser() {
|
||||
try {
|
||||
await deleteUser(this.hass!, this.user!.id);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: err.code,
|
||||
});
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "reload-users");
|
||||
navigate(this, "/config/users");
|
||||
}
|
||||
|
||||
private async _promptDeleteUser(_ev): Promise<void> {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.users.editor.confirm_user_deletion",
|
||||
"name",
|
||||
this._name
|
||||
),
|
||||
confirm: () => this._deleteUser(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 16px auto 16px;
|
||||
}
|
||||
hass-subpage ha-card:first-of-type {
|
||||
direction: ltr;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.user-experiment {
|
||||
padding: 8px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-user-editor": HaUserEditor;
|
||||
}
|
||||
}
|
20
src/panels/config/users/show-dialog-add-user.ts
Normal file
20
src/panels/config/users/show-dialog-add-user.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { User } from "../../../data/user";
|
||||
|
||||
export interface AddUserDialogParams {
|
||||
userAddedCallback: (user: User) => void;
|
||||
}
|
||||
|
||||
export const loadAddUserDialog = () =>
|
||||
import(/* webpackChunkName: "add-user-dialog" */ "./dialog-add-user");
|
||||
|
||||
export const showAddUserDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: AddUserDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-add-user",
|
||||
dialogImport: loadAddUserDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
22
src/panels/config/users/show-dialog-user-detail.ts
Normal file
22
src/panels/config/users/show-dialog-user-detail.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { User, UpdateUserParams } from "../../../data/user";
|
||||
|
||||
export interface UserDetailDialogParams {
|
||||
entry: User;
|
||||
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadUserDetailDialog = () =>
|
||||
import(/* webpackChunkName: "user-detail-dialog" */ "./dialog-user-detail");
|
||||
|
||||
export const showUserDetailDialog = (
|
||||
element: HTMLElement,
|
||||
detailParams: UserDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-user-detail",
|
||||
dialogImport: loadUserDetailDialog,
|
||||
dialogParams: detailParams,
|
||||
});
|
||||
};
|
@ -12,6 +12,7 @@ class HaPanelIframe extends PolymerElement {
|
||||
iframe {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: calc(100% - 64px);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import { AlarmPanelCardConfig } from "./types";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
const ICONS = {
|
||||
armed_away: "hass:shield-lock",
|
||||
@ -50,22 +50,21 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
) {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): AlarmPanelCardConfig {
|
||||
const includeDomains = ["alarm_control_panel"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
type: "alarm-panel",
|
||||
states: ["arm_home", "arm_away"],
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
@ -173,6 +172,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
class="${classMap({ [stateObj.state]: true })}"
|
||||
.icon="${ICONS[stateObj.state] || "hass:shield-outline"}"
|
||||
.label="${this._stateIconLabel(stateObj.state)}"
|
||||
@click=${this._handleMoreInfo}
|
||||
></ha-label-badge>
|
||||
<div id="armActions" class="actions">
|
||||
${(stateObj.state === "disarmed"
|
||||
@ -248,16 +248,22 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleActionClick(e: MouseEvent): void {
|
||||
const input = this._input!;
|
||||
const code =
|
||||
input && input.value && input.value.length > 0 ? input.value : "";
|
||||
const input = this._input;
|
||||
callAlarmAction(
|
||||
this.hass!,
|
||||
this._config!.entity,
|
||||
(e.currentTarget! as any).action,
|
||||
code
|
||||
input?.value || undefined
|
||||
);
|
||||
input.value = "";
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMoreInfo() {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@ -281,6 +287,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disarmed {
|
||||
|
@ -30,7 +30,7 @@ import { ButtonCardConfig } from "./types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { computeActiveState } from "../../../common/entity/compute_active_state";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
@ -46,26 +46,24 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): ButtonCardConfig {
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
["light", "switch"]
|
||||
);
|
||||
|
||||
return {
|
||||
type: "button",
|
||||
tap_action: { action: "toggle" },
|
||||
hold_action: { action: "more-info" },
|
||||
show_icon: true,
|
||||
show_name: true,
|
||||
state_color: true,
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
}
|
||||
@ -230,6 +228,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
text-align: center;
|
||||
padding: 4% 0;
|
||||
font-size: 1.2rem;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ha-card:focus {
|
||||
|
@ -15,10 +15,11 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
|
||||
return document.createElement("hui-conditional-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
public static getStubConfig(): ConditionalCardConfig {
|
||||
return {
|
||||
type: "conditional",
|
||||
conditions: [],
|
||||
card: {},
|
||||
card: { type: "" },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,6 @@ import { createHeaderFooterElement } from "../create-element/create-header-foote
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
@ -41,21 +40,19 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
) {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): EntitiesCardConfig {
|
||||
const maxEntities = 3;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
["light", "switch", "sensor"]
|
||||
);
|
||||
|
||||
return { title: "My Title", entities: foundEntities };
|
||||
return { type: "entities", title: "My Title", entities: foundEntities };
|
||||
}
|
||||
|
||||
@property() private _config?: EntitiesCardConfig;
|
||||
@ -188,6 +185,9 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -22,7 +22,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { GaugeCardConfig } from "./types";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
|
||||
@ -44,10 +43,9 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): GaugeCardConfig {
|
||||
const includeDomains = ["sensor"];
|
||||
const maxEntities = 1;
|
||||
const entityFilter = (stateObj: HassEntity): boolean => {
|
||||
@ -56,15 +54,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains,
|
||||
entityFilter
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "gauge", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
@ -136,7 +133,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
<div class="container">
|
||||
<div class="gauge-a"></div>
|
||||
<div class="gauge-b"></div>
|
||||
<div
|
||||
class="gauge-c"
|
||||
style=${styleMap({
|
||||
@ -144,16 +140,17 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
"background-color": this._computeSeverity(state),
|
||||
})}
|
||||
></div>
|
||||
<div class="gauge-data">
|
||||
<div id="percent">
|
||||
${stateObj.state}
|
||||
${this._config.unit ||
|
||||
stateObj.attributes.unit_of_measurement ||
|
||||
""}
|
||||
</div>
|
||||
<div id="name">
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
<div class="gauge-b"></div>
|
||||
</div>
|
||||
<div class="gauge-data">
|
||||
<div id="percent">
|
||||
${stateObj.state}
|
||||
${this._config.unit ||
|
||||
stateObj.attributes.unit_of_measurement ||
|
||||
""}
|
||||
</div>
|
||||
<div id="name">
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
@ -250,9 +247,14 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
height: calc(var(--base-unit) * 3);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 0 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
ha-card:focus {
|
||||
outline: none;
|
||||
@ -261,15 +263,10 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
.container {
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: calc(var(--base-unit) * 2);
|
||||
position: absolute;
|
||||
top: calc(var(--base-unit) * 1.5);
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -50%);
|
||||
position: relative;
|
||||
}
|
||||
.gauge-a {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: var(--primary-background-color);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
@ -279,7 +276,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
0px 0px;
|
||||
}
|
||||
.gauge-b {
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
background-color: var(--paper-card-background-color);
|
||||
width: calc(var(--base-unit) * 2.5);
|
||||
@ -291,7 +287,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
0px 0px;
|
||||
}
|
||||
.gauge-c {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
background-color: var(--label-badge-blue);
|
||||
width: calc(var(--base-unit) * 4);
|
||||
@ -307,15 +302,12 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
transition: all 1.3s ease-in-out;
|
||||
}
|
||||
.gauge-data {
|
||||
z-index: 4;
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
line-height: calc(var(--base-unit) * 0.3);
|
||||
position: absolute;
|
||||
width: calc(var(--base-unit) * 4);
|
||||
height: var(--base-unit);
|
||||
top: var(--base-unit);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top: calc(var(--base-unit) * -0.5);
|
||||
}
|
||||
.init .gauge-data {
|
||||
transition: all 1s ease-out;
|
||||
|
@ -27,7 +27,7 @@ import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
@ -44,18 +44,16 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): GlanceCardConfig {
|
||||
const includeDomains = ["sensor"];
|
||||
const maxEntities = 3;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
|
@ -20,7 +20,6 @@ import { HomeAssistant } from "../../../types";
|
||||
import { HistoryGraphCardConfig } from "./types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-history-graph-card")
|
||||
@ -34,22 +33,20 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): HistoryGraphCardConfig {
|
||||
const includeDomains = ["sensor"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entities: foundEntities };
|
||||
return { type: "history-graph", entities: foundEntities };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -13,6 +13,7 @@ import "../../../components/ha-card";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { IframeCardConfig } from "./types";
|
||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||
|
||||
@customElement("hui-iframe-card")
|
||||
export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
@ -22,10 +23,17 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
return document.createElement("hui-iframe-card-editor");
|
||||
}
|
||||
public static getStubConfig(): object {
|
||||
return { url: "https://www.home-assistant.io", aspect_ratio: "50%" };
|
||||
public static getStubConfig(): IframeCardConfig {
|
||||
return {
|
||||
type: "iframe",
|
||||
url: "https://www.home-assistant.io",
|
||||
aspect_ratio: "50%",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isPanel = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public editMode = false;
|
||||
@property() protected _config?: IframeCardConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
@ -51,17 +59,29 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const aspectRatio = this._config.aspect_ratio || "50%";
|
||||
let padding = "";
|
||||
if (!this.isPanel && this._config.aspect_ratio) {
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
padding = `${((100 * ratio.h) / ratio.w).toFixed(2)}%`;
|
||||
}
|
||||
} else if (!this.isPanel) {
|
||||
padding = "50%";
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card .header="${this._config.title}">
|
||||
<div
|
||||
id="root"
|
||||
style="${styleMap({
|
||||
"padding-top": aspectRatio,
|
||||
"padding-top": padding,
|
||||
})}"
|
||||
>
|
||||
<iframe src="${this._config.url}"></iframe>
|
||||
<iframe
|
||||
src="${this._config.url}"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-pointer-lock allow-same-origin allow-scripts"
|
||||
allowfullscreen="true"
|
||||
></iframe>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@ -69,6 +89,15 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host([ispanel]) ha-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([ispanel][editMode]) ha-card {
|
||||
height: calc(100% - 51px);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -78,6 +107,10 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([ispanel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
border: none;
|
||||
|
@ -29,7 +29,6 @@ import { toggleEntity } from "../common/entity/toggle-entity";
|
||||
import { LightCardConfig } from "./types";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { SUPPORT_BRIGHTNESS } from "../../../data/light";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
@ -44,22 +43,20 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): LightCardConfig {
|
||||
const includeDomains = ["light"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "light", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
@ -119,40 +116,45 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
tabindex="0"
|
||||
></paper-icon-button>
|
||||
|
||||
<div id="controls">
|
||||
<div id="slider">
|
||||
<round-slider
|
||||
min="0"
|
||||
.value=${brightness}
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setBrightness}
|
||||
style=${styleMap({
|
||||
visibility: supportsFeature(stateObj, SUPPORT_BRIGHTNESS)
|
||||
? "visible"
|
||||
: "hidden",
|
||||
})}
|
||||
></round-slider>
|
||||
<paper-icon-button
|
||||
class="light-button ${classMap({
|
||||
"state-on": stateObj.state === "on",
|
||||
"state-unavailable": stateObj.state === "unavailable",
|
||||
})}"
|
||||
.icon=${this._config.icon || stateIcon(stateObj)}
|
||||
style=${styleMap({
|
||||
filter: this._computeBrightness(stateObj),
|
||||
color: this._computeColor(stateObj),
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
tabindex="0"
|
||||
></paper-icon-button>
|
||||
<div class="content">
|
||||
<div id="controls">
|
||||
<div id="slider">
|
||||
${supportsFeature(stateObj, SUPPORT_BRIGHTNESS)
|
||||
? html`
|
||||
<round-slider
|
||||
min="0"
|
||||
.value=${brightness}
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setBrightness}
|
||||
></round-slider>
|
||||
`
|
||||
: ""}
|
||||
<paper-icon-button
|
||||
class="light-button ${classMap({
|
||||
"slider-center": supportsFeature(
|
||||
stateObj,
|
||||
SUPPORT_BRIGHTNESS
|
||||
),
|
||||
"state-on": stateObj.state === "on",
|
||||
"state-unavailable": stateObj.state === "unavailable",
|
||||
})}"
|
||||
.icon=${this._config.icon || stateIcon(stateObj)}
|
||||
style=${styleMap({
|
||||
filter: this._computeBrightness(stateObj),
|
||||
color: this._computeColor(stateObj),
|
||||
})}
|
||||
@click=${this._handleClick}
|
||||
tabindex="0"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="info">
|
||||
<div class="brightness">
|
||||
%
|
||||
<div id="info">
|
||||
<div class="brightness">
|
||||
%
|
||||
</div>
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@ -259,6 +261,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
ha-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
@ -276,6 +280,13 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
z-index: 25;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -301,13 +312,6 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
width: 60%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
max-width: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.light-button.state-on {
|
||||
@ -318,9 +322,17 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
color: var(--state-icon-unavailable-color);
|
||||
}
|
||||
|
||||
.slider-center {
|
||||
position: absolute;
|
||||
max-width: calc(100% - 40px);
|
||||
box-sizing: border-box;
|
||||
border-radius: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#info {
|
||||
display: flex-vertical;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-top: -56px;
|
||||
padding: 16px;
|
||||
|
@ -30,7 +30,6 @@ import { EntityConfig } from "../entity-rows/types";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { MapCardConfig } from "./types";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-map-card")
|
||||
@ -44,28 +43,28 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): MapCardConfig {
|
||||
const includeDomains = ["device_tracker"];
|
||||
const maxEntities = 2;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entities: foundEntities };
|
||||
return { type: "map", entities: foundEntities };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isPanel = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public editMode = false;
|
||||
|
||||
@property()
|
||||
private _config?: MapCardConfig;
|
||||
@ -116,9 +115,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
if (!this._config) {
|
||||
return 3;
|
||||
if (!this._config?.aspect_ratio) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
const ar =
|
||||
ratio && ratio.w > 0 && ratio.h > 0
|
||||
@ -209,6 +209,11 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
if (!this._config.aspect_ratio) {
|
||||
root.style.paddingBottom = "100%";
|
||||
return;
|
||||
}
|
||||
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
|
||||
root.style.paddingBottom =
|
||||
@ -453,15 +458,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host([ispanel]) ha-card {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/**
|
||||
* In panel mode we want a full height map. Since parent #view
|
||||
* only sets min-height, we need absolute positioning here
|
||||
*/
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:host([ispanel][editMode]) ha-card {
|
||||
height: calc(100% - 51px);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
|
@ -29,8 +29,9 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
return document.createElement("hui-markdown-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
public static getStubConfig(): MarkdownCardConfig {
|
||||
return {
|
||||
type: "markdown",
|
||||
content:
|
||||
"The **Markdown** card allows you to write any text. You can style it **bold**, *italicized*, ~strikethrough~ etc. You can do images, links, and more.\n\nFor more information see the [Markdown Cheatsheet](https://commonmark.org/help).",
|
||||
};
|
||||
|
@ -30,7 +30,6 @@ import { stateIcon } from "../../../common/entity/state_icon";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { contrast } from "../common/color/contrast";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
SUPPORT_PAUSE,
|
||||
@ -176,22 +175,20 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): MediaControlCardConfig {
|
||||
const includeDomains = ["media_player"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "media-control", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
@ -694,8 +691,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
Vibrant.from(this._image)
|
||||
.useGenerator(customGenerator)
|
||||
new Vibrant(this._image, {
|
||||
colorCount: 16,
|
||||
generator: customGenerator,
|
||||
})
|
||||
.getPalette()
|
||||
.then(([foreground, background]: [string, string]) => {
|
||||
this._backgroundColor = background;
|
||||
|
@ -30,8 +30,9 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
return document.createElement("hui-picture-card-editor");
|
||||
}
|
||||
public static getStubConfig(): object {
|
||||
public static getStubConfig(): PictureCardConfig {
|
||||
return {
|
||||
type: "picture",
|
||||
image: "https://demo.home-assistant.io/stub_config/t-shirt-promo.png",
|
||||
tap_action: { action: "none" },
|
||||
hold_action: { action: "none" },
|
||||
|
@ -15,24 +15,21 @@ import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceElementConfig, LovelaceElement } from "../elements/types";
|
||||
import { PictureElementsCardConfig } from "./types";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-picture-elements-card")
|
||||
class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): PictureElementsCardConfig {
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
["sensor", "binary_sensor"]
|
||||
);
|
||||
|
||||
|
@ -27,7 +27,7 @@ import { PictureEntityCardConfig } from "./types";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@ -42,21 +42,20 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): PictureEntityCardConfig {
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
["light", "switch"]
|
||||
);
|
||||
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: foundEntities[0] || "",
|
||||
image: "https://demo.home-assistant.io/stub_config/bedroom.png",
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@ -46,17 +46,15 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): PictureGlanceCardConfig {
|
||||
const maxEntities = 2;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
["sensor", "binary_sensor"]
|
||||
);
|
||||
|
||||
|
@ -22,7 +22,6 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { PlantStatusCardConfig, PlantAttributeTarget } from "./types";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
const SENSORS = {
|
||||
@ -44,22 +43,20 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): PlantStatusCardConfig {
|
||||
const includeDomains = ["plant"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "plant-status", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -26,7 +26,6 @@ import { fetchRecent } from "../../../data/history";
|
||||
import { SensorCardConfig } from "./types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
|
||||
@ -179,10 +178,9 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): SensorCardConfig {
|
||||
const includeDomains = ["sensor"];
|
||||
const maxEntities = 1;
|
||||
const entityFilter = (stateObj: HassEntity): boolean => {
|
||||
@ -194,15 +192,14 @@ class HuiSensorCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains,
|
||||
entityFilter
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "", graph: "line" };
|
||||
return { type: "sensor", entity: foundEntities[0] || "", graph: "line" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -37,8 +37,8 @@ class HuiShoppingListCard extends LitElement implements LovelaceCard {
|
||||
return document.createElement("hui-shopping-list-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return {};
|
||||
public static getStubConfig(): ShoppingListCardConfig {
|
||||
return { type: "shopping-list" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -34,7 +34,6 @@ import {
|
||||
} from "../../../data/climate";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
@ -59,22 +58,20 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): ThermostatCardConfig {
|
||||
const includeDomains = ["climate"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "thermostat", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -24,7 +24,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
const cardinalDirections = [
|
||||
@ -76,22 +75,20 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): WeatherForecastCardConfig {
|
||||
const includeDomains = ["weather"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return { entity: foundEntities[0] || "" };
|
||||
return { type: "weather-forecast", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -33,13 +33,15 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
state_color?: boolean;
|
||||
show_name?: boolean;
|
||||
show_icon?: boolean;
|
||||
}
|
||||
|
||||
export interface EntitiesCardConfig extends LovelaceCardConfig {
|
||||
type: "entities";
|
||||
show_header_toggle?: boolean;
|
||||
title?: string;
|
||||
entities: EntitiesCardEntityConfig[];
|
||||
entities: Array<EntitiesCardEntityConfig | string>;
|
||||
theme?: string;
|
||||
icon?: string;
|
||||
header?: LovelaceHeaderFooterConfig;
|
||||
@ -135,8 +137,8 @@ export interface LightCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface MapCardConfig extends LovelaceCardConfig {
|
||||
type: "map";
|
||||
title: string;
|
||||
aspect_ratio: string;
|
||||
title?: string;
|
||||
aspect_ratio?: string;
|
||||
default_zoom?: number;
|
||||
entities?: Array<EntityConfig | string>;
|
||||
geo_location_sources?: string[];
|
||||
|
@ -50,18 +50,18 @@ const addEntities = (entities: Set<string>, obj) => {
|
||||
if (obj.card) {
|
||||
addEntities(entities, obj.card);
|
||||
}
|
||||
if (obj.cards) {
|
||||
if (obj.cards && Array.isArray(obj.cards)) {
|
||||
obj.cards.forEach((card) => addEntities(entities, card));
|
||||
}
|
||||
if (obj.elements) {
|
||||
if (obj.elements && Array.isArray(obj.elements)) {
|
||||
obj.elements.forEach((card) => addEntities(entities, card));
|
||||
}
|
||||
if (obj.badges) {
|
||||
if (obj.badges && Array.isArray(obj.badges)) {
|
||||
obj.badges.forEach((badge) => addEntityId(entities, badge));
|
||||
}
|
||||
};
|
||||
|
||||
export const computeUsedEntities = (config) => {
|
||||
export const computeUsedEntities = (config: LovelaceConfig): Set<string> => {
|
||||
const entities = new Set<string>();
|
||||
config.views.forEach((view) => addEntities(entities, view));
|
||||
return entities;
|
||||
@ -70,13 +70,26 @@ export const computeUsedEntities = (config) => {
|
||||
export const computeUnusedEntities = (
|
||||
hass: HomeAssistant,
|
||||
config: LovelaceConfig
|
||||
): string[] => {
|
||||
): Set<string> => {
|
||||
const usedEntities = computeUsedEntities(config);
|
||||
return Object.keys(hass.states)
|
||||
.filter(
|
||||
(entity) =>
|
||||
!usedEntities.has(entity) &&
|
||||
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
|
||||
)
|
||||
.sort();
|
||||
const unusedEntities = calcUnusedEntities(hass, usedEntities);
|
||||
return unusedEntities;
|
||||
};
|
||||
|
||||
export const calcUnusedEntities = (
|
||||
hass: HomeAssistant,
|
||||
usedEntities: Set<string>
|
||||
): Set<string> => {
|
||||
const unusedEntities: Set<string> = new Set();
|
||||
|
||||
for (const entity of Object.keys(hass.states)) {
|
||||
if (
|
||||
!usedEntities.has(entity) &&
|
||||
!EXCLUDED_DOMAINS.includes(entity.split(".", 1)[0])
|
||||
) {
|
||||
unusedEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return unusedEntities;
|
||||
};
|
||||
|
@ -1,59 +1,72 @@
|
||||
import {
|
||||
computeUnusedEntities,
|
||||
computeUsedEntities,
|
||||
} from "./compute-unused-entities";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const arrayFilter = (
|
||||
array: any[],
|
||||
conditions: Array<(value: any) => boolean>,
|
||||
maxSize: number
|
||||
) => {
|
||||
if (!maxSize || maxSize > array.length) {
|
||||
maxSize = array.length;
|
||||
}
|
||||
|
||||
const filteredArray: any[] = [];
|
||||
|
||||
for (let i = 0; i < array.length && filteredArray.length < maxSize; i++) {
|
||||
let meetsConditions = true;
|
||||
|
||||
for (const condition of conditions) {
|
||||
if (!condition(array[i])) {
|
||||
meetsConditions = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (meetsConditions) {
|
||||
filteredArray.push(array[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredArray;
|
||||
};
|
||||
|
||||
export const findEntities = (
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
maxEntities: number,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[],
|
||||
entities: string[],
|
||||
entitiesFallback: string[],
|
||||
includeDomains?: string[],
|
||||
entityFilter?: (stateObj: HassEntity) => boolean
|
||||
) => {
|
||||
let entityIds: string[];
|
||||
|
||||
entityIds = !entities?.length
|
||||
? computeUnusedEntities(hass, lovelaceConfig)
|
||||
: entities;
|
||||
const conditions: Array<(value: string) => boolean> = [];
|
||||
|
||||
if (includeDomains && includeDomains.length) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains!.includes(computeDomain(eid))
|
||||
);
|
||||
if (includeDomains?.length) {
|
||||
conditions.push((eid) => includeDomains!.includes(computeDomain(eid)));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
entityIds = entityIds.filter(
|
||||
conditions.push(
|
||||
(eid) => hass.states[eid] && entityFilter(hass.states[eid])
|
||||
);
|
||||
}
|
||||
|
||||
if (entityIds.length < (maxEntities || 1)) {
|
||||
let fillEntityIds =
|
||||
entitiesFill && entitiesFill.length
|
||||
? entitiesFill
|
||||
: [...computeUsedEntities(lovelaceConfig)];
|
||||
entityIds = arrayFilter(entities, conditions, maxEntities);
|
||||
|
||||
if (includeDomains && includeDomains.length) {
|
||||
fillEntityIds = fillEntityIds.filter((eid) =>
|
||||
includeDomains!.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
if (entityIds.length < maxEntities && entitiesFallback.length) {
|
||||
const fallbackEntityIds = findEntities(
|
||||
hass,
|
||||
maxEntities - entityIds.length,
|
||||
entitiesFallback,
|
||||
[],
|
||||
includeDomains,
|
||||
entityFilter
|
||||
);
|
||||
|
||||
if (entityFilter) {
|
||||
fillEntityIds = fillEntityIds.filter(
|
||||
(eid) => hass.states[eid] && entityFilter(hass.states[eid])
|
||||
);
|
||||
}
|
||||
|
||||
entityIds = [...entityIds, ...fillEntityIds];
|
||||
entityIds.push(...fallbackEntityIds);
|
||||
}
|
||||
|
||||
return entityIds.slice(0, maxEntities);
|
||||
return entityIds;
|
||||
};
|
||||
|
109
src/panels/lovelace/common/graph/coordinates.ts
Normal file
109
src/panels/lovelace/common/graph/coordinates.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { strokeWidth } from "../../../../data/graph";
|
||||
|
||||
const average = (items: any[]): number => {
|
||||
return (
|
||||
items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) /
|
||||
items.length
|
||||
);
|
||||
};
|
||||
|
||||
const lastValue = (items: any[]): number => {
|
||||
return parseFloat(items[items.length - 1].state) || 0;
|
||||
};
|
||||
|
||||
const calcPoints = (
|
||||
history: any,
|
||||
hours: number,
|
||||
width: number,
|
||||
detail: number,
|
||||
min: number,
|
||||
max: number
|
||||
): number[][] => {
|
||||
const coords = [] as number[][];
|
||||
const height = 80;
|
||||
let yRatio = (max - min) / height;
|
||||
yRatio = yRatio !== 0 ? yRatio : height;
|
||||
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
|
||||
xRatio = isFinite(xRatio) ? xRatio : width;
|
||||
|
||||
const first = history.filter(Boolean)[0];
|
||||
let last = [average(first), lastValue(first)];
|
||||
|
||||
const getCoords = (item: any[], i: number, offset = 0, depth = 1) => {
|
||||
if (depth > 1 && item) {
|
||||
return item.forEach((subItem, index) =>
|
||||
getCoords(subItem, i, index, depth - 1)
|
||||
);
|
||||
}
|
||||
|
||||
const x = xRatio * (i + offset / 6);
|
||||
|
||||
if (item) {
|
||||
last = [average(item), lastValue(item)];
|
||||
}
|
||||
const y =
|
||||
height + strokeWidth / 2 - ((item ? last[0] : last[1]) - min) / yRatio;
|
||||
return coords.push([x, y]);
|
||||
};
|
||||
|
||||
for (let i = 0; i < history.length; i += 1) {
|
||||
getCoords(history[i], i, 0, detail);
|
||||
}
|
||||
|
||||
if (coords.length === 1) {
|
||||
coords[1] = [width, coords[0][1]];
|
||||
}
|
||||
|
||||
coords.push([width, coords[coords.length - 1][1]]);
|
||||
return coords;
|
||||
};
|
||||
|
||||
export const coordinates = (
|
||||
history: any,
|
||||
hours: number,
|
||||
width: number,
|
||||
detail: number
|
||||
): number[][] | undefined => {
|
||||
history.forEach((item) => (item.state = Number(item.state)));
|
||||
history = history.filter((item) => !Number.isNaN(item.state));
|
||||
|
||||
const min = Math.min.apply(
|
||||
Math,
|
||||
history.map((item) => item.state)
|
||||
);
|
||||
const max = Math.max.apply(
|
||||
Math,
|
||||
history.map((item) => item.state)
|
||||
);
|
||||
const now = new Date().getTime();
|
||||
|
||||
const reduce = (res, item, point) => {
|
||||
const age = now - new Date(item.last_changed).getTime();
|
||||
|
||||
let key = Math.abs(age / (1000 * 3600) - hours);
|
||||
if (point) {
|
||||
key = (key - Math.floor(key)) * 60;
|
||||
key = Number((Math.round(key / 10) * 10).toString()[0]);
|
||||
} else {
|
||||
key = Math.floor(key);
|
||||
}
|
||||
if (!res[key]) {
|
||||
res[key] = [];
|
||||
}
|
||||
res[key].push(item);
|
||||
return res;
|
||||
};
|
||||
|
||||
history = history.reduce((res, item) => reduce(res, item, false), []);
|
||||
if (detail > 1) {
|
||||
history = history.map((entry) =>
|
||||
entry.reduce((res, item) => reduce(res, item, true), [])
|
||||
);
|
||||
}
|
||||
|
||||
if (!history.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return calcPoints(history, hours, width, detail, min, max);
|
||||
};
|
24
src/panels/lovelace/common/graph/get-history-coordinates.ts
Normal file
24
src/panels/lovelace/common/graph/get-history-coordinates.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { fetchRecent } from "../../../../data/history";
|
||||
import { coordinates } from "../graph/coordinates";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
export const getHistoryCoordinates = async (
|
||||
hass: HomeAssistant,
|
||||
entity: string,
|
||||
hours: number,
|
||||
detail: number
|
||||
) => {
|
||||
const endTime = new Date();
|
||||
const startTime = new Date();
|
||||
startTime.setHours(endTime.getHours() - hours);
|
||||
|
||||
const stateHistory = await fetchRecent(hass, entity, startTime, endTime);
|
||||
|
||||
if (stateHistory.length < 1 || stateHistory[0].length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = coordinates(stateHistory[0], hours, 500, detail);
|
||||
|
||||
return coords;
|
||||
};
|
36
src/panels/lovelace/common/graph/get-path.ts
Normal file
36
src/panels/lovelace/common/graph/get-path.ts
Normal file
@ -0,0 +1,36 @@
|
||||
const midPoint = (
|
||||
_Ax: number,
|
||||
_Ay: number,
|
||||
_Bx: number,
|
||||
_By: number
|
||||
): number[] => {
|
||||
const _Zx = (_Ax - _Bx) / 2 + _Bx;
|
||||
const _Zy = (_Ay - _By) / 2 + _By;
|
||||
return [_Zx, _Zy];
|
||||
};
|
||||
|
||||
export const getPath = (coords: number[][]): string => {
|
||||
if (!coords.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let next: number[];
|
||||
let Z: number[];
|
||||
const X = 0;
|
||||
const Y = 1;
|
||||
let path = "";
|
||||
let last = coords.filter(Boolean)[0];
|
||||
|
||||
path += `M ${last[X]},${last[Y]}`;
|
||||
|
||||
for (const coord of coords) {
|
||||
next = coord;
|
||||
Z = midPoint(last[X], last[Y], next[X], next[Y]);
|
||||
path += ` ${Z[X]},${Z[Y]}`;
|
||||
path += ` Q${next[X]},${next[Y]}`;
|
||||
last = next;
|
||||
}
|
||||
|
||||
path += ` ${next![X]},${next![Y]}`;
|
||||
return path;
|
||||
};
|
@ -22,6 +22,7 @@ import { hasAction } from "../common/has-action";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
|
||||
@customElement("hui-buttons-base")
|
||||
export class HuiButtonsBase extends LitElement {
|
||||
@ -31,9 +32,12 @@ export class HuiButtonsBase extends LitElement {
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
const entitiesShowingIcons = this.configEntities?.filter(
|
||||
(entity) => entity.show_icon !== false
|
||||
);
|
||||
this._badges.forEach((badge, index: number) => {
|
||||
badge.hass = hass;
|
||||
badge.stateObj = hass.states[this.configEntities![index].entity];
|
||||
badge.stateObj = hass.states[entitiesShowingIcons![index].entity];
|
||||
});
|
||||
}
|
||||
|
||||
@ -46,22 +50,33 @@ export class HuiButtonsBase extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<state-badge
|
||||
title=${computeTooltip(this._hass!, entityConf)}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
.hass=${this._hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
tabindex="0"
|
||||
></state-badge>
|
||||
<div
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(entityConf.hold_action),
|
||||
hasDoubleClick: hasAction(entityConf.double_tap_action),
|
||||
})}
|
||||
.config=${entityConf}
|
||||
tabindex="0"
|
||||
>
|
||||
${entityConf.show_icon !== false
|
||||
? html`
|
||||
<state-badge
|
||||
title=${computeTooltip(this._hass!, entityConf)}
|
||||
.hass=${this._hass}
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
stateColor
|
||||
></state-badge>
|
||||
`
|
||||
: ""}
|
||||
<span>
|
||||
${entityConf.show_name ||
|
||||
(entityConf.name && entityConf.show_name !== false)
|
||||
? entityConf.name || computeStateName(stateObj)
|
||||
: ""}
|
||||
</span>
|
||||
<mwc-ripple unbounded></mwc-ripple>
|
||||
</div>
|
||||
`;
|
||||
@ -88,8 +103,10 @@ export class HuiButtonsBase extends LitElement {
|
||||
.missing {
|
||||
color: #fce588;
|
||||
}
|
||||
state-badge {
|
||||
div {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -78,6 +78,11 @@ export class HuiCardOptions extends LitElement {
|
||||
"ui.panel.lovelace.editor.edit_card.move"
|
||||
)}</paper-item
|
||||
>
|
||||
<paper-item @tap=${this._duplicateCard}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||
)}</paper-item
|
||||
>
|
||||
<paper-item .class="delete-item" @tap="${this._deleteCard}">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
@ -152,6 +157,17 @@ export class HuiCardOptions extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _duplicateCard(): void {
|
||||
const path = this.path!;
|
||||
const cardConfig = this.lovelace!.config.views[path[0]].cards![path[1]];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
cardConfig,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: [path[0]],
|
||||
});
|
||||
}
|
||||
|
||||
private _editCard(): void {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
|
78
src/panels/lovelace/components/hui-graph-base.ts
Normal file
78
src/panels/lovelace/components/hui-graph-base.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
property,
|
||||
css,
|
||||
CSSResult,
|
||||
svg,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
|
||||
import { strokeWidth } from "../../../data/graph";
|
||||
import { getPath } from "../common/graph/get-path";
|
||||
|
||||
@customElement("hui-graph-base")
|
||||
export class HuiGraphBase extends LitElement {
|
||||
@property() public coordinates?: any;
|
||||
@property() private _path?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._path
|
||||
? svg`<svg width="100%" height="100%" viewBox="0 0 500 100">
|
||||
<g>
|
||||
<mask id="fill">
|
||||
<path
|
||||
class='fill'
|
||||
fill='white'
|
||||
d="${this._path} L 500, 100 L 0, 100 z"
|
||||
/>
|
||||
</mask>
|
||||
<rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect>
|
||||
<mask id="line">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="var(--accent-color)"
|
||||
stroke-width="${strokeWidth}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d=${this._path}
|
||||
></path>
|
||||
</mask>
|
||||
<rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect>
|
||||
</g>
|
||||
</svg>`
|
||||
: svg`<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this.coordinates) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("coordinates")) {
|
||||
this._path = getPath(this.coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.fill {
|
||||
opacity: 0.1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-graph-base": HuiGraphBase;
|
||||
}
|
||||
}
|
@ -10,18 +10,7 @@ import {
|
||||
import "@material/mwc-button";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"theme-changed": undefined;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"theme-changed": HASSDomEvent<undefined>;
|
||||
}
|
||||
}
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("hui-theme-select-editor")
|
||||
export class HuiThemeSelectEditor extends LitElement {
|
||||
@ -30,32 +19,34 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const themes = ["Backend-selected", "default"].concat(
|
||||
Object.keys(this.hass!.themes.themes).sort()
|
||||
);
|
||||
|
||||
return html`
|
||||
<paper-dropdown-menu
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.generic.theme") +
|
||||
" (" +
|
||||
this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
) +
|
||||
")"}
|
||||
`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
dynamic-align
|
||||
@value-changed="${this._changed}"
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected="${this.value}"
|
||||
.selected=${this.value}
|
||||
attr-for-selected="theme"
|
||||
@iron-select=${this._changed}
|
||||
>
|
||||
${themes.map((theme) => {
|
||||
return html`
|
||||
<paper-item theme="${theme}">${theme}</paper-item>
|
||||
`;
|
||||
})}
|
||||
<paper-item theme="remove"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.no_theme"
|
||||
)}</paper-item
|
||||
>
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
.sort()
|
||||
.map((theme) => {
|
||||
return html`
|
||||
<paper-item theme=${theme}>${theme}</paper-item>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`;
|
||||
@ -70,11 +61,11 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _changed(ev): void {
|
||||
if (!this.hass || ev.target.value === "") {
|
||||
if (!this.hass || ev.target.selected === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.target.value;
|
||||
fireEvent(this, "theme-changed");
|
||||
this.value = ev.target.selected === "remove" ? "" : ev.target.selected;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,8 @@ export class HuiUnavailable extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="disabled-overlay">
|
||||
<div>${this.text}</div>
|
||||
</div>
|
||||
<div class="disabled-overlay"></div>
|
||||
<div class="disabled-overlay-text">${this.text}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -31,11 +30,11 @@ export class HuiUnavailable extends LitElement {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--state-icon-unavailable-color);
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.disabled-overlay div {
|
||||
.disabled-overlay-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@ -44,6 +43,8 @@ export class HuiUnavailable extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
z-index: 50;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user