Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
eb95d617a1 Refactor integration page to build device/sub-entry tree in parent 2026-04-02 15:36:08 +02:00
5 changed files with 248 additions and 237 deletions

View File

@@ -38,14 +38,6 @@ export interface HASSDomEvent<T> extends Event {
detail: T;
}
export type HASSDomTargetEvent<T extends EventTarget> = Event & {
target: T;
};
export type HASSDomCurrentTargetEvent<T extends EventTarget> = Event & {
currentTarget: T;
};
/**
* Dispatches a custom event with an optional detail value.
*

View File

@@ -17,10 +17,6 @@ import memoizeOne from "memoize-one";
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
@@ -107,7 +103,6 @@ export interface DataTableRowData {
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@@ -171,10 +166,6 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
@query("lit-virtualizer") private _scroller?: HTMLElement;
@state() private _collapsedGroups: string[] = [];
@state() private _lastSelectedRowId: string | null = null;
@@ -189,8 +180,6 @@ export class HaDataTable extends LitElement {
private _lastUpdate = 0;
private _didAutoFocusScroller = false;
// @ts-ignore
@restoreScroll(".scroller") private _savedScrollPos?: number;
@@ -253,26 +242,16 @@ export class HaDataTable extends LitElement {
this.updateComplete.then(() => this._calcTableHeight());
}
protected updated(changedProps: PropertyValues) {
if (!this._headerRow) {
protected updated() {
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
if (!header) {
return;
}
if (this._headerRow.scrollWidth > this._headerRow.clientWidth) {
this.style.setProperty(
"--table-row-width",
`${this._headerRow.scrollWidth}px`
);
if (header.scrollWidth > header.clientWidth) {
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
} else {
this.style.removeProperty("--table-row-width");
}
this._focusTableOnLoad();
// Refocus on toggle checkbox changes
if (changedProps.has("selectable")) {
this._focusScroller();
}
}
public willUpdate(properties: PropertyValues) {
@@ -538,7 +517,6 @@ export class HaDataTable extends LitElement {
<lit-virtualizer
scroller
class="mdc-data-table__content scroller ha-scrollbar"
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
@@ -851,10 +829,8 @@ export class HaDataTable extends LitElement {
): Promise<DataTableRowData[]> => filterData(data, columns, filter)
);
private _handleHeaderClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { columnId: string }>
) {
const columnId = ev.currentTarget.columnId;
private _handleHeaderClick(ev: Event) {
const columnId = (ev.currentTarget as any).columnId;
if (!this.columns[columnId].sortable) {
return;
}
@@ -872,12 +848,11 @@ export class HaDataTable extends LitElement {
column: columnId,
direction: this.sortDirection,
});
this._focusScroller();
}
private _handleHeaderRowCheckboxClick(ev: HASSDomTargetEvent<HaCheckbox>) {
if (ev.target.checked) {
private _handleHeaderRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox;
if (checkbox.checked) {
this.selectAll();
} else {
this._checkedRows = [];
@@ -886,10 +861,9 @@ export class HaDataTable extends LitElement {
this._lastSelectedRowId = null;
}
private _handleRowCheckboxClicked = (
ev: HASSDomCurrentTargetEvent<HaCheckbox & { rowId: string }>
) => {
const rowId = ev.currentTarget.rowId;
private _handleRowCheckboxClicked = (ev: Event) => {
const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId;
const groupedData = this._groupData(
this._filteredData,
@@ -926,7 +900,7 @@ export class HaDataTable extends LitElement {
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
];
}
} else if (!ev.currentTarget.checked) {
} else if (!checkbox.checked) {
if (!this._checkedRows.includes(rowId)) {
this._checkedRows = [...this._checkedRows, rowId];
}
@@ -964,9 +938,7 @@ export class HaDataTable extends LitElement {
return checkedRows;
}
private _handleRowClick = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { rowId: string }>
) => {
private _handleRowClick = (ev: Event) => {
if (
ev
.composedPath()
@@ -982,13 +954,14 @@ export class HaDataTable extends LitElement {
) {
return;
}
const rowId = ev.currentTarget.rowId;
const rowId = (ev.currentTarget as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
};
private _setTitle(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
if (ev.currentTarget.scrollWidth > ev.currentTarget.offsetWidth) {
ev.currentTarget.setAttribute("title", ev.currentTarget.innerText);
private _setTitle(ev: Event) {
const target = ev.currentTarget as HTMLElement;
if (target.scrollWidth > target.offsetWidth) {
target.setAttribute("title", target.innerText);
}
}
@@ -1010,27 +983,6 @@ export class HaDataTable extends LitElement {
this._debounceSearch((ev.target as HTMLInputElement).value);
}
private _focusTableOnLoad() {
if (
this._didAutoFocusScroller ||
this.autoHeight ||
(document.activeElement &&
!AUTO_FOCUS_ALLOWED_ACTIVE_TAGS.includes(
document.activeElement.tagName
))
) {
return;
}
this._focusScroller();
}
private _focusScroller(): void {
this._scroller?.focus({
preventScroll: true,
});
}
private async _calcTableHeight() {
if (this.autoHeight) {
return;
@@ -1040,27 +992,23 @@ export class HaDataTable extends LitElement {
}
@eventOptions({ passive: true })
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
this._savedScrollPos = e.target.scrollTop;
private _saveScrollPos(e: Event) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
if (this._headerRow) {
this._headerRow.scrollLeft = e.target.scrollLeft;
}
this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = (
e.target as HTMLDivElement
).scrollLeft;
}
@eventOptions({ passive: true })
private _scrollContent(e: HASSDomTargetEvent<HTMLDivElement>) {
if (!this._scroller) {
return;
}
this._scroller.scrollLeft = e.target.scrollLeft;
private _scrollContent(e: Event) {
this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = (
e.target as HTMLDivElement
).scrollLeft;
}
private _collapseGroup = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
const groupName = ev.currentTarget.group;
private _collapseGroup = (ev: Event) => {
const groupName = (ev.currentTarget as any).group;
if (this._collapsedGroups.includes(groupName)) {
this._collapsedGroups = this._collapsedGroups.filter(
(grp) => grp !== groupName
@@ -1483,11 +1431,6 @@ export class HaDataTable extends LitElement {
contain: size layout !important;
overscroll-behavior: contain;
}
lit-virtualizer:focus,
lit-virtualizer:focus-visible {
outline: none;
}
`,
];
}

View File

@@ -18,13 +18,11 @@ import {
mdiStopCircleOutline,
mdiWrench,
} from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -34,22 +32,16 @@ import {
fetchApplicationCredentialsConfigEntry,
} from "../../../data/application_credential";
import { getSignedPath } from "../../../data/auth";
import type {
ConfigEntry,
DisableConfigEntryResult,
SubEntry,
} from "../../../data/config_entries";
import type { DisableConfigEntryResult } from "../../../data/config_entries";
import {
deleteConfigEntry,
disableConfigEntry,
enableConfigEntry,
ERROR_STATES,
getSubEntries,
RECOVERABLE_STATES,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -74,6 +66,7 @@ import {
showPromptDialog,
} from "../../lovelace/custom-card-helpers";
import "./ha-config-entry-device-row";
import type { ConfigEntryData } from "./ha-config-integration-page";
import { renderConfigEntryError } from "./ha-config-integration-page";
import "./ha-config-sub-entry-row";
@@ -89,22 +82,14 @@ export class HaConfigEntryRow extends LitElement {
@property({ attribute: false }) public entities!: EntityRegistryEntry[];
@property({ attribute: false }) public entry!: ConfigEntry;
@property({ attribute: false }) public data!: ConfigEntryData;
@state() private _expanded = true;
@state() private _devicesExpanded = true;
@state() private _subEntries?: SubEntry[];
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("entry")) {
this._fetchSubEntries();
}
}
protected render() {
const item = this.entry;
const item = this.data.entry;
let stateText: Parameters<typeof this.hass.localize> | undefined;
let stateTextExtra: TemplateResult | string | undefined;
@@ -127,15 +112,17 @@ export class HaConfigEntryRow extends LitElement {
stateTextExtra = renderConfigEntryError(this.hass, item);
}
const devices = this._getDevices();
const services = this._getServices();
const entities = this._getEntities();
const ownDevices = [...this.data.devices, ...this.data.services];
const ownDevices = [...devices, ...services].filter(
(device) =>
!device.config_entries_subentries[item.entry_id].length ||
device.config_entries_subentries[item.entry_id][0] === null
);
const allDevices = [
...this.data.devices,
...this.data.subEntries.flatMap((s) => s.devices),
];
const allServices = [
...this.data.services,
...this.data.subEntries.flatMap((s) => s.services),
];
const statusLine: (TemplateResult | string)[] = [];
@@ -157,7 +144,7 @@ export class HaConfigEntryRow extends LitElement {
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
)}.`);
}
} else if (!devices.length && !services.length && entities.length) {
} else if (!allDevices.length && !allServices.length && entities.length) {
statusLine.push(
html`<a
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
@@ -171,7 +158,7 @@ export class HaConfigEntryRow extends LitElement {
const configPanel = this._configPanel(item.domain, this.hass.panels);
const subEntries = this._subEntries || [];
const subEntries = this.data.subEntries;
return html`<ha-md-list>
<ha-md-list-item
@@ -245,29 +232,29 @@ export class HaConfigEntryRow extends LitElement {
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${devices.length
${allDevices.length
? html`
<a
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
href=${allDevices.length === 1
? `/config/devices/device/${allDevices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-dropdown-item value="devices">
<ha-svg-icon .path=${mdiDevices} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
{ count: allDevices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
</a>
`
: nothing}
${services.length
${allServices.length
? html`
<a
href=${services.length === 1
? `/config/devices/device/${services[0].id}`
href=${allServices.length === 1
? `/config/devices/device/${allServices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-dropdown-item value="services">
@@ -277,7 +264,7 @@ export class HaConfigEntryRow extends LitElement {
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
{ count: allServices.length }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
@@ -449,7 +436,7 @@ export class HaConfigEntryRow extends LitElement {
</ha-md-list>`
: nothing}
${subEntries.map(
(subEntry) => html`
(subEntryData) => html`
<ha-config-sub-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
@@ -457,7 +444,7 @@ export class HaConfigEntryRow extends LitElement {
.diagnosticHandler=${this.diagnosticHandler}
.entities=${this.entities}
.entry=${item}
.subEntry=${subEntry}
.data=${subEntryData}
data-entry-id=${item.entry_id}
></ha-config-sub-entry-row>
`
@@ -478,18 +465,6 @@ export class HaConfigEntryRow extends LitElement {
</ha-md-list>`;
}
private async _fetchSubEntries() {
this._subEntries = this.entry.num_subentries
? (await getSubEntries(this.hass, this.entry.entry_id)).sort((a, b) =>
caseInsensitiveStringCompare(
a.title,
b.title,
this.hass.locale.language
)
)
: undefined;
}
private _configPanel = memoizeOne(
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
Object.values(panels).find(
@@ -499,39 +474,9 @@ export class HaConfigEntryRow extends LitElement {
private _getEntities = (): EntityRegistryEntry[] =>
this.entities.filter(
(entity) => entity.config_entry_id === this.entry.entry_id
(entity) => entity.config_entry_id === this.data.entry.entry_id
);
private _getDevices = (): DeviceRegistryEntry[] =>
Object.values(this.hass.devices)
.filter(
(device) =>
device.config_entries.includes(this.entry.entry_id) &&
device.entry_type !== "service"
)
.sort((a, b) =>
caseInsensitiveStringCompare(
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
this.hass.locale.language
)
);
private _getServices = (): DeviceRegistryEntry[] =>
Object.values(this.hass.devices)
.filter(
(device) =>
device.config_entries.includes(this.entry.entry_id) &&
device.entry_type === "service"
)
.sort((a, b) =>
caseInsensitiveStringCompare(
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
this.hass.locale.language
)
);
private _toggleExpand() {
this._expanded = !this._expanded;
}
@@ -583,7 +528,7 @@ export class HaConfigEntryRow extends LitElement {
};
private _showOptions() {
showOptionsFlowDialog(this, this.entry, { manifest: this.manifest });
showOptionsFlowDialog(this, this.data.entry, { manifest: this.manifest });
}
// Return an application credentials id for this config entry to prompt the
@@ -648,7 +593,7 @@ export class HaConfigEntryRow extends LitElement {
}
private _handleReload = async () => {
const result = await reloadConfigEntry(this.hass, this.entry.entry_id);
const result = await reloadConfigEntry(this.hass, this.data.entry.entry_id);
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
@@ -661,16 +606,19 @@ export class HaConfigEntryRow extends LitElement {
private _handleReconfigure = async () => {
showConfigFlowDialog(this, {
startFlowHandler: this.entry.domain,
startFlowHandler: this.data.entry.domain,
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, this.entry.domain),
entryId: this.entry.entry_id,
manifest: await fetchIntegrationManifest(
this.hass,
this.data.entry.domain
),
entryId: this.data.entry.entry_id,
navigateToResult: true,
});
};
private _handleCopy = async () => {
await copyToClipboard(this.entry.entry_id);
await copyToClipboard(this.data.entry.entry_id);
showToast(this, {
message:
this.hass?.localize("ui.common.copied_clipboard") ||
@@ -681,7 +629,7 @@ export class HaConfigEntryRow extends LitElement {
private _handleRename = async () => {
const newName = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
defaultValue: this.entry.title,
defaultValue: this.data.entry.title,
inputLabel: this.hass.localize(
"ui.panel.config.integrations.rename_input_label"
),
@@ -689,7 +637,7 @@ export class HaConfigEntryRow extends LitElement {
if (newName === null) {
return;
}
await updateConfigEntry(this.hass, this.entry.entry_id, {
await updateConfigEntry(this.hass, this.data.entry.entry_id, {
title: newName,
});
};
@@ -705,12 +653,12 @@ export class HaConfigEntryRow extends LitElement {
}
private _handleDisable = async () => {
const entryId = this.entry.entry_id;
const entryId = this.data.entry.entry_id;
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_confirm_title",
{ title: this.entry.title }
{ title: this.data.entry.title }
),
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_confirm_text"
@@ -745,7 +693,7 @@ export class HaConfigEntryRow extends LitElement {
};
private _handleEnable = async () => {
const entryId = this.entry.entry_id;
const entryId = this.data.entry.entry_id;
let result: DisableConfigEntryResult;
try {
@@ -770,7 +718,7 @@ export class HaConfigEntryRow extends LitElement {
};
private _handleDelete = async () => {
const entryId = this.entry.entry_id;
const entryId = this.data.entry.entry_id;
const applicationCredentialsId =
await this._applicationCredentialForRemove(entryId);
@@ -778,7 +726,7 @@ export class HaConfigEntryRow extends LitElement {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_title",
{ title: this.entry.title }
{ title: this.data.entry.title }
),
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_text"
@@ -807,14 +755,14 @@ export class HaConfigEntryRow extends LitElement {
private _handleSystemOptions = () => {
showConfigEntrySystemOptionsDialog(this, {
entry: this.entry,
entry: this.data.entry,
manifest: this.manifest,
});
};
private _addSubEntry = (flowType: string) => {
showSubConfigFlowDialog(this, this.entry, flowType, {
startFlowHandler: this.entry.entry_id,
showSubConfigFlowDialog(this, this.data.entry, flowType, {
startFlowHandler: this.data.entry.entry_id,
});
};

View File

@@ -29,8 +29,12 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import { getSignedPath } from "../../../data/auth";
import type { ConfigEntry } from "../../../data/config_entries";
import { ERROR_STATES, getConfigEntries } from "../../../data/config_entries";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import {
ERROR_STATES,
getConfigEntries,
getSubEntries,
} from "../../../data/config_entries";
import { ATTENTION_SOURCES } from "../../../data/config_flow";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
@@ -69,6 +73,19 @@ import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import { showPickConfigEntryDialog } from "./show-pick-config-entry-dialog";
export interface SubEntryData {
subEntry: SubEntry;
devices: DeviceRegistryEntry[];
services: DeviceRegistryEntry[];
}
export interface ConfigEntryData {
entry: ConfigEntry;
devices: DeviceRegistryEntry[];
services: DeviceRegistryEntry[];
subEntries: SubEntryData[];
}
export const renderConfigEntryError = (
hass: HomeAssistant,
entry: ConfigEntry
@@ -132,6 +149,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
window.location.hash.substring(1)
);
@state() private _subEntries: Record<string, SubEntry[]> = {};
@state() private _domainEntities: Record<string, string[]> = {};
@queryAll("ha-config-entry-row")
@@ -190,10 +209,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.hass.loadBackendTranslation("config", [this.domain]);
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
this._extraConfigEntries = undefined;
this._subEntries = {};
this._fetchManifest();
this._fetchDiagnostics();
this._fetchEntitySources();
}
if (
changedProperties.has("configEntries") ||
changedProperties.has("_extraConfigEntries")
) {
const entries = this._domainConfigEntries(
this.domain,
this._extraConfigEntries || this.configEntries
);
this._fetchAllSubEntries(entries);
}
}
private async _fetchEntitySources() {
@@ -280,6 +310,19 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
);
});
const normalData = this._buildEntryData(
normalEntries,
this.hass.devices,
this._subEntries,
this.hass.locale.language
);
const attentionData = this._buildEntryData(
attentionEntries,
this.hass.devices,
this._subEntries,
this.hass.locale.language
);
const devicesRegs = this._getDevices(configEntries, this.hass.devices);
const entities = this._getEntities(configEntries, this._entities);
let numberOfEntities = entities.length;
@@ -632,7 +675,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</div>
`
: nothing}
${attentionFlows.length || attentionEntries.length
${attentionFlows.length || attentionData.length
? html`
<div class="section">
<h3 class="section-header">
@@ -672,8 +715,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
})}
</ha-md-list>`
: nothing}
${attentionEntries.map(
(item) =>
${attentionData.map(
(data) =>
html`<ha-config-entry-row
class="attention"
.hass=${this.hass}
@@ -681,8 +724,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.manifest=${this._manifest}
.diagnosticHandler=${this._diagnosticHandler}
.entities=${this._entities}
.entry=${item}
data-entry-id=${item.entry_id}
.data=${data}
data-entry-id=${data.entry.entry_id}
></ha-config-entry-row>`
)}
</div>
@@ -699,7 +742,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`ui.panel.config.integrations.integration_page.entries`
)}
</h3>
${normalEntries.length === 0
${normalData.length === 0
? html`<div class="card-content no-entries">
${this._manifest &&
!this._manifest.config_flow &&
@@ -714,16 +757,16 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
)}
</div>`
: html`
${normalEntries.map(
(item) =>
${normalData.map(
(data) =>
html`<ha-config-entry-row
.hass=${this.hass}
.narrow=${this.narrow}
.manifest=${this._manifest}
.diagnosticHandler=${this._diagnosticHandler}
.entities=${this._entities}
.entry=${item}
data-entry-id=${item.entry_id}
.data=${data}
data-entry-id=${data.entry.entry_id}
></ha-config-entry-row>`
)}
`}
@@ -797,6 +840,102 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
}
private async _fetchAllSubEntries(entries: ConfigEntry[]) {
const entriesWithSubs = entries.filter((e) => e.num_subentries > 0);
if (!entriesWithSubs.length) {
this._subEntries = {};
return;
}
const results: Record<string, SubEntry[]> = {};
await Promise.all(
entriesWithSubs.map(async (entry) => {
try {
results[entry.entry_id] = (
await getSubEntries(this.hass, entry.entry_id)
).sort((a, b) =>
caseInsensitiveStringCompare(
a.title,
b.title,
this.hass.locale.language
)
);
} catch {
results[entry.entry_id] = [];
}
})
);
this._subEntries = results;
}
private _buildEntryData = memoizeOne(
(
entries: ConfigEntry[],
devices: HomeAssistant["devices"],
subEntries: Record<string, SubEntry[]>,
language: string
): ConfigEntryData[] => {
const sortDevices = (a: DeviceRegistryEntry, b: DeviceRegistryEntry) =>
caseInsensitiveStringCompare(
a.name_by_user || a.name || "",
b.name_by_user || b.name || "",
language
);
return entries.map((entry) => {
const allDevices = Object.values(devices).filter((device) =>
device.config_entries.includes(entry.entry_id)
);
const entrySubs = (subEntries[entry.entry_id] || []).map(
(sub): SubEntryData => {
const subDevs = allDevices
.filter(
(d) =>
d.config_entries_subentries[entry.entry_id]?.includes(
sub.subentry_id
) && d.entry_type !== "service"
)
.sort(sortDevices);
const subServices = allDevices
.filter(
(d) =>
d.config_entries_subentries[entry.entry_id]?.includes(
sub.subentry_id
) && d.entry_type === "service"
)
.sort(sortDevices);
return { subEntry: sub, devices: subDevs, services: subServices };
}
);
const ownDevices = allDevices
.filter(
(d) =>
(!d.config_entries_subentries[entry.entry_id]?.length ||
d.config_entries_subentries[entry.entry_id][0] === null) &&
d.entry_type !== "service"
)
.sort(sortDevices);
const ownServices = allDevices
.filter(
(d) =>
(!d.config_entries_subentries[entry.entry_id]?.length ||
d.config_entries_subentries[entry.entry_id][0] === null) &&
d.entry_type === "service"
)
.sort(sortDevices);
return {
entry,
devices: ownDevices,
services: ownServices,
subEntries: entrySubs,
};
});
}
);
private async _handleEnableDebugLogging() {
const integration = this.domain;
await setIntegrationLogLevel(

View File

@@ -13,12 +13,12 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import type { ConfigEntry } from "../../../data/config_entries";
import { deleteSubEntry, updateSubEntry } from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { DiagnosticInfo } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { IntegrationManifest } from "../../../data/integration";
import type { SubEntryData } from "./ha-config-integration-page";
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../../../types";
import {
@@ -41,16 +41,16 @@ class HaConfigSubEntryRow extends LitElement {
@property({ attribute: false }) public entry!: ConfigEntry;
@property({ attribute: false }) public subEntry!: SubEntry;
@property({ attribute: false }) public data!: SubEntryData;
@state() private _expanded = true;
protected render() {
const subEntry = this.subEntry;
const subEntry = this.data.subEntry;
const configEntry = this.entry;
const devices = this._getDevices();
const services = this._getServices();
const devices = this.data.devices;
const services = this.data.services;
const entities = this._getEntities();
return html`<ha-md-list>
@@ -202,30 +202,19 @@ class HaConfigSubEntryRow extends LitElement {
private _getEntities = (): EntityRegistryEntry[] =>
this.entities.filter(
(entity) => entity.config_subentry_id === this.subEntry.subentry_id
);
private _getDevices = (): DeviceRegistryEntry[] =>
Object.values(this.hass.devices).filter(
(device) =>
device.config_entries_subentries[this.entry.entry_id]?.includes(
this.subEntry.subentry_id
) && device.entry_type !== "service"
);
private _getServices = (): DeviceRegistryEntry[] =>
Object.values(this.hass.devices).filter(
(device) =>
device.config_entries_subentries[this.entry.entry_id]?.includes(
this.subEntry.subentry_id
) && device.entry_type === "service"
(entity) => entity.config_subentry_id === this.data.subEntry.subentry_id
);
private async _handleReconfigureSub(): Promise<void> {
showSubConfigFlowDialog(this, this.entry, this.subEntry.subentry_type, {
startFlowHandler: this.entry.entry_id,
subEntryId: this.subEntry.subentry_id,
});
showSubConfigFlowDialog(
this,
this.entry,
this.data.subEntry.subentry_type,
{
startFlowHandler: this.entry.entry_id,
subEntryId: this.data.subEntry.subentry_id,
}
);
}
private _handleMenuAction = (ev: CustomEvent) => {
@@ -244,7 +233,7 @@ class HaConfigSubEntryRow extends LitElement {
private _handleRenameSub = async (): Promise<void> => {
const newName = await showPromptDialog(this, {
title: this.hass.localize("ui.common.rename"),
defaultValue: this.subEntry.title,
defaultValue: this.data.subEntry.title,
inputLabel: this.hass.localize(
"ui.panel.config.integrations.rename_input_label"
),
@@ -255,7 +244,7 @@ class HaConfigSubEntryRow extends LitElement {
await updateSubEntry(
this.hass,
this.entry.entry_id,
this.subEntry.subentry_id,
this.data.subEntry.subentry_id,
{ title: newName }
);
};
@@ -264,7 +253,7 @@ class HaConfigSubEntryRow extends LitElement {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_title",
{ title: this.subEntry.title }
{ title: this.data.subEntry.title }
),
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_text"
@@ -280,7 +269,7 @@ class HaConfigSubEntryRow extends LitElement {
await deleteSubEntry(
this.hass,
this.entry.entry_id,
this.subEntry.subentry_id
this.data.subEntry.subentry_id
);
};