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 = ( const createManifest = (
isCustom: boolean, isCustom: boolean,
isCloud: boolean isCloud: boolean,
name = "ESPHome"
): IntegrationManifest => ({ ): IntegrationManifest => ({
name: "ESPHome", name,
domain: "esphome", domain: "esphome",
is_built_in: !isCustom, is_built_in: !isCustom,
config_flow: false, config_flow: false,
@@ -103,7 +104,7 @@ const configFlows: DataEntryFlowProgressExtended[] = [
}, },
}, },
step_id: "discovery_confirm", step_id: "discovery_confirm",
localized_title: "Roku: Living room Roku", localized_title: "Living room Roku",
}, },
{ {
flow_id: "adbb401329d8439ebb78ef29837826a8", flow_id: "adbb401329d8439ebb78ef29837826a8",
@@ -139,9 +140,9 @@ const configEntries: Array<{
{ {
items: [ items: [
loadedEntry, loadedEntry,
longNameEntry,
setupErrorEntry, setupErrorEntry,
migrationErrorEntry, migrationErrorEntry,
longNameEntry,
setupRetryEntry, setupRetryEntry,
failedUnloadEntry, failedUnloadEntry,
notLoadedEntry, notLoadedEntry,
@@ -211,47 +212,78 @@ export class DemoIntegrationCard extends LitElement {
return html``; return html``;
} }
return html` return html`
<div class="filters"> <div class="container">
<ha-formfield label="Custom Integration"> <div class="filters">
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch> <ha-formfield label="Custom Integration">
</ha-formfield> <ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
<ha-formfield label="Relies on cloud"> </ha-formfield>
<ha-switch @change=${this._toggleCloud}></ha-switch> <ha-formfield label="Relies on cloud">
</ha-formfield> <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> </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() { static get styles() {
return css` return css`
:host { .container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px; grid-gap: 16px 16px;
@@ -280,7 +312,7 @@ export class DemoIntegrationCard extends LitElement {
margin-bottom: 64px; margin-bottom: 64px;
} }
:host > * { .container > * {
max-width: 500px; 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" "ui.panel.config.integrations.ignore.ignored"
)} )}
.domain=${this.entry.domain} .domain=${this.entry.domain}
.localizedDomainName=${this.entry.localized_domain_name}
.label=${this.entry.title === "Ignored" .label=${this.entry.title === "Ignored"
? // In 2020.2 we added support for entry.title. All ignored entries before ? // In 2020.2 we added support for entry.title. All ignored entries before
// that have title "Ignored" so we fallback to localized domain name. // that have title "Ignored" so we fallback to localized domain name.

View File

@@ -1,18 +1,14 @@
import { import {
TemplateResult,
html,
customElement, customElement,
LitElement, LitElement,
property, property,
CSSResult,
css, css,
} from "lit-element"; } from "lit-element";
import { TemplateResult, html } from "lit-html"; import type { IntegrationManifest } from "../../../data/integration";
import { IntegrationManifest } from "../../../data/integration"; import type { HomeAssistant } from "../../../types";
import { HomeAssistant } from "../../../types"; import "./ha-integration-header";
import { brandsUrl } from "../../../util/brands-url";
import {
haConfigIntegrationRenderIcons,
haConfigIntegrationsStyles,
} from "./ha-config-integrations-common";
@customElement("ha-integration-action-card") @customElement("ha-integration-action-card")
export class HaIntegrationActionCard extends LitElement { export class HaIntegrationActionCard extends LitElement {
@@ -20,6 +16,8 @@ export class HaIntegrationActionCard extends LitElement {
@property() public banner!: string; @property() public banner!: string;
@property() public localizedDomainName?: string;
@property() public domain!: string; @property() public domain!: string;
@property() public label!: string; @property() public label!: string;
@@ -29,82 +27,48 @@ export class HaIntegrationActionCard extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card outlined> <ha-card outlined>
<div class="banner"> <ha-integration-header
${this.banner} .hass=${this.hass}
</div> .banner=${this.banner}
<div class="content"> .domain=${this.domain}
${haConfigIntegrationRenderIcons(this.hass, this.manifest)} .label=${this.label}
<div class="image"> .localizedDomainName=${this.localizedDomainName}
<img .manifest=${this.manifest}
src=${brandsUrl(this.domain, "logo")} ></ha-integration-header>
referrerpolicy="no-referrer" <div class="content"></div>
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>${this.label}</h2>
</div>
<div class="actions"><slot></slot></div> <div class="actions"><slot></slot></div>
</ha-card> </ha-card>
`; `;
} }
private _onImageLoad(ev) { static styles = css`
ev.target.style.visibility = "initial"; ha-card {
} display: flex;
flex-direction: column;
private _onImageError(ev) { height: 100%;
ev.target.style.visibility = "hidden"; --ha-card-border-color: var(--state-color);
} --mdc-theme-primary: var(--state-color);
}
static get styles(): CSSResult[] { .content {
return [ position: relative;
haConfigIntegrationsStyles, flex: 1;
css` }
ha-card { .attention {
display: flex; --state-color: var(--error-color);
flex-direction: column; --text-on-state-color: var(--text-primary-color);
height: 100%; }
--ha-card-border-color: var(--state-color); .discovered {
--mdc-theme-primary: var(--state-color); --state-color: var(--primary-color);
} --text-on-state-color: var(--text-primary-color);
.content { }
position: relative; .actions {
flex: 1; display: flex;
} justify-content: space-between;
.image { align-items: center;
height: 60px; padding: 8px 6px 0;
margin-top: 16px; height: 48px;
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 { declare global {

View File

@@ -31,7 +31,7 @@ import {
} from "../../../data/config_entries"; } from "../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_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 { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import { import {
@@ -40,16 +40,11 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import type { ConfigEntryExtended } from "./ha-config-integrations";
import { ConfigEntryExtended } from "./ha-config-integrations"; import "./ha-integration-header";
import {
haConfigIntegrationRenderIcons,
haConfigIntegrationsStyles,
} from "./ha-config-integrations-common";
const ERROR_STATES: ConfigEntry["state"][] = [ const ERROR_STATES: ConfigEntry["state"][] = [
"failed_unload",
"migration_error", "migration_error",
"setup_error", "setup_error",
"setup_retry", "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; const hasItem = item !== undefined;
return html` return html`
@@ -116,42 +99,37 @@ export class HaIntegrationCard extends LitElement {
hasMultiple: this.items.length > 1, hasMultiple: this.items.length > 1,
disabled: this.disabled, disabled: this.disabled,
"state-not-loaded": hasItem && item!.state === "not_loaded", "state-not-loaded": hasItem && item!.state === "not_loaded",
"state-failed-unload": hasItem && item!.state === "failed_unload",
"state-error": hasItem && ERROR_STATES.includes(item!.state), "state-error": hasItem && ERROR_STATES.includes(item!.state),
})}" })}"
.configEntry=${item} .configEntry=${item}
> >
${this.disabled <ha-integration-header
? html` .hass=${this.hass}
<div class="banner"> .banner=${this.disabled
${this.hass.localize( ? this.hass.localize(
"ui.panel.config.integrations.config_entry.disable.disabled" "ui.panel.config.integrations.config_entry.disable.disabled"
)} )
</div> : undefined}
` .domain=${this.domain}
: ""} .label=${item
${this.items.length > 1 ? item.title || item.localized_domain_name || this.domain
? html` : undefined}
<div class="back-btn"> .localizedDomainName=${item ? item.localized_domain_name : undefined}
<ha-icon-button .manifest=${this.manifest}
icon="hass:chevron-left" >
@click=${this._back} ${this.items.length > 1
></ha-icon-button> ? html`
</div> <div class="back-btn" slot="above-header">
` <ha-icon-button
: ""} icon="hass:chevron-left"
<div class="header"> @click=${this._back}
<img ></ha-icon-button>
src=${brandsUrl(this.domain, "icon")} </div>
referrerpolicy="no-referrer" `
@error=${this._onImageError} : ""}
@load=${this._onImageLoad} </ha-integration-header>
/>
<div class="info">
<div class="primary">${primary}</div>
${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
</div>
${haConfigIntegrationRenderIcons(this.hass, this.manifest)}
</div>
${item ${item
? this._renderSingleEntry(item) ? this._renderSingleEntry(item)
: this._renderGroupedIntegration()} : 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) { private _showOptions(ev) {
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry); showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
} }
@@ -605,7 +575,6 @@ export class HaIntegrationCard extends LitElement {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
haConfigIntegrationsStyles,
css` css`
ha-card { ha-card {
display: flex; display: flex;
@@ -619,16 +588,17 @@ export class HaIntegrationCard extends LitElement {
--state-color: var(--error-color); --state-color: var(--error-color);
--text-on-state-color: var(--text-primary-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-not-loaded {
--state-message-color: var(--primary-text-color); --state-message-color: var(--primary-text-color);
} }
:host(.highlight) ha-card { :host(.highlight) ha-card {
--state-color: var(--accent-color); --state-color: var(--primary-color);
--text-on-state-color: var(--text-primary-color); --text-on-state-color: var(--text-primary-color);
} }
ha-card.group {
max-height: 200px;
}
.back-btn { .back-btn {
background-color: var(--state-color); background-color: var(--state-color);
@@ -644,39 +614,6 @@ export class HaIntegrationCard extends LitElement {
height: 0px; 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 { .message {
font-weight: bold; font-weight: bold;
padding-bottom: 16px; padding-bottom: 16px;
@@ -706,15 +643,34 @@ export class HaIntegrationCard extends LitElement {
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;
} }
@media (min-width: 563px) { @media (min-width: 563px) {
ha-card.group {
position: relative;
min-height: 164px;
}
paper-listbox { paper-listbox {
flex: 1; position: absolute;
top: 64px;
left: 0;
right: 0;
bottom: 0;
overflow: auto; overflow: auto;
} }
.disabled paper-listbox {
top: 100px;
}
} }
paper-item { paper-item {
cursor: pointer; cursor: pointer;
min-height: 35px; 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 { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); 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;
}
}