mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 08:29:52 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			20251029.0
			...
			fixes-inte
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cdf53773c5 | ||
| 
						 | 
					3174037c54 | ||
| 
						 | 
					4af060890a | 
@@ -44,9 +44,10 @@ const createConfigEntry = (
 | 
			
		||||
 | 
			
		||||
const createManifest = (
 | 
			
		||||
  isCustom: boolean,
 | 
			
		||||
  isCloud: boolean
 | 
			
		||||
  isCloud: boolean,
 | 
			
		||||
  name = "ESPHome"
 | 
			
		||||
): IntegrationManifest => ({
 | 
			
		||||
  name: "ESPHome",
 | 
			
		||||
  name,
 | 
			
		||||
  domain: "esphome",
 | 
			
		||||
  is_built_in: !isCustom,
 | 
			
		||||
  config_flow: false,
 | 
			
		||||
@@ -103,7 +104,7 @@ const configFlows: DataEntryFlowProgressExtended[] = [
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    step_id: "discovery_confirm",
 | 
			
		||||
    localized_title: "Roku: Living room Roku",
 | 
			
		||||
    localized_title: "Living room Roku",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    flow_id: "adbb401329d8439ebb78ef29837826a8",
 | 
			
		||||
@@ -139,9 +140,9 @@ const configEntries: Array<{
 | 
			
		||||
  {
 | 
			
		||||
    items: [
 | 
			
		||||
      loadedEntry,
 | 
			
		||||
      longNameEntry,
 | 
			
		||||
      setupErrorEntry,
 | 
			
		||||
      migrationErrorEntry,
 | 
			
		||||
      longNameEntry,
 | 
			
		||||
      setupRetryEntry,
 | 
			
		||||
      failedUnloadEntry,
 | 
			
		||||
      notLoadedEntry,
 | 
			
		||||
@@ -211,47 +212,78 @@ export class DemoIntegrationCard extends LitElement {
 | 
			
		||||
      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 class="container">
 | 
			
		||||
        <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,
 | 
			
		||||
                flow.handler === "roku" ? "Roku" : "Philips Hue"
 | 
			
		||||
              )}
 | 
			
		||||
            ></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>
 | 
			
		||||
          `
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <!-- One that is standalone to see how it increases height if height
 | 
			
		||||
           not defined by other cards. -->
 | 
			
		||||
        <ha-integration-card
 | 
			
		||||
          .hass=${this.hass}
 | 
			
		||||
          domain="esphome"
 | 
			
		||||
          .items=${[
 | 
			
		||||
            loadedEntry,
 | 
			
		||||
            setupErrorEntry,
 | 
			
		||||
            migrationErrorEntry,
 | 
			
		||||
            setupRetryEntry,
 | 
			
		||||
            failedUnloadEntry,
 | 
			
		||||
          ]}
 | 
			
		||||
          .manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
 | 
			
		||||
          .entityRegistryEntries=${createEntityRegistryEntries(loadedEntry)}
 | 
			
		||||
          .deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
 | 
			
		||||
        ></ha-integration-card>
 | 
			
		||||
      </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>
 | 
			
		||||
        `
 | 
			
		||||
      )}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -272,7 +304,7 @@ export class DemoIntegrationCard extends LitElement {
 | 
			
		||||
 | 
			
		||||
  static get styles() {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
      .container {
 | 
			
		||||
        display: grid;
 | 
			
		||||
        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 | 
			
		||||
        grid-gap: 16px 16px;
 | 
			
		||||
@@ -280,7 +312,7 @@ export class DemoIntegrationCard extends LitElement {
 | 
			
		||||
        margin-bottom: 64px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :host > * {
 | 
			
		||||
      .container > * {
 | 
			
		||||
        max-width: 500px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
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_integration"
 | 
			
		||||
        ),
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
      `;
 | 
			
		||||
};
 | 
			
		||||
@@ -31,6 +31,7 @@ export class HaIgnoredConfigEntryCard extends LitElement {
 | 
			
		||||
          "ui.panel.config.integrations.ignore.ignored"
 | 
			
		||||
        )}
 | 
			
		||||
        .domain=${this.entry.domain}
 | 
			
		||||
        .localizedDomainName=${this.entry.localized_domain_name}
 | 
			
		||||
        .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.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,14 @@
 | 
			
		||||
import {
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
  html,
 | 
			
		||||
  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";
 | 
			
		||||
import type { IntegrationManifest } from "../../../data/integration";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import "./ha-integration-header";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-integration-action-card")
 | 
			
		||||
export class HaIntegrationActionCard extends LitElement {
 | 
			
		||||
@@ -20,6 +16,8 @@ export class HaIntegrationActionCard extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public banner!: string;
 | 
			
		||||
 | 
			
		||||
  @property() public localizedDomainName?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public domain!: string;
 | 
			
		||||
 | 
			
		||||
  @property() public label!: string;
 | 
			
		||||
@@ -29,82 +27,48 @@ export class HaIntegrationActionCard extends LitElement {
 | 
			
		||||
  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>
 | 
			
		||||
        <ha-integration-header
 | 
			
		||||
          .hass=${this.hass}
 | 
			
		||||
          .banner=${this.banner}
 | 
			
		||||
          .domain=${this.domain}
 | 
			
		||||
          .label=${this.label}
 | 
			
		||||
          .localizedDomainName=${this.localizedDomainName}
 | 
			
		||||
          .manifest=${this.manifest}
 | 
			
		||||
        ></ha-integration-header>
 | 
			
		||||
        <div class="content"></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;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  static styles = 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;
 | 
			
		||||
    }
 | 
			
		||||
    .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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ import {
 | 
			
		||||
} from "../../../data/config_entries";
 | 
			
		||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
 | 
			
		||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
 | 
			
		||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
 | 
			
		||||
import type { 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";
 | 
			
		||||
import {
 | 
			
		||||
@@ -40,16 +40,11 @@ import {
 | 
			
		||||
  showPromptDialog,
 | 
			
		||||
} from "../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
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";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
 | 
			
		||||
import "./ha-integration-header";
 | 
			
		||||
 | 
			
		||||
const ERROR_STATES: ConfigEntry["state"][] = [
 | 
			
		||||
  "failed_unload",
 | 
			
		||||
  "migration_error",
 | 
			
		||||
  "setup_error",
 | 
			
		||||
  "setup_retry",
 | 
			
		||||
@@ -93,18 +88,6 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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`
 | 
			
		||||
@@ -116,42 +99,37 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
          hasMultiple: this.items.length > 1,
 | 
			
		||||
          disabled: this.disabled,
 | 
			
		||||
          "state-not-loaded": hasItem && item!.state === "not_loaded",
 | 
			
		||||
          "state-failed-unload": hasItem && item!.state === "failed_unload",
 | 
			
		||||
          "state-error": hasItem && ERROR_STATES.includes(item!.state),
 | 
			
		||||
        })}"
 | 
			
		||||
        .configEntry=${item}
 | 
			
		||||
      >
 | 
			
		||||
        ${this.disabled
 | 
			
		||||
          ? html`
 | 
			
		||||
              <div class="banner">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.integrations.config_entry.disable.disabled"
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            `
 | 
			
		||||
          : ""}
 | 
			
		||||
        ${this.items.length > 1
 | 
			
		||||
          ? html`
 | 
			
		||||
              <div class="back-btn">
 | 
			
		||||
                <ha-icon-button
 | 
			
		||||
                  icon="hass:chevron-left"
 | 
			
		||||
                  @click=${this._back}
 | 
			
		||||
                ></ha-icon-button>
 | 
			
		||||
              </div>
 | 
			
		||||
            `
 | 
			
		||||
          : ""}
 | 
			
		||||
        <div class="header">
 | 
			
		||||
          <img
 | 
			
		||||
            src=${brandsUrl(this.domain, "icon")}
 | 
			
		||||
            referrerpolicy="no-referrer"
 | 
			
		||||
            @error=${this._onImageError}
 | 
			
		||||
            @load=${this._onImageLoad}
 | 
			
		||||
          />
 | 
			
		||||
          <div class="info">
 | 
			
		||||
            <div class="primary">${primary}</div>
 | 
			
		||||
            ${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
 | 
			
		||||
          </div>
 | 
			
		||||
          ${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
 | 
			
		||||
        </div>
 | 
			
		||||
        <ha-integration-header
 | 
			
		||||
          .hass=${this.hass}
 | 
			
		||||
          .banner=${this.disabled
 | 
			
		||||
            ? this.hass.localize(
 | 
			
		||||
                "ui.panel.config.integrations.config_entry.disable.disabled"
 | 
			
		||||
              )
 | 
			
		||||
            : undefined}
 | 
			
		||||
          .domain=${this.domain}
 | 
			
		||||
          .label=${item
 | 
			
		||||
            ? item.title || item.localized_domain_name || this.domain
 | 
			
		||||
            : undefined}
 | 
			
		||||
          .localizedDomainName=${item ? item.localized_domain_name : undefined}
 | 
			
		||||
          .manifest=${this.manifest}
 | 
			
		||||
        >
 | 
			
		||||
          ${this.items.length > 1
 | 
			
		||||
            ? html`
 | 
			
		||||
                <div class="back-btn" slot="above-header">
 | 
			
		||||
                  <ha-icon-button
 | 
			
		||||
                    icon="hass:chevron-left"
 | 
			
		||||
                    @click=${this._back}
 | 
			
		||||
                  ></ha-icon-button>
 | 
			
		||||
                </div>
 | 
			
		||||
              `
 | 
			
		||||
            : ""}
 | 
			
		||||
        </ha-integration-header>
 | 
			
		||||
 | 
			
		||||
        ${item
 | 
			
		||||
          ? this._renderSingleEntry(item)
 | 
			
		||||
          : this._renderGroupedIntegration()}
 | 
			
		||||
@@ -441,14 +419,6 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onImageLoad(ev) {
 | 
			
		||||
    ev.target.style.visibility = "initial";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onImageError(ev) {
 | 
			
		||||
    ev.target.style.visibility = "hidden";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _showOptions(ev) {
 | 
			
		||||
    showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
 | 
			
		||||
  }
 | 
			
		||||
@@ -605,7 +575,6 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
  static get styles(): CSSResult[] {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
      haConfigIntegrationsStyles,
 | 
			
		||||
      css`
 | 
			
		||||
        ha-card {
 | 
			
		||||
          display: flex;
 | 
			
		||||
@@ -619,16 +588,17 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
          --state-color: var(--error-color);
 | 
			
		||||
          --text-on-state-color: var(--text-primary-color);
 | 
			
		||||
        }
 | 
			
		||||
        .state-failed-unload {
 | 
			
		||||
          --state-color: var(--warning-color);
 | 
			
		||||
          --text-on-state-color: var(--primary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        .state-not-loaded {
 | 
			
		||||
          --state-message-color: var(--primary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        :host(.highlight) ha-card {
 | 
			
		||||
          --state-color: var(--accent-color);
 | 
			
		||||
          --state-color: var(--primary-color);
 | 
			
		||||
          --text-on-state-color: var(--text-primary-color);
 | 
			
		||||
        }
 | 
			
		||||
        ha-card.group {
 | 
			
		||||
          max-height: 200px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .back-btn {
 | 
			
		||||
          background-color: var(--state-color);
 | 
			
		||||
@@ -644,39 +614,6 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
          height: 0px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          position: relative;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          padding: 16px 8px 8px 16px;
 | 
			
		||||
        }
 | 
			
		||||
        .group.disabled .header {
 | 
			
		||||
          padding-top: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .header img {
 | 
			
		||||
          margin-right: 16px;
 | 
			
		||||
          width: 40px;
 | 
			
		||||
          height: 40px;
 | 
			
		||||
        }
 | 
			
		||||
        .header .info div,
 | 
			
		||||
        paper-item-body {
 | 
			
		||||
          word-wrap: break-word;
 | 
			
		||||
          display: -webkit-box;
 | 
			
		||||
          -webkit-box-orient: vertical;
 | 
			
		||||
          -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;
 | 
			
		||||
@@ -706,15 +643,34 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
          --mdc-menu-min-width: 200px;
 | 
			
		||||
        }
 | 
			
		||||
        @media (min-width: 563px) {
 | 
			
		||||
          ha-card.group {
 | 
			
		||||
            position: relative;
 | 
			
		||||
            min-height: 164px;
 | 
			
		||||
          }
 | 
			
		||||
          paper-listbox {
 | 
			
		||||
            flex: 1;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 64px;
 | 
			
		||||
            left: 0;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            bottom: 0;
 | 
			
		||||
            overflow: auto;
 | 
			
		||||
          }
 | 
			
		||||
          .disabled paper-listbox {
 | 
			
		||||
            top: 100px;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        paper-item {
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
          min-height: 35px;
 | 
			
		||||
        }
 | 
			
		||||
        paper-item-body {
 | 
			
		||||
          word-wrap: break-word;
 | 
			
		||||
          display: -webkit-box;
 | 
			
		||||
          -webkit-box-orient: vertical;
 | 
			
		||||
          -webkit-line-clamp: 2;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
        }
 | 
			
		||||
        mwc-list-item ha-svg-icon {
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										174
									
								
								src/panels/config/integrations/ha-integration-header.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/panels/config/integrations/ha-integration-header.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
import { mdiPackageVariant, mdiCloud } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-tooltip/paper-tooltip";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  html,
 | 
			
		||||
  customElement,
 | 
			
		||||
  property,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
import { brandsUrl } from "../../../util/brands-url";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-integration-header")
 | 
			
		||||
export class HaIntegrationHeader extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public banner!: string;
 | 
			
		||||
 | 
			
		||||
  @property() public localizedDomainName?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public domain!: string;
 | 
			
		||||
 | 
			
		||||
  @property() public label!: string;
 | 
			
		||||
 | 
			
		||||
  @property() public manifest?: IntegrationManifest;
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    let primary: string;
 | 
			
		||||
    let secondary: string | undefined;
 | 
			
		||||
 | 
			
		||||
    const domainName =
 | 
			
		||||
      this.localizedDomainName ||
 | 
			
		||||
      domainToName(this.hass.localize, this.domain, this.manifest);
 | 
			
		||||
 | 
			
		||||
    if (this.label) {
 | 
			
		||||
      primary = this.label;
 | 
			
		||||
      secondary = primary === domainName ? undefined : domainName;
 | 
			
		||||
    } else {
 | 
			
		||||
      primary = domainName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const icons: [string, string][] = [];
 | 
			
		||||
 | 
			
		||||
    if (this.manifest) {
 | 
			
		||||
      if (!this.manifest.is_built_in) {
 | 
			
		||||
        icons.push([
 | 
			
		||||
          mdiPackageVariant,
 | 
			
		||||
          this.hass.localize(
 | 
			
		||||
            "ui.panel.config.integrations.config_entry.provided_by_custom_integration"
 | 
			
		||||
          ),
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        this.manifest.iot_class &&
 | 
			
		||||
        this.manifest.iot_class.startsWith("cloud_")
 | 
			
		||||
      ) {
 | 
			
		||||
        icons.push([
 | 
			
		||||
          mdiCloud,
 | 
			
		||||
          this.hass.localize(
 | 
			
		||||
            "ui.panel.config.integrations.config_entry.depends_on_cloud"
 | 
			
		||||
          ),
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      ${!this.banner
 | 
			
		||||
        ? ""
 | 
			
		||||
        : html`<div class="banner">
 | 
			
		||||
            ${this.banner}
 | 
			
		||||
          </div>`}
 | 
			
		||||
      <slot name="above-header"></slot>
 | 
			
		||||
      <div class="header">
 | 
			
		||||
        <img
 | 
			
		||||
          src=${brandsUrl(this.domain, "icon")}
 | 
			
		||||
          referrerpolicy="no-referrer"
 | 
			
		||||
          @error=${this._onImageError}
 | 
			
		||||
          @load=${this._onImageLoad}
 | 
			
		||||
        />
 | 
			
		||||
        <div class="info">
 | 
			
		||||
          <div class="primary">${primary}</div>
 | 
			
		||||
          ${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
 | 
			
		||||
        </div>
 | 
			
		||||
        ${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>
 | 
			
		||||
            `}
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onImageLoad(ev) {
 | 
			
		||||
    ev.target.style.visibility = "initial";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onImageError(ev) {
 | 
			
		||||
    ev.target.style.visibility = "hidden";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    .banner {
 | 
			
		||||
      background-color: var(--state-color);
 | 
			
		||||
      color: var(--text-on-state-color);
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      padding: 8px;
 | 
			
		||||
    }
 | 
			
		||||
    .header {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: relative;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      padding: 16px 8px 8px 16px;
 | 
			
		||||
    }
 | 
			
		||||
    .header img {
 | 
			
		||||
      margin-right: 16px;
 | 
			
		||||
      width: 40px;
 | 
			
		||||
      height: 40px;
 | 
			
		||||
    }
 | 
			
		||||
    .header .info div {
 | 
			
		||||
      word-wrap: break-word;
 | 
			
		||||
      display: -webkit-box;
 | 
			
		||||
      -webkit-box-orient: vertical;
 | 
			
		||||
      -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);
 | 
			
		||||
    }
 | 
			
		||||
    .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;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-integration-header": HaIntegrationHeader;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user