mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-31 01:54:43 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb6daebf5 | ||
|
|
59620e652d | ||
|
|
bfa7b9a541 | ||
|
|
ebba11980a | ||
|
|
68f6b3dbab | ||
|
|
25abbb2ba0 | ||
|
|
d02c03dacf |
No files matched your search
@@ -2,13 +2,13 @@
|
||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
||||
"frontend-modern": {
|
||||
"app": 627654,
|
||||
"core": 57901,
|
||||
"core": 55147,
|
||||
"authorize": 593862,
|
||||
"onboarding": 709255
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 724225,
|
||||
"core": 163115,
|
||||
"core": 157026,
|
||||
"authorize": 693913,
|
||||
"onboarding": 812415
|
||||
}
|
||||
|
||||
@@ -117,16 +117,6 @@ module.exports.swcOptions = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
const corejsPolyfills = (exclude = []) => [
|
||||
"babel-plugin-polyfill-corejs3",
|
||||
{
|
||||
method: "usage-global",
|
||||
version: dependencies["core-js"],
|
||||
shippedProposals: true,
|
||||
exclude,
|
||||
},
|
||||
];
|
||||
|
||||
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
@@ -148,7 +138,14 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
// Inject Core-JS polyfills on demand. Babel 8 removed preset-env's
|
||||
// `useBuiltIns`/`corejs` options, so the equivalent polyfill provider is
|
||||
// configured directly here (`usage-global` matches the old `useBuiltIns: "usage"`).
|
||||
corejsPolyfills(),
|
||||
[
|
||||
"babel-plugin-polyfill-corejs3",
|
||||
{
|
||||
method: "usage-global",
|
||||
version: dependencies["core-js"],
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
||||
{
|
||||
@@ -194,16 +191,6 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw, worker }) => ({
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
],
|
||||
},
|
||||
{
|
||||
// It only constructs `Uint8Array`s, but any reference to one pulls in
|
||||
// every typed array polyfill, and this lands in the `core` entrypoint.
|
||||
test: /[\\/]node_modules[\\/]home-assistant-js-websocket[\\/]/,
|
||||
plugins: [
|
||||
corejsPolyfills([
|
||||
/^(es|esnext)\.(array-buffer|typed-array|uint8-array)\./,
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.8.0",
|
||||
"home-assistant-js-websocket": "9.7.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
@@ -109,7 +109,7 @@
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"maplibre-gl": "6.15.0",
|
||||
"marked": "18.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
|
||||
@@ -13,27 +13,3 @@ index 95366713472f3ec5f3649547bb1fe202b806a2e6..e91f99229d554e79077486e1caf3602c
|
||||
originX: e.pinchX,
|
||||
originY: e.pinchY,
|
||||
isAvailableBehavior: null
|
||||
diff --git a/lib/label/LabelManager.js b/lib/label/LabelManager.js
|
||||
index 3d233be98b14223168726ebb3d9cd9b768fa2554..d6abfe5484ea8c2f0cb3a9d7c7c2fe31e7516fc7 100644
|
||||
--- a/lib/label/LabelManager.js
|
||||
+++ b/lib/label/LabelManager.js
|
||||
@@ -43,6 +43,7 @@
|
||||
*/
|
||||
// TODO: move labels out of viewport.
|
||||
import { BoundingRect, updateProps, initProps, isElementRemoved } from '../util/graphic.js';
|
||||
+import { getAnimationConfig } from '../animation/basicTransition.js';
|
||||
import { getECData } from '../util/innerStore.js';
|
||||
import { parsePercent } from '../util/number.js';
|
||||
import Transformable from 'zrender/lib/core/Transformable.js';
|
||||
@@ -395,6 +396,11 @@ var LabelManager = /** @class */function () {
|
||||
textEl.attr(layoutStore.oldLayoutEmphasis);
|
||||
}
|
||||
}
|
||||
+ // An instant update, e.g. on roam, stops all animations and would leave the fade in half done
|
||||
+ var updateConfig = getAnimationConfig('update', seriesModel, dataIndex);
|
||||
+ if (!(updateConfig && updateConfig.duration > 0)) {
|
||||
+ textEl.stopAnimation('enter', true);
|
||||
+ }
|
||||
updateProps(textEl, newProps, seriesModel, dataIndex);
|
||||
}
|
||||
layoutStore.oldLayout = newProps;
|
||||
Generated
+26
-26
@@ -171,7 +171,7 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
'@material/[email protected]': 9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987
|
||||
[email protected]: 4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5
|
||||
[email protected]: b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0
|
||||
[email protected]: f2ec2ae6877dfc201c542b4f9ded1d03831258bf328ae7bba876d14ce4508a06
|
||||
[email protected]: cdc7ece96d53357d38ff3c124a2553f86e3a5dc333d641d223eb6f07e28275f0
|
||||
[email protected]: 75e6346ad8d8af554c167f77ac7d9b9652f010398efdc6b399925a4795133099
|
||||
@@ -278,7 +278,7 @@ importers:
|
||||
version: 1.0.3
|
||||
'@maplibre/maplibre-gl-leaflet':
|
||||
specifier: 0.1.4
|
||||
version: 0.1.4(@types/[email protected])([email protected])([email protected]6.0)
|
||||
version: 0.1.4(@types/[email protected])([email protected])([email protected]5.0)
|
||||
'@material/mwc-list':
|
||||
specifier: 0.27.0
|
||||
version: 0.27.0(patch_hash=9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987)
|
||||
@@ -302,7 +302,7 @@ importers:
|
||||
version: 4.4.0
|
||||
'@versatiles/style':
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1([email protected]6.0)
|
||||
version: 7.0.1([email protected]5.0)
|
||||
'@vibrant/color':
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4
|
||||
@@ -347,10 +347,10 @@ importers:
|
||||
version: 0.0.1
|
||||
echarts:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0(patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5)
|
||||
version: 6.1.0(patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0)
|
||||
echarts-extension-chart2music:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1([email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5))([email protected])
|
||||
version: 0.1.1([email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0))([email protected])
|
||||
element-internals-polyfill:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
@@ -364,8 +364,8 @@ importers:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
home-assistant-js-websocket:
|
||||
specifier: 9.8.0
|
||||
version: 9.8.0
|
||||
specifier: 9.7.1
|
||||
version: 9.7.1
|
||||
idb-keyval:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
@@ -388,8 +388,8 @@ importers:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
maplibre-gl:
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
specifier: 6.15.0
|
||||
version: 6.15.0
|
||||
marked:
|
||||
specifier: 18.1.1
|
||||
version: 18.1.1
|
||||
@@ -2258,8 +2258,8 @@ packages:
|
||||
'@maplibre/[email protected]':
|
||||
resolution: {integrity: sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==}
|
||||
|
||||
'@maplibre/[email protected].3':
|
||||
resolution: {integrity: sha512-dEe5XUd64RQbxNxy2OzIkJnwNDWjVmSXjCd0A7zLirm7QJ/WWbmtDFIkfsiM4yt1cplqzG6oSivRYeWx1SM1yg==}
|
||||
'@maplibre/[email protected].2':
|
||||
resolution: {integrity: sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==}
|
||||
|
||||
'@marijn/[email protected]':
|
||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||
@@ -5205,8 +5205,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==}
|
||||
|
||||
home-assistant-js-websocket@9.8.0:
|
||||
resolution: {integrity: sha512-Nl5asoKQJwJ5a8N0U72jkSAUdO+hsY/T+Upp0VaZMUSt6A2gtdyvKfBIEYbpnYK3NP83Q/YGaC5XYkO5c08Img==}
|
||||
home-assistant-js-websocket@9.7.1:
|
||||
resolution: {integrity: sha512-zOAiyF+gUc/IsiswYdWKns6SCBbvWb8iJtMX9+Dk0wlbdfcp+I38T2Vx36krWeWMk/GPwoSiYwCs5hk/+Hsn7w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
@@ -5898,8 +5898,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==}
|
||||
|
||||
[email protected]6.0:
|
||||
resolution: {integrity: sha512-gApO66EMtoDZuio680RUBvskR7ahFd/NNhLN4tBAs+Y7FeT4gQV8zVFgYVMRmFxQznOEzecW19w6xOd9XjZjiA==}
|
||||
[email protected]5.0:
|
||||
resolution: {integrity: sha512-kxFetbvUHDOVaAP1CYb226Mdk2QhzIPddqM0xJTFD35wu78Mpd6NM7+H68ALjc6Oa0kAOX5EAdMKyvD2u3qfcg==}
|
||||
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
|
||||
|
||||
[email protected]:
|
||||
@@ -9784,11 +9784,11 @@ snapshots:
|
||||
dependencies:
|
||||
kdbush: 4.1.0
|
||||
|
||||
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]6.0)':
|
||||
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]5.0)':
|
||||
dependencies:
|
||||
'@types/leaflet': 1.9.22
|
||||
leaflet: 1.9.4
|
||||
maplibre-gl: 6.16.0
|
||||
maplibre-gl: 6.15.0
|
||||
|
||||
'@maplibre/[email protected]':
|
||||
dependencies:
|
||||
@@ -9803,7 +9803,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
|
||||
'@maplibre/[email protected].3':
|
||||
'@maplibre/[email protected].2':
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@types/geojson': 7946.0.16
|
||||
@@ -11279,11 +11279,11 @@ snapshots:
|
||||
'@unrs/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@versatiles/[email protected]([email protected]6.0)':
|
||||
'@versatiles/[email protected]([email protected]5.0)':
|
||||
dependencies:
|
||||
'@maplibre/maplibre-gl-style-spec': 26.4.4
|
||||
optionalDependencies:
|
||||
maplibre-gl: 6.16.0
|
||||
maplibre-gl: 6.15.0
|
||||
|
||||
'@vibrant/[email protected]': {}
|
||||
|
||||
@@ -12190,14 +12190,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
wcwidth: 1.0.1
|
||||
|
||||
[email protected]([email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5))([email protected]):
|
||||
[email protected]([email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0))([email protected]):
|
||||
dependencies:
|
||||
chart2music: 1.20.0([email protected])
|
||||
echarts: 6.1.0(patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5)
|
||||
echarts: 6.1.0(patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
[email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5):
|
||||
[email protected](patch_hash=b02e30d0fb3e2b865321c17ca1234593db0d05a1365cb6c932cd47c0e673fca0):
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
zrender: 6.1.0
|
||||
@@ -12962,7 +12962,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
home-assistant-js-websocket@9.8.0: {}
|
||||
home-assistant-js-websocket@9.7.1: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13666,7 +13666,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]6.0:
|
||||
[email protected]5.0:
|
||||
dependencies:
|
||||
'@mapbox/point-geometry': 1.1.0
|
||||
'@mapbox/tiny-sdf': 2.3.0
|
||||
@@ -13675,7 +13675,7 @@ snapshots:
|
||||
'@maplibre/geojson-vt': 6.3.0
|
||||
'@maplibre/maplibre-gl-style-spec': 26.4.4
|
||||
'@maplibre/mlt': 1.3.0
|
||||
'@maplibre/vt-pbf': 4.3.3
|
||||
'@maplibre/vt-pbf': 4.3.2
|
||||
'@types/geojson': 7946.0.16
|
||||
bidi-js: 1.1.0
|
||||
earcut: 3.2.4
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20261028.2"
|
||||
version = "20260930.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { VisibilityCondition } from "../../panels/lovelace/common/validate-condition";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
// YAML dashboards can hold entries like `- ` (null) or a bare string.
|
||||
const isConditionObject = (c: unknown): c is VisibilityCondition =>
|
||||
typeof c === "object" && c !== null;
|
||||
|
||||
/**
|
||||
* Drop entries that are not condition objects, at any depth. Returns the
|
||||
* same array when every entry is valid.
|
||||
*/
|
||||
export const dropInvalidConditions = (
|
||||
conditions: VisibilityCondition[]
|
||||
): VisibilityCondition[] => {
|
||||
let changed = false;
|
||||
const valid: VisibilityCondition[] = [];
|
||||
for (const c of conditions) {
|
||||
if (!isConditionObject(c)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if ("conditions" in c && c.conditions != null) {
|
||||
const children = ensureArray(c.conditions);
|
||||
const validChildren = dropInvalidConditions(children);
|
||||
if (validChildren !== children) {
|
||||
changed = true;
|
||||
valid.push({ ...c, conditions: validChildren } as VisibilityCondition);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
valid.push(c);
|
||||
}
|
||||
return changed ? valid : conditions;
|
||||
};
|
||||
@@ -26,8 +26,10 @@ export interface QueryParamsControllerOptions {
|
||||
* Called when the page of the host is navigated to while the host is
|
||||
* connected, like a second link to an open page or coming back to it. Read
|
||||
* the URL with `searchParams()` and apply it, then `write()` the state back.
|
||||
* Gets the `popstate` or `location-changed` event, or nothing when the host
|
||||
* is connected again.
|
||||
*/
|
||||
navigated: () => void;
|
||||
navigated: (ev?: Event) => void;
|
||||
/**
|
||||
* The path of the host's page, for a page that can first be connected on
|
||||
* a sub-path it sends on to itself, like an add link. Defaults to the path
|
||||
@@ -159,7 +161,7 @@ export class QueryParamsController<
|
||||
}
|
||||
// Leaving the page gets here too, before the router swaps the page
|
||||
if (this._isActive()) {
|
||||
this._options.navigated();
|
||||
this._options.navigated(ev);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeCssColor } from "../color/compute-color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateColorCss } from "./state_color";
|
||||
|
||||
export const ALERT_DOMAINS = [
|
||||
"alarm_control_panel",
|
||||
@@ -10,6 +8,8 @@ export const ALERT_DOMAINS = [
|
||||
"lock",
|
||||
];
|
||||
|
||||
export const DEFAULT_ALERT_COLOR = "red";
|
||||
|
||||
export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
switch (computeDomain(entityId)) {
|
||||
case "alarm_control_panel":
|
||||
@@ -25,16 +25,3 @@ export const computeAlertActiveStates = (entityId: string): string[] => {
|
||||
|
||||
export const isAlertActive = (stateObj: HassEntity): boolean =>
|
||||
computeAlertActiveStates(stateObj.entity_id).includes(stateObj.state);
|
||||
|
||||
export const computeAlertColor = (
|
||||
stateObj: HassEntity,
|
||||
color: string
|
||||
): string | undefined => {
|
||||
if (color === "none") {
|
||||
return undefined;
|
||||
}
|
||||
if (color === "state") {
|
||||
return stateColorCss(stateObj);
|
||||
}
|
||||
return computeCssColor(color);
|
||||
};
|
||||
@@ -89,8 +89,6 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
@@ -165,10 +163,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "small-controls", type: Boolean })
|
||||
public smallControls?: boolean;
|
||||
|
||||
// "auto" shows the zoom slider once zoomed. "never" hides every zoom
|
||||
// control, for charts zoomed from outside like the synced History charts.
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
// For charts zoomed from outside, like the synced History charts
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
// For charts sized to their content, like timelines, which grow to fit the
|
||||
// zoom slider instead of giving up plot height
|
||||
@@ -620,7 +617,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
>
|
||||
${
|
||||
(this._isZoomed || this._isGraphRoamed) &&
|
||||
this.zoomSlider !== "never" &&
|
||||
!this.hideZoomControls &&
|
||||
!showZoomSlider
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -972,7 +969,6 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
fireEvent(this, "chart-setup");
|
||||
} finally {
|
||||
this._loading = false;
|
||||
this._applyDeferredWork();
|
||||
@@ -1067,12 +1063,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _showZoomSlider() {
|
||||
const [start, end] = this._zoomRange;
|
||||
return (
|
||||
this.zoomSlider !== "never" &&
|
||||
!this.hideZoomControls &&
|
||||
this._hasZoomableXAxis() &&
|
||||
(this.zoomSlider === "always" ||
|
||||
this._zoomSliderHeld ||
|
||||
start !== 0 ||
|
||||
end !== 100)
|
||||
(this._zoomSliderHeld || start !== 0 || end !== 100)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1303,10 +1296,6 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
},
|
||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||
graph: {
|
||||
itemStyle: {
|
||||
borderColor: style.getPropertyValue("--primary-text-color"),
|
||||
borderWidth: 0,
|
||||
},
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor,
|
||||
@@ -2329,6 +2318,5 @@ declare global {
|
||||
end: number;
|
||||
};
|
||||
"chart-sankeyroam": { zoom: number };
|
||||
"chart-setup": undefined;
|
||||
}
|
||||
}
|
||||
@@ -69,13 +69,6 @@ export interface NetworkData {
|
||||
|
||||
const PHYSICS_DISABLE_THRESHOLD = 512;
|
||||
|
||||
const SELECTED_NODE_MAX_ZOOM = 2;
|
||||
|
||||
// Keeps neighbors clear of the search field, buttons and legend
|
||||
const SELECTED_NODE_FIT_MARGIN = 72;
|
||||
|
||||
const FOLLOW_ROAM = "follow-selected-node";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@@ -99,12 +92,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchFilter = "";
|
||||
|
||||
/**
|
||||
* Id of a node to highlight. The view centers on it, zoomed so its neighbors
|
||||
* stay visible, and follows it until the user moves the view.
|
||||
*/
|
||||
@property({ attribute: false }) public selectedNode?: string;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state() private _highlightedNodes?: Set<string>;
|
||||
@@ -119,10 +106,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
||||
|
||||
private _followSelectedNode = false;
|
||||
|
||||
private _followedChart?: EChartsType;
|
||||
|
||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
||||
|
||||
constructor() {
|
||||
@@ -162,13 +145,11 @@ export class HaNetworkGraph extends LitElement {
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile,
|
||||
hasHighlightedNodes,
|
||||
this.selectedNode
|
||||
hasHighlightedNodes
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
@chart-setup=${this._updateFollowing}
|
||||
>
|
||||
<slot name="search" slot="search"></slot>
|
||||
<slot name="button" slot="button"></slot>
|
||||
@@ -231,10 +212,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
this._applyHighlighting();
|
||||
this._updateMouseoverHandler();
|
||||
}
|
||||
if (changedProperties.has("selectedNode")) {
|
||||
this._followSelectedNode = !!this.selectedNode;
|
||||
this._updateFollowing();
|
||||
}
|
||||
}
|
||||
|
||||
private _nodeMatchesFilter(node: NetworkNode, lowerFilter: string): boolean {
|
||||
@@ -265,8 +242,7 @@ export class HaNetworkGraph extends LitElement {
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean,
|
||||
isMobile: boolean,
|
||||
hasHighlightedNodes?: boolean,
|
||||
selectedNode?: string
|
||||
hasHighlightedNodes?: boolean
|
||||
) => ({
|
||||
id: "network",
|
||||
type: "graph",
|
||||
@@ -309,12 +285,7 @@ export class HaNetworkGraph extends LitElement {
|
||||
},
|
||||
edgeSymbol: ["none", "arrow"],
|
||||
edgeSymbolSize: 10,
|
||||
data: this._getSeriesData(
|
||||
data.nodes,
|
||||
data.links,
|
||||
this._nodePositions,
|
||||
selectedNode
|
||||
),
|
||||
data: this._getSeriesData(data.nodes, data.links, this._nodePositions),
|
||||
links: data.links.map((link) => ({
|
||||
...link,
|
||||
value: link.reverseValue
|
||||
@@ -332,28 +303,25 @@ export class HaNetworkGraph extends LitElement {
|
||||
(
|
||||
nodes: NetworkNode[],
|
||||
links: NetworkLink[],
|
||||
nodePositions: Record<string, { x: number; y: number }>,
|
||||
selectedNode?: string
|
||||
nodePositions: Record<string, { x: number; y: number }>
|
||||
) =>
|
||||
this._getPositionedNodes(nodes, links, nodePositions).map((node) => {
|
||||
const selected = node.id === selectedNode;
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: selected
|
||||
? { ...node.itemStyle, borderWidth: 3 }
|
||||
: node.itemStyle || {},
|
||||
label: selected ? { ...node.label, fontWeight: "bold" } : node.label,
|
||||
fixed: node.fixed,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
} as NonNullable<GraphSeriesOption["data"]>[number];
|
||||
}),
|
||||
this._getPositionedNodes(nodes, links, nodePositions).map(
|
||||
(node) =>
|
||||
({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
context: node.context,
|
||||
category: node.category,
|
||||
value: node.value,
|
||||
symbolSize: node.symbolSize || 30,
|
||||
symbol: node.symbol || "circle",
|
||||
itemStyle: node.itemStyle || {},
|
||||
label: node.label,
|
||||
fixed: node.fixed,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
}) as NonNullable<GraphSeriesOption["data"]>[number]
|
||||
),
|
||||
deepEqual
|
||||
);
|
||||
|
||||
@@ -509,94 +477,6 @@ export class HaNetworkGraph extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _updateFollowing() {
|
||||
const chart = this._followSelectedNode ? this._baseChart?.chart : undefined;
|
||||
if (chart !== this._followedChart) {
|
||||
const previous = this._followedChart;
|
||||
if (previous && !previous.isDisposed()) {
|
||||
previous.off("rendered", this._centerOnSelectedNode);
|
||||
previous.off("graphroam", this._handleGraphRoam);
|
||||
previous.getZr().off("drag", this._stopFollowing);
|
||||
}
|
||||
chart?.on("rendered", this._centerOnSelectedNode);
|
||||
chart?.on("graphroam", this._handleGraphRoam);
|
||||
chart?.getZr().on("drag", this._stopFollowing);
|
||||
this._followedChart = chart;
|
||||
}
|
||||
this._centerOnSelectedNode();
|
||||
}
|
||||
|
||||
private _stopFollowing = () => {
|
||||
this._followSelectedNode = false;
|
||||
this._updateFollowing();
|
||||
};
|
||||
|
||||
// A roam dispatched while ECharts renders is queued, so a flag set around
|
||||
// dispatchAction would be cleared before the event fires
|
||||
private _handleGraphRoam = (ev: unknown) => {
|
||||
if ((ev as { from?: string }).from !== FOLLOW_ROAM) {
|
||||
this._stopFollowing();
|
||||
}
|
||||
};
|
||||
|
||||
private _centerOnSelectedNode = () => {
|
||||
const chart = this._followedChart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
const series: any = chart
|
||||
// @ts-ignore private method but no other way to get the graph positions
|
||||
.getModel()
|
||||
.getSeriesByIndex(0);
|
||||
const node = series?.getGraph().getNodeById(this.selectedNode);
|
||||
const position: number[] | undefined = node?.getLayout();
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
const toPixel = (point: number[]) =>
|
||||
chart.convertToPixel({ seriesId: "network" }, point);
|
||||
const [x, y] = toPixel(position);
|
||||
const zoom = series.coordinateSystem.getRoamTransform()[0];
|
||||
const centerX = chart.getWidth() / 2;
|
||||
const centerY = chart.getHeight() / 2;
|
||||
// Zoom so the neighbors stay in view
|
||||
const fitX = Math.max(centerX - SELECTED_NODE_FIT_MARGIN, centerX / 2);
|
||||
const fitY = Math.max(centerY - SELECTED_NODE_FIT_MARGIN, centerY / 2);
|
||||
let targetZoom = SELECTED_NODE_MAX_ZOOM;
|
||||
for (const edge of node.edges) {
|
||||
const neighbor: number[] | undefined = (
|
||||
edge.node1 === node ? edge.node2 : edge.node1
|
||||
).getLayout();
|
||||
if (neighbor) {
|
||||
const [neighborX, neighborY] = toPixel(neighbor);
|
||||
targetZoom = Math.min(
|
||||
targetZoom,
|
||||
(zoom * fitX) / Math.abs(neighborX - x),
|
||||
(zoom * fitY) / Math.abs(neighborY - y)
|
||||
);
|
||||
}
|
||||
}
|
||||
const zoomChanged = Math.abs(zoom - targetZoom) >= 1e-3;
|
||||
if (
|
||||
!zoomChanged &&
|
||||
Math.abs(centerX - x) < 0.5 &&
|
||||
Math.abs(centerY - y) < 0.5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
chart.dispatchAction({
|
||||
type: "graphRoam",
|
||||
from: FOLLOW_ROAM,
|
||||
seriesId: "network",
|
||||
dx: centerX - x,
|
||||
dy: centerY - y,
|
||||
// Any zoom, even 1, rescales every node and edge
|
||||
zoom: zoomChanged ? targetZoom / zoom : undefined,
|
||||
originX: centerX,
|
||||
originY: centerY,
|
||||
});
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._emphasisGuardHandler) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import {
|
||||
@@ -106,8 +105,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@@ -160,7 +159,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { DEFAULT_CHART_WIDTH, MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -69,8 +68,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@@ -106,7 +105,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
grow-for-zoom-slider
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
@@ -29,9 +29,9 @@ import type {
|
||||
} from "../../common/translations/localize";
|
||||
import { formattersContext, registriesContext } from "../../data/context";
|
||||
import type { HomeAssistantFormatters, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
@@ -121,8 +121,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@query("ha-button.add") private _addButton?: HTMLElement;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -161,7 +159,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${
|
||||
this.allowCustom
|
||||
? html`
|
||||
@@ -266,9 +264,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
@@ -294,16 +290,22 @@ export class HaEntityNamePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
class="add"
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addItem}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._localize("ui.common.add")}
|
||||
</ha-button>
|
||||
${
|
||||
this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this._localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
${this._renderRestoreButton()}
|
||||
@@ -371,10 +373,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
if (!value.length) {
|
||||
await this.updateComplete;
|
||||
this._addButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
@@ -631,7 +629,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -724,8 +722,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: var(--ha-space-1) 0 0;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -245,15 +245,15 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newOrder: string[] = [];
|
||||
|
||||
for (const floorId of floorIds) {
|
||||
const isOnFloor = (areaId: string) =>
|
||||
(this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR) === floorId;
|
||||
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
|
||||
// Reordering returns the hidden areas of every floor, not just this one
|
||||
newHidden.push(...(value.hidden ?? []).filter(isOnFloor));
|
||||
newHidden.push(...(value.hidden ?? []));
|
||||
newOrder.push(...(value.order ?? []));
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter(isOnFloor);
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -28,10 +26,6 @@ import "./chips/ha-input-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
@@ -88,8 +82,6 @@ export class HaEntityTypePicker extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _titlesLoaded = false;
|
||||
|
||||
protected willUpdate() {
|
||||
@@ -245,13 +237,40 @@ export class HaEntityTypePicker extends LitElement {
|
||||
const localize = this._i18n?.localize;
|
||||
const value = this.value ?? [];
|
||||
const chipLabels = localize ? this._chipLabels(localize, value) : undefined;
|
||||
const addLabel = localize?.("ui.common.add");
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label id="label">${this.label}</label>` : nothing}
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${
|
||||
value.length && chipLabels
|
||||
? html`
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels.get(key)!;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-generic-picker
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.value=${""}
|
||||
.addButtonLabel=${localize?.("ui.components.entity-type-picker.add")}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchFn=${this._searchFn}
|
||||
@@ -259,53 +278,10 @@ export class HaEntityTypePicker extends LitElement {
|
||||
.emptyLabel=${localize?.("ui.components.entity-type-picker.no_types")}
|
||||
no-sort
|
||||
@value-changed=${this._itemAdded}
|
||||
>
|
||||
<ha-chip-set
|
||||
slot="field"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(key) => key,
|
||||
(key) => {
|
||||
const label = chipLabels?.get(key) ?? key;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-key=${key}
|
||||
.label=${label}
|
||||
.disabled=${this.disabled}
|
||||
@remove=${this._removeItem}
|
||||
selected
|
||||
>
|
||||
${this._renderIcon(key, "icon")} ${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-generic-picker>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _itemAdded(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.detail.value;
|
||||
@@ -344,15 +320,7 @@ export class HaEntityTypePicker extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
ha-chip-set {
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: var(--ha-space-1);
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import {
|
||||
@@ -23,10 +24,21 @@ import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-badge";
|
||||
import "./ha-list";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
export interface IntegrationFilterCount {
|
||||
key: string;
|
||||
path: string;
|
||||
count: number;
|
||||
label: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations extends LitElement {
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
@@ -35,6 +47,12 @@ export class HaFilterIntegrations extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
/** Counts to show beside each integration, by domain. */
|
||||
@property({ attribute: false }) public counts?: Record<
|
||||
string,
|
||||
IntegrationFilterCount[]
|
||||
>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -120,6 +138,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name}
|
||||
${this._renderCounts(integration.domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>`
|
||||
@@ -131,6 +150,32 @@ export class HaFilterIntegrations extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCounts(domain: string) {
|
||||
const counts = this.counts?.[domain];
|
||||
|
||||
if (!counts?.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<span class="counts">
|
||||
${counts.map(
|
||||
(item) =>
|
||||
html`<ha-badge
|
||||
id="count-${domain}-${item.key}"
|
||||
role="img"
|
||||
aria-label=${item.label}
|
||||
style=${styleMap({ "--badge-color": item.color })}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${item.path}></ha-svg-icon>
|
||||
${item.count}
|
||||
</ha-badge>
|
||||
<ha-tooltip for="count-${domain}-${item.key}">
|
||||
${item.label}
|
||||
</ha-tooltip>`
|
||||
)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -246,6 +291,15 @@ export class HaFilterIntegrations extends LitElement {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
.counts {
|
||||
display: inline-flex;
|
||||
gap: var(--ha-space-1);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
vertical-align: middle;
|
||||
--ha-badge-size: 22px;
|
||||
--ha-badge-icon-size: 14px;
|
||||
--ha-badge-font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-button";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import "../radio/ha-radio-group";
|
||||
import "../radio/ha-radio-option";
|
||||
|
||||
@@ -30,7 +28,10 @@ import "../radio/ha-radio-option";
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@@ -47,8 +48,6 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
@@ -84,11 +83,7 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
if (this.selector.select?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this._i18n.locale.language
|
||||
)
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,8 +136,8 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
return html`
|
||||
<div class="checkbox-list">
|
||||
${this.label} ${this._renderHelper()}
|
||||
<div>
|
||||
${this.label}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-checkbox
|
||||
@@ -156,6 +151,7 @@ export class HaSelectSelector extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -163,80 +159,67 @@ export class HaSelectSelector extends LitElement {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : ensureArray(this.value);
|
||||
|
||||
const addLabel = this._i18n.localize("ui.common.add");
|
||||
|
||||
return html`
|
||||
${
|
||||
this.label
|
||||
? html`<span id="label" class="label chips-label"
|
||||
>${this.label}</span
|
||||
>`
|
||||
value?.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item
|
||||
}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${""}
|
||||
.addButtonLabel=${this.label}
|
||||
.getItems=${this._getItems(options, value, true)}
|
||||
.allowCustomValue=${this.selector.select.custom_value ?? false}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
>
|
||||
<ha-sortable
|
||||
slot="field"
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
draggable-selector="ha-input-chip"
|
||||
handle-selector="button.primary.action"
|
||||
>
|
||||
<ha-chip-set
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)?.label ||
|
||||
item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
.title=${label}
|
||||
selected
|
||||
>
|
||||
${
|
||||
this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${addLabel}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -281,11 +264,6 @@ export class HaSelectSelector extends LitElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._picker?.open();
|
||||
}
|
||||
|
||||
private _getOptions = memoizeOne(
|
||||
(selector: SelectSelector) =>
|
||||
selector.select?.options?.map((option) =>
|
||||
@@ -426,37 +404,19 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.chips-label {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-sortable ha-chip-set {
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-select-box + ha-input-helper-text {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
ha-generic-picker + ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-list ha-input-helper-text {
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@@ -98,9 +98,8 @@ export class HaUnitOfMeasurementSelector extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (units.length === 1 && units[0] !== null) {
|
||||
// Preselect the only allowed unit; "no unit" is not allowed either, so
|
||||
// this also applies to an optional field
|
||||
if (this.required && units.length === 1 && units[0] !== null) {
|
||||
// Preselect the only allowed unit
|
||||
fireEvent(this, "value-changed", { value: units[0] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
@@ -129,7 +128,7 @@ export class HaListVirtualized extends HaListBase {
|
||||
tabindex="-1"
|
||||
scroller
|
||||
.items=${this.rows}
|
||||
.renderItem=${this._getRenderItem(this.rowRenderer)}
|
||||
.renderItem=${this.rowRenderer}
|
||||
style="min-height: 36px; height: 100%;"
|
||||
.layout=${
|
||||
!this._unpinned && this.pinIndex !== undefined
|
||||
@@ -364,19 +363,7 @@ export class HaListVirtualized extends HaListBase {
|
||||
return Math.max(1, this.rangeEnd - this.rangeStart + 1);
|
||||
}
|
||||
|
||||
// The virtualizer renders its last range again when it re-renders before it
|
||||
// lays out shorter rows, so rows past the end arrive as undefined.
|
||||
private _keyFunction = (
|
||||
item: HaListVirtualizedItem | undefined,
|
||||
index: number
|
||||
) => item?.id ?? index;
|
||||
|
||||
private _getRenderItem = memoizeOne(
|
||||
(rowRenderer?: RenderItemFunction<HaListVirtualizedItem>) =>
|
||||
rowRenderer &&
|
||||
((item: HaListVirtualizedItem | undefined, index: number) =>
|
||||
item ? rowRenderer(item, index) : nothing)
|
||||
);
|
||||
private _keyFunction = (item: HaListVirtualizedItem) => item.id;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleUnpinned() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min";
|
||||
import "../resources/scoped-registry-ready";
|
||||
import "../layouts/home-assistant";
|
||||
|
||||
import("../resources/append-ha-style");
|
||||
@@ -152,6 +152,16 @@
|
||||
window.latestJS = true;
|
||||
}
|
||||
</script>
|
||||
<% if (obj.hassUrl === "") { %>
|
||||
<script>
|
||||
{%- for extra_module in extra_modules -%}
|
||||
// Caught, or the boot recovery guard reads it as a stale build.
|
||||
import("{{ extra_module }}").catch(function (err) {
|
||||
console.error("Failed to load extra module {{ extra_module }}", err);
|
||||
});
|
||||
{%- endfor -%}
|
||||
</script>
|
||||
<% } %>
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
// Below the [legacy] browserslist floor the entry bundles cannot be
|
||||
@@ -176,24 +186,10 @@
|
||||
|
||||
<% if (obj.hassUrl === "") { %>
|
||||
<script>
|
||||
// The app entry calls this right after it installs the scoped custom
|
||||
// element registry polyfill, so extra modules define their elements in
|
||||
// the registry that the app uses.
|
||||
window.__haLoadExtraModules = function () {
|
||||
{%- for extra_module in extra_modules -%}
|
||||
// Caught, or the boot recovery guard reads it as a stale build.
|
||||
import("{{ extra_module }}").catch(function (err) {
|
||||
console.error("Failed to load extra module {{ extra_module }}", err);
|
||||
});
|
||||
if (!window.latestJS) {
|
||||
{%- for extra_script in extra_js_es5 -%}
|
||||
_ls("{{ extra_script }}");
|
||||
{%- endfor -%}
|
||||
if (!window.latestJS) {
|
||||
{%- for extra_script in extra_js_es5 -%}
|
||||
_ls("{{ extra_script }}");
|
||||
{%- endfor -%}
|
||||
}
|
||||
};
|
||||
if (window.__haScopedRegistryReady) {
|
||||
window.__haLoadExtraModules();
|
||||
}
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
@@ -1064,9 +1064,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.search-toolbar ha-input-search {
|
||||
min-width: 0;
|
||||
}
|
||||
.filters {
|
||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
||||
|
||||
@@ -180,13 +180,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
/* Keep the negative z-index panel backgrounds (the resolver's ::before
|
||||
and the Lovelace view background) in this host's stacking context.
|
||||
Isolating the conditionally rendered partial-panel-resolver instead
|
||||
can intermittently leave the Android WebView (Chromium 109-124)
|
||||
accessibility tree with an empty panel subtree while the page paints
|
||||
normally, which breaks UI automation such as the companion app E2E. */
|
||||
isolation: isolate;
|
||||
--ha-sidebar-width: calc(56px + var(--safe-area-inset-left, 0px));
|
||||
--ha-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
|
||||
--safe-area-content-inset-left: 0px;
|
||||
@@ -208,6 +201,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
partial-panel-resolver {
|
||||
display: block;
|
||||
height: 100%;
|
||||
isolation: isolate;
|
||||
}
|
||||
partial-panel-resolver::before {
|
||||
content: "";
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { ConditionEvaluation } from "../common/controllers/condition-evalua
|
||||
import { ConditionEvaluatorController } from "../common/controllers/condition-evaluator-controller";
|
||||
import { maxColumnsContext } from "../panels/lovelace/common/context";
|
||||
import { evaluateConditionsLocally } from "../common/condition/evaluate-locally";
|
||||
import { dropInvalidConditions } from "../common/condition/sanitize";
|
||||
import type {
|
||||
ConditionContext,
|
||||
VisibilityCondition,
|
||||
@@ -154,28 +153,27 @@ export const ConditionalListenerMixin = <
|
||||
const finalConditions = conditions ?? config?.visibility;
|
||||
const entityId = this._conditionContext.entity_id;
|
||||
|
||||
this.__conditions = finalConditions;
|
||||
|
||||
// Fold in the host entity and keep a stable array across hass updates.
|
||||
if (
|
||||
finalConditions !== this.__observedSource ||
|
||||
entityId !== this.__observedEntityId
|
||||
) {
|
||||
const validConditions =
|
||||
finalConditions && dropInvalidConditions(finalConditions);
|
||||
// Tree content changed; don't keep the previous result for a frame.
|
||||
const signature = validConditions
|
||||
? JSON.stringify(validConditions)
|
||||
const signature = finalConditions
|
||||
? JSON.stringify(finalConditions)
|
||||
: undefined;
|
||||
if (signature !== this.__conditionsSignature) {
|
||||
this.__conditionsSignature = signature;
|
||||
this.__conditionResult = "unknown";
|
||||
}
|
||||
this.__conditions = validConditions;
|
||||
this.__observedSource = finalConditions;
|
||||
this.__observedEntityId = entityId;
|
||||
this.__observed =
|
||||
validConditions && entityId
|
||||
? validConditions.map((c) => addEntityToCondition(c, entityId))
|
||||
: validConditions;
|
||||
finalConditions && entityId
|
||||
? finalConditions.map((c) => addEntityToCondition(c, entityId))
|
||||
: finalConditions;
|
||||
}
|
||||
|
||||
this.__conditionEvaluator.observe(
|
||||
|
||||
@@ -942,7 +942,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
+4
-13
@@ -27,7 +27,6 @@ import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeEntityNameParts } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { getDeviceAreaId } from "../../../../common/entity/context/get_device_context";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon";
|
||||
@@ -1068,16 +1067,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
private _renderEntityIcon =
|
||||
(stateObj: HassEntity) => (slot: string | undefined) =>
|
||||
stateObj.attributes.entity_picture ||
|
||||
stateObj.attributes.entity_picture_local
|
||||
? html`<state-badge
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
: html`<ha-state-icon
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
html`<ha-state-icon
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
|
||||
private _renderItem(
|
||||
label: string,
|
||||
@@ -1902,8 +1895,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
state-badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
margin: var(--ha-space-1);
|
||||
}
|
||||
|
||||
wa-tree-item[selected]::part(item):hover {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
mdiPlaylistCheck,
|
||||
mdiRobotOutline,
|
||||
mdiScriptTextOutline,
|
||||
mdiViewDashboardOutline,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "../../../../common/decorators/consume";
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
@@ -35,8 +34,7 @@ type DeviceAddToAction =
|
||||
kind: "add-to";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & { kind: "scene" })
|
||||
| (AddToActionListItem & { kind: "dashboard" });
|
||||
| (AddToActionListItem & { kind: "scene" });
|
||||
|
||||
@customElement("dialog-device-add-to")
|
||||
export class DialogDeviceAddTo extends LitElement {
|
||||
@@ -96,20 +94,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
}
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.dashboards_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "dashboard",
|
||||
iconPath: mdiViewDashboardOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.dashboard"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
@@ -202,11 +186,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
this._handleCreateScene();
|
||||
return;
|
||||
}
|
||||
if (action.kind === "dashboard") {
|
||||
this.closeDialog();
|
||||
this._params.addToDashboard();
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleAddToAction(action.key);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { until } from "lit/directives/until";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-list";
|
||||
@@ -13,6 +14,11 @@ import type { EntityRegistryEntry } from "../../../../data/entity/entity_registr
|
||||
import { entryIcon } from "../../../../data/icons";
|
||||
import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
computeCards,
|
||||
computeSection,
|
||||
} from "../../../lovelace/common/generate-lovelace-config";
|
||||
import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view";
|
||||
import type { EntityRegistryStateEntry } from "../ha-config-device-page";
|
||||
import { entityRowElement } from "../../../lovelace/entity-rows/entity-row-element-directive";
|
||||
|
||||
@@ -120,6 +126,13 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" @click=${this._addToLovelaceView}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.add_entities_lovelace"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -176,6 +189,27 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
showMoreInfoDialog(this, { entityId: entry.entity_id });
|
||||
}
|
||||
|
||||
private _addToLovelaceView(): void {
|
||||
const entities = this.entities
|
||||
.filter((entity) => !entity.disabled_by)
|
||||
.map((entity) => entity.entity_id);
|
||||
|
||||
addEntitiesToLovelaceView(
|
||||
this,
|
||||
this.hass,
|
||||
computeCards(
|
||||
this.hass,
|
||||
entities,
|
||||
{ title: this.deviceName },
|
||||
{ entityName: { type: "entity" } }
|
||||
),
|
||||
computeSection(entities, {
|
||||
title: this.deviceName,
|
||||
}),
|
||||
entities
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -249,6 +283,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
height: 40px;
|
||||
--mdc-ripple-color: transparent;
|
||||
}
|
||||
.card-actions {
|
||||
padding: 4px 16px 4px 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface DeviceAddToDialogParams {
|
||||
device: DeviceRegistryEntry;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
addToDashboard: () => void;
|
||||
}
|
||||
|
||||
export const loadDeviceAddToDialog = () => import("./ha-device-add-to-dialog");
|
||||
|
||||
@@ -117,8 +117,6 @@ import type { ESPHomeSetupController } from "./device-detail/integration-element
|
||||
import "./device-detail/ha-device-linked-devices-card";
|
||||
import "./device-detail/ha-device-via-devices-card";
|
||||
import { showDeviceAddToDialog } from "./device-detail/show-dialog-device-add-to";
|
||||
import { addEntitiesToLovelaceView } from "../../lovelace/editor/add-entities-to-view";
|
||||
import { showAddDeviceCardDialog } from "../../lovelace/editor/card-editor/show-add-device-card-dialog";
|
||||
import {
|
||||
loadDeviceRegistryDetailDialog,
|
||||
showDeviceRegistryDetailDialog,
|
||||
@@ -517,186 +515,205 @@ export class HaConfigDevicePage extends LitElement {
|
||||
const hasSceneSupport =
|
||||
isComponentLoaded(this.hass.config, "scene") && sceneEntityIds.length;
|
||||
|
||||
const relatedCard = html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.related_heading"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
.disabled=${device.disabled_by}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.add_to.item")}
|
||||
</ha-button>
|
||||
</h1>
|
||||
${
|
||||
!this._related
|
||||
? html`
|
||||
<div class="card-content loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: this._related.automation?.length ||
|
||||
this._related.script?.length ||
|
||||
this._related.scene?.length
|
||||
? html`
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "automation") &&
|
||||
this._related.automation?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).automation.map(
|
||||
(automation) =>
|
||||
automation
|
||||
? html`<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(automation)}
|
||||
.href=${
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`
|
||||
const relatedCard =
|
||||
isComponentLoaded(this.hass.config, "automation") ||
|
||||
isComponentLoaded(this.hass.config, "script") ||
|
||||
hasSceneSupport
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.related_heading"
|
||||
)}
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
.disabled=${device.disabled_by}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>
|
||||
</h1>
|
||||
${
|
||||
!this._related
|
||||
? html`
|
||||
<div class="card-content loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: this._related.automation?.length ||
|
||||
this._related.script?.length ||
|
||||
this._related.scene?.length
|
||||
? html`
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "automation") &&
|
||||
this._related.automation?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(
|
||||
this._related
|
||||
).automation.map((automation) =>
|
||||
automation
|
||||
? html`<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(automation)}
|
||||
.href=${
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`
|
||||
}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "script") &&
|
||||
this._related.script?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).script.map(
|
||||
(script) => {
|
||||
if (!script) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._entityReg.find(
|
||||
(e) => e.entity_id === script.entity_id
|
||||
);
|
||||
const url = entry
|
||||
? `/config/script/edit/${entry.unique_id}`
|
||||
: `/config/script/show/${script.entity_id}`;
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(script)}
|
||||
.href=${url}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasSceneSupport && this._related.scene?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).scene.map(
|
||||
(scene) => {
|
||||
if (!scene) {
|
||||
return nothing;
|
||||
}
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "script") &&
|
||||
this._related.script?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).script.map(
|
||||
(script) => {
|
||||
if (!script) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._entityReg.find(
|
||||
(e) => e.entity_id === script.entity_id
|
||||
);
|
||||
const url = entry
|
||||
? `/config/script/edit/${entry.unique_id}`
|
||||
: `/config/script/show/${script.entity_id}`;
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(script)}
|
||||
.href=${url}
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasSceneSupport && this._related.scene?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).scene.map(
|
||||
(scene) => {
|
||||
if (!scene) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sceneId = `scene-${slugify(
|
||||
scene.entity_id
|
||||
)}`;
|
||||
const sceneId = `scene-${slugify(
|
||||
scene.entity_id
|
||||
)}`;
|
||||
|
||||
return scene.attributes.id
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
.href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item-base
|
||||
id=${sceneId}
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-base>
|
||||
<ha-tooltip
|
||||
.for=${sceneId}
|
||||
placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: html`
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.config.devices.add_prompt", {
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_scripts_or_scenes"
|
||||
),
|
||||
type: this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
})}
|
||||
${add_prompt}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
`;
|
||||
return scene.attributes.id
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
.href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item-base
|
||||
id=${sceneId}
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-base>
|
||||
<ha-tooltip
|
||||
.for=${sceneId}
|
||||
placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: html`
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.add_prompt",
|
||||
{
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_scripts_or_scenes"
|
||||
),
|
||||
type: this.hass.localize(
|
||||
`ui.panel.config.devices.type.${
|
||||
device.entry_type || "device"
|
||||
}`
|
||||
),
|
||||
}
|
||||
)}
|
||||
${add_prompt}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</ha-card>
|
||||
`
|
||||
: "";
|
||||
|
||||
const infoColumn = html`
|
||||
${
|
||||
@@ -1429,16 +1446,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
sceneEntityIds.length > 0,
|
||||
addToDashboard: () => this._addToDashboard(),
|
||||
});
|
||||
}
|
||||
|
||||
private _addToDashboard() {
|
||||
const deviceId = this.deviceId;
|
||||
addEntitiesToLovelaceView(this, this.hass, {
|
||||
sectionStrategy: { type: "device", device_id: deviceId },
|
||||
showCardDialog: (params) =>
|
||||
showAddDeviceCardDialog(this, { ...params, deviceId }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1645,7 +1652,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
@@ -101,8 +101,6 @@ export class DialogYamlIntegration extends LitElement {
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
--ha-dialog-header-white-space: normal;
|
||||
--ha-dialog-header-title-height: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
} else if (this._currentPage === "device") {
|
||||
el.ieee = this.routeTail.path.split("/")[1];
|
||||
} else if (this._currentPage === "visualization") {
|
||||
el.deviceId = this.routeTail.path.substr(1);
|
||||
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "section") {
|
||||
el.sectionId = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
-7
@@ -39,8 +39,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public deviceId?: string;
|
||||
|
||||
@state()
|
||||
private _networkData: NetworkData = {
|
||||
nodes: [],
|
||||
@@ -80,11 +78,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
.searchFilter=${this._searchFilter}
|
||||
.data=${this._networkData}
|
||||
.searchableAttributes=${this._getSearchableAttributes}
|
||||
.selectedNode=${
|
||||
this._devices.find(
|
||||
(device) => device.device_reg_id === this.deviceId
|
||||
)?.ieee
|
||||
}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAlertCircle,
|
||||
mdiAlertOctagon,
|
||||
mdiBugOutline,
|
||||
mdiChevronDown,
|
||||
mdiChip,
|
||||
mdiDns,
|
||||
mdiFilterVariant,
|
||||
mdiInformationOutline,
|
||||
mdiPackageVariant,
|
||||
mdiPuzzle,
|
||||
mdiRadar,
|
||||
@@ -17,16 +22,14 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { atLeastVersion } from "../../../common/config/version";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import {
|
||||
getHistoryState,
|
||||
navigate,
|
||||
updateHistoryState,
|
||||
} from "../../../common/navigate";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { currentPath } from "../../../common/url/current-path";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import {
|
||||
@@ -35,12 +38,14 @@ import {
|
||||
logsLinkFromUrl,
|
||||
logsQueryParamConfig,
|
||||
logsQueryParams,
|
||||
type LogsState,
|
||||
} from "../../../common/url/logs-query-params";
|
||||
import { createQueryString } from "../../../common/url/query-params";
|
||||
import "../../../components/ha-app-icon";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-combo-box-item";
|
||||
import "../../../components/ha-filter-integrations";
|
||||
import type { IntegrationFilterCount } from "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-pane";
|
||||
import "../../../components/ha-filter-pane-chip";
|
||||
import "../../../components/ha-generic-picker";
|
||||
@@ -55,6 +60,7 @@ import {
|
||||
fetchHassioAddonsInfo,
|
||||
type HassioAddonInfo,
|
||||
} from "../../../data/hassio/addon";
|
||||
import type { LoggedError, SystemLogLevel } from "../../../data/system_log";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
@@ -66,6 +72,18 @@ import type { SystemLogCard } from "./system-log-card";
|
||||
|
||||
const NO_INTEGRATIONS: string[] = [];
|
||||
|
||||
const LOG_LEVELS: {
|
||||
level: SystemLogLevel;
|
||||
path: string;
|
||||
color?: string;
|
||||
}[] = [
|
||||
{ level: "critical", path: mdiAlertOctagon, color: "var(--error-color)" },
|
||||
{ level: "error", path: mdiAlertCircle, color: "var(--error-color)" },
|
||||
{ level: "warning", path: mdiAlert, color: "var(--warning-color)" },
|
||||
{ level: "info", path: mdiInformationOutline },
|
||||
{ level: "debug", path: mdiBugOutline },
|
||||
];
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "core",
|
||||
@@ -93,8 +111,6 @@ const logProviders: LogProvider[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const LOGS_HISTORY_STATE = "logsPage";
|
||||
|
||||
interface LogProviderPickerItem extends PickerComboBoxItem {
|
||||
addon?: HassioAddonInfo;
|
||||
hasAppIcon?: boolean;
|
||||
@@ -128,6 +144,29 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@state() private _integrationsExpanded = true;
|
||||
|
||||
@state() private _logItems?: LoggedError[];
|
||||
|
||||
// What the user picked, shown again when the page is opened without params,
|
||||
// like from the sidebar. What a link shows isn't stored.
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "logs-search",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedSearch = "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "logs-integrations",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedIntegrations: string[] = [];
|
||||
|
||||
@storage({ key: "logs-raw", state: false, subscribe: false })
|
||||
private _storedRaw = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: ContextType<typeof manifestsContext>;
|
||||
@@ -138,7 +177,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _queryParams = new QueryParamsController(this, logsQueryParamConfig, {
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyUrl(),
|
||||
navigated: (ev) => this._navigated(ev),
|
||||
});
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -164,6 +203,10 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _filterChanged(ev: HASSDomTargetEvent<HaInputSearch>) {
|
||||
this._filter = ev.target.value ?? "";
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedSearch = this._filter;
|
||||
}
|
||||
this._writeUrl(true);
|
||||
}
|
||||
|
||||
@@ -232,6 +275,15 @@ export class HaConfigLogs extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.value=${this._integrations}
|
||||
.expanded=${this._integrationsExpanded}
|
||||
.counts=${
|
||||
this._detail
|
||||
? undefined
|
||||
: this._getIntegrationCounts(
|
||||
this._logItems,
|
||||
this._manifests,
|
||||
this.hass.localize
|
||||
)
|
||||
}
|
||||
@data-table-filter-changed=${this._integrationsChanged}
|
||||
@expanded-changed=${this._integrationsExpandedChanged}
|
||||
></ha-filter-integrations>
|
||||
@@ -277,6 +329,7 @@ export class HaConfigLogs extends LitElement {
|
||||
this._manifests
|
||||
)}
|
||||
@switch-log-view=${this._showDetail}
|
||||
@system-log-items-changed=${this._logItemsChanged}
|
||||
></system-log-card>
|
||||
`
|
||||
: html`<error-log-card
|
||||
@@ -301,6 +354,10 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _showDetail() {
|
||||
this._detail = !this._detail;
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedRaw = this._detail;
|
||||
}
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
@@ -317,12 +374,12 @@ export class HaConfigLogs extends LitElement {
|
||||
this._selectedLogProvider = provider;
|
||||
this._filter = "";
|
||||
this._integrations = [];
|
||||
this._storedSearch = "";
|
||||
this._storedIntegrations = [];
|
||||
// Keep the search typed so far on the entry Back returns to
|
||||
this._queryParams.flush();
|
||||
// Picking another provider is a navigation of its own, so Back returns
|
||||
// to the previous one. Both entries are marked, so going back to one
|
||||
// without params shows the default view instead of keeping the provider.
|
||||
updateHistoryState({ [LOGS_HISTORY_STATE]: true });
|
||||
// to the previous one
|
||||
const search = createQueryString(
|
||||
logsQueryParams({
|
||||
provider,
|
||||
@@ -332,10 +389,9 @@ export class HaConfigLogs extends LitElement {
|
||||
}),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
// From the current path, so a trailing slash still matches the controller
|
||||
navigate(`${currentPath()}${search ? `?${search}` : ""}`, {
|
||||
data: { [LOGS_HISTORY_STATE]: true },
|
||||
});
|
||||
navigate(`${currentPath()}${search ? `?${search}` : ""}`);
|
||||
}
|
||||
|
||||
private _toggleFilters() {
|
||||
@@ -351,12 +407,27 @@ export class HaConfigLogs extends LitElement {
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
this._integrations = Array.isArray(value) ? value : [];
|
||||
const integrations = Array.isArray(value) ? value : [];
|
||||
|
||||
// The filter also sends the value it was given once it has its items, in
|
||||
// its own order, so only a change is the user's
|
||||
if (
|
||||
this._currentProvider() === DEFAULT_LOG_PROVIDER &&
|
||||
!deepEqual([...integrations].sort(), [...this._integrations].sort())
|
||||
) {
|
||||
this._storedIntegrations = integrations;
|
||||
}
|
||||
|
||||
this._integrations = integrations;
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
private _clearIntegrations() {
|
||||
this._integrations = [];
|
||||
|
||||
if (this._currentProvider() === DEFAULT_LOG_PROVIDER) {
|
||||
this._storedIntegrations = [];
|
||||
}
|
||||
this._writeUrl();
|
||||
}
|
||||
|
||||
@@ -366,20 +437,49 @@ export class HaConfigLogs extends LitElement {
|
||||
this._integrationsExpanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
// Until the apps are loaded, the provider is the one the URL asks for
|
||||
private _currentProvider(): string {
|
||||
return this._providersLoaded
|
||||
? this._selectedLogProvider
|
||||
: (logsLinkFromUrl(this._queryParams.searchParams() ?? "")?.provider ??
|
||||
DEFAULT_LOG_PROVIDER);
|
||||
}
|
||||
|
||||
private _writeUrl(debounced = false) {
|
||||
this._queryParams.write(
|
||||
logsQueryParams({
|
||||
// Until the apps are loaded, keep the provider the URL asks for
|
||||
provider: this._providersLoaded
|
||||
? this._selectedLogProvider
|
||||
: (logsLinkFromUrl(this._queryParams.searchParams() ?? "")
|
||||
?.provider ?? DEFAULT_LOG_PROVIDER),
|
||||
search: this._filter,
|
||||
integrations: this._integrations,
|
||||
raw: this._detail,
|
||||
}),
|
||||
debounced
|
||||
);
|
||||
this._queryParams.write(logsQueryParams(this._urlState()), debounced);
|
||||
}
|
||||
|
||||
private _urlState(): LogsState {
|
||||
return {
|
||||
provider: this._currentProvider(),
|
||||
search: this._filter,
|
||||
integrations: this._integrations,
|
||||
raw: this._detail,
|
||||
};
|
||||
}
|
||||
|
||||
// Closing a dialog, like an entry's details, goes back to the URL the page
|
||||
// is already showing, so there's nothing to apply. A link is always applied.
|
||||
private _navigated(ev?: Event) {
|
||||
const searchParams = this._queryParams.searchParams();
|
||||
|
||||
if (ev?.type === "popstate" && searchParams) {
|
||||
const shown = createQueryString(
|
||||
logsQueryParams(this._urlState()),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
const back = createQueryString(
|
||||
logsQueryParams(logsLinkFromUrl(searchParams) ?? DEFAULT_LOGS_STATE),
|
||||
logsQueryParamConfig
|
||||
);
|
||||
|
||||
if (back === shown) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._applyUrl();
|
||||
}
|
||||
|
||||
private async _init() {
|
||||
@@ -389,18 +489,30 @@ export class HaConfigLogs extends LitElement {
|
||||
// Keep a search typed while the apps were loading
|
||||
this._queryParams.flush();
|
||||
this._providersLoaded = true;
|
||||
this._applyUrl();
|
||||
// What was stored is already applied, and an empty URL now is a link like
|
||||
// `?provider=core` that was tidied up
|
||||
this._applyUrl(false);
|
||||
}
|
||||
|
||||
private _applyUrl() {
|
||||
private _applyUrl(restoreStored = true) {
|
||||
const searchParams = this._queryParams.searchParams();
|
||||
if (!searchParams) {
|
||||
return;
|
||||
}
|
||||
// A link is shown, but not stored. Opened without params, like from the
|
||||
// sidebar or Back to before another provider was picked, the page shows
|
||||
// what the user picked.
|
||||
const link =
|
||||
logsLinkFromUrl(searchParams) ??
|
||||
(getHistoryState()?.[LOGS_HISTORY_STATE]
|
||||
? DEFAULT_LOGS_STATE
|
||||
(searchParams.toString() === ""
|
||||
? restoreStored
|
||||
? {
|
||||
...DEFAULT_LOGS_STATE,
|
||||
search: this._storedSearch,
|
||||
integrations: this._storedIntegrations,
|
||||
raw: this._storedRaw,
|
||||
}
|
||||
: DEFAULT_LOGS_STATE
|
||||
: undefined);
|
||||
let missingProvider: string | undefined;
|
||||
if (link) {
|
||||
@@ -485,6 +597,87 @@ export class HaConfigLogs extends LitElement {
|
||||
: undefined
|
||||
);
|
||||
|
||||
// Matches entries to integrations by logger name, the same way
|
||||
// `_getIntegrationLoggers` filters them
|
||||
private _getIntegrationCounts = memoizeOne(
|
||||
(
|
||||
items: LoggedError[] | undefined,
|
||||
manifests: ContextType<typeof manifestsContext> | undefined,
|
||||
localize: LocalizeFunc
|
||||
): Record<string, IntegrationFilterCount[]> | undefined => {
|
||||
if (!items?.length || !manifests) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const loggerDomains = new Map<string, string[]>();
|
||||
|
||||
for (const { domain, loggers } of Object.values(manifests)) {
|
||||
for (const logger of [
|
||||
`homeassistant.components.${domain}`,
|
||||
`custom_components.${domain}`,
|
||||
...(loggers ?? []),
|
||||
]) {
|
||||
loggerDomains.set(logger, [
|
||||
...(loggerDomains.get(logger) ?? []),
|
||||
domain,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const levelCounts: Record<
|
||||
string,
|
||||
Partial<Record<SystemLogLevel, number>>
|
||||
> = {};
|
||||
|
||||
for (const item of items) {
|
||||
const parts = item.name.split(".");
|
||||
|
||||
const domains = new Set(
|
||||
parts.flatMap(
|
||||
(_part, index) =>
|
||||
loggerDomains.get(parts.slice(0, index + 1).join(".")) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
for (const domain of domains) {
|
||||
const counts = (levelCounts[domain] ??= {});
|
||||
|
||||
counts[item.level] = (counts[item.level] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(levelCounts).map(([domain, counts]) => [
|
||||
domain,
|
||||
LOG_LEVELS.flatMap(({ level, path, color }) => {
|
||||
const count = counts[level];
|
||||
|
||||
return count
|
||||
? [
|
||||
{
|
||||
key: level,
|
||||
path,
|
||||
color,
|
||||
count,
|
||||
label: localize(
|
||||
`ui.panel.config.logs.level_count.${level}`,
|
||||
{ count }
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _logItemsChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["system-log-items-changed"]>
|
||||
) {
|
||||
this._logItems = ev.detail.items;
|
||||
}
|
||||
|
||||
private _getLogProviderItems = (): LogProviderPickerItem[] =>
|
||||
this._logProviders.map((provider) => ({
|
||||
id: provider.key,
|
||||
|
||||
@@ -294,6 +294,14 @@ export class SystemLogCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("_items")) {
|
||||
fireEvent(this, "system-log-items-changed", { items: this._items });
|
||||
}
|
||||
}
|
||||
|
||||
protected serviceCalled(ev): void {
|
||||
// Check if this is for us
|
||||
if (ev.detail.success && ev.detail.domain === "system_log") {
|
||||
@@ -404,4 +412,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"system-log-card": SystemLogCard;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"system-log-items-changed": { items: LoggedError[] | undefined };
|
||||
}
|
||||
}
|
||||
@@ -106,10 +106,6 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
.discovery-card ha-list-item-button::part(headline),
|
||||
.discovery-card ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,6 @@ const mountPartitionLabel = ({
|
||||
return drive ? `${drive} — ${identity}, ${size}` : `${identity}, ${size}`;
|
||||
};
|
||||
|
||||
// Supervisor accepts only ASCII letters, digits and underscores.
|
||||
const mountNameFromLabel = (label?: string): string =>
|
||||
(label ?? "").replace(/^\W+|\W+$/g, "").replace(/\W+/g, "_");
|
||||
|
||||
const requiredFieldsFilled = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data?: MountFormData
|
||||
@@ -309,8 +305,6 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
|
||||
private _originalType?: SupervisorMountType;
|
||||
|
||||
private _suggestedName?: string;
|
||||
|
||||
@state() private _diskIdentity?: string;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -535,9 +529,7 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._validationError = {};
|
||||
this._validationWarning = {};
|
||||
const previous = this._data;
|
||||
this._data = ev.detail.value;
|
||||
this._suggestName(previous);
|
||||
if (this._data?.name && !/^\w+$/.test(this._data.name)) {
|
||||
this._validationError.name = "invalid_name";
|
||||
}
|
||||
@@ -564,32 +556,6 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
);
|
||||
}
|
||||
|
||||
// Name a new disk mount after the partition's volume label, unless the user
|
||||
// typed a name of their own.
|
||||
private _suggestName(previous?: MountFormData) {
|
||||
const data = this._data;
|
||||
if (data?.name !== previous?.name) {
|
||||
this._suggestedName = undefined;
|
||||
}
|
||||
if (
|
||||
this._existing ||
|
||||
data?.type !== SupervisorMountType.DISK ||
|
||||
(data.device === previous?.device &&
|
||||
previous?.type === SupervisorMountType.DISK) ||
|
||||
(data.name && data.name !== this._suggestedName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const name = mountNameFromLabel(
|
||||
this._partitions?.find((entry) => entry.partition.device === data.device)
|
||||
?.partition.label
|
||||
);
|
||||
if (name !== (data.name ?? "")) {
|
||||
data.name = name;
|
||||
}
|
||||
this._suggestedName = name;
|
||||
}
|
||||
|
||||
private async _connectMount(ev) {
|
||||
const progressButton = ev.target as HaProgressButton;
|
||||
this._error = undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/ha-switch";
|
||||
@@ -43,13 +43,6 @@ class HaDebugConnectionRow extends LitElement {
|
||||
setDebugConnection(debugConnection);
|
||||
storeState(this.hass);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
|
||||
@@ -41,13 +41,6 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
this._disabled = (ev.target as HaSwitch).checked;
|
||||
setViewTransitionDisabled(this._disabled);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
@@ -42,13 +42,6 @@ class HaDebugSafeAreaToolRow extends LitElement {
|
||||
private _checkedChanged(ev: Event) {
|
||||
this._enabled = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -901,14 +901,16 @@ class HaPanelHistory extends LitElement {
|
||||
entities,
|
||||
},
|
||||
];
|
||||
addEntitiesToLovelaceView(this, this.hass, {
|
||||
cardConfig: cards,
|
||||
sectionConfig: {
|
||||
addEntitiesToLovelaceView(
|
||||
this,
|
||||
this.hass,
|
||||
cards,
|
||||
{
|
||||
title: this.hass.localize("panel.history"),
|
||||
cards,
|
||||
},
|
||||
entities,
|
||||
});
|
||||
entities
|
||||
);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import {
|
||||
consumeEntityState,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
ALERT_DOMAINS,
|
||||
computeAlertColor,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
isAlertActive,
|
||||
} from "../../../common/entity/alert_active";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -120,14 +121,12 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
|
||||
const active = isAlertActive(stateObj);
|
||||
|
||||
const color = computeAlertColor(
|
||||
stateObj,
|
||||
active
|
||||
? (this._config.alert_color ?? "state")
|
||||
: (this._config.clear_color ?? "none")
|
||||
);
|
||||
const color = active
|
||||
? (this._config.alert_color ?? DEFAULT_ALERT_COLOR)
|
||||
: this._config.clear_color;
|
||||
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const name = this._formatters.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
@@ -144,9 +143,10 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
.type=${hasTileCardAction(this._config) ? "button" : "badge"}
|
||||
.label=${showState && showName ? name : undefined}
|
||||
.iconOnly=${!content}
|
||||
class=${classMap({ alert: true, pulse })}
|
||||
class=${classMap({ alert: true, pulse, "no-color": !hasColor })}
|
||||
style=${styleMap({
|
||||
"--ha-alert-color": color,
|
||||
"--ha-alert-color":
|
||||
color && hasColor ? computeCssColor(color) : undefined,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
@@ -205,7 +205,10 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
);
|
||||
background: var(--ha-alert-badge-background);
|
||||
--ha-card-background: transparent;
|
||||
--badge-color: var(--ha-alert-color, var(--secondary-text-color));
|
||||
--badge-color: var(--ha-alert-color);
|
||||
}
|
||||
ha-badge.alert.no-color {
|
||||
--badge-color: var(--secondary-text-color);
|
||||
}
|
||||
ha-badge.alert::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -5,11 +5,12 @@ import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
ALERT_DOMAINS,
|
||||
computeAlertColor,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
isAlertActive,
|
||||
} from "../../../common/entity/alert_active";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -119,14 +120,12 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const active = isAlertActive(stateObj);
|
||||
|
||||
const color = computeAlertColor(
|
||||
stateObj,
|
||||
active
|
||||
? (this._config.alert_color ?? "state")
|
||||
: (this._config.clear_color ?? "none")
|
||||
);
|
||||
const color = active
|
||||
? (this._config.alert_color ?? DEFAULT_ALERT_COLOR)
|
||||
: this._config.clear_color;
|
||||
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const areaName = this._formatters.formatEntityName(stateObj, {
|
||||
type: "area",
|
||||
@@ -134,9 +133,10 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({ pulse })}
|
||||
class=${classMap({ pulse, "no-color": !hasColor })}
|
||||
style=${styleMap({
|
||||
"--ha-alert-color": color,
|
||||
"--ha-alert-color":
|
||||
color && hasColor ? computeCssColor(color) : undefined,
|
||||
"--ha-alert-static-opacity": pulse
|
||||
? undefined
|
||||
: "var(--ha-alert-pulse-opacity)",
|
||||
@@ -211,7 +211,12 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
--tile-color: var(--ha-alert-color, var(--secondary-text-color));
|
||||
}
|
||||
ha-card:not(.no-color) {
|
||||
--tile-color: var(--ha-alert-color);
|
||||
}
|
||||
ha-card.no-color {
|
||||
--tile-color: var(--secondary-text-color);
|
||||
}
|
||||
ha-card::before {
|
||||
position: absolute;
|
||||
|
||||
@@ -310,11 +310,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#states:first-child > div:first-child > hui-section-row::part(divider) {
|
||||
#states > div:first-child > hui-section-row::part(divider) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#states:first-child > div:first-child > hui-section-row::part(label) {
|
||||
#states > div:first-child > hui-section-row::part(label) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -38,11 +38,7 @@ import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { GaugeCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_MIN = 0;
|
||||
@@ -84,8 +80,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
return { type: "gauge", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: GaugeCardConfig;
|
||||
|
||||
@state()
|
||||
@@ -193,17 +187,10 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const name = this._formatters.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
// In a sections view with a set number of rows the card height is fixed,
|
||||
// so the gauge also has to fit in the height, not only in the width.
|
||||
const fixedHeight =
|
||||
this.layout === "grid" &&
|
||||
typeof this._config.grid_options?.rows === "number";
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({
|
||||
action: hasAnyAction(this._config),
|
||||
"fixed-height": fixedHeight,
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
@@ -216,34 +203,24 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<div class="gauge-container">
|
||||
<ha-gauge
|
||||
.min=${this._config.min!}
|
||||
.max=${this._config.max!}
|
||||
.value=${value}
|
||||
.valueText=${valueToDisplay}
|
||||
.locale=${this._i18n.locale}
|
||||
.label=${customUnit ?? ""}
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(Number(value)),
|
||||
})}
|
||||
.needle=${this._config!.needle}
|
||||
.levels=${this._config!.needle ? this._severityLevels() : undefined}
|
||||
></ha-gauge>
|
||||
</div>
|
||||
<ha-gauge
|
||||
.min=${this._config.min!}
|
||||
.max=${this._config.max!}
|
||||
.value=${value}
|
||||
.valueText=${valueToDisplay}
|
||||
.locale=${this._i18n.locale}
|
||||
.label=${customUnit ?? ""}
|
||||
style=${styleMap({
|
||||
"--gauge-color": this._computeSeverity(Number(value)),
|
||||
})}
|
||||
.needle=${this._config!.needle}
|
||||
.levels=${this._config!.needle ? this._severityLevels() : undefined}
|
||||
></ha-gauge>
|
||||
<p class="title" .title=${name}>${name}</p>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
// With one row there is almost no room left for the gauge after the
|
||||
// padding and the name, so the card needs at least 2 rows.
|
||||
return {
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this._themes) {
|
||||
@@ -389,25 +366,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.gauge-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* With a fixed card height the container takes the space left after
|
||||
the name, and the gauge is sized to fit inside it. */
|
||||
ha-card.fixed-height .gauge-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
ha-card.fixed-height ha-gauge {
|
||||
width: min(250px, 100cqw, 100cqh * 100 / 55);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LovelacePanelConfig } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type {
|
||||
LovelaceConfig,
|
||||
LovelaceRawConfig,
|
||||
@@ -12,48 +10,20 @@ import {
|
||||
isStrategyDashboard,
|
||||
saveConfig,
|
||||
} from "../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
||||
import { fetchDashboards } from "../../../data/lovelace/dashboard";
|
||||
import { getPanelTitleFromUrlPath, LOVELACE_PANEL } from "../../../data/panel";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../views/const";
|
||||
import { getViewType } from "../views/get-view-type";
|
||||
import type { SuggestCardDialogParams } from "./card-editor/show-suggest-card-dialog";
|
||||
import { showSuggestCardDialog } from "./card-editor/show-suggest-card-dialog";
|
||||
import { showEditSectionDialog } from "./section-editor/show-edit-section-dialog";
|
||||
import { showSelectViewDialog } from "./select-view/show-select-view-dialog";
|
||||
|
||||
export type AddToViewCardDialogParams = Pick<
|
||||
SuggestCardDialogParams,
|
||||
"lovelaceConfig" | "saveConfig" | "path" | "yaml"
|
||||
>;
|
||||
|
||||
export interface AddEntitiesToViewOptions {
|
||||
cardConfig?: LovelaceCardConfig[];
|
||||
sectionConfig?: LovelaceSectionConfig;
|
||||
entities?: string[];
|
||||
sectionStrategy?: LovelaceStrategyConfig;
|
||||
showCardDialog?: (params: AddToViewCardDialogParams) => void;
|
||||
}
|
||||
|
||||
export const addEntitiesToLovelaceView = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
options: AddEntitiesToViewOptions
|
||||
cardConfig: LovelaceCardConfig[],
|
||||
sectionConfig?: LovelaceSectionConfig,
|
||||
entities?: string[]
|
||||
) => {
|
||||
const { sectionStrategy } = options;
|
||||
const showCardDialog =
|
||||
options.showCardDialog ??
|
||||
((params: AddToViewCardDialogParams) =>
|
||||
showSuggestCardDialog(element, {
|
||||
...params,
|
||||
cardConfig: options.cardConfig ?? [],
|
||||
sectionConfig: options.sectionConfig,
|
||||
entities: options.entities,
|
||||
}));
|
||||
await hass.loadFragmentTranslation("lovelace");
|
||||
hass.loadFragmentTranslation("lovelace");
|
||||
const dashboards = await fetchDashboards(hass);
|
||||
|
||||
const storageDashs = dashboards.filter(
|
||||
@@ -66,7 +36,11 @@ export const addEntitiesToLovelaceView = async (
|
||||
|
||||
if (mainLovelaceMode !== "storage" && !storageDashs.length) {
|
||||
// no storage dashboards, just show the YAML config
|
||||
showCardDialog({ yaml: true });
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
entities,
|
||||
yaml: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +94,12 @@ export const addEntitiesToLovelaceView = async (
|
||||
if (!hasViewList(lovelaceConfig)) {
|
||||
if (dashboards.length > storageDashs.length) {
|
||||
// all storage dashboards are generated, but we have YAML dashboards just show the YAML config
|
||||
showCardDialog({ yaml: true });
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
entities,
|
||||
yaml: true,
|
||||
});
|
||||
} else {
|
||||
// all storage dashboards are generated
|
||||
showAlertDialog(element, {
|
||||
@@ -137,59 +116,21 @@ export const addEntitiesToLovelaceView = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const showAddDialog = (
|
||||
viewUrlPath: string | null,
|
||||
config: LovelaceConfig,
|
||||
viewIndex: number
|
||||
) => {
|
||||
const save = async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
try {
|
||||
await saveConfig(hass!, viewUrlPath, newConfig);
|
||||
} catch (_err: any) {
|
||||
alert(hass.localize("ui.panel.lovelace.add_entities.saving_failed"));
|
||||
return;
|
||||
}
|
||||
const dashboardPath = viewUrlPath ?? LOVELACE_PANEL;
|
||||
const viewPath = newConfig.views[viewIndex].path || viewIndex;
|
||||
const dashboardTitle = getPanelTitleFromUrlPath(hass, dashboardPath);
|
||||
showToast(element, {
|
||||
message: dashboardTitle
|
||||
? {
|
||||
translationKey: "ui.panel.lovelace.add_entities.added",
|
||||
args: { dashboard: dashboardTitle },
|
||||
}
|
||||
: { translationKey: "ui.panel.lovelace.add_entities.added_unnamed" },
|
||||
action: {
|
||||
text: { translationKey: "ui.panel.lovelace.add_entities.open" },
|
||||
action: () => navigate(`/${dashboardPath}/${viewPath}`),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
if (
|
||||
sectionStrategy &&
|
||||
!isStrategyView(view) &&
|
||||
getViewType(view) === SECTIONS_VIEW_LAYOUT
|
||||
) {
|
||||
showEditSectionDialog(element, {
|
||||
lovelaceConfig: config,
|
||||
saveConfig: save,
|
||||
path: ["views", viewIndex, "sections", view.sections?.length ?? 0],
|
||||
newSection: { strategy: sectionStrategy },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showCardDialog({
|
||||
lovelaceConfig: config,
|
||||
saveConfig: save,
|
||||
path: ["views", viewIndex],
|
||||
});
|
||||
};
|
||||
|
||||
if (!storageDashs.length && lovelaceConfig.views.length === 1) {
|
||||
showAddDialog(null, lovelaceConfig, 0);
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
lovelaceConfig,
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
try {
|
||||
await saveConfig(hass!, null, newConfig);
|
||||
} catch (_err: any) {
|
||||
alert(hass.localize("ui.panel.lovelace.add_entities.saving_failed"));
|
||||
}
|
||||
},
|
||||
path: ["views", 0],
|
||||
entities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,6 +140,23 @@ export const addEntitiesToLovelaceView = async (
|
||||
allowDashboardChange: true,
|
||||
actionLabel: hass.localize("ui.common.next"),
|
||||
dashboards,
|
||||
viewSelectedCallback: showAddDialog,
|
||||
viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => {
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
lovelaceConfig: selectedDashConfig,
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
try {
|
||||
await saveConfig(hass!, newUrlPath, newConfig);
|
||||
} catch {
|
||||
alert(
|
||||
hass.localize("ui.panel.lovelace.add_entities.saving_failed")
|
||||
);
|
||||
}
|
||||
},
|
||||
path: ["views", viewIndex],
|
||||
entities,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,299 +0,0 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-filter-chip";
|
||||
import type { HaFilterChip } from "../../../../components/chips/ha-filter-chip";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { computeDeviceEntityCategory } from "../../../../data/device/device_entity_category";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../cards/hui-card";
|
||||
import type { DeviceSectionCategory } from "../../strategies/device/device-section-strategy";
|
||||
import {
|
||||
DEFAULT_DEVICE_SECTION_CATEGORIES,
|
||||
DEVICE_SECTION_CATEGORIES,
|
||||
DeviceSectionStrategy,
|
||||
} from "../../strategies/device/device-section-strategy";
|
||||
import { addCards } from "../config-util";
|
||||
import type { AddDeviceCardDialogParams } from "./show-add-device-card-dialog";
|
||||
|
||||
@customElement("hui-dialog-add-device-card")
|
||||
export class HuiDialogAddDeviceCard
|
||||
extends LitElement
|
||||
implements HassDialog<AddDeviceCardDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: AddDeviceCardDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _categories: DeviceSectionCategory[] =
|
||||
DEFAULT_DEVICE_SECTION_CATEGORIES;
|
||||
|
||||
@state() private _cardConfig?: LovelaceCardConfig[];
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
public showDialog(params: AddDeviceCardDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
const available = this._availableCategories(
|
||||
params.deviceId,
|
||||
this.hass.entities
|
||||
);
|
||||
const categories = DEFAULT_DEVICE_SECTION_CATEGORIES.filter((category) =>
|
||||
available.includes(category)
|
||||
);
|
||||
this._updateCategories(categories.length ? categories : available);
|
||||
}
|
||||
|
||||
private _availableCategories = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: HomeAssistant["entities"]
|
||||
): DeviceSectionCategory[] => {
|
||||
const categories = new Set(
|
||||
Object.values(entities)
|
||||
.filter((entry) => entry.device_id === deviceId && !entry.hidden)
|
||||
.map(computeDeviceEntityCategory)
|
||||
);
|
||||
return DEVICE_SECTION_CATEGORIES.filter((category) =>
|
||||
categories.has(category)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._cardConfig = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const device = this.hass.devices[this._params.deviceId];
|
||||
const deviceName = device
|
||||
? computeDeviceNameDisplay(device, this.hass.localize)
|
||||
: this._params.deviceId;
|
||||
const availableCategories = this._availableCategories(
|
||||
this._params.deviceId,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.add_device_card.header",
|
||||
{ device: deviceName }
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${
|
||||
availableCategories.length > 1
|
||||
? html`
|
||||
<p class="label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy.device.categories"
|
||||
)}
|
||||
</p>
|
||||
<ha-chip-set>
|
||||
${availableCategories.map(
|
||||
(category) => html`
|
||||
<ha-filter-chip
|
||||
.value=${category}
|
||||
.selected=${this._categories.includes(category)}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.strategy.device.categories.${category}`
|
||||
)}
|
||||
@click=${this._categoryToggled}
|
||||
></ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="preview">
|
||||
${this._cardConfig?.map(
|
||||
(cardConfig) => html`
|
||||
<hui-card
|
||||
.hass=${this.hass}
|
||||
.config=${cardConfig}
|
||||
preview
|
||||
></hui-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
this._params.yaml && this._cardConfig
|
||||
? html`
|
||||
<div class="editor">
|
||||
<ha-yaml-editor
|
||||
.defaultValue=${this._cardConfig}
|
||||
in-dialog
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._params.yaml ? "ui.common.close" : "ui.common.cancel"
|
||||
)}
|
||||
</ha-button>
|
||||
${
|
||||
this._params.yaml
|
||||
? nothing
|
||||
: html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._saving || !this._cardConfig?.length}
|
||||
.loading=${this._saving}
|
||||
@click=${this._save}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.add_device_card.add"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _categoryToggled(ev: MouseEvent): void {
|
||||
const chip = ev.currentTarget as HaFilterChip;
|
||||
const category = chip.value as DeviceSectionCategory;
|
||||
this._updateCategories(
|
||||
this._availableCategories(
|
||||
this._params!.deviceId,
|
||||
this.hass.entities
|
||||
).filter((item) =>
|
||||
item === category ? chip.selected : this._categories.includes(item)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateCategories(
|
||||
categories: DeviceSectionCategory[]
|
||||
): Promise<void> {
|
||||
this._categories = categories;
|
||||
const deviceId = this._params!.deviceId;
|
||||
const section = await DeviceSectionStrategy.generate(
|
||||
{
|
||||
type: "device",
|
||||
device_id: deviceId,
|
||||
categories,
|
||||
display: "list",
|
||||
heading: "none",
|
||||
group_by_category: true,
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
if (
|
||||
this._categories !== categories ||
|
||||
this._params?.deviceId !== deviceId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const device = this.hass.devices[deviceId];
|
||||
const title = device
|
||||
? computeDeviceNameDisplay(device, this.hass.localize)
|
||||
: undefined;
|
||||
this._cardConfig = (section.cards ?? []).map((card) => ({
|
||||
...card,
|
||||
title,
|
||||
}));
|
||||
this._yamlEditor?.setValue(this._cardConfig);
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (
|
||||
!this._params?.lovelaceConfig ||
|
||||
!this._params.path ||
|
||||
!this._params.saveConfig ||
|
||||
!this._cardConfig?.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
await this._params.saveConfig(
|
||||
addCards(this._params.lovelaceConfig, this._params.path, this._cardConfig)
|
||||
);
|
||||
this._saving = false;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
.label {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-4);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
hui-card {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.editor {
|
||||
padding-top: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-add-device-card": HuiDialogAddDeviceCard;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/vie
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
@@ -281,6 +282,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
);
|
||||
await this._params!.saveConfig(newConfig);
|
||||
this._saving = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { AddToViewCardDialogParams } from "../add-entities-to-view";
|
||||
|
||||
export interface AddDeviceCardDialogParams extends AddToViewCardDialogParams {
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
const importAddDeviceCardDialog = () => import("./hui-dialog-add-device-card");
|
||||
|
||||
export const showAddDeviceCardDialog = (
|
||||
element: HTMLElement,
|
||||
addDeviceCardDialogParams: AddDeviceCardDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-add-device-card",
|
||||
dialogImport: importAddDeviceCardDialog,
|
||||
dialogParams: addDeviceCardDialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiAlertOutline, mdiTextShort } from "@mdi/js";
|
||||
import { DEFAULT_ALERT_COLOR } from "../../../../common/entity/alert_active";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { contentLayoutSchema, displayedElementsSchema } from "./content-schema";
|
||||
@@ -17,11 +18,7 @@ export const alertSchema = () =>
|
||||
{
|
||||
name: "clear_color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_none: true,
|
||||
include_state: true,
|
||||
default_color: "none",
|
||||
},
|
||||
ui_color: { include_none: true, default_color: "none" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -29,8 +26,7 @@ export const alertSchema = () =>
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_none: true,
|
||||
include_state: true,
|
||||
default_color: "state",
|
||||
default_color: DEFAULT_ALERT_COLOR,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface EditSectionDialogParams {
|
||||
lovelace?: Lovelace;
|
||||
lovelace: Lovelace;
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelacePath;
|
||||
|
||||
@@ -7,7 +7,6 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import { computeUnusedEntities } from "../../common/compute-unused-entities";
|
||||
import {
|
||||
computeCards,
|
||||
@@ -112,11 +111,6 @@ export class HuiUnusedEntities extends LitElement {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _saveConfig = async (config: LovelaceConfig): Promise<void> => {
|
||||
await this.lovelace.saveConfig(config);
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
};
|
||||
|
||||
private _addToLovelaceView(): void {
|
||||
const cardConfig = computeCards(this.hass, this._selectedEntities, {});
|
||||
const sectionConfig = computeSection(this._selectedEntities, {});
|
||||
@@ -124,7 +118,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
if (this.lovelace.config.views.length === 1) {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this._saveConfig,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ["views", 0],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
@@ -138,7 +132,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
viewSelectedCallback: (_urlPath, _selectedDashConfig, viewIndex) => {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this._saveConfig,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ["views", viewIndex],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
|
||||
@@ -319,7 +319,7 @@ export class LovelacePanel extends LitElement {
|
||||
// Refreshing a YAML config can trigger an update event. We will ignore
|
||||
// all update events while fetching the config and for 2 seconds after the config is back.
|
||||
// We ignore because we already have the latest config.
|
||||
if (confMode === "yaml") {
|
||||
if (this.lovelace && this.lovelace.mode === "yaml") {
|
||||
this._ignoreNextUpdateEvent = true;
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ export class LovelacePanel extends LitElement {
|
||||
} finally {
|
||||
this._loading = false;
|
||||
// Ignore updates for another 2 seconds.
|
||||
if (confMode === "yaml") {
|
||||
if (this.lovelace && this.lovelace.mode === "yaml") {
|
||||
setTimeout(() => {
|
||||
this._ignoreNextUpdateEvent = false;
|
||||
}, 2000);
|
||||
|
||||
@@ -92,22 +92,16 @@ export const filterUnavailableBatteryEntities = (
|
||||
return states[entityId]?.state === "unavailable";
|
||||
});
|
||||
|
||||
const computeMaintenanceTileCard = (
|
||||
states: HomeAssistant["states"],
|
||||
const computeBatteryTileCard = (
|
||||
entities: HomeAssistant["entities"],
|
||||
entityId: string
|
||||
): TileCardConfig => {
|
||||
const deviceId = entities[entityId]?.device_id;
|
||||
if (!deviceId) {
|
||||
return { type: "tile", entity: entityId, name: { type: "entity" } };
|
||||
}
|
||||
const isProblem = states[entityId]?.attributes?.device_class === "problem";
|
||||
const entity = entities[entityId];
|
||||
const deviceId = entity?.device_id;
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name: isProblem
|
||||
? [{ type: "device" }, { type: "entity" }]
|
||||
: { type: "device" },
|
||||
name: { type: deviceId ? "device" : "entity" },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -136,9 +130,7 @@ const processAreasForBattery = (
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of areaBatteryEntities) {
|
||||
areaCards.push(
|
||||
computeMaintenanceTileCard(hass.states, hass.entities, entityId)
|
||||
);
|
||||
areaCards.push(computeBatteryTileCard(hass.entities, entityId));
|
||||
}
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
@@ -178,9 +170,7 @@ const processUnassignedEntities = (
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of unassignedEntities) {
|
||||
cards.push(
|
||||
computeMaintenanceTileCard(hass.states, hass.entities, entityId)
|
||||
);
|
||||
cards.push(computeBatteryTileCard(hass.entities, entityId));
|
||||
}
|
||||
|
||||
return cards;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Install the Intl.PluralRules polyfill together with its locale data.
|
||||
// FormatJS replaces Intl.PluralRules as soon as polyfill-force runs, and a
|
||||
// data-less polyfill throws from select(). Keeping both imports in one module
|
||||
// closes that gap for every caller, including the should-polyfill probes
|
||||
// that other entry bundles run.
|
||||
import "@formatjs/intl-pluralrules/polyfill-force";
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
import "@formatjs/intl-pluralrules/locale-data/en";
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
polyfillTimeZoneData,
|
||||
} from "./locale-data-polyfill";
|
||||
|
||||
let polyfilled = false;
|
||||
|
||||
const _polyfillTimeZoneData = polyfillTimeZoneData;
|
||||
|
||||
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
|
||||
@@ -45,6 +47,10 @@ const setDefaultTimeZone = () => {
|
||||
};
|
||||
|
||||
const polyfillIntl = async () => {
|
||||
if (polyfilled) {
|
||||
return;
|
||||
}
|
||||
polyfilled = true;
|
||||
const locale = getLocalLanguage();
|
||||
const polyfills: Promise<unknown>[] = [];
|
||||
if (shouldPolyfillGetCanonicalLocales()) {
|
||||
@@ -73,10 +79,14 @@ const polyfillIntl = async () => {
|
||||
polyfills.push(import("@formatjs/intl-numberformat/polyfill-force"));
|
||||
}
|
||||
if (shouldPolyfillPluralRules(locale)) {
|
||||
// The polyfill and its English data are one chunk, so Intl.PluralRules
|
||||
// is never installed without data. Its should-polyfill probe calls
|
||||
// select(), which throws on a data-less polyfill.
|
||||
polyfills.push(import("./intl-pluralrules"));
|
||||
polyfills.push(
|
||||
import("@formatjs/intl-pluralrules/polyfill-force").then(
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
() => import("@formatjs/intl-pluralrules/locale-data/en")
|
||||
)
|
||||
);
|
||||
}
|
||||
if (shouldPolyfillRelativeTimeFormat(locale)) {
|
||||
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill-force"));
|
||||
@@ -90,9 +100,4 @@ const polyfillIntl = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Each legacy entry bundle (core, app, authorize, onboarding) carries its own
|
||||
// copy of this module, and the bundles load in parallel. A module-level flag
|
||||
// only stops re-runs within one bundle, so a second bundle could probe
|
||||
// Intl.* while the first was still loading locale data. Share one promise
|
||||
// per page instead, so every bundle waits for the same run.
|
||||
await (globalThis.__haIntlPolyfill ??= polyfillIntl());
|
||||
await polyfillIntl();
|
||||
@@ -1,5 +0,0 @@
|
||||
// Imported right after the scoped custom element registry polyfill. The index
|
||||
// page starts the extra modules from Core only after this has run, so their
|
||||
// elements are defined in the registry that the app uses.
|
||||
window.__haScopedRegistryReady = true;
|
||||
window.__haLoadExtraModules?.();
|
||||
+14
-14
@@ -17,7 +17,6 @@
|
||||
"climate": "Climate",
|
||||
"home": "Overview",
|
||||
"maintenance": "Maintenance",
|
||||
"marketplace": "Marketplace",
|
||||
"notfound": "Page not found"
|
||||
},
|
||||
"state": {
|
||||
@@ -858,6 +857,7 @@
|
||||
},
|
||||
"mode_composed": "Composed",
|
||||
"mode_custom": "Custom",
|
||||
"add": "Add",
|
||||
"search": "Search name",
|
||||
"custom_name": "Custom name",
|
||||
"no_more_parts": "Nothing left to add",
|
||||
@@ -944,6 +944,7 @@
|
||||
"collapse": "Collapse"
|
||||
},
|
||||
"entity-type-picker": {
|
||||
"add": "Add type",
|
||||
"no_types": "No types available"
|
||||
},
|
||||
"subpage-data-table": {
|
||||
@@ -1910,12 +1911,10 @@
|
||||
"automation_condition": "Create as a new condition",
|
||||
"automation_action": "Create as a new action",
|
||||
"script_action": "Create as a new action",
|
||||
"scene": "Create as a new scene",
|
||||
"dashboard": "Add to a dashboard"
|
||||
"scene": "Create as a new scene"
|
||||
},
|
||||
"automations_heading": "Automations",
|
||||
"scripts_heading": "Scripts",
|
||||
"dashboards_heading": "Dashboards",
|
||||
"app_actions": "App actions",
|
||||
"no_actions": "No actions available",
|
||||
"action_failed": "Failed to perform the action {error}"
|
||||
@@ -3207,8 +3206,8 @@
|
||||
"disable": "[%key:ui::common::disable%]",
|
||||
"disable_confirm_title": "Disable the Model Context Protocol server?",
|
||||
"disable_confirm_text": "AI agents connected to the server will lose access to Home Assistant.",
|
||||
"your_api_header": "Main URL",
|
||||
"apis_header": "Individual URLs",
|
||||
"your_api_header": "Your MCP API",
|
||||
"apis_header": "Individual MCP APIs",
|
||||
"copy_url": "Copy URL",
|
||||
"configure": "Configure",
|
||||
"error_load": "Failed to load the Model Context Protocol configuration",
|
||||
@@ -5083,6 +5082,13 @@
|
||||
"info": "INFO",
|
||||
"debug": "DEBUG"
|
||||
},
|
||||
"level_count": {
|
||||
"critical": "{count} critical {count, plural,\n one {entry}\n other {entries}\n}",
|
||||
"error": "{count} {count, plural,\n one {error}\n other {errors}\n}",
|
||||
"warning": "{count} {count, plural,\n one {warning}\n other {warnings}\n}",
|
||||
"info": "{count} info {count, plural,\n one {entry}\n other {entries}\n}",
|
||||
"debug": "{count} debug {count, plural,\n one {entry}\n other {entries}\n}"
|
||||
},
|
||||
"custom_integration": "custom integration",
|
||||
"error_from_custom_integration": "This log entry is from a custom integration.",
|
||||
"show_full_logs": "Show raw logs",
|
||||
@@ -7475,6 +7481,7 @@
|
||||
"notify": "Notifiers",
|
||||
"assist": "[%key:ui::panel::lovelace::menu::assist%]",
|
||||
"config": "Configuration",
|
||||
"add_entities_lovelace": "Add to dashboard",
|
||||
"none": "This device has no entities",
|
||||
"show_less": "Show less",
|
||||
"disabled_entities": "+{count} disabled {count, plural,\n one {entity}\n other {entities}\n}",
|
||||
@@ -10224,10 +10231,7 @@
|
||||
"add_entities": {
|
||||
"yaml_unsupported": "You cannot use this function when using your dashboard in YAML mode.",
|
||||
"generated_unsupported": "You can only use this function when you have taken control of your dashboard.",
|
||||
"saving_failed": "Saving dashboard configuration failed.",
|
||||
"added": "Added to {dashboard}",
|
||||
"added_unnamed": "Added to the dashboard",
|
||||
"open": "Open"
|
||||
"saving_failed": "Saving dashboard configuration failed."
|
||||
},
|
||||
"views": {
|
||||
"unnamed_view": "Unnamed view",
|
||||
@@ -10638,10 +10642,6 @@
|
||||
"theme_helper": "Apply a specific theme to this section, overriding the view theme"
|
||||
}
|
||||
},
|
||||
"add_device_card": {
|
||||
"header": "Add {device} to dashboard",
|
||||
"add": "[%key:ui::panel::lovelace::editor::suggest_card::add%]"
|
||||
},
|
||||
"suggest_card": {
|
||||
"header": "We created a suggestion for you",
|
||||
"create_own": "Pick different card",
|
||||
|
||||
@@ -41,11 +41,6 @@ declare global {
|
||||
var __HASS_URL__: string;
|
||||
// Read by intl-polyfill before a polyfill replaces Intl.DateTimeFormat
|
||||
var __haNativeTimeZone: string | undefined;
|
||||
// Shared by every legacy entry bundle so the Intl polyfills load once
|
||||
var __haIntlPolyfill: Promise<void> | undefined;
|
||||
// Handshake between the app entry and the extra modules in index.html
|
||||
var __haScopedRegistryReady: boolean | undefined;
|
||||
var __haLoadExtraModules: (() => void) | undefined;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
interface Window {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dropInvalidConditions } from "../../../src/common/condition/sanitize";
|
||||
import type { VisibilityCondition } from "../../../src/panels/lovelace/common/validate-condition";
|
||||
|
||||
const conds = (c: unknown[]): VisibilityCondition[] =>
|
||||
c as VisibilityCondition[];
|
||||
|
||||
const state = { condition: "state", entity: "light.kitchen", state: "on" };
|
||||
const screen = { condition: "screen", media_query: "(max-width: 600px)" };
|
||||
|
||||
describe("dropInvalidConditions", () => {
|
||||
it("returns the same array when every entry is valid", () => {
|
||||
const conditions = conds([
|
||||
state,
|
||||
{ condition: "or", conditions: [screen] },
|
||||
{ condition: "not", conditions: screen },
|
||||
]);
|
||||
expect(dropInvalidConditions(conditions)).toBe(conditions);
|
||||
});
|
||||
|
||||
it("drops invalid entries at any depth without changing the input", () => {
|
||||
const nested = { condition: "and", conditions: [screen, undefined, 5] };
|
||||
const conditions = conds([
|
||||
null,
|
||||
"screen",
|
||||
state,
|
||||
{ condition: "or", conditions: [null, nested] },
|
||||
{ condition: "not", conditions: "screen" },
|
||||
{ condition: "and", conditions: "" },
|
||||
]);
|
||||
|
||||
expect(dropInvalidConditions(conditions)).toEqual([
|
||||
state,
|
||||
{
|
||||
condition: "or",
|
||||
conditions: [{ condition: "and", conditions: [screen] }],
|
||||
},
|
||||
{ condition: "not", conditions: [] },
|
||||
{ condition: "and", conditions: [] },
|
||||
]);
|
||||
expect(nested.conditions).toEqual([screen, undefined, 5]);
|
||||
});
|
||||
});
|
||||
@@ -1,184 +0,0 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import "../../src/components/ha-areas-floors-display-editor";
|
||||
import type {
|
||||
AreasFloorsDisplayValue,
|
||||
HaAreasFloorsDisplayEditor,
|
||||
} from "../../src/components/ha-areas-floors-display-editor";
|
||||
import type { HaItemDisplayEditor } from "../../src/components/ha-items-display-editor";
|
||||
import {
|
||||
areasContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../../src/data/context";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../src/types";
|
||||
|
||||
beforeAll(() => {
|
||||
// jsdom's ElementInternals lacks the validity API used by the webawesome
|
||||
// buttons that render inside the items editor.
|
||||
const internalsProto = window.ElementInternals.prototype as any;
|
||||
internalsProto.setValidity = vi.fn();
|
||||
internalsProto.setFormValue = vi.fn();
|
||||
Object.defineProperty(internalsProto, "validity", {
|
||||
get: () => ({ valid: true }),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
class MockResizeObserver {
|
||||
observe = vi.fn();
|
||||
|
||||
unobserve = vi.fn();
|
||||
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
const FLOORS = {
|
||||
floor_a: { floor_id: "floor_a", name: "A", level: 0 },
|
||||
floor_b: { floor_id: "floor_b", name: "B", level: 1 },
|
||||
} as unknown as HomeAssistant["floors"];
|
||||
|
||||
const area = (areaId: string, floorId: string | null) => ({
|
||||
area_id: areaId,
|
||||
name: areaId,
|
||||
floor_id: floorId,
|
||||
icon: null,
|
||||
});
|
||||
|
||||
const AREAS = Object.fromEntries(
|
||||
[
|
||||
area("a1", "floor_a"),
|
||||
area("a2", "floor_a"),
|
||||
area("a3", "floor_a"),
|
||||
area("b1", "floor_b"),
|
||||
area("b2", "floor_b"),
|
||||
area("b3", "floor_b"),
|
||||
area("u1", null),
|
||||
area("u2", null),
|
||||
].map((entry) => [entry.area_id, entry])
|
||||
) as unknown as HomeAssistant["areas"];
|
||||
|
||||
describe("ha-areas-floors-display-editor", () => {
|
||||
let host: HTMLDivElement | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
host?.remove();
|
||||
host = undefined;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const mount = async (
|
||||
value: AreasFloorsDisplayValue
|
||||
): Promise<HaAreasFloorsDisplayEditor> => {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
new ContextProvider(host, {
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
localize: (key: string) => key,
|
||||
} as unknown as HomeAssistantInternationalization,
|
||||
});
|
||||
new ContextProvider(host, { context: areasContext, initialValue: AREAS });
|
||||
new ContextProvider(host, { context: floorsContext, initialValue: FLOORS });
|
||||
const el = document.createElement(
|
||||
"ha-areas-floors-display-editor"
|
||||
) as HaAreasFloorsDisplayEditor;
|
||||
el.value = value;
|
||||
host.appendChild(el);
|
||||
await el.updateComplete;
|
||||
return el;
|
||||
};
|
||||
|
||||
// Drags the first visible area of a floor below the second one and returns
|
||||
// the configuration the editor emits.
|
||||
const reorderFirstTwo = async (
|
||||
el: HaAreasFloorsDisplayEditor,
|
||||
floorId: string
|
||||
): Promise<AreasFloorsDisplayValue> => {
|
||||
const editor = Array.from(
|
||||
el.shadowRoot!.querySelectorAll("ha-items-display-editor")
|
||||
).find(
|
||||
(child) =>
|
||||
(child as HaItemDisplayEditor & { floorId: string }).floorId === floorId
|
||||
)!;
|
||||
await editor.updateComplete;
|
||||
const emitted: AreasFloorsDisplayValue[] = [];
|
||||
el.addEventListener(
|
||||
"value-changed",
|
||||
(ev) => emitted.push((ev as CustomEvent).detail.value),
|
||||
{ once: true }
|
||||
);
|
||||
editor.shadowRoot!.querySelector("ha-sortable")!.dispatchEvent(
|
||||
new CustomEvent("item-moved", {
|
||||
detail: { oldIndex: 0, newIndex: 1 },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
expect(emitted).toHaveLength(1);
|
||||
return emitted[0];
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "hidden area on another floor",
|
||||
floorId: "floor_a",
|
||||
value: {
|
||||
areas_display: { hidden: ["b1"], order: ["b3", "b2"] },
|
||||
},
|
||||
expected: {
|
||||
hidden: ["b1"],
|
||||
order: ["a2", "a1", "a3", "b3", "b2"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hidden areas on the reordered and another floor",
|
||||
floorId: "floor_a",
|
||||
value: { areas_display: { hidden: ["a3", "b1"] } },
|
||||
expected: { hidden: ["a3", "b1"], order: ["a2", "a1"] },
|
||||
},
|
||||
{
|
||||
name: "reordering areas without a floor",
|
||||
floorId: UNASSIGNED_FLOOR,
|
||||
value: { areas_display: { hidden: ["b1"] } },
|
||||
expected: { hidden: ["b1"], order: ["u2", "u1"] },
|
||||
},
|
||||
{
|
||||
name: "no hidden areas",
|
||||
floorId: "floor_b",
|
||||
value: {},
|
||||
expected: { order: ["b2", "b1", "b3"] },
|
||||
},
|
||||
])(
|
||||
"keeps hidden areas unchanged when reordering: $name",
|
||||
async ({ floorId, value, expected }) => {
|
||||
const el = await mount(value);
|
||||
|
||||
const first = await reorderFirstTwo(el, floorId);
|
||||
expect(first.areas_display).toEqual(expected);
|
||||
|
||||
// Saving and reordering again must not grow the hidden list
|
||||
el.value = first;
|
||||
await el.updateComplete;
|
||||
const second = await reorderFirstTwo(el, floorId);
|
||||
expect(second.areas_display?.hidden).toEqual(expected.hidden);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import template from "lodash.template";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import patterns from "../../src/util/stale-build-patterns.json";
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../.."
|
||||
);
|
||||
|
||||
// Renders a page template as build-scripts/gulp/entry-html.js does.
|
||||
const renderTemplate = (templateFile, data) =>
|
||||
template(readFileSync(templateFile, "utf-8"))({
|
||||
...data,
|
||||
renderTemplate: (childTemplate) =>
|
||||
renderTemplate(
|
||||
path.resolve(path.dirname(templateFile), childTemplate),
|
||||
data
|
||||
),
|
||||
});
|
||||
|
||||
// The index script that starts the extra modules, with Core's Jinja loops
|
||||
// rendered for one extra module and one legacy extra script.
|
||||
const extraModulesScript = () => {
|
||||
const html = renderTemplate(
|
||||
path.join(repoRoot, "src/html/index.html.template"),
|
||||
{
|
||||
modernRegex: "/modern/",
|
||||
hassUrl: "",
|
||||
staleBuildPatterns: patterns,
|
||||
useCacheRecovery: true,
|
||||
latestEntryJS: ["/frontend_latest/app.a1b2c3d4e5.js"],
|
||||
es5EntryJS: ["/frontend_es5/app.d1e2f3a4b5.js"],
|
||||
latestCustomPanelJS: "/frontend_latest/custom-panel.e1f2a3b4c5.js",
|
||||
es5CustomPanelJS: "/frontend_es5/custom-panel.f1a2b3c4d5.js",
|
||||
}
|
||||
);
|
||||
const script = html
|
||||
.split("<script>")
|
||||
.map((part) => part.slice(0, part.indexOf("</script>")))
|
||||
.find((part) => part.includes("__haLoadExtraModules"));
|
||||
return script
|
||||
.replace(
|
||||
/\{%-? for extra_module in extra_modules -?%\}([\s\S]*?)\{%-? endfor -?%\}/,
|
||||
(_match, body) => body.replaceAll("{{ extra_module }}", "/local/card.js")
|
||||
)
|
||||
.replace(
|
||||
/\{%-? for extra_script in extra_js_es5 -?%\}([\s\S]*?)\{%-? endfor -?%\}/,
|
||||
(_match, body) =>
|
||||
body.replaceAll("{{ extra_script }}", "/local/legacy.js")
|
||||
)
|
||||
.replace(/\bimport\(/g, "loadModule(");
|
||||
};
|
||||
|
||||
describe("index.html extra modules", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const setup = (latestJS) => {
|
||||
const win = { latestJS };
|
||||
const started = [];
|
||||
vi.stubGlobal("window", win);
|
||||
vi.resetModules();
|
||||
return {
|
||||
started,
|
||||
runIndexScript: () => {
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function("window", "_ls", "loadModule", extraModulesScript())(
|
||||
win,
|
||||
(src) => started.push(src),
|
||||
(src) => {
|
||||
started.push(src);
|
||||
return Promise.resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
// What app.ts runs right after the polyfill import.
|
||||
runAppEntry: () => import("../../src/resources/scoped-registry-ready"),
|
||||
};
|
||||
};
|
||||
|
||||
it.each([
|
||||
["modern", true, ["/local/card.js"]],
|
||||
["legacy", false, ["/local/card.js", "/local/legacy.js"]],
|
||||
])("waits for the app entry on the %s build", async (_, latestJS, all) => {
|
||||
const { started, runIndexScript, runAppEntry } = setup(latestJS);
|
||||
runIndexScript();
|
||||
expect(started).toEqual([]);
|
||||
await runAppEntry();
|
||||
expect(started).toEqual(all);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["modern", true, ["/local/card.js"]],
|
||||
["legacy", false, ["/local/card.js", "/local/legacy.js"]],
|
||||
])(
|
||||
"starts right away when the app entry ran first on the %s build",
|
||||
async (_, latestJS, all) => {
|
||||
const { started, runIndexScript, runAppEntry } = setup(latestJS);
|
||||
await runAppEntry();
|
||||
expect(started).toEqual([]);
|
||||
runIndexScript();
|
||||
expect(started).toEqual(all);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -2,13 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
filterLowBatteryEntities,
|
||||
filterProblemEntities,
|
||||
MaintenanceViewStrategy,
|
||||
} from "../../../../src/panels/maintenance/strategies/maintenance-view-strategy";
|
||||
import {
|
||||
mockDevice,
|
||||
mockEntity,
|
||||
} from "../../../common/entity/context/context-mock";
|
||||
import type { LovelaceSectionConfig } from "../../../../src/data/lovelace/config/section";
|
||||
import { mockEntity } from "../../../common/entity/context/context-mock";
|
||||
import { createMockEntityState, createMockHass } from "../../../fixtures/hass";
|
||||
|
||||
describe("filterLowBatteryEntities", () => {
|
||||
@@ -214,70 +209,3 @@ describe("filterProblemEntities", () => {
|
||||
).toEqual(["binary_sensor.has_problem"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MaintenanceViewStrategy.generate", () => {
|
||||
it("names problem tiles after the device and the entity", async () => {
|
||||
const hass = createMockHass(
|
||||
{
|
||||
"sensor.microwave_battery": createMockEntityState(
|
||||
"sensor.microwave_battery",
|
||||
"80",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
"binary_sensor.microwave_overheating": createMockEntityState(
|
||||
"binary_sensor.microwave_overheating",
|
||||
"off",
|
||||
{ device_class: "problem" }
|
||||
),
|
||||
"binary_sensor.standalone_problem": createMockEntityState(
|
||||
"binary_sensor.standalone_problem",
|
||||
"off",
|
||||
{ device_class: "problem" }
|
||||
),
|
||||
},
|
||||
{
|
||||
devices: {
|
||||
microwave: mockDevice({ id: "microwave", name: "Microwave" }),
|
||||
},
|
||||
entities: {
|
||||
"sensor.microwave_battery": mockEntity({
|
||||
entity_id: "sensor.microwave_battery",
|
||||
device_id: "microwave",
|
||||
}),
|
||||
"binary_sensor.microwave_overheating": mockEntity({
|
||||
entity_id: "binary_sensor.microwave_overheating",
|
||||
device_id: "microwave",
|
||||
}),
|
||||
"binary_sensor.standalone_problem": mockEntity({
|
||||
entity_id: "binary_sensor.standalone_problem",
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const view = await MaintenanceViewStrategy.generate(
|
||||
{ type: "maintenance" },
|
||||
hass
|
||||
);
|
||||
|
||||
const section = view.sections?.[0] as LovelaceSectionConfig;
|
||||
|
||||
expect(section.cards?.slice(1)).toEqual([
|
||||
{
|
||||
type: "tile",
|
||||
entity: "sensor.microwave_battery",
|
||||
name: { type: "device" },
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.microwave_overheating",
|
||||
name: [{ type: "device" }, { type: "entity" }],
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "binary_sensor.standalone_problem",
|
||||
name: { type: "entity" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user