mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-15 21:57:22 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b77315891 | |||
| 1762ee10fe |
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import type {
|
||||
Condition,
|
||||
ConditionContext,
|
||||
} from "../../panels/lovelace/common/validate-condition";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { setupConditionListeners } from "../condition/listeners";
|
||||
|
||||
/**
|
||||
* Reactive controller that manages the media-query and time-based listeners
|
||||
* needed to keep a set of lovelace visibility conditions evaluated live.
|
||||
*
|
||||
* The host is responsible for the actual evaluation (e.g. computing visible /
|
||||
* hidden / invalid state); the controller only triggers it via the supplied
|
||||
* `onUpdate` callback when something the conditions depend on changes. Call
|
||||
* `setup()` whenever the conditions change; the controller clears previous
|
||||
* listeners and re-subscribes. Listeners are automatically released when the
|
||||
* host disconnects.
|
||||
*/
|
||||
export class ConditionListenersController implements ReactiveController {
|
||||
private _unsubs: (() => void)[] = [];
|
||||
|
||||
constructor(host: ReactiveControllerHost) {
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setup(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
onUpdate: () => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
this.clear();
|
||||
if (!conditions.length) {
|
||||
return;
|
||||
}
|
||||
setupConditionListeners(
|
||||
conditions,
|
||||
hass,
|
||||
(unsub) => this._unsubs.push(unsub),
|
||||
() => onUpdate(),
|
||||
getContext
|
||||
);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (const unsub of this._unsubs) {
|
||||
unsub();
|
||||
}
|
||||
this._unsubs = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
/**
|
||||
* @element ha-automation-row-live-test
|
||||
*
|
||||
* @summary
|
||||
* Small status indicator dot used in automation/condition rows to surface the
|
||||
* live evaluation result. Renders an optional tooltip with details on hover.
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@property({ reflect: true }) public state: LiveTestState = "unknown";
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
id="indicator"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
:host([state="pass"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-success-loud-hover);
|
||||
border-color: var(--ha-color-fill-success-loud-hover);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
:host([state="fail"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-warning-loud-hover);
|
||||
border-color: var(--ha-color-fill-warning-loud-hover);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([state="invalid"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-danger-loud-hover);
|
||||
border-color: var(--ha-color-fill-danger-loud-hover);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
:host([state="unknown"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
border-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row-live-test": HaAutomationRowLiveTest;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -39,6 +39,7 @@ import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/automation/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/automation/ha-automation-row-live-test";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-condition-icon";
|
||||
import "../../../../components/ha-dropdown";
|
||||
@@ -46,7 +47,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -498,23 +498,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}
|
||||
<div
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
id="live-test"
|
||||
class=${this._liveTestResult.state}
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.hass.localize(
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
>
|
||||
${this._liveTestResult.message
|
||||
? html`<ha-tooltip for="live-test">
|
||||
${this._liveTestResult.message}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
</div></ha-automation-row
|
||||
>`
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@@ -1064,52 +1056,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
#live-test {
|
||||
position: absolute;
|
||||
inset-inline-end: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
#live-test.pass {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
#live-test.pass:hover {
|
||||
background-color: var(--ha-color-fill-success-loud-hover);
|
||||
border-color: var(--ha-color-fill-success-loud-hover);
|
||||
}
|
||||
#live-test.fail {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
#live-test.fail:hover {
|
||||
background-color: var(--ha-color-fill-warning-loud-hover);
|
||||
border-color: var(--ha-color-fill-warning-loud-hover);
|
||||
}
|
||||
#live-test.invalid {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
#live-test.invalid:hover {
|
||||
background-color: var(--ha-color-fill-danger-loud-hover);
|
||||
border-color: var(--ha-color-fill-danger-loud-hover);
|
||||
}
|
||||
#live-test.unknown {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
#live-test.unknown:hover {
|
||||
background-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
border-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [rowStyles, overflowStyles];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { conditionsEntityContext } from "../conditions/context";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-badge-visibility-editor")
|
||||
export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
@@ -34,11 +34,10 @@ export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_badge.visibility.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -62,10 +61,8 @@ export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { conditionsEntityContext } from "../conditions/context";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-card-visibility-editor")
|
||||
export class HuiCardVisibilityEditor extends LitElement {
|
||||
@@ -34,11 +34,10 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -62,10 +61,8 @@ export class HuiCardVisibilityEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import deepClone from "deep-clone-simple";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/automation/ha-automation-row-live-test";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
@@ -33,11 +35,11 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
import type {
|
||||
AndCondition,
|
||||
Condition,
|
||||
LegacyCondition,
|
||||
OrCondition,
|
||||
AndCondition,
|
||||
NotCondition,
|
||||
OrCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
@@ -103,6 +105,13 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
@state() private _testingResult?: boolean;
|
||||
|
||||
@state() private _liveTestResult: {
|
||||
state: "pass" | "fail" | "invalid" | "unknown";
|
||||
message?: string;
|
||||
} = { state: "unknown" };
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
|
||||
private get _editor() {
|
||||
if (!this._condition) return undefined;
|
||||
return customElements.get(
|
||||
@@ -116,6 +125,14 @@ export class HaCardConditionEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _setupConditionListeners() {
|
||||
this._listeners.setup(
|
||||
this.condition ? [this.condition as Condition] : [],
|
||||
this.hass,
|
||||
() => this._evaluateLiveTest()
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("condition")) {
|
||||
this._condition = {
|
||||
@@ -143,7 +160,61 @@ export class HaCardConditionEditor extends LitElement {
|
||||
if (!this._uiAvailable && !this._yamlMode) {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
this._setupConditionListeners();
|
||||
}
|
||||
|
||||
if (changedProperties.has("condition") || changedProperties.has("hass")) {
|
||||
this._evaluateLiveTest();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if ((changedProperties as Map<string, unknown>).has("_entityContext")) {
|
||||
this._evaluateLiveTest();
|
||||
}
|
||||
}
|
||||
|
||||
private _evaluateLiveTest() {
|
||||
if (!this.condition || !this._condition) {
|
||||
this._liveTestResult = { state: "unknown" };
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isNoEntityCondition(this._condition.condition, this._noEntity) ||
|
||||
containsNoEntityCondition(this._condition, this._noEntity)
|
||||
) {
|
||||
this._liveTestResult = {
|
||||
state: "unknown",
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.live_test_state.unknown"
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateConditionalConfig([this.condition])) {
|
||||
this._liveTestResult = {
|
||||
state: "invalid",
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.live_test_state.invalid"
|
||||
),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const testContext =
|
||||
this._entityContext?.mode === "current"
|
||||
? { entity_id: this._entityContext.entityId }
|
||||
: {};
|
||||
const pass = checkConditionsMet([this.condition], this.hass, testContext);
|
||||
this._liveTestResult = {
|
||||
state: pass ? "pass" : "fail",
|
||||
message: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${pass ? "pass" : "fail"}`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -151,6 +222,10 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
if (!condition) return nothing;
|
||||
|
||||
const hideLiveTest =
|
||||
isNoEntityCondition(condition.condition, this._noEntity) ||
|
||||
containsNoEntityCondition(condition, this._noEntity);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
@@ -164,6 +239,33 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition}
|
||||
</h3>
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._testingResult !== undefined}
|
||||
.variant=${this._testingResult ? "success" : "warning"}
|
||||
slot="event"
|
||||
class="event-chip"
|
||||
aria-live="polite"
|
||||
>
|
||||
${this._testingResult
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_pass"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test>
|
||||
`}
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -268,23 +370,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
<div
|
||||
class="testing ${classMap({
|
||||
active: this._testingResult !== undefined,
|
||||
pass: this._testingResult === true,
|
||||
error: this._testingResult === false,
|
||||
})}"
|
||||
>
|
||||
${this._testingResult
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_pass"
|
||||
)
|
||||
: this._testingResult === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -419,41 +504,9 @@ export class HaCardConditionEditor extends LitElement {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.testing {
|
||||
.event-chip {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
background-color: var(--divider-color, #e0e0e0);
|
||||
color: var(--text-primary-color);
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: calc(
|
||||
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
border-top-left-radius: calc(
|
||||
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
|
||||
--ha-card-border-width,
|
||||
1px
|
||||
)
|
||||
);
|
||||
}
|
||||
.testing.active {
|
||||
max-height: 100px;
|
||||
}
|
||||
.testing.error {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
inset-inline-end: 40px;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlertCircle, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { HaRowItem } from "../../../../components/item/ha-row-item";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
Condition,
|
||||
LegacyCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
validateConditionalConfig,
|
||||
} from "../../common/validate-condition";
|
||||
import type { ConditionsEntityContext } from "./context";
|
||||
import { conditionsEntityContext } from "./context";
|
||||
|
||||
type VisibilityState = "visible" | "hidden" | "invalid";
|
||||
|
||||
const STATE_ICONS: Record<VisibilityState, string> = {
|
||||
visible: mdiEye,
|
||||
hidden: mdiEyeOff,
|
||||
invalid: mdiAlertCircle,
|
||||
};
|
||||
|
||||
/**
|
||||
* @element ha-visibility-status
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* Row-style banner that surfaces the live visibility result for a set of
|
||||
* lovelace conditions. Replaces the static explanation alert at the top of
|
||||
* card / section / badge / conditional-card visibility editors.
|
||||
*
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state (reflected for styling).
|
||||
*/
|
||||
@customElement("ha-visibility-status")
|
||||
export class HaVisibilityStatus extends HaRowItem {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditions: (Condition | LegacyCondition)[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionsEntityContext, subscribe: true })
|
||||
private _entityContext?: ConditionsEntityContext;
|
||||
|
||||
@property({ reflect: true })
|
||||
public state: VisibilityState = "visible";
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("conditions") || changedProperties.has("hass")) {
|
||||
this._listeners.setup(
|
||||
(this.conditions ?? []) as Condition[],
|
||||
this.hass,
|
||||
() => this._evaluate()
|
||||
);
|
||||
}
|
||||
if (
|
||||
changedProperties.has("hass") ||
|
||||
changedProperties.has("conditions") ||
|
||||
(changedProperties as Map<string, unknown>).has("_entityContext")
|
||||
) {
|
||||
this._evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override _renderInner(): TemplateResult {
|
||||
return html`
|
||||
<div part="start" class="start">
|
||||
<ha-svg-icon .path=${STATE_ICONS[this.state]}></ha-svg-icon>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
<div part="headline" class="headline">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.headline`
|
||||
)}
|
||||
</div>
|
||||
<div part="supporting-text" class="supporting">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.supporting`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _evaluate() {
|
||||
const conditions = this.conditions ?? [];
|
||||
let newState: VisibilityState;
|
||||
if (conditions.length === 0) {
|
||||
newState = "visible";
|
||||
} else if (!validateConditionalConfig(conditions)) {
|
||||
newState = "invalid";
|
||||
} else {
|
||||
const context =
|
||||
this._entityContext?.mode === "current"
|
||||
? { entity_id: this._entityContext.entityId }
|
||||
: {};
|
||||
newState = checkConditionsMet(conditions, this.hass, context)
|
||||
? "visible"
|
||||
: "hidden";
|
||||
}
|
||||
if (newState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = newState;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
transition: background-color var(--ha-animation-duration-normal)
|
||||
ease-in-out;
|
||||
}
|
||||
.base {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
:host([state="visible"]) {
|
||||
background-color: var(--ha-color-fill-success-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
:host([state="hidden"]) {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
:host([state="invalid"]) {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
.start {
|
||||
align-self: start;
|
||||
}
|
||||
.start ha-svg-icon {
|
||||
color: var(--visibility-status-color);
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.headline {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-visibility-status": HaVisibilityStatus;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { any, array, assert, assign, object, optional } from "superstruct";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tab-group";
|
||||
@@ -21,6 +20,7 @@ import "../card-editor/hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
|
||||
import "../card-editor/hui-card-picker";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
import "../hui-element-editor";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -147,11 +147,10 @@ export class HuiConditionalCardEditor
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.explanation"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${this._config.conditions ?? []}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${this._config.conditions}
|
||||
@@ -246,9 +245,9 @@ export class HuiConditionalCardEditor
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
ha-visibility-status {
|
||||
margin-top: var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
.card {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import "../conditions/ha-visibility-status";
|
||||
|
||||
@customElement("hui-section-visibility-editor")
|
||||
export class HuiDialogEditSection extends LitElement {
|
||||
@@ -16,11 +16,10 @@ export class HuiDialogEditSection extends LitElement {
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.visibility.explanation`
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-visibility-status
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
></ha-visibility-status>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@@ -30,6 +29,12 @@ export class HuiDialogEditSection extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-visibility-status {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
|
||||
+21
-11
@@ -8917,9 +8917,6 @@
|
||||
"tab_visibility": "Visibility",
|
||||
"tab_layout": "Layout",
|
||||
"paste_condition": "Paste condition",
|
||||
"visibility": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
|
||||
},
|
||||
"layout": {
|
||||
"full_width": "Full width",
|
||||
"full_width_helper": "Take up the full width of the section whatever its size",
|
||||
@@ -8950,10 +8947,7 @@
|
||||
"cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]",
|
||||
"duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]",
|
||||
"tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]",
|
||||
"visibility": {
|
||||
"explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown."
|
||||
}
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]"
|
||||
},
|
||||
"suggest_badge": {
|
||||
"header": "[%key:ui::panel::lovelace::editor::suggest_card::header%]",
|
||||
@@ -9025,9 +9019,6 @@
|
||||
"background_opacity": "Opacity",
|
||||
"theme": "Theme",
|
||||
"theme_helper": "Apply a specific theme to this section, overriding the view theme"
|
||||
},
|
||||
"visibility": {
|
||||
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||
}
|
||||
},
|
||||
"suggest_card": {
|
||||
@@ -9069,11 +9060,30 @@
|
||||
}
|
||||
},
|
||||
"condition-editor": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
|
||||
"add": "Add condition",
|
||||
"test": "[%key:ui::panel::config::automation::editor::conditions::test%]",
|
||||
"testing_pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
|
||||
"testing_error": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
|
||||
"live_test_state": {
|
||||
"pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
|
||||
"fail": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
|
||||
"invalid": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::invalid%]",
|
||||
"unknown": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::unknown%]"
|
||||
},
|
||||
"visibility_status": {
|
||||
"visible": {
|
||||
"headline": "Current visibility: Visible",
|
||||
"supporting": "All visibility conditions are met"
|
||||
},
|
||||
"hidden": {
|
||||
"headline": "Current visibility: Hidden",
|
||||
"supporting": "Not all visibility conditions are met"
|
||||
},
|
||||
"invalid": {
|
||||
"headline": "Visibility status unknown",
|
||||
"supporting": "One or more conditions have an invalid configuration"
|
||||
}
|
||||
},
|
||||
"invalid_config_title": "Invalid configuration",
|
||||
"invalid_config_text": "The condition cannot be tested because the configuration is not valid.",
|
||||
"condition": {
|
||||
|
||||
Reference in New Issue
Block a user