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() {
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 {

View File

@ -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<string> | 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<string> | 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<string>();
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<DeviceItem> = {
icon: {
title: "",
type: "icon",
template: (device) =>
device.domains.length
? html`<img
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: device.domains[0],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>`
: "",
},
};
const columns: DataTableColumnContainer<DeviceItem> = {
icon: {
title: "",
type: "icon",
template: (device) =>
device.domains.length
? html`<img
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: device.domains[0],
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>`
: "",
},
};
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}
<div class="secondary">${device.area} | ${device.integration}</div>
`,
};
} 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}
<div class="secondary">${device.area} | ${device.integration}</div>
`,
};
} 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}
<ha-battery-icon
.hass=${this.hass}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
`
: 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`<div
tabindex="0"
style="display:inline-block; position: relative;"
>
<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;
}
}
}
return battery &&
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
? html`
${batteryDomain === "sensor"
? this.hass.formatEntityState(battery)
: nothing}
<ha-battery-icon
.hass=${this.hass}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>
`
: 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 {
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<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>
${this.narrow && activeFilters?.length
? html`<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-check-list-item
left
@request-selected=${this._showDisabledChanged}
.selected=${this._showDisabled}
>
${this.hass!.localize(
"ui.panel.config.devices.picker.filter.show_disabled"
)}
</ha-check-list-item>
</ha-button-menu>
${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="device"
.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-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}
.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>
`;
}
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<RequestSelectedDetail>) {
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 (

View File

@ -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<string> | 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<EntityRow> => ({
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<string> | 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<string>();
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` <hass-loading-screen></hass-loading-screen> `;
}
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"
></ha-integration-overflow-menu>
${this._selectedEntities.length
? html`
<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.hass.localize(
"ui.panel.config.entities.picker.selected",
{ number: this._selectedEntities.length }
)}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button @click=${this._enableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._disableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._hideSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}</mwc-button
>
<mwc-button
@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>
<div class="header-btns" slot="selection-bar">
${!this.narrow
? html`
<mwc-button
@click=${this._enableSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._disableSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._hideSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._removeSelected}
.disabled=${!this._selectedEntities.length}
class="warning"
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}
.path=${mdiFilterVariant}
id="enable-btn"
.disabled=${!this._selectedEntities.length}
@click=${this._enableSelected}
.path=${mdiUndo}
.label=${this.hass.localize("ui.common.enable")}
></ha-icon-button>
${this.narrow && activeFilters?.length
? html`<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-check-list-item
@request-selected=${this._showDisabledChanged}
.selected=${this._showDisabled}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_disabled"
<simple-tooltip animation-delay="0" for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</ha-check-list-item>
<ha-check-list-item
@request-selected=${this._showHiddenChanged}
.selected=${this._showHidden}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_hidden"
</simple-tooltip>
<ha-icon-button
id="disable-btn"
.disabled=${!this._selectedEntities.length}
@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"
)}
</ha-check-list-item>
<ha-check-list-item
@request-selected=${this._showRestoredChanged}
graphic="control"
.selected=${this._showUnavailable}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_unavailable"
</simple-tooltip>
<ha-icon-button
id="hide-btn"
.disabled=${!this._selectedEntities.length}
@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"
)}
</ha-check-list-item>
<ha-check-list-item
@request-selected=${this._showReadOnlyChanged}
graphic="control"
.selected=${this._showReadOnly}
left
>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_readonly"
</simple-tooltip>
<ha-icon-button
class="warning"
id="remove-btn"
.disabled=${!this._selectedEntities.length}
@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"
)}
</ha-check-list-item>
</ha-button-menu>
`}
</simple-tooltip>
`}
</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
? html`<ha-fab
.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 {
super.willUpdate(changedProps);
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) {
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 (

View File

@ -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`<hass-loading-screen></hass-loading-screen>`;
}
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`
<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button @click=${this._exposeSelected}
>${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}</mwc-button
>
<mwc-button @click=${this._unexposeSelected}
>${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}</mwc-button
>
`
: html`
<ha-icon-button
id="enable-btn"
@click=${this._exposeSelected}
.path=${mdiPlusBoxMultiple}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="enable-btn">
${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}
</simple-tooltip>
<ha-icon-button
id="disable-btn"
@click=${this._unexposeSelected}
.path=${mdiCloseBoxMultiple}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}
></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 class="header-btns" slot="selection-bar">
${!this.narrow
? html`
<mwc-button @click=${this._exposeSelected}
>${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}</mwc-button
>
<mwc-button @click=${this._unexposeSelected}
>${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}</mwc-button
>
`
: html`
<ha-icon-button
id="enable-btn"
@click=${this._exposeSelected}
.path=${mdiPlusBoxMultiple}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="enable-btn">
${this.hass.localize(
"ui.panel.config.voice_assistants.expose.expose"
)}
</simple-tooltip>
<ha-icon-button
id="disable-btn"
@click=${this._unexposeSelected}
.path=${mdiCloseBoxMultiple}
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="disable-btn">
${this.hass.localize(
"ui.panel.config.voice_assistants.expose.unexpose"
)}
</simple-tooltip>
`}
</div>
`
: ""}
@ -615,26 +566,6 @@ export class VoiceAssistantsExpose extends LitElement {
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</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>
`;
}
@ -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 {

View File

@ -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",