From 2abfd0392d3cb32217ef23d39e9cd2e4e0b4c081 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 Apr 2020 20:38:02 +0200 Subject: [PATCH] Group config entries by integration (#5646) --- .../integrations/ha-config-integrations.ts | 411 ++++++------------ .../integrations/ha-integration-card.ts | 404 +++++++++++++++++ 2 files changed, 542 insertions(+), 273 deletions(-) create mode 100644 src/panels/config/integrations/ha-integration-card.ts diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 289a94d620..38dfbcbae4 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -12,7 +12,7 @@ import { } from "lit-element"; import memoizeOne from "memoize-one"; import * as Fuse from "fuse.js"; -import { compare } from "../../../common/string/compare"; +import { caseInsensitiveCompare } from "../../../common/string/compare"; import { computeRTL } from "../../../common/util/compute_rtl"; import { afterNextRender, @@ -25,7 +25,6 @@ import { ConfigEntry, deleteConfigEntry, getConfigEntries, - updateConfigEntry, } from "../../../data/config_entries"; import { DISCOVERY_SOURCES, @@ -44,29 +43,44 @@ import { subscribeEntityRegistry, } from "../../../data/entity_registry"; import { domainToName } from "../../../data/integration"; -import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; -import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; -import { - showAlertDialog, - showConfirmationDialog, - showPromptDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "../../../common/search/search-input"; +import "./ha-integration-card"; +import type { + ConfigEntryRemovedEvent, + ConfigEntryUpdatedEvent, + HaIntegrationCard, +} from "./ha-integration-card"; +import { HASSDomEvent } from "../../../common/dom/fire_event"; interface DataEntryFlowProgressExtended extends DataEntryFlowProgress { localized_title?: string; } -interface ConfigEntryExtended extends ConfigEntry { +export interface ConfigEntryExtended extends ConfigEntry { localized_domain_name?: string; } +const groupByIntegration = ( + entries: ConfigEntryExtended[] +): Map => { + const result = new Map(); + entries.forEach((entry) => { + if (result.has(entry.domain)) { + result.get(entry.domain).push(entry); + } else { + result.set(entry.domain, [entry]); + } + }); + return result; +}; + @customElement("ha-config-integrations") class HaConfigIntegrations extends SubscribeMixin(LitElement) { @property() public hass!: HomeAssistant; @@ -145,6 +159,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } ); + private _filterGroupConfigEntries = memoizeOne( + ( + configEntries: ConfigEntryExtended[], + filter?: string + ): [Map, ConfigEntryExtended[]] => { + const filteredConfigEnties = this._filterConfigEntries( + configEntries, + filter + ); + const ignored: ConfigEntryExtended[] = []; + filteredConfigEnties.forEach((item, index) => { + if (item.source === "ignore") { + ignored.push(filteredConfigEnties.splice(index, 1)[0]); + } + }); + return [groupByIntegration(filteredConfigEnties), ignored]; + } + ); + private _filterConfigEntriesInProgress = memoizeOne( ( configEntriesInProgress: DataEntryFlowProgressExtended[], @@ -185,22 +218,32 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { this._configEntries.length ) { afterNextRender(() => { - const card = this.shadowRoot!.getElementById( - this._searchParms.get("config_entry")! + const entryId = this._searchParms.get("config_entry")!; + const configEntry = this._configEntries.find( + (entry) => entry.entry_id === entryId ); + if (!configEntry) { + return; + } + const card: HaIntegrationCard = this.shadowRoot!.querySelector( + `[data-domain=${configEntry?.domain}]` + ) as HaIntegrationCard; if (card) { - card.scrollIntoView(); + card.scrollIntoView({ + block: "center", + }); card.classList.add("highlight"); + card.selectedConfigEntryId = entryId; } }); } } protected render(): TemplateResult { - const configEntries = this._filterConfigEntries( - this._configEntries, - this._filter - ); + const [ + groupedConfigEntries, + ignoredConfigEntries, + ] = this._filterGroupConfigEntries(this._configEntries, this._filter); const configEntriesInProgress = this._filterConfigEntriesInProgress( this._configEntriesInProgress, this._filter @@ -265,44 +308,46 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ` : ""} -
+
${this._showIgnored - ? configEntries - .filter((item) => item.source === "ignore") - .map( - (item: ConfigEntryExtended) => html` - -
- ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignored" + ? ignoredConfigEntries.map( + (item: ConfigEntryExtended) => html` + +
+ ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignored" + )} +
+
+
+ +
+

+ ${item.localized_domain_name} +

+ -
-
- -
-

- ${item.localized_domain_name} -

- ${this.hass.localize( - "ui.panel.config.integrations.ignore.stop_ignore" - )} -
- - ` - ) + >${this.hass.localize( + "ui.panel.config.integrations.ignore.stop_ignore" + )}
+
+
+ ` + ) : ""} ${configEntriesInProgress.length ? configEntriesInProgress.map( @@ -352,119 +397,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ` ) : ""} - ${configEntries.length - ? configEntries.map((item: ConfigEntryExtended) => { - const devices = this._getDevices(item); - const entities = this._getEntities(item); - return item.source === "ignore" - ? "" - : html` - -
-
- -
-

- ${item.localized_domain_name} -

-

- ${item.localized_domain_name === item.title - ? html` ` - : item.title} -

- ${devices.length || entities.length - ? html` -
- ${devices.length - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.devices", - "count", - devices.length - )} - ` - : ""} - ${devices.length && entities.length - ? "and" - : ""} - ${entities.length - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.entities", - "count", - entities.length - )} - ` - : ""} -
- ` - : ""} -
-
-
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.rename" - )} - ${item.supports_options - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.options" - )} - ` - : ""} -
- - - - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.system_options" - )} - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.delete" - )} - - -
-
- `; - }) + ${groupedConfigEntries.size + ? Array.from(groupedConfigEntries.entries()).map( + ([domain, items]) => + html`` + ) : !this._configEntries.length ? html` @@ -488,7 +432,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { : ""} ${this._filter && !configEntriesInProgress.length && - !configEntries.length && + !groupedConfigEntries.size && this._configEntries.length ? html`
@@ -522,9 +466,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { private _loadConfigEntries() { getConfigEntries(this.hass).then((configEntries) => { this._configEntries = configEntries - .sort((conf1, conf2) => - compare(conf1.domain + conf1.title, conf2.domain + conf2.title) - ) .map( (entry: ConfigEntry): ConfigEntryExtended => ({ ...entry, @@ -533,10 +474,31 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { entry.domain ), }) + ) + .sort((conf1, conf2) => + caseInsensitiveCompare( + conf1.localized_domain_name + conf1.title, + conf2.localized_domain_name + conf2.title + ) ); }); } + private _handleRemoved(ev: HASSDomEvent) { + this._configEntries = this._configEntries.filter( + (entry) => entry.entry_id !== ev.detail.entryId + ); + } + + private _handleUpdated(ev: HASSDomEvent) { + const newEntry = ev.detail.entry; + this._configEntries = this._configEntries!.map((entry) => + entry.entry_id === newEntry.entry_id + ? { ...newEntry, localized_domain_name: entry.localized_domain_name } + : entry + ); + } + private _createFlow() { showConfigFlowDialog(this, { dialogClosedCallback: () => { @@ -612,22 +574,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { }); } - private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] { - if (!this._entityRegistryEntries) { - return []; - } - return this._entityRegistryEntries.filter( - (entity) => entity.config_entry_id === configEntry.entry_id - ); - } - - private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] { - if (!this._deviceRegistryEntries) { - return []; - } - return this._deviceRegistryEntries.filter((device) => - device.config_entries.includes(configEntry.entry_id) - ); + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; } private _onImageLoad(ev) { @@ -638,68 +586,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ev.target.style.visibility = "hidden"; } - private _showOptions(ev) { - showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry); - } - - private _showSystemOptions(ev) { - showConfigEntrySystemOptionsDialog(this, { - entry: ev.target.closest("ha-card").configEntry, - }); - } - - private async _editEntryName(ev) { - const configEntry = ev.target.closest("ha-card").configEntry; - const newName = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), - defaultValue: configEntry.title, - inputLabel: this.hass.localize( - "ui.panel.config.integrations.rename_input_label" - ), - }); - if (newName === null) { - return; - } - const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, { - title: newName, - }); - this._configEntries = this._configEntries!.map((entry) => - entry.entry_id === newEntry.entry_id ? newEntry : entry - ); - } - - private async _removeIntegration(ev) { - const entryId = ev.target.closest("ha-card").configEntry.entry_id; - - const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.delete_confirm" - ), - }); - - if (!confirmed) { - return; - } - - deleteConfigEntry(this.hass, entryId).then((result) => { - this._configEntries = this._configEntries.filter( - (entry) => entry.entry_id !== entryId - ); - - if (result.require_restart) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.restart_confirm" - ), - }); - } - }); - } - - private _handleSearchChange(ev: CustomEvent) { - this._filter = ev.detail.value; - } - static get styles(): CSSResult[] { return [ haStyle, @@ -717,9 +603,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { flex-direction: column; justify-content: space-between; } - ha-card.highlight { - border: 1px solid var(--accent-color); - } .discovered { border: 1px solid var(--primary-color); } @@ -742,21 +625,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { padding: 16px; text-align: center; } - ha-card.integration .card-content { - padding-bottom: 3px; - } - .card-actions { - border-top: none; - display: flex; - justify-content: space-between; - align-items: center; - padding-right: 5px; - } - .helper { - display: inline-block; - height: 100%; - vertical-align: middle; - } .image { display: flex; align-items: center; @@ -787,11 +655,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { top: 2px; } img { - max-height: 60px; + max-height: 100%; max-width: 90%; } - a { - color: var(--primary-color); + .none-found { + margin: auto; + text-align: center; } h1 { margin-bottom: 0; @@ -821,10 +690,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { left: 24px; right: auto; } - paper-menu-button { - color: var(--secondary-text-color); - padding: 0; - } `, ]; } diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts new file mode 100644 index 0000000000..2b08ff4de3 --- /dev/null +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -0,0 +1,404 @@ +import { + customElement, + LitElement, + property, + html, + CSSResult, + css, + TemplateResult, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +import { ConfigEntryExtended } from "./ha-config-integrations"; +import { domainToName } from "../../../data/integration"; +import { + ConfigEntry, + updateConfigEntry, + deleteConfigEntry, +} from "../../../data/config_entries"; +import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { DeviceRegistryEntry } from "../../../data/device_registry"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; +import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; +import { + showPromptDialog, + showConfirmationDialog, + showAlertDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../resources/styles"; +import "../../../components/ha-icon-next"; +import { fireEvent } from "../../../common/dom/fire_event"; + +export interface ConfigEntryUpdatedEvent { + entry: ConfigEntry; +} + +export interface ConfigEntryRemovedEvent { + entryId: string; +} + +declare global { + // for fire event + interface HASSDomEvents { + "entry-updated": ConfigEntryUpdatedEvent; + "entry-removed": ConfigEntryRemovedEvent; + } +} + +@customElement("ha-integration-card") +export class HaIntegrationCard extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public domain!: string; + + @property() public items!: ConfigEntryExtended[]; + + @property() public entityRegistryEntries!: EntityRegistryEntry[]; + + @property() public deviceRegistryEntries!: DeviceRegistryEntry[]; + + @property() public selectedConfigEntryId?: string; + + protected render(): TemplateResult { + if (this.items.length === 1) { + return this._renderSingleEntry(this.items[0]); + } + if (this.selectedConfigEntryId) { + const configEntry = this.items.find( + (entry) => entry.entry_id === this.selectedConfigEntryId + ); + if (configEntry) { + return this._renderSingleEntry(configEntry); + } + } + return this._renderGroupedIntegration(); + } + + private _renderGroupedIntegration(): TemplateResult { + return html` + +
+ +

+ ${domainToName(this.hass.localize, this.domain)} +

+
+ + ${this.items.map( + (item) => + html`${item.title}` + )} + +
+ `; + } + + private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult { + const devices = this._getDevices(item); + const entities = this._getEntities(item); + return html` + + ${this.items.length > 1 + ? html`` + : ""} +
+
+ +
+

+ ${item.localized_domain_name} +

+

+ ${item.localized_domain_name === item.title ? "" : item.title} +

+ ${devices.length || entities.length + ? html` + + ` + : ""} +
+
+
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.rename" + )} + ${item.supports_options + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.options" + )} + ` + : ""} +
+ + + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.system_options" + )} + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.delete" + )} + + +
+
+ `; + } + + private _selectConfigEntry(ev: Event) { + this.selectedConfigEntryId = (ev.currentTarget as any).entryId; + } + + private _back() { + this.selectedConfigEntryId = undefined; + this.classList.remove("highlight"); + } + + private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] { + if (!this.entityRegistryEntries) { + return []; + } + return this.entityRegistryEntries.filter( + (entity) => entity.config_entry_id === configEntry.entry_id + ); + } + + private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] { + if (!this.deviceRegistryEntries) { + return []; + } + return this.deviceRegistryEntries.filter((device) => + device.config_entries.includes(configEntry.entry_id) + ); + } + + private _onImageLoad(ev) { + ev.target.style.visibility = "initial"; + } + + private _onImageError(ev) { + ev.target.style.visibility = "hidden"; + } + + private _showOptions(ev) { + showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry); + } + + private _showSystemOptions(ev) { + showConfigEntrySystemOptionsDialog(this, { + entry: ev.target.closest("ha-card").configEntry, + }); + } + + private async _editEntryName(ev) { + const configEntry = ev.target.closest("ha-card").configEntry; + const newName = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), + defaultValue: configEntry.title, + inputLabel: this.hass.localize( + "ui.panel.config.integrations.rename_input_label" + ), + }); + if (newName === null) { + return; + } + const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, { + title: newName, + }); + fireEvent(this, "entry-updated", { entry: newEntry }); + } + + private async _removeIntegration(ev) { + const entryId = ev.target.closest("ha-card").configEntry.entry_id; + + const confirmed = await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm" + ), + }); + + if (!confirmed) { + return; + } + deleteConfigEntry(this.hass, entryId).then((result) => { + fireEvent(this, "entry-removed", { entryId }); + + if (result.require_restart) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.restart_confirm" + ), + }); + } + }); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + :host { + max-width: 500px; + } + ha-card { + display: flex; + flex-direction: column; + height: 100%; + } + ha-card.single { + justify-content: space-between; + } + :host(.highlight) ha-card { + border: 1px solid var(--accent-color); + } + .card-content { + padding: 16px; + text-align: center; + } + ha-card.integration .card-content { + padding-bottom: 3px; + } + .card-actions { + border-top: none; + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 5px; + } + .group-header { + display: flex; + align-items: center; + height: 40px; + padding: 16px 16px 8px 16px; + vertical-align: middle; + } + .group-header h1 { + margin: 0; + } + .group-header img { + margin-right: 8px; + } + .image { + display: flex; + align-items: center; + justify-content: center; + height: 60px; + margin-bottom: 16px; + vertical-align: middle; + } + img { + max-height: 100%; + max-width: 90%; + } + + .none-found { + margin: auto; + text-align: center; + } + a { + color: var(--primary-color); + } + h1 { + margin-bottom: 0; + } + h2 { + margin-top: 0; + min-height: 24px; + } + paper-menu-button { + color: var(--secondary-text-color); + padding: 0; + } + @media (min-width: 563px) { + paper-listbox { + max-height: 150px; + overflow: auto; + } + } + paper-item { + cursor: pointer; + min-height: 35px; + } + .back-btn { + position: absolute; + background: #ffffffe0; + border-radius: 50%; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-integration-card": HaIntegrationCard; + } +}