Group config entries by integration (#5646)

This commit is contained in:
Bram Kragten 2020-04-30 20:38:02 +02:00 committed by GitHub
parent 462c1f94d6
commit 2abfd0392d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 542 additions and 273 deletions

View File

@ -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<string, ConfigEntryExtended[]> => {
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<string, ConfigEntryExtended[]>, 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) {
`
: ""}
<div class="container">
<div
class="container"
@entry-removed=${this._handleRemoved}
@entry-updated=${this._handleUpdated}
>
${this._showIgnored
? configEntries
.filter((item) => item.source === "ignore")
.map(
(item: ConfigEntryExtended) => html`
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html`
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${item.localized_domain_name}
</h2>
<mwc-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
aria-label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${item.localized_domain_name}
</h2>
<mwc-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
aria-label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
>${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}</mwc-button
>
</div>
</ha-card>
`
)
>${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}</mwc-button
>
</div>
</ha-card>
`
)
: ""}
${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`
<ha-card
class="integration"
.configEntry=${item}
.id=${item.entry_id}
>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h1>
${item.localized_domain_name}
</h1>
<h2>
${item.localized_domain_name === item.title
? html`&nbsp;`
: item.title}
</h2>
${devices.length || entities.length
? html`
<div>
${devices.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
devices.length
)}</a
>
`
: ""}
${devices.length && entities.length
? "and"
: ""}
${entities.length
? html`
<a
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
entities.length
)}</a
>
`
: ""}
</div>
`
: ""}
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}</mwc-button
>
`
: ""}
</div>
<paper-menu-button
horizontal-align="right"
vertical-align="top"
vertical-offset="40"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._showSystemOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}</paper-item
>
<paper-item
class="warning"
@tap=${this._removeIntegration}
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</ha-card>
`;
})
${groupedConfigEntries.size
? Array.from(groupedConfigEntries.entries()).map(
([domain, items]) =>
html`<ha-integration-card
data-domain=${domain}
.hass=${this.hass}
.domain=${domain}
.items=${items}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card>`
)
: !this._configEntries.length
? html`
<ha-card>
@ -488,7 +432,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: ""}
${this._filter &&
!configEntriesInProgress.length &&
!configEntries.length &&
!groupedConfigEntries.size &&
this._configEntries.length
? html`
<div class="none-found">
@ -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<ConfigEntryRemovedEvent>) {
this._configEntries = this._configEntries.filter(
(entry) => entry.entry_id !== ev.detail.entryId
);
}
private _handleUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
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;
}
`,
];
}

View File

@ -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`
<ha-card class="group">
<div class="group-header">
<img
src="https://brands.home-assistant.io/${this.domain}/icon.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<h1>
${domainToName(this.hass.localize, this.domain)}
</h1>
</div>
<paper-listbox>
${this.items.map(
(item) =>
html`<paper-item
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body>${item.title}</paper-item-body
><ha-icon-next></ha-icon-next
></paper-item>`
)}
</paper-listbox>
</ha-card>
`;
}
private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
const devices = this._getDevices(item);
const entities = this._getEntities(item);
return html`
<ha-card
class="single integration"
.configEntry=${item}
.id=${item.entry_id}
>
${this.items.length > 1
? html`<paper-icon-button
class="back-btn"
icon="hass:chevron-left"
@click=${this._back}
></paper-icon-button>`
: ""}
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h1>
${item.localized_domain_name}
</h1>
<h2>
${item.localized_domain_name === item.title ? "" : item.title}
</h2>
${devices.length || entities.length
? html`
<div>
${devices.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
devices.length
)}</a
>
`
: ""}
${devices.length && entities.length ? "and" : ""}
${entities.length
? html`
<a
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
entities.length
)}</a
>
`
: ""}
</div>
`
: ""}
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}</mwc-button
>
`
: ""}
</div>
<paper-menu-button
horizontal-align="right"
vertical-align="top"
vertical-offset="40"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._showSystemOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}</paper-item
>
<paper-item class="warning" @tap=${this._removeIntegration}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</ha-card>
`;
}
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;
}
}