Add state and related to entity reg dialog (#4473)

* Add state and related to entity reg dialog

* Replace more-info-settings, remove state tab add state button
This commit is contained in:
Bram Kragten 2020-01-23 00:03:47 +01:00 committed by Paulus Schoutsen
parent a544295167
commit ae8a9940ed
10 changed files with 824 additions and 387 deletions

View File

@ -0,0 +1,323 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import { SceneEntity } from "../data/scene";
import { findRelated, ItemType, RelatedResult } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import "./ha-switch";
@customElement("ha-related-items")
export class HaRelatedItems extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public itemType!: ItemType;
@property() public itemId!: string;
@property() private _entries?: ConfigEntry[];
@property() private _devices?: DeviceRegistryEntry[];
@property() private _areas?: AreaRegistryEntry[];
@property() private _related?: RelatedResult;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
];
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass).then((configEntries) => {
this._entries = configEntries;
});
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
(changedProps.has("itemId") || changedProps.has("itemType")) &&
this.itemId &&
this.itemType
) {
this._findRelated();
}
}
protected render(): TemplateResult | void {
if (!this._related) {
return html``;
}
return html`
${this._related.config_entry && this._entries
? this._related.config_entry.map((relatedConfigEntryId) => {
const entry: ConfigEntry | undefined = this._entries!.find(
(configEntry) => configEntry.entry_id === relatedConfigEntryId
);
if (!entry) {
return;
}
return html`
<h3>
${this.hass.localize(
"ui.components.related-items.integration"
)}:
</h3>
<a
href="/config/integrations/config_entry/${relatedConfigEntryId}"
@click=${this._close}
>
${this.hass.localize(`component.${entry.domain}.config.title`)}:
${entry.title}
</a>
`;
})
: ""}
${this._related.device && this._devices
? this._related.device.map((relatedDeviceId) => {
const device: DeviceRegistryEntry | undefined = this._devices!.find(
(dev) => dev.id === relatedDeviceId
);
if (!device) {
return;
}
return html`
<h3>
${this.hass.localize("ui.components.related-items.device")}:
</h3>
<a
href="/config/devices/device/${relatedDeviceId}"
@click=${this._close}
>
${device.name_by_user || device.name}
</a>
`;
})
: ""}
${this._related.area && this._areas
? this._related.area.map((relatedAreaId) => {
const area: AreaRegistryEntry | undefined = this._areas!.find(
(ar) => ar.area_id === relatedAreaId
);
if (!area) {
return;
}
return html`
<h3>
${this.hass.localize("ui.components.related-items.area")}:
</h3>
${area.name}
`;
})
: ""}
${this._related.entity
? html`
<h3>
${this.hass.localize("ui.components.related-items.entity")}:
</h3>
<ul>
${this._related.entity.map((entityId) => {
const entity: HassEntity | undefined = this.hass.states[
entityId
];
if (!entity) {
return;
}
return html`
<li>
<button
@click=${this._openMoreInfo}
.entityId="${entityId}"
class="link"
>
${entity.attributes.friendly_name || entityId}
</button>
</li>
`;
})}
</ul>
`
: ""}
${this._related.group
? html`
<h3>${this.hass.localize("ui.components.related-items.group")}:</h3>
<ul>
${this._related.group.map((groupId) => {
const group: HassEntity | undefined = this.hass.states[groupId];
if (!group) {
return;
}
return html`
<li>
<button
class="link"
@click=${this._openMoreInfo}
.entityId="${groupId}"
>
${group.attributes.friendly_name || group.entity_id}
</button>
</li>
`;
})}
</ul>
`
: ""}
${this._related.scene
? html`
<h3>${this.hass.localize("ui.components.related-items.scene")}:</h3>
<ul>
${this._related.scene.map((sceneId) => {
const scene: SceneEntity | undefined = this.hass.states[
sceneId
];
if (!scene) {
return;
}
return html`
<li>
<button
class="link"
@click=${this._openMoreInfo}
.entityId="${sceneId}"
>
${scene.attributes.friendly_name || scene.entity_id}
</button>
</li>
`;
})}
</ul>
`
: ""}
${this._related.automation
? html`
<h3>
${this.hass.localize("ui.components.related-items.automation")}:
</h3>
<ul>
${this._related.automation.map((automationId) => {
const automation: HassEntity | undefined = this.hass.states[
automationId
];
if (!automation) {
return;
}
return html`
<li>
<button
class="link"
@click=${this._openMoreInfo}
.entityId="${automationId}"
>
${automation.attributes.friendly_name ||
automation.entity_id}
</button>
</li>
`;
})}
</ul>
`
: ""}
${this._related.script
? html`
<h3>
${this.hass.localize("ui.components.related-items.script")}:
</h3>
<ul>
${this._related.script.map((scriptId) => {
const script: HassEntity | undefined = this.hass.states[
scriptId
];
if (!script) {
return;
}
return html`
<li>
<button
class="link"
@click=${this._openMoreInfo}
.entityId="${scriptId}"
>
${script.attributes.friendly_name || script.entity_id}
</button>
</li>
`;
})}
</ul>
`
: ""}
`;
}
private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId);
await this.updateComplete;
fireEvent(this, "iron-resize");
}
private _openMoreInfo(ev: CustomEvent) {
const entityId = (ev.target as any).entityId;
fireEvent(this, "hass-more-info", { entityId });
}
private _close() {
fireEvent(this, "close-dialog");
}
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
button.link {
color: var(--primary-color);
text-align: left;
cursor: pointer;
background: none;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
padding: 0px;
font: inherit;
text-decoration: underline;
}
h3 {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(
--paper-font-title_-_-webkit-font-smoothing
);
font-size: var(--paper-font-title_-_font-size);
font-weight: var(--paper-font-headline-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
opacity: var(--dark-primary-opacity);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-related-items": HaRelatedItems;
}
}

33
src/data/search.ts Normal file
View File

@ -0,0 +1,33 @@
import { HomeAssistant } from "../types";
export interface RelatedResult {
area?: string[];
automation?: string[];
config_entry?: string[];
device?: string[];
entity?: string[];
group?: string[];
scene?: string[];
script?: string[];
}
export type ItemType =
| "area"
| "automation"
| "config_entry"
| "device"
| "entity"
| "group"
| "scene"
| "script";
export const findRelated = (
hass: HomeAssistant,
itemType: ItemType,
itemId: string
): Promise<RelatedResult> =>
hass.callWS({
type: "search/related",
item_type: itemType,
item_id: itemId,
});

View File

@ -6,7 +6,6 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../resources/ha-style";
import "./more-info/more-info-controls";
import "./more-info/more-info-settings";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@ -26,8 +25,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
border-radius: 2px;
}
more-info-controls,
more-info-settings {
more-info-controls {
--more-info-header-background: var(--secondary-background-color);
--more-info-header-color: var(--primary-text-color);
--ha-more-info-app-toolbar-title: {
@ -46,8 +44,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
more-info-controls,
more-info-settings {
more-info-controls {
--more-info-header-background: var(--primary-color);
--more-info-header-color: var(--text-primary-color);
}
@ -79,24 +76,14 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
}
</style>
<template is="dom-if" if="[[!_page]]">
<more-info-controls
class="no-padding"
hass="[[hass]]"
state-obj="[[stateObj]]"
dialog-element="[[_dialogElement]]"
can-configure="[[_registryInfo]]"
large="{{large}}"
></more-info-controls>
</template>
<template is="dom-if" if='[[_equals(_page, "settings")]]'>
<more-info-settings
class="no-padding"
hass="[[hass]]"
state-obj="[[stateObj]]"
registry-info="{{_registryInfo}}"
></more-info-settings>
</template>
<more-info-controls
class="no-padding"
hass="[[hass]]"
state-obj="[[stateObj]]"
dialog-element="[[_dialogElement]]"
registry-entry="[[_registryInfo]]"
large="{{large}}"
></more-info-controls>
`;
}
@ -118,11 +105,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
_dialogElement: Object,
_registryInfo: Object,
_page: {
type: String,
value: null,
},
dataDomain: {
computed: "_computeDomain(stateObj)",
reflectToAttribute: true,
@ -137,9 +119,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
ready() {
super.ready();
this._dialogElement = this;
this.addEventListener("more-info-page", (ev) => {
this._page = ev.detail.page;
});
}
_computeDomain(stateObj) {
@ -154,7 +133,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) {
if (!newVal) {
this.setProperties({
opened: false,
_page: null,
_registryInfo: null,
large: false,
});

View File

@ -5,6 +5,7 @@ declare global {
// for fire event
interface HASSDomEvents {
"show-dialog": ShowDialogParams<unknown>;
"close-dialog": undefined;
}
// for add event listener
interface HTMLElementEventMap {

View File

@ -22,6 +22,7 @@ import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showConfirmationDialog } from "../confirmation/show-dialog-confirmation";
import { showEntityRegistryDetailDialog } from "../../panels/config/entities/show-dialog-entity-registry-detail";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
@ -87,7 +88,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
<div class="main-title" main-title="" on-click="enlarge">
[[_computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[canConfigure]]">
<template is="dom-if" if="[[registryEntry]]">
<paper-icon-button
aria-label$="[[localize('ui.dialogs.more_info_control.settings')]]"
icon="hass:settings"
@ -158,7 +159,7 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
},
dialogElement: Object,
canConfigure: Boolean,
registryEntry: Object,
domain: {
type: String,
@ -259,7 +260,8 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
_gotoSettings() {
this.fire("more-info-page", { page: "settings" });
showEntityRegistryDetailDialog(this, { entry: this.registryEntry });
this.fire("hass-more-info", { entityId: null });
}
_gotoEdit() {

View File

@ -1,141 +0,0 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { updateEntityRegistryEntry } from "../../data/entity_registry";
import { showSaveSuccessToast } from "../../util/toast-saved-success";
import "../../components/ha-paper-icon-button-arrow-prev";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
*/
class MoreInfoSettings extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
app-toolbar {
color: var(--more-info-header-color);
background-color: var(--more-info-header-background);
/* to fit save button */
padding-right: 0;
}
app-toolbar [main-title] {
@apply --ha-more-info-app-toolbar-title;
}
app-toolbar mwc-button {
font-size: 0.8em;
margin: 0;
--mdc-theme-primary: var(--more-info-header-color);
}
.form {
padding: 0 24px 24px;
}
</style>
<app-toolbar>
<ha-paper-icon-button-arrow-prev
aria-label$="[[localize('ui.dialogs.more_info_settings.back')]]"
on-click="_backTapped"
></ha-paper-icon-button-arrow-prev>
<div main-title="">[[_computeStateName(stateObj)]]</div>
<mwc-button on-click="_save" disabled="[[_computeInvalid(_entityId)]]"
>[[localize('ui.dialogs.more_info_settings.save')]]</mwc-button
>
</app-toolbar>
<div class="form">
<paper-input
value="{{_name}}"
label="[[localize('ui.dialogs.more_info_settings.name')]]"
></paper-input>
<paper-input
value="{{_entityId}}"
label="[[localize('ui.dialogs.more_info_settings.entity_id')]]"
error-message="Domain needs to stay the same"
invalid="[[_computeInvalid(_entityId)]]"
></paper-input>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
registryInfo: {
type: Object,
observer: "_registryInfoChanged",
notify: true,
},
_name: String,
_entityId: String,
};
}
_computeStateName(stateObj) {
if (!stateObj) return "";
return computeStateName(stateObj);
}
_computeInvalid(entityId) {
return computeDomain(this.stateObj.entity_id) !== computeDomain(entityId);
}
_registryInfoChanged(newVal) {
if (newVal) {
this.setProperties({
_name: newVal.name,
_entityId: newVal.entity_id,
});
} else {
this.setProperties({
_name: "",
_entityId: "",
});
}
}
_backTapped() {
this.fire("more-info-page", { page: null });
}
async _save() {
try {
const info = await updateEntityRegistryEntry(
this.hass,
this.stateObj.entity_id,
{
name: this._name,
new_entity_id: this._entityId,
}
);
showSaveSuccessToast(this, this.hass);
this.registryInfo = info;
// Keep the more info dialog open at the new entity.
if (this.stateObj.entity_id !== this._entityId) {
this.fire("hass-more-info", { entityId: this._entityId });
}
} catch (err) {
alert(`save failed: ${err.message}`);
}
}
}
customElements.define("more-info-settings", MoreInfoSettings);

View File

@ -1,206 +1,151 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { HassEntity } from "home-assistant-js-websocket";
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/dialog/ha-paper-dialog";
import "../../../components/ha-switch";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail";
// tslint:disable-next-line: no-duplicate-imports
import { HaPaperDialog } from "../../../components/dialog/ha-paper-dialog";
import "../../../components/ha-related-items";
import "../../../dialogs/more-info/controls/more-info-content";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import "../../../state-summary/state-card-content";
import { HomeAssistant } from "../../../types";
import { HassEntity } from "home-assistant-js-websocket";
// tslint:disable-next-line: no-duplicate-imports
import { HaSwitch } from "../../../components/ha-switch";
import "./entity-registry-settings";
import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
updateEntityRegistryEntry,
removeEntityRegistryEntry,
} from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
class DialogEntityRegistryDetail extends LitElement {
@customElement("dialog-entity-registry-detail")
export class DialogEntityRegistryDetail extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _name!: string;
@property() private _entityId!: string;
@property() private _disabledBy!: string | null;
@property() private _error?: string;
@property() private _params?: EntityRegistryDetailDialogParams;
@property() private _submitting?: boolean;
private _origEntityId!: string;
@property() private _curTab?: string;
@query("ha-paper-dialog") private _dialog!: HaPaperDialog;
private _curTabIndex = 0;
public async showDialog(
params: EntityRegistryDetailDialogParams
): Promise<void> {
this._params = params;
this._error = undefined;
this._name = this._params.entry.name || "";
this._origEntityId = this._params.entry.entity_id;
this._entityId = this._params.entry.entity_id;
this._disabledBy = this._params.entry.disabled_by;
await this.updateComplete;
}
public closeDialog(): void {
this._params = undefined;
}
protected render(): TemplateResult | void {
if (!this._params) {
return html``;
}
const entry = this._params.entry;
const stateObj: HassEntity | undefined = this.hass.states[entry.entity_id];
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this._params.entry.entity_id);
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
@opened-changed=${this._openedChanged}
>
<h2>
${stateObj
? computeStateName(stateObj)
: entry.name || entry.entity_id}
</h2>
<paper-dialog-scrollable>
${!stateObj
? html`
<div>
${this.hass!.localize(
"ui.panel.config.entities.editor.unavailable"
)}
</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.dialogs.more_info_settings.name")}
.placeholder=${stateObj ? computeStateName(stateObj) : ""}
.disabled=${this._submitting}
></paper-input>
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
.label=${this.hass.localize(
"ui.dialogs.more_info_settings.entity_id"
)}
error-message="Domain needs to stay the same"
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@change=${this._disabledByChanged}
>
<div>
<div>
${this.hass.localize(
"ui.panel.config.entities.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.panel.config.entities.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.panel.config.entities.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.panel.config.entities.editor.note"
)}
</div>
</div>
</ha-switch>
</div>
<app-toolbar>
<paper-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.entities.dialog.dismiss"
)}
icon="hass:close"
dialog-dismiss
></paper-icon-button>
<div class="main-title" main-title>
${stateObj
? computeStateName(stateObj)
: entry.name || entry.entity_id}
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button
class="warning"
@click="${this._confirmDeleteEntry}"
.disabled=${this._submitting ||
!(stateObj && stateObj.attributes.restored)}
>
${this.hass.localize("ui.panel.config.entities.editor.delete")}
</mwc-button>
<mwc-button
@click="${this._updateEntry}"
.disabled=${invalidDomainUpdate || this._submitting}
>
${this.hass.localize("ui.panel.config.entities.editor.update")}
</mwc-button>
</div>
${stateObj
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.panel.config.entities.dialog.control"
)}
icon="hass:tune"
@click=${this._openMoreInfo}
></paper-icon-button>
`
: ""}
</app-toolbar>
<paper-tabs
scrollable
hide-scroll-buttons
.selected=${this._curTabIndex}
@selected-item-changed=${this._handleTabSelected}
>
<paper-tab id="tab-settings">
${this.hass.localize("ui.panel.config.entities.dialog.settings")}
</paper-tab>
<paper-tab id="tab-related">
${this.hass.localize("ui.panel.config.entities.dialog.related")}
</paper-tab>
</paper-tabs>
${cache(
this._curTab === "tab-settings"
? html`
<entity-registry-settings
.hass=${this.hass}
.entry=${entry}
.dialogElement=${this._dialog}
@close-dialog=${this._closeDialog}
></entity-registry-settings>
`
: this._curTab === "tab-related"
? html`
<paper-dialog-scrollable>
<ha-related-items
.hass=${this.hass}
.itemId=${entry.entity_id}
itemType="entity"
@close-dialog=${this._closeDialog}
></ha-related-items>
</paper-dialog-scrollable>
`
: html``
)}
</ha-paper-dialog>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._name = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._entityId = ev.detail.value;
}
private async _updateEntry(): Promise<void> {
this._submitting = true;
try {
await updateEntityRegistryEntry(this.hass!, this._origEntityId, {
name: this._name.trim() || null,
disabled_by: this._disabledBy,
new_entity_id: this._entityId.trim(),
});
this._params = undefined;
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
private _handleTabSelected(ev: CustomEvent): void {
if (!ev.detail.value) {
return;
}
this._curTab = ev.detail.value.id;
this._resizeDialog();
}
private async _deleteEntry(): Promise<void> {
this._submitting = true;
try {
await removeEntityRegistryEntry(this.hass!, this._entityId);
this._params = undefined;
} finally {
this._submitting = false;
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog as HTMLElement, "iron-resize");
}
private _confirmDeleteEntry(): void {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.entities.editor.confirm_delete"
),
confirm: () => this._deleteEntry(),
private _openMoreInfo(): void {
fireEvent(this, "hass-more-info", {
entityId: this._params!.entry.entity_id,
});
this._params = undefined;
}
private _closeDialog(): void {
this._params = undefined;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
@ -208,36 +153,88 @@ class DialogEntityRegistryDetail extends LitElement {
this._params = undefined;
}
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
app-toolbar {
color: var(--primary-text-color);
background-color: var(--secondary-background-color);
margin: 0;
padding: 0 16px;
}
app-toolbar [main-title] {
/* Design guideline states 24px, changed to 16 to align with state info */
margin-left: 16px;
line-height: 1.3em;
max-height: 2.6em;
overflow: hidden;
/* webkit and blink still support simple multiline text-overflow */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
@media all and (min-width: 451px) and (min-height: 501px) {
.main-title {
pointer-events: auto;
cursor: default;
}
}
ha-paper-dialog {
width: 450px;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
app-toolbar {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
ha-paper-dialog {
height: 100%;
max-height: 100% !important;
width: 100% !important;
border-radius: 0px;
position: fixed !important;
margin: 0;
}
ha-paper-dialog::before {
content: "";
position: fixed;
z-index: -1;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: inherit;
}
}
paper-dialog-scrollable {
padding-bottom: 16px;
}
mwc-button.warning {
--mdc-theme-primary: var(--google-red-500);
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
:host {
--paper-font-title_-_white-space: normal;
}
ha-paper-dialog {
min-width: 400px;
max-width: 450px;
}
.form {
padding-bottom: 24px;
}
mwc-button.warning {
margin-right: auto;
}
.error {
color: var(--google-red-500);
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
paper-tabs {
--paper-tabs-selection-bar-color: var(--primary-color);
text-transform: uppercase;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin-top: 0;
}
`,
];
@ -249,8 +246,3 @@ declare global {
"dialog-entity-registry-detail": DialogEntityRegistryDetail;
}
}
customElements.define(
"dialog-entity-registry-detail",
DialogEntityRegistryDetail
);

View File

@ -0,0 +1,237 @@
import {
LitElement,
html,
css,
CSSResult,
TemplateResult,
property,
customElement,
PropertyValues,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import "../../../components/ha-switch";
import { PolymerChangedEvent } from "../../../polymer-types";
import { HomeAssistant } from "../../../types";
import { HassEntity } from "home-assistant-js-websocket";
// tslint:disable-next-line: no-duplicate-imports
import { HaSwitch } from "../../../components/ha-switch";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
updateEntityRegistryEntry,
removeEntityRegistryEntry,
EntityRegistryEntry,
} from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends LitElement {
@property() public hass!: HomeAssistant;
@property() public entry!: EntityRegistryEntry;
@property() public dialogElement!: HTMLElement;
@property() private _name!: string;
@property() private _entityId!: string;
@property() private _disabledBy!: string | null;
@property() private _error?: string;
@property() private _submitting?: boolean;
private _origEntityId!: string;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("entry")) {
this._error = undefined;
this._name = this.entry.name || "";
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
}
}
protected render(): TemplateResult | void {
if (this.entry.entity_id !== this._origEntityId) {
return;
}
const stateObj: HassEntity | undefined = this.hass.states[
this.entry.entity_id
];
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
return html`
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
${!stateObj
? html`
<div>
${this.hass!.localize(
"ui.panel.config.entities.editor.unavailable"
)}
</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.entities.editor.name")}
.placeholder=${stateObj ? computeStateName(stateObj) : ""}
.disabled=${this._submitting}
></paper-input>
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
.label=${this.hass.localize(
"ui.panel.config.entities.editor.entity_id"
)}
error-message="Domain needs to stay the same"
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
></paper-input>
<div class="row">
<ha-switch
.checked=${!this._disabledBy}
@change=${this._disabledByChanged}
>
<div>
<div>
${this.hass.localize(
"ui.panel.config.entities.editor.enabled_label"
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.panel.config.entities.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
${this.hass.localize(
"ui.panel.config.entities.editor.enabled_description"
)}
<br />${this.hass.localize(
"ui.panel.config.entities.editor.note"
)}
</div>
</div>
</ha-switch>
</div>
</div>
</paper-dialog-scrollable>
<div class="buttons">
<mwc-button
class="warning"
@click="${this._confirmDeleteEntry}"
.disabled=${this._submitting ||
!(stateObj && stateObj.attributes.restored)}
>
${this.hass.localize("ui.panel.config.entities.editor.delete")}
</mwc-button>
<mwc-button
@click="${this._updateEntry}"
.disabled=${invalidDomainUpdate || this._submitting}
>
${this.hass.localize("ui.panel.config.entities.editor.update")}
</mwc-button>
</div>
`;
}
private _nameChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._name = ev.detail.value;
}
private _entityIdChanged(ev: PolymerChangedEvent<string>): void {
this._error = undefined;
this._entityId = ev.detail.value;
}
private async _updateEntry(): Promise<void> {
this._submitting = true;
try {
await updateEntityRegistryEntry(this.hass!, this._origEntityId, {
name: this._name.trim() || null,
disabled_by: this._disabledBy,
new_entity_id: this._entityId.trim(),
});
fireEvent(this as HTMLElement, "close-dialog");
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
this._submitting = false;
}
}
private async _deleteEntry(): Promise<void> {
this._submitting = true;
try {
await removeEntityRegistryEntry(this.hass!, this._entityId);
fireEvent(this as HTMLElement, "close-dialog");
} finally {
this._submitting = false;
}
}
private _confirmDeleteEntry(): void {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.entities.editor.confirm_delete"
),
confirm: () => this._deleteEntry(),
});
}
private _disabledByChanged(ev: Event): void {
this._disabledBy = (ev.target as HaSwitch).checked ? null : "user";
}
static get styles(): CSSResult {
return css`
:host {
display: block;
margin-bottom: 0 !important;
padding: 0 !important;
}
.form {
padding-bottom: 24px;
}
.buttons {
display: flex;
justify-content: flex-end;
padding: 8px;
}
mwc-button.warning {
margin-right: auto;
--mdc-theme-primary: var(--google-red-500);
}
.error {
color: var(--google-red-500);
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
}
.secondary {
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"entity-registry-settings": EntityRegistrySettings;
}
}

View File

@ -46,9 +46,11 @@ import {
DataTableColumnData,
} from "../../../components/data-table/ha-data-table";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail";
@customElement("ha-config-entities")
export class HaConfigEntities extends LitElement {
export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@ -59,8 +61,6 @@ export class HaConfigEntities extends LitElement {
@property() private _selectedEntities: string[] = [];
@query("ha-data-table") private _dataTable!: HaDataTable;
private _unsubEntities?: UnsubscribeFunc;
private _columns = memoize(
(narrow, _language): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
@ -196,10 +196,23 @@ export class HaConfigEntities extends LitElement {
}
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubEntities) {
this._unsubEntities();
const dialog = document
.querySelector("home-assistant")!
.shadowRoot!.querySelector("dialog-entity-registry-detail") as
| DialogEntityRegistryDetail
| undefined;
if (dialog) {
dialog.closeDialog();
}
}
@ -362,18 +375,6 @@ export class HaConfigEntities extends LitElement {
loadEntityRegistryDetailDialog();
}
protected updated(changedProps) {
super.updated(changedProps);
if (!this._unsubEntities) {
this._unsubEntities = subscribeEntityRegistry(
this.hass.connection,
(entities) => {
this._entities = entities;
}
);
}
}
private _showDisabledChanged() {
this._showDisabled = !this._showDisabled;
}

View File

@ -553,6 +553,16 @@
},
"service-picker": {
"service": "Service"
},
"related-items": {
"integration": "Integration",
"device": "Device",
"area": "Area",
"entity": "Related entities",
"group": "Part of the following groups",
"scene": "Part of the following scenes",
"script": "Part of the following scripts",
"automation": "Part of the following automations"
}
},
"dialogs": {
@ -593,12 +603,6 @@
"confirm_remove_text": "Are you sure you want to remove this entity?"
}
},
"more_info_settings": {
"back": "Go back",
"save": "Save",
"name": "Name Override",
"entity_id": "Entity ID"
},
"options_flow": {
"form": {
"header": "Options"
@ -1300,9 +1304,16 @@
"confirm_text": "Entities can only be removed when the integration is no longer providing the entities."
}
},
"dialog": {
"settings": "Settings",
"control": "Control",
"related": "Related",
"dismiss": "Dismiss"
},
"editor": {
"name": "Name Override",
"entity_id": "Entity ID",
"unavailable": "This entity is not currently available.",
"default_name": "New Area",
"enabled_label": "Enable entity",
"enabled_cause": "Disabled by {cause}.",
"enabled_description": "Disabled entities will not be added to Home Assistant.",