Allow to disable config entry (#8442)

This commit is contained in:
Bram Kragten 2021-02-24 17:10:59 +01:00 committed by GitHub
parent 54a2b2534a
commit 7c1fd542da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 322 additions and 39 deletions

View File

@ -9,6 +9,7 @@ export interface ConfigEntry {
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
disabled_by: string | null;
}
export interface ConfigEntryMutableParams {
@ -43,6 +44,27 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
});
export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: null,
});
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string

View File

@ -19,7 +19,7 @@ import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
import {
computeDeviceName,
DeviceRegistryEntry,
@ -264,11 +264,13 @@ export class HaConfigDevicePage extends LitElement {
)}
</p>
</div>
<div class="card-actions" slot="actions">
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>
</div>`
: ""}
`
: html``
}
@ -629,6 +631,41 @@ export class HaConfigDevicePage extends LitElement {
updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
const disabled =
updates.disabled_by === "user" && device.disabled_by !== "user";
if (disabled) {
for (const cnfg_entry of device.config_entries) {
if (
!this.devices.some(
(dvc) =>
dvc.id !== device.id &&
dvc.config_entries.includes(cnfg_entry)
)
) {
const config_entry = this.entries.find(
(entry) => entry.entry_id === cnfg_entry
);
if (
config_entry &&
!config_entry.disabled_by &&
// eslint-disable-next-line no-await-in-loop
(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.confirm_disable_config_entry",
"entry_name",
config_entry.title
),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"),
}))
) {
disableConfigEntry(this.hass, cnfg_entry);
delete updates.disabled_by;
}
}
}
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
if (

View File

@ -1,4 +1,5 @@
import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route";
@ -122,6 +123,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@internalProperty() private _showIgnored = false;
@internalProperty() private _showDisabled = false;
@internalProperty() private _searchParms = new URLSearchParams(
window.location.hash.substring(1)
);
@ -181,18 +184,30 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
(
configEntries: ConfigEntryExtended[],
filter?: string
): [Map<string, ConfigEntryExtended[]>, ConfigEntryExtended[]] => {
): [
Map<string, ConfigEntryExtended[]>,
ConfigEntryExtended[],
Map<string, ConfigEntryExtended[]>
] => {
const filteredConfigEnties = this._filterConfigEntries(
configEntries,
filter
);
const ignored: ConfigEntryExtended[] = [];
const disabled: ConfigEntryExtended[] = [];
for (let i = filteredConfigEnties.length - 1; i >= 0; i--) {
if (filteredConfigEnties[i].source === "ignore") {
ignored.push(filteredConfigEnties.splice(i, 1)[0]);
}
if (filteredConfigEnties[i].disabled_by !== null) {
disabled.push(filteredConfigEnties.splice(i, 1)[0]);
}
return [groupByIntegration(filteredConfigEnties), ignored];
}
return [
groupByIntegration(filteredConfigEnties),
ignored,
groupByIntegration(disabled),
];
}
);
@ -254,6 +269,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
const [
groupedConfigEntries,
ignoredConfigEntries,
disabledConfigEntries,
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
const configEntriesInProgress = this._filterConfigEntriesInProgress(
this._configEntriesInProgress,
@ -289,7 +305,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._toggleShowIgnored}
@action=${this._handleMenuAction}
>
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
@ -305,6 +321,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
this._showDisabled
? "ui.panel.config.integrations.disable.hide_disabled"
: "ui.panel.config.integrations.disable.show_disabled"
)}
</mwc-list-item>
</ha-button-menu>
${!this.narrow
@ -319,6 +342,20 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.search"
)}
></search-input>
${!this._showDisabled && disabledConfigEntries.size
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
"number",
disabledConfigEntries.size
)}
<mwc-button @click=${this._toggleShowDisabled}
>${this.hass.localize(
"ui.panel.config.filtering.show"
)}</mwc-button
>
</div>`
: ""}
</div>
`
: ""}
@ -433,6 +470,21 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
)
: ""}
${this._showDisabled
? Array.from(disabledConfigEntries.entries()).map(
([domain, items]) =>
html`<ha-integration-card
data-domain=${domain}
disabled
.hass=${this.hass}
.domain=${domain}
.items=${items}
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card> `
)
: ""}
${groupedConfigEntries.size
? Array.from(groupedConfigEntries.entries()).map(
([domain, items]) =>
@ -596,10 +648,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
getConfigFlowInProgressCollection(this.hass.connection).refresh();
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._toggleShowIgnored();
break;
case 1:
this._toggleShowDisabled();
break;
}
}
private _toggleShowIgnored() {
this._showIgnored = !this._showIgnored;
}
private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled;
}
private async _removeIgnoredIntegration(ev: Event) {
const entry = (ev.target! as any).entry;
showConfirmationDialog(this, {
@ -767,11 +834,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
margin-left: 16px;
}
.search {
display: flex;
align-items: center;
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
flex: 1;
position: relative;
top: 2px;
}
@ -796,6 +866,32 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
text-overflow: ellipsis;
white-space: normal;
}
.active-filters {
color: var(--primary-text-color);
position: relative;
display: flex;
align-items: center;
padding: 2px 2px 2px 8px;
margin-left: 4px;
font-size: 14px;
}
.active-filters ha-icon {
color: var(--primary-color);
}
.active-filters mwc-button {
margin-left: 8px;
}
.active-filters::before {
background-color: var(--primary-color);
opacity: 0.12;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
}
`,
];
}

View File

@ -9,12 +9,15 @@ import {
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-icon-next";
import {
ConfigEntry,
deleteConfigEntry,
disableConfigEntry,
enableConfigEntry,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
@ -88,6 +91,8 @@ export class HaIntegrationCard extends LitElement {
@property() public selectedConfigEntryId?: string;
@property({ type: Boolean }) public disabled = false;
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
}
@ -109,7 +114,14 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult {
return html`
<ha-card outlined class="group">
<ha-card outlined class="group ${classMap({ disabled: this.disabled })}">
${this.disabled
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled"
)}
</div>`
: ""}
<div class="group-header">
<img
src=${brandsUrl(this.domain, "icon")}
@ -148,7 +160,9 @@ export class HaIntegrationCard extends LitElement {
return html`
<ha-card
outlined
class="single integration"
class="single integration ${classMap({
disabled: Boolean(item.disabled_by),
})}"
.configEntry=${item}
.id=${item.entry_id}
>
@ -159,6 +173,17 @@ export class HaIntegrationCard extends LitElement {
@click=${this._back}
></ha-icon-button>`
: ""}
${item.disabled_by
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
) || item.disabled_by
)}
</div>`
: ""}
<div class="card-content">
<div class="image">
<img
@ -222,11 +247,16 @@ export class HaIntegrationCard extends LitElement {
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
${item.disabled_by === "user"
? html`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</mwc-button>`
: ""}
<mwc-button @click=${this._editEntryName}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
)}
</mwc-button>
${item.domain in integrationsWithPanel
? html`<a
href=${`${
@ -279,13 +309,25 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
</a>
`}
${item.state === "loaded" && item.supports_unload
${!item.disabled_by &&
item.state === "loaded" &&
item.supports_unload
? html`<mwc-list-item @request-selected="${this._handleReload}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</mwc-list-item>`
: ""}
${item.disabled_by === "user"
? html`<mwc-list-item @request-selected="${this._handleEnable}">
${this.hass.localize("ui.common.enable")}
</mwc-list-item>`
: html`<mwc-list-item
class="warning"
@request-selected="${this._handleDisable}"
>
${this.hass.localize("ui.common.disable")}
</mwc-list-item>`}
<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
@ -370,6 +412,24 @@ export class HaIntegrationCard extends LitElement {
);
}
private _handleDisable(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._disableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleEnable(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source && !shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._enableIntegration(
((ev.target as HTMLElement).closest("ha-card") as any).configEntry
);
}
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@ -385,6 +445,48 @@ export class HaIntegrationCard extends LitElement {
});
}
private async _disableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const confirmed = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disable_confirm"
),
});
if (!confirmed) {
return;
}
const result = await disableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: "user" },
});
}
private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const result = await enableConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.enable_restart_confirm"
),
});
}
fireEvent(this, "entry-updated", {
entry: { ...configEntry, disabled_by: null },
});
}
private async _removeIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
@ -397,7 +499,7 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) {
return;
}
deleteConfigEntry(this.hass, entryId).then((result) => {
const result = await deleteConfigEntry(this.hass, entryId);
fireEvent(this, "entry-removed", { entryId });
if (result.require_restart) {
@ -407,13 +509,12 @@ export class HaIntegrationCard extends LitElement {
),
});
}
});
}
private async _reloadIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
reloadConfigEntry(this.hass, entryId).then((result) => {
const result = await reloadConfigEntry(this.hass, entryId);
const locale_key = result.require_restart
? "reload_restart_confirm"
: "reload_confirm";
@ -422,7 +523,6 @@ export class HaIntegrationCard extends LitElement {
`ui.panel.config.integrations.config_entry.${locale_key}`
),
});
});
}
private async _editEntryName(ev) {
@ -461,6 +561,15 @@ export class HaIntegrationCard extends LitElement {
:host(.highlight) ha-card {
border: 1px solid var(--accent-color);
}
.disabled {
--ha-card-border-color: var(--warning-color);
}
.disabled .header {
background: var(--warning-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.card-content {
padding: 16px;
text-align: center;

View File

@ -98,7 +98,7 @@
"disabled_by": {
"user": "User",
"integration": "Integration",
"config_entry": "Config Entry",
"config_entry": "Config entry",
"device": "Device"
}
},
@ -834,7 +834,8 @@
"introduction": "In this view it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"filtering": {
"filtering_by": "Filtering by",
"clear": "Clear"
"clear": "Clear",
"show": "Show"
},
"advanced_mode": {
"hint_enable": "Missing config options? Enable advanced mode on",
@ -1832,7 +1833,7 @@
"disabled_by": {
"user": "User",
"integration": "Integration",
"config_entry": "Config Entry"
"config_entry": "Config entry"
},
"enabled_description": "Disabled devices will not be shown and entities belonging to the device will be disabled and not added to Home Assistant.",
"automation": {
@ -1883,6 +1884,7 @@
"scenes": "Scenes",
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
"disabled": "Disabled",
"data_table": {
"device": "Device",
@ -2040,6 +2042,11 @@
"rename_dialog": "Edit the name of this config entry",
"rename_input_label": "Entry name",
"search": "Search integrations",
"disable": {
"show_disabled": "Show disabled integrations",
"hide_disabled": "Hide disabled integrations",
"disabled_integrations": "{number} disabled"
},
"ignore": {
"ignore": "Ignore",
"confirm_ignore_title": "Ignore discovery of {name}?",
@ -2065,6 +2072,8 @@
"restart_confirm": "Restart Home Assistant to finish removing this integration",
"reload_confirm": "The integration was reloaded",
"reload_restart_confirm": "Restart Home Assistant to finish reloading this integration",
"disable_restart_confirm": "Restart Home Assistant to finish disabling this integration",
"enable_restart_confirm": "Restart Home Assistant to finish enabling this integration",
"manuf": "by {manufacturer}",
"hub": "Connected via",
"firmware": "Firmware: {version}",
@ -2072,7 +2081,17 @@
"device_unavailable": "Device unavailable",
"entity_unavailable": "Entity unavailable",
"area": "In {area}",
"no_area": "No Area"
"no_area": "No Area",
"disable": {
"disabled": "Disabled",
"disabled_cause": "Disabled by {cause}",
"disabled_by": {
"user": "user",
"integration": "integration",
"device": "device"
},
"disable_confirm": "Are you sure you want to disable this config entry? It's devices and entities will be disabled."
}
},
"config_flow": {
"aborted": "Aborted",