Show config entry state on card (#8911)

This commit is contained in:
Paulus Schoutsen 2021-04-16 04:16:59 -07:00 committed by GitHub
parent 2dcd0d2b0a
commit 60fe48d355
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1233 additions and 713 deletions

View File

@ -0,0 +1,299 @@
import {
customElement,
html,
css,
internalProperty,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import "../../../src/panels/config/integrations/ha-integration-card";
import "../../../src/panels/config/integrations/ha-ignored-config-entry-card";
import "../../../src/panels/config/integrations/ha-config-flow-card";
import type {
ConfigEntryExtended,
DataEntryFlowProgressExtended,
} from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit-html/directives/class-map";
const createConfigEntry = (
title: string,
override: Partial<ConfigEntryExtended> = {}
): ConfigEntryExtended => ({
entry_id: title,
domain: "esphome",
localized_domain_name: "ESPHome",
title,
source: "zeroconf",
state: "loaded",
connection_class: "local_push",
supports_options: false,
supports_unload: true,
disabled_by: null,
...override,
});
const createManifest = (
isCustom: boolean,
isCloud: boolean
): IntegrationManifest => ({
name: "ESPHome",
domain: "esphome",
is_built_in: !isCustom,
config_flow: false,
documentation: "https://www.home-assistant.io/integrations/esphome/",
iot_class: isCloud ? "cloud_polling" : "local_polling",
});
const loadedEntry = createConfigEntry("Loaded");
const nameAsDomainEntry = createConfigEntry("ESPHome");
const longNameEntry = createConfigEntry(
"Entry with a super long name that is going to the next line"
);
const configPanelEntry = createConfigEntry("Config Panel", {
domain: "mqtt",
localized_domain_name: "MQTT",
});
const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error",
});
const migrationErrorEntry = createConfigEntry("Migration Error", {
state: "migration_error",
});
const setupRetryEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
});
const failedUnloadEntry = createConfigEntry("Failed Unload", {
state: "failed_unload",
});
const notLoadedEntry = createConfigEntry("Not Loaded", { state: "not_loaded" });
const disabledEntry = createConfigEntry("Disabled", {
state: "not_loaded",
disabled_by: "user",
});
const disabledFailedUnloadEntry = createConfigEntry(
"Disabled - Failed Unload",
{
state: "failed_unload",
disabled_by: "user",
}
);
const configFlows: DataEntryFlowProgressExtended[] = [
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "roku",
context: {
source: "ssdp",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Roku: Living room Roku",
},
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "hue",
context: {
source: "reauth",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Philips Hue",
},
];
const configEntries: Array<{
items: ConfigEntryExtended[];
is_custom?: boolean;
disabled?: boolean;
highlight?: string;
}> = [
{ items: [loadedEntry] },
{ items: [configPanelEntry] },
{ items: [optionsFlowEntry] },
{ items: [nameAsDomainEntry] },
{ items: [longNameEntry] },
{ items: [setupErrorEntry] },
{ items: [migrationErrorEntry] },
{ items: [setupRetryEntry] },
{ items: [failedUnloadEntry] },
{ items: [notLoadedEntry] },
{
items: [
loadedEntry,
longNameEntry,
setupErrorEntry,
migrationErrorEntry,
setupRetryEntry,
failedUnloadEntry,
notLoadedEntry,
disabledEntry,
nameAsDomainEntry,
configPanelEntry,
optionsFlowEntry,
],
},
{ disabled: true, items: [disabledEntry] },
{ disabled: true, items: [disabledFailedUnloadEntry] },
{
disabled: true,
items: [disabledEntry, disabledFailedUnloadEntry],
},
{
items: [loadedEntry, configPanelEntry],
highlight: "Loaded",
},
];
const createEntityRegistryEntries = (
item: ConfigEntryExtended
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
entity_id: "binary_sensor.updater",
name: null,
icon: null,
platform: "updater",
},
];
const createDeviceRegistryEntries = (
item: ConfigEntryExtended
): DeviceRegistryEntry[] => [
{
entry_type: null,
config_entries: [item.entry_id],
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
name: "Tag Reader",
sw_version: null,
id: "mock-device-id",
identifiers: [],
via_device_id: null,
area_id: null,
name_by_user: null,
disabled_by: null,
},
];
@customElement("demo-integration-card")
export class DemoIntegrationCard extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() isCustomIntegration = false;
@internalProperty() isCloud = false;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<div class="filters">
<ha-formfield label="Custom Integration">
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
</ha-formfield>
<ha-formfield label="Relies on cloud">
<ha-switch @change=${this._toggleCloud}></ha-switch>
</ha-formfield>
</div>
<ha-ignored-config-entry-card
.hass=${this.hass}
.entry=${createConfigEntry("Ignored Entry")}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
></ha-ignored-config-entry-card>
${configFlows.map(
(flow) => html`
<ha-config-flow-card
.hass=${this.hass}
.flow=${flow}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
></ha-config-flow-card>
`
)}
${configEntries.map(
(info) => html`
<ha-integration-card
class=${classMap({
highlight: info.highlight !== undefined,
})}
.hass=${this.hass}
domain="esphome"
.items=${info.items}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
.entityRegistryEntries=${createEntityRegistryEntries(info.items[0])}
.deviceRegistryEntries=${createDeviceRegistryEntries(info.items[0])}
?disabled=${info.disabled}
.selectedConfigEntryId=${info.highlight}
></ha-integration-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
}
private _toggleCustomIntegration() {
this.isCustomIntegration = !this.isCustomIntegration;
}
private _toggleCloud() {
this.isCloud = !this.isCloud;
}
static get styles() {
return css`
:host {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin-bottom: 64px;
}
:host > * {
max-width: 500px;
}
ha-formfield {
margin: 8px 0;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-integration-card": DemoIntegrationCard;
}
}

View File

@ -5,11 +5,17 @@ export interface ConfigEntry {
domain: string;
title: string;
source: string;
state: string;
state:
| "loaded"
| "setup_error"
| "migration_error"
| "setup_retry"
| "not_loaded"
| "failed_unload";
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
disabled_by: string | null;
disabled_by: "user" | null;
}
export interface ConfigEntryMutableParams {

View File

@ -9,13 +9,13 @@ export interface DeviceRegistryEntry {
config_entries: string[];
connections: Array<[string, string]>;
identifiers: Array<[string, string]>;
manufacturer: string;
model?: string;
name?: string;
sw_version?: string;
via_device_id?: string;
area_id?: string;
name_by_user?: string;
manufacturer: string | null;
model: string | null;
name: string | null;
sw_version: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;
entry_type: "service" | null;
disabled_by: string | null;
}

View File

@ -5,12 +5,12 @@ import { HomeAssistant } from "../types";
export interface EntityRegistryEntry {
entity_id: string;
name: string;
icon?: string;
name: string | null;
icon: string | null;
platform: string;
config_entry_id?: string;
device_id?: string;
area_id?: string;
config_entry_id: string | null;
device_id: string | null;
area_id: string | null;
disabled_by: string | null;
}

View File

@ -15,7 +15,13 @@ export interface IntegrationManifest {
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
zeroconf?: string[];
homekit?: { models: string[] };
quality_scale?: string;
quality_scale?: "gold" | "internal" | "platinum" | "silver";
iot_class:
| "assumed_state"
| "cloud_polling"
| "cloud_push"
| "local_polling"
| "local_push";
}
export const integrationIssuesUrl = (

View File

@ -33,7 +33,7 @@ class DialogDeviceRegistryDetail extends LitElement {
@internalProperty() private _params?: DeviceRegistryDetailDialogParams;
@internalProperty() private _areaId?: string;
@internalProperty() private _areaId?: string | null;
@internalProperty() private _disabledBy!: string | null;

View File

@ -38,7 +38,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@internalProperty() private _entityId!: string;
@internalProperty() private _areaId?: string;
@internalProperty() private _areaId?: string | null;
@internalProperty() private _disabledBy!: string | null;

View File

@ -663,6 +663,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entity_id: entityId,
platform: computeDomain(entityId),
disabled_by: null,
area_id: null,
config_entry_id: null,
device_id: null,
icon: null,
readonly: true,
selectable: false,
});

View File

@ -0,0 +1,130 @@
import {
customElement,
LitElement,
property,
css,
html,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import {
ATTENTION_SOURCES,
DISCOVERY_SOURCES,
ignoreConfigFlow,
localizeConfigFlowTitle,
} from "../../../data/config_flow";
import type { IntegrationManifest } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import "./ha-integration-action-card";
@customElement("ha-config-flow-card")
export class HaConfigFlowCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flow!: DataEntryFlowProgressExtended;
@property() public manifest?: IntegrationManifest;
protected render(): TemplateResult {
const attention = ATTENTION_SOURCES.includes(this.flow.context.source);
return html`
<ha-integration-action-card
class=${classMap({
discovered: !attention,
attention: attention,
})}
.hass=${this.hass}
.manifest=${this.manifest}
.banner=${this.hass.localize(
`ui.panel.config.integrations.${
attention ? "attention" : "discovered"
}`
)}
.domain=${this.flow.handler}
.label=${this.flow.localized_title}
>
<mwc-button
unelevated
@click=${this._continueFlow}
.label=${this.hass.localize(
`ui.panel.config.integrations.${
attention ? "reconfigure" : "configure"
}`
)}
></mwc-button>
${DISCOVERY_SOURCES.includes(this.flow.context.source) &&
this.flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.label=${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
></mwc-button>
`
: ""}
</ha-integration-action-card>
`;
}
private _continueFlow() {
showConfigFlowDialog(this, {
continueFlowId: this.flow.flow_id,
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
});
}
private async _ignoreFlow() {
const confirmed = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_ignore_title",
"name",
localizeConfigFlowTitle(this.hass.localize, this.flow)
),
text: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_ignore"
),
confirmText: this.hass!.localize(
"ui.panel.config.integrations.ignore.ignore"
),
});
if (!confirmed) {
return;
}
await ignoreConfigFlow(
this.hass,
this.flow.flow_id,
localizeConfigFlowTitle(this.hass.localize, this.flow)
);
this._handleFlowUpdated();
}
private _handleFlowUpdated() {
fireEvent(this, "change", undefined, {
bubbles: false,
});
}
static styles = css`
.attention {
--state-color: var(--error-color);
--text-on-state-color: var(--text-primary-color);
}
.discovered {
--state-color: var(--primary-color);
--text-on-state-color: var(--text-primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-flow-card": HaConfigFlowCard;
}
}

View File

@ -0,0 +1,75 @@
import { mdiPackageVariant, mdiCloud } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, html } from "lit-element";
import { IntegrationManifest } from "../../../data/integration";
import { HomeAssistant } from "../../../types";
export const haConfigIntegrationsStyles = css`
.banner {
background-color: var(--state-color);
color: var(--text-on-state-color);
text-align: center;
padding: 8px;
}
.icons {
position: absolute;
top: 0px;
right: 16px;
color: var(--text-on-state-color, var(--secondary-text-color));
background-color: var(--state-color, #e0e0e0);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 1px 4px 2px;
}
.icons ha-svg-icon {
width: 20px;
height: 20px;
}
paper-tooltip {
white-space: nowrap;
}
`;
export const haConfigIntegrationRenderIcons = (
hass: HomeAssistant,
manifest?: IntegrationManifest
) => {
const icons: [string, string][] = [];
if (manifest) {
if (!manifest.is_built_in) {
icons.push([
mdiPackageVariant,
hass.localize(
"ui.panel.config.integrations.config_entry.provided_by_custom_component"
),
]);
}
if (manifest.iot_class && manifest.iot_class.startsWith("cloud_")) {
icons.push([
mdiCloud,
hass.localize(
"ui.panel.config.integrations.config_entry.depends_on_cloud"
),
]);
}
}
return icons.length === 0
? ""
: html`
<div class="icons">
${icons.map(
([icon, description]) => html`
<span>
<ha-svg-icon .path=${icon}></ha-svg-icon>
<paper-tooltip animation-delay="0"
>${description}</paper-tooltip
>
</span>
`
)}
</div>
`;
};

View File

@ -2,9 +2,8 @@ import "@material/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route";
import Fuse from "fuse.js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
@ -16,31 +15,15 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-checkbox";
import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
} from "../../../data/config_entries";
import {
ATTENTION_SOURCES,
DISCOVERY_SOURCES,
getConfigFlowInProgressCollection,
ignoreConfigFlow,
localizeConfigFlowTitle,
subscribeConfigFlowInProgress,
} from "../../../data/config_flow";
@ -60,21 +43,43 @@ import {
} from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { configSections } from "../ha-panel-config";
import "./ha-integration-card";
import type {
ConfigEntryRemovedEvent,
ConfigEntryUpdatedEvent,
HaIntegrationCard,
} from "./ha-integration-card";
interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
import type { HomeAssistant, Route } from "../../../types";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { HaIntegrationCard } from "./ha-integration-card";
import "../../../common/search/search-input";
import "../../../components/ha-button-menu";
import "../../../components/ha-fab";
import "../../../components/ha-checkbox";
import "../../../components/ha-svg-icon";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import "./ha-integration-card";
import "./ha-config-flow-card";
import "./ha-ignored-config-entry-card";
export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry;
}
export interface ConfigEntryRemovedEvent {
entryId: string;
}
declare global {
// for fire event
interface HASSDomEvents {
"entry-updated": ConfigEntryUpdatedEvent;
"entry-removed": ConfigEntryRemovedEvent;
}
}
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
localized_title?: string;
}
@ -119,9 +124,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@internalProperty()
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
@internalProperty() private _manifests!: {
[domain: string]: IntegrationManifest;
};
@internalProperty()
private _manifests: Record<string, IntegrationManifest> = {};
@internalProperty() private _showIgnored = false;
@ -217,12 +221,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
configEntriesInProgress: DataEntryFlowProgressExtended[],
filter?: string
): DataEntryFlowProgressExtended[] => {
configEntriesInProgress = configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => ({
...flow,
title: localizeConfigFlowTitle(this.hass.localize, flow),
})
);
if (!filter) {
return configEntriesInProgress;
}
@ -349,11 +347,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"number",
disabledConfigEntries.size
)}
<mwc-button @click=${this._toggleShowDisabled}>
${this.hass.localize(
<mwc-button
@click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show"
)}
</mwc-button>
></mwc-button>
</div>`
: ""}
${filterMenu}
@ -362,112 +361,30 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<div
class="container"
@entry-removed=${this._handleRemoved}
@entry-updated=${this._handleUpdated}
@entry-removed=${this._handleEntryRemoved}
@entry-updated=${this._handleEntryUpdated}
>
${this._showIgnored
? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html`
<ha-card outlined class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}
</div>
<div class="card-content">
<div class="image">
<img
src=${brandsUrl(item.domain, "logo")}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${// In 2020.2 we added support for item.title. All ignored entries before
// that have title "Ignored" so we fallback to localized domain name.
item.title === "Ignored"
? item.localized_domain_name
: item.title}
</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>
(entry: ConfigEntryExtended) => html`
<ha-ignored-config-entry-card
.hass=${this.hass}
.manifest=${this._manifests[entry.domain]}
.entry=${entry}
@change=${this._handleFlowUpdated}
></ha-ignored-config-entry-card>
`
)
: ""}
${configEntriesInProgress.length
? configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => {
const attention = ATTENTION_SOURCES.includes(
flow.context.source
);
return html`
<ha-card
outlined
class=${classMap({
discovered: !attention,
attention: attention,
})}
>
<div class="header">
${this.hass.localize(
`ui.panel.config.integrations.${
attention ? "attention" : "discovered"
}`
)}
</div>
<div class="card-content">
<div class="image">
<img
src=${brandsUrl(flow.handler, "logo")}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${flow.localized_title}
</h2>
<div>
<mwc-button
unelevated
@click=${this._continueFlow}
.flowId=${flow.flow_id}
>
${this.hass.localize(
`ui.panel.config.integrations.${
attention ? "reconfigure" : "configure"
}`
)}
</mwc-button>
${DISCOVERY_SOURCES.includes(flow.context.source) &&
flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.flow=${flow}
>
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
`
: ""}
</div>
</div>
</ha-card>
`;
}
(flow: DataEntryFlowProgressExtended) => html`
<ha-config-flow-card
.hass=${this.hass}
.flow=${flow}
@change=${this._handleFlowUpdated}
></ha-config-flow-card>
`
)
: ""}
${this._showDisabled
@ -498,25 +415,28 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card>`
)
: !this._configEntries.length
: // If we're showing 0 cards, show empty state text
(!this._showIgnored || ignoredConfigEntries.length === 0) &&
(!this._showDisabled || disabledConfigEntries.size === 0) &&
groupedConfigEntries.size === 0
? html`
<ha-card outlined>
<div class="card-content">
<h1>
${this.hass.localize("ui.panel.config.integrations.none")}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.integrations.no_integrations"
)}
</p>
<mwc-button @click=${this._createFlow} unelevated
>${this.hass.localize(
"ui.panel.config.integrations.add_integration"
)}</mwc-button
>
</div>
</ha-card>
<div class="empty-message">
<h1>
${this.hass.localize("ui.panel.config.integrations.none")}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.integrations.no_integrations"
)}
</p>
<mwc-button
@click=${this._createFlow}
unelevated
.label=${this.hass.localize(
"ui.panel.config.integrations.add_integration"
)}
></mwc-button>
</div>
`
: ""}
${this._filter &&
@ -524,7 +444,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
!groupedConfigEntries.size &&
this._configEntries.length
? html`
<div class="none-found">
<div class="empty-message">
<h1>
${this.hass.localize(
"ui.panel.config.integrations.none_found"
@ -581,13 +501,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._manifests = manifests;
}
private _handleRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
this._configEntries = this._configEntries!.filter(
(entry) => entry.entry_id !== ev.detail.entryId
);
}
private _handleUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
private _handleEntryUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
const newEntry = ev.detail.entry;
this._configEntries = this._configEntries!.map((entry) =>
entry.entry_id === newEntry.entry_id
@ -599,6 +519,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
private _handleFlowUpdated() {
this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass.connection).refresh();
this._fetchManifests();
}
private _createFlow() {
@ -608,50 +529,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
},
showAdvanced: this.showAdvanced,
});
// For config entries. Also loading config flow ones for add integration
// For config entries. Also loading config flow ones for added integration
this.hass.loadBackendTranslation("title", undefined, true);
}
private _continueFlow(ev: Event) {
showConfigFlowDialog(this, {
continueFlowId: (ev.target! as any).flowId,
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
});
}
private async _ignoreFlow(ev: Event) {
const flow = (ev.target! as any).flow;
const confirmed = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_ignore_title",
"name",
localizeConfigFlowTitle(this.hass.localize, flow)
),
text: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_ignore"
),
confirmText: this.hass!.localize(
"ui.panel.config.integrations.ignore.ignore"
),
});
if (!confirmed) {
return;
}
await ignoreConfigFlow(
this.hass,
flow.flow_id,
localizeConfigFlowTitle(this.hass.localize, flow)
);
this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass.connection).refresh();
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._toggleShowIgnored();
this._showIgnored = !this._showIgnored;
break;
case 1:
this._toggleShowDisabled();
@ -659,54 +544,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
}
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, {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore_title",
"name",
this.hass.localize(`component.${entry.domain}.title`)
),
text: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore"
),
confirmText: this.hass!.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
),
confirm: async () => {
const result = await deleteConfigEntry(this.hass, entry.entry_id);
if (result.require_restart) {
alert(
this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
)
);
}
this._loadConfigEntries();
},
});
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private async _highlightEntry() {
await nextRender();
const entryId = this._searchParms.get("config_entry")!;
@ -769,66 +614,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
padding: 8px 16px 16px;
margin-bottom: 64px;
}
ha-card {
.container > * {
max-width: 500px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.attention {
--ha-card-border-color: var(--error-color);
}
.attention .header {
background: var(--error-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.attention mwc-button {
--mdc-theme-primary: var(--error-color);
}
.discovered {
--ha-card-border-color: var(--primary-color);
}
.discovered .header {
background: var(--primary-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.ignored {
--ha-card-border-color: var(--light-theme-disabled-color);
}
.ignored img {
filter: grayscale(1);
}
.ignored .header {
background: var(--light-theme-disabled-color);
color: var(--text-primary-color);
padding: 8px;
text-align: center;
}
.card-content {
display: flex;
height: 100%;
margin-top: 0;
padding: 16px;
text-align: center;
flex-direction: column;
justify-content: space-between;
}
.image {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
margin-bottom: 16px;
vertical-align: middle;
}
.none-found {
.empty-message {
margin: auto;
text-align: center;
}
.empty-message h1 {
margin-bottom: 0;
}
search-input.header {
display: block;
position: relative;
@ -848,27 +645,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
position: relative;
top: 2px;
}
img {
max-height: 100%;
max-width: 90%;
}
.none-found {
margin: auto;
text-align: center;
}
h1 {
margin-bottom: 0;
}
h2 {
margin-top: 0;
word-wrap: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
.active-filters {
color: var(--primary-text-color);
position: relative;

View File

@ -0,0 +1,95 @@
import {
customElement,
LitElement,
property,
css,
html,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import { deleteConfigEntry } from "../../../data/config_entries";
import type { IntegrationManifest } from "../../../data/integration";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import type { ConfigEntryExtended } from "./ha-config-integrations";
import "./ha-integration-action-card";
@customElement("ha-ignored-config-entry-card")
export class HaIgnoredConfigEntryCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entry!: ConfigEntryExtended;
@property() public manifest?: IntegrationManifest;
protected render(): TemplateResult {
return html`
<ha-integration-action-card
.hass=${this.hass}
.manifest=${this.manifest}
.banner=${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}
.domain=${this.entry.domain}
.label=${this.entry.title === "Ignored"
? // In 2020.2 we added support for entry.title. All ignored entries before
// that have title "Ignored" so we fallback to localized domain name.
this.entry.localized_domain_name
: this.entry.title}
>
<mwc-button
unelevated
@click=${this._removeIgnoredIntegration}
.label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
></mwc-button>
</ha-integration-action-card>
`;
}
private async _removeIgnoredIntegration() {
showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore_title",
"name",
this.hass.localize(`component.${this.entry.domain}.title`)
),
text: this.hass!.localize(
"ui.panel.config.integrations.ignore.confirm_delete_ignore"
),
confirmText: this.hass!.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
),
confirm: async () => {
const result = await deleteConfigEntry(this.hass, this.entry.entry_id);
if (result.require_restart) {
alert(
this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
)
);
}
fireEvent(this, "change", undefined, {
bubbles: false,
});
},
});
}
static styles = css`
:host {
--state-color: var(--divider-color, #e0e0e0);
}
mwc-button {
--mdc-theme-primary: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-ignored-config-entry-card": HaIgnoredConfigEntryCard;
}
}

View File

@ -0,0 +1,114 @@
import {
customElement,
LitElement,
property,
CSSResult,
css,
} from "lit-element";
import { TemplateResult, html } from "lit-html";
import { IntegrationManifest } from "../../../data/integration";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import {
haConfigIntegrationRenderIcons,
haConfigIntegrationsStyles,
} from "./ha-config-integrations-common";
@customElement("ha-integration-action-card")
export class HaIntegrationActionCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public banner!: string;
@property() public domain!: string;
@property() public label!: string;
@property() public manifest?: IntegrationManifest;
protected render(): TemplateResult {
return html`
<ha-card outlined>
<div class="banner">
${this.banner}
</div>
<div class="content">
${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
<div class="image">
<img
src=${brandsUrl(this.domain, "logo")}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>${this.label}</h2>
</div>
<div class="actions"><slot></slot></div>
</ha-card>
`;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
static get styles(): CSSResult[] {
return [
haConfigIntegrationsStyles,
css`
ha-card {
display: flex;
flex-direction: column;
height: 100%;
--ha-card-border-color: var(--state-color);
--mdc-theme-primary: var(--state-color);
}
.content {
position: relative;
flex: 1;
}
.image {
height: 60px;
margin-top: 16px;
display: flex;
align-items: center;
justify-content: space-around;
}
img {
max-width: 90%;
max-height: 100%;
}
h2 {
text-align: center;
margin: 16px 8px 0;
}
.attention {
--state-color: var(--error-color);
--text-on-state-color: var(--text-primary-color);
}
.discovered {
--state-color: var(--primary-color);
--text-on-state-color: var(--text-primary-color);
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 6px 0;
height: 48px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-integration-action-card": HaIntegrationActionCard;
}
}

View File

@ -1,4 +1,8 @@
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-listbox";
import "@material/mwc-button";
import "@polymer/paper-item";
import "@polymer/paper-tooltip/paper-tooltip";
import { mdiAlertCircle, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
import {
@ -14,7 +18,9 @@ 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 "../../../components/ha-button-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-card";
import {
ConfigEntry,
deleteConfigEntry,
@ -23,8 +29,8 @@ import {
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { domainToName, IntegrationManifest } from "../../../data/integration";
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
@ -37,48 +43,25 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { ConfigEntryExtended } from "./ha-config-integrations";
import {
haConfigIntegrationRenderIcons,
haConfigIntegrationsStyles,
} from "./ha-config-integrations-common";
export interface ConfigEntryUpdatedEvent {
entry: ConfigEntry;
}
export interface ConfigEntryRemovedEvent {
entryId: string;
}
declare global {
// for fire event
interface HASSDomEvents {
"entry-updated": ConfigEntryUpdatedEvent;
"entry-removed": ConfigEntryRemovedEvent;
}
}
const ERROR_STATES: ConfigEntry["state"][] = [
"failed_unload",
"migration_error",
"setup_error",
"setup_retry",
];
const integrationsWithPanel = {
hassio: {
buttonLocalizeKey: "ui.panel.config.hassio.button",
path: "/hassio/dashboard",
},
mqtt: {
buttonLocalizeKey: "ui.panel.config.mqtt.button",
path: "/config/mqtt",
},
zha: {
buttonLocalizeKey: "ui.panel.config.zha.button",
path: "/config/zha/dashboard",
},
ozw: {
buttonLocalizeKey: "ui.panel.config.ozw.button",
path: "/config/ozw/dashboard",
},
zwave: {
buttonLocalizeKey: "ui.panel.config.zwave.button",
path: "/config/zwave",
},
zwave_js: {
buttonLocalizeKey: "ui.panel.config.zwave_js.button",
path: "/config/zwave_js/dashboard",
},
hassio: "/hassio/dashboard",
mqtt: "/config/mqtt",
zha: "/config/zha/dashboard",
ozw: "/config/ozw/dashboard",
zwave: "/config/zwave",
zwave_js: "/config/zwave_js/dashboard",
};
@customElement("ha-integration-card")
@ -89,7 +72,7 @@ export class HaIntegrationCard extends LitElement {
@property() public items!: ConfigEntryExtended[];
@property() public manifest!: IntegrationManifest;
@property() public manifest?: IntegrationManifest;
@property() public entityRegistryEntries!: EntityRegistryEntry[];
@ -99,291 +82,325 @@ export class HaIntegrationCard extends LitElement {
@property({ type: Boolean }) public disabled = false;
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
}
protected render(): TemplateResult {
let item = this._selectededConfigEntry;
if (this.items.length === 1) {
return this._renderSingleEntry(this.items[0]);
}
if (this.selectedConfigEntryId) {
const configEntry = this.items.find(
item = this.items[0];
} else if (this.selectedConfigEntryId) {
item = this.items.find(
(entry) => entry.entry_id === this.selectedConfigEntryId
);
if (configEntry) {
return this._renderSingleEntry(configEntry);
}
}
return this._renderGroupedIntegration();
}
private _renderGroupedIntegration(): TemplateResult {
let primary: string;
let secondary: string | undefined;
if (item) {
primary = item.title || item.localized_domain_name || this.domain;
if (primary !== item.localized_domain_name) {
secondary = item.localized_domain_name;
}
} else {
primary = domainToName(this.hass.localize, this.domain, this.manifest);
}
const hasItem = item !== undefined;
return html`
<ha-card outlined class="group ${classMap({ disabled: this.disabled })}">
<ha-card
outlined
class="${classMap({
single: hasItem,
group: !hasItem,
hasMultiple: this.items.length > 1,
disabled: this.disabled,
"state-not-loaded": hasItem && item!.state === "not_loaded",
"state-error": hasItem && ERROR_STATES.includes(item!.state),
})}"
.configEntry=${item}
>
${this.disabled
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled"
)}
</div>`
? html`
<div class="banner">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled"
)}
</div>
`
: ""}
<div class="group-header">
${this.items.length > 1
? html`
<div class="back-btn">
<ha-icon-button
icon="hass:chevron-left"
@click=${this._back}
></ha-icon-button>
</div>
`
: ""}
<div class="header">
<img
src=${brandsUrl(this.domain, "icon")}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<h2>
${domainToName(this.hass.localize, this.domain)}
</h2>
<div class="info">
<div class="primary">${primary}</div>
${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
</div>
${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
</div>
<paper-listbox>
${this.items.map(
(item) =>
html`<paper-item
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</paper-item-body
>
${item.state === "not_loaded"
? html`<span>
<ha-svg-icon
class="error"
.path=${mdiAlertCircle}
></ha-svg-icon
><paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.not_loaded",
"logs_link",
this.hass.localize(
"ui.panel.config.integrations.config_entry.logs"
)
)}
</paper-tooltip>
</span>`
: ""}
<ha-icon-next></ha-icon-next>
</paper-item>`
)}
</paper-listbox>
${item
? this._renderSingleEntry(item)
: this._renderGroupedIntegration()}
</ha-card>
`;
}
private _renderGroupedIntegration(): TemplateResult {
return html`
<paper-listbox>
${this.items.map(
(item) =>
html`<paper-item
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</paper-item-body
>
${ERROR_STATES.includes(item.state)
? html`<span>
<ha-svg-icon
class="error"
.path=${mdiAlertCircle}
></ha-svg-icon
><paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
`ui.panel.config.integrations.config_entry.state.${item.state}`
)}
</paper-tooltip>
</span>`
: ""}
<ha-icon-next></ha-icon-next>
</paper-item>`
)}
</paper-listbox>
`;
}
private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
const devices = this._getDevices(item);
const services = this._getServices(item);
const entities = this._getEntities(item);
let stateText: [string, ...unknown[]] | undefined;
let stateTextExtra: TemplateResult | string | undefined;
if (item.disabled_by) {
stateText = [
"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,
];
if (item.state === "failed_unload") {
stateTextExtra = html`.
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
)}.`;
}
} else if (item.state === "not_loaded") {
stateText = ["ui.panel.config.integrations.config_entry.not_loaded"];
} else if (ERROR_STATES.includes(item.state)) {
stateText = [
`ui.panel.config.integrations.config_entry.state.${item.state}`,
];
stateTextExtra = html`
<br />
<a href="/config/logs"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.check_the_logs"
)}</a
>
`;
}
return html`
<ha-card
outlined
class="single integration ${classMap({
disabled: Boolean(item.disabled_by),
"not-loaded": !item.disabled_by && item.state === "not_loaded",
})}"
.configEntry=${item}
.id=${item.entry_id}
>
${this.items.length > 1
? html`<ha-icon-button
class="back-btn"
icon="hass:chevron-left"
@click=${this._back}
></ha-icon-button>`
<div class="content">
${stateText
? html`
<div class="message">
${this.hass.localize(...stateText)}${stateTextExtra}
</div>
`
: ""}
${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>`
: item.state === "not_loaded"
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.not_loaded",
"logs_link",
html`<a href="/config/logs"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.logs"
)}</a
>`
)}
</div>`
${devices.length || services.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
>${services.length ? "," : ""}
`
: ""}
${services.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.services",
"count",
services.length
)}</a
>
`
: ""}
${(devices.length || services.length) && entities.length
? this.hass.localize("ui.common.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 class="card-content">
<div class="image">
<img
src=${brandsUrl(item.domain, "logo")}
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${item.localized_domain_name}
</h2>
<h3>
${item.localized_domain_name === item.title ? "" : item.title}
</h3>
${devices.length || services.length || entities.length
</div>
<div class="actions">
<div>
${item.disabled_by === "user"
? html`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</mwc-button>`
: item.domain in integrationsWithPanel
? html`<a
href=${`${
integrationsWithPanel[item.domain].path
}?config_entry=${item.entry_id}`}
><mwc-button>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</mwc-button></a
>`
: item.supports_options
? 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
>${services.length ? "," : ""}
`
: ""}
${services.length
? html`
<a
href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.services",
"count",
services.length
)}</a
>
`
: ""}
${(devices.length || services.length) && entities.length
? this.hass.localize("ui.common.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>
<mwc-button @click=${this._showOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</mwc-button>
`
: ""}
</div>
<div class="card-actions">
<div>
${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>
${item.domain in integrationsWithPanel
? html`<a
href=${`${
integrationsWithPanel[item.domain].path
}?config_entry=${item.entry_id}`}
><mwc-button>
${!this.manifest
? ""
: html`
<ha-button-menu corner="BOTTOM_START">
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item @request-selected="${this._editEntryName}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</mwc-list-item>
<mwc-list-item @request-selected="${this._handleSystemOptions}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</mwc-list-item>
<a
href=${this.manifest.documentation}
rel="noreferrer"
target="_blank"
>
<mwc-list-item hasMeta>
${this.hass.localize(
integrationsWithPanel[item.domain].buttonLocalizeKey
)}
</mwc-button></a
>`
: item.supports_options
? html`
<mwc-button @click=${this._showOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}
</mwc-button>
`
: ""}
</div>
<ha-button-menu corner="BOTTOM_START">
<mwc-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item @request-selected="${this._handleSystemOptions}">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</mwc-list-item>
${!this.manifest
? ""
: html`
<a
href=${this.manifest.documentation}
rel="noreferrer"
target="_blank"
>
<mwc-list-item hasMeta>
"ui.panel.config.integrations.config_entry.documentation"
)}<ha-svg-icon
slot="meta"
.path=${mdiOpenInNew}
></ha-svg-icon>
</mwc-list-item>
</a>
${!item.disabled_by &&
item.state === "loaded" &&
item.supports_unload &&
item.source !== "system"
? html`<mwc-list-item
@request-selected="${this._handleReload}"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.documentation"
)}<ha-svg-icon
slot="meta"
.path=${mdiOpenInNew}
></ha-svg-icon>
</mwc-list-item>
</a>
`}
${!item.disabled_by &&
item.state === "loaded" &&
item.supports_unload &&
item.source !== "system"
? 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>`
: item.source !== "system"
? html`<mwc-list-item
class="warning"
@request-selected="${this._handleDisable}"
>
${this.hass.localize("ui.common.disable")}
</mwc-list-item>`
: ""}
${item.source !== "system"
? html`<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
</mwc-list-item>`
: ""}
</ha-button-menu>
</div>
</ha-card>
"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>`
: item.source !== "system"
? html`<mwc-list-item
class="warning"
@request-selected="${this._handleDisable}"
>
${this.hass.localize("ui.common.disable")}
</mwc-list-item>`
: ""}
${item.source !== "system"
? html`<mwc-list-item
class="warning"
@request-selected="${this._handleDelete}"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
</mwc-list-item>`
: ""}
</ha-button-menu>
`}
</div>
`;
}
private get _selectededConfigEntry(): ConfigEntryExtended | undefined {
return this.items.length === 1
? this.items[0]
: this.selectedConfigEntryId
? this.items.find(
(entry) => entry.entry_id === this.selectedConfigEntryId
)
: undefined;
}
private _selectConfigEntry(ev: Event) {
this.selectedConfigEntryId = (ev.currentTarget as any).entryId;
}
@ -588,109 +605,109 @@ export class HaIntegrationCard extends LitElement {
static get styles(): CSSResult[] {
return [
haStyle,
haConfigIntegrationsStyles,
css`
:host {
max-width: 500px;
}
ha-card {
display: flex;
flex-direction: column;
height: 100%;
--state-color: var(--divider-color, #e0e0e0);
--ha-card-border-color: var(--state-color);
--state-message-color: var(--state-color);
}
ha-card.single {
justify-content: space-between;
.state-error {
--state-color: var(--error-color);
--text-on-state-color: var(--text-primary-color);
}
.state-not-loaded {
--state-message-color: var(--primary-text-color);
}
:host(.highlight) ha-card {
border: 1px solid var(--accent-color);
--state-color: var(--accent-color);
--text-on-state-color: var(--text-primary-color);
}
.disabled {
--ha-card-border-color: var(--warning-color);
ha-card.group {
max-height: 200px;
}
.not-loaded {
--ha-card-border-color: var(--error-color);
.back-btn {
background-color: var(--state-color);
color: var(--text-on-state-color);
--mdc-icon-button-size: 32px;
transition: height 0.1s;
overflow: hidden;
}
.hasMultiple.single .back-btn {
height: 32px;
}
.hasMultiple.group .back-btn {
height: 0px;
}
.header {
padding: 8px;
text-align: center;
}
.disabled .header {
background: var(--warning-color);
color: var(--text-primary-color);
}
.not-loaded .header {
background: var(--error-color);
color: var(--text-primary-color);
}
.not-loaded .header a {
color: var(--text-primary-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;
position: relative;
align-items: center;
padding-right: 5px;
padding: 16px 8px 8px 16px;
}
.group-header {
display: flex;
align-items: center;
.group.disabled .header {
padding-top: 8px;
}
.header img {
margin-right: 16px;
width: 40px;
height: 40px;
padding: 16px 16px 8px 16px;
justify-content: center;
}
.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 {
min-height: 24px;
}
h3 {
.header .info div,
paper-item-body {
word-wrap: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.primary {
font-size: 16px;
font-weight: 400;
color: var(--primary-text-color);
}
.secondary {
font-size: 14px;
color: var(--secondary-text-color);
}
.message {
font-weight: bold;
padding-bottom: 16px;
color: var(--state-message-color);
}
.content {
flex: 1;
padding: 0px 16px 0 72px;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0 0 8px;
height: 48px;
}
.actions a {
text-decoration: none;
}
a {
color: var(--primary-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
@media (min-width: 563px) {
paper-listbox {
max-height: 150px;
flex: 1;
overflow: auto;
}
}
@ -701,11 +718,6 @@ export class HaIntegrationCard extends LitElement {
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
.back-btn {
position: absolute;
background: rgba(var(--rgb-card-background-color), 0.6);
border-radius: 50%;
}
`,
];
}

View File

@ -177,7 +177,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
});
}
private _computeEntityName(entity: EntityRegistryEntry): string {
private _computeEntityName(entity: EntityRegistryEntry): string | null {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}

View File

@ -2142,7 +2142,7 @@
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"services": "{count} {count, plural,\n one {service}\n other {services}\n}",
"rename": "Rename",
"options": "Options",
"configure": "Configure",
"system_options": "System options",
"documentation": "Documentation",
"delete": "Delete",
@ -2161,8 +2161,8 @@
"entity_unavailable": "Entity unavailable",
"area": "In {area}",
"no_area": "No Area",
"not_loaded": "Not loaded, check the {logs_link}",
"logs": "logs",
"not_loaded": "Not loaded",
"check_the_logs": "Check the logs",
"disable": {
"disabled": "Disabled",
"disabled_cause": "Disabled by {cause}",
@ -2172,6 +2172,16 @@
"device": "device"
},
"disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled."
},
"provided_by_custom_component": "Provided by a custom component",
"depends_on_cloud": "Depends on the cloud",
"state": {
"loaded": "Not loaded",
"setup_error": "Failed to set up",
"migration_error": "Migration error",
"setup_retry": "Retrying to set up",
"not_loaded": "Not loaded",
"failed_unload": "Failed to unload"
}
},
"config_flow": {
@ -2243,11 +2253,7 @@
"create": "Create"
}
},
"hassio": {
"button": "Configure"
},
"mqtt": {
"button": "Configure",
"title": "MQTT",
"description_publish": "Publish a packet",
"topic": "topic",
@ -2261,7 +2267,6 @@
"message_received": "Message {id} received on {topic} at {time}:"
},
"ozw": {
"button": "Configure",
"common": {
"zwave": "Z-Wave",
"node_id": "Node ID",
@ -2376,7 +2381,6 @@
}
},
"zha": {
"button": "Configure",
"common": {
"clusters": "Clusters",
"manufacturer_code_override": "Manufacturer Code Override",
@ -2464,7 +2468,6 @@
}
},
"zwave": {
"button": "Configure",
"description": "Manage your Z-Wave network",
"learn_more": "Learn more about Z-Wave",
"common": {
@ -2555,7 +2558,6 @@
}
},
"zwave_js": {
"button": "Configure",
"navigation": {
"network": "Network"
},