mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 08:33:31 +01:00
392 lines
13 KiB
TypeScript
392 lines
13 KiB
TypeScript
import { mdiFileCodeOutline, mdiPackageVariant, mdiWeb } from "@mdi/js";
|
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
|
import { LitElement, css, html, nothing } from "lit";
|
|
import { customElement, property } from "lit/decorators";
|
|
import { classMap } from "lit/directives/class-map";
|
|
import memoizeOne from "memoize-one";
|
|
import { PROTOCOL_INTEGRATIONS } from "../../../common/integrations/protocolIntegrationPicked";
|
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
|
import "../../../components/ha-button";
|
|
import "../../../components/ha-card";
|
|
import "../../../components/ha-ripple";
|
|
import "../../../components/ha-svg-icon";
|
|
import "../../../components/ha-tooltip";
|
|
import type { ConfigEntry } from "../../../data/config_entries";
|
|
import { ERROR_STATES } from "../../../data/config_entries";
|
|
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
|
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
|
import type {
|
|
IntegrationLogInfo,
|
|
IntegrationManifest,
|
|
} from "../../../data/integration";
|
|
import { LogSeverity } from "../../../data/integration";
|
|
import { haStyle } from "../../../resources/styles";
|
|
import type { HomeAssistant } from "../../../types";
|
|
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
|
import "./ha-integration-header";
|
|
|
|
@customElement("ha-integration-card")
|
|
export class HaIntegrationCard extends LitElement {
|
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
|
|
@property() public domain!: string;
|
|
|
|
@property({ attribute: false }) public items!: ConfigEntryExtended[];
|
|
|
|
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
|
|
|
@property({ attribute: false })
|
|
public entityRegistryEntries!: EntityRegistryEntry[];
|
|
|
|
@property({ attribute: "supports-diagnostics", type: Boolean })
|
|
public supportsDiagnostics = false;
|
|
|
|
@property({ attribute: false }) public logInfo?: IntegrationLogInfo;
|
|
|
|
@property({ attribute: false }) public domainEntities: string[] = [];
|
|
|
|
protected render(): TemplateResult {
|
|
const entryState = this._getState(this.items);
|
|
|
|
const debugLoggingEnabled =
|
|
this.logInfo && this.logInfo.level === LogSeverity.DEBUG;
|
|
|
|
return html`
|
|
<ha-card
|
|
outlined
|
|
class=${classMap({
|
|
"state-loaded": entryState === "loaded",
|
|
"state-not-loaded": entryState === "not_loaded",
|
|
"state-failed-unload": entryState === "failed_unload",
|
|
"state-setup": entryState === "setup_in_progress",
|
|
"state-error": ERROR_STATES.includes(entryState),
|
|
"debug-logging": Boolean(debugLoggingEnabled),
|
|
})}
|
|
>
|
|
<a
|
|
href=${`/config/integrations/integration/${this.domain}`}
|
|
class="ripple-anchor"
|
|
>
|
|
<ha-ripple></ha-ripple>
|
|
<ha-integration-header
|
|
.hass=${this.hass}
|
|
.domain=${this.domain}
|
|
.localizedDomainName=${this.items[0].localized_domain_name}
|
|
.error=${ERROR_STATES.includes(entryState)
|
|
? this.hass.localize(
|
|
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
|
)
|
|
: undefined}
|
|
.warning=${entryState !== "loaded" &&
|
|
!ERROR_STATES.includes(entryState)
|
|
? this.hass.localize(
|
|
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
|
)
|
|
: debugLoggingEnabled
|
|
? this.hass.localize(
|
|
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
|
)
|
|
: undefined}
|
|
.manifest=${this.manifest}
|
|
>
|
|
</ha-integration-header>
|
|
</a>
|
|
|
|
${this._renderSingleEntry()}
|
|
</ha-card>
|
|
`;
|
|
}
|
|
|
|
private _renderSingleEntry(): TemplateResult {
|
|
const devices = this._getDevices(this.items, this.hass.devices);
|
|
const entitiesCount = devices.length
|
|
? 0
|
|
: this._getEntityCount(
|
|
this.items,
|
|
this.entityRegistryEntries,
|
|
this.domainEntities
|
|
);
|
|
|
|
const services = !devices.some((device) => device.entry_type !== "service");
|
|
|
|
return html`
|
|
<div class="card-actions">
|
|
${devices.length > 0
|
|
? html`<ha-button
|
|
appearance="plain"
|
|
href=${devices.length === 1 &&
|
|
// Always link to device page for protocol integrations to show Add Device button
|
|
// @ts-expect-error
|
|
!PROTOCOL_INTEGRATIONS.includes(this.domain)
|
|
? `/config/devices/device/${devices[0].id}`
|
|
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`}
|
|
>
|
|
${this.hass.localize(
|
|
`ui.panel.config.integrations.config_entry.${
|
|
services ? "services" : "devices"
|
|
}`,
|
|
{ count: devices.length }
|
|
)}
|
|
</ha-button>`
|
|
: entitiesCount > 0
|
|
? html`<ha-button
|
|
appearance="plain"
|
|
href=${`/config/entities?historyBack=1&domain=${this.domain}`}
|
|
>
|
|
${this.hass.localize(
|
|
`ui.panel.config.integrations.config_entry.entities`,
|
|
{ count: entitiesCount }
|
|
)}
|
|
</ha-button>`
|
|
: this.items.find((itm) => itm.source !== "yaml")
|
|
? html`<ha-button
|
|
appearance="plain"
|
|
href=${`/config/integrations/integration/${this.domain}`}
|
|
>
|
|
${this.hass.localize(
|
|
`ui.panel.config.integrations.config_entry.entries`,
|
|
{
|
|
count: this.items.filter((itm) => itm.source !== "yaml")
|
|
.length,
|
|
}
|
|
)}
|
|
</ha-button>`
|
|
: html`<div class="spacer"></div>`}
|
|
<div class="icons">
|
|
${this.manifest && !this.manifest.is_built_in
|
|
? html`<span
|
|
class="icon ${this.manifest.overwrites_built_in
|
|
? "overwrites"
|
|
: "custom"}"
|
|
>
|
|
<ha-svg-icon
|
|
id="icon-custom"
|
|
.path=${mdiPackageVariant}
|
|
></ha-svg-icon>
|
|
<ha-tooltip
|
|
for="icon-custom"
|
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
|
>
|
|
${this.hass.localize(
|
|
this.manifest.overwrites_built_in
|
|
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
|
: "ui.panel.config.integrations.config_entry.custom_integration"
|
|
)}
|
|
</ha-tooltip>
|
|
</span>`
|
|
: nothing}
|
|
${this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
|
? html`<div class="icon cloud">
|
|
<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
|
<ha-tooltip
|
|
for="icon-cloud"
|
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
|
>
|
|
${this.hass.localize(
|
|
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
|
)}
|
|
</ha-tooltip>
|
|
</div>`
|
|
: nothing}
|
|
${this.manifest &&
|
|
!this.manifest?.config_flow &&
|
|
!this.items.every((itm) => itm.source === "system")
|
|
? html`<div class="icon yaml">
|
|
<ha-svg-icon
|
|
id="icon-yaml"
|
|
.path=${mdiFileCodeOutline}
|
|
></ha-svg-icon>
|
|
<ha-tooltip
|
|
for="icon-yaml"
|
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
|
>
|
|
${this.hass.localize(
|
|
"ui.panel.config.integrations.config_entry.no_config_flow"
|
|
)}
|
|
</ha-tooltip>
|
|
</div>`
|
|
: nothing}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private _getState = memoizeOne(
|
|
(configEntry: ConfigEntry[]): ConfigEntry["state"] => {
|
|
if (configEntry.length === 1) {
|
|
return configEntry[0].state;
|
|
}
|
|
let entryState: ConfigEntry["state"];
|
|
for (const entry of configEntry) {
|
|
if (ERROR_STATES.includes(entry.state)) {
|
|
return entry.state;
|
|
}
|
|
entryState = entry.state;
|
|
}
|
|
return entryState!;
|
|
}
|
|
);
|
|
|
|
private _getEntityCount = memoizeOne(
|
|
(
|
|
configEntry: ConfigEntry[],
|
|
entityRegistryEntries: EntityRegistryEntry[],
|
|
domainEntities: string[]
|
|
): number => {
|
|
if (!entityRegistryEntries) {
|
|
return domainEntities.length;
|
|
}
|
|
|
|
const entryIds = configEntry
|
|
.map((entry) => entry.entry_id)
|
|
.filter(Boolean);
|
|
|
|
if (!entryIds.length) {
|
|
return domainEntities.length;
|
|
}
|
|
|
|
const entityRegEntities = entityRegistryEntries.filter(
|
|
(entity) =>
|
|
entity.config_entry_id && entryIds.includes(entity.config_entry_id)
|
|
);
|
|
|
|
if (entityRegEntities.length === domainEntities.length) {
|
|
return domainEntities.length;
|
|
}
|
|
|
|
const entityIds = new Set<string>(
|
|
entityRegEntities.map((reg) => reg.entity_id)
|
|
);
|
|
|
|
for (const entity of domainEntities) {
|
|
entityIds.add(entity);
|
|
}
|
|
|
|
return entityIds.size;
|
|
}
|
|
);
|
|
|
|
private _getDevices = memoizeOne(
|
|
(
|
|
configEntry: ConfigEntry[],
|
|
deviceRegistryEntries: HomeAssistant["devices"]
|
|
): DeviceRegistryEntry[] => {
|
|
if (!deviceRegistryEntries) {
|
|
return [];
|
|
}
|
|
const entryIds = configEntry.map((entry) => entry.entry_id);
|
|
return Object.values(deviceRegistryEntries).filter((device) =>
|
|
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
|
);
|
|
}
|
|
);
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
haStyle,
|
|
css`
|
|
ha-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
--state-color: var(--divider-color, #e0e0e0);
|
|
--state-message-color: var(--state-color);
|
|
}
|
|
.ripple-anchor {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
outline: none;
|
|
}
|
|
.ripple-anchor:focus-visible:before {
|
|
position: absolute;
|
|
display: block;
|
|
content: "";
|
|
inset: 0;
|
|
background-color: var(--secondary-text-color);
|
|
opacity: 0.08;
|
|
}
|
|
ha-integration-header {
|
|
height: 100%;
|
|
}
|
|
.card-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.debug-logging {
|
|
--state-color: var(--warning-color);
|
|
--ha-card-border-color: var(--state-color);
|
|
--text-on-state-color: var(--primary-text-color);
|
|
}
|
|
.state-error {
|
|
--state-color: var(--error-color);
|
|
--ha-card-border-color: var(--state-color);
|
|
--text-on-state-color: var(--text-primary-color);
|
|
}
|
|
.state-failed-unload {
|
|
--state-color: var(--warning-color);
|
|
--ha-card-border-color: var(--state-color);
|
|
--text-on-state-color: var(--primary-text-color);
|
|
}
|
|
.state-not-loaded {
|
|
opacity: 0.8;
|
|
--state-color: var(--warning-color);
|
|
--ha-card-border-color: var(--state-color);
|
|
--state-message-color: var(--primary-text-color);
|
|
}
|
|
.state-setup {
|
|
opacity: 0.8;
|
|
--state-message-color: var(--secondary-text-color);
|
|
}
|
|
:host(.highlight) ha-card {
|
|
--state-color: var(--primary-color);
|
|
--ha-card-border-color: var(--state-color);
|
|
--text-on-state-color: var(--text-primary-color);
|
|
}
|
|
.content {
|
|
flex: 1;
|
|
--mdc-list-side-padding-right: 20px;
|
|
--mdc-list-side-padding-left: 24px;
|
|
--mdc-list-item-graphic-margin: 24px;
|
|
}
|
|
a {
|
|
text-decoration: none;
|
|
color: var(--primary-text-color);
|
|
}
|
|
.icons {
|
|
display: flex;
|
|
}
|
|
.icon {
|
|
color: var(--label-badge-grey);
|
|
padding: 4px;
|
|
margin-left: 8px;
|
|
margin-inline-start: 8px;
|
|
margin-inline-end: initial;
|
|
}
|
|
.icon.custom {
|
|
color: var(--warning-color);
|
|
}
|
|
.icon.overwrites {
|
|
color: var(--error-color);
|
|
}
|
|
.icon ha-svg-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
display: block;
|
|
}
|
|
.spacer {
|
|
height: 36px;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-integration-card": HaIntegrationCard;
|
|
}
|
|
}
|