diff --git a/gallery/src/demos/demo-integration-card.ts b/gallery/src/demos/demo-integration-card.ts new file mode 100644 index 0000000000..1e805910a8 --- /dev/null +++ b/gallery/src/demos/demo-integration-card.ts @@ -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 => ({ + 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` +
+ + + + + + +
+ + + + ${configFlows.map( + (flow) => html` + + ` + )} + ${configEntries.map( + (info) => html` + + ` + )} + `; + } + + 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; + } +} diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index d75de78536..603aab6d06 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -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 { diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 8fc1807586..d2d119d300 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -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; } diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 81796ff822..0bf6a5b253 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -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; } diff --git a/src/data/integration.ts b/src/data/integration.ts index d8caf1b591..f19be078eb 100644 --- a/src/data/integration.ts +++ b/src/data/integration.ts @@ -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 = ( diff --git a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts index e60439f2de..99bd51dcb7 100644 --- a/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/panels/config/devices/device-registry-detail/dialog-device-registry-detail.ts @@ -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; diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 1b1690394c..1063639aa4 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -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; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index be295a57a4..c1277e7b8a 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -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, }); diff --git a/src/panels/config/integrations/ha-config-flow-card.ts b/src/panels/config/integrations/ha-config-flow-card.ts new file mode 100644 index 0000000000..fd6769b22c --- /dev/null +++ b/src/panels/config/integrations/ha-config-flow-card.ts @@ -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` + + + ${DISCOVERY_SOURCES.includes(this.flow.context.source) && + this.flow.context.unique_id + ? html` + + ` + : ""} + + `; + } + + 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; + } +} diff --git a/src/panels/config/integrations/ha-config-integrations-common.ts b/src/panels/config/integrations/ha-config-integrations-common.ts new file mode 100644 index 0000000000..063e35c178 --- /dev/null +++ b/src/panels/config/integrations/ha-config-integrations-common.ts @@ -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` +
+ ${icons.map( + ([icon, description]) => html` + + + ${description} + + ` + )} +
+ `; +}; diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index b0823087f0..845319c01a 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -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 = {}; @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 )} - - ${this.hass.localize( + + > ` : ""} ${filterMenu} @@ -362,112 +361,30 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
${this._showIgnored ? ignoredConfigEntries.map( - (item: ConfigEntryExtended) => html` - -
- ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignored" - )} -
-
-
- -
-

- ${// 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} -

- ${this.hass.localize( - "ui.panel.config.integrations.ignore.stop_ignore" - )} -
-
+ (entry: ConfigEntryExtended) => html` + ` ) : ""} ${configEntriesInProgress.length ? configEntriesInProgress.map( - (flow: DataEntryFlowProgressExtended) => { - const attention = ATTENTION_SOURCES.includes( - flow.context.source - ); - return html` - -
- ${this.hass.localize( - `ui.panel.config.integrations.${ - attention ? "attention" : "discovered" - }` - )} -
-
-
- -
-

- ${flow.localized_title} -

-
- - ${this.hass.localize( - `ui.panel.config.integrations.${ - attention ? "reconfigure" : "configure" - }` - )} - - ${DISCOVERY_SOURCES.includes(flow.context.source) && - flow.context.unique_id - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignore" - )} - - ` - : ""} -
-
-
- `; - } + (flow: DataEntryFlowProgressExtended) => html` + + ` ) : ""} ${this._showDisabled @@ -498,25 +415,28 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { .deviceRegistryEntries=${this._deviceRegistryEntries} >` ) - : !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` - -
-

- ${this.hass.localize("ui.panel.config.integrations.none")} -

-

- ${this.hass.localize( - "ui.panel.config.integrations.no_integrations" - )} -

- ${this.hass.localize( - "ui.panel.config.integrations.add_integration" - )} -
-
+
+

+ ${this.hass.localize("ui.panel.config.integrations.none")} +

+

+ ${this.hass.localize( + "ui.panel.config.integrations.no_integrations" + )} +

+ +
` : ""} ${this._filter && @@ -524,7 +444,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { !groupedConfigEntries.size && this._configEntries.length ? html` -
+

${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) { + private _handleEntryRemoved(ev: HASSDomEvent) { this._configEntries = this._configEntries!.filter( (entry) => entry.entry_id !== ev.detail.entryId ); } - private _handleUpdated(ev: HASSDomEvent) { + private _handleEntryUpdated(ev: HASSDomEvent) { 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) { 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; diff --git a/src/panels/config/integrations/ha-ignored-config-entry-card.ts b/src/panels/config/integrations/ha-ignored-config-entry-card.ts new file mode 100644 index 0000000000..e4332bd65e --- /dev/null +++ b/src/panels/config/integrations/ha-ignored-config-entry-card.ts @@ -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` + + + + `; + } + + 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; + } +} diff --git a/src/panels/config/integrations/ha-integration-action-card.ts b/src/panels/config/integrations/ha-integration-action-card.ts new file mode 100644 index 0000000000..f3223f7f2c --- /dev/null +++ b/src/panels/config/integrations/ha-integration-action-card.ts @@ -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` + + +
+ ${haConfigIntegrationRenderIcons(this.hass, this.manifest)} +
+ +
+

${this.label}

+
+
+
+ `; + } + + 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; + } +} diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index be687830cc..fc975fb6bc 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -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` - + ${this.disabled - ? html`
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.disable.disabled" - )} -
` + ? html` + + ` : ""} -
+ ${this.items.length > 1 + ? html` +
+ +
+ ` + : ""} +
-

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

+
+
${primary}
+ ${secondary ? html`
${secondary}
` : ""} +
+ ${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
- - ${this.items.map( - (item) => - html`${item.title || - this.hass.localize( - "ui.panel.config.integrations.config_entry.unnamed_entry" - )} - ${item.state === "not_loaded" - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.not_loaded", - "logs_link", - this.hass.localize( - "ui.panel.config.integrations.config_entry.logs" - ) - )} - - ` - : ""} - - ` - )} - + ${item + ? this._renderSingleEntry(item) + : this._renderGroupedIntegration()} `; } + private _renderGroupedIntegration(): TemplateResult { + return html` + + ${this.items.map( + (item) => + html`${item.title || + this.hass.localize( + "ui.panel.config.integrations.config_entry.unnamed_entry" + )} + ${ERROR_STATES.includes(item.state) + ? html` + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.state.${item.state}` + )} + + ` + : ""} + + ` + )} + + `; + } + 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` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.check_the_logs" + )} + `; + } + return html` - - ${this.items.length > 1 - ? html`` +
+ ${stateText + ? html` +
+ ${this.hass.localize(...stateText)}${stateTextExtra} +
+ ` : ""} - ${item.disabled_by - ? html`
- ${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 - )} -
` - : item.state === "not_loaded" - ? html`
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.not_loaded", - "logs_link", - html`${this.hass.localize( - "ui.panel.config.integrations.config_entry.logs" - )}` - )} -
` + ${devices.length || services.length || entities.length + ? html` +
+ ${devices.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.devices", + "count", + devices.length + )}${services.length ? "," : ""} + ` + : ""} + ${services.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.services", + "count", + services.length + )} + ` + : ""} + ${(devices.length || services.length) && entities.length + ? this.hass.localize("ui.common.and") + : ""} + ${entities.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.entities", + "count", + entities.length + )} + ` + : ""} +
+ ` : ""} -
-
- -
-

- ${item.localized_domain_name} -

-

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

- ${devices.length || services.length || entities.length +
+
+
+ ${item.disabled_by === "user" + ? html` + ${this.hass.localize("ui.common.enable")} + ` + : item.domain in integrationsWithPanel + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.configure" + )} + ` + : item.supports_options ? html` -
- ${devices.length - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.devices", - "count", - devices.length - )}${services.length ? "," : ""} - ` - : ""} - ${services.length - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.services", - "count", - services.length - )} - ` - : ""} - ${(devices.length || services.length) && entities.length - ? this.hass.localize("ui.common.and") - : ""} - ${entities.length - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.entities", - "count", - entities.length - )} - ` - : ""} -
+ + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.configure" + )} + ` : ""}
-
-
- ${item.disabled_by === "user" - ? html` - ${this.hass.localize("ui.common.enable")} - ` - : ""} - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.rename" - )} - - ${item.domain in integrationsWithPanel - ? html` + ${!this.manifest + ? "" + : html` + + + + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.rename" + )} + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.system_options" + )} + + + + ${this.hass.localize( - integrationsWithPanel[item.domain].buttonLocalizeKey - )} - ` - : item.supports_options - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.options" - )} - - ` - : ""} -
- - - - - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.system_options" - )} - - ${!this.manifest - ? "" - : html` - - + "ui.panel.config.integrations.config_entry.documentation" + )} + + + ${!item.disabled_by && + item.state === "loaded" && + item.supports_unload && + item.source !== "system" + ? html` ${this.hass.localize( - "ui.panel.config.integrations.config_entry.documentation" - )} - - - `} - ${!item.disabled_by && - item.state === "loaded" && - item.supports_unload && - item.source !== "system" - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.reload" - )} - ` - : ""} - ${item.disabled_by === "user" - ? html` - ${this.hass.localize("ui.common.enable")} - ` - : item.source !== "system" - ? html` - ${this.hass.localize("ui.common.disable")} - ` - : ""} - ${item.source !== "system" - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.delete" - )} - ` - : ""} - -
- + "ui.panel.config.integrations.config_entry.reload" + )} + ` + : ""} + ${item.disabled_by === "user" + ? html` + ${this.hass.localize("ui.common.enable")} + ` + : item.source !== "system" + ? html` + ${this.hass.localize("ui.common.disable")} + ` + : ""} + ${item.source !== "system" + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.delete" + )} + ` + : ""} + + `} +
`; } + 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%; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts index d2c37635ac..c810a397ee 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts @@ -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]); } diff --git a/src/translations/en.json b/src/translations/en.json index a48c21b76b..d6a4e3f0e0 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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" },