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:
parent
f5ff55abc5
commit
ae8671af96
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue