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
This commit is contained in:
Bram Kragten 2024-03-27 17:26:56 +01:00 committed by GitHub
parent f5ff55abc5
commit ae8671af96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 607 additions and 714 deletions

View File

@ -470,6 +470,7 @@ export class HaTabsSubpageDataTable extends LitElement {
private _disableSelectMode() { private _disableSelectMode() {
this._selectMode = false; this._selectMode = false;
this._dataTable.clearSelection();
} }
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
@ -665,6 +666,7 @@ export class HaTabsSubpageDataTable extends LitElement {
.select-mode-chip { .select-mode-chip {
--md-assist-chip-icon-label-space: 0; --md-assist-chip-icon-label-space: 0;
--md-assist-chip-trailing-space: 8px;
} }
ha-dialog { ha-dialog {

View File

@ -1,7 +1,6 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import { mdiPlus } from "@mdi/js";
import { mdiCancel, mdiFilterVariant, mdiPlus } from "@mdi/js";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -10,6 +9,7 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event"; 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-button-menu";
import "../../../components/ha-check-list-item"; import "../../../components/ha-check-list-item";
import "../../../components/ha-fab"; 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-icon-button";
import "../../../components/ha-alert";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context"; import { fullEntitiesContext } from "../../../data/context";
import { import {
@ -41,7 +46,7 @@ import {
findBatteryChargingEntity, findBatteryChargingEntity,
findBatteryEntity, findBatteryEntity,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { IntegrationManifest, domainToName } from "../../../data/integration"; import { IntegrationManifest } from "../../../data/integration";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@ -77,11 +82,14 @@ export class HaConfigDeviceDashboard extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showDisabled = false;
@state() private _filter: string = history.state?.filter || ""; @state() private _filter: string = history.state?.filter || "";
@state() private _numHiddenDevices = 0; @state() private _filters: Record<
string,
{ value: string[] | undefined; items: Set<string> | undefined }
> = {};
@state() private _expandedFilter?: string;
private _ignoreLocationChange = false; private _ignoreLocationChange = false;
@ -104,55 +112,72 @@ export class HaConfigDeviceDashboard extends LitElement {
} }
if (window.location.search.substring(1) !== this._searchParms.toString()) { if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search); this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
} }
}; };
private _popState = () => { private _popState = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) { if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search); this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
} }
}; };
private _activeFilters = memoizeOne( private _states = memoizeOne((localize: LocalizeFunc) => [
( {
entries: ConfigEntry[], value: "disabled",
filters: URLSearchParams, label: localize("ui.panel.config.devices.data_table.disabled_by"),
localize: LocalizeFunc },
): string[] | undefined => { ]);
const filterTexts: string[] = [];
filters.forEach((value, key) => { firstUpdated() {
switch (key) { this._filters = {
case "config_entry": { "ha-filter-states": {
const configEntry = entries.find( value: [],
(entry) => entry.entry_id === value items: undefined,
); },
if (!configEntry) { };
break; this._setFiltersFromUrl();
} }
const integrationName = domainToName(localize, configEntry.domain);
filterTexts.push( private _setFiltersFromUrl() {
`${this.hass.localize( if (this._searchParms.has("domain")) {
"ui.panel.config.integrations.integration" this._filters = {
)} "${integrationName}${ ...this._filters,
integrationName !== configEntry.title "ha-filter-states": {
? `: ${configEntry.title}` value: [
: "" ...(this._filters["ha-filter-states"]?.value || []),
}"` "disabled",
); ],
break; items: undefined,
} },
case "domain": { "ha-filter-integrations": {
filterTexts.push( value: [this._searchParms.get("domain")!],
`${this.hass.localize( items: undefined,
"ui.panel.config.integrations.integration" },
)} "${domainToName(localize, value)}"` };
);
}
}
});
return filterTexts.length ? filterTexts : 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( private _devicesAndFilterDomains = memoizeOne(
( (
@ -161,17 +186,16 @@ export class HaConfigDeviceDashboard extends LitElement {
entities: EntityRegistryEntry[], entities: EntityRegistryEntry[],
areas: HomeAssistant["areas"], areas: HomeAssistant["areas"],
manifests: IntegrationManifest[], manifests: IntegrationManifest[],
filters: URLSearchParams, filters: Record<
showDisabled: boolean, string,
{ value: string[] | undefined; items: Set<string> | undefined }
>,
localize: LocalizeFunc localize: LocalizeFunc
) => { ) => {
// Some older installations might have devices pointing at invalid entryIDs // Some older installations might have devices pointing at invalid entryIDs
// So we guard for that. // So we guard for that.
let outputDevices: DeviceRowData[] = Object.values(devices); let outputDevices: DeviceRowData[] = Object.values(devices);
// If nothing gets filtered, this is our correct count of devices
let startLength = outputDevices.length;
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) { for (const entity of entities) {
if (!entity.device_id) { if (!entity.device_id) {
@ -193,33 +217,48 @@ export class HaConfigDeviceDashboard extends LitElement {
manifestLookup[manifest.domain] = manifest; manifestLookup[manifest.domain] = manifest;
} }
let filterConfigEntry: ConfigEntry | undefined; let filteredConfigEntry: ConfigEntry | undefined;
const filteredDomains = new Set<string>(); const filteredDomains = new Set<string>();
filters.forEach((value, key) => { Object.entries(filters).forEach(([key, flter]) => {
if (key === "config_entry") { if (key === "config_entry" && flter.value?.length) {
outputDevices = outputDevices.filter((device) => 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); const configEntries = entries.filter(
if (filterConfigEntry) { (entry) => entry.entry_id && flter.value?.includes(entry.entry_id)
filteredDomains.add(filterConfigEntry.domain); );
configEntries.forEach((configEntry) => {
filteredDomains.add(configEntry.domain);
});
if (configEntries.length === 1) {
filteredConfigEntry = configEntries[0];
} }
} } else if (key === "ha-filter-integrations" && flter.value?.length) {
if (key === "domain") {
const entryIds = entries const entryIds = entries
.filter((entry) => entry.domain === value) .filter((entry) => flter.value!.includes(entry.domain))
.map((entry) => entry.entry_id); .map((entry) => entry.entry_id);
outputDevices = outputDevices.filter((device) => outputDevices = outputDevices.filter((device) =>
device.config_entries.some((entryId) => entryIds.includes(entryId)) device.config_entries.some((entryId) => entryIds.includes(entryId))
); );
startLength = outputDevices.length; flter.value!.forEach((domain) => filteredDomains.add(domain));
filteredDomains.add(value); } 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) { if (!showDisabled) {
outputDevices = outputDevices.filter((device) => !device.disabled_by); outputDevices = outputDevices.filter((device) => !device.disabled_by);
} }
@ -270,165 +309,140 @@ export class HaConfigDeviceDashboard extends LitElement {
}; };
}); });
this._numHiddenDevices = startLength - formattedOutputDevices.length;
return { return {
devicesOutput: formattedOutputDevices, devicesOutput: formattedOutputDevices,
filteredConfigEntry: filterConfigEntry, filteredConfigEntry,
filteredDomains, filteredDomains,
}; };
} }
); );
private _columns = memoizeOne( private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => {
(localize: LocalizeFunc, narrow: boolean, showDisabled: boolean) => { type DeviceItem = ReturnType<
type DeviceItem = ReturnType< typeof this._devicesAndFilterDomains
typeof this._devicesAndFilterDomains >["devicesOutput"][number];
>["devicesOutput"][number];
const columns: DataTableColumnContainer<DeviceItem> = { const columns: DataTableColumnContainer<DeviceItem> = {
icon: { icon: {
title: "", title: "",
type: "icon", type: "icon",
template: (device) => template: (device) =>
device.domains.length device.domains.length
? html`<img ? html`<img
alt="" alt=""
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
src=${brandsUrl({ src=${brandsUrl({
domain: device.domains[0], domain: device.domains[0],
type: "icon", type: "icon",
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
})} })}
/>` />`
: "", : "",
}, },
}; };
if (narrow) { if (narrow) {
columns.name = { columns.name = {
title: localize("ui.panel.config.devices.data_table.device"), title: localize("ui.panel.config.devices.data_table.device"),
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (device) => html` template: (device) => html`
${device.name} ${device.name}
<div class="secondary">${device.area} | ${device.integration}</div> <div class="secondary">${device.area} | ${device.integration}</div>
`, `,
}; };
} else { } else {
columns.name = { columns.name = {
title: localize("ui.panel.config.devices.data_table.device"), title: localize("ui.panel.config.devices.data_table.device"),
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
direction: "asc", direction: "asc",
}; };
} }
columns.manufacturer = { columns.manufacturer = {
title: localize("ui.panel.config.devices.data_table.manufacturer"), title: localize("ui.panel.config.devices.data_table.manufacturer"),
sortable: true, sortable: true,
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
width: "15%", groupable: true,
}; width: "15%",
columns.model = { };
title: localize("ui.panel.config.devices.data_table.model"), columns.model = {
sortable: true, title: localize("ui.panel.config.devices.data_table.model"),
hidden: narrow, sortable: true,
filterable: true, hidden: narrow,
width: "15%", filterable: true,
}; width: "15%",
columns.area = { };
title: localize("ui.panel.config.devices.data_table.area"), columns.area = {
sortable: true, title: localize("ui.panel.config.devices.data_table.area"),
hidden: narrow, sortable: true,
filterable: true, hidden: narrow,
width: "15%", filterable: true,
}; groupable: true,
columns.integration = { width: "15%",
title: localize("ui.panel.config.devices.data_table.integration"), };
sortable: true, columns.integration = {
hidden: narrow, title: localize("ui.panel.config.devices.data_table.integration"),
filterable: true, sortable: true,
width: "15%", hidden: narrow,
}; filterable: true,
columns.battery_entity = { groupable: true,
title: localize("ui.panel.config.devices.data_table.battery"), width: "15%",
sortable: true, };
filterable: true, columns.battery_entity = {
type: "numeric", title: localize("ui.panel.config.devices.data_table.battery"),
width: narrow ? "105px" : "15%", sortable: true,
maxWidth: "105px", filterable: true,
valueColumn: "battery_level", type: "numeric",
template: (device) => { width: narrow ? "105px" : "15%",
const batteryEntityPair = device.battery_entity; maxWidth: "105px",
const battery = valueColumn: "battery_level",
batteryEntityPair && batteryEntityPair[0] template: (device) => {
? this.hass.states[batteryEntityPair[0]] const batteryEntityPair = device.battery_entity;
: undefined; const battery =
const batteryDomain = battery batteryEntityPair && batteryEntityPair[0]
? computeStateDomain(battery) ? this.hass.states[batteryEntityPair[0]]
: undefined;
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined; : undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
return battery && return battery &&
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any)) (batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
? html` ? html`
${batteryDomain === "sensor" ${batteryDomain === "sensor"
? this.hass.formatEntityState(battery) ? this.hass.formatEntityState(battery)
: nothing} : nothing}
<ha-battery-icon <ha-battery-icon
.hass=${this.hass} .hass=${this.hass}
.batteryStateObj=${battery} .batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging} .batteryChargingStateObj=${batteryCharging}
></ha-battery-icon> ></ha-battery-icon>
` `
: html``; : html``;
}, },
}; };
if (showDisabled) { columns.disabled_by = {
columns.disabled_by = { title: "",
title: "", label: localize("ui.panel.config.devices.data_table.disabled_by"),
label: localize("ui.panel.config.devices.data_table.disabled_by"), hidden: true,
type: "icon", template: (device) =>
template: (device) => device.disabled_by
device.disabled_by ? this.hass.localize("ui.panel.config.devices.disabled")
? html`<div : "",
tabindex="0" };
style="display:inline-block; position: relative;" return columns;
> });
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize("ui.panel.config.devices.disabled")}
</simple-tooltip>
</div>`
: "—",
};
}
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;
}
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
const { devicesOutput } = this._devicesAndFilterDomains( const { devicesOutput } = this._devicesAndFilterDomains(
@ -437,13 +451,7 @@ export class HaConfigDeviceDashboard extends LitElement {
this.entities, this.entities,
this.hass.areas, this.hass.areas,
this.manifests, this.manifests,
this._searchParms, this._filters,
this._showDisabled,
this.hass.localize
);
const activeFilters = this._activeFilters(
this.entries,
this._searchParms,
this.hass.localize this.hass.localize
); );
@ -456,22 +464,16 @@ export class HaConfigDeviceDashboard extends LitElement {
: "/config"} : "/config"}
.tabs=${configSections.devices} .tabs=${configSections.devices}
.route=${this.route} .route=${this.route}
.activeFilters=${activeFilters}
.numHidden=${this._numHiddenDevices}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.devices.picker.search" "ui.panel.config.devices.picker.search"
)} )}
.hiddenLabel=${this.hass.localize( .columns=${this._columns(this.hass.localize, this.narrow)}
"ui.panel.config.devices.picker.filter.hidden_devices",
{ number: this._numHiddenDevices }
)}
.columns=${this._columns(
this.hass.localize,
this.narrow,
this._showDisabled
)}
.data=${devicesOutput} .data=${devicesOutput}
.filter=${this._filter} .filter=${this._filter}
hasFilters
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
).length}
@clear-filter=${this._clearFilter} @clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
@ -490,37 +492,62 @@ export class HaConfigDeviceDashboard extends LitElement {
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab> </ha-fab>
<ha-button-menu slot="filter-menu" multi> ${this._filters.config_entry?.value?.length
<ha-icon-button ? html`<ha-alert slot="filter-pane">
slot="trigger" Filtering by config entry
.label=${this.hass!.localize( ${this.entries?.find(
"ui.panel.config.devices.picker.filter.filter" (entry) =>
)} entry.entry_id === this._filters.config_entry!.value![0]
.path=${mdiFilterVariant} )?.title || this._filters.config_entry.value[0]}
></ha-icon-button> </ha-alert>`
${this.narrow && activeFilters?.length : nothing}
? html`<mwc-list-item @click=${this._clearFilter} <ha-filter-floor-areas
>${this.hass.localize("ui.components.data-table.filtering_by")} .hass=${this.hass}
${activeFilters.join(", ")} type="device"
<span class="clear" .value=${this._filters["ha-filter-floor-areas"]?.value}
>${this.hass.localize("ui.common.clear")}</span @data-table-filter-changed=${this._filterChanged}
></mwc-list-item slot="filter-pane"
>` .expanded=${this._expandedFilter === "ha-filter-floor-areas"}
: ""} .narrow=${this.narrow}
<ha-check-list-item @expanded-changed=${this._filterExpanded}
left ></ha-filter-floor-areas>
@request-selected=${this._showDisabledChanged} <ha-filter-integrations
.selected=${this._showDisabled} .hass=${this.hass}
> .value=${this._filters["ha-filter-integrations"]?.value}
${this.hass!.localize( @data-table-filter-changed=${this._filterChanged}
"ui.panel.config.devices.picker.filter.show_disabled" slot="filter-pane"
)} .expanded=${this._expandedFilter === "ha-filter-integrations"}
</ha-check-list-item> .narrow=${this.narrow}
</ha-button-menu> @expanded-changed=${this._filterExpanded}
></ha-filter-integrations>
<ha-filter-states
.hass=${this.hass}
.value=${this._filters["ha-filter-states"]?.value}
.states=${this._states(this.hass.localize)}
.label=${this.hass.localize("ui.panel.config.devices.picker.state")}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-states"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-states>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
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( private _batteryEntity(
deviceId: string, deviceId: string,
deviceEntityLookup: DeviceEntityLookup deviceEntityLookup: DeviceEntityLookup
@ -549,27 +576,11 @@ export class HaConfigDeviceDashboard extends LitElement {
navigate(`/config/devices/device/${deviceId}`); navigate(`/config/devices/device/${deviceId}`);
} }
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showDisabled = ev.detail.selected;
}
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, ""); 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() { private _addDevice() {
const { filteredConfigEntry, filteredDomains } = const { filteredConfigEntry, filteredDomains } =
this._devicesAndFilterDomains( this._devicesAndFilterDomains(
@ -578,8 +589,7 @@ export class HaConfigDeviceDashboard extends LitElement {
this.entities, this.entities,
this.hass.areas, this.hass.areas,
this.manifests, this.manifests,
this._searchParms, this._filters,
this._showDisabled,
this.hass.localize this.hass.localize
); );
if ( if (

View File

@ -1,12 +1,10 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { import {
mdiAlertCircle, mdiAlertCircle,
mdiCancel, mdiCancel,
mdiDelete, mdiDelete,
mdiEyeOff, mdiEyeOff,
mdiFilterVariant,
mdiPencilOff, mdiPencilOff,
mdiPlus, mdiPlus,
mdiRestoreAlert, mdiRestoreAlert,
@ -22,7 +20,6 @@ import {
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
@ -34,7 +31,6 @@ import {
PROTOCOL_INTEGRATIONS, PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked, protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked"; } from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import type { import type {
DataTableColumnContainer, DataTableColumnContainer,
@ -43,9 +39,14 @@ import type {
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item"; 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";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-alert";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context"; import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
@ -56,7 +57,6 @@ import {
updateEntityRegistryEntry, updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { entryIcon } from "../../../data/icons"; import { entryIcon } from "../../../data/icons";
import { domainToName } from "../../../data/integration";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@ -106,22 +106,19 @@ export class HaConfigEntities extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entities!: EntityRegistryEntry[]; _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 _filter: string = history.state?.filter || "";
@state() private _numHiddenEntities = 0;
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _filters: Record<
string,
{ value: string[] | undefined; items: Set<string> | undefined }
> = {};
@state() private _selectedEntities: string[] = []; @state() private _selectedEntities: string[] = [];
@state() private _expandedFilter?: string;
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; private _dataTable!: HaTabsSubpageDataTable;
@ -140,71 +137,41 @@ export class HaConfigEntities extends LitElement {
private _locationChanged = () => { private _locationChanged = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) { if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search); this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
} }
}; };
private _popState = () => { private _popState = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) { if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search); this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
} }
}; };
private _activeFilters = memoize( private _states = memoize((localize: LocalizeFunc) => [
( {
filters: URLSearchParams, value: "disabled",
localize: LocalizeFunc, label: localize("ui.panel.config.entities.picker.status.disabled"),
entries?: ConfigEntry[] },
): string[] | undefined => { {
const filterTexts: string[] = []; value: "hidden",
filters.forEach((value, key) => { label: localize("ui.panel.config.entities.picker.status.hidden"),
switch (key) { },
case "config_entry": { {
// If we are requested to show the entities for a given config entry, value: "unavailable",
// also show the disabled ones by default. label: localize("ui.panel.config.entities.picker.status.unavailable"),
this._showDisabled = true; },
{
if (!entries) { value: "readonly",
this._loadConfigEntries(); label: localize("ui.panel.config.entities.picker.status.readonly"),
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 _columns = memoize( private _columns = memoize(
( (
localize: LocalizeFunc, localize: LocalizeFunc,
narrow, narrow,
_language, _language
showDisabled
): DataTableColumnContainer<EntityRow> => ({ ): DataTableColumnContainer<EntityRow> => ({
icon: { icon: {
title: "", title: "",
@ -255,6 +222,7 @@ export class HaConfigEntities extends LitElement {
title: localize("ui.panel.config.entities.picker.headers.integration"), title: localize("ui.panel.config.entities.picker.headers.integration"),
hidden: narrow, hidden: narrow,
sortable: true, sortable: true,
groupable: true,
filterable: true, filterable: true,
width: "20%", width: "20%",
}, },
@ -263,17 +231,16 @@ export class HaConfigEntities extends LitElement {
sortable: true, sortable: true,
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
groupable: true,
width: "15%", width: "15%",
}, },
disabled_by: { disabled_by: {
title: localize("ui.panel.config.entities.picker.headers.disabled_by"), title: localize("ui.panel.config.entities.picker.headers.disabled_by"),
sortable: true, hidden: true,
hidden: narrow || !showDisabled,
filterable: true, filterable: true,
width: "15%",
template: (entry) => template: (entry) =>
entry.disabled_by === null entry.disabled_by === null
? "" ? ""
: this.hass.localize( : this.hass.localize(
`config_entry.disabled_by.${entry.disabled_by}` `config_entry.disabled_by.${entry.disabled_by}`
), ),
@ -283,6 +250,7 @@ export class HaConfigEntities extends LitElement {
type: "icon", type: "icon",
sortable: true, sortable: true,
filterable: true, filterable: true,
groupable: true,
width: "68px", width: "68px",
template: (entry) => template: (entry) =>
entry.unavailable || entry.unavailable ||
@ -343,17 +311,24 @@ export class HaConfigEntities extends LitElement {
devices: HomeAssistant["devices"], devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"], areas: HomeAssistant["areas"],
stateEntities: StateEntity[], stateEntities: StateEntity[],
filters: URLSearchParams, filters: Record<
showDisabled: boolean, string,
showUnavailable: boolean, { value: string[] | undefined; items: Set<string> | undefined }
showReadOnly: boolean, >,
showHidden: boolean,
entries?: ConfigEntry[] entries?: ConfigEntry[]
) => { ) => {
const result: EntityRow[] = []; const result: EntityRow[] = [];
// If nothing gets filtered, this is our correct count of entities const stateFilters = filters["ha-filter-states"]?.value;
let startLength = entities.length + stateEntities.length;
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 let filteredEntities = showReadOnly
? entities.concat(stateEntities) ? entities.concat(stateEntities)
@ -362,48 +337,47 @@ export class HaConfigEntities extends LitElement {
let filteredConfigEntry: ConfigEntry | undefined; let filteredConfigEntry: ConfigEntry | undefined;
const filteredDomains = new Set<string>(); const filteredDomains = new Set<string>();
filters.forEach((value, key) => { Object.entries(filters).forEach(([key, flter]) => {
if (key === "config_entry") { if (key === "config_entry" && flter.value?.length) {
filteredEntities = filteredEntities.filter( 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) { if (!entries) {
this._loadConfigEntries(); this._loadConfigEntries();
return; 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); filteredDomains.add(configEntry.domain);
filteredConfigEntry = configEntry; });
if (configEntries.length === 1) {
filteredConfigEntry = configEntries[0];
} }
} } else if (key === "ha-filter-integrations" && flter.value?.length) {
if (key === "domain") {
if (!entries) { if (!entries) {
this._loadConfigEntries(); this._loadConfigEntries();
return; return;
} }
const entryIds = entries const entryIds = entries
.filter((entry) => entry.domain === value) .filter((entry) => flter.value!.includes(entry.domain))
.map((entry) => entry.entry_id); .map((entry) => entry.entry_id);
filteredEntities = filteredEntities.filter( filteredEntities = filteredEntities.filter(
(entity) => (entity) =>
entity.config_entry_id && entity.config_entry_id &&
entryIds.includes(entity.config_entry_id) entryIds.includes(entity.config_entry_id)
); );
filteredDomains.add(value); flter.value!.forEach((domain) => filteredDomains.add(domain));
startLength = filteredEntities.length; } else if (flter.items) {
filteredEntities = filteredEntities.filter((entity) =>
flter.items!.has(entity.entity_id)
);
} }
}); });
@ -454,11 +428,12 @@ export class HaConfigEntities extends LitElement {
? localize( ? localize(
"ui.panel.config.entities.picker.status.readonly" "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 }; return { filteredEntities: result, filteredConfigEntry, filteredDomains };
} }
); );
@ -467,11 +442,6 @@ export class HaConfigEntities extends LitElement {
if (!this.hass || this._entities === undefined) { if (!this.hass || this._entities === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
const activeFilters = this._activeFilters(
this._searchParms,
this.hass.localize,
this._entries
);
const { filteredEntities, filteredDomains } = const { filteredEntities, filteredDomains } =
this._filteredEntitiesAndDomains( this._filteredEntitiesAndDomains(
@ -480,11 +450,7 @@ export class HaConfigEntities extends LitElement {
this.hass.devices, this.hass.devices,
this.hass.areas, this.hass.areas,
this._stateEntities, this._stateEntities,
this._searchParms, this._filters,
this._showDisabled,
this._showUnavailable,
this._showReadOnly,
this._showHidden,
this._entries this._entries
); );
@ -506,20 +472,17 @@ export class HaConfigEntities extends LitElement {
.columns=${this._columns( .columns=${this._columns(
this.hass.localize, this.hass.localize,
this.narrow, this.narrow,
this.hass.language, this.hass.language
this._showDisabled
)} )}
.data=${filteredEntities} .data=${filteredEntities}
.activeFilters=${activeFilters}
.numHidden=${this._numHiddenEntities}
.hideFilterMenu=${this._selectedEntities.length > 0}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.entities.picker.search" "ui.panel.config.entities.picker.search"
)} )}
.hiddenLabel=${this.hass.localize( hasFilters
"ui.panel.config.entities.picker.filter.hidden_entities", .filters=${Object.values(this._filters).filter(
{ number: this._numHiddenEntities } (filter) => filter.value?.length
)} ).length}
.selected=${this._selectedEntities.length}
.filter=${this._filter} .filter=${this._filter}
selectable selectable
clickable clickable
@ -534,157 +497,142 @@ export class HaConfigEntities extends LitElement {
.hass=${this.hass} .hass=${this.hass}
slot="toolbar-icon" slot="toolbar-icon"
></ha-integration-overflow-menu> ></ha-integration-overflow-menu>
${this._selectedEntities.length <div class="header-btns" slot="selection-bar">
? html` ${!this.narrow
<div ? html`
class=${classMap({ <mwc-button
"header-toolbar": this.narrow, @click=${this._enableSelected}
"table-header": !this.narrow, .disabled=${!this._selectedEntities.length}
})} >${this.hass.localize(
slot="header" "ui.panel.config.entities.picker.enable_selected.button"
> )}</mwc-button
<p class="selected-txt"> >
${this.hass.localize( <mwc-button
"ui.panel.config.entities.picker.selected", @click=${this._disableSelected}
{ number: this._selectedEntities.length } .disabled=${!this._selectedEntities.length}
)} >${this.hass.localize(
</p> "ui.panel.config.entities.picker.disable_selected.button"
<div class="header-btns"> )}</mwc-button
${!this.narrow >
? html` <mwc-button
<mwc-button @click=${this._enableSelected} @click=${this._hideSelected}
>${this.hass.localize( .disabled=${!this._selectedEntities.length}
"ui.panel.config.entities.picker.enable_selected.button" >${this.hass.localize(
)}</mwc-button "ui.panel.config.entities.picker.hide_selected.button"
> )}</mwc-button
<mwc-button @click=${this._disableSelected} >
>${this.hass.localize( <mwc-button
"ui.panel.config.entities.picker.disable_selected.button" @click=${this._removeSelected}
)}</mwc-button .disabled=${!this._selectedEntities.length}
> class="warning"
<mwc-button @click=${this._hideSelected} >${this.hass.localize(
>${this.hass.localize( "ui.panel.config.entities.picker.remove_selected.button"
"ui.panel.config.entities.picker.hide_selected.button" )}</mwc-button
)}</mwc-button >
> `
<mwc-button : html`
@click=${this._removeSelected}
class="warning"
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<ha-icon-button
id="enable-btn"
@click=${this._enableSelected}
.path=${mdiUndo}
.label=${this.hass.localize("ui.common.enable")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</simple-tooltip>
<ha-icon-button
id="disable-btn"
@click=${this._disableSelected}
.path=${mdiCancel}
.label=${this.hass.localize("ui.common.disable")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</simple-tooltip>
<ha-icon-button
id="hide-btn"
@click=${this._hideSelected}
.path=${mdiEyeOff}
.label=${this.hass.localize("ui.common.hide")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="hide-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</simple-tooltip>
<ha-icon-button
class="warning"
id="remove-btn"
@click=${this._removeSelected}
.path=${mdiDelete}
.label=${this.hass.localize("ui.common.remove")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}
</simple-tooltip>
`}
</div>
</div>
`
: html`
<ha-button-menu slot="filter-menu" multi>
<ha-icon-button <ha-icon-button
slot="trigger" id="enable-btn"
.label=${this.hass!.localize( .disabled=${!this._selectedEntities.length}
"ui.panel.config.entities.picker.filter.filter" @click=${this._enableSelected}
)} .path=${mdiUndo}
.path=${mdiFilterVariant} .label=${this.hass.localize("ui.common.enable")}
></ha-icon-button> ></ha-icon-button>
${this.narrow && activeFilters?.length <simple-tooltip animation-delay="0" for="enable-btn">
? html`<mwc-list-item @click=${this._clearFilter} ${this.hass.localize(
>${this.hass.localize( "ui.panel.config.entities.picker.enable_selected.button"
"ui.components.data-table.filtering_by"
)}
${activeFilters.join(", ")}
<span class="clear"
>${this.hass.localize("ui.common.clear")}</span
></mwc-list-item
>`
: ""}
<ha-check-list-item
@request-selected=${this._showDisabledChanged}
.selected=${this._showDisabled}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_disabled"
)} )}
</ha-check-list-item> </simple-tooltip>
<ha-check-list-item <ha-icon-button
@request-selected=${this._showHiddenChanged} id="disable-btn"
.selected=${this._showHidden} .disabled=${!this._selectedEntities.length}
left @click=${this._disableSelected}
> .path=${mdiCancel}
${this.hass!.localize( .label=${this.hass.localize("ui.common.disable")}
"ui.panel.config.entities.picker.filter.show_hidden" ></ha-icon-button>
<simple-tooltip animation-delay="0" for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)} )}
</ha-check-list-item> </simple-tooltip>
<ha-check-list-item <ha-icon-button
@request-selected=${this._showRestoredChanged} id="hide-btn"
graphic="control" .disabled=${!this._selectedEntities.length}
.selected=${this._showUnavailable} @click=${this._hideSelected}
left .path=${mdiEyeOff}
> .label=${this.hass.localize("ui.common.hide")}
${this.hass!.localize( ></ha-icon-button>
"ui.panel.config.entities.picker.filter.show_unavailable" <simple-tooltip animation-delay="0" for="hide-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)} )}
</ha-check-list-item> </simple-tooltip>
<ha-check-list-item <ha-icon-button
@request-selected=${this._showReadOnlyChanged} class="warning"
graphic="control" id="remove-btn"
.selected=${this._showReadOnly} .disabled=${!this._selectedEntities.length}
left @click=${this._removeSelected}
> .path=${mdiDelete}
${this.hass!.localize( .label=${this.hass.localize("ui.common.remove")}
"ui.panel.config.entities.picker.filter.show_readonly" ></ha-icon-button>
<simple-tooltip animation-delay="0" for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)} )}
</ha-check-list-item> </simple-tooltip>
</ha-button-menu> `}
`} </div>
${this._filters.config_entry?.value?.length
? html`<ha-alert slot="filter-pane">
Filtering by config entry
${this._entries?.find(
(entry) =>
entry.entry_id === this._filters.config_entry!.value![0]
)?.title || this._filters.config_entry.value[0]}
</ha-alert>`
: nothing}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-floor-areas"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-devices"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-devices"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-integrations
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-integrations"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-integrations>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.entities.picker.headers.status"
)}
.value=${this._filters["ha-filter-states"]?.value}
.states=${this._states(this.hass.localize)}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-states"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-states>
${includeAddDeviceFab ${includeAddDeviceFab
? html`<ha-fab ? html`<ha-fab
.label=${this.hass.localize("ui.panel.config.devices.add_device")} .label=${this.hass.localize("ui.panel.config.devices.add_device")}
@ -699,6 +647,68 @@ export class HaConfigEntities extends LitElement {
`; `;
} }
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 };
}
protected firstUpdated() {
this._filters = {
"ha-filter-states": {
value: ["unavailable", "readonly"],
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 = {};
}
public willUpdate(changedProps: PropertyValues<this>): void { public willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
const oldHass = changedProps.get("hass"); const oldHass = changedProps.get("hass");
@ -746,34 +756,6 @@ export class HaConfigEntities extends LitElement {
} }
} }
private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showDisabled = ev.detail.selected;
}
private _showHiddenChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showHidden = ev.detail.selected;
}
private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showUnavailable = ev.detail.selected;
}
private _showReadOnlyChanged(ev: CustomEvent<RequestSelectedDetail>) {
if (ev.detail.source !== "property") {
return;
}
this._showReadOnly = ev.detail.selected;
}
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, ""); history.replaceState({ filter: this._filter }, "");
@ -927,18 +909,6 @@ export class HaConfigEntities extends LitElement {
this._entries = await getConfigEntries(this.hass); 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() { private _addDevice() {
const { filteredConfigEntry, filteredDomains } = const { filteredConfigEntry, filteredDomains } =
this._filteredEntitiesAndDomains( this._filteredEntitiesAndDomains(
@ -947,11 +917,7 @@ export class HaConfigEntities extends LitElement {
this.hass.devices, this.hass.devices,
this.hass.areas, this.hass.areas,
this._stateEntities, this._stateEntities,
this._searchParms, this._filters,
this._showDisabled,
this._showUnavailable,
this._showReadOnly,
this._showHidden,
this._entries this._entries
); );
if ( if (

View File

@ -3,23 +3,14 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { import {
mdiCloseBoxMultiple, mdiCloseBoxMultiple,
mdiCloseCircleOutline, mdiCloseCircleOutline,
mdiFilterVariant,
mdiPlus, mdiPlus,
mdiPlusBoxMultiple, mdiPlusBoxMultiple,
} from "@mdi/js"; } from "@mdi/js";
import { import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one"; 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 { computeStateName } from "../../../common/entity/compute_state_name";
import { import {
EntityFilter, EntityFilter,
@ -42,13 +33,13 @@ import {
getExtendedEntityRegistryEntries, getExtendedEntityRegistryEntries,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { import {
exposeEntities,
ExposeEntitySettings, ExposeEntitySettings,
exposeEntities,
voiceAssistants, voiceAssistants,
} from "../../../data/expose"; } from "../../../data/expose";
import { import {
fetchCloudGoogleEntities,
GoogleEntity, GoogleEntity,
fetchCloudGoogleEntities,
} from "../../../data/google_assistant"; } from "../../../data/google_assistant";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
@ -87,8 +78,6 @@ export class VoiceAssistantsExpose extends LitElement {
@state() private _filter: string = history.state?.filter || ""; @state() private _filter: string = history.state?.filter || "";
@state() private _numHiddenEntities = 0;
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selectedEntities: string[] = []; @state() private _selectedEntities: string[] = [];
@ -101,23 +90,6 @@ export class VoiceAssistantsExpose extends LitElement {
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; 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( private _columns = memoize(
( (
narrow: boolean, 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[]; let filteredAssistants: string[];
filters.forEach((value, key) => { filters.forEach((value, key) => {
@ -366,8 +335,6 @@ export class VoiceAssistantsExpose extends LitElement {
}; };
} }
this._numHiddenEntities = startLength - Object.values(result).length;
if (alexaManual || googleManual) { if (alexaManual || googleManual) {
const manFilterFuncs = this._getEntityFilterFuncs( const manFilterFuncs = this._getEntityFilterFuncs(
(this.cloudStatus as CloudStatusLoggedIn).google_entities, (this.cloudStatus as CloudStatusLoggedIn).google_entities,
@ -501,7 +468,6 @@ export class VoiceAssistantsExpose extends LitElement {
if (!this.hass || !this.exposedEntities || !this._extEntities) { if (!this.hass || !this.exposedEntities || !this._extEntities) {
return html`<hass-loading-screen></hass-loading-screen>`; return html`<hass-loading-screen></hass-loading-screen>`;
} }
const activeFilters = this._activeFilters(this._searchParms);
const filteredEntities = this._filteredEntities( const filteredEntities = this._filteredEntities(
this._extEntities, this._extEntities,
@ -529,16 +495,9 @@ export class VoiceAssistantsExpose extends LitElement {
this.hass.localize this.hass.localize
)} )}
.data=${filteredEntities} .data=${filteredEntities}
.activeFilters=${activeFilters}
.numHidden=${this._numHiddenEntities}
.hideFilterMenu=${this._selectedEntities.length > 0}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.entities.picker.search" "ui.panel.config.entities.picker.search"
)} )}
.hiddenLabel=${this.hass.localize(
"ui.panel.config.entities.picker.filter.hidden_entities",
{ number: this._numHiddenEntities }
)}
.filter=${this._filter} .filter=${this._filter}
selectable selectable
.selected=${this._selectedEntities.length} .selected=${this._selectedEntities.length}
@ -552,56 +511,48 @@ export class VoiceAssistantsExpose extends LitElement {
> >
${this._selectedEntities.length ${this._selectedEntities.length
? html` ? html`
<div <div class="header-btns" slot="selection-bar">
class=${classMap({ ${!this.narrow
"header-toolbar": this.narrow, ? html`
"table-header": !this.narrow, <mwc-button @click=${this._exposeSelected}
})} >${this.hass.localize(
slot="header" "ui.panel.config.voice_assistants.expose.expose"
> )}</mwc-button
<div class="header-btns"> >
${!this.narrow <mwc-button @click=${this._unexposeSelected}
? html` >${this.hass.localize(
<mwc-button @click=${this._exposeSelected} "ui.panel.config.voice_assistants.expose.unexpose"
>${this.hass.localize( )}</mwc-button
"ui.panel.config.voice_assistants.expose.expose" >
)}</mwc-button `
> : html`
<mwc-button @click=${this._unexposeSelected} <ha-icon-button
>${this.hass.localize( id="enable-btn"
"ui.panel.config.voice_assistants.expose.unexpose" @click=${this._exposeSelected}
)}</mwc-button .path=${mdiPlusBoxMultiple}
> .label=${this.hass.localize(
` "ui.panel.config.voice_assistants.expose.expose"
: html` )}
<ha-icon-button ></ha-icon-button>
id="enable-btn" <simple-tooltip animation-delay="0" for="enable-btn">
@click=${this._exposeSelected} ${this.hass.localize(
.path=${mdiPlusBoxMultiple} "ui.panel.config.voice_assistants.expose.expose"
.label=${this.hass.localize( )}
"ui.panel.config.voice_assistants.expose.expose" </simple-tooltip>
)} <ha-icon-button
></ha-icon-button> id="disable-btn"
<simple-tooltip animation-delay="0" for="enable-btn"> @click=${this._unexposeSelected}
${this.hass.localize( .path=${mdiCloseBoxMultiple}
"ui.panel.config.voice_assistants.expose.expose" .label=${this.hass.localize(
)} "ui.panel.config.voice_assistants.expose.unexpose"
</simple-tooltip> )}
<ha-icon-button ></ha-icon-button>
id="disable-btn" <simple-tooltip animation-delay="0" for="disable-btn">
@click=${this._unexposeSelected} ${this.hass.localize(
.path=${mdiCloseBoxMultiple} "ui.panel.config.voice_assistants.expose.unexpose"
.label=${this.hass.localize( )}
"ui.panel.config.voice_assistants.expose.unexpose" </simple-tooltip>
)} `}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="disable-btn">
${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}
</simple-tooltip>
`}
</div>
</div> </div>
` `
: ""} : ""}
@ -615,26 +566,6 @@ export class VoiceAssistantsExpose extends LitElement {
> >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab> </ha-fab>
${this.narrow && activeFilters?.length
? html`
<ha-button-menu slot="filter-menu" multi>
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.config.devices.picker.filter.filter"
)}
.path=${mdiFilterVariant}
></ha-icon-button>
<mwc-list-item @click=${this._clearFilter}>
${this.hass.localize("ui.components.data-table.filtering_by")}
${activeFilters.join(", ")}
<span class="clear">
${this.hass.localize("ui.common.clear")}
</span>
</mwc-list-item>
</ha-button-menu>
`
: nothing}
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
@ -759,9 +690,7 @@ export class VoiceAssistantsExpose extends LitElement {
} }
private _clearFilter() { private _clearFilter() {
if (this._activeFilters(this._searchParms)) { navigate(window.location.pathname, { replace: true });
navigate(window.location.pathname, { replace: true });
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -3994,12 +3994,7 @@
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?", "confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": { "picker": {
"search": "Search devices", "search": "Search devices",
"filter": { "state": "State"
"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"
}
} }
}, },
"entities": { "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.", "introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.",
"search": "Search entities", "search": "Search entities",
"unnamed_entity": "Unnamed entity", "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": { "status": {
"restored": "Restored", "restored": "Restored",
"available": "Available", "available": "Available",