Compare commits

...

3 Commits

Author SHA1 Message Date
Paulus Schoutsen
cdf53773c5 Align layout of all cards 2021-04-16 11:47:56 -07:00
Paulus Schoutsen
3174037c54 One more fix 2021-04-16 11:07:17 -07:00
Paulus Schoutsen
4af060890a Fixes for integration cards 2021-04-16 10:49:24 -07:00
6 changed files with 353 additions and 301 deletions

View File

@@ -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;
}

View File

@@ -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>
`;
};

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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);
}

View 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;
}
}