From 95b76dbb85a5ac0f290effbb9060d7a2da1a1490 Mon Sep 17 00:00:00 2001 From: Phi Dong Date: Thu, 3 Oct 2019 02:50:36 -0700 Subject: [PATCH 01/52] Fix issue where help icon overlapped with header text on mobile devices (#3868) --- src/panels/config/zwave/ha-config-zwave.js | 9 +++++++-- src/panels/config/zwave/zwave-network.ts | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/panels/config/zwave/ha-config-zwave.js b/src/panels/config/zwave/ha-config-zwave.js index 6169a54bdc..ccfaaa09ef 100644 --- a/src/panels/config/zwave/ha-config-zwave.js +++ b/src/panels/config/zwave/ha-config-zwave.js @@ -44,6 +44,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { margin-top: 24px; } + .sectionHeader { + position: relative; + padding-right: 40px; + } + .node-info { margin-left: 16px; } @@ -77,7 +82,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { .toggle-help-icon { position: absolute; - top: 6px; + top: -6px; right: 0; color: var(--primary-color); } @@ -102,7 +107,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { -
+
Z-Wave Node Management -
+
${this.hass!.localize( "ui.panel.config.zwave.network_management.header" @@ -234,6 +234,11 @@ export class ZwaveNetwork extends LitElement { margin-top: 24px; } + .sectionHeader { + position: relative; + padding-right: 40px; + } + .network-status { text-align: center; } From e148559d3e1348dcf79bdc4e4bef2ffcde9bb000 Mon Sep 17 00:00:00 2001 From: Sven <1040402+svendroid@users.noreply.github.com> Date: Thu, 3 Oct 2019 12:22:45 +0200 Subject: [PATCH 02/52] Add localization to user config page (#3845) (#3869) --- .../config/users/ha-config-user-picker.js | 3 +- src/panels/config/users/ha-user-editor.ts | 51 +++++++++++++++---- src/translations/en.json | 16 +++++- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/panels/config/users/ha-config-user-picker.js b/src/panels/config/users/ha-config-user-picker.js index 8610e6b5b9..483abc1a55 100644 --- a/src/panels/config/users/ha-config-user-picker.js +++ b/src/panels/config/users/ha-config-user-picker.js @@ -68,7 +68,8 @@ class HaUserPicker extends EventsMixin(
[[_computeGroup(localize, user)]]
diff --git a/src/panels/config/users/ha-user-editor.ts b/src/panels/config/users/ha-user-editor.ts index 66775e4247..f45f8a08c5 100644 --- a/src/panels/config/users/ha-user-editor.ts +++ b/src/panels/config/users/ha-user-editor.ts @@ -52,15 +52,15 @@ class HaUserEditor extends LitElement { - + - + - + + - +
ID${hass.localize("ui.panel.config.users.editor.id")} ${user.id}
Owner${hass.localize("ui.panel.config.users.editor.owner")} ${user.is_owner}
Group${hass.localize("ui.panel.config.users.editor.group")} Active${hass.localize("ui.panel.config.users.editor.active")} ${user.is_active}
System generated + ${hass.localize( + "ui.panel.config.users.editor.system_generated" + )} + ${user.system_generated}
@@ -114,7 +118,9 @@ class HaUserEditor extends LitElement { ${user.system_generated ? html` - Unable to remove system generated users. + ${hass.localize( + "ui.panel.config.users.editor.system_generated_users_not_removable" + )} ` : ""}
@@ -124,12 +130,19 @@ class HaUserEditor extends LitElement { } private get _name() { - return this.user && (this.user.name || "Unnamed user"); + return ( + this.user && + (this.user.name || + this.hass!.localize("ui.panel.config.users.editor.unnamed_user")) + ); } private async _handleRenameUser(ev): Promise { ev.currentTarget.blur(); - const newName = prompt("New name?", this.user!.name); + const newName = prompt( + this.hass!.localize("ui.panel.config.users.editor.enter_new_name"), + this.user!.name + ); if (newName === null || newName === this.user!.name) { return; } @@ -140,7 +153,11 @@ class HaUserEditor extends LitElement { }); fireEvent(this, "reload-users"); } catch (err) { - alert(`User rename failed: ${err.message}`); + alert( + `${this.hass!.localize( + "ui.panel.config.users.editor.user_rename_failed" + )} ${err.message}` + ); } } @@ -154,13 +171,25 @@ class HaUserEditor extends LitElement { showSaveSuccessToast(this, this.hass!); fireEvent(this, "reload-users"); } catch (err) { - alert(`Group update failed: ${err.message}`); + alert( + `${this.hass!.localize( + "ui.panel.config.users.editor.group_update_failed" + )} ${err.message}` + ); selectEl.value = this.user!.group_ids[0]; } } private async _deleteUser(ev): Promise { - if (!confirm(`Are you sure you want to delete ${this._name}`)) { + if ( + !confirm( + this.hass!.localize( + "ui.panel.config.users.editor.confirm_user_deletion", + "name", + this._name + ) + ) + ) { ev.target.blur(); return; } diff --git a/src/translations/en.json b/src/translations/en.json index 8af25be437..bd919711b7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -977,7 +977,8 @@ "caption": "Users", "description": "Manage users", "picker": { - "title": "Users" + "title": "Users", + "system_generated": "System generated" }, "editor": { "caption": "View user", @@ -985,7 +986,18 @@ "change_password": "Change password", "activate_user": "Activate user", "deactivate_user": "Deactivate user", - "delete_user": "Delete user" + "delete_user": "Delete user", + "id": "ID", + "owner": "Owner", + "group": "Group", + "active": "Active", + "system_generated": "System generated", + "system_generated_users_not_removable": "Unable to remove system generated users.", + "unnamed_user": "Unnamed User", + "enter_new_name": "Enter new name", + "user_rename_failed": "User rename failed:", + "group_update_failed": "Group update failed:", + "confirm_user_deletion": "Are you sure you want to delete {name}?" }, "add_user": { "caption": "Add user", From 184575fd5497bc397d44942d05e588f58c859faa Mon Sep 17 00:00:00 2001 From: Sven <1040402+svendroid@users.noreply.github.com> Date: Thu, 3 Oct 2019 14:38:34 +0200 Subject: [PATCH 03/52] Add localization to persons config page (#3846) (#3871) --- .../config/person/dialog-person-detail.ts | 24 +++++++++++---- src/panels/config/person/ha-config-person.ts | 30 ++++++++++++------- src/translations/en.json | 15 +++++++++- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 6a191e72a0..54d3af044b 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -55,7 +55,11 @@ class DialogPersonDetail extends LitElement { opened @opened-changed="${this._openedChanged}" > -

${this._params.entry ? this._params.entry.name : "New Person"}

+

+ ${this._params.entry + ? this._params.entry.name + : this.hass!.localize("ui.panel.config.person.detail.new_person")} +

${this._error ? html` @@ -66,12 +70,18 @@ class DialogPersonDetail extends LitElement { - DELETE + ${this.hass!.localize("ui.panel.config.person.detail.delete")} ` : html``} @@ -112,7 +122,9 @@ class DialogPersonDetail extends LitElement { @click="${this._updateEntry}" .disabled=${nameInvalid || this._submitting} > - ${this._params.entry ? "UPDATE" : "CREATE"} + ${this._params.entry + ? this.hass!.localize("ui.panel.config.person.detail.update") + : this.hass!.localize("ui.panel.config.person.detail.create")}
diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 94fe6c427c..46958c4fd6 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -55,17 +55,21 @@ class HaConfigPerson extends LitElement { `; } + const hass = this.hass; return html` - + - Persons + ${hass.localize("ui.panel.config.person.caption")} - Here you can define each person of interest in Home Assistant. + ${hass.localize("ui.panel.config.person.introduction")} ${this._configItems.length > 0 ? html`

- Note: persons configured via configuration.yaml cannot be - edited via the UI. + ${hass.localize( + "ui.panel.config.person.note_about_persons_configured_in_yaml" + )}

` : ""} @@ -83,9 +87,13 @@ class HaConfigPerson extends LitElement { ${this._storageItems.length === 0 ? html`
- Looks like you have not created any persons yet. + ${hass.localize( + "ui.panel.config.person.no_persons_created_yet" + )} - CREATE PERSON
` @@ -112,7 +120,7 @@ class HaConfigPerson extends LitElement { `; @@ -180,9 +188,11 @@ class HaConfigPerson extends LitElement { }, removeEntry: async () => { if ( - !confirm(`Are you sure you want to delete this person? + !confirm(`${this.hass!.localize( + "ui.panel.config.person.confirm_delete" + )} -All devices belonging to this person will become unassigned.`) +${this.hass!.localize("ui.panel.config.person.confirm_delete2")}`) ) { return false; } diff --git a/src/translations/en.json b/src/translations/en.json index bd919711b7..0e879b76d2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -939,11 +939,24 @@ "person": { "caption": "Persons", "description": "Manage the persons that Home Assistant tracks.", + "introduction": "Here you can define each person of interest in Home Assistant.", + "note_about_persons_configured_in_yaml": "Note: persons configured via configuration.yaml cannot be edited via the UI.", + "no_persons_created_yet": "Looks like you have not created any persons yet.", + "create_person": "Create Person", + "add_person": "Add Person", + "confirm_delete": "Are you sure you want to delete this person?", + "confirm_delete2": "All devices belonging to this person will become unassigned.", "detail": { + "new_person": "New Person", "name": "Name", + "name_error_msg": "Name is required", + "linked_user": "Linked User", "device_tracker_intro": "Select the devices that belong to this person.", "device_tracker_picked": "Track Device", - "device_tracker_pick": "Pick device to track" + "device_tracker_pick": "Pick device to track", + "delete": "Delete", + "create": "Create", + "update": "Update" } }, "integrations": { From 56bac8a8c1c5df4983aa34d2164b9b7a1aabc7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Br=C3=BCckner?= Date: Thu, 3 Oct 2019 20:31:53 +0200 Subject: [PATCH 04/52] Make script editor translatable (#3866) * Make script editor's script picker translatable (home-assistant/home-assistant-polymer#3848) * Make script editor translatable (home-assistant/home-assistant-polymer#3848) * Fix linting errors (home-assistant/home-assistant-polymer#3866) * Fix linting errors (home-assistant/home-assistant-polymer#3866) * Move unsaved_confirm translation key to common section (home-assistant/home-assistant-polymer#3866) Instead of adding the same text multiple times for every section, add a common section to indicate reusable translations. * Add variable to localization text * Use JavaScript instead of Polymer data binding --- src/panels/config/script/ha-script-editor.js | 23 +++++++--- src/panels/config/script/ha-script-picker.ts | 44 ++++++++++++++------ src/translations/en.json | 25 +++++++++-- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.js b/src/panels/config/script/ha-script-editor.js index 8de26171b0..fc55899406 100644 --- a/src/panels/config/script/ha-script-editor.js +++ b/src/panels/config/script/ha-script-editor.js @@ -100,7 +100,10 @@ class HaScriptEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) { -
Script [[computeName(script)]]
+
+ [[localize('ui.panel.config.script.caption'), 'name', + computeName(script)]] +
@@ -126,7 +127,9 @@ class HaForm extends EventsMixin(PolymerElement) { required="[[schema.required]]" auto-validate="[[schema.required]]" error-message="Required" - > + > + [[computeSuffix(schema)]] +
- - `; } @@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { device: Object, devices: Array, areas: Array, - entities: Array, hass: Object, narrow: { type: Boolean, reflectToAttribute: true, }, - hideSettings: { type: Boolean, value: false }, - hideEntities: { type: Boolean, value: false }, _childDevices: { type: Array, computed: "_computeChildDevices(device, devices)", @@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { loadDeviceRegistryDetailDialog(); } - connectedCallback() { - super.connectedCallback(); - this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }); - this._unsubDevices = subscribeDeviceRegistry( - this.hass.connection, - (devices) => { - this.devices = devices; - this.device = devices.find((device) => device.id === this.device.id); - } - ); - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubAreas) { - this._unsubAreas(); - } - if (this._unsubDevices) { - this._unsubDevices(); - } - } - _computeArea(areas, device) { if (!areas || !device || !device.area_id) { return "No Area"; @@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { .sort((dev1, dev2) => compare(dev1.name, dev2.name)); } - _computeDeviceEntities(hass, device, entities) { - return entities - .filter((entity) => entity.device_id === device.id) - .sort((ent1, ent2) => - compare( - computeEntityName(hass, ent1) || `zzz${ent1.entity_id}`, - computeEntityName(hass, ent2) || `zzz${ent2.entity_id}` - ) - ); - } - - _computeStateObj(entity, hass) { - return hass.states[entity.entity_id]; - } - - _computeEntityName(entity, hass) { - return ( - computeEntityName(hass, entity) || - `(${this.localize( - "ui.panel.config.integrations.config_entry.entity_unavailable" - )})` - ); - } - _deviceName(device) { return device.name_by_user || device.name; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 07a53b6d8d..e238d511b5 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage"; import "../../../layouts/hass-error-screen"; import "../ha-config-section"; +import "./device-detail/ha-device-card"; import "./device-detail/ha-device-triggers-card"; import "./device-detail/ha-device-conditions-card"; import "./device-detail/ha-device-actions-card"; @@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement { .areas=${this.areas} .devices=${this.devices} .device=${device} - .entities=${this.entities} - hide-settings - hide-entities > ${entities.length diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index d448859b0f..c60bcabe91 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,19 +1,5 @@ -import "@polymer/paper-tooltip/paper-tooltip"; -import "@material/mwc-button"; -import "@polymer/iron-icon/iron-icon"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; - -import "../../../components/ha-card"; -import "../../../components/data-table/ha-data-table"; -import "../../../components/entity/ha-state-icon"; import "../../../layouts/hass-subpage"; -import "../../../resources/ha-style"; -import "../../../components/ha-icon-next"; - -import "../ha-config-section"; - -import memoizeOne from "memoize-one"; +import "./ha-devices-data-table"; import { LitElement, @@ -21,33 +7,14 @@ import { TemplateResult, property, customElement, + CSSResult, + css, } from "lit-element"; import { HomeAssistant } from "../../../types"; -// tslint:disable-next-line -import { - DataTabelColumnContainer, - RowClickedEvent, - DataTabelRowData, -} from "../../../components/data-table/ha-data-table"; -// tslint:disable-next-line import { DeviceRegistryEntry } from "../../../data/device_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; -import { navigate } from "../../../common/navigate"; -import { LocalizeFunc } from "../../../common/translations/localize"; -import { computeStateName } from "../../../common/entity/compute_state_name"; - -interface DeviceRowData extends DeviceRegistryEntry { - device?: DeviceRowData; - area?: string; - integration?: string; - battery_entity?: string; -} - -interface DeviceEntityLookup { - [deviceId: string]: EntityRegistryEntry[]; -} @customElement("ha-config-devices-dashboard") export class HaConfigDeviceDashboard extends LitElement { @@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement { @property() public areas!: AreaRegistryEntry[]; @property() public domain!: string; - private _devices = memoizeOne( - ( - devices: DeviceRegistryEntry[], - entries: ConfigEntry[], - entities: EntityRegistryEntry[], - areas: AreaRegistryEntry[], - domain: string, - localize: LocalizeFunc - ) => { - // Some older installations might have devices pointing at invalid entryIDs - // So we guard for that. - - let outputDevices: DeviceRowData[] = devices; - - const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; - for (const device of devices) { - deviceLookup[device.id] = device; - } - - const deviceEntityLookup: DeviceEntityLookup = {}; - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - - const entryLookup: { [entryId: string]: ConfigEntry } = {}; - for (const entry of entries) { - entryLookup[entry.entry_id] = entry; - } - - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - - if (domain) { - outputDevices = outputDevices.filter((device) => - device.config_entries.find( - (entryId) => - entryId in entryLookup && entryLookup[entryId].domain === domain - ) - ); - } - - outputDevices = outputDevices.map((device) => { - return { - ...device, - name: - device.name_by_user || - device.name || - this._fallbackDeviceName(device.id, deviceEntityLookup) || - "No name", - model: device.model || "", - manufacturer: device.manufacturer || "", - area: device.area_id ? areaLookup[device.area_id].name : "No area", - integration: device.config_entries.length - ? device.config_entries - .filter((entId) => entId in entryLookup) - .map( - (entId) => - localize( - `component.${entryLookup[entId].domain}.config.title` - ) || entryLookup[entId].domain - ) - .join(", ") - : "No integration", - battery_entity: this._batteryEntity(device.id, deviceEntityLookup), - }; - }); - - return outputDevices; - } - ); - - private _columns = memoizeOne( - (narrow: boolean): DataTabelColumnContainer => - narrow - ? { - device: { - title: "Device", - sortable: true, - filterKey: "name", - filterable: true, - direction: "asc", - template: (device: DeviceRowData) => { - const battery = device.battery_entity - ? this.hass.states[device.battery_entity] - : undefined; - // Have to work on a nice layout for mobile - return html` - ${device.name_by_user || device.name}
- ${device.area} | ${device.integration}
- ${battery - ? html` - ${battery.state}% - - ` - : ""} - `; - }, - }, - } - : { - device_name: { - title: "Device", - sortable: true, - filterable: true, - direction: "asc", - }, - manufacturer: { - title: "Manufacturer", - sortable: true, - filterable: true, - }, - model: { - title: "Model", - sortable: true, - filterable: true, - }, - area: { - title: "Area", - sortable: true, - filterable: true, - }, - integration: { - title: "Integration", - sortable: true, - filterable: true, - }, - battery: { - title: "Battery", - sortable: true, - type: "numeric", - template: (batteryEntity: string) => { - const battery = batteryEntity - ? this.hass.states[batteryEntity] - : undefined; - return battery - ? html` - ${battery.state}% - - ` - : html` - - - `; - }, - }, - } - ); - protected render(): TemplateResult { return html` - { - // We don't need a lot of this data for mobile view, but kept it for filtering... - const data: DataTabelRowData = { - device_name: device.name, - id: device.id, - manufacturer: device.manufacturer, - model: device.model, - area: device.area, - integration: device.integration, - }; - if (this.narrow) { - data.device = device; - return data; - } - data.battery = device.battery_entity; - return data; - })} - @row-click=${this._handleRowClicked} - > +
+ +
`; } - private _batteryEntity( - 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" - ); - - return batteryEntity ? batteryEntity.entity_id : undefined; - } - - private _fallbackDeviceName( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - for (const entity of deviceEntityLookup[deviceId] || []) { - const stateObj = this.hass.states[entity.entity_id]; - if (stateObj) { - return computeStateName(stateObj); + static get styles(): CSSResult { + return css` + .content { + padding: 4px; } - } - - return undefined; - } - - private _handleRowClicked(ev: CustomEvent) { - const deviceId = (ev.detail as RowClickedEvent).id; - navigate(this, `/config/devices/device/${deviceId}`); + ha-devices-data-table { + width: 100%; + } + `; } } diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts new file mode 100644 index 0000000000..d21e31aaa8 --- /dev/null +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -0,0 +1,265 @@ +import "../../../components/data-table/ha-data-table"; +import "../../../components/entity/ha-state-icon"; + +import memoizeOne from "memoize-one"; + +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +// tslint:disable-next-line +import { + DataTabelColumnContainer, + RowClickedEvent, + DataTabelRowData, +} from "../../../components/data-table/ha-data-table"; +// tslint:disable-next-line +import { DeviceRegistryEntry } from "../../../data/device_registry"; +import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { ConfigEntry } from "../../../data/config_entries"; +import { AreaRegistryEntry } from "../../../data/area_registry"; +import { navigate } from "../../../common/navigate"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { computeStateName } from "../../../common/entity/compute_state_name"; + +export interface DeviceRowData extends DeviceRegistryEntry { + device?: DeviceRowData; + area?: string; + integration?: string; + battery_entity?: string; +} + +export interface DeviceEntityLookup { + [deviceId: string]: EntityRegistryEntry[]; +} + +@customElement("ha-devices-data-table") +export class HaDevicesDataTable extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public devices!: DeviceRegistryEntry[]; + @property() public entries!: ConfigEntry[]; + @property() public entities!: EntityRegistryEntry[]; + @property() public areas!: AreaRegistryEntry[]; + @property() public domain!: string; + + private _devices = memoizeOne( + ( + devices: DeviceRegistryEntry[], + entries: ConfigEntry[], + entities: EntityRegistryEntry[], + areas: AreaRegistryEntry[], + domain: string, + localize: LocalizeFunc + ) => { + // Some older installations might have devices pointing at invalid entryIDs + // So we guard for that. + + let outputDevices: DeviceRowData[] = devices; + + const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; + for (const device of devices) { + deviceLookup[device.id] = device; + } + + const deviceEntityLookup: DeviceEntityLookup = {}; + for (const entity of entities) { + if (!entity.device_id) { + continue; + } + if (!(entity.device_id in deviceEntityLookup)) { + deviceEntityLookup[entity.device_id] = []; + } + deviceEntityLookup[entity.device_id].push(entity); + } + + const entryLookup: { [entryId: string]: ConfigEntry } = {}; + for (const entry of entries) { + entryLookup[entry.entry_id] = entry; + } + + const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; + for (const area of areas) { + areaLookup[area.area_id] = area; + } + + if (domain) { + outputDevices = outputDevices.filter((device) => + device.config_entries.find( + (entryId) => + entryId in entryLookup && entryLookup[entryId].domain === domain + ) + ); + } + + outputDevices = outputDevices.map((device) => { + return { + ...device, + name: + device.name_by_user || + device.name || + this._fallbackDeviceName(device.id, deviceEntityLookup) || + "No name", + model: device.model || "", + manufacturer: device.manufacturer || "", + area: device.area_id ? areaLookup[device.area_id].name : "No area", + integration: device.config_entries.length + ? device.config_entries + .filter((entId) => entId in entryLookup) + .map( + (entId) => + localize( + `component.${entryLookup[entId].domain}.config.title` + ) || entryLookup[entId].domain + ) + .join(", ") + : "No integration", + battery_entity: this._batteryEntity(device.id, deviceEntityLookup), + }; + }); + + return outputDevices; + } + ); + + private _columns = memoizeOne( + (narrow: boolean): DataTabelColumnContainer => + narrow + ? { + name: { + title: "Device", + sortable: true, + filterKey: "name", + filterable: true, + direction: "asc", + template: (name, device: DataTabelRowData) => { + const battery = device.battery_entity + ? this.hass.states[device.battery_entity] + : undefined; + // Have to work on a nice layout for mobile + return html` + ${name}
+ ${device.area} | ${device.integration}
+ ${battery + ? html` + ${battery.state}% + + ` + : ""} + `; + }, + }, + } + : { + name: { + title: "Device", + sortable: true, + filterable: true, + direction: "asc", + }, + manufacturer: { + title: "Manufacturer", + sortable: true, + filterable: true, + }, + model: { + title: "Model", + sortable: true, + filterable: true, + }, + area: { + title: "Area", + sortable: true, + filterable: true, + }, + integration: { + title: "Integration", + sortable: true, + filterable: true, + }, + battery_entity: { + title: "Battery", + sortable: true, + type: "numeric", + template: (batteryEntity: string) => { + const battery = batteryEntity + ? this.hass.states[batteryEntity] + : undefined; + return battery + ? html` + ${battery.state}% + + ` + : html` + - + `; + }, + }, + } + ); + + protected render(): TemplateResult { + return html` + + `; + } + + private _batteryEntity( + 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" + ); + + return batteryEntity ? batteryEntity.entity_id : undefined; + } + + private _fallbackDeviceName( + deviceId: string, + deviceEntityLookup: DeviceEntityLookup + ): string | undefined { + for (const entity of deviceEntityLookup[deviceId] || []) { + const stateObj = this.hass.states[entity.entity_id]; + if (stateObj) { + return computeStateName(stateObj); + } + } + + return undefined; + } + + private _handleRowClicked(ev: CustomEvent) { + const deviceId = (ev.detail as RowClickedEvent).id; + navigate(this, `/config/devices/device/${deviceId}`); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-devices-data-table": HaDevicesDataTable; + } +} diff --git a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js index 10d9e7f2bc..63cd18774d 100644 --- a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js +++ b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js @@ -20,7 +20,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) { return html` `; } - set hass(hass: HomeAssistant) { - this._hass = hass; - if (this._hass) { - this.setScrollBarDirection(); - } - } - set value(value: string) { if (this.codemirror) { if (value !== this.codemirror.getValue()) { @@ -83,6 +76,22 @@ export class HuiYamlEditor extends HTMLElement { return this.codemirror.getValue(); } + set rtl(rtl: boolean) { + this._rtl = rtl; + this.setScrollBarDirection(); + } + + set autofocus(autofocus: boolean) { + this._autofocus = autofocus; + if (this.codemirror) { + this.codemirror.focus(); + } + } + + set error(error: boolean) { + this.classList.toggle("error-state", error); + } + get hasComments(): boolean { return this.shadowRoot!.querySelector("span.cm-comment") ? true : false; } @@ -96,16 +105,13 @@ export class HuiYamlEditor extends HTMLElement { lineNumbers: true, mode: "yaml", tabSize: 2, - autofocus: true, + autofocus: this._autofocus, viewportMargin: Infinity, extraKeys: { Tab: "indentMore", "Shift-Tab": "indentLess", }, - gutters: - this._hass && computeRTL(this._hass!) - ? ["rtl-gutter", "CodeMirror-linenumbers"] - : [], + gutters: this._rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [], } ); this.setScrollBarDirection(); @@ -120,18 +126,14 @@ export class HuiYamlEditor extends HTMLElement { } private setScrollBarDirection(): void { - if (!this.codemirror) { - return; + if (this.codemirror) { + this.codemirror.getWrapperElement().classList.toggle("rtl", this._rtl); } - - this.codemirror - .getWrapperElement() - .classList.toggle("rtl", computeRTL(this._hass!)); } } declare global { interface HTMLElementTagNameMap { - "hui-yaml-editor": HuiYamlEditor; + "ha-yaml-editor": HaYamlEditor; } } diff --git a/src/panels/config/js/preact-types.ts b/src/panels/config/js/preact-types.ts index 374fbebb04..f103f63230 100644 --- a/src/panels/config/js/preact-types.ts +++ b/src/panels/config/js/preact-types.ts @@ -20,6 +20,7 @@ declare global { "ha-device-picker": any; "ha-device-condition-picker": any; "ha-textarea": any; + "ha-yaml-editor": any; "ha-service-picker": any; "mwc-button": any; "ha-device-trigger-picker": any; diff --git a/src/panels/config/js/yaml_textarea.tsx b/src/panels/config/js/yaml_textarea.tsx index 6e3ca7b04c..f9e8edf634 100644 --- a/src/panels/config/js/yaml_textarea.tsx +++ b/src/panels/config/js/yaml_textarea.tsx @@ -1,6 +1,8 @@ import { h, Component } from "preact"; import yaml from "js-yaml"; -import "../../../components/ha-textarea"; +import "../../../components/ha-yaml-editor"; +// tslint:disable-next-line +import { HaYamlEditor } from "../../../components/ha-yaml-editor"; const isEmpty = (obj: object) => { for (const key in obj) { @@ -12,6 +14,8 @@ const isEmpty = (obj: object) => { }; export default class YAMLTextArea extends Component { + private _yamlEditor!: HaYamlEditor; + constructor(props) { super(props); @@ -34,7 +38,7 @@ export default class YAMLTextArea extends Component { } public onChange(ev) { - const value = ev.target.value; + const value = ev.detail.value; let parsed; let isValid = true; @@ -59,22 +63,30 @@ export default class YAMLTextArea extends Component { } } + public componentDidMount() { + setTimeout(() => { + this._yamlEditor.codemirror.refresh(); + }, 1); + } + public render({ label }, { value, isValid }) { const style: any = { minWidth: 300, width: "100%", }; - if (!isValid) { - style.border = "1px solid red"; - } return ( - +
+

{label}

+ +
); } + + private _storeYamlEditorRef = (yamlEditor) => (this._yamlEditor = yamlEditor); } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts index b2eb3a6ec4..792cd9c118 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts @@ -15,11 +15,12 @@ import { HomeAssistant } from "../../../../types"; import { LovelaceCardConfig } from "../../../../data/lovelace"; import { LovelaceCardEditor } from "../../types"; import { getCardElementTag } from "../../common/get-card-element-tag"; +import { computeRTL } from "../../../../common/util/compute_rtl"; -import "../../components/hui-yaml-editor"; +import "../../../../components/ha-yaml-editor"; // This is not a duplicate import, one is for types, one is for element. // tslint:disable-next-line -import { HuiYamlEditor } from "../../components/hui-yaml-editor"; +import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { fireEvent } from "../../../../common/dom/fire_event"; import { EntityConfig } from "../../entity-rows/types"; @@ -43,7 +44,7 @@ export interface UIConfigChangedEvent extends Event { @customElement("hui-card-editor") export class HuiCardEditor extends LitElement { - @property() public hass?: HomeAssistant; + @property() public hass!: HomeAssistant; @property() private _yaml?: string; @property() private _config?: LovelaceCardConfig; @@ -93,8 +94,8 @@ export class HuiCardEditor extends LitElement { return this._error !== undefined; } - private get _yamlEditor(): HuiYamlEditor { - return this.shadowRoot!.querySelector("hui-yaml-editor")!; + private get _yamlEditor(): HaYamlEditor { + return this.shadowRoot!.querySelector("ha-yaml-editor")!; } public toggleMode() { @@ -120,11 +121,12 @@ export class HuiCardEditor extends LitElement { ` : html`
- + >
`} ${this._error diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index d4570dcebe..ba225a95f5 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -14,11 +14,12 @@ import { Lovelace } from "./types"; import "../../components/ha-icon"; import { haStyle } from "../../resources/styles"; -import "./components/hui-yaml-editor"; +import "../../components/ha-yaml-editor"; // This is not a duplicate import, one is for types, one is for element. // tslint:disable-next-line -import { HuiYamlEditor } from "./components/hui-yaml-editor"; +import { HaYamlEditor } from "../../components/ha-yaml-editor"; import { HomeAssistant } from "../../types"; +import { computeRTL } from "../../common/util/compute_rtl"; const lovelaceStruct = struct.interface({ title: "string?", @@ -27,7 +28,7 @@ const lovelaceStruct = struct.interface({ }); class LovelaceFullConfigEditor extends LitElement { - public hass?: HomeAssistant; + public hass!: HomeAssistant; public lovelace?: Lovelace; public closeEditor?: () => void; private _saving?: boolean; @@ -80,12 +81,14 @@ class LovelaceFullConfigEditor extends LitElement {
- - +
`; @@ -205,8 +208,8 @@ class LovelaceFullConfigEditor extends LitElement { this._changed = false; } - private get yamlEditor(): HuiYamlEditor { - return this.shadowRoot!.querySelector("hui-yaml-editor")!; + private get yamlEditor(): HaYamlEditor { + return this.shadowRoot!.querySelector("ha-yaml-editor")!; } } diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index 3e57ed0b8d..577a79d5da 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -33,6 +33,8 @@ documentContainer.innerHTML = ` --scrollbar-thumb-color: rgb(194, 194, 194); + --error-state-color: #db4437; + /* states and badges */ --state-icon-color: #44739e; --state-icon-active-color: #FDD835; From 495f4aa19c0fe365c55e49c76f5a3eee836900ab Mon Sep 17 00:00:00 2001 From: Ian Richardson Date: Fri, 11 Oct 2019 06:14:34 -0500 Subject: [PATCH 38/52] ability to hide tabs (#3811) * ability to hide tabs * address review comments * address review comments * address review comments * review comments --- src/data/lovelace.ts | 5 +++++ src/panels/lovelace/hui-root.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 103b1d147f..5f32b70b4b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -18,6 +18,11 @@ export interface LovelaceViewConfig { theme?: string; panel?: boolean; background?: string; + visible?: boolean | ShowViewConfig[]; +} + +export interface ShowViewConfig { + user?: string; } export interface LovelaceCardConfig { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index e01f7ddfe1..1f099a1364 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -236,7 +236,20 @@ class HUIRoot extends LitElement { > ${this.lovelace!.config.views.map( (view) => html` - + e.user === this.hass!.user!.id + )) || + view.visible === false) + ), + })}" + > ${this._editMode ? html` Date: Fri, 11 Oct 2019 13:16:54 +0200 Subject: [PATCH 39/52] Allow device conditions to specify extra fields (#3973) --- src/data/device_automation.ts | 9 +++ src/panels/config/js/condition/device.tsx | 85 +++++++++++++++++++++-- src/translations/en.json | 7 +- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index 093de90b2f..0a308fcdbc 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -39,6 +39,15 @@ export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => device_id: deviceId, }); +export const fetchDeviceConditionCapabilities = ( + hass: HomeAssistant, + condition: DeviceCondition +) => + hass.callWS({ + type: "device_automation/condition/capabilities", + condition, + }); + export const fetchDeviceTriggerCapabilities = ( hass: HomeAssistant, trigger: DeviceTrigger diff --git a/src/panels/config/js/condition/device.tsx b/src/panels/config/js/condition/device.tsx index 30962fec46..6c3206c4b6 100644 --- a/src/panels/config/js/condition/device.tsx +++ b/src/panels/config/js/condition/device.tsx @@ -2,29 +2,49 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-condition-picker"; +import "../../../../components/ha-form"; +import { + fetchDeviceConditionCapabilities, + deviceAutomationsEqual, +} from "../../../../data/device_automation"; export default class DeviceCondition extends Component { + private _origCondition; + constructor() { super(); this.devicePicked = this.devicePicked.bind(this); this.deviceConditionPicked = this.deviceConditionPicked.bind(this); - this.state = { device_id: undefined }; + this._extraFieldsChanged = this._extraFieldsChanged.bind(this); + this.state = { device_id: undefined, capabilities: undefined }; } public devicePicked(ev) { - this.setState({ device_id: ev.target.value }); + this.setState({ ...this.state, device_id: ev.target.value }); } public deviceConditionPicked(ev) { - const deviceCondition = ev.target.value; - this.props.onChange(this.props.index, deviceCondition); + let condition = ev.target.value; + if ( + this._origCondition && + deviceAutomationsEqual(this._origCondition, condition) + ) { + condition = this._origCondition; + } + this.props.onChange(this.props.index, condition); } /* eslint-disable camelcase */ - public render({ condition, hass }, { device_id }) { + public render({ condition, hass }, { device_id, capabilities }) { if (device_id === undefined) { device_id = condition.device_id; } + const extraFieldsData = + capabilities && capabilities.extra_fields + ? capabilities.extra_fields.map((item) => { + return { [item.name]: this.props.condition[item.name] }; + }) + : undefined; return (
@@ -41,9 +61,64 @@ export default class DeviceCondition extends Component { hass={hass} label="Condition" /> + {extraFieldsData && ( + + )}
); } + + public componentDidMount() { + if (!this.state.capabilities) { + this._getCapabilities(); + } + if (this.props.condition) { + this._origCondition = this.props.condition; + } + } + + public componentDidUpdate(prevProps) { + if (prevProps.condition !== this.props.condition) { + this._getCapabilities(); + } + } + + private async _getCapabilities() { + const condition = this.props.condition; + + const capabilities = condition.domain + ? await fetchDeviceConditionCapabilities(this.props.hass, condition) + : null; + this.setState({ ...this.state, capabilities }); + } + + private _extraFieldsChanged(ev) { + if (!ev.detail.path) { + return; + } + const item = ev.detail.path.replace("data.", ""); + const value = ev.detail.value || undefined; + + this.props.onChange(this.props.index, { + ...this.props.condition, + [item]: value, + }); + } + + private _extraFieldsComputeLabelCallback(localize) { + // Returns a callback for ha-form to calculate labels per schema object + return (schema) => + localize( + `ui.panel.config.automation.editor.condition.type.device.extra_fields.${ + schema.name + }` + ) || schema.name; + } } (DeviceCondition as any).defaultConfig = { diff --git a/src/translations/en.json b/src/translations/en.json index 83deb0d04b..a707ab7015 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -830,7 +830,12 @@ "label": "And" }, "device": { - "label": "Device" + "label": "Device", + "extra_fields": { + "above": "Above", + "below": "Below", + "for": "Duration" + } }, "numeric_state": { "label": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]", From 6f7ea03e350ba94c673ba6dcd889f675c6d20a86 Mon Sep 17 00:00:00 2001 From: Hayley McIldoon Date: Fri, 11 Oct 2019 08:55:12 -0400 Subject: [PATCH 40/52] Remove redundant text in Cloud panel #3824 (#3967) --- src/panels/config/cloud/account/cloud-alexa-pref.ts | 5 ----- src/panels/config/cloud/account/cloud-google-pref.ts | 5 ----- src/translations/en.json | 2 -- 3 files changed, 12 deletions(-) diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index 59f102f0db..0f0b227871 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -66,11 +66,6 @@ export class CloudAlexaPref extends LitElement { - ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.requirements" - )} ${alexa_enabled ? html`
diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index fdb446197f..16cb09761a 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -79,11 +79,6 @@ export class CloudGooglePref extends LitElement { - ${this.hass!.localize( - "ui.panel.config.cloud.account.google.requirements" - )} ${google_enabled ? html`

Enable State Reporting

diff --git a/src/translations/en.json b/src/translations/en.json index a707ab7015..857ba13728 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1018,7 +1018,6 @@ "info": "With the Alexa integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Alexa-enabled device.", "enable_ha_skill": "Enable the Home Assistant skill for Alexa", "config_documentation": "Config documentation", - "requirements": "This integration requires an Alexa-enabled device like the Amazon Echo.", "enable_state_reporting": "Enable State Reporting", "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.", "sync_entities": "Sync Entities", @@ -1033,7 +1032,6 @@ "info": "With the Google Assistant integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Google Assistant-enabled device.", "enable_ha_skill": "Activate the Home Assistant skill for Google Assistant", "config_documentation": "Config documentation", - "requirements": "This integration requires a Google Assistant-enabled device like the Google Home or Android phone.", "enter_pin_info": "Please enter a pin to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this pin when interacting with such devices via Google Assistant.", "devices_pin": "Secure Devices Pin", "enter_pin_hint": "Enter a PIN to use secure devices", From 90526ac56325593e9556cbe90924b82a94da047a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 11 Oct 2019 14:55:45 +0200 Subject: [PATCH 41/52] Migrate entity registry to data-table (#3965) * Migrate entity registry to data-table * icons * Styling * Review comments * fix not selector * typos + move columns out of class * Localize + comments * Fucked up the rebase --- src/components/data-table/ha-data-table.ts | 51 +++-- .../data-table/sort_filter_worker.ts | 26 +-- .../config/devices/ha-devices-data-table.ts | 8 +- .../ha-config-entity-registry.ts | 175 ++++++++++-------- .../unused-entities/hui-unused-entities.ts | 4 +- src/translations/en.json | 8 +- 6 files changed, 158 insertions(+), 114 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index afa109088e..d009bd8c77 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -59,31 +59,31 @@ export interface SortingChangedEvent { export type SortingDirection = "desc" | "asc" | null; -export interface DataTabelColumnContainer { - [key: string]: DataTabelColumnData; +export interface DataTableColumnContainer { + [key: string]: DataTableColumnData; } -export interface DataTabelSortColumnData { +export interface DataTableSortColumnData { sortable?: boolean; filterable?: boolean; filterKey?: string; direction?: SortingDirection; } -export interface DataTabelColumnData extends DataTabelSortColumnData { +export interface DataTableColumnData extends DataTableSortColumnData { title: string; - type?: "numeric"; - template?: (data: any, row: DataTabelRowData) => TemplateResult; + type?: "numeric" | "icon"; + template?: (data: any, row: T) => TemplateResult; } -export interface DataTabelRowData { +export interface DataTableRowData { [key: string]: any; } @customElement("ha-data-table") export class HaDataTable extends BaseElement { - @property({ type: Object }) public columns: DataTabelColumnContainer = {}; - @property({ type: Array }) public data: DataTabelRowData[] = []; + @property({ type: Object }) public columns: DataTableColumnContainer = {}; + @property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Boolean }) public selectable = false; @property({ type: String }) public id = "id"; protected mdcFoundation!: MDCDataTableFoundation; @@ -98,9 +98,9 @@ export class HaDataTable extends BaseElement { @property({ type: String }) private _filter = ""; @property({ type: String }) private _sortColumn?: string; @property({ type: String }) private _sortDirection: SortingDirection = null; - @property({ type: Array }) private _filteredData: DataTabelRowData[] = []; + @property({ type: Array }) private _filteredData: DataTableRowData[] = []; private _sortColumns: { - [key: string]: DataTabelSortColumnData; + [key: string]: DataTableSortColumnData; } = {}; private curRequest = 0; private _worker: any | undefined; @@ -134,8 +134,8 @@ export class HaDataTable extends BaseElement { } } - const clonedColumns: DataTabelColumnContainer = deepClone(this.columns); - Object.values(clonedColumns).forEach((column: DataTabelColumnData) => { + const clonedColumns: DataTableColumnContainer = deepClone(this.columns); + Object.values(clonedColumns).forEach((column: DataTableColumnData) => { delete column.title; delete column.type; delete column.template; @@ -190,9 +190,12 @@ export class HaDataTable extends BaseElement { const [key, column] = columnEntry; const sorted = key === this._sortColumn; const classes = { - "mdc-data-table__cell--numeric": Boolean( + "mdc-data-table__header-cell--numeric": Boolean( column.type && column.type === "numeric" ), + "mdc-data-table__header-cell--icon": Boolean( + column.type && column.type === "icon" + ), sortable: Boolean(column.sortable), "not-sorted": Boolean(column.sortable && !sorted), }; @@ -222,8 +225,8 @@ export class HaDataTable extends BaseElement { ${repeat( this._filteredData!, - (row: DataTabelRowData) => row[this.id], - (row: DataTabelRowData) => html` + (row: DataTableRowData) => row[this.id], + (row: DataTableRowData) => html` ${column.template @@ -516,6 +522,11 @@ export class HaDataTable extends BaseElement { text-align: left; } + .mdc-data-table__cell--icon { + color: var(--secondary-text-color); + text-align: center; + } + .mdc-data-table__header-cell { font-family: Roboto, sans-serif; -moz-osx-font-smoothing: grayscale; @@ -543,6 +554,10 @@ export class HaDataTable extends BaseElement { text-align: left; } + .mdc-data-table__header-cell--icon { + text-align: center; + } + /* custom from here */ .mdc-data-table { @@ -554,7 +569,7 @@ export class HaDataTable extends BaseElement { .mdc-data-table__header-cell.sortable { cursor: pointer; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric) + .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon) span { position: relative; left: -24px; @@ -565,7 +580,7 @@ export class HaDataTable extends BaseElement { .mdc-data-table__header-cell.not-sorted ha-icon { left: -36px; } - .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric):hover + .mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover span { left: 0px; } diff --git a/src/components/data-table/sort_filter_worker.ts b/src/components/data-table/sort_filter_worker.ts index 47e36007b9..bd4eb831d3 100644 --- a/src/components/data-table/sort_filter_worker.ts +++ b/src/components/data-table/sort_filter_worker.ts @@ -1,7 +1,7 @@ import { - DataTabelColumnContainer, - DataTabelColumnData, - DataTabelRowData, + DataTableColumnContainer, + DataTableColumnData, + DataTableRowData, SortingDirection, } from "./ha-data-table"; @@ -9,8 +9,8 @@ import memoizeOne from "memoize-one"; export const filterSortData = memoizeOne( async ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string, direction: SortingDirection, sortColumn?: string @@ -27,8 +27,8 @@ export const filterSortData = memoizeOne( const _memFilterData = memoizeOne( async ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string ) => { if (!filter) { @@ -40,8 +40,8 @@ const _memFilterData = memoizeOne( const _memSortData = memoizeOne( ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, direction: SortingDirection, sortColumn: string ) => { @@ -50,8 +50,8 @@ const _memSortData = memoizeOne( ); export const filterData = ( - data: DataTabelRowData[], - columns: DataTabelColumnContainer, + data: DataTableRowData[], + columns: DataTableColumnContainer, filter: string ) => data.filter((row) => { @@ -71,8 +71,8 @@ export const filterData = ( }); export const sortData = ( - data: DataTabelRowData[], - column: DataTabelColumnData, + data: DataTableRowData[], + column: DataTableColumnData, direction: SortingDirection, sortColumn: string ) => diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts index d21e31aaa8..32ba9d881c 100644 --- a/src/panels/config/devices/ha-devices-data-table.ts +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -13,9 +13,9 @@ import { import { HomeAssistant } from "../../../types"; // tslint:disable-next-line import { - DataTabelColumnContainer, + DataTableColumnContainer, RowClickedEvent, - DataTabelRowData, + DataTableRowData, } from "../../../components/data-table/ha-data-table"; // tslint:disable-next-line import { DeviceRegistryEntry } from "../../../data/device_registry"; @@ -127,7 +127,7 @@ export class HaDevicesDataTable extends LitElement { ); private _columns = memoizeOne( - (narrow: boolean): DataTabelColumnContainer => + (narrow: boolean): DataTableColumnContainer => narrow ? { name: { @@ -136,7 +136,7 @@ export class HaDevicesDataTable extends LitElement { filterKey: "name", filterable: true, direction: "asc", - template: (name, device: DataTabelRowData) => { + template: (name, device: DataTableRowData) => { const battery = device.battery_entity ? this.hass.states[device.battery_entity] : undefined; diff --git a/src/panels/config/entity_registry/ha-config-entity-registry.ts b/src/panels/config/entity_registry/ha-config-entity-registry.ts index e720e6e319..b0424d4511 100644 --- a/src/panels/config/entity_registry/ha-config-entity-registry.ts +++ b/src/panels/config/entity_registry/ha-config-entity-registry.ts @@ -6,9 +6,6 @@ import { CSSResult, property, } from "lit-element"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; import { HomeAssistant } from "../../../types"; import { @@ -18,7 +15,7 @@ import { } from "../../../data/entity_registry"; import "../../../layouts/hass-subpage"; import "../../../layouts/hass-loading-screen"; -import "../../../components/ha-card"; +import "../../../components/data-table/ha-data-table"; import "../../../components/ha-icon"; import "../../../components/ha-switch"; import { domainIcon } from "../../../common/entity/domain_icon"; @@ -30,11 +27,14 @@ import { loadEntityRegistryDetailDialog, } from "./show-dialog-entity-registry-detail"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { compare } from "../../../common/string/compare"; -import { classMap } from "lit-html/directives/class-map"; // tslint:disable-next-line import { HaSwitch } from "../../../components/ha-switch"; import memoize from "memoize-one"; +// tslint:disable-next-line +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../components/data-table/ha-data-table"; class HaConfigEntityRegistry extends LitElement { @property() public hass!: HomeAssistant; @@ -43,11 +43,76 @@ class HaConfigEntityRegistry extends LitElement { @property() private _showDisabled = false; private _unsubEntities?: UnsubscribeFunc; + private _columns = memoize( + (_language): DataTableColumnContainer => { + return { + icon: { + title: "", + type: "icon", + template: (icon) => html` + + `, + }, + name: { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.name" + ), + sortable: true, + filterable: true, + direction: "asc", + }, + entity_id: { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.entity_id" + ), + sortable: true, + filterable: true, + }, + platform: { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.integration" + ), + sortable: true, + filterable: true, + template: (platform) => + html` + ${this.hass.localize(`component.${platform}.config.title`) || + platform} + `, + }, + disabled_by: { + title: this.hass.localize( + "ui.panel.config.entity_registry.picker.headers.enabled" + ), + type: "icon", + template: (disabledBy) => html` + + `, + }, + }; + } + ); + private _filteredEntities = memoize( (entities: EntityRegistryEntry[], showDisabled: boolean) => - showDisabled + (showDisabled ? entities : entities.filter((entity) => !Boolean(entity.disabled_by)) + ).map((entry) => { + const state = this.hass!.states[entry.entity_id]; + return { + ...entry, + icon: state + ? stateIcon(state) + : domainIcon(computeDomain(entry.entity_id)), + name: + computeEntityRegistryName(this.hass!, entry) || + this.hass!.localize("state.default.unavailable"), + }; + }) ); public disconnectedCallback() { @@ -89,56 +154,21 @@ class HaConfigEntityRegistry extends LitElement { "ui.panel.config.entity_registry.picker.integrations_page" )} - - - - ${this.hass.localize( - "ui.panel.config.entity_registry.picker.show_disabled" - )}${this.hass.localize( + "ui.panel.config.entity_registry.picker.show_disabled" + )} - ${this._filteredEntities(this._entities, this._showDisabled).map( - (entry) => { - const state = this.hass!.states[entry.entity_id]; - return html` - - - -
- ${computeEntityRegistryName(this.hass!, entry) || - `(${this.hass!.localize( - "state.default.unavailable" - )})`} -
-
- ${entry.entity_id} -
-
-
- ${entry.platform} - ${entry.disabled_by - ? html` -
(disabled) - ` - : ""} -
-
- `; - } - )} -
+ + + `; @@ -155,9 +185,7 @@ class HaConfigEntityRegistry extends LitElement { this._unsubEntities = subscribeEntityRegistry( this.hass.connection, (entities) => { - this._entities = entities.sort((ent1, ent2) => - compare(ent1.entity_id, ent2.entity_id) - ); + this._entities = entities; } ); } @@ -167,8 +195,14 @@ class HaConfigEntityRegistry extends LitElement { this._showDisabled = (ev.target as HaSwitch).checked; } - private _openEditEntry(ev: MouseEvent): void { - const entry = (ev.currentTarget! as any).entry; + private _openEditEntry(ev: CustomEvent): void { + const entryId = (ev.detail as RowClickedEvent).id; + const entry = this._entities!.find( + (entity) => entity.entity_id === entryId + ); + if (!entry) { + return; + } showEntityRegistryDetailDialog(this, { entry, }); @@ -179,23 +213,12 @@ class HaConfigEntityRegistry extends LitElement { a { color: var(--primary-color); } - ha-card { + ha-data-table { margin-bottom: 24px; - direction: ltr; + margin-top: 0px; } - paper-icon-item { - cursor: pointer; - color: var(--primary-text-color); - } - ha-icon { - margin-left: 8px; - } - .platform { - text-align: right; - margin: 0 0 0 8px; - } - .disabled-entry { - color: var(--secondary-text-color); + ha-switch { + margin-top: 16px; } `; } diff --git a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts index ebe06225dc..10e4b21293 100644 --- a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts +++ b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts @@ -22,7 +22,7 @@ import "../../../../components/data-table/ha-data-table"; // tslint:disable-next-line import { SelectionChangedEvent, - DataTabelColumnContainer, + DataTableColumnContainer, } from "../../../../components/data-table/ha-data-table"; import { computeStateName } from "../../../../common/entity/compute_state_name"; @@ -55,7 +55,7 @@ export class HuiUnusedEntities extends LitElement { } private _columns = memoizeOne((narrow: boolean) => { - const columns: DataTabelColumnContainer = { + const columns: DataTableColumnContainer = { entity: { title: "Entity", sortable: true, diff --git a/src/translations/en.json b/src/translations/en.json index 857ba13728..e0cc3bf6d1 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1110,7 +1110,13 @@ "introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.", "introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant. Note, removing the entity registry entry won't remove the entity. To do that, follow the link below and remove it from the integrations page.", "integrations_page": "Integrations page", - "show_disabled": "Show disabled entities" + "show_disabled": "Show disabled entities", + "headers": { + "name": "Name", + "entity_id": "Entity ID", + "integration": "Integration", + "enabled": "Enabled" + } }, "editor": { "unavailable": "This entity is not currently available.", From 4728c122255b62ca74b649848fa3f3f91d1b139b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 11 Oct 2019 17:48:56 +0200 Subject: [PATCH 42/52] Update google cloud card (#3978) * Update google cloud card * Oops --- .../config/cloud/account/cloud-google-pref.ts | 50 +++++++++++++++---- src/translations/en.json | 7 ++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index 16cb09761a..a0a3190428 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -81,18 +81,31 @@ export class CloudGooglePref extends LitElement { ${google_enabled ? html` -

Enable State Reporting

+
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.google.enable_state_reporting" + )} +

+
+ +
+

- If you enable state reporting, Home Assistant will send - all state changes of exposed entities to Google. This - allows you to always see the latest states in the Google app. + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.info_state_reporting" + )}

-
+

+ ${this.hass!.localize( + "ui.panel.config.cloud.account.google.security_devices" + )} +

${this.hass!.localize( "ui.panel.config.cloud.account.google.enter_pin_info" )} @@ -194,7 +207,7 @@ export class CloudGooglePref extends LitElement { font-weight: 500; } .secure_devices { - padding-top: 16px; + padding-top: 8px; } paper-input { width: 250px; @@ -208,6 +221,25 @@ export class CloudGooglePref extends LitElement { .spacer { flex-grow: 1; } + .state-reporting { + display: flex; + margin-top: 1.5em; + } + .state-reporting + p { + margin-top: 0.5em; + } + h3 { + margin: 0 0 8px 0; + } + .state-reporting h3 { + flex-grow: 1; + margin: 0; + } + .state-reporting-switch { + margin-top: 0.25em; + margin-right: 7px; + margin-left: 0.5em; + } `; } } diff --git a/src/translations/en.json b/src/translations/en.json index e0cc3bf6d1..bcd426090d 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1032,9 +1032,12 @@ "info": "With the Google Assistant integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Google Assistant-enabled device.", "enable_ha_skill": "Activate the Home Assistant skill for Google Assistant", "config_documentation": "Config documentation", + "enable_state_reporting": "Enable State Reporting", + "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Google. This allows you to always see the latest states in the Google app.", + "security_devices": "Security Devices", "enter_pin_info": "Please enter a pin to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this pin when interacting with such devices via Google Assistant.", - "devices_pin": "Secure Devices Pin", - "enter_pin_hint": "Enter a PIN to use secure devices", + "devices_pin": "Security Devices Pin", + "enter_pin_hint": "Enter a PIN to use security devices", "sync_entities": "Sync Entities to Google", "manage_entities": "Manage Entities", "enter_pin_error": "Unable to store pin:" From 12840231beedc957c80147bb77fbb13959e3b294 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Oct 2019 21:33:51 +0200 Subject: [PATCH 43/52] Add code mirror editors to the dev-tools data fields (#3981) * Add yaml code mirror editor to the dev-tools yaml fields * Add jinja2 editor on dev template * Migrate to UpdatingElement, review comments * update cm, add types * types * dev tools mqtt --- package.json | 3 +- src/components/ha-code-editor.ts | 160 ++++++++++++++++++ src/components/ha-yaml-editor.ts | 139 --------------- src/panels/config/js/preact-types.ts | 2 +- src/panels/config/js/yaml_textarea.tsx | 20 +-- .../event/developer-tools-event.js | 60 +++++-- .../mqtt/developer-tools-mqtt.ts | 10 +- .../service/developer-tools-service.js | 28 +-- .../state/developer-tools-state.js | 71 +++++--- .../template/developer-tools-template.js | 28 +-- .../editor/card-editor/hui-card-editor.ts | 47 +++-- src/panels/lovelace/hui-editor.ts | 34 ++-- src/resources/codemirror.ondemand.ts | 13 ++ src/resources/codemirror.ts | 13 ++ yarn.lock | 24 ++- 15 files changed, 387 insertions(+), 265 deletions(-) create mode 100644 src/components/ha-code-editor.ts delete mode 100644 src/components/ha-yaml-editor.ts create mode 100644 src/resources/codemirror.ondemand.ts create mode 100644 src/resources/codemirror.ts diff --git a/package.json b/package.json index c54c656c04..00ead71952 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@webcomponents/webcomponentsjs": "^2.2.7", "chart.js": "~2.8.0", "chartjs-chart-timeline": "^0.3.0", - "codemirror": "^5.45.0", + "codemirror": "^5.49.0", "cpx": "^1.5.0", "deep-clone-simple": "^1.1.1", "es6-object-assign": "^1.1.0", @@ -117,6 +117,7 @@ "@types/chai": "^4.1.7", "@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-sender": "^1.0.1", + "@types/codemirror": "^0.0.78", "@types/hls.js": "^0.12.3", "@types/leaflet": "^1.4.3", "@types/memoize-one": "4.1.0", diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts new file mode 100644 index 0000000000..fcbc24306b --- /dev/null +++ b/src/components/ha-code-editor.ts @@ -0,0 +1,160 @@ +import { loadCodeMirror } from "../resources/codemirror.ondemand"; +import { fireEvent } from "../common/dom/fire_event"; +import { + UpdatingElement, + property, + customElement, + PropertyValues, +} from "lit-element"; +import { Editor } from "codemirror"; + +declare global { + interface HASSDomEvents { + "editor-save": undefined; + } +} + +@customElement("ha-code-editor") +export class HaCodeEditor extends UpdatingElement { + public codemirror?: Editor; + @property() public mode?: string; + @property() public autofocus = false; + @property() public rtl = false; + @property() public error = false; + @property() private _value = ""; + + public set value(value: string) { + this._value = value; + } + + public get value(): string { + return this.codemirror ? this.codemirror.getValue() : this._value; + } + + public get hasComments(): boolean { + return this.shadowRoot!.querySelector("span.cm-comment") ? true : false; + } + + public connectedCallback() { + super.connectedCallback(); + if (!this.codemirror) { + return; + } + this.codemirror.refresh(); + if (this.autofocus !== false) { + this.codemirror.focus(); + } + } + + protected update(changedProps: PropertyValues): void { + super.update(changedProps); + + if (!this.codemirror) { + return; + } + + if (changedProps.has("mode")) { + this.codemirror.setOption("mode", this.mode); + } + if (changedProps.has("autofocus")) { + this.codemirror.setOption("autofocus", this.autofocus !== false); + } + if (changedProps.has("_value") && this._value !== this.value) { + this.codemirror.setValue(this._value); + } + if (changedProps.has("rtl")) { + this.codemirror.setOption("gutters", this._calcGutters()); + this._setScrollBarDirection(); + } + if (changedProps.has("error")) { + this.classList.toggle("error-state", this.error); + } + } + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._load(); + } + + private async _load(): Promise { + const loaded = await loadCodeMirror(); + + const codeMirror = loaded.codeMirror; + + const shadowRoot = this.attachShadow({ mode: "open" }); + + shadowRoot!.innerHTML = ` + `; + + this.codemirror = codeMirror(shadowRoot, { + value: this._value, + lineNumbers: true, + tabSize: 2, + mode: this.mode, + autofocus: this.autofocus !== false, + viewportMargin: Infinity, + extraKeys: { + Tab: "indentMore", + "Shift-Tab": "indentLess", + }, + gutters: this._calcGutters(), + }); + this._setScrollBarDirection(); + this.codemirror!.on("changes", () => this._onChange()); + } + + private _onChange(): void { + const newValue = this.value; + if (newValue === this._value) { + return; + } + this._value = newValue; + fireEvent(this, "value-changed", { value: this._value }); + } + + private _calcGutters(): string[] { + return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : []; + } + + private _setScrollBarDirection(): void { + if (this.codemirror) { + this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-code-editor": HaCodeEditor; + } +} diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts deleted file mode 100644 index 11190cbfef..0000000000 --- a/src/components/ha-yaml-editor.ts +++ /dev/null @@ -1,139 +0,0 @@ -// @ts-ignore -import CodeMirror from "codemirror"; -import "codemirror/mode/yaml/yaml"; -// @ts-ignore -import codeMirrorCSS from "codemirror/lib/codemirror.css"; -import { fireEvent } from "../common/dom/fire_event"; -import { customElement } from "lit-element"; - -declare global { - interface HASSDomEvents { - "yaml-changed": { - value: string; - }; - "yaml-save": undefined; - } -} - -@customElement("ha-yaml-editor") -export class HaYamlEditor extends HTMLElement { - public codemirror?: any; - private _autofocus = false; - private _rtl = false; - private _value: string; - - public constructor() { - super(); - CodeMirror.commands.save = (cm: CodeMirror) => { - fireEvent(cm.getWrapperElement(), "yaml-save"); - }; - this._value = ""; - const shadowRoot = this.attachShadow({ mode: "open" }); - shadowRoot.innerHTML = ` - `; - } - - set value(value: string) { - if (this.codemirror) { - if (value !== this.codemirror.getValue()) { - this.codemirror.setValue(value); - } - } - this._value = value; - } - - get value(): string { - return this.codemirror.getValue(); - } - - set rtl(rtl: boolean) { - this._rtl = rtl; - this.setScrollBarDirection(); - } - - set autofocus(autofocus: boolean) { - this._autofocus = autofocus; - if (this.codemirror) { - this.codemirror.focus(); - } - } - - set error(error: boolean) { - this.classList.toggle("error-state", error); - } - - get hasComments(): boolean { - return this.shadowRoot!.querySelector("span.cm-comment") ? true : false; - } - - public connectedCallback(): void { - if (!this.codemirror) { - this.codemirror = CodeMirror( - (this.shadowRoot as unknown) as HTMLElement, - { - value: this._value, - lineNumbers: true, - mode: "yaml", - tabSize: 2, - autofocus: this._autofocus, - viewportMargin: Infinity, - extraKeys: { - Tab: "indentMore", - "Shift-Tab": "indentLess", - }, - gutters: this._rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [], - } - ); - this.setScrollBarDirection(); - this.codemirror.on("changes", () => this._onChange()); - } else { - this.codemirror.refresh(); - } - } - - private _onChange(): void { - fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() }); - } - - private setScrollBarDirection(): void { - if (this.codemirror) { - this.codemirror.getWrapperElement().classList.toggle("rtl", this._rtl); - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-yaml-editor": HaYamlEditor; - } -} diff --git a/src/panels/config/js/preact-types.ts b/src/panels/config/js/preact-types.ts index f103f63230..617fe5d2dd 100644 --- a/src/panels/config/js/preact-types.ts +++ b/src/panels/config/js/preact-types.ts @@ -20,7 +20,7 @@ declare global { "ha-device-picker": any; "ha-device-condition-picker": any; "ha-textarea": any; - "ha-yaml-editor": any; + "ha-code-editor": any; "ha-service-picker": any; "mwc-button": any; "ha-device-trigger-picker": any; diff --git a/src/panels/config/js/yaml_textarea.tsx b/src/panels/config/js/yaml_textarea.tsx index f9e8edf634..5050576db2 100644 --- a/src/panels/config/js/yaml_textarea.tsx +++ b/src/panels/config/js/yaml_textarea.tsx @@ -1,8 +1,6 @@ import { h, Component } from "preact"; import yaml from "js-yaml"; -import "../../../components/ha-yaml-editor"; -// tslint:disable-next-line -import { HaYamlEditor } from "../../../components/ha-yaml-editor"; +import "../../../components/ha-code-editor"; const isEmpty = (obj: object) => { for (const key in obj) { @@ -14,8 +12,6 @@ const isEmpty = (obj: object) => { }; export default class YAMLTextArea extends Component { - private _yamlEditor!: HaYamlEditor; - constructor(props) { super(props); @@ -63,12 +59,6 @@ export default class YAMLTextArea extends Component { } } - public componentDidMount() { - setTimeout(() => { - this._yamlEditor.codemirror.refresh(); - }, 1); - } - public render({ label }, { value, isValid }) { const style: any = { minWidth: 300, @@ -77,16 +67,14 @@ export default class YAMLTextArea extends Component { return (

{label}

-
); } - - private _storeYamlEditorRef = (yamlEditor) => (this._yamlEditor = yamlEditor); } diff --git a/src/panels/developer-tools/event/developer-tools-event.js b/src/panels/developer-tools/event/developer-tools-event.js index 25fd569034..c1b50bd068 100644 --- a/src/panels/developer-tools/event/developer-tools-event.js +++ b/src/panels/developer-tools/event/developer-tools-event.js @@ -1,17 +1,18 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import yaml from "js-yaml"; +import "../../../components/ha-code-editor"; import "../../../resources/ha-style"; import "./events-list"; import "./event-subscribe-card"; import { EventsMixin } from "../../../mixins/events-mixin"; +const ERROR_SENTINEL = {}; /* * @appliesMixin EventsMixin */ @@ -32,6 +33,11 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { .ha-form { margin-right: 16px; + max-width: 400px; + } + + mwc-button { + margin-top: 8px; } .header { @@ -62,11 +68,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { required value="{{eventType}}" > - - Fire Event +

Event Data (YAML, optional)

+ + Fire Event
@@ -97,6 +108,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { type: String, value: "", }, + + parsedJSON: { + type: Object, + computed: "_computeParsedEventData(eventData)", + }, + + validJSON: { + type: Boolean, + computed: "_computeValidJSON(parsedJSON)", + }, }; } @@ -104,19 +125,28 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { this.eventType = ev.detail.eventType; } - fireEvent() { - var eventData; - + _computeParsedEventData(eventData) { try { - eventData = this.eventData ? yaml.safeLoad(this.eventData) : {}; + return eventData.trim() ? yaml.safeLoad(eventData) : {}; } catch (err) { - /* eslint-disable no-alert */ - alert("Error parsing YAML: " + err); - /* eslint-enable no-alert */ + return ERROR_SENTINEL; + } + } + + _computeValidJSON(parsedJSON) { + return parsedJSON !== ERROR_SENTINEL; + } + + _yamlChanged(ev) { + this.eventData = ev.detail.value; + } + + fireEvent() { + if (!this.eventType) { + alert("Event type is a mandatory field"); return; } - - this.hass.callApi("POST", "events/" + this.eventType, eventData).then( + this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then( function() { this.fire("hass-notification", { message: "Event " + this.eventType + " successful fired!", diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts index 3320eb52c3..93f0215ee9 100644 --- a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts @@ -9,12 +9,12 @@ import { } from "lit-element"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { HomeAssistant } from "../../../types"; import { haStyle } from "../../../resources/styles"; import "../../../components/ha-card"; +import "../../../components/ha-code-editor"; import "./mqtt-subscribe-card"; @customElement("developer-tools-mqtt") @@ -48,12 +48,12 @@ class HaPanelDevMqtt extends LitElement { @value-changed=${this._handleTopic} > - Payload (template allowed)

+
+ >
Publish diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index 1656816d12..de04bd4ed9 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; @@ -7,6 +6,7 @@ import yaml from "js-yaml"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-code-editor"; import "../../../components/ha-service-picker"; import "../../../resources/ha-style"; import "../../../util/app-localstorage-document"; @@ -30,6 +30,10 @@ class HaPanelDevService extends PolymerElement { max-width: 400px; } + mwc-button { + margin-top: 8px; + } + .description { margin-top: 24px; white-space: pre-wrap; @@ -109,20 +113,16 @@ class HaPanelDevService extends PolymerElement { allow-custom-entity > - +

Service Data (YAML, optional)

+ Call Service -