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; domain: string;
title: string; title: string;
source: string; source: string;
state: string; state:
| "loaded"
| "setup_error"
| "migration_error"
| "setup_retry"
| "not_loaded"
| "failed_unload";
connection_class: string; connection_class: string;
supports_options: boolean; supports_options: boolean;
supports_unload: boolean; supports_unload: boolean;
disabled_by: string | null; disabled_by: "user" | null;
} }
export interface ConfigEntryMutableParams { export interface ConfigEntryMutableParams {

View File

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

View File

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

View File

@ -15,7 +15,13 @@ export interface IntegrationManifest {
ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>; ssdp?: Array<{ manufacturer?: string; modelName?: string; st?: string }>;
zeroconf?: string[]; zeroconf?: string[];
homekit?: { models: 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 = ( export const integrationIssuesUrl = (

View File

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

View File

@ -663,6 +663,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entity_id: entityId, entity_id: entityId,
platform: computeDomain(entityId), platform: computeDomain(entityId),
disabled_by: null, disabled_by: null,
area_id: null,
config_entry_id: null,
device_id: null,
icon: null,
readonly: true, readonly: true,
selectable: false, 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 { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiFilterVariant, mdiPlus } from "@mdi/js"; import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import "@polymer/app-route/app-route";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
@ -16,31 +15,15 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare"; import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params"; import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status"; import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button-menu"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-checkbox";
import "../../../components/ha-svg-icon";
import { import {
ConfigEntry,
deleteConfigEntry,
getConfigEntries,
} from "../../../data/config_entries";
import {
ATTENTION_SOURCES,
DISCOVERY_SOURCES,
getConfigFlowInProgressCollection, getConfigFlowInProgressCollection,
ignoreConfigFlow,
localizeConfigFlowTitle, localizeConfigFlowTitle,
subscribeConfigFlowInProgress, subscribeConfigFlowInProgress,
} from "../../../data/config_flow"; } from "../../../data/config_flow";
@ -60,21 +43,43 @@ import {
} from "../../../data/integration"; } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; 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 { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { configSections } from "../ha-panel-config"; 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; localized_title?: string;
} }
@ -119,9 +124,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@internalProperty() @internalProperty()
private _deviceRegistryEntries: DeviceRegistryEntry[] = []; private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
@internalProperty() private _manifests!: { @internalProperty()
[domain: string]: IntegrationManifest; private _manifests: Record<string, IntegrationManifest> = {};
};
@internalProperty() private _showIgnored = false; @internalProperty() private _showIgnored = false;
@ -217,12 +221,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
configEntriesInProgress: DataEntryFlowProgressExtended[], configEntriesInProgress: DataEntryFlowProgressExtended[],
filter?: string filter?: string
): DataEntryFlowProgressExtended[] => { ): DataEntryFlowProgressExtended[] => {
configEntriesInProgress = configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => ({
...flow,
title: localizeConfigFlowTitle(this.hass.localize, flow),
})
);
if (!filter) { if (!filter) {
return configEntriesInProgress; return configEntriesInProgress;
} }
@ -349,11 +347,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"number", "number",
disabledConfigEntries.size disabledConfigEntries.size
)} )}
<mwc-button @click=${this._toggleShowDisabled}> <mwc-button
${this.hass.localize( @click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show" "ui.panel.config.integrations.disable.show"
)} )}
</mwc-button> ></mwc-button>
</div>` </div>`
: ""} : ""}
${filterMenu} ${filterMenu}
@ -362,112 +361,30 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<div <div
class="container" class="container"
@entry-removed=${this._handleRemoved} @entry-removed=${this._handleEntryRemoved}
@entry-updated=${this._handleUpdated} @entry-updated=${this._handleEntryUpdated}
> >
${this._showIgnored ${this._showIgnored
? ignoredConfigEntries.map( ? ignoredConfigEntries.map(
(item: ConfigEntryExtended) => html` (entry: ConfigEntryExtended) => html`
<ha-card outlined class="ignored"> <ha-ignored-config-entry-card
<div class="header"> .hass=${this.hass}
${this.hass.localize( .manifest=${this._manifests[entry.domain]}
"ui.panel.config.integrations.ignore.ignored" .entry=${entry}
)} @change=${this._handleFlowUpdated}
</div> ></ha-ignored-config-entry-card>
<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>
` `
) )
: ""} : ""}
${configEntriesInProgress.length ${configEntriesInProgress.length
? configEntriesInProgress.map( ? configEntriesInProgress.map(
(flow: DataEntryFlowProgressExtended) => { (flow: DataEntryFlowProgressExtended) => html`
const attention = ATTENTION_SOURCES.includes( <ha-config-flow-card
flow.context.source .hass=${this.hass}
);
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} .flow=${flow}
> @change=${this._handleFlowUpdated}
${this.hass.localize( ></ha-config-flow-card>
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
` `
: ""}
</div>
</div>
</ha-card>
`;
}
) )
: ""} : ""}
${this._showDisabled ${this._showDisabled
@ -498,10 +415,12 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.deviceRegistryEntries=${this._deviceRegistryEntries} .deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card>` ></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` ? html`
<ha-card outlined> <div class="empty-message">
<div class="card-content">
<h1> <h1>
${this.hass.localize("ui.panel.config.integrations.none")} ${this.hass.localize("ui.panel.config.integrations.none")}
</h1> </h1>
@ -510,13 +429,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.no_integrations" "ui.panel.config.integrations.no_integrations"
)} )}
</p> </p>
<mwc-button @click=${this._createFlow} unelevated <mwc-button
>${this.hass.localize( @click=${this._createFlow}
unelevated
.label=${this.hass.localize(
"ui.panel.config.integrations.add_integration" "ui.panel.config.integrations.add_integration"
)}</mwc-button )}
> ></mwc-button>
</div> </div>
</ha-card>
` `
: ""} : ""}
${this._filter && ${this._filter &&
@ -524,7 +444,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
!groupedConfigEntries.size && !groupedConfigEntries.size &&
this._configEntries.length this._configEntries.length
? html` ? html`
<div class="none-found"> <div class="empty-message">
<h1> <h1>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.none_found" "ui.panel.config.integrations.none_found"
@ -581,13 +501,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._manifests = manifests; this._manifests = manifests;
} }
private _handleRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) { private _handleEntryRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
this._configEntries = this._configEntries!.filter( this._configEntries = this._configEntries!.filter(
(entry) => entry.entry_id !== ev.detail.entryId (entry) => entry.entry_id !== ev.detail.entryId
); );
} }
private _handleUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) { private _handleEntryUpdated(ev: HASSDomEvent<ConfigEntryUpdatedEvent>) {
const newEntry = ev.detail.entry; const newEntry = ev.detail.entry;
this._configEntries = this._configEntries!.map((entry) => this._configEntries = this._configEntries!.map((entry) =>
entry.entry_id === newEntry.entry_id entry.entry_id === newEntry.entry_id
@ -599,6 +519,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
private _handleFlowUpdated() { private _handleFlowUpdated() {
this._loadConfigEntries(); this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass.connection).refresh(); getConfigFlowInProgressCollection(this.hass.connection).refresh();
this._fetchManifests();
} }
private _createFlow() { private _createFlow() {
@ -608,50 +529,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}, },
showAdvanced: this.showAdvanced, 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); 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>) { private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
this._toggleShowIgnored(); this._showIgnored = !this._showIgnored;
break; break;
case 1: case 1:
this._toggleShowDisabled(); this._toggleShowDisabled();
@ -659,54 +544,14 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
} }
} }
private _toggleShowIgnored() {
this._showIgnored = !this._showIgnored;
}
private _toggleShowDisabled() { private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled; 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) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private async _highlightEntry() { private async _highlightEntry() {
await nextRender(); await nextRender();
const entryId = this._searchParms.get("config_entry")!; const entryId = this._searchParms.get("config_entry")!;
@ -769,66 +614,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
padding: 8px 16px 16px; padding: 8px 16px 16px;
margin-bottom: 64px; margin-bottom: 64px;
} }
ha-card { .container > * {
max-width: 500px; max-width: 500px;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.attention {
--ha-card-border-color: var(--error-color); .empty-message {
}
.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 {
margin: auto; margin: auto;
text-align: center; text-align: center;
} }
.empty-message h1 {
margin-bottom: 0;
}
search-input.header { search-input.header {
display: block; display: block;
position: relative; position: relative;
@ -848,27 +645,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
position: relative; position: relative;
top: 2px; 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 { .active-filters {
color: var(--primary-text-color); color: var(--primary-text-color);
position: relative; 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 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 "@polymer/paper-tooltip/paper-tooltip";
import { mdiAlertCircle, mdiDotsVertical, mdiOpenInNew } from "@mdi/js"; import { mdiAlertCircle, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
import { import {
@ -14,7 +18,9 @@ import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-button-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-card";
import { import {
ConfigEntry, ConfigEntry,
deleteConfigEntry, deleteConfigEntry,
@ -23,8 +29,8 @@ import {
reloadConfigEntry, reloadConfigEntry,
updateConfigEntry, updateConfigEntry,
} from "../../../data/config_entries"; } from "../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { domainToName, IntegrationManifest } from "../../../data/integration"; import { domainToName, IntegrationManifest } from "../../../data/integration";
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
@ -37,48 +43,25 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { ConfigEntryExtended } from "./ha-config-integrations"; import { ConfigEntryExtended } from "./ha-config-integrations";
import {
haConfigIntegrationRenderIcons,
haConfigIntegrationsStyles,
} from "./ha-config-integrations-common";
export interface ConfigEntryUpdatedEvent { const ERROR_STATES: ConfigEntry["state"][] = [
entry: ConfigEntry; "failed_unload",
} "migration_error",
"setup_error",
export interface ConfigEntryRemovedEvent { "setup_retry",
entryId: string; ];
}
declare global {
// for fire event
interface HASSDomEvents {
"entry-updated": ConfigEntryUpdatedEvent;
"entry-removed": ConfigEntryRemovedEvent;
}
}
const integrationsWithPanel = { const integrationsWithPanel = {
hassio: { hassio: "/hassio/dashboard",
buttonLocalizeKey: "ui.panel.config.hassio.button", mqtt: "/config/mqtt",
path: "/hassio/dashboard", zha: "/config/zha/dashboard",
}, ozw: "/config/ozw/dashboard",
mqtt: { zwave: "/config/zwave",
buttonLocalizeKey: "ui.panel.config.mqtt.button", zwave_js: "/config/zwave_js/dashboard",
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",
},
}; };
@customElement("ha-integration-card") @customElement("ha-integration-card")
@ -89,7 +72,7 @@ export class HaIntegrationCard extends LitElement {
@property() public items!: ConfigEntryExtended[]; @property() public items!: ConfigEntryExtended[];
@property() public manifest!: IntegrationManifest; @property() public manifest?: IntegrationManifest;
@property() public entityRegistryEntries!: EntityRegistryEntry[]; @property() public entityRegistryEntries!: EntityRegistryEntry[];
@ -99,46 +82,85 @@ export class HaIntegrationCard extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
}
protected render(): TemplateResult { protected render(): TemplateResult {
let item = this._selectededConfigEntry;
if (this.items.length === 1) { if (this.items.length === 1) {
return this._renderSingleEntry(this.items[0]); item = this.items[0];
} } else if (this.selectedConfigEntryId) {
if (this.selectedConfigEntryId) { item = this.items.find(
const configEntry = this.items.find(
(entry) => entry.entry_id === this.selectedConfigEntryId (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` 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 ${this.disabled
? html`<div class="header"> ? html`
<div class="banner">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled" "ui.panel.config.integrations.config_entry.disable.disabled"
)} )}
</div>` </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 <img
src=${brandsUrl(this.domain, "icon")} src=${brandsUrl(this.domain, "icon")}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
@error=${this._onImageError} @error=${this._onImageError}
@load=${this._onImageLoad} @load=${this._onImageLoad}
/> />
<h2> <div class="info">
${domainToName(this.hass.localize, this.domain)} <div class="primary">${primary}</div>
</h2> ${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
</div> </div>
${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
</div>
${item
? this._renderSingleEntry(item)
: this._renderGroupedIntegration()}
</ha-card>
`;
}
private _renderGroupedIntegration(): TemplateResult {
return html`
<paper-listbox> <paper-listbox>
${this.items.map( ${this.items.map(
(item) => (item) =>
@ -151,7 +173,7 @@ export class HaIntegrationCard extends LitElement {
"ui.panel.config.integrations.config_entry.unnamed_entry" "ui.panel.config.integrations.config_entry.unnamed_entry"
)}</paper-item-body )}</paper-item-body
> >
${item.state === "not_loaded" ${ERROR_STATES.includes(item.state)
? html`<span> ? html`<span>
<ha-svg-icon <ha-svg-icon
class="error" class="error"
@ -159,11 +181,7 @@ export class HaIntegrationCard extends LitElement {
></ha-svg-icon ></ha-svg-icon
><paper-tooltip animation-delay="0" position="left"> ><paper-tooltip animation-delay="0" position="left">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.not_loaded", `ui.panel.config.integrations.config_entry.state.${item.state}`
"logs_link",
this.hass.localize(
"ui.panel.config.integrations.config_entry.logs"
)
)} )}
</paper-tooltip> </paper-tooltip>
</span>` </span>`
@ -172,7 +190,6 @@ export class HaIntegrationCard extends LitElement {
</paper-item>` </paper-item>`
)} )}
</paper-listbox> </paper-listbox>
</ha-card>
`; `;
} }
@ -181,61 +198,48 @@ export class HaIntegrationCard extends LitElement {
const services = this._getServices(item); const services = this._getServices(item);
const entities = this._getEntities(item); const entities = this._getEntities(item);
return html` let stateText: [string, ...unknown[]] | undefined;
<ha-card let stateTextExtra: TemplateResult | string | undefined;
outlined
class="single integration ${classMap({ if (item.disabled_by) {
disabled: Boolean(item.disabled_by), stateText = [
"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>`
: ""}
${item.disabled_by
? html`<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled_cause", "ui.panel.config.integrations.config_entry.disable.disabled_cause",
"cause", "cause",
this.hass.localize( this.hass.localize(
`ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}` `ui.panel.config.integrations.config_entry.disable.disabled_by.${item.disabled_by}`
) || item.disabled_by ) || item.disabled_by,
)} ];
</div>` if (item.state === "failed_unload") {
: item.state === "not_loaded" stateTextExtra = html`.
? html`<div class="header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.not_loaded", "ui.panel.config.integrations.config_entry.disable_restart_confirm"
"logs_link", )}.`;
html`<a href="/config/logs" }
} 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( >${this.hass.localize(
"ui.panel.config.integrations.config_entry.logs" "ui.panel.config.integrations.config_entry.check_the_logs"
)}</a )}</a
>` >
)} `;
</div>` }
: ""}
<div class="card-content"> return html`
<div class="image"> <div class="content">
<img ${stateText
src=${brandsUrl(item.domain, "logo")} ? html`
referrerpolicy="no-referrer" <div class="message">
@error=${this._onImageError} ${this.hass.localize(...stateText)}${stateTextExtra}
@load=${this._onImageLoad}
/>
</div> </div>
<h2> `
${item.localized_domain_name} : ""}
</h2>
<h3>
${item.localized_domain_name === item.title ? "" : item.title}
</h3>
${devices.length || services.length || entities.length ${devices.length || services.length || entities.length
? html` ? html`
<div> <div>
@ -282,26 +286,20 @@ export class HaIntegrationCard extends LitElement {
` `
: ""} : ""}
</div> </div>
<div class="card-actions"> <div class="actions">
<div> <div>
${item.disabled_by === "user" ${item.disabled_by === "user"
? html`<mwc-button unelevated @click=${this._handleEnable}> ? html`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")} ${this.hass.localize("ui.common.enable")}
</mwc-button>` </mwc-button>`
: ""} : item.domain in integrationsWithPanel
<mwc-button @click=${this._editEntryName}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</mwc-button>
${item.domain in integrationsWithPanel
? html`<a ? html`<a
href=${`${ href=${`${
integrationsWithPanel[item.domain].path integrationsWithPanel[item.domain].path
}?config_entry=${item.entry_id}`} }?config_entry=${item.entry_id}`}
><mwc-button> ><mwc-button>
${this.hass.localize( ${this.hass.localize(
integrationsWithPanel[item.domain].buttonLocalizeKey "ui.panel.config.integrations.config_entry.configure"
)} )}
</mwc-button></a </mwc-button></a
>` >`
@ -309,12 +307,15 @@ export class HaIntegrationCard extends LitElement {
? html` ? html`
<mwc-button @click=${this._showOptions}> <mwc-button @click=${this._showOptions}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.options" "ui.panel.config.integrations.config_entry.configure"
)} )}
</mwc-button> </mwc-button>
` `
: ""} : ""}
</div> </div>
${!this.manifest
? ""
: html`
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
<mwc-icon-button <mwc-icon-button
.title=${this.hass.localize("ui.common.menu")} .title=${this.hass.localize("ui.common.menu")}
@ -323,14 +324,17 @@ export class HaIntegrationCard extends LitElement {
> >
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button> </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}"> <mwc-list-item @request-selected="${this._handleSystemOptions}">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options" "ui.panel.config.integrations.config_entry.system_options"
)} )}
</mwc-list-item> </mwc-list-item>
${!this.manifest
? ""
: html`
<a <a
href=${this.manifest.documentation} href=${this.manifest.documentation}
rel="noreferrer" rel="noreferrer"
@ -345,19 +349,22 @@ export class HaIntegrationCard extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
</a> </a>
`}
${!item.disabled_by && ${!item.disabled_by &&
item.state === "loaded" && item.state === "loaded" &&
item.supports_unload && item.supports_unload &&
item.source !== "system" item.source !== "system"
? html`<mwc-list-item @request-selected="${this._handleReload}"> ? html`<mwc-list-item
@request-selected="${this._handleReload}"
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload" "ui.panel.config.integrations.config_entry.reload"
)} )}
</mwc-list-item>` </mwc-list-item>`
: ""} : ""}
${item.disabled_by === "user" ${item.disabled_by === "user"
? html`<mwc-list-item @request-selected="${this._handleEnable}"> ? html`<mwc-list-item
@request-selected="${this._handleEnable}"
>
${this.hass.localize("ui.common.enable")} ${this.hass.localize("ui.common.enable")}
</mwc-list-item>` </mwc-list-item>`
: item.source !== "system" : item.source !== "system"
@ -379,11 +386,21 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>` </mwc-list-item>`
: ""} : ""}
</ha-button-menu> </ha-button-menu>
`}
</div> </div>
</ha-card>
`; `;
} }
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) { private _selectConfigEntry(ev: Event) {
this.selectedConfigEntryId = (ev.currentTarget as any).entryId; this.selectedConfigEntryId = (ev.currentTarget as any).entryId;
} }
@ -588,109 +605,109 @@ export class HaIntegrationCard extends LitElement {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
haConfigIntegrationsStyles,
css` css`
:host {
max-width: 500px;
}
ha-card { ha-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
--state-color: var(--divider-color, #e0e0e0);
--ha-card-border-color: var(--state-color);
--state-message-color: var(--state-color);
} }
ha-card.single { .state-error {
justify-content: space-between; --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 { :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.group {
--ha-card-border-color: var(--warning-color); 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 { .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; display: flex;
justify-content: space-between; position: relative;
align-items: center; align-items: center;
padding-right: 5px; padding: 16px 8px 8px 16px;
} }
.group-header { .group.disabled .header {
display: flex; padding-top: 8px;
align-items: center; }
.header img {
margin-right: 16px;
width: 40px;
height: 40px; height: 40px;
padding: 16px 16px 8px 16px;
justify-content: center;
} }
.group-header h1 { .header .info div,
margin: 0; paper-item-body {
}
.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 {
word-wrap: break-word; word-wrap: break-word;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 3; -webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; 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 { ha-button-menu {
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;
} }
@media (min-width: 563px) { @media (min-width: 563px) {
paper-listbox { paper-listbox {
max-height: 150px; flex: 1;
overflow: auto; overflow: auto;
} }
} }
@ -701,11 +718,6 @@ export class HaIntegrationCard extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); 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]) { if (this.hass.states[entity.entity_id]) {
return computeStateName(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}", "entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"services": "{count} {count, plural,\n one {service}\n other {services}\n}", "services": "{count} {count, plural,\n one {service}\n other {services}\n}",
"rename": "Rename", "rename": "Rename",
"options": "Options", "configure": "Configure",
"system_options": "System options", "system_options": "System options",
"documentation": "Documentation", "documentation": "Documentation",
"delete": "Delete", "delete": "Delete",
@ -2161,8 +2161,8 @@
"entity_unavailable": "Entity unavailable", "entity_unavailable": "Entity unavailable",
"area": "In {area}", "area": "In {area}",
"no_area": "No Area", "no_area": "No Area",
"not_loaded": "Not loaded, check the {logs_link}", "not_loaded": "Not loaded",
"logs": "logs", "check_the_logs": "Check the logs",
"disable": { "disable": {
"disabled": "Disabled", "disabled": "Disabled",
"disabled_cause": "Disabled by {cause}", "disabled_cause": "Disabled by {cause}",
@ -2172,6 +2172,16 @@
"device": "device" "device": "device"
}, },
"disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled." "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": { "config_flow": {
@ -2243,11 +2253,7 @@
"create": "Create" "create": "Create"
} }
}, },
"hassio": {
"button": "Configure"
},
"mqtt": { "mqtt": {
"button": "Configure",
"title": "MQTT", "title": "MQTT",
"description_publish": "Publish a packet", "description_publish": "Publish a packet",
"topic": "topic", "topic": "topic",
@ -2261,7 +2267,6 @@
"message_received": "Message {id} received on {topic} at {time}:" "message_received": "Message {id} received on {topic} at {time}:"
}, },
"ozw": { "ozw": {
"button": "Configure",
"common": { "common": {
"zwave": "Z-Wave", "zwave": "Z-Wave",
"node_id": "Node ID", "node_id": "Node ID",
@ -2376,7 +2381,6 @@
} }
}, },
"zha": { "zha": {
"button": "Configure",
"common": { "common": {
"clusters": "Clusters", "clusters": "Clusters",
"manufacturer_code_override": "Manufacturer Code Override", "manufacturer_code_override": "Manufacturer Code Override",
@ -2464,7 +2468,6 @@
} }
}, },
"zwave": { "zwave": {
"button": "Configure",
"description": "Manage your Z-Wave network", "description": "Manage your Z-Wave network",
"learn_more": "Learn more about Z-Wave", "learn_more": "Learn more about Z-Wave",
"common": { "common": {
@ -2555,7 +2558,6 @@
} }
}, },
"zwave_js": { "zwave_js": {
"button": "Configure",
"navigation": { "navigation": {
"network": "Network" "network": "Network"
}, },