Compare commits

..
Author SHA1 Message Date
Paulus SchoutsenandClaude Opus 5.5 14866713ba Fix values and time picker in the adjust statistic dialog (#54876)
* Show exact values in the adjust statistic dialog

The dialog rounded each change to the display precision and showed the
raw JavaScript number. This caused three problems:

- The adjustment was calculated from the rounded value. Setting a
  change of 1.2345 to 0 left 0.0045 in the database.
- Changes smaller than the precision showed as 0. They could not be
  found or set to 0, because the new value equaled the shown value.
- The numbers did not use the number format of the user's locale.

The adjustment is now calculated from the stored value. Small changes
get more digits, and the values are formatted with the user's locale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Wrap the time field below the date field on narrow screens

The date and time fields of the datetime selector are too wide for one
row on a phone. The time field overflowed the dialog and cut off the
AM/PM field in the adjust statistic dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Redraw the adjust statistic dialog when the locale changes

The values use the number format of the user's locale. The dialog only
redrew on hass changes when the translations changed, so a new number
format did not show until another change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

* Show a missing change as zero in the adjust statistic dialog

A statistic row without a sum has a null change. The digit count
treated it as a very small value and showed 20 decimals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

---------

Co-authored-by: Claude <[email protected]>
2026-10-13 08:32:47 +03:00
Aidan TimsonandClaude Opus 5.5 d1365b02da Add sticky group headers to data tables (#54848)
* Add sticky group headers to data table

lit-virtualizer positions rows absolutely, so CSS sticky positioning
can't be used. Render an overlay copy of the current group header at the
top of the scroller, pushed up by the next group header, and allow
collapsing the group from it.

With the group header now always visible, leave the grouped column out
of the narrow secondary line (except for auto-height tables, which have
no sticky header).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Keep sticky group header aligned and clear of focused rows

Shift the sticky group header's cell with the scroller's horizontal
scroll, so it lines up with the group header rows it covers, including
in RTL.

Add scroll padding to grouped tables, so a focused control in a row is
not left hidden under the sticky group header. Collapsing a group from
the sticky header scrolls a rendered header to the top directly, as
scrollIntoView would now leave room for the padding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Keep the sticky group header fixed instead of pushing it

Browsers scroll off the main thread, so the push offset set from scroll
events lagged behind the rows. Scrolling fast showed one header taking
over the other late, and scrolling up on mobile left a gap above the
next group header where rows showed through.

The sticky group header now stays at the top of the scroller, and the
next group header slides under it, so it no longer needs to move in
step with the scroll.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Resize the sticky group header with the data table

The sticky group header's width was only measured on scroll, so after
resizing the window it kept its old width until the next scroll, letting
rows show through beside it. Observe the table's size to update it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Count a sticky group header within a pixel of the top as reached

The virtualizer can place rows at fractional positions, so a group
header scrolled to the top could sit a fraction of a pixel below it.
The sticky header then kept showing the previous group over it, for
example after collapsing a group from the sticky header.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Refresh the sticky group after renders and make its whole row clickable

Filtering, sorting or collapsing can replace the virtualizer's items
without changing the visible range, so no visibility event fires and the
sticky group header kept showing the previous group until the next
scroll. Recompute it after each render.

The sticky header's cell shifts with horizontal scrolling, which left a
strip at the end of the header that didn't respond to clicks. Handle
clicks on the whole sticky row instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Explain the sticky group scroll back for unrendered headers

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

* Memoize the data table row renderer

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qmCtZuDLFdT8P7zqUcELk

---------

Co-authored-by: Claude <[email protected]>
2026-10-13 08:31:08 +03:00
Erwin Douna 707dc96fb1 Localize live indicator on logs page (#54858) 2026-10-13 08:13:38 +03:00
Paulus SchoutsenandClaude db1a628b57 Keep hidden columns when reopening data table settings (#54874)
Keep column settings for tables without fixed last columns

The data table settings dialog dropped the stored column order when the
table had no lastFixed columns. With no column order, the dialog showed
every column as visible. Reopening the dialog on the statistics table
then showed hidden columns as enabled again.


Claude-Session: https://claude.ai/code/session_014uzH9wKHRBfTGqXypaYEgV

Co-authored-by: Claude <[email protected]>
2026-10-13 07:55:48 +03:00
renovate[bot] 21c059e9fc Update dependency @types/node to v24.19.2 (#54868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-13 06:28:23 +02:00
renovate[bot] 33c734ea8b Update pnpm to v12.11.2 (#54869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-13 06:28:05 +02:00
12 changed files with 416 additions and 470 deletions

No files matched your search

+2 -2
View File
@@ -168,7 +168,7 @@
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.6",
"@types/node": "24.19.1",
"@types/node": "24.19.2",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
@@ -225,7 +225,7 @@
"webpackbar": "7.0.0",
"workbox-build": "7.4.1"
},
"packageManager": "[email protected].1",
"packageManager": "[email protected].2",
"volta": {
"node": "24.21.0"
}
+92 -92
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.11.1
version: 12.11.1
specifier: 12.11.2
version: 12.11.2
packages:
'@pnpm/[email protected].1':
resolution: {integrity: sha512-zoVTfgk2x7/Co7jBGDiDs9dyi2YnxdIDc0+iV2z+QxXHHq39hSJemhuyAgHRiwjpa052+FreoZx0C6nZ/kj1Lw==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
cpu: [arm64]
os: [android]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-vPO6nxrh1m18dkhf8DdqcX7ePNGDj4OWXTp6CFZOspc6NJavmMgKMsaV2ChCRyoe+thACnPdOyVic5HucYOK8Q==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
cpu: [x64]
os: [android]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-MLzzUKCLgJ/yHdO+qv/Ndk+lIPhtEUfkdVE2aznJ9/5R8ipuSwKBgR66hwjU6fHANERxl05zWe3G5gGYvoFvkw==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
cpu: [arm64]
os: [darwin]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-Rh5j7Vio5vmJR9hACJdgMrnEMIUNGcRc07o01mBdm6qtCohzbu0YyAKyV/VOKK/HQ19yls/7h1ktaY6orOX41A==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
cpu: [x64]
os: [darwin]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-y8UlliptKeCtJwQNuS5KvvX69x/um34+93kicwPMRGq4WIPA6XDEHDYJJUR8eigo/fmN59ge0o9hQL5PNHvalA==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
cpu: [x64]
os: [freebsd]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-4EmYe+LjHZMt9alrhpRd5W1oLuvWDt4wO0utSLCbrny5jOrl0PBYfLiPd2NLeIyaguV6Gtv0cLLC4gBzK1/blA==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-NoRfRJ6aHpph2aYNdpe1GszTTNpfdiqquaUTzONBc2v4MyzWfQ3ZskEGrf4J8yKnk/Mr61CaNG/awMpg4FwWCw==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-VFtJm5fN2SDISD0yihaITm9vWokuUAlM3FztEmNP1TrGGiMOfm3pUloekNk8coyxh0R12EILy92r/Zf7Fldvaw==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-dy5zX4ve0H7iDzQep/xGqaLm9bzL1QOvuulIxDOVLd3rbpxtf8P0Fu4OJ3uBjIj+xJ4k5XSLgpWMVL0/A+qFIA==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-xr/oooiSgbwT3n/9Djy6gStvVllIPEUJvI+p3lpk3wvvSwsxag7URvAb3E0s38DybnzuO8w/TGWn0KCdLnMBIA==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-kWWVru2mB3iOB5zBnfXVJkF8GAPhIA0TuL8XLrdhYWUrku8XvSwieemJlJevSFMZrcZ2ZcPVXCGJeB3pmdzDMw==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-c7kh0wtjg05in2wkmn6frs/tciuaGw8XNZUuggR88mvUvmJ81jsCP1MpM85wTFEptdeB4OnlRG0ESUZbu49q8Q==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-UqURM0VnIONCwFCp3Hz9o6X6gwcyTDvetbYJjAcsCwr4Rega2IXB0s/E+mQUpOUG5zl48Yi7xhLJI1U78OM+FQ==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
cpu: [arm64]
os: [win32]
'@pnpm/[email protected].1':
resolution: {integrity: sha512-aZaGoOswHuooaeCQ0+WS75FmxE9lg1zTjffHnY1gQN9Na/DX1oQ0MsyseadbzZoFF/i6XtnMvE7m5vpTyTRJ8g==}
'@pnpm/[email protected].2':
resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
cpu: [x64]
os: [win32]
[email protected].1:
resolution: {integrity: sha512-Ww4yiCOafqQVXSCU9UcGbhIqE3azzXhaaAyREuEWkT5kqq7PIN2DzDQDD6ZwE/XFUR+c9DDdg4QYi0xguKKtHg==}
[email protected].2:
resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
'@pnpm/[email protected].1':
'@pnpm/[email protected].2':
optional: true
[email protected].1:
[email protected].2:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.11.1
'@pnpm/exe.android-x64': 12.11.1
'@pnpm/exe.darwin-arm64': 12.11.1
'@pnpm/exe.darwin-x64': 12.11.1
'@pnpm/exe.freebsd-x64': 12.11.1
'@pnpm/exe.linux-arm64': 12.11.1
'@pnpm/exe.linux-arm64-musl': 12.11.1
'@pnpm/exe.linux-ppc64': 12.11.1
'@pnpm/exe.linux-riscv64': 12.11.1
'@pnpm/exe.linux-s390x': 12.11.1
'@pnpm/exe.linux-x64': 12.11.1
'@pnpm/exe.linux-x64-musl': 12.11.1
'@pnpm/exe.win32-arm64': 12.11.1
'@pnpm/exe.win32-x64': 12.11.1
'@pnpm/exe.android-arm64': 12.11.2
'@pnpm/exe.android-x64': 12.11.2
'@pnpm/exe.darwin-arm64': 12.11.2
'@pnpm/exe.darwin-x64': 12.11.2
'@pnpm/exe.freebsd-x64': 12.11.2
'@pnpm/exe.linux-arm64': 12.11.2
'@pnpm/exe.linux-arm64-musl': 12.11.2
'@pnpm/exe.linux-ppc64': 12.11.2
'@pnpm/exe.linux-riscv64': 12.11.2
'@pnpm/exe.linux-s390x': 12.11.2
'@pnpm/exe.linux-x64': 12.11.2
'@pnpm/exe.linux-x64-musl': 12.11.2
'@pnpm/exe.win32-arm64': 12.11.2
'@pnpm/exe.win32-x64': 12.11.2
---
lockfileVersion: '9.0'
@@ -267,7 +267,7 @@ importers:
version: 6.1.21(@fullcalendar/[email protected])
'@home-assistant/webawesome':
specifier: 3.14.0-ha.0
version: 3.14.0-ha.0(@floating-ui/[email protected])(@types/[email protected].1)
version: 3.14.0-ha.0(@floating-ui/[email protected])(@types/[email protected].2)
'@lezer/common':
specifier: 1.5.3
version: 1.5.3
@@ -561,8 +561,8 @@ importers:
specifier: 3.7.6
version: 3.7.6
'@types/node':
specifier: 24.19.1
version: 24.19.1
specifier: 24.19.2
version: 24.19.2
'@types/qrcode':
specifier: 1.5.6
version: 1.5.6
@@ -712,13 +712,13 @@ importers:
version: 8.71.1([email protected]([email protected]))([email protected])([email protected])
vite:
specifier: 8.3.4
version: 8.3.4(@types/[email protected].1)([email protected])([email protected])
version: 8.3.4(@types/[email protected].2)([email protected])([email protected])
vite-tsconfig-paths:
specifier: 6.1.1
version: 6.1.1([email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
version: 6.1.1([email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
vitest:
specifier: 5.0.3
version: 5.0.3(@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
version: 5.0.3(@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
webpack-stats-plugin:
specifier: 1.1.3
version: 1.1.3
@@ -3366,8 +3366,8 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/[email protected].1':
resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==}
'@types/[email protected].2':
resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==}
'@types/[email protected]':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
@@ -9499,12 +9499,12 @@ snapshots:
dependencies:
is-negated-glob: 1.0.0
'@home-assistant/[email protected](@floating-ui/[email protected])(@types/[email protected].1)':
'@home-assistant/[email protected](@floating-ui/[email protected])(@types/[email protected].2)':
dependencies:
'@ctrl/tinycolor': 4.1.0
'@floating-ui/dom': 1.8.0
'@konnorr/qr-creator': 1.1.0
'@lit-labs/ssr': 4.1.0(@types/[email protected].1)
'@lit-labs/ssr': 4.1.0(@types/[email protected].2)
'@lit-labs/ssr-client': 1.1.8
'@lit/context': 1.1.6
'@lit/react': 1.0.8
@@ -9740,7 +9740,7 @@ snapshots:
'@lit-labs/[email protected]': {}
'@lit-labs/[email protected](@types/[email protected].1)':
'@lit-labs/[email protected](@types/[email protected].2)':
dependencies:
'@lit-labs/ssr-client': 1.1.8
'@lit-labs/ssr-dom-shim': 1.6.0
@@ -9753,7 +9753,7 @@ snapshots:
node-fetch: 3.3.2
parse5: 7.3.0
optionalDependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@lit-labs/[email protected]':
dependencies:
@@ -10976,11 +10976,11 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@types/[email protected]':
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@types/[email protected]': {}
@@ -11038,7 +11038,7 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/[email protected].1':
'@types/[email protected].2':
dependencies:
undici-types: 7.24.6
@@ -11046,11 +11046,11 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@types/[email protected]':
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@types/[email protected]': {}
@@ -11068,7 +11068,7 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
@@ -11362,7 +11362,7 @@ snapshots:
obug: 2.1.4
std-env: 4.2.0
tinyrainbow: 3.1.1
vitest: 5.0.3(@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
vitest: 5.0.3(@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
'@vitest/[email protected]': {}
@@ -11370,14 +11370,14 @@ snapshots:
dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/[email protected]([email protected](@types/[email protected].1)([email protected])([email protected]))':
'@vitest/[email protected]([email protected](@types/[email protected].2)([email protected])([email protected]))':
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.3
estree-walker: 3.0.3
magic-string: 1.2.3
optionalDependencies:
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
'@vitest/[email protected]': {}
@@ -12237,7 +12237,7 @@ snapshots:
[email protected]([email protected]):
dependencies:
'@types/cors': 2.8.19
'@types/node': 24.19.1
'@types/node': 24.19.2
'@types/ws': 8.18.1
accepts: 1.3.8
base64id: 2.0.0
@@ -13330,7 +13330,7 @@ snapshots:
[email protected]:
dependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -15416,17 +15416,17 @@ snapshots:
- bare-abort-controller
- react-native-b4a
[email protected]([email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected])):
[email protected]([email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected])):
dependencies:
debug: 4.4.3([email protected])
globrex: 0.1.2
tsconfck: 3.1.6([email protected])
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
transitivePeerDependencies:
- supports-color
- typescript
[email protected](@types/[email protected].1)([email protected])([email protected]):
[email protected](@types/[email protected].2)([email protected])([email protected]):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
@@ -15434,15 +15434,15 @@ snapshots:
rolldown: 1.2.13
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
fsevents: 2.3.3
terser: 5.50.0
yaml: 2.9.1
[email protected](@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected])):
[email protected](@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected])):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.3([email protected](@types/[email protected].1)([email protected])([email protected]))
'@vitest/mocker': 5.0.3([email protected](@types/[email protected].2)([email protected])([email protected]))
chai: 6.2.2
es-module-lexer: 2.3.2
expect-type: 1.4.0
@@ -15453,10 +15453,10 @@ snapshots:
tinybench: 6.2.0
tinyexec: 1.3.1
tinyglobby: 0.2.17
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
why-is-node-running: 3.2.1
optionalDependencies:
'@types/node': 24.19.1
'@types/node': 24.19.2
'@vitest/coverage-v8': 5.0.3([email protected])
jsdom: 30.1.2
transitivePeerDependencies:
@@ -66,15 +66,15 @@ export class DialogDataTableSettings extends LitElement {
}
private _preserveLastFixed(columnOrder) {
let strippedColumnOrder;
const lastFixedCount = this._lastFixedCount();
if (lastFixedCount && columnOrder) {
strippedColumnOrder = [...columnOrder];
strippedColumnOrder.splice(
columnOrder.length - lastFixedCount,
lastFixedCount
);
if (!lastFixedCount || !columnOrder) {
return columnOrder;
}
const strippedColumnOrder = [...columnOrder];
strippedColumnOrder.splice(
columnOrder.length - lastFixedCount,
lastFixedCount
);
return strippedColumnOrder;
}
+279 -30
View File
@@ -1,3 +1,7 @@
import type {
LitVirtualizer,
VisibilityChangedEvent,
} from "@lit-labs/virtualizer";
import type { ContextType } from "@lit/context";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
@@ -191,7 +195,29 @@ export class HaDataTable extends LitElement {
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
@query("lit-virtualizer") private _scroller?: HTMLElement;
@query("lit-virtualizer")
private _scroller?: LitVirtualizer<DataTableRowData>;
@query(".sticky-group-header") private _stickyGroupHeader?: HTMLElement;
@query(".sticky-group-header .group-header")
private _stickyGroupHeaderCell?: HTMLElement;
// Group pinned to the top of the scroller while its own header is scrolled out of view
@state() private _stickyGroup?: string;
// Position and width of the sticky group header, applied directly on scroll
// to avoid re-rendering the table on every frame
private _stickyGroupPosition = { top: 0, width: 0, scrollLeft: 0 };
private _stickyGroupFrame?: number;
// Resizing doesn't scroll, but changes the width of the sticky group header
private _resizeObserver = new ResizeObserver(() =>
this._scheduleStickyGroupUpdate()
);
private _firstVisibleIndex = 0;
@state() private _collapsedGroups: string[] = [];
@@ -237,6 +263,7 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._resizeObserver.observe(this);
if (this._filteredData?.length) {
// Force update of location of rows
this._filteredData = [...this._filteredData];
@@ -247,7 +274,21 @@ export class HaDataTable extends LitElement {
this.updateComplete.then(() => this._calcTableHeight());
}
public disconnectedCallback() {
super.disconnectedCallback();
this._resizeObserver.disconnect();
if (this._stickyGroupFrame !== undefined) {
cancelAnimationFrame(this._stickyGroupFrame);
this._stickyGroupFrame = undefined;
}
}
protected updated(changedProps: PropertyValues<this>) {
// The sticky group header may have just been rendered, and the items may
// have changed (filter, sort, collapse) without the visible range changing
this._positionStickyGroupHeader();
this._scheduleStickyGroupUpdate();
if (!this._headerRow) {
return;
}
@@ -343,6 +384,7 @@ export class HaDataTable extends LitElement {
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
this._stickyGroup = undefined;
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -373,6 +415,12 @@ export class HaDataTable extends LitElement {
}
}
private _memRenderRow = memoizeOne(
(columns: DataTableColumnContainer, narrow: boolean) =>
(row: DataTableRowData, index: number) =>
this._renderRow(columns, narrow, row, index)
);
private _sortedColumns = memoizeOne(
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
if (!columnOrder || !columnOrder.length) {
@@ -408,8 +456,7 @@ export class HaDataTable extends LitElement {
protected render() {
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
this._renderRow(columns, this.narrow, row, index);
const renderRow = this._memRenderRow(columns, this.narrow);
const filteredDataLength = this._filteredData?.length || 0;
@@ -655,9 +702,12 @@ export class HaDataTable extends LitElement {
: html`
<lit-virtualizer
scroller
class="mdc-data-table__content scroller ha-scrollbar"
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
)}"
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
@scroll=${this._saveScrollPos}
@visibilityChanged=${this._handleVisibilityChanged}
.items=${this._groupData(
this._filteredData,
this._i18n?.localize,
@@ -672,6 +722,25 @@ export class HaDataTable extends LitElement {
.keyFunction=${this._keyFunction}
.renderItem=${renderRow}
></lit-virtualizer>
${
this.groupColumn &&
!this.autoHeight &&
this._stickyGroup !== undefined
? html`<div
class="mdc-data-table__row sticky-group-header"
aria-hidden="true"
.group=${this._stickyGroup}
@click=${this._collapseStickyGroup}
>
${this._renderGroupHeader(
this._stickyGroup,
this._collapsedGroups.includes(this._stickyGroup),
this._i18n?.localize,
true
)}
</div>`
: nothing
}
`
}
</div>
@@ -692,7 +761,14 @@ export class HaDataTable extends LitElement {
return nothing;
}
if (row.append) {
return html`<div class="mdc-data-table__row">${row.content}</div>`;
return html`<div
class="mdc-data-table__row ${classMap({
"group-header-row": row.groupHeader !== undefined,
})}"
.group=${row.groupHeader}
>
${row.content}
</div>`;
}
if (row.empty) {
return html`<div class="mdc-data-table__row empty-row"></div>`;
@@ -800,7 +876,13 @@ export class HaDataTable extends LitElement {
key: string,
column: DataTableColumnData
): boolean {
if (column.main || column.showNarrow) {
// The grouped value is already shown in the sticky group header, which
// isn't available for auto-height tables
if (
column.main ||
column.showNarrow ||
(!this.autoHeight && key === this.groupColumn)
) {
return false;
}
return this._isColumnVisible(key, column);
@@ -948,33 +1030,15 @@ export class HaDataTable extends LitElement {
const groupedItems: DataTableRowData[] = [];
sortedEntries.forEach(([groupName, rows]) => {
const collapsed = collapsedGroups.includes(groupName);
groupedItems.push({
append: true,
selectable: false,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${
localize?.(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")
}
class=${collapsed ? "collapsed" : ""}
>
</ha-icon-button>
${
groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") ||
"Ungrouped"
: groupName || ""
}
</div>`,
groupHeader: groupName,
content: this._renderGroupHeader(
groupName,
collapsedGroups.includes(groupName),
localize
),
});
if (!collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
@@ -995,6 +1059,40 @@ export class HaDataTable extends LitElement {
}
);
private _renderGroupHeader(
groupName: string,
collapsed: boolean,
localize: LocalizeFunc | undefined,
sticky = false
) {
// The sticky copy is hidden from assistive technology, so its button is
// made inert. It handles clicks on its whole row instead, as its cell
// shifts with horizontal scrolling.
return html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${sticky ? nothing : this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${
localize?.(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")
}
class=${collapsed ? "collapsed" : ""}
?inert=${sticky}
>
</ha-icon-button>
${
groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
: groupName || ""
}
</div>`;
}
private _memFilterData = memoizeOne(
(
data: DataTableRowData[],
@@ -1197,6 +1295,101 @@ export class HaDataTable extends LitElement {
if (this._headerRow) {
this._headerRow.scrollLeft = e.target.scrollLeft;
}
this._scheduleStickyGroupUpdate();
}
private _handleVisibilityChanged(ev: VisibilityChangedEvent) {
this._firstVisibleIndex = ev.first;
// Fired before the virtualizer positions its children, so measure later
this._scheduleStickyGroupUpdate();
}
private _scheduleStickyGroupUpdate() {
if (!this.groupColumn) {
this._stickyGroup = undefined;
return;
}
if (this._stickyGroupFrame !== undefined) {
return;
}
this._stickyGroupFrame = requestAnimationFrame(() => {
this._stickyGroupFrame = undefined;
this._updateStickyGroup();
});
}
/**
* The virtualizer positions rows absolutely, so `position: sticky` can't be
* used. Instead, an overlay copy of the current group header is shown at the
* top of the scroller, and the next group header slides under it. The
* overlay doesn't move with the scroll, as browsers scroll off the main
* thread and it would lag behind the rows.
*/
private _updateStickyGroup() {
const scroller = this._scroller;
if (
!scroller ||
!this.groupColumn ||
this.autoHeight ||
scroller.scrollTop <= 0
) {
this._stickyGroup = undefined;
return;
}
const scrollerTop = scroller.getBoundingClientRect().top;
let group: string | undefined;
let currentTop = -Infinity;
scroller
.querySelectorAll<HTMLElement & { group: string }>(".group-header-row")
.forEach((row) => {
const top = row.getBoundingClientRect().top - scrollerTop;
// Rows can sit at fractional positions, so a header less than a
// pixel below the top counts as reached
if (top < 1 && top > currentTop) {
currentTop = top;
group = row.group;
}
});
if (group === undefined) {
// The current group header is not rendered, find it from the first visible row
const items = scroller.items;
for (
let i = Math.min(this._firstVisibleIndex, items.length - 1);
i >= 0;
i--
) {
if (items[i]?.append && items[i].groupHeader !== undefined) {
group = items[i].groupHeader;
break;
}
}
}
this._stickyGroup = group;
this._stickyGroupPosition = {
top: scroller.offsetTop,
width: scroller.clientWidth,
scrollLeft: scroller.scrollLeft,
};
this._positionStickyGroupHeader();
}
private _positionStickyGroupHeader() {
const header = this._stickyGroupHeader;
if (!header) {
return;
}
const { top, width, scrollLeft } = this._stickyGroupPosition;
header.style.width = `${width}px`;
header.style.transform = `translateY(${top}px)`;
// Follow the horizontal scroll of the group header rows it covers
if (this._stickyGroupHeaderCell) {
this._stickyGroupHeaderCell.style.transform = `translateX(${-scrollLeft}px)`;
}
}
@eventOptions({ passive: true })
@@ -1208,6 +1401,40 @@ export class HaDataTable extends LitElement {
this._scroller.scrollLeft = e.target.scrollLeft;
}
private _collapseStickyGroup = async (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
const groupName = ev.currentTarget.group;
this._collapseGroup(ev);
await this.updateComplete;
const scroller = this._scroller;
if (!scroller) {
return;
}
// Keep the toggled group in view, its rows may have been above the viewport.
// Scroll a rendered header to the very top ourselves, as scrollIntoView
// would leave room for the sticky header (scroll-padding-top).
const row = Array.from(
scroller.querySelectorAll<HTMLElement & { group: string }>(
".group-header-row"
)
).find((headerRow) => headerRow.group === groupName);
if (row) {
scroller.scrollTop +=
row.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
return;
}
const index = scroller.items.findIndex(
(item) => item?.append && item.groupHeader === groupName
);
if (index !== -1) {
// The header isn't rendered, but element() still returns a proxy for
// any index in the items: its scrollIntoView() pins that index in the
// virtualizer's layout, which renders it and aligns it to the top
scroller.element(index)?.scrollIntoView({ block: "start" });
}
};
private _collapseGroup = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
@@ -1562,6 +1789,28 @@ export class HaDataTable extends LitElement {
transform: rotate(180deg);
}
.sticky-group-header {
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 1;
/* Match the top border of the group header rows it covers */
border-top: 1px solid transparent;
border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color);
will-change: transform;
}
.sticky-group-header .group-header {
/* Don't paint over the bottom divider */
background-color: transparent;
}
/* Keep focused rows clear of the sticky group header */
lit-virtualizer.grouped {
scroll-padding-top: var(--data-table-row-height, 52px);
}
:host {
display: block;
}
@@ -91,6 +91,8 @@ export class HaDateTimeSelector extends LitElement {
below for its hint while ha-time-input does not. */
align-items: flex-start;
flex-direction: row;
/* Narrow screens have no room for both fields on one row. */
flex-wrap: wrap;
}
ha-date-input {
+1 -7
View File
@@ -261,19 +261,13 @@ export const updateStatisticsMetadata = (
unit_class,
});
// Without a range, the statistics are removed completely.
export const clearStatistics = (
hass: Pick<HomeAssistant, "callWS">,
statistic_ids: string[],
range?: { start: Date; end: Date }
statistic_ids: string[]
) =>
hass.callWS<undefined>({
type: "recorder/clear_statistics",
statistic_ids,
...(range && {
start_time: range.start.toISOString(),
end_time: range.end.toISOString(),
}),
});
export const calculateStatisticSumGrowth = (
+1 -1
View File
@@ -339,7 +339,7 @@ class ErrorLogCard extends LitElement {
streaming && this._boot === 0 && !this._error
? html`<div class="live-indicator">
<ha-svg-icon path=${mdiCircle}></ha-svg-icon>
Live
${localize("ui.panel.config.logs.live")}
</div>`
: nothing
}
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-spinner";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog";
@@ -66,8 +67,6 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
@state() private _chosenStat?: StatisticValue;
private _origAmount?: number;
@state() private _amount?: number;
private _showingOutliers = false;
@@ -125,7 +124,6 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
this._moment = undefined;
this._stats5min = undefined;
this._statsHour = undefined;
this._origAmount = undefined;
this._amount = undefined;
this._chosenStat = undefined;
this._initDirtyTracking({ type: "deep" }, { amount: undefined });
@@ -202,9 +200,13 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
if (changedProps.size !== 1 || !changedProps.has("hass")) {
return true;
}
// We only respond to hass changes if the translations changed
// We only respond to hass changes if the translations or locale changed
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
return !oldHass || oldHass.localize !== this.hass.localize;
return (
!oldHass ||
oldHass.localize !== this.hass.localize ||
oldHass.locale !== this.hass.locale
);
}
private _renderPickStatistic() {
@@ -228,8 +230,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
);
const rows: TemplateResult[] = [];
for (const stat of data) {
const multiple = 10 ** this._precision;
const growth = Math.round(stat.change! * multiple) / multiple;
const digits = this._fractionDigits(stat.change);
rows.push(html`
<ha-list-item
twoline
@@ -237,7 +238,13 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
.stat=${stat}
@click=${this._setChosenStatistic}
>
<span>${growth} ${unit}</span>
<span
>${formatNumber(stat.change!, this.hass.locale, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
})}
${unit}</span
>
<span slot="secondary">
${formatDateTime(
new Date(stat.start),
@@ -283,15 +290,22 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
this._initDirtyTracking({ type: "deep" }, { amount: undefined });
}
// Small changes get more digits, so they do not round to zero.
private _fractionDigits(value: number | null | undefined): number {
if (!value || Math.abs(value) >= 10 ** -this._precision) {
return this._precision;
}
return Math.min(Math.ceil(-Math.log10(Math.abs(value))) + 1, 20);
}
private _setChosenStatistic(ev) {
const stat = ev.currentTarget.stat;
const multiple = 10 ** this._precision;
const multiple = 10 ** this._fractionDigits(stat.change);
const growth = Math.round(stat.change! * multiple) / multiple;
this._chosenStat = stat;
this._origAmount = growth;
this._amount = growth;
this._initDirtyTracking({ type: "deep" }, { amount: this._origAmount });
this._initDirtyTracking({ type: "deep" }, { amount: growth });
}
private _dateTimeSelectorChanged(ev) {
@@ -393,7 +407,10 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
.label=${this.hass.localize(
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.new_value"
)}
.selector=${this._amountSelector(unit || undefined, this._precision)}
.selector=${this._amountSelector(
unit || undefined,
this._fractionDigits(this._chosenStat!.change)
)}
.value=${this._amount}
.disabled=${this._busy}
@value-changed=${this._amountChanged}
@@ -572,7 +589,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
this.hass,
params.statistic.statistic_id,
chosenStat.start,
amount - this._origAmount!,
// The shown value is rounded, so compare with the stored value.
amount - chosenStat.change!,
unit || null
);
} catch (err: any) {
@@ -1,257 +0,0 @@
import type { ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { consume } from "../../../../common/decorators/consume";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import "../../../../components/chart/statistics-chart";
import "../../../../components/date-picker/ha-date-range-picker";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-spinner";
import {
apiContext,
internationalizationContext,
} from "../../../../data/context";
import type { Statistics, StatisticType } from "../../../../data/recorder";
import {
clearStatistics,
fetchStatistics,
StatisticMeanType,
} from "../../../../data/recorder";
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
import { haStyleDialog } from "../../../../resources/styles";
import { showToast } from "../../../../util/toast";
import type { DialogStatisticsClearRangeParams } from "./show-dialog-statistics-clear-range";
// Longer ranges use hourly statistics to keep the preview fast.
const MAX_5MINUTE_PREVIEW_MS = 3 * 24 * 60 * 60 * 1000;
@customElement("dialog-statistics-clear-range")
class DialogStatisticsClearRange extends DialogMixin<DialogStatisticsClearRangeParams>(
LitElement
) {
@state()
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state() private _start = new Date();
@state() private _end = new Date();
@state() private _statistics?: Statistics;
@state() private _busy = false;
@state() private _error?: string;
// Incremented for every fetch, so results of outdated fetches are ignored
private _fetchId = 0;
connectedCallback() {
super.connectedCallback();
this._end.setMinutes(
this._end.getMinutes() - (this._end.getMinutes() % 5),
0,
0
);
this._start = new Date(this._end.getTime() - 24 * 60 * 60 * 1000);
this._fetchStatistics();
}
private get _validRange() {
return this._start.getTime() < this._end.getTime();
}
private get _statTypes(): StatisticType[] {
return this.params!.statistic.mean_type !== StatisticMeanType.NONE
? ["mean", "min", "max"]
: ["change"];
}
protected render() {
if (!this.params) {
return nothing;
}
const localize = this._i18n.localize;
const statisticId = this.params.statistic.statistic_id;
const hasData = Boolean(this._statistics?.[statisticId]?.length);
return html`
<ha-dialog
open
width="large"
.headerTitle=${localize(
"ui.panel.config.tools.tabs.statistics.clear_range.title"
)}
.preventScrimClose=${this._busy}
>
<p>
${localize(
"ui.panel.config.tools.tabs.statistics.clear_range.info_text",
{ name: html`<b>${this.params.name}</b>` }
)}
</p>
<ha-date-range-picker
time-picker
.startDate=${this._start}
.endDate=${this._end}
.disabled=${this._busy}
@value-changed=${this._rangeChanged}
></ha-date-range-picker>
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing
}
${
!this._validRange
? html`<ha-alert alert-type="error">
${localize(
"ui.panel.config.tools.tabs.statistics.clear_range.invalid_range"
)}
</ha-alert>`
: !this._statistics
? html`<div class="preview center">
<ha-spinner></ha-spinner>
</div>`
: !hasData
? html`<div class="preview center">
${localize(
"ui.panel.config.tools.tabs.statistics.clear_range.no_statistics_found"
)}
</div>`
: html`<div class="preview">
<statistics-chart
.statisticsData=${this._statistics}
.metadata=${{ [statisticId]: this.params.statistic }}
.names=${{ [statisticId]: this.params.name }}
.statTypes=${this._statTypes}
.chartType=${
this._statTypes[0] === "change" ? "bar" : "line"
}
.startTime=${this._start}
.endTime=${this._end}
hide-legend
.clickForMoreInfo=${false}
height="240px"
></statistics-chart>
</div>`
}
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
.disabled=${this._busy}
@click=${this.closeDialog}
>
${localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
variant="danger"
.loading=${this._busy}
.disabled=${this._busy || !this._validRange || !hasData}
@click=${this._delete}
>
${localize("ui.common.delete")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private _rangeChanged(
ev: HASSDomEvent<{ value: { startDate: Date; endDate: Date } }>
) {
this._start = ev.detail.value.startDate;
this._end = ev.detail.value.endDate;
this._fetchStatistics();
}
private async _fetchStatistics() {
const fetchId = ++this._fetchId;
this._statistics = undefined;
this._error = undefined;
if (!this._validRange) {
return;
}
const start = this._start;
const end = this._end;
try {
const statistics = await fetchStatistics(
this._api.callWS,
start,
end,
[this.params!.statistic.statistic_id],
end.getTime() - start.getTime() > MAX_5MINUTE_PREVIEW_MS
? "hour"
: "5minute",
undefined,
this._statTypes.includes("change") ? ["change"] : ["mean", "min", "max"]
);
if (fetchId === this._fetchId) {
this._statistics = statistics;
}
} catch (err: any) {
if (fetchId === this._fetchId) {
this._statistics = {};
this._error = err.message || err;
}
}
}
private async _delete() {
this._busy = true;
this._error = undefined;
try {
await clearStatistics(this._api, [this.params!.statistic.statistic_id], {
start: this._start,
end: this._end,
});
} catch (err: any) {
this._busy = false;
this._error = err.message || err;
return;
}
showToast(this, {
message: this._i18n.localize(
"ui.panel.config.tools.tabs.statistics.clear_range.deleted"
),
});
this.closeDialog();
}
static styles = [
haStyleDialog,
css`
p {
margin-top: 0;
}
ha-date-range-picker {
display: block;
margin-bottom: var(--ha-space-4);
}
.preview {
min-height: 240px;
}
.center {
display: flex;
align-items: center;
justify-content: center;
color: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-statistics-clear-range": DialogStatisticsClearRange;
}
}
@@ -1,21 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { StatisticsMetaData } from "../../../../data/recorder";
export const loadClearRangeDialog = () =>
import("./dialog-statistics-clear-range");
export interface DialogStatisticsClearRangeParams {
statistic: StatisticsMetaData;
name: string;
}
export const showStatisticsClearRangeDialog = (
element: HTMLElement,
dialogParams: DialogStatisticsClearRangeParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-statistics-clear-range",
dialogImport: loadClearRangeDialog,
dialogParams,
});
};
@@ -2,7 +2,6 @@ import {
mdiArrowDown,
mdiArrowUp,
mdiClose,
mdiDeleteClock,
mdiFormatListChecks,
mdiMenuDown,
mdiSlopeUphill,
@@ -79,7 +78,6 @@ import type { HomeAssistantRegistries } from "../../../../types";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
import { showStatisticsClearRangeDialog } from "./show-dialog-statistics-clear-range";
const FIX_ISSUES_ORDER: Record<StatisticsValidationResult["type"], number> = {
no_state: 0,
@@ -307,24 +305,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
maxWidth: "113px",
showNarrow: true,
},
clear_range: {
title: "",
label: localize(
"ui.panel.config.tools.tabs.statistics.clear_range.title"
),
type: "icon-button",
showNarrow: true,
template: (statistic) => html`
<ha-icon-button
.label=${localize(
"ui.panel.config.tools.tabs.statistics.clear_range.title"
)}
.path=${mdiDeleteClock}
.statistic=${statistic}
@click=${this._showStatisticsClearRangeDialog}
></ha-icon-button>
`,
},
actions: {
title: "",
label: localize("ui.panel.config.tools.tabs.statistics.adjust_sum"),
@@ -747,19 +727,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
});
}
private _showStatisticsClearRangeDialog(
ev: HASSDomCurrentTargetEvent<
HTMLElement & { statistic: DisplayedStatisticData }
>
) {
ev.stopPropagation();
const statistic = ev.currentTarget.statistic;
showStatisticsClearRangeDialog(this, {
statistic,
name: statistic.displayName,
});
}
private _rowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const id = ev.detail.id;
if (id in this._states) {
+1 -7
View File
@@ -4312,13 +4312,6 @@
}
},
"adjust_sum": "Adjust sum",
"clear_range": {
"title": "Delete statistics in a time range",
"info_text": "Delete the statistics of {name} between the start and end time. Use this to remove bad measurements from your graphs. Deleted statistics cannot be restored.",
"invalid_range": "The end time must be after the start time.",
"no_statistics_found": "No statistics found for this period.",
"deleted": "Statistics deleted"
},
"data_table": {
"select_all_issues": "Select all with issues",
"name": "Name",
@@ -4833,6 +4826,7 @@
"environment": "Debug logging has been disabled, but no log file was downloaded because HA_DISABLE_LOG_FILE is set and Home Assistant is not writing a log file."
},
"scroll_down_button": "New logs - Click to scroll",
"live": "Live",
"provider_not_found": "Log provider not found",
"provider_not_available": "Logs for ''{provider}'' are not available on your system.",
"haos_boots_title": "Logs of HAOS startup",