From ae8671af96d60701aacb3aebc1278e13a007741c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 27 Mar 2024 17:26:56 +0100 Subject: [PATCH] Add filtering and grouping to device and entities config pages (#20204) * Add filtering and grouping to device and entities config pages * Update hass-tabs-subpage-data-table.ts * Change label * Update ha-config-voice-assistants-expose.ts * fix expose multi select * Update ha-config-voice-assistants-expose.ts --- src/layouts/hass-tabs-subpage-data-table.ts | 2 + .../devices/ha-config-devices-dashboard.ts | 556 +++++++++-------- .../config/entities/ha-config-entities.ts | 582 +++++++++--------- .../ha-config-voice-assistants-expose.ts | 165 ++--- src/translations/en.json | 16 +- 5 files changed, 607 insertions(+), 714 deletions(-) diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 73bec8c7a0..772f1a169c 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -470,6 +470,7 @@ export class HaTabsSubpageDataTable extends LitElement { private _disableSelectMode() { this._selectMode = false; + this._dataTable.clearSelection(); } private _handleSearchChange(ev: CustomEvent) { @@ -665,6 +666,7 @@ export class HaTabsSubpageDataTable extends LitElement { .select-mode-chip { --md-assist-chip-icon-label-space: 0; + --md-assist-chip-trailing-space: 8px; } ha-dialog { diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 96a76e0867..0194fd079c 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,7 +1,6 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; -import { mdiCancel, mdiFilterVariant, mdiPlus } from "@mdi/js"; +import { mdiPlus } from "@mdi/js"; import { CSSResultGroup, LitElement, @@ -10,6 +9,7 @@ import { html, nothing, } from "lit"; + import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { HASSDomEvent } from "../../../common/dom/fire_event"; @@ -28,7 +28,12 @@ import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; import "../../../components/ha-fab"; +import "../../../components/ha-filter-devices"; +import "../../../components/ha-filter-floor-areas"; +import "../../../components/ha-filter-integrations"; +import "../../../components/ha-filter-states"; import "../../../components/ha-icon-button"; +import "../../../components/ha-alert"; import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; import { fullEntitiesContext } from "../../../data/context"; import { @@ -41,7 +46,7 @@ import { findBatteryChargingEntity, findBatteryEntity, } from "../../../data/entity_registry"; -import { IntegrationManifest, domainToName } from "../../../data/integration"; +import { IntegrationManifest } from "../../../data/integration"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; @@ -77,11 +82,14 @@ export class HaConfigDeviceDashboard extends LitElement { @state() private _searchParms = new URLSearchParams(window.location.search); - @state() private _showDisabled = false; - @state() private _filter: string = history.state?.filter || ""; - @state() private _numHiddenDevices = 0; + @state() private _filters: Record< + string, + { value: string[] | undefined; items: Set | undefined } + > = {}; + + @state() private _expandedFilter?: string; private _ignoreLocationChange = false; @@ -104,55 +112,72 @@ export class HaConfigDeviceDashboard extends LitElement { } if (window.location.search.substring(1) !== this._searchParms.toString()) { this._searchParms = new URLSearchParams(window.location.search); + this._setFiltersFromUrl(); } }; private _popState = () => { if (window.location.search.substring(1) !== this._searchParms.toString()) { this._searchParms = new URLSearchParams(window.location.search); + this._setFiltersFromUrl(); } }; - private _activeFilters = memoizeOne( - ( - entries: ConfigEntry[], - filters: URLSearchParams, - localize: LocalizeFunc - ): string[] | undefined => { - const filterTexts: string[] = []; - filters.forEach((value, key) => { - switch (key) { - case "config_entry": { - const configEntry = entries.find( - (entry) => entry.entry_id === value - ); - if (!configEntry) { - break; - } - const integrationName = domainToName(localize, configEntry.domain); - filterTexts.push( - `${this.hass.localize( - "ui.panel.config.integrations.integration" - )} "${integrationName}${ - integrationName !== configEntry.title - ? `: ${configEntry.title}` - : "" - }"` - ); - break; - } - case "domain": { - filterTexts.push( - `${this.hass.localize( - "ui.panel.config.integrations.integration" - )} "${domainToName(localize, value)}"` - ); - } - } - }); - return filterTexts.length ? filterTexts : undefined; + private _states = memoizeOne((localize: LocalizeFunc) => [ + { + value: "disabled", + label: localize("ui.panel.config.devices.data_table.disabled_by"), + }, + ]); + + firstUpdated() { + this._filters = { + "ha-filter-states": { + value: [], + items: undefined, + }, + }; + this._setFiltersFromUrl(); + } + + private _setFiltersFromUrl() { + if (this._searchParms.has("domain")) { + this._filters = { + ...this._filters, + "ha-filter-states": { + value: [ + ...(this._filters["ha-filter-states"]?.value || []), + "disabled", + ], + items: undefined, + }, + "ha-filter-integrations": { + value: [this._searchParms.get("domain")!], + items: undefined, + }, + }; } - ); + if (this._searchParms.has("config_entry")) { + this._filters = { + ...this._filters, + "ha-filter-states": { + value: [ + ...(this._filters["ha-filter-states"]?.value || []), + "disabled", + ], + items: undefined, + }, + config_entry: { + value: [this._searchParms.get("config_entry")!], + items: undefined, + }, + }; + } + } + + private _clearFilter() { + this._filters = {}; + } private _devicesAndFilterDomains = memoizeOne( ( @@ -161,17 +186,16 @@ export class HaConfigDeviceDashboard extends LitElement { entities: EntityRegistryEntry[], areas: HomeAssistant["areas"], manifests: IntegrationManifest[], - filters: URLSearchParams, - showDisabled: boolean, + filters: Record< + string, + { value: string[] | undefined; items: Set | undefined } + >, localize: LocalizeFunc ) => { // Some older installations might have devices pointing at invalid entryIDs // So we guard for that. let outputDevices: DeviceRowData[] = Object.values(devices); - // If nothing gets filtered, this is our correct count of devices - let startLength = outputDevices.length; - const deviceEntityLookup: DeviceEntityLookup = {}; for (const entity of entities) { if (!entity.device_id) { @@ -193,33 +217,48 @@ export class HaConfigDeviceDashboard extends LitElement { manifestLookup[manifest.domain] = manifest; } - let filterConfigEntry: ConfigEntry | undefined; + let filteredConfigEntry: ConfigEntry | undefined; const filteredDomains = new Set(); - filters.forEach((value, key) => { - if (key === "config_entry") { + Object.entries(filters).forEach(([key, flter]) => { + if (key === "config_entry" && flter.value?.length) { outputDevices = outputDevices.filter((device) => - device.config_entries.includes(value) + device.config_entries.some((entryId) => + flter.value?.includes(entryId) + ) ); - startLength = outputDevices.length; - filterConfigEntry = entries.find((entry) => entry.entry_id === value); - if (filterConfigEntry) { - filteredDomains.add(filterConfigEntry.domain); + + const configEntries = entries.filter( + (entry) => entry.entry_id && flter.value?.includes(entry.entry_id) + ); + + configEntries.forEach((configEntry) => { + filteredDomains.add(configEntry.domain); + }); + if (configEntries.length === 1) { + filteredConfigEntry = configEntries[0]; } - } - if (key === "domain") { + } else if (key === "ha-filter-integrations" && flter.value?.length) { const entryIds = entries - .filter((entry) => entry.domain === value) + .filter((entry) => flter.value!.includes(entry.domain)) .map((entry) => entry.entry_id); outputDevices = outputDevices.filter((device) => device.config_entries.some((entryId) => entryIds.includes(entryId)) ); - startLength = outputDevices.length; - filteredDomains.add(value); + flter.value!.forEach((domain) => filteredDomains.add(domain)); + } else if (flter.items) { + outputDevices = outputDevices.filter((device) => + flter.items!.has(device.id) + ); } }); + const stateFilters = filters["ha-filter-states"]?.value; + + const showDisabled = + stateFilters?.length && stateFilters.includes("disabled"); + if (!showDisabled) { outputDevices = outputDevices.filter((device) => !device.disabled_by); } @@ -270,165 +309,140 @@ export class HaConfigDeviceDashboard extends LitElement { }; }); - this._numHiddenDevices = startLength - formattedOutputDevices.length; return { devicesOutput: formattedOutputDevices, - filteredConfigEntry: filterConfigEntry, + filteredConfigEntry, filteredDomains, }; } ); - private _columns = memoizeOne( - (localize: LocalizeFunc, narrow: boolean, showDisabled: boolean) => { - type DeviceItem = ReturnType< - typeof this._devicesAndFilterDomains - >["devicesOutput"][number]; + private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => { + type DeviceItem = ReturnType< + typeof this._devicesAndFilterDomains + >["devicesOutput"][number]; - const columns: DataTableColumnContainer = { - icon: { - title: "", - type: "icon", - template: (device) => - device.domains.length - ? html`` - : "", - }, - }; + const columns: DataTableColumnContainer = { + icon: { + title: "", + type: "icon", + template: (device) => + device.domains.length + ? html`` + : "", + }, + }; - if (narrow) { - columns.name = { - title: localize("ui.panel.config.devices.data_table.device"), - main: true, - sortable: true, - filterable: true, - direction: "asc", - grows: true, - template: (device) => html` - ${device.name} -
${device.area} | ${device.integration}
- `, - }; - } else { - columns.name = { - title: localize("ui.panel.config.devices.data_table.device"), - main: true, - sortable: true, - filterable: true, - grows: true, - direction: "asc", - }; - } + if (narrow) { + columns.name = { + title: localize("ui.panel.config.devices.data_table.device"), + main: true, + sortable: true, + filterable: true, + direction: "asc", + grows: true, + template: (device) => html` + ${device.name} +
${device.area} | ${device.integration}
+ `, + }; + } else { + columns.name = { + title: localize("ui.panel.config.devices.data_table.device"), + main: true, + sortable: true, + filterable: true, + grows: true, + direction: "asc", + }; + } - columns.manufacturer = { - title: localize("ui.panel.config.devices.data_table.manufacturer"), - sortable: true, - hidden: narrow, - filterable: true, - width: "15%", - }; - columns.model = { - title: localize("ui.panel.config.devices.data_table.model"), - sortable: true, - hidden: narrow, - filterable: true, - width: "15%", - }; - columns.area = { - title: localize("ui.panel.config.devices.data_table.area"), - sortable: true, - hidden: narrow, - filterable: true, - width: "15%", - }; - columns.integration = { - title: localize("ui.panel.config.devices.data_table.integration"), - sortable: true, - hidden: narrow, - filterable: true, - width: "15%", - }; - columns.battery_entity = { - title: localize("ui.panel.config.devices.data_table.battery"), - sortable: true, - filterable: true, - type: "numeric", - width: narrow ? "105px" : "15%", - maxWidth: "105px", - valueColumn: "battery_level", - template: (device) => { - const batteryEntityPair = device.battery_entity; - const battery = - batteryEntityPair && batteryEntityPair[0] - ? this.hass.states[batteryEntityPair[0]] - : undefined; - const batteryDomain = battery - ? computeStateDomain(battery) + columns.manufacturer = { + title: localize("ui.panel.config.devices.data_table.manufacturer"), + sortable: true, + hidden: narrow, + filterable: true, + groupable: true, + width: "15%", + }; + columns.model = { + title: localize("ui.panel.config.devices.data_table.model"), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }; + columns.area = { + title: localize("ui.panel.config.devices.data_table.area"), + sortable: true, + hidden: narrow, + filterable: true, + groupable: true, + width: "15%", + }; + columns.integration = { + title: localize("ui.panel.config.devices.data_table.integration"), + sortable: true, + hidden: narrow, + filterable: true, + groupable: true, + width: "15%", + }; + columns.battery_entity = { + title: localize("ui.panel.config.devices.data_table.battery"), + sortable: true, + filterable: true, + type: "numeric", + width: narrow ? "105px" : "15%", + maxWidth: "105px", + valueColumn: "battery_level", + template: (device) => { + const batteryEntityPair = device.battery_entity; + const battery = + batteryEntityPair && batteryEntityPair[0] + ? this.hass.states[batteryEntityPair[0]] + : undefined; + const batteryDomain = battery ? computeStateDomain(battery) : undefined; + const batteryCharging = + batteryEntityPair && batteryEntityPair[1] + ? this.hass.states[batteryEntityPair[1]] : undefined; - const batteryCharging = - batteryEntityPair && batteryEntityPair[1] - ? this.hass.states[batteryEntityPair[1]] - : undefined; - return battery && - (batteryDomain === "binary_sensor" || !isNaN(battery.state as any)) - ? html` - ${batteryDomain === "sensor" - ? this.hass.formatEntityState(battery) - : nothing} - - ` - : html`—`; - }, - }; - if (showDisabled) { - columns.disabled_by = { - title: "", - label: localize("ui.panel.config.devices.data_table.disabled_by"), - type: "icon", - template: (device) => - device.disabled_by - ? html`
- - - ${this.hass.localize("ui.panel.config.devices.disabled")} - -
` - : "—", - }; - } - return columns; - } - ); - - public willUpdate(changedProps) { - if (changedProps.has("_searchParms")) { - if ( - this._searchParms.get("config_entry") || - this._searchParms.get("domain") - ) { - // If we are requested to show the devices for a given config entry / domain, - // also show the disabled ones by default. - this._showDisabled = true; - } - } - } + return battery && + (batteryDomain === "binary_sensor" || !isNaN(battery.state as any)) + ? html` + ${batteryDomain === "sensor" + ? this.hass.formatEntityState(battery) + : nothing} + + ` + : html`—`; + }, + }; + columns.disabled_by = { + title: "", + label: localize("ui.panel.config.devices.data_table.disabled_by"), + hidden: true, + template: (device) => + device.disabled_by + ? this.hass.localize("ui.panel.config.devices.disabled") + : "", + }; + return columns; + }); protected render(): TemplateResult { const { devicesOutput } = this._devicesAndFilterDomains( @@ -437,13 +451,7 @@ export class HaConfigDeviceDashboard extends LitElement { this.entities, this.hass.areas, this.manifests, - this._searchParms, - this._showDisabled, - this.hass.localize - ); - const activeFilters = this._activeFilters( - this.entries, - this._searchParms, + this._filters, this.hass.localize ); @@ -456,22 +464,16 @@ export class HaConfigDeviceDashboard extends LitElement { : "/config"} .tabs=${configSections.devices} .route=${this.route} - .activeFilters=${activeFilters} - .numHidden=${this._numHiddenDevices} .searchLabel=${this.hass.localize( "ui.panel.config.devices.picker.search" )} - .hiddenLabel=${this.hass.localize( - "ui.panel.config.devices.picker.filter.hidden_devices", - { number: this._numHiddenDevices } - )} - .columns=${this._columns( - this.hass.localize, - this.narrow, - this._showDisabled - )} + .columns=${this._columns(this.hass.localize, this.narrow)} .data=${devicesOutput} .filter=${this._filter} + hasFilters + .filters=${Object.values(this._filters).filter( + (filter) => filter.value?.length + ).length} @clear-filter=${this._clearFilter} @search-changed=${this._handleSearchChange} @row-click=${this._handleRowClicked} @@ -490,37 +492,62 @@ export class HaConfigDeviceDashboard extends LitElement { > - - - ${this.narrow && activeFilters?.length - ? html`${this.hass.localize("ui.components.data-table.filtering_by")} - ${activeFilters.join(", ")} - ${this.hass.localize("ui.common.clear")}` - : ""} - - ${this.hass!.localize( - "ui.panel.config.devices.picker.filter.show_disabled" - )} - - + ${this._filters.config_entry?.value?.length + ? html` + Filtering by config entry + ${this.entries?.find( + (entry) => + entry.entry_id === this._filters.config_entry!.value![0] + )?.title || this._filters.config_entry.value[0]} + ` + : nothing} + + + `; } + private _filterExpanded(ev) { + if (ev.detail.expanded) { + this._expandedFilter = ev.target.localName; + } else if (this._expandedFilter === ev.target.localName) { + this._expandedFilter = undefined; + } + } + + private _filterChanged(ev) { + const type = ev.target.localName; + this._filters = { ...this._filters, [type]: ev.detail }; + } + private _batteryEntity( deviceId: string, deviceEntityLookup: DeviceEntityLookup @@ -549,27 +576,11 @@ export class HaConfigDeviceDashboard extends LitElement { navigate(`/config/devices/device/${deviceId}`); } - private _showDisabledChanged(ev: CustomEvent) { - if (ev.detail.source !== "property") { - return; - } - this._showDisabled = ev.detail.selected; - } - private _handleSearchChange(ev: CustomEvent) { this._filter = ev.detail.value; history.replaceState({ filter: this._filter }, ""); } - private _clearFilter() { - if ( - this._activeFilters(this.entries, this._searchParms, this.hass.localize) - ) { - navigate(window.location.pathname, { replace: true }); - } - this._showDisabled = true; - } - private _addDevice() { const { filteredConfigEntry, filteredDomains } = this._devicesAndFilterDomains( @@ -578,8 +589,7 @@ export class HaConfigDeviceDashboard extends LitElement { this.entities, this.hass.areas, this.manifests, - this._searchParms, - this._showDisabled, + this._filters, this.hass.localize ); if ( diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index a96db58f20..8e6bc34bd5 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -1,12 +1,10 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import { mdiAlertCircle, mdiCancel, mdiDelete, mdiEyeOff, - mdiFilterVariant, mdiPencilOff, mdiPlus, mdiRestoreAlert, @@ -22,7 +20,6 @@ import { nothing, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import { until } from "lit/directives/until"; @@ -34,7 +31,6 @@ import { PROTOCOL_INTEGRATIONS, protocolIntegrationPicked, } from "../../../common/integrations/protocolIntegrationPicked"; -import { navigate } from "../../../common/navigate"; import { LocalizeFunc } from "../../../common/translations/localize"; import type { DataTableColumnContainer, @@ -43,9 +39,14 @@ import type { } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; +import "../../../components/ha-filter-devices"; +import "../../../components/ha-filter-floor-areas"; +import "../../../components/ha-filter-integrations"; +import "../../../components/ha-filter-states"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-alert"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { fullEntitiesContext } from "../../../data/context"; import { UNAVAILABLE } from "../../../data/entity"; @@ -56,7 +57,6 @@ import { updateEntityRegistryEntry, } from "../../../data/entity_registry"; import { entryIcon } from "../../../data/icons"; -import { domainToName } from "../../../data/integration"; import { showAlertDialog, showConfirmationDialog, @@ -106,22 +106,19 @@ export class HaConfigEntities extends LitElement { @consume({ context: fullEntitiesContext, subscribe: true }) _entities!: EntityRegistryEntry[]; - @state() private _showDisabled = false; - - @state() private _showHidden = false; - - @state() private _showUnavailable = true; - - @state() private _showReadOnly = true; - @state() private _filter: string = history.state?.filter || ""; - @state() private _numHiddenEntities = 0; - @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _filters: Record< + string, + { value: string[] | undefined; items: Set | undefined } + > = {}; + @state() private _selectedEntities: string[] = []; + @state() private _expandedFilter?: string; + @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; @@ -140,71 +137,41 @@ export class HaConfigEntities extends LitElement { private _locationChanged = () => { if (window.location.search.substring(1) !== this._searchParms.toString()) { this._searchParms = new URLSearchParams(window.location.search); + this._setFiltersFromUrl(); } }; private _popState = () => { if (window.location.search.substring(1) !== this._searchParms.toString()) { this._searchParms = new URLSearchParams(window.location.search); + this._setFiltersFromUrl(); } }; - private _activeFilters = memoize( - ( - filters: URLSearchParams, - localize: LocalizeFunc, - entries?: ConfigEntry[] - ): string[] | undefined => { - const filterTexts: string[] = []; - filters.forEach((value, key) => { - switch (key) { - case "config_entry": { - // If we are requested to show the entities for a given config entry, - // also show the disabled ones by default. - this._showDisabled = true; - - if (!entries) { - this._loadConfigEntries(); - break; - } - const configEntry = entries.find( - (entry) => entry.entry_id === value - ); - if (!configEntry) { - break; - } - const integrationName = domainToName(localize, configEntry.domain); - filterTexts.push( - `${this.hass.localize( - "ui.panel.config.integrations.integration" - )} "${integrationName}${ - integrationName !== configEntry.title - ? `: ${configEntry.title}` - : "" - }"` - ); - break; - } - case "domain": { - this._showDisabled = true; - filterTexts.push( - `${this.hass.localize( - "ui.panel.config.integrations.integration" - )} "${domainToName(localize, value)}"` - ); - } - } - }); - return filterTexts.length ? filterTexts : undefined; - } - ); + private _states = memoize((localize: LocalizeFunc) => [ + { + value: "disabled", + label: localize("ui.panel.config.entities.picker.status.disabled"), + }, + { + value: "hidden", + label: localize("ui.panel.config.entities.picker.status.hidden"), + }, + { + value: "unavailable", + label: localize("ui.panel.config.entities.picker.status.unavailable"), + }, + { + value: "readonly", + label: localize("ui.panel.config.entities.picker.status.readonly"), + }, + ]); private _columns = memoize( ( localize: LocalizeFunc, narrow, - _language, - showDisabled + _language ): DataTableColumnContainer => ({ icon: { title: "", @@ -255,6 +222,7 @@ export class HaConfigEntities extends LitElement { title: localize("ui.panel.config.entities.picker.headers.integration"), hidden: narrow, sortable: true, + groupable: true, filterable: true, width: "20%", }, @@ -263,17 +231,16 @@ export class HaConfigEntities extends LitElement { sortable: true, hidden: narrow, filterable: true, + groupable: true, width: "15%", }, disabled_by: { title: localize("ui.panel.config.entities.picker.headers.disabled_by"), - sortable: true, - hidden: narrow || !showDisabled, + hidden: true, filterable: true, - width: "15%", template: (entry) => entry.disabled_by === null - ? "—" + ? "" : this.hass.localize( `config_entry.disabled_by.${entry.disabled_by}` ), @@ -283,6 +250,7 @@ export class HaConfigEntities extends LitElement { type: "icon", sortable: true, filterable: true, + groupable: true, width: "68px", template: (entry) => entry.unavailable || @@ -343,17 +311,24 @@ export class HaConfigEntities extends LitElement { devices: HomeAssistant["devices"], areas: HomeAssistant["areas"], stateEntities: StateEntity[], - filters: URLSearchParams, - showDisabled: boolean, - showUnavailable: boolean, - showReadOnly: boolean, - showHidden: boolean, + filters: Record< + string, + { value: string[] | undefined; items: Set | undefined } + >, entries?: ConfigEntry[] ) => { const result: EntityRow[] = []; - // If nothing gets filtered, this is our correct count of entities - let startLength = entities.length + stateEntities.length; + const stateFilters = filters["ha-filter-states"]?.value; + + const showReadOnly = + !stateFilters?.length || stateFilters.includes("readonly"); + const showDisabled = + !stateFilters?.length || stateFilters.includes("disabled"); + const showHidden = + !stateFilters?.length || stateFilters.includes("hidden"); + const showUnavailable = + !stateFilters?.length || stateFilters.includes("unavailable"); let filteredEntities = showReadOnly ? entities.concat(stateEntities) @@ -362,48 +337,47 @@ export class HaConfigEntities extends LitElement { let filteredConfigEntry: ConfigEntry | undefined; const filteredDomains = new Set(); - filters.forEach((value, key) => { - if (key === "config_entry") { + Object.entries(filters).forEach(([key, flter]) => { + if (key === "config_entry" && flter.value?.length) { filteredEntities = filteredEntities.filter( - (entity) => entity.config_entry_id === value + (entity) => + entity.config_entry_id && + flter.value?.includes(entity.config_entry_id) ); - // If we have an active filter and `showReadOnly` is true, the length of `entities` is correct. - // If however, the read-only entities were not added before, we need to check how many would - // have matched the active filter and add that number to the count. - startLength = filteredEntities.length; - if (!showReadOnly) { - startLength += stateEntities.filter( - (entity) => entity.config_entry_id === value - ).length; - } if (!entries) { this._loadConfigEntries(); return; } - const configEntry = entries.find((entry) => entry.entry_id === value); + const configEntries = entries.filter( + (entry) => entry.entry_id && flter.value?.includes(entry.entry_id) + ); - if (configEntry) { + configEntries.forEach((configEntry) => { filteredDomains.add(configEntry.domain); - filteredConfigEntry = configEntry; + }); + if (configEntries.length === 1) { + filteredConfigEntry = configEntries[0]; } - } - if (key === "domain") { + } else if (key === "ha-filter-integrations" && flter.value?.length) { if (!entries) { this._loadConfigEntries(); return; } const entryIds = entries - .filter((entry) => entry.domain === value) + .filter((entry) => flter.value!.includes(entry.domain)) .map((entry) => entry.entry_id); filteredEntities = filteredEntities.filter( (entity) => entity.config_entry_id && entryIds.includes(entity.config_entry_id) ); - filteredDomains.add(value); - startLength = filteredEntities.length; + flter.value!.forEach((domain) => filteredDomains.add(domain)); + } else if (flter.items) { + filteredEntities = filteredEntities.filter((entity) => + flter.items!.has(entity.entity_id) + ); } }); @@ -454,11 +428,12 @@ export class HaConfigEntities extends LitElement { ? localize( "ui.panel.config.entities.picker.status.readonly" ) - : undefined, + : localize( + "ui.panel.config.entities.picker.status.available" + ), }); } - this._numHiddenEntities = startLength - result.length; return { filteredEntities: result, filteredConfigEntry, filteredDomains }; } ); @@ -467,11 +442,6 @@ export class HaConfigEntities extends LitElement { if (!this.hass || this._entities === undefined) { return html` `; } - const activeFilters = this._activeFilters( - this._searchParms, - this.hass.localize, - this._entries - ); const { filteredEntities, filteredDomains } = this._filteredEntitiesAndDomains( @@ -480,11 +450,7 @@ export class HaConfigEntities extends LitElement { this.hass.devices, this.hass.areas, this._stateEntities, - this._searchParms, - this._showDisabled, - this._showUnavailable, - this._showReadOnly, - this._showHidden, + this._filters, this._entries ); @@ -506,20 +472,17 @@ export class HaConfigEntities extends LitElement { .columns=${this._columns( this.hass.localize, this.narrow, - this.hass.language, - this._showDisabled + this.hass.language )} .data=${filteredEntities} - .activeFilters=${activeFilters} - .numHidden=${this._numHiddenEntities} - .hideFilterMenu=${this._selectedEntities.length > 0} .searchLabel=${this.hass.localize( "ui.panel.config.entities.picker.search" )} - .hiddenLabel=${this.hass.localize( - "ui.panel.config.entities.picker.filter.hidden_entities", - { number: this._numHiddenEntities } - )} + hasFilters + .filters=${Object.values(this._filters).filter( + (filter) => filter.value?.length + ).length} + .selected=${this._selectedEntities.length} .filter=${this._filter} selectable clickable @@ -534,157 +497,142 @@ export class HaConfigEntities extends LitElement { .hass=${this.hass} slot="toolbar-icon" > - ${this._selectedEntities.length - ? html` -
-

- ${this.hass.localize( - "ui.panel.config.entities.picker.selected", - { number: this._selectedEntities.length } - )} -

-
- ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - ` - : html` - - - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - - `} -
-
- ` - : html` - +
+ ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} + ` + : html` - ${this.narrow && activeFilters?.length - ? html`${this.hass.localize( - "ui.components.data-table.filtering_by" - )} - ${activeFilters.join(", ")} - ${this.hass.localize("ui.common.clear")}` - : ""} - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_disabled" + + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_hidden" + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_unavailable" + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" )} - - - ${this.hass!.localize( - "ui.panel.config.entities.picker.filter.show_readonly" + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" )} - - - `} + + `} +
+ ${this._filters.config_entry?.value?.length + ? html` + Filtering by config entry + ${this._entries?.find( + (entry) => + entry.entry_id === this._filters.config_entry!.value![0] + )?.title || this._filters.config_entry.value[0]} + ` + : nothing} + + + + ${includeAddDeviceFab ? html`): void { super.willUpdate(changedProps); const oldHass = changedProps.get("hass"); @@ -746,34 +756,6 @@ export class HaConfigEntities extends LitElement { } } - private _showDisabledChanged(ev: CustomEvent) { - if (ev.detail.source !== "property") { - return; - } - this._showDisabled = ev.detail.selected; - } - - private _showHiddenChanged(ev: CustomEvent) { - if (ev.detail.source !== "property") { - return; - } - this._showHidden = ev.detail.selected; - } - - private _showRestoredChanged(ev: CustomEvent) { - if (ev.detail.source !== "property") { - return; - } - this._showUnavailable = ev.detail.selected; - } - - private _showReadOnlyChanged(ev: CustomEvent) { - if (ev.detail.source !== "property") { - return; - } - this._showReadOnly = ev.detail.selected; - } - private _handleSearchChange(ev: CustomEvent) { this._filter = ev.detail.value; history.replaceState({ filter: this._filter }, ""); @@ -927,18 +909,6 @@ export class HaConfigEntities extends LitElement { this._entries = await getConfigEntries(this.hass); } - private _clearFilter() { - if ( - this._activeFilters(this._searchParms, this.hass.localize, this._entries) - ) { - navigate(window.location.pathname, { replace: true }); - } - this._showDisabled = true; - this._showReadOnly = true; - this._showUnavailable = true; - this._showHidden = true; - } - private _addDevice() { const { filteredConfigEntry, filteredDomains } = this._filteredEntitiesAndDomains( @@ -947,11 +917,7 @@ export class HaConfigEntities extends LitElement { this.hass.devices, this.hass.areas, this._stateEntities, - this._searchParms, - this._showDisabled, - this._showUnavailable, - this._showReadOnly, - this._showHidden, + this._filters, this._entries ); if ( diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index 9c79ea7879..72820db82e 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -3,23 +3,14 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { mdiCloseBoxMultiple, mdiCloseCircleOutline, - mdiFilterVariant, mdiPlus, mdiPlusBoxMultiple, } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; +import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import memoize from "memoize-one"; -import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { EntityFilter, @@ -42,13 +33,13 @@ import { getExtendedEntityRegistryEntries, } from "../../../data/entity_registry"; import { - exposeEntities, ExposeEntitySettings, + exposeEntities, voiceAssistants, } from "../../../data/expose"; import { - fetchCloudGoogleEntities, GoogleEntity, + fetchCloudGoogleEntities, } from "../../../data/google_assistant"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; @@ -87,8 +78,6 @@ export class VoiceAssistantsExpose extends LitElement { @state() private _filter: string = history.state?.filter || ""; - @state() private _numHiddenEntities = 0; - @state() private _searchParms = new URLSearchParams(window.location.search); @state() private _selectedEntities: string[] = []; @@ -101,23 +90,6 @@ export class VoiceAssistantsExpose extends LitElement { @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; - private _activeFilters = memoize( - (filters: URLSearchParams): string[] | undefined => { - const filterTexts: string[] = []; - filters.forEach((value, key) => { - switch (key) { - case "assistants": { - const assistants = value.split(","); - assistants.forEach((assistant) => { - filterTexts.push(voiceAssistants[assistant]?.name || assistant); - }); - } - } - }); - return filterTexts.length ? filterTexts : undefined; - } - ); - private _columns = memoize( ( narrow: boolean, @@ -319,9 +291,6 @@ export class VoiceAssistantsExpose extends LitElement { ) ); - // If nothing gets filtered, this is our correct count of entities - const startLength = filteredEntities.length; - let filteredAssistants: string[]; filters.forEach((value, key) => { @@ -366,8 +335,6 @@ export class VoiceAssistantsExpose extends LitElement { }; } - this._numHiddenEntities = startLength - Object.values(result).length; - if (alexaManual || googleManual) { const manFilterFuncs = this._getEntityFilterFuncs( (this.cloudStatus as CloudStatusLoggedIn).google_entities, @@ -501,7 +468,6 @@ export class VoiceAssistantsExpose extends LitElement { if (!this.hass || !this.exposedEntities || !this._extEntities) { return html``; } - const activeFilters = this._activeFilters(this._searchParms); const filteredEntities = this._filteredEntities( this._extEntities, @@ -529,16 +495,9 @@ export class VoiceAssistantsExpose extends LitElement { this.hass.localize )} .data=${filteredEntities} - .activeFilters=${activeFilters} - .numHidden=${this._numHiddenEntities} - .hideFilterMenu=${this._selectedEntities.length > 0} .searchLabel=${this.hass.localize( "ui.panel.config.entities.picker.search" )} - .hiddenLabel=${this.hass.localize( - "ui.panel.config.entities.picker.filter.hidden_entities", - { number: this._numHiddenEntities } - )} .filter=${this._filter} selectable .selected=${this._selectedEntities.length} @@ -552,56 +511,48 @@ export class VoiceAssistantsExpose extends LitElement { > ${this._selectedEntities.length ? html` -
-
- ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.voice_assistants.expose.expose" - )} - ${this.hass.localize( - "ui.panel.config.voice_assistants.expose.unexpose" - )} - ` - : html` - - - ${this.hass.localize( - "ui.panel.config.voice_assistants.expose.expose" - )} - - - - ${this.hass.localize( - "ui.panel.config.voice_assistants.expose.unexpose" - )} - - `} -
+
+ ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.voice_assistants.expose.expose" + )} + ${this.hass.localize( + "ui.panel.config.voice_assistants.expose.unexpose" + )} + ` + : html` + + + ${this.hass.localize( + "ui.panel.config.voice_assistants.expose.expose" + )} + + + + ${this.hass.localize( + "ui.panel.config.voice_assistants.expose.unexpose" + )} + + `}
` : ""} @@ -615,26 +566,6 @@ export class VoiceAssistantsExpose extends LitElement { > - ${this.narrow && activeFilters?.length - ? html` - - - - ${this.hass.localize("ui.components.data-table.filtering_by")} - ${activeFilters.join(", ")} - - ${this.hass.localize("ui.common.clear")} - - - - ` - : nothing} `; } @@ -759,9 +690,7 @@ export class VoiceAssistantsExpose extends LitElement { } private _clearFilter() { - if (this._activeFilters(this._searchParms)) { - navigate(window.location.pathname, { replace: true }); - } + navigate(window.location.pathname, { replace: true }); } static get styles(): CSSResultGroup { diff --git a/src/translations/en.json b/src/translations/en.json index d1a4b1a6fb..30474d0c1b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3994,12 +3994,7 @@ "confirm_delete_integration": "Are you sure you want to remove this device from {integration}?", "picker": { "search": "Search devices", - "filter": { - "filter": "Filter", - "show_disabled": "Show disabled devices", - "hidden_devices": "{number} {number, plural,\n one {device}\n other {devices}\n} not shown", - "show_all": "Show all" - } + "state": "State" } }, "entities": { @@ -4011,15 +4006,6 @@ "introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.", "search": "Search entities", "unnamed_entity": "Unnamed entity", - "filter": { - "filter": "Filter", - "show_hidden": "Show hidden entities", - "show_disabled": "Show disabled entities", - "show_unavailable": "Show unavailable entities", - "show_readonly": "Show read-only entities", - "hidden_entities": "{number} {number, plural,\n one {entity}\n other {entities}\n} not shown", - "show_all": "Show all" - }, "status": { "restored": "Restored", "available": "Available",