Compare commits
359 Commits
websocket_
...
20250306.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9ef72e4afc | ||
![]() |
022ef982ca | ||
![]() |
f132a32fd4 | ||
![]() |
782df0473c | ||
![]() |
690cd47945 | ||
![]() |
68960ba03d | ||
![]() |
c3a60a9c3f | ||
![]() |
4783720aaa | ||
![]() |
d55b806ce5 | ||
![]() |
e2ff8ce302 | ||
![]() |
b26bc1dcf0 | ||
![]() |
76b03d3a40 | ||
![]() |
7120200fd4 | ||
![]() |
209d0ae5f4 | ||
![]() |
81bafba4e4 | ||
![]() |
819225d32b | ||
![]() |
7f7575dcbc | ||
![]() |
56eef4bf31 | ||
![]() |
a384bc2273 | ||
![]() |
9d5d0e448f | ||
![]() |
f020269447 | ||
![]() |
99f86bb9cf | ||
![]() |
587751f5b4 | ||
![]() |
52b199c92b | ||
![]() |
5caa47acc1 | ||
![]() |
7833a680a9 | ||
![]() |
92681e7036 | ||
![]() |
ac18c6c018 | ||
![]() |
7e0cd35ea8 | ||
![]() |
45dee566b7 | ||
![]() |
71a4dceedc | ||
![]() |
27d883c1f6 | ||
![]() |
014db0e60f | ||
![]() |
c80247d992 | ||
![]() |
df33e97996 | ||
![]() |
4974a12221 | ||
![]() |
a13bcac0aa | ||
![]() |
cf288f7cd1 | ||
![]() |
1c8284609f | ||
![]() |
1fdadbf1b8 | ||
![]() |
3b272ae411 | ||
![]() |
a048c36861 | ||
![]() |
2a6c1773f3 | ||
![]() |
bf17012753 | ||
![]() |
bbaf23e049 | ||
![]() |
34b7929165 | ||
![]() |
8f06e70a11 | ||
![]() |
50ac60b35e | ||
![]() |
4caca19e32 | ||
![]() |
a7b1c45c00 | ||
![]() |
10c3e4c6f8 | ||
![]() |
1f50c359dc | ||
![]() |
a3e24a3dc0 | ||
![]() |
a906285a03 | ||
![]() |
5c933a43b2 | ||
![]() |
151a7fbc40 | ||
![]() |
4fa915c869 | ||
![]() |
d47e5c847b | ||
![]() |
db5036aed3 | ||
![]() |
bb672d0272 | ||
![]() |
e26d3d39f0 | ||
![]() |
e54c3a69af | ||
![]() |
cc04457d72 | ||
![]() |
9e1d64e728 | ||
![]() |
0cfe7f8d12 | ||
![]() |
2b1f301db6 | ||
![]() |
fc4996412e | ||
![]() |
ece4a6345f | ||
![]() |
a4c08a78b9 | ||
![]() |
a438fc5e41 | ||
![]() |
783132ae46 | ||
![]() |
680d81001c | ||
![]() |
a917383d7a | ||
![]() |
455a6761cd | ||
![]() |
acf42d7637 | ||
![]() |
3857c7321a | ||
![]() |
5eec814988 | ||
![]() |
4a1b7d46ca | ||
![]() |
75fadcca42 | ||
![]() |
41c93f5f7e | ||
![]() |
99559ff716 | ||
![]() |
753fe719e3 | ||
![]() |
5c14afd944 | ||
![]() |
23f1925c84 | ||
![]() |
edd37565a6 | ||
![]() |
fb3f779121 | ||
![]() |
4d7634ac67 | ||
![]() |
ba5c1133c6 | ||
![]() |
0a05dd8f71 | ||
![]() |
400106ec09 | ||
![]() |
a7a4194e09 | ||
![]() |
0bd7d27c57 | ||
![]() |
8175e45921 | ||
![]() |
cae36b393b | ||
![]() |
f84ad92356 | ||
![]() |
fb1ee2ed1d | ||
![]() |
9073282174 | ||
![]() |
91bd5cba08 | ||
![]() |
a68bdbfe08 | ||
![]() |
f3d614b0d3 | ||
![]() |
f3c9e4a4a0 | ||
![]() |
d22a82c4a6 | ||
![]() |
5cddc6e5c6 | ||
![]() |
c5c067ef19 | ||
![]() |
694bb3088c | ||
![]() |
ad487470fd | ||
![]() |
2801d071ba | ||
![]() |
71b65f208f | ||
![]() |
ab4efb7412 | ||
![]() |
c7a46ec25b | ||
![]() |
83d4a408f6 | ||
![]() |
06932d1479 | ||
![]() |
24211d5f25 | ||
![]() |
d387f19a31 | ||
![]() |
347ee2a4c3 | ||
![]() |
1363884773 | ||
![]() |
0256da511d | ||
![]() |
c52217c1ce | ||
![]() |
cdd17eed2e | ||
![]() |
4546c6f624 | ||
![]() |
2c34760204 | ||
![]() |
0b64861297 | ||
![]() |
94a5e737cc | ||
![]() |
05163588fc | ||
![]() |
ee64536862 | ||
![]() |
695a6a506e | ||
![]() |
3ee3cfa6cb | ||
![]() |
00d0cb7afa | ||
![]() |
3ae34403bd | ||
![]() |
1434966170 | ||
![]() |
8dd70f7017 | ||
![]() |
84a0289e1b | ||
![]() |
a25e1d3f7f | ||
![]() |
f53ac41eee | ||
![]() |
b9acd40b0f | ||
![]() |
7524dc8709 | ||
![]() |
cbedf62c39 | ||
![]() |
63a98155cd | ||
![]() |
7369b7e0d5 | ||
![]() |
922abafabf | ||
![]() |
f1bb4a5694 | ||
![]() |
e0b9cb8ccb | ||
![]() |
06f27650da | ||
![]() |
a772eaffd7 | ||
![]() |
c39be4a9b8 | ||
![]() |
c68002214f | ||
![]() |
8dbc203130 | ||
![]() |
64274d7355 | ||
![]() |
c07f4de39d | ||
![]() |
37ee2bf308 | ||
![]() |
d9559b7f07 | ||
![]() |
fce07daa20 | ||
![]() |
5d6fcaf6bb | ||
![]() |
0abccb88d6 | ||
![]() |
5dc5879773 | ||
![]() |
41df7a3f4a | ||
![]() |
920ec035c5 | ||
![]() |
043e8d6e2e | ||
![]() |
d8e36894a0 | ||
![]() |
65b6a3c6a3 | ||
![]() |
b16f82cedb | ||
![]() |
02deeb4ce7 | ||
![]() |
0c6651c2c2 | ||
![]() |
abbf56db1d | ||
![]() |
bc0cc8b387 | ||
![]() |
b66f41db7d | ||
![]() |
05fbe204c5 | ||
![]() |
ee199fbbc0 | ||
![]() |
56ab29da81 | ||
![]() |
10abaa538d | ||
![]() |
f25dac7f68 | ||
![]() |
99065a689f | ||
![]() |
ac88d5993a | ||
![]() |
b09ce45d31 | ||
![]() |
78e2809fe7 | ||
![]() |
a631bf9854 | ||
![]() |
1349c8520c | ||
![]() |
6d1a55cc3a | ||
![]() |
23a9ae6835 | ||
![]() |
dbd1e928de | ||
![]() |
e86ad21ce2 | ||
![]() |
0d97afb3f2 | ||
![]() |
0ab9098f23 | ||
![]() |
4498747fb1 | ||
![]() |
76977b64fa | ||
![]() |
2ca7395733 | ||
![]() |
0900869957 | ||
![]() |
91e8750f44 | ||
![]() |
936f66c41c | ||
![]() |
9ab5be4730 | ||
![]() |
a30e501031 | ||
![]() |
dcb04067b8 | ||
![]() |
bf962b29af | ||
![]() |
0ae6fa0763 | ||
![]() |
03a415beff | ||
![]() |
44cc75afbc | ||
![]() |
748642a8d6 | ||
![]() |
3d5c65d652 | ||
![]() |
a26bf80b13 | ||
![]() |
497c6c35f1 | ||
![]() |
b0b06a2787 | ||
![]() |
f3d55447ca | ||
![]() |
1b3d4b77d3 | ||
![]() |
6ec4041c4c | ||
![]() |
d919e8d333 | ||
![]() |
af7bb85667 | ||
![]() |
9061e2039b | ||
![]() |
906e6f4a88 | ||
![]() |
73fbe9a69d | ||
![]() |
2a0f69a629 | ||
![]() |
9411a77f14 | ||
![]() |
de3bf2e088 | ||
![]() |
16181b48ae | ||
![]() |
8682debe61 | ||
![]() |
bdbc9bc1b4 | ||
![]() |
79b9f8d083 | ||
![]() |
3918194d2d | ||
![]() |
e9fef1f873 | ||
![]() |
35face602b | ||
![]() |
9d7d332790 | ||
![]() |
803ac496f6 | ||
![]() |
f1173dd84b | ||
![]() |
44dcca9923 | ||
![]() |
bd74d39dd8 | ||
![]() |
172d6c3079 | ||
![]() |
56539e8065 | ||
![]() |
8f6867f142 | ||
![]() |
d51f8995dd | ||
![]() |
f2e35dc70a | ||
![]() |
6487b9b7ea | ||
![]() |
e50b658db7 | ||
![]() |
6efe237639 | ||
![]() |
4a94cfc05b | ||
![]() |
7cbdb1dcfd | ||
![]() |
553bb61db7 | ||
![]() |
786ff787d1 | ||
![]() |
28b3f2970a | ||
![]() |
7d170a710e | ||
![]() |
cc40b50675 | ||
![]() |
b6eaff46e9 | ||
![]() |
674bb0d16a | ||
![]() |
6ff018afc9 | ||
![]() |
ad48732bb7 | ||
![]() |
fef162346a | ||
![]() |
72d208d1ac | ||
![]() |
5a8b1b0fd4 | ||
![]() |
4cfc651799 | ||
![]() |
b4a3f4cb2c | ||
![]() |
f0507a88a6 | ||
![]() |
fe041e442d | ||
![]() |
e5fea98460 | ||
![]() |
31180e3a9e | ||
![]() |
ce0f02a45b | ||
![]() |
53f090356e | ||
![]() |
776c4da688 | ||
![]() |
849922f7be | ||
![]() |
a26701808f | ||
![]() |
904ee2e418 | ||
![]() |
11ae3a77e8 | ||
![]() |
3a12019b64 | ||
![]() |
6c2cf1ff60 | ||
![]() |
02ae0b5864 | ||
![]() |
85fe2213c1 | ||
![]() |
7dbc78f1d6 | ||
![]() |
f965a3504f | ||
![]() |
077f5efe7e | ||
![]() |
ef3bea71a0 | ||
![]() |
fcf655b0ec | ||
![]() |
b263b74916 | ||
![]() |
0f4b6b423a | ||
![]() |
72df585c5e | ||
![]() |
4698a63642 | ||
![]() |
6eb43a7d61 | ||
![]() |
af35b15400 | ||
![]() |
0d50d2664f | ||
![]() |
ff1159402e | ||
![]() |
f8742ae690 | ||
![]() |
c786d26542 | ||
![]() |
3f8ff94002 | ||
![]() |
64a968543b | ||
![]() |
aea98f702b | ||
![]() |
863ff622be | ||
![]() |
730cea6646 | ||
![]() |
7d1f8d618a | ||
![]() |
67b970fcaa | ||
![]() |
38bcdaa6f6 | ||
![]() |
8f1389de66 | ||
![]() |
37ac796c8f | ||
![]() |
716cd19d41 | ||
![]() |
173725f011 | ||
![]() |
ad561b885b | ||
![]() |
d77bdf4ac6 | ||
![]() |
ac3796ec31 | ||
![]() |
8c3fdfb6fb | ||
![]() |
b7c7d0b4b5 | ||
![]() |
8b0e6eed3a | ||
![]() |
603f884e8c | ||
![]() |
97dfccf4c7 | ||
![]() |
fd1e31c0cc | ||
![]() |
1de740e7b5 | ||
![]() |
5abfb90b16 | ||
![]() |
6b691063a8 | ||
![]() |
d1d746e7e6 | ||
![]() |
2fcb64d4a1 | ||
![]() |
3769f8c7c0 | ||
![]() |
f0a56e75f5 | ||
![]() |
15f33e1f19 | ||
![]() |
181122177b | ||
![]() |
684cd0f627 | ||
![]() |
277202e363 | ||
![]() |
b388d1fd42 | ||
![]() |
251e6399f5 | ||
![]() |
f44c5d7a63 | ||
![]() |
cae1ca52f0 | ||
![]() |
f8de2c64a5 | ||
![]() |
34ef5be720 | ||
![]() |
1402802031 | ||
![]() |
816989ab4d | ||
![]() |
d4497ca39c | ||
![]() |
6e39242ca3 | ||
![]() |
0197e32783 | ||
![]() |
87dfed4beb | ||
![]() |
dae991dc89 | ||
![]() |
6197e3483b | ||
![]() |
b2a6c8bd36 | ||
![]() |
938855e13c | ||
![]() |
a8712e3b8e | ||
![]() |
b15b577057 | ||
![]() |
653aeae3d8 | ||
![]() |
0aea6141ad | ||
![]() |
5243c1d871 | ||
![]() |
9449f5ad0a | ||
![]() |
c337bc5f97 | ||
![]() |
6aab60cf45 | ||
![]() |
52e9bc3213 | ||
![]() |
e48b2383cf | ||
![]() |
002a249777 | ||
![]() |
10498ce18d | ||
![]() |
6a5936b2b2 | ||
![]() |
dc68aaa803 | ||
![]() |
e7931ce049 | ||
![]() |
59b2582fe3 | ||
![]() |
8577b0721c | ||
![]() |
91319be855 | ||
![]() |
0dff538298 | ||
![]() |
6ac6d9c6eb | ||
![]() |
6ba0071296 | ||
![]() |
fef5dc4232 | ||
![]() |
ce58962dbb | ||
![]() |
9fb1e1d2ed | ||
![]() |
a29544c1e6 | ||
![]() |
b2b71edd04 | ||
![]() |
028472fc7b | ||
![]() |
b056ce228b | ||
![]() |
0cd4256c0e | ||
![]() |
e274c5b23f | ||
![]() |
ea57846465 | ||
![]() |
3f2e2bc659 | ||
![]() |
e3f2f66206 |
@@ -5,12 +5,15 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"remoteEnv": {
|
||||
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
|
22
.devcontainer/post_create.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will run after the container is created
|
||||
|
||||
# add github cli
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
|
||||
# Update package lists
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install necessary packages
|
||||
sudo apt-get install -y libpcap-dev gh
|
||||
|
||||
# Display a message
|
||||
echo "Post-create script has been executed successfully."
|
6
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
42
.vscode/tasks.json
vendored
@@ -1,6 +1,42 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Develop and serve Frontend",
|
||||
"type": "shell",
|
||||
"command": "script/develop_and_serve -c ${input:coreUrl}",
|
||||
// Sync changes here to other tasks until issue resolved
|
||||
// https://github.com/Microsoft/vscode/issues/61497
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Frontend",
|
||||
"type": "gulp",
|
||||
@@ -241,6 +277,12 @@
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
"description": "The URL of the Home Assistant Core instance",
|
||||
"default": "http://127.0.0.1:8123"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -90,6 +90,10 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
|
@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
@@ -1,11 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -15,17 +20,14 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
...compat.extends(
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
),
|
||||
export default tseslint.config(
|
||||
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.strict,
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -43,7 +45,7 @@ export default [
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
parser: tseslint.parser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -184,5 +186,5 @@ export default [
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
10
gallery/public/images/select_box/card.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="64" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
7
gallery/public/images/select_box/text_only.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 964 B |
@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
3
gallery/src/pages/components/ha-select-box.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Select box
|
||||
---
|
152
gallery/src/pages/components/ha-select-box.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-select-box";
|
||||
import type { SelectBoxOption } from "../../../../src/components/ha-select-box";
|
||||
|
||||
const basicOptions: SelectBoxOption[] = [
|
||||
{
|
||||
value: "text-only",
|
||||
label: "Text only",
|
||||
},
|
||||
{
|
||||
value: "card",
|
||||
label: "Card",
|
||||
},
|
||||
{
|
||||
value: "disabled",
|
||||
label: "Disabled option",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const fullOptions: SelectBoxOption[] = [
|
||||
{
|
||||
value: "text-only",
|
||||
label: "Text only",
|
||||
description: "Only text, no border and background",
|
||||
image: "/images/select_box/text_only.svg",
|
||||
},
|
||||
{
|
||||
value: "card",
|
||||
label: "Card",
|
||||
description: "With border and background",
|
||||
image: "/images/select_box/card.svg",
|
||||
},
|
||||
{
|
||||
value: "disabled",
|
||||
label: "Disabled",
|
||||
description: "Option that can not be selected",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const selects: {
|
||||
id: string;
|
||||
label: string;
|
||||
class?: string;
|
||||
options: SelectBoxOption[];
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
id: "basic",
|
||||
label: "Basic",
|
||||
options: basicOptions,
|
||||
},
|
||||
{
|
||||
id: "full",
|
||||
label: "With description and image",
|
||||
options: fullOptions,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-select-box")
|
||||
export class DemoHaSelectBox extends LitElement {
|
||||
@state() private value?: string = "off";
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.value = e.detail.value as string;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(selects, (select) => {
|
||||
const { id, label, options } = select;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<ha-select-box
|
||||
.value=${this.value}
|
||||
.options=${options}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-select-box>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Column layout</b></p>
|
||||
<div class="vertical-selects">
|
||||
${repeat(selects, (select) => {
|
||||
const { options } = select;
|
||||
return html`
|
||||
<ha-select-box
|
||||
.value=${this.value}
|
||||
.options=${options}
|
||||
.maxColumns=${1}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-select-box>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.custom {
|
||||
--mdc-icon-size: 24px;
|
||||
--control-select-color: var(--state-fan-active-color);
|
||||
--control-select-thickness: 130px;
|
||||
--control-select-border-radius: 36px;
|
||||
}
|
||||
|
||||
p.title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vertical-selects ha-select-box {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-select-box": DemoHaSelectBox;
|
||||
}
|
||||
}
|
@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
30
gallery/src/pages/components/ha-tooltip.markdown
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Tooltip
|
||||
---
|
||||
|
||||
A tooltip's target is its _first child element_, so you should only wrap one element inside of the tooltip. If you need the tooltip to show up for multiple elements, nest them inside a container first.
|
||||
|
||||
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
|
||||
|
||||
<ha-tooltip content="This is a tooltip">
|
||||
<ha-button>Hover Me</ha-button>
|
||||
</ha-tooltip>
|
||||
|
||||
```
|
||||
<ha-tooltip content="This is a tooltip">
|
||||
<ha-button>Hover Me</ha-button>
|
||||
</ha-tooltip>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
This element is based on sholace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
|
||||
|
||||
<a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a>
|
||||
|
||||
### HA style tokens
|
||||
|
||||
In your theme settings use this without the prefixed `--`.
|
||||
|
||||
- `--ha-tooltip-border-radius` (Default: 4px)
|
||||
- `--ha-tooltip-arrow-size` (Default: 8px)
|
2
gallery/src/pages/components/ha-tooltip.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-button";
|
@@ -32,6 +32,8 @@ const createConfigEntry = (
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
|
@@ -253,13 +253,9 @@ export class HassioBackups extends LitElement {
|
||||
"backup.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
id="delete-btn"
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
<simple-tooltip animation-delay="0" for="delete-btn">
|
||||
${this.supervisor.localize("backup.delete_selected")}
|
||||
</simple-tooltip>
|
||||
`}
|
||||
</div>
|
||||
</div> `
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
@@ -10,8 +8,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"backup-uploaded": { backup: HassioBackup };
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
|
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
@@ -19,13 +18,10 @@ import type {
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant, TranslationDict } from "../../../src/types";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
private _localize = (key: BackupOrRestoreKey) =>
|
||||
this.supervisor?.localize(`backup.${key}`) ||
|
||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this._localize("full_backup")
|
||||
: this._localize("partial_backup")}
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this._localize("name")}
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield .label=${this._localize("full_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("folders")}
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("addons")}
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this._localize("password_protection")}
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this._localize("password")}
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this._localize("confirm_password")}
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
|
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
|
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
@@ -43,7 +42,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -62,9 +61,13 @@ class HassioBackupDialog
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._localize("no_backup_found");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._localize("restore_no_home_assistant");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -82,13 +85,6 @@ class HassioBackupDialog
|
||||
return true;
|
||||
}
|
||||
|
||||
private _localize(key: BackupOrRestoreKey) {
|
||||
return (
|
||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
@@ -102,7 +98,7 @@ class HassioBackupDialog
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._localize("close")}
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
@@ -150,7 +146,6 @@ class HassioBackupDialog
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
@@ -161,7 +156,7 @@ class HassioBackupDialog
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._localize("restore")}
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
@@ -196,18 +191,22 @@ class HassioBackupDialog
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
),
|
||||
text: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
),
|
||||
confirmText: this._localize("restore"),
|
||||
dismissText: this._localize("cancel"),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
@@ -227,7 +226,8 @@ class HassioBackupDialog
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message || this._localize("restore_start_failed");
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ class HassioBackupDialog
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: this._localize("cancel"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._localize("unnamed_backup");
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiDelete, mdiDeleteOff } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -8,6 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
@@ -118,28 +118,27 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
<div>${repo.maintainer}</div>
|
||||
<div>${repo.url}</div>
|
||||
</div>
|
||||
<div class="delete" slot="end">
|
||||
<ha-icon-button
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
? mdiDeleteOff
|
||||
: mdiDelete}
|
||||
@click=${this._removeRepository}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<simple-tooltip
|
||||
animation-delay="0"
|
||||
position="bottom"
|
||||
offset="1"
|
||||
>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
usedRepositories.includes(repo.slug)
|
||||
? "dialog.repositories.used"
|
||||
: "dialog.repositories.remove"
|
||||
)}
|
||||
</simple-tooltip>
|
||||
</div>
|
||||
<ha-tooltip
|
||||
class="delete"
|
||||
slot="end"
|
||||
.content=${this._dialogParams!.supervisor.localize(
|
||||
usedRepositories.includes(repo.slug)
|
||||
? "dialog.repositories.used"
|
||||
: "dialog.repositories.remove"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<ha-icon-button
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
? mdiDeleteOff
|
||||
: mdiDelete}
|
||||
@click=${this._removeRepository}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</ha-tooltip>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import type { TranslationDict } from "../../../src/types";
|
||||
|
||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
@@ -22,6 +22,8 @@ import {
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { fileDownload } from "../../../src/util/file_download";
|
||||
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
|
||||
import { waitForSeconds } from "../../../src/common/util/wait";
|
||||
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
|
||||
|
||||
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
|
||||
declare global {
|
||||
@@ -216,7 +218,7 @@ class LandingPageLogs extends LitElement {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
// fallback to observerlogs if there is a problem with supervisor
|
||||
// fallback to observer logs if there is a problem with supervisor
|
||||
this._loadObserverLogs();
|
||||
}
|
||||
}
|
||||
@@ -251,6 +253,9 @@ class LandingPageLogs extends LitElement {
|
||||
|
||||
this._scheduleObserverLogs();
|
||||
} catch (err) {
|
||||
// wait because there is a moment where landingpage is down and core is not up yet
|
||||
await waitForSeconds(ASSUME_CORE_START_SECONDS);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._error = true;
|
||||
|
@@ -1,13 +1,7 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
type CSSResultGroup,
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
LocalizeFunc,
|
||||
@@ -16,33 +10,24 @@ import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
setSupervisorNetworkDns,
|
||||
type NetworkInfo,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
import type { NetworkInterface } from "../../../src/data/hassio/network";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("landing-page-network")
|
||||
class LandingPageNetwork extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
@property({ attribute: false }) public networkInfo?: NetworkInfo;
|
||||
|
||||
@state() private _getNetworkInfoError = false;
|
||||
|
||||
@state() private _dnsPrimaryInterfaceNameservers?: string;
|
||||
|
||||
@state() private _dnsPrimaryInterface?: string;
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
protected render() {
|
||||
if (!this._networkIssue && !this._getNetworkInfoError) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._getNetworkInfoError) {
|
||||
if (this.error) {
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
<p>${this.localize("network_issue.error_get_network_info")}</p>
|
||||
@@ -50,6 +35,16 @@ class LandingPageNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
let dnsPrimaryInterfaceNameservers: string | undefined;
|
||||
|
||||
const primaryInterface = this._getPrimaryInterface(
|
||||
this.networkInfo?.interfaces
|
||||
);
|
||||
if (primaryInterface) {
|
||||
dnsPrimaryInterfaceNameservers =
|
||||
this._getPrimaryNameservers(primaryInterface);
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
@@ -57,11 +52,11 @@ class LandingPageNetwork extends LitElement {
|
||||
>
|
||||
<p>
|
||||
${this.localize("network_issue.description", {
|
||||
dns: this._dnsPrimaryInterfaceNameservers || "?",
|
||||
dns: dnsPrimaryInterfaceNameservers || "?",
|
||||
})}
|
||||
</p>
|
||||
<p>${this.localize("network_issue.resolve_different")}</p>
|
||||
${!this._dnsPrimaryInterfaceNameservers
|
||||
${!dnsPrimaryInterfaceNameservers
|
||||
? html`
|
||||
<p>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
@@ -73,7 +68,7 @@ class LandingPageNetwork extends LitElement {
|
||||
({ translationKey }, key) =>
|
||||
html`<ha-button
|
||||
.index=${key}
|
||||
.disabled=${!this._dnsPrimaryInterfaceNameservers}
|
||||
.disabled=${!dnsPrimaryInterfaceNameservers}
|
||||
@click=${this._setDns}
|
||||
>${this.localize(translationKey)}</ha-button
|
||||
>`
|
||||
@@ -83,76 +78,40 @@ class LandingPageNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this._fetchSupervisorInfo();
|
||||
}
|
||||
private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) =>
|
||||
interfaces?.find((intf) => intf.primary && intf.enabled)
|
||||
);
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
setTimeout(
|
||||
() => this._fetchSupervisorInfo(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchSupervisorInfo() {
|
||||
let data;
|
||||
try {
|
||||
const response = await getSupervisorNetworkInfo();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch network info");
|
||||
}
|
||||
|
||||
({ data } = await response.json());
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._getNetworkInfoError = true;
|
||||
this._dnsPrimaryInterfaceNameservers = undefined;
|
||||
this._dnsPrimaryInterface = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._getNetworkInfoError = false;
|
||||
|
||||
const primaryInterface = data.interfaces.find(
|
||||
(intf) => intf.primary && intf.enabled
|
||||
);
|
||||
if (primaryInterface) {
|
||||
this._dnsPrimaryInterfaceNameservers = [
|
||||
private _getPrimaryNameservers = memoizeOne(
|
||||
(primaryInterface: NetworkInterface) =>
|
||||
[
|
||||
...(primaryInterface.ipv4?.nameservers || []),
|
||||
...(primaryInterface.ipv6?.nameservers || []),
|
||||
].join(", ");
|
||||
|
||||
this._dnsPrimaryInterface = primaryInterface.interface;
|
||||
} else {
|
||||
this._dnsPrimaryInterfaceNameservers = undefined;
|
||||
this._dnsPrimaryInterface = undefined;
|
||||
}
|
||||
|
||||
if (!data.host_internet) {
|
||||
this._networkIssue = true;
|
||||
} else {
|
||||
this._networkIssue = false;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._networkIssue,
|
||||
});
|
||||
this._scheduleFetchSupervisorInfo();
|
||||
}
|
||||
].join(", ")
|
||||
);
|
||||
|
||||
private async _setDns(ev) {
|
||||
const primaryInterface = this._getPrimaryInterface(
|
||||
this.networkInfo?.interfaces
|
||||
);
|
||||
|
||||
const index = ev.target?.index;
|
||||
try {
|
||||
const dnsPrimaryInterface = primaryInterface?.interface;
|
||||
if (!dnsPrimaryInterface) {
|
||||
throw new Error("No primary interface found");
|
||||
}
|
||||
|
||||
const response = await setSupervisorNetworkDns(
|
||||
index,
|
||||
this._dnsPrimaryInterface!
|
||||
dnsPrimaryInterface
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to set DNS");
|
||||
}
|
||||
this._networkIssue = false;
|
||||
|
||||
// notify landing page to trigger a network info reload
|
||||
fireEvent(this, "dns-set");
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
@@ -183,4 +142,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"landing-page-network": LandingPageNetwork;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"dns-set": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,17 @@
|
||||
import type { LandingPageKeys } from "../../../src/common/translations/localize";
|
||||
import type { HassioResponse } from "../../../src/data/hassio/common";
|
||||
import type {
|
||||
DockerNetwork,
|
||||
NetworkInterface,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import { handleFetchPromise } from "../../../src/util/hass-call-api";
|
||||
|
||||
export interface NetworkInfo {
|
||||
interfaces: NetworkInterface[];
|
||||
docker: DockerNetwork;
|
||||
host_internet: boolean;
|
||||
supervisor_internet: boolean;
|
||||
}
|
||||
|
||||
export const ALTERNATIVE_DNS_SERVERS: {
|
||||
ipv4: string[];
|
||||
@@ -18,7 +31,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
|
||||
];
|
||||
|
||||
export async function getSupervisorLogs(lines = 100) {
|
||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
@@ -26,22 +39,29 @@ export async function getSupervisorLogs(lines = 100) {
|
||||
}
|
||||
|
||||
export async function getSupervisorLogsFollow(lines = 500) {
|
||||
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo() {
|
||||
return fetch("/supervisor/network/info");
|
||||
export async function pingSupervisor() {
|
||||
return fetch("/supervisor-api/supervisor/ping");
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
|
||||
const responseData = await handleFetchPromise<HassioResponse<NetworkInfo>>(
|
||||
fetch("/supervisor-api/network/info")
|
||||
);
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
) =>
|
||||
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
|
||||
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ipv4: {
|
||||
|
@@ -10,36 +10,56 @@ import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
import {
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
} from "./data/supervisor";
|
||||
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 5;
|
||||
export const ASSUME_CORE_START_SECONDS = 30;
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@property({ attribute: false }) public translationFragment = "landing-page";
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
|
||||
@state() private _supervisorError = false;
|
||||
|
||||
@state() private _networkInfo?: NetworkInfo;
|
||||
|
||||
@state() private _coreStatusChecked = false;
|
||||
|
||||
@state() private _networkInfoError = false;
|
||||
|
||||
@state() private _coreCheckActive = false;
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
render() {
|
||||
const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h1>${this.localize("header")}</h1>
|
||||
${!this._networkIssue && !this._supervisorError
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<mwc-linear-progress indeterminate></mwc-linear-progress>
|
||||
`
|
||||
: nothing}
|
||||
<landing-page-network
|
||||
@value-changed=${this._networkInfoChanged}
|
||||
.localize=${this.localize}
|
||||
></landing-page-network>
|
||||
|
||||
${networkIssue || this._networkInfoError
|
||||
? html`
|
||||
<landing-page-network
|
||||
.localize=${this.localize}
|
||||
.networkInfo=${this._networkInfo}
|
||||
.error=${this._networkInfoError}
|
||||
@dns-set=${this._fetchSupervisorInfo}
|
||||
></landing-page-network>
|
||||
`
|
||||
: nothing}
|
||||
${this._supervisorError
|
||||
? html`
|
||||
<ha-alert
|
||||
@@ -88,24 +108,66 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
}
|
||||
import("../../src/components/ha-language-picker");
|
||||
|
||||
this._scheduleCoreCheck();
|
||||
this._fetchSupervisorInfo(true);
|
||||
}
|
||||
|
||||
private _scheduleCoreCheck() {
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
setTimeout(
|
||||
() => this._checkCoreAvailability(),
|
||||
SCHEDULE_CORE_CHECK_SECONDS * 1000
|
||||
() => this._fetchSupervisorInfo(true),
|
||||
// on assumed core start check every second, otherwise every 5 seconds
|
||||
(this._coreCheckActive
|
||||
? SCHEDULE_CORE_CHECK_SECONDS
|
||||
: SCHEDULE_FETCH_NETWORK_INFO_SECONDS) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleTurnOffCoreCheck() {
|
||||
setTimeout(() => {
|
||||
this._coreCheckActive = false;
|
||||
}, ASSUME_CORE_START_SECONDS * 1000);
|
||||
}
|
||||
|
||||
private async _fetchSupervisorInfo(schedule = false) {
|
||||
try {
|
||||
const response = await pingSupervisor();
|
||||
if (!response.ok) {
|
||||
throw new Error("ping-failed");
|
||||
}
|
||||
|
||||
this._networkInfo = await getSupervisorNetworkInfo();
|
||||
this._networkInfoError = false;
|
||||
this._coreStatusChecked = false;
|
||||
} catch (err: any) {
|
||||
if (!this._coreStatusChecked) {
|
||||
// wait before show errors, because we assume that core is starting
|
||||
this._coreCheckActive = true;
|
||||
this._scheduleTurnOffCoreCheck();
|
||||
}
|
||||
await this._checkCoreAvailability();
|
||||
|
||||
// assume supervisor update if ping fails -> don't show an error
|
||||
if (!this._coreCheckActive && err.message !== "ping-failed") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._networkInfoError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule) {
|
||||
this._scheduleFetchSupervisorInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkCoreAvailability() {
|
||||
try {
|
||||
const response = await fetch("/manifest.json");
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error("Failed to fetch manifest");
|
||||
}
|
||||
} finally {
|
||||
this._scheduleCoreCheck();
|
||||
} catch (_err) {
|
||||
this._coreStatusChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
this._supervisorError = true;
|
||||
}
|
||||
|
||||
private _networkInfoChanged(ev: CustomEvent) {
|
||||
this._networkIssue = ev.detail.value;
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
const language = ev.detail.value;
|
||||
if (language !== this.language && language) {
|
||||
|
91
package.json
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.5.1",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
"@formatjs/intl-displaynames": "6.8.9",
|
||||
"@formatjs/intl-durationformat": "0.7.2",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@formatjs/intl-listformat": "7.7.9",
|
||||
"@formatjs/intl-locale": "4.2.9",
|
||||
"@formatjs/intl-numberformat": "8.15.2",
|
||||
"@formatjs/intl-pluralrules": "5.4.2",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.9",
|
||||
"@formatjs/intl-listformat": "7.7.10",
|
||||
"@formatjs/intl-locale": "4.2.10",
|
||||
"@formatjs/intl-numberformat": "8.15.3",
|
||||
"@formatjs/intl-pluralrules": "5.4.3",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.10",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -53,10 +53,9 @@
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.15",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
@@ -90,15 +89,16 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.0",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.2",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.2",
|
||||
"@vaadin/combo-box": "24.6.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "2.3.1",
|
||||
"barcode-detector": "3.0.0",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.40.0",
|
||||
@@ -110,22 +110,24 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"element-internals-polyfill": "1.3.13",
|
||||
"fuse.js": "7.0.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.14",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.6",
|
||||
"marked": "15.0.7",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -137,7 +139,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.0",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -152,22 +154,22 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@lokalise/node-api": "13.2.1",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@rspack/cli": "1.2.5",
|
||||
"@rspack/core": "1.2.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.20",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -175,6 +177,7 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
@@ -183,14 +186,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
"@typescript-eslint/parser": "8.21.0",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.19.0",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -198,7 +199,7 @@
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"eslint-plugin-wc": "2.2.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
@@ -215,16 +216,18 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.4",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.6",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -238,7 +241,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.14.0",
|
||||
"globals": "16.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
10
public/static/images/form/markdown_card.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="64" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="63" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
10
public/static/images/form/markdown_card_dark.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="94" height="64" viewBox="0 0 94 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V56C94 60.4183 90.4183 64 86 64H8C3.58172 64 0 60.4183 0 56V8Z" fill="#1C1C1C"/>
|
||||
<path d="M0.5 8C0.5 3.85786 3.85786 0.5 8 0.5H86C90.1421 0.5 93.5 3.85786 93.5 8V56C93.5 60.1421 90.1421 63.5 86 63.5H8C3.85786 63.5 0.5 60.1421 0.5 56V8Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H31C32.6569 24 34 25.3431 34 27V29C34 30.6569 32.6569 32 31 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M38 27C38 25.3431 39.3431 24 41 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H41C39.3431 32 38 30.6569 38 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 39C8 37.3431 9.34315 36 11 36H53C54.6569 36 56 37.3431 56 39V41C56 42.6569 54.6569 44 53 44H11C9.34315 44 8 42.6569 8 41V39Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M60 39C60 37.3431 61.3431 36 63 36H83C84.6569 36 86 37.3431 86 39V41C86 42.6569 84.6569 44 83 44H63C61.3431 44 60 42.6569 60 41V39Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 51C8 49.3431 9.34315 48 11 48H31C32.6569 48 34 49.3431 34 51V53C34 54.6569 32.6569 56 31 56H11C9.34315 56 8 54.6569 8 53V51Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
7
public/static/images/form/markdown_text_only.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 964 B |
7
public/static/images/form/markdown_text_only_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11C0 9.34315 1.34315 8 3 8H23C24.6569 8 26 9.34315 26 11V13C26 14.6569 24.6569 16 23 16H3C1.34315 16 0 14.6569 0 13V11Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M30 11C30 9.34315 31.3431 8 33 8H91C92.6569 8 94 9.34315 94 11V13C94 14.6569 92.6569 16 91 16H33C31.3431 16 30 14.6569 30 13V11Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 23C0 21.3431 1.34315 20 3 20H61C62.6569 20 64 21.3431 64 23V25C64 26.6569 62.6569 28 61 28H3C1.34315 28 0 26.6569 0 25V23Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M68 23C68 21.3431 69.3431 20 71 20H91C92.6569 20 94 21.3431 94 23V25C94 26.6569 92.6569 28 91 28H71C69.3431 28 68 26.6569 68 25V23Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 35C0 33.3431 1.34315 32 3 32H23C24.6569 32 26 33.3431 26 35V37C26 38.6569 24.6569 40 23 40H3C1.34315 40 0 38.6569 0 37V35Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 964 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="40" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<circle cx="20" cy="20" r="12" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 652 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="40" viewBox="0 0 94 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="40" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="39" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<circle cx="20" cy="20" r="12" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M40 14C40 10.6863 42.6863 8 46 8H65C68.3137 8 71 10.6863 71 14C71 17.3137 68.3137 20 65 20H46C42.6863 20 40 17.3137 40 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M40 28C40 25.7909 41.7909 24 44 24H77C79.2091 24 81 25.7909 81 28C81 30.2091 79.2091 32 77 32H44C41.7909 32 40 30.2091 40 28Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 654 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="72" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<circle cx="47" cy="20" r="12" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 699 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="72" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="71" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<circle cx="47" cy="20" r="12" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M31.5 46C31.5 42.6863 34.1863 40 37.5 40H56.5C59.8137 40 62.5 42.6863 62.5 46C62.5 49.3137 59.8137 52 56.5 52H37.5C34.1863 52 31.5 49.3137 31.5 46Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M26.5 60C26.5 57.7909 28.2909 56 30.5 56H63.5C65.7091 56 67.5 57.7909 67.5 60C67.5 62.2091 65.7091 64 63.5 64H30.5C28.2909 64 26.5 62.2091 26.5 60Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 701 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="48" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="28" width="78" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="48" viewBox="0 0 94 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="48" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="47" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="28" width="78" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="51" y="8" width="35" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="51" y="8" width="35" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
|
||||
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H83C84.6569 24 86 25.3431 86 27V29C86 30.6569 84.6569 32 83 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 44C8 46.2091 9.79086 48 12 48H16C18.2091 48 20 46.2091 20 44C20 41.7909 18.2091 40 16 40H12C9.79086 40 8 41.7909 8 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M24.5 44C24.5 46.2091 26.2909 48 28.5 48H32.5C34.7091 48 36.5 46.2091 36.5 44C36.5 41.7909 34.7091 40 32.5 40H28.5C26.2909 40 24.5 41.7909 24.5 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M41 44C41 46.2091 42.7909 48 45 48H49C51.2091 48 53 46.2091 53 44C53 41.7909 51.2091 40 49 40H45C42.7909 40 41 41.7909 41 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M57.5 44C57.5 46.2091 59.2909 48 61.5 48H65.5C67.7091 48 69.5 46.2091 69.5 44C69.5 41.7909 67.7091 40 65.5 40H61.5C59.2909 40 57.5 41.7909 57.5 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M74 44C74 46.2091 75.7909 48 78 48H82C84.2091 48 86 46.2091 86 44C86 41.7909 84.2091 40 82 40H78C75.7909 40 74 41.7909 74 44Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
|
||||
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M8 12C8 14.2091 9.79086 16 12 16H16C18.2091 16 20 14.2091 20 12C20 9.79086 18.2091 8 16 8H12C9.79086 8 8 9.79086 8 12Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M24.5 12C24.5 14.2091 26.2909 16 28.5 16H32.5C34.7091 16 36.5 14.2091 36.5 12C36.5 9.79086 34.7091 8 32.5 8H28.5C26.2909 8 24.5 9.79086 24.5 12Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M41 12C41 14.2091 42.7909 16 45 16H49C51.2091 16 53 14.2091 53 12C53 9.79086 51.2091 8 49 8H45C42.7909 8 41 9.79086 41 12Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M57.5 12C57.5 14.2091 59.2909 16 61.5 16H65.5C67.7091 16 69.5 14.2091 69.5 12C69.5 9.79086 67.7091 8 65.5 8H61.5C59.2909 8 57.5 9.79086 57.5 12Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M74 12C74 14.2091 75.7909 16 78 16H82C84.2091 16 86 14.2091 86 12C86 9.79086 84.2091 8 82 8H78C75.7909 8 74 9.79086 74 12Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M8 30C8 26.6863 10.6863 24 14 24H33C36.3137 24 39 26.6863 39 30C39 33.3137 36.3137 36 33 36H14C10.6863 36 8 33.3137 8 30Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 43C8 41.3431 9.34315 40 11 40H83C84.6569 40 86 41.3431 86 43V45C86 46.6569 84.6569 48 83 48H11C9.34315 48 8 46.6569 8 45V43Z" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
11
public/static/images/form/view_header_layout_center.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M25 44C25 41.7909 26.7909 40 29 40H33C35.2091 40 37 41.7909 37 44C37 46.2091 35.2091 48 33 48H29C26.7909 48 25 46.2091 25 44Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M41 44C41 41.7909 42.7909 40 45 40H49C51.2091 40 53 41.7909 53 44C53 46.2091 51.2091 48 49 48H45C42.7909 48 41 46.2091 41 44Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M57 44C57 41.7909 58.7909 40 61 40H65C67.2091 40 69 41.7909 69 44C69 46.2091 67.2091 48 65 48H61C58.7909 48 57 46.2091 57 44Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M73 44C73 41.7909 74.7909 40 77 40H81C83.2091 40 85 41.7909 85 44C85 46.2091 83.2091 48 81 48H77C74.7909 48 73 46.2091 73 44Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
11
public/static/images/form/view_header_layout_center_dark.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
|
||||
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M31.5 14C31.5 10.6863 34.1863 8 37.5 8H56.5C59.8137 8 62.5 10.6863 62.5 14C62.5 17.3137 59.8137 20 56.5 20H37.5C34.1863 20 31.5 17.3137 31.5 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M23 27C23 25.3431 24.3431 24 26 24H68C69.6569 24 71 25.3431 71 27V29C71 30.6569 69.6569 32 68 32H26C24.3431 32 23 30.6569 23 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M9 44C9 41.7909 10.7909 40 13 40H17C19.2091 40 21 41.7909 21 44C21 46.2091 19.2091 48 17 48H13C10.7909 48 9 46.2091 9 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="25" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="41" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="57" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="73" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
24
public/static/images/form/view_header_layout_responsive.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_887_2968)">
|
||||
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="white"/>
|
||||
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<rect x="8" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="24" y="47" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="59" width="12" height="8" rx="4" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="white"/>
|
||||
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="white"/>
|
||||
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="black" fill-opacity="0.32"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_887_2968">
|
||||
<rect width="94" height="72" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 39H55V64C55 68.4183 51.4183 72 47 72H8C3.58172 72 0 68.4183 0 64V39Z" fill="#1C1C1C"/>
|
||||
<path d="M1.34748 68.4449C0.772837 67.5866 0.359906 66.6109 0.152272 65.5613L0.642766 65.4643C0.549158 64.9911 0.5 64.5015 0.5 64V61.9167H0V57.75H0.5V53.5833H0V49.4167H0.5V45.25H0V41.0833H0.5V39.5H1.96429V39H5.89286V39.5H9.82143V39H13.75V39.5H17.6786V39H21.6071V39.5H25.5357V39H29.4643V39.5H33.3929V39H37.3214V39.5H41.25V39H45.1786V39.5H49.1071V39H53.0357V39.5H54.5V41.0833H55V45.25H54.5V49.4167H55V53.5833H54.5V57.75H55V61.9167H54.5V64C54.5 64.5015 54.4508 64.9911 54.3572 65.4643L54.8477 65.5613C54.6401 66.6109 54.2272 67.5866 53.6525 68.4449L53.237 68.1668C52.6893 68.9849 51.9849 69.6893 51.1668 70.237L51.4449 70.6525C50.5866 71.2272 49.6109 71.6401 48.5613 71.8477L48.4643 71.3572C47.9911 71.4508 47.5015 71.5 47 71.5H45.05V72H41.15V71.5H37.25V72H33.35V71.5H29.45V72H25.55V71.5H21.65V72H17.75V71.5H13.85V72H9.95V71.5H8C7.49847 71.5 7.00892 71.4508 6.53574 71.3572L6.4387 71.8477C5.38915 71.6401 4.41341 71.2272 3.55507 70.6525L3.83323 70.237C3.01513 69.6893 2.31067 68.9849 1.76296 68.1668L1.34748 68.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M8 51C8 48.7909 9.79086 47 12 47H16C18.2091 47 20 48.7909 20 51C20 53.2091 18.2091 55 16 55H12C9.79086 55 8 53.2091 8 51Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M24 51C24 48.7909 25.7909 47 28 47H32C34.2091 47 36 48.7909 36 51C36 53.2091 34.2091 55 32 55H28C25.7909 55 24 53.2091 24 51Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 63C8 60.7909 9.79086 59 12 59H16C18.2091 59 20 60.7909 20 63C20 65.2091 18.2091 67 16 67H12C9.79086 67 8 65.2091 8 63Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M54 0H86C90.4183 0 94 3.58172 94 8V32C94 36.4183 90.4183 40 86 40H54V0Z" fill="#1C1C1C"/>
|
||||
<path d="M84 39.5V40H80V39.5H76V40H72V39.5H68V40H64V39.5H60V40H56V39.5H54.5V38H54V34H54.5V30H54V26H54.5V22H54V18H54.5V14H54V10H54.5V6H54V2H54.5V0.5H56V0H60V0.5H64V0H68V0.5H72V0H76V0.5H80V0H84V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152272C88.6109 0.359906 89.5866 0.772836 90.4449 1.34748L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V32C93.5 32.5015 93.4508 32.9911 93.3572 33.4643L93.8477 33.5613C93.6401 34.6109 93.2272 35.5866 92.6525 36.4449L92.237 36.1668C91.6893 36.9849 90.9849 37.6893 90.1668 38.237L90.4449 38.6525C89.5866 39.2272 88.6109 39.6401 87.5613 39.8477L87.4643 39.3572C86.9911 39.4508 86.5015 39.5 86 39.5H84Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M58 28C58 30.2091 59.7909 32 62 32H66C68.2091 32 70 30.2091 70 28C70 25.7909 68.2091 24 66 24H62C59.7909 24 58 25.7909 58 28Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M74 28C74 30.2091 75.7909 32 78 32H82C84.2091 32 86 30.2091 86 28C86 25.7909 84.2091 24 82 24H78C75.7909 24 74 25.7909 74 28Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M74 16C74 18.2091 75.7909 20 78 20H82C84.2091 20 86 18.2091 86 16C86 13.7909 84.2091 12 82 12H78C75.7909 12 74 13.7909 74 16Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H55V40H0V8Z" fill="#1C1C1C"/>
|
||||
<path d="M3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.95833V0H13.875V0.5H17.7917V0H21.7083V0.5H25.625V0H29.5417V0.5H33.4583V0H37.375V0.5H41.2917V0H45.2083V0.5H49.125V0H53.0417V0.5H54.5V2H55V6H54.5V10H55V14H54.5V18H55V22H54.5V26H55V30H54.5V34H55V38H54.5V39.5H53.0357V40H49.1071V39.5H45.1786V40H41.25V39.5H37.3214V40H33.3929V39.5H29.4643V40H25.5357V39.5H21.6071V40H17.6786V39.5H13.75V40H9.82143V39.5H5.89286V40H1.96429V39.5H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H44C45.6569 24 47 25.3431 47 27V29C47 30.6569 45.6569 32 44 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M79 48V54.5C79 58.09 76.09 61 72.5 61H66.83L69.92 64.09L68.5 65.5L63 60L68.5 54.5L69.91 55.91L66.83 59H72.5C75 59 77 57 77 54.5V48H79Z" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
9
public/static/images/form/view_header_layout_start.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="56" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="55" rx="7.5" stroke="black" stroke-opacity="0.12" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M24 44C24 41.7909 25.7909 40 28 40H32C34.2091 40 36 41.7909 36 44C36 46.2091 34.2091 48 32 48H28C25.7909 48 24 46.2091 24 44Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M40 44C40 41.7909 41.7909 40 44 40H48C50.2091 40 52 41.7909 52 44C52 46.2091 50.2091 48 48 48H44C41.7909 48 40 46.2091 40 44Z" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
11
public/static/images/form/view_header_layout_start_dark.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="94" height="56" viewBox="0 0 94 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H86C90.4183 0 94 3.58172 94 8V48C94 52.4183 90.4183 56 86 56H8C3.58172 56 0 52.4183 0 48V8Z" fill="#1C1C1C"/>
|
||||
<path d="M1.34748 52.4449C0.772837 51.5866 0.359906 50.6109 0.152272 49.5613L0.642766 49.4643C0.549158 48.9911 0.5 48.5015 0.5 48V46H0V42H0.5V38H0V34H0.5V30H0V26H0.5V22H0V18H0.5V14H0V10H0.5V8C0.5 7.49847 0.549158 7.00892 0.642766 6.53574L0.152272 6.4387C0.359906 5.38915 0.772837 4.41341 1.34748 3.55508L1.76296 3.83324C2.31067 3.01513 3.01513 2.31067 3.83323 1.76296L3.55507 1.34748C4.41341 0.772837 5.38915 0.359906 6.4387 0.152272L6.53574 0.642766C7.00892 0.549158 7.49847 0.5 8 0.5H9.94999V0H13.85V0.5H17.75V0H21.65V0.5H25.55V0H29.45V0.5H33.35V0H37.25V0.5H41.15V0H45.05V0.5H48.95V0H52.85V0.5H56.75V0H60.65V0.5H64.55V0H68.45V0.5H72.35V0H76.25V0.5H80.15V0H84.05V0.5H86C86.5015 0.5 86.9911 0.549158 87.4643 0.642766L87.5613 0.152273C88.6108 0.359907 89.5866 0.772837 90.4449 1.34747L90.1668 1.76296C90.9849 2.31067 91.6893 3.01513 92.237 3.83323L92.6525 3.55507C93.2272 4.41341 93.6401 5.38915 93.8477 6.4387L93.3572 6.53574C93.4508 7.00892 93.5 7.49847 93.5 8V10H94V14H93.5V18H94V22H93.5V26H94V30H93.5V34H94V38H93.5V42H94V46H93.5V48C93.5 48.5015 93.4508 48.9911 93.3572 49.4643L93.8477 49.5613C93.6401 50.6109 93.2272 51.5866 92.6525 52.4449L92.237 52.1668C91.6893 52.9849 90.9849 53.6893 90.1668 54.237L90.4449 54.6525C89.5866 55.2272 88.6108 55.6401 87.5613 55.8477L87.4643 55.3572C86.9911 55.4508 86.5015 55.5 86 55.5H84.05V56H80.15V55.5H76.25V56H72.35V55.5H68.45V56H64.55V55.5H60.65V56H56.75V55.5H52.85V56H48.95V55.5H45.05V56H41.15V55.5H37.25V56H33.35V55.5H29.45V56H25.55V55.5H21.65V56H17.75V55.5H13.85V56H9.95V55.5H8C7.49847 55.5 7.00892 55.4508 6.53574 55.3572L6.4387 55.8477C5.38915 55.6401 4.41341 55.2272 3.55508 54.6525L3.83323 54.237C3.01513 53.6893 2.31067 52.9849 1.76296 52.1668L1.34748 52.4449Z" stroke="white" stroke-opacity="0.24" stroke-dasharray="4 4"/>
|
||||
<path d="M8 14C8 10.6863 10.6863 8 14 8H33C36.3137 8 39 10.6863 39 14C39 17.3137 36.3137 20 33 20H14C10.6863 20 8 17.3137 8 14Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M8 27C8 25.3431 9.34315 24 11 24H53C54.6569 24 56 25.3431 56 27V29C56 30.6569 54.6569 32 53 32H11C9.34315 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M8 44C8 41.7909 9.79086 40 12 40H16C18.2091 40 20 41.7909 20 44C20 46.2091 18.2091 48 16 48H12C9.79086 48 8 46.2091 8 44Z" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="24" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="56" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="72" y="40" width="12" height="8" rx="4" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250129.0"
|
||||
version = "20250306.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
|
||||
HASS_URL="$coreUrl" ./script/develop &
|
||||
|
||||
# serve the frontend
|
||||
yarn dlx serve -l $frontendPort ./hass_frontend -s &
|
||||
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
|
||||
|
||||
# keep the script running while serving
|
||||
wait
|
||||
|
3
script/serve-config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cleanUrls": false
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
@@ -74,3 +75,12 @@ export function getGraphColorByIndex(
|
||||
getColorByIndex(index);
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
export const getAllGraphColors = memoizeOne(
|
||||
(style: CSSStyleDeclaration) =>
|
||||
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
|
||||
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
|
||||
// this is not ideal, but we need to memoize the colors
|
||||
newArgs[0].getPropertyValue("--graph-color-1") ===
|
||||
lastArgs[0].getPropertyValue("--graph-color-1")
|
||||
);
|
||||
|
@@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor];
|
||||
if (!rgbFromColorName) {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
return rgb2hex(rgbFromColorName);
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
}
|
||||
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
}
|
||||
|
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
|
||||
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
|
||||
|
||||
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (
|
||||
dateObj: Date,
|
||||
|
@@ -16,11 +16,22 @@ export const setupLeafletMap = async (
|
||||
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
await import("leaflet.markercluster");
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
|
||||
const markerClusterStyle = document.createElement("link");
|
||||
markerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.css"
|
||||
);
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
|
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
return value;
|
||||
}
|
||||
|
||||
if (domain === "datetime") {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stringCompare } from "../string/compare";
|
||||
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
@@ -237,6 +240,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
@@ -269,7 +273,19 @@ export const getStates = (
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push("home", "not_home");
|
||||
result.push(
|
||||
...Object.entries(hass.states)
|
||||
.filter(
|
||||
([entityId, stateObj]) =>
|
||||
computeDomain(entityId) === "zone" &&
|
||||
entityId !== "zone.home" &&
|
||||
stateObj.attributes.friendly_name
|
||||
)
|
||||
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
|
||||
.sort((zone1, zone2) =>
|
||||
stringCompare(zone1, zone2, hass.locale.language)
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
|
32
src/common/map/decorated_marker.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
|
||||
import { Marker } from "leaflet";
|
||||
|
||||
export class DecoratedMarker extends Marker {
|
||||
decorationLayer: Layer | undefined;
|
||||
|
||||
constructor(
|
||||
latlng: LatLngExpression,
|
||||
decorationLayer?: Layer,
|
||||
options?: MarkerOptions
|
||||
) {
|
||||
super(latlng, options);
|
||||
|
||||
this.decorationLayer = decorationLayer;
|
||||
}
|
||||
|
||||
onAdd(map: Map) {
|
||||
super.onAdd(map);
|
||||
|
||||
// If decoration has been provided, add it to the map as well
|
||||
this.decorationLayer?.addTo(map);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemove(map: Map) {
|
||||
// If decoration has been provided, remove it from the map as well
|
||||
this.decorationLayer?.remove();
|
||||
|
||||
return super.onRemove(map);
|
||||
}
|
||||
}
|
6
src/common/util/wait.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const waitForMs = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);
|
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { XAXisOption } from "echarts/types/dist/shared";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
formatDateMonth,
|
||||
@@ -7,56 +6,46 @@ import {
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
} from "../../common/datetime/format_date";
|
||||
import { formatTime } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
export function getLabelFormatter(
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
dayDifference = 0
|
||||
minutesDifference: number
|
||||
) {
|
||||
return (value: number | Date) => {
|
||||
const date = new Date(value);
|
||||
if (dayDifference > 88) {
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
return date.getDate() === 1
|
||||
? `{bold|${formatDateVeryShort(date, locale, config)}}`
|
||||
: formatDateVeryShort(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 7) {
|
||||
const label = formatDateVeryShort(date, locale, config);
|
||||
return date.getDate() === 1 ? `{bold|${label}}` : label;
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
return formatDateWeekdayShort(date, locale, config);
|
||||
}
|
||||
const dayDifference = minutesDifference / 60 / 24;
|
||||
const date = new Date(value);
|
||||
if (dayDifference > 88) {
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
return date.getDate() === 1
|
||||
? `{bold|${formatDateVeryShort(date, locale, config)}}`
|
||||
: formatDateVeryShort(date, locale, config);
|
||||
}
|
||||
if (dayDifference > 7) {
|
||||
const label = formatDateVeryShort(date, locale, config);
|
||||
return date.getDate() === 1 ? `{bold|${label}}` : label;
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
return formatDateWeekdayShort(date, locale, config);
|
||||
}
|
||||
if (minutesDifference && minutesDifference < 5) {
|
||||
return formatTimeWithSeconds(date, locale, config);
|
||||
}
|
||||
if (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0
|
||||
) {
|
||||
// show only date for the beginning of the day
|
||||
if (
|
||||
date.getHours() === 0 &&
|
||||
date.getMinutes() === 0 &&
|
||||
date.getSeconds() === 0
|
||||
) {
|
||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||
}
|
||||
return formatTime(date, locale, config);
|
||||
};
|
||||
}
|
||||
|
||||
export function getTimeAxisLabelConfig(
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
dayDifference?: number
|
||||
): XAXisOption["axisLabel"] {
|
||||
return {
|
||||
formatter: getLabelFormatter(locale, config, dayDifference),
|
||||
rich: {
|
||||
bold: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
hideOverlap: true,
|
||||
};
|
||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||
}
|
||||
return formatTime(date, locale, config);
|
||||
}
|
||||
|
@@ -1,27 +1,37 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../ha-icon-button";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import { themesContext } from "../../data/context";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../chips/ha-assist-chip";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
|
||||
@customElement("ha-chart-base")
|
||||
export class HaChartBase extends LitElement {
|
||||
@@ -35,8 +45,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@property({ attribute: "external-hidden", type: Boolean })
|
||||
public externalHidden = false;
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: false }) public extraComponents?: any[];
|
||||
|
||||
@state()
|
||||
@consume({ context: themesContext, subscribe: true })
|
||||
@@ -44,10 +56,18 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
@state() private _zoomRatio = 1;
|
||||
|
||||
@state() private _minutesDifference = 24 * 60;
|
||||
|
||||
@state() private _hiddenDatasets = new Set<string>();
|
||||
|
||||
private _modifierPressed = false;
|
||||
|
||||
private _isTouchDevice = "ontouchstart" in window;
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => this.chart?.resize(),
|
||||
@@ -59,12 +79,16 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
this.chart?.dispose();
|
||||
this.chart = undefined;
|
||||
this._originalZrFlush = undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -75,31 +99,46 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
this._listeners.push(
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
this._reducedMotion = matches;
|
||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
this._setChartOptions({ animation: !this._reducedMotion });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
|
||||
if (
|
||||
!this._modifierPressed &&
|
||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||
) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
// drag to zoom
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
key: "dataZoomSelect",
|
||||
dataZoomSelectActive: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
if (
|
||||
this._modifierPressed &&
|
||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||
) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
this.chart?.dispatchAction({
|
||||
type: "takeGlobalCursor",
|
||||
key: "dataZoomSelect",
|
||||
dataZoomSelectActive: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -116,49 +155,42 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_themes")) {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
this.chart.setOption(
|
||||
{ series: this.data },
|
||||
{ lazyUpdate: true, replaceMerge: ["series"] }
|
||||
);
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
|
||||
chartOptions.series = this._getSeries();
|
||||
}
|
||||
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
|
||||
this.chart.setOption(this._createOptions(), {
|
||||
lazyUpdate: true,
|
||||
// if we replace the whole object, it will reset the dataZoom
|
||||
replaceMerge: [
|
||||
"xAxis",
|
||||
"yAxis",
|
||||
"dataZoom",
|
||||
"dataset",
|
||||
"tooltip",
|
||||
"legend",
|
||||
"grid",
|
||||
"visualMap",
|
||||
],
|
||||
});
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||
}
|
||||
if (Object.keys(chartOptions).length > 0) {
|
||||
this._setChartOptions(chartOptions);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="chart-container"
|
||||
style=${styleMap({
|
||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
@wheel=${this._handleWheel}
|
||||
class="container ${classMap({ "has-height": !!this.height })}"
|
||||
style=${styleMap({ height: this.height })}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
<div
|
||||
class="chart-container"
|
||||
style=${styleMap({
|
||||
height: this.height ? undefined : `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
</div>
|
||||
${this._renderLegend()}
|
||||
${this._isZoomed
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -173,6 +205,84 @@ export class HaChartBase extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return nothing;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
|
||||
if (!legend.show) {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items = (legend.data ||
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => d.name ?? d.id) ||
|
||||
[]) as string[];
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
const overflowLimit = isMobile
|
||||
? LEGEND_OVERFLOW_LIMIT_MOBILE
|
||||
: LEGEND_OVERFLOW_LIMIT;
|
||||
return html`<div class="chart-legend">
|
||||
<ul>
|
||||
${items.map((item: string, index: number) => {
|
||||
if (!this.expandLegend && index >= overflowLimit) {
|
||||
return nothing;
|
||||
}
|
||||
const dataset = datasets.find(
|
||||
(d) => d.id === item || d.name === item
|
||||
);
|
||||
const color = dataset?.color as string;
|
||||
const borderColor = dataset?.itemStyle?.borderColor as string;
|
||||
return html`<li
|
||||
.name=${item}
|
||||
@click=${this._legendClick}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
|
||||
.title=${item}
|
||||
>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: color,
|
||||
borderColor: borderColor || color,
|
||||
})}
|
||||
></div>
|
||||
<div class="label">${item}</div>
|
||||
</li>`;
|
||||
})}
|
||||
${items.length > overflowLimit
|
||||
? html`<li>
|
||||
<ha-assist-chip
|
||||
@click=${this._toggleExpandedLegend}
|
||||
filled
|
||||
label=${this.expandLegend
|
||||
? this.hass.localize(
|
||||
"ui.components.history_charts.collapse_legend"
|
||||
)
|
||||
: `${this.hass.localize("ui.components.history_charts.expand_legend")} (${items.length - overflowLimit})`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${this.expandLegend ? mdiChevronUp : mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
</li>`
|
||||
: nothing}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _formatTimeLabel = (value: number | Date) =>
|
||||
formatTimeLabel(
|
||||
value,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._minutesDifference * this._zoomRatio
|
||||
);
|
||||
|
||||
private async _setupChart() {
|
||||
if (this._loading) return;
|
||||
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
|
||||
@@ -183,45 +293,64 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
const echarts = (await import("../../resources/echarts")).default;
|
||||
|
||||
this.chart = echarts.init(
|
||||
container,
|
||||
this._themes.darkMode ? "dark" : "light"
|
||||
);
|
||||
this.chart.on("legendselectchanged", (params: any) => {
|
||||
if (this.externalHidden) {
|
||||
const isSelected = params.selected[params.name];
|
||||
if (isSelected) {
|
||||
fireEvent(this, "dataset-unhidden", { name: params.name });
|
||||
} else {
|
||||
fireEvent(this, "dataset-hidden", { name: params.name });
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.extraComponents?.length) {
|
||||
echarts.use(this.extraComponents);
|
||||
}
|
||||
|
||||
echarts.registerTheme("custom", this._createTheme());
|
||||
|
||||
this.chart = echarts.init(container, "custom");
|
||||
this.chart.on("datazoom", (e: any) => {
|
||||
const { start, end } = e.batch?.[0] ?? e;
|
||||
this._isZoomed = start !== 0 || end !== 100;
|
||||
this._zoomRatio = (end - start) / 100;
|
||||
});
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
this.chart.on("mousemove", (e: ECElementEvent) => {
|
||||
if (e.componentType === "series" && e.componentSubType === "custom") {
|
||||
// custom series do not support cursor style so we need to set it manually
|
||||
this.chart?.getZr()?.setCursorStyle("default");
|
||||
this.chart.getZr().on("dblclick", this._handleClickZoom);
|
||||
if (this._isTouchDevice) {
|
||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||
if (!e.zrByTouch) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._lastTapTime &&
|
||||
Date.now() - this._lastTapTime < DOUBLE_TAP_TIME
|
||||
) {
|
||||
this._handleClickZoom(e);
|
||||
} else {
|
||||
this._lastTapTime = Date.now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._hiddenDatasets.add(stat);
|
||||
}
|
||||
});
|
||||
this.chart.setOption({ ...this._createOptions(), series: this.data });
|
||||
|
||||
this.chart.setOption({
|
||||
...this._createOptions(),
|
||||
series: this._getSeries(),
|
||||
});
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
|
||||
const xAxis = (this.options?.xAxis?.[0] ??
|
||||
this.options?.xAxis) as XAXisOption;
|
||||
const yAxis = (this.options?.yAxis?.[0] ??
|
||||
this.options?.yAxis) as YAXisOption;
|
||||
if (xAxis.type === "value" && yAxis.type === "category") {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
if (xAxis?.type === "value" && yAxis?.type === "category") {
|
||||
// vertical data zoom doesn't work well in this case and horizontal is pointless
|
||||
return undefined;
|
||||
}
|
||||
@@ -230,31 +359,66 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
moveOnMouseMove: this._isZoomed,
|
||||
preventDefaultMouseMove: this._isZoomed,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
};
|
||||
}
|
||||
|
||||
private _createOptions(): ECOption {
|
||||
const darkMode = this._themes.darkMode ?? false;
|
||||
|
||||
let xAxis = this.options?.xAxis;
|
||||
if (xAxis) {
|
||||
xAxis = Array.isArray(xAxis) ? xAxis : [xAxis];
|
||||
xAxis = xAxis.map((axis: XAXisOption) => {
|
||||
if (axis.type !== "time" || axis.show === false) {
|
||||
return axis;
|
||||
}
|
||||
if (axis.max && axis.min) {
|
||||
this._minutesDifference = differenceInMinutes(
|
||||
axis.max as Date,
|
||||
axis.min as Date
|
||||
);
|
||||
}
|
||||
const dayDifference = this._minutesDifference / 60 / 24;
|
||||
let minInterval: number | undefined;
|
||||
if (dayDifference) {
|
||||
minInterval =
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined;
|
||||
}
|
||||
return {
|
||||
axisLine: { show: false },
|
||||
splitLine: { show: true },
|
||||
...axis,
|
||||
axisLabel: {
|
||||
formatter: this._formatTimeLabel,
|
||||
rich: { bold: { fontWeight: "bold" } },
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
const options = {
|
||||
backgroundColor: "transparent",
|
||||
animation: !this._reducedMotion,
|
||||
darkMode,
|
||||
aria: {
|
||||
show: true,
|
||||
},
|
||||
darkMode: this._themes.darkMode ?? false,
|
||||
aria: { show: true },
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
toolbox: {
|
||||
top: Infinity,
|
||||
left: Infinity,
|
||||
feature: {
|
||||
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
|
||||
},
|
||||
iconStyle: { opacity: 0 },
|
||||
},
|
||||
...this.options,
|
||||
legend: this.options?.legend
|
||||
? {
|
||||
// we should create our own theme but this is a quick fix for now
|
||||
inactiveColor: darkMode ? "#444" : "#ccc",
|
||||
...this.options.legend,
|
||||
}
|
||||
: undefined,
|
||||
legend: { show: false },
|
||||
xAxis,
|
||||
};
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
@@ -268,48 +432,267 @@ export class HaChartBase extends LitElement {
|
||||
tooltips.forEach((tooltip) => {
|
||||
tooltip.confine = true;
|
||||
tooltip.appendTo = undefined;
|
||||
tooltip.triggerOn = "click";
|
||||
});
|
||||
options.tooltip = tooltips;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private _getDefaultHeight() {
|
||||
return Math.max(this.clientWidth / 2, 400);
|
||||
private _createTheme() {
|
||||
const style = getComputedStyle(this);
|
||||
return {
|
||||
color: getAllGraphColors(style),
|
||||
backgroundColor: "transparent",
|
||||
textStyle: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
fontFamily: "Roboto, Noto, sans-serif",
|
||||
},
|
||||
title: {
|
||||
textStyle: { color: style.getPropertyValue("--primary-text-color") },
|
||||
subtextStyle: {
|
||||
color: style.getPropertyValue("--secondary-text-color"),
|
||||
},
|
||||
},
|
||||
line: {
|
||||
lineStyle: { width: 1.5 },
|
||||
symbolSize: 1,
|
||||
symbol: "circle",
|
||||
smooth: false,
|
||||
},
|
||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||
categoryAxis: {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
valueAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
logAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
timeAxis: {
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
},
|
||||
splitArea: {
|
||||
show: false,
|
||||
areaStyle: {
|
||||
color: [
|
||||
style.getPropertyValue("--divider-color") + "3F",
|
||||
style.getPropertyValue("--divider-color") + "7F",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
textStyle: { color: style.getPropertyValue("--primary-text-color") },
|
||||
inactiveColor: style.getPropertyValue("--disabled-text-color"),
|
||||
pageIconColor: style.getPropertyValue("--primary-text-color"),
|
||||
pageIconInactiveColor: style.getPropertyValue("--disabled-text-color"),
|
||||
pageTextStyle: {
|
||||
color: style.getPropertyValue("--secondary-text-color"),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: style.getPropertyValue("--card-background-color"),
|
||||
borderColor: style.getPropertyValue("--divider-color"),
|
||||
textStyle: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
fontSize: 12,
|
||||
},
|
||||
axisPointer: {
|
||||
lineStyle: { color: style.getPropertyValue("--info-color") },
|
||||
crossStyle: { color: style.getPropertyValue("--info-color") },
|
||||
},
|
||||
},
|
||||
timeline: {},
|
||||
};
|
||||
}
|
||||
|
||||
private _getSeries() {
|
||||
if (!Array.isArray(this.data)) {
|
||||
return this.data;
|
||||
}
|
||||
return this.data.filter(
|
||||
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
|
||||
);
|
||||
}
|
||||
|
||||
private _getDefaultHeight() {
|
||||
return Math.max(this.clientWidth / 2, 200);
|
||||
}
|
||||
|
||||
private _setChartOptions(options: ECOption) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (!this._originalZrFlush) {
|
||||
const dataSize = ensureArray(this.data).reduce(
|
||||
(acc, series) => acc + ((series.data as any[]) || []).length,
|
||||
0
|
||||
);
|
||||
if (dataSize > 10000) {
|
||||
// delay the last bit of the render to avoid blocking the main thread
|
||||
// this is not that impactful with sampling enabled but it doesn't hurt to have it
|
||||
const zr = this.chart.getZr();
|
||||
this._originalZrFlush = zr.flush;
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.call(zr);
|
||||
}, 5);
|
||||
};
|
||||
}
|
||||
}
|
||||
const replaceMerge = options.series ? ["series"] : [];
|
||||
this.chart.setOption(options, { replaceMerge });
|
||||
}
|
||||
|
||||
private _handleClickZoom = (e: ECElementEvent) => {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const range = this._isZoomed
|
||||
? [0, 100]
|
||||
: [
|
||||
(e.offsetX / this.chart.getWidth()) * 100 - 15,
|
||||
(e.offsetX / this.chart.getWidth()) * 100 + 15,
|
||||
];
|
||||
this.chart.dispatchAction({
|
||||
type: "dataZoom",
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
});
|
||||
};
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
}
|
||||
|
||||
private _handleWheel(e: WheelEvent) {
|
||||
// if the window is not focused, we don't receive the keydown events but scroll still works
|
||||
if (!this.options?.dataZoom) {
|
||||
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
|
||||
if (modifierPressed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (modifierPressed !== this._modifierPressed) {
|
||||
this._modifierPressed = modifierPressed;
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
private _legendClick(ev: any) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const name = ev.currentTarget?.name;
|
||||
if (this._hiddenDatasets.has(name)) {
|
||||
this._hiddenDatasets.delete(name);
|
||||
fireEvent(this, "dataset-unhidden", { name });
|
||||
} else {
|
||||
this._hiddenDatasets.add(name);
|
||||
fireEvent(this, "dataset-hidden", { name });
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _toggleExpandedLegend() {
|
||||
this.expandLegend = !this.expandLegend;
|
||||
setTimeout(() => {
|
||||
this.chart?.resize();
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.container.has-height {
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
}
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.zoom-reset {
|
||||
position: absolute;
|
||||
@@ -321,6 +704,67 @@ export class HaChartBase extends LitElement {
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
.chart-legend {
|
||||
max-height: 60%;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.chart-legend ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.chart-legend li {
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
box-sizing: border-box;
|
||||
max-width: 220px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chart-legend .hidden {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.chart-legend .label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chart-legend .bullet {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.chart-legend .hidden .bullet {
|
||||
border-color: var(--secondary-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
ha-assist-chip {
|
||||
height: 100%;
|
||||
--_label-text-weight: 500;
|
||||
--_leading-space: 8px;
|
||||
--_trailing-space: 8px;
|
||||
--_icon-label-space: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,17 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LitElement, html, css, svg, nothing } from "lit";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { CallbackDataParams } from "echarts/types/dist/shared";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import { SankeyChart } from "echarts/charts";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import "./ha-chart-base";
|
||||
import { NODE_SIZE } from "../trace/hat-graph-const";
|
||||
import "../ha-alert";
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
@@ -25,34 +33,14 @@ export interface SankeyChartData {
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
type ProcessedNode = Node & {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
};
|
||||
|
||||
type ProcessedLink = Link & {
|
||||
value: number;
|
||||
offset: {
|
||||
source: number;
|
||||
target: number;
|
||||
};
|
||||
passThroughNodeIds: string[];
|
||||
};
|
||||
|
||||
interface Section {
|
||||
nodes: ProcessedNode[];
|
||||
offset: number;
|
||||
index: number;
|
||||
totalValue: number;
|
||||
statePerPixel: number;
|
||||
}
|
||||
|
||||
const MIN_SIZE = 3;
|
||||
const DEFAULT_COLOR = "var(--primary-color)";
|
||||
const NODE_WIDTH = 15;
|
||||
const OVERFLOW_MARGIN = 5;
|
||||
const FONT_SIZE = 12;
|
||||
const MIN_DISTANCE = FONT_SIZE / 2;
|
||||
const NODE_GAP = 8;
|
||||
const LABEL_DISTANCE = 5;
|
||||
|
||||
@customElement("ha-sankey-chart")
|
||||
export class HaSankeyChart extends LitElement {
|
||||
@@ -65,141 +53,144 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ attribute: false }) public loadingText?: string;
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
private _statePerPixel = 0;
|
||||
public chart?: EChartsType;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
@state() private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect,
|
||||
});
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
willUpdate() {
|
||||
this._statePerPixel = 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._sizeController.value) {
|
||||
return this.loadingText ?? nothing;
|
||||
}
|
||||
const options = {
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
|
||||
const { width, height } = this._sizeController.value;
|
||||
const { nodes, paths } = this._processNodesAndPaths(
|
||||
this.data.nodes,
|
||||
this.data.links
|
||||
);
|
||||
|
||||
return html`
|
||||
<svg
|
||||
width=${width}
|
||||
height=${height}
|
||||
viewBox="0 0 ${width} ${height}"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<defs>
|
||||
${paths.map(
|
||||
(path, i) => svg`
|
||||
<linearGradient id="gradient${path.sourceNode.id}.${path.targetNode.id}.${i}" gradientTransform="${
|
||||
this.vertical ? "rotate(90)" : ""
|
||||
}">
|
||||
<stop offset="0%" stop-color="${path.sourceNode.color}"></stop>
|
||||
<stop offset="100%" stop-color="${path.targetNode.color}"></stop>
|
||||
</linearGradient>
|
||||
`
|
||||
)}
|
||||
</defs>
|
||||
${paths.map(
|
||||
(path, i) =>
|
||||
svg`
|
||||
<path d="${path.path.map(([cmd, x, y]) => `${cmd}${x},${y}`).join(" ")} Z"
|
||||
fill="url(#gradient${path.sourceNode.id}.${path.targetNode.id}.${i})" fill-opacity="0.4" />
|
||||
`
|
||||
)}
|
||||
${nodes.map((node) =>
|
||||
node.passThrough
|
||||
? nothing
|
||||
: svg`
|
||||
<g transform="translate(${node.x},${node.y})">
|
||||
<rect
|
||||
class="node"
|
||||
width=${this.vertical ? node.size : NODE_WIDTH}
|
||||
height=${this.vertical ? NODE_WIDTH : node.size}
|
||||
style="fill: ${node.color}"
|
||||
>
|
||||
<title>${node.tooltip}</title>
|
||||
</rect>
|
||||
${
|
||||
this.vertical
|
||||
? nothing
|
||||
: svg`
|
||||
<text
|
||||
class="node-label"
|
||||
x=${NODE_WIDTH + 5}
|
||||
y=${node.size / 2}
|
||||
text-anchor="start"
|
||||
dominant-baseline="middle"
|
||||
>${node.label}</text>
|
||||
`
|
||||
}
|
||||
</g>
|
||||
`
|
||||
)}
|
||||
</svg>
|
||||
${this.vertical
|
||||
? nodes.map((node) => {
|
||||
if (!node.label) {
|
||||
return nothing;
|
||||
}
|
||||
const labelWidth = MIN_DISTANCE + node.size;
|
||||
const fontSize = this._getVerticalLabelFontSize(
|
||||
node.label,
|
||||
labelWidth
|
||||
);
|
||||
return html`<div
|
||||
class="node-label vertical"
|
||||
style="
|
||||
left: ${node.x - MIN_DISTANCE / 2}px;
|
||||
top: ${node.y + NODE_WIDTH}px;
|
||||
width: ${labelWidth}px;
|
||||
height: ${FONT_SIZE * 3}px;
|
||||
font-size: ${fontSize}px;
|
||||
line-height: ${fontSize}px;
|
||||
"
|
||||
title=${node.label}
|
||||
>
|
||||
${node.label}
|
||||
</div>`;
|
||||
})
|
||||
: nothing}
|
||||
`;
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data, this._sizeController.value?.width)}
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
private _processNodesAndPaths = memoizeOne(
|
||||
(rawNodes: Node[], rawLinks: Link[]) => {
|
||||
const filteredNodes = rawNodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
|
||||
const { links, passThroughNodes } = this._processLinks(
|
||||
filteredNodes,
|
||||
indexes,
|
||||
rawLinks
|
||||
);
|
||||
const nodes = this._processNodes(
|
||||
[...filteredNodes, ...passThroughNodes],
|
||||
indexes
|
||||
);
|
||||
const paths = this._processPaths(nodes, links);
|
||||
return { nodes, paths };
|
||||
private _renderTooltip = (params: CallbackDataParams) => {
|
||||
const data = params.data as Record<string, any>;
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
|
||||
}
|
||||
);
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return `${source?.label ?? data.source} → ${target?.label ?? data.target}<br>${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private _processLinks(nodes: Node[], indexes: number[], rawLinks: Link[]) {
|
||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
|
||||
const links = this._processLinks(filteredNodes, data.links);
|
||||
const sectionWidth = width / indexes.length;
|
||||
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
|
||||
|
||||
return {
|
||||
id: "sankey",
|
||||
type: "sankey",
|
||||
nodes: filteredNodes.map((node) => ({
|
||||
id: node.id,
|
||||
value: node.value,
|
||||
itemStyle: {
|
||||
color: node.color,
|
||||
},
|
||||
depth: node.index,
|
||||
})),
|
||||
links,
|
||||
draggable: false,
|
||||
orient: this.vertical ? "vertical" : "horizontal",
|
||||
nodeWidth: 15,
|
||||
nodeGap: NODE_GAP,
|
||||
lineStyle: {
|
||||
color: "gradient",
|
||||
opacity: 0.4,
|
||||
},
|
||||
layoutIterations: 0,
|
||||
label: {
|
||||
formatter: (params) =>
|
||||
data.nodes.find((node) => node.id === (params.data as Node).id)
|
||||
?.label ?? (params.data as Node).id,
|
||||
position: this.vertical ? "bottom" : "right",
|
||||
distance: LABEL_DISTANCE,
|
||||
minMargin: 5,
|
||||
overflow: "break",
|
||||
},
|
||||
labelLayout: (params) => {
|
||||
if (this.vertical) {
|
||||
// reduce the label font size so the longest word fits on one line
|
||||
const longestWord = params.text
|
||||
.split(" ")
|
||||
.reduce(
|
||||
(longest, current) =>
|
||||
longest.length > current.length ? longest : current,
|
||||
""
|
||||
);
|
||||
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
|
||||
const fontSize = Math.min(
|
||||
FONT_SIZE,
|
||||
(params.rect.width / wordWidth) * FONT_SIZE
|
||||
);
|
||||
return {
|
||||
fontSize: fontSize > 1 ? fontSize : 0,
|
||||
width: params.rect.width,
|
||||
align: "center",
|
||||
};
|
||||
}
|
||||
|
||||
// estimate the number of lines after the label is wrapped
|
||||
// this is a very rough estimate, but it works for now
|
||||
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
|
||||
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
|
||||
const fontSize = Math.min(
|
||||
(params.rect.height / lineCount) * FONT_SIZE,
|
||||
FONT_SIZE
|
||||
);
|
||||
return {
|
||||
fontSize,
|
||||
lineHeight: fontSize,
|
||||
width: labelSpace,
|
||||
height: params.rect.height,
|
||||
};
|
||||
},
|
||||
top: this.vertical ? 0 : OVERFLOW_MARGIN,
|
||||
bottom: this.vertical ? 25 : OVERFLOW_MARGIN,
|
||||
left: this.vertical ? OVERFLOW_MARGIN : 0,
|
||||
right: this.vertical ? OVERFLOW_MARGIN : labelSpace + LABEL_DISTANCE,
|
||||
emphasis: {
|
||||
focus: "adjacency",
|
||||
},
|
||||
} as SankeySeriesOption;
|
||||
});
|
||||
|
||||
private _processLinks(nodes: Node[], rawLinks: Link[]) {
|
||||
const accountedIn = new Map<string, number>();
|
||||
const accountedOut = new Map<string, number>();
|
||||
const links: ProcessedLink[] = [];
|
||||
const passThroughNodes: Node[] = [];
|
||||
rawLinks.forEach((link) => {
|
||||
const sourceNode = nodes.find((n) => n.id === link.source);
|
||||
const targetNode = nodes.find((n) => n.id === link.target);
|
||||
@@ -222,307 +213,25 @@ export class HaSankeyChart extends LitElement {
|
||||
accountedIn.set(targetNode.id, targetAccounted + value);
|
||||
accountedOut.set(sourceNode.id, sourceAccounted + value);
|
||||
|
||||
// handle links across sections
|
||||
const sourceIndex = indexes.findIndex((i) => i === sourceNode.index);
|
||||
const targetIndex = indexes.findIndex((i) => i === targetNode.index);
|
||||
const passThroughSections = indexes.slice(sourceIndex + 1, targetIndex);
|
||||
// create pass-through nodes to reserve space
|
||||
const passThroughNodeIds = passThroughSections.map((index) => {
|
||||
const node = {
|
||||
passThrough: true,
|
||||
id: `${sourceNode.id}-${targetNode.id}-${index}`,
|
||||
value,
|
||||
index,
|
||||
};
|
||||
passThroughNodes.push(node);
|
||||
return node.id;
|
||||
});
|
||||
|
||||
if (value > 0) {
|
||||
links.push({
|
||||
...link,
|
||||
value,
|
||||
offset: {
|
||||
source: sourceAccounted / (sourceNode.value || 1),
|
||||
target: targetAccounted / (targetNode.value || 1),
|
||||
},
|
||||
passThroughNodeIds,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { links, passThroughNodes };
|
||||
}
|
||||
|
||||
private _processNodes(filteredNodes: Node[], indexes: number[]) {
|
||||
// add MIN_DISTANCE as padding
|
||||
const sectionSize = this.vertical
|
||||
? this._sizeController.value!.width - MIN_DISTANCE * 2
|
||||
: this._sizeController.value!.height - MIN_DISTANCE * 2;
|
||||
|
||||
const nodesPerSection: Record<number, Node[]> = {};
|
||||
filteredNodes.forEach((node) => {
|
||||
if (!nodesPerSection[node.index]) {
|
||||
nodesPerSection[node.index] = [node];
|
||||
} else {
|
||||
nodesPerSection[node.index].push(node);
|
||||
}
|
||||
});
|
||||
|
||||
const sectionFlexSize = this._getSectionFlexSize(
|
||||
Object.values(nodesPerSection)
|
||||
);
|
||||
|
||||
const sections: Section[] = indexes.map((index, i) => {
|
||||
const nodes: ProcessedNode[] = nodesPerSection[index].map(
|
||||
(node: Node) => ({
|
||||
...node,
|
||||
color: node.color || DEFAULT_COLOR,
|
||||
x: 0,
|
||||
y: 0,
|
||||
size: 0,
|
||||
})
|
||||
);
|
||||
const availableSpace =
|
||||
sectionSize - (nodes.length * MIN_DISTANCE - MIN_DISTANCE);
|
||||
const totalValue = nodes.reduce(
|
||||
(acc: number, node: Node) => acc + node.value,
|
||||
0
|
||||
);
|
||||
const { nodes: sizedNodes, statePerPixel } = this._setNodeSizes(
|
||||
nodes,
|
||||
availableSpace,
|
||||
totalValue
|
||||
);
|
||||
return {
|
||||
nodes: sizedNodes,
|
||||
offset: sectionFlexSize * i,
|
||||
index,
|
||||
totalValue,
|
||||
statePerPixel,
|
||||
};
|
||||
});
|
||||
|
||||
sections.forEach((section) => {
|
||||
// calc sizes again with the best statePerPixel
|
||||
let totalSize = 0;
|
||||
if (section.statePerPixel !== this._statePerPixel) {
|
||||
section.nodes.forEach((node) => {
|
||||
const size = Math.max(
|
||||
MIN_SIZE,
|
||||
Math.floor(node.value / this._statePerPixel)
|
||||
);
|
||||
totalSize += size;
|
||||
node.size = size;
|
||||
});
|
||||
} else {
|
||||
totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0);
|
||||
}
|
||||
// calc margin between boxes
|
||||
const emptySpace = sectionSize - totalSize;
|
||||
const spacerSize = emptySpace / (section.nodes.length - 1);
|
||||
|
||||
// account for MIN_DISTANCE padding and center single node sections
|
||||
let offset =
|
||||
section.nodes.length > 1 ? MIN_DISTANCE : emptySpace / 2 + MIN_DISTANCE;
|
||||
// calc positions - swap x/y for vertical layout
|
||||
section.nodes.forEach((node) => {
|
||||
if (this.vertical) {
|
||||
node.x = offset;
|
||||
node.y = section.offset;
|
||||
} else {
|
||||
node.x = section.offset;
|
||||
node.y = offset;
|
||||
}
|
||||
offset += node.size + spacerSize;
|
||||
});
|
||||
});
|
||||
|
||||
return sections.flatMap((section) => section.nodes);
|
||||
}
|
||||
|
||||
private _processPaths(nodes: ProcessedNode[], links: ProcessedLink[]) {
|
||||
const flowDirection = this.vertical ? "y" : "x";
|
||||
const orthDirection = this.vertical ? "x" : "y"; // orthogonal to the flow
|
||||
const nodesById = new Map(nodes.map((n) => [n.id, n]));
|
||||
return links.map((link) => {
|
||||
const { source, target, value, offset, passThroughNodeIds } = link;
|
||||
const pathNodes = [source, ...passThroughNodeIds, target].map(
|
||||
(id) => nodesById.get(id)!
|
||||
);
|
||||
const offsets = [
|
||||
offset.source,
|
||||
...link.passThroughNodeIds.map(() => 0),
|
||||
offset.target,
|
||||
];
|
||||
|
||||
const sourceNode = pathNodes[0];
|
||||
const targetNode = pathNodes[pathNodes.length - 1];
|
||||
|
||||
let path: [string, number, number][] = [
|
||||
[
|
||||
"M",
|
||||
sourceNode[flowDirection] + NODE_WIDTH,
|
||||
sourceNode[orthDirection] + offset.source * sourceNode.size,
|
||||
],
|
||||
]; // starting point
|
||||
|
||||
// traverse the path forwards. stop before the last node
|
||||
for (let i = 0; i < pathNodes.length - 1; i++) {
|
||||
const node = pathNodes[i];
|
||||
const nextNode = pathNodes[i + 1];
|
||||
const flowMiddle =
|
||||
(nextNode[flowDirection] - node[flowDirection]) / 2 +
|
||||
node[flowDirection];
|
||||
const orthStart = node[orthDirection] + offsets[i] * node.size;
|
||||
const orthEnd =
|
||||
nextNode[orthDirection] + offsets[i + 1] * nextNode.size;
|
||||
path.push(
|
||||
["L", node[flowDirection] + NODE_WIDTH, orthStart],
|
||||
["C", flowMiddle, orthStart],
|
||||
["", flowMiddle, orthEnd],
|
||||
["", nextNode[flowDirection], orthEnd]
|
||||
);
|
||||
}
|
||||
// traverse the path backwards. stop before the first node
|
||||
for (let i = pathNodes.length - 1; i > 0; i--) {
|
||||
const node = pathNodes[i];
|
||||
const prevNode = pathNodes[i - 1];
|
||||
const flowMiddle =
|
||||
(node[flowDirection] - prevNode[flowDirection]) / 2 +
|
||||
prevNode[flowDirection];
|
||||
const orthStart =
|
||||
node[orthDirection] +
|
||||
offsets[i] * node.size +
|
||||
Math.max((value / (node.value || 1)) * node.size, 0);
|
||||
const orthEnd =
|
||||
prevNode[orthDirection] +
|
||||
offsets[i - 1] * prevNode.size +
|
||||
Math.max((value / (prevNode.value || 1)) * prevNode.size, 0);
|
||||
path.push(
|
||||
["L", node[flowDirection], orthStart],
|
||||
["C", flowMiddle, orthStart],
|
||||
["", flowMiddle, orthEnd],
|
||||
["", prevNode[flowDirection] + NODE_WIDTH, orthEnd]
|
||||
);
|
||||
}
|
||||
|
||||
if (this.vertical) {
|
||||
// Just swap x and y coordinates for vertical layout
|
||||
path = path.map((c) => [c[0], c[2], c[1]]);
|
||||
}
|
||||
return {
|
||||
sourceNode,
|
||||
targetNode,
|
||||
value,
|
||||
path,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _setNodeSizes(
|
||||
nodes: ProcessedNode[],
|
||||
availableSpace: number,
|
||||
totalValue: number
|
||||
): { nodes: ProcessedNode[]; statePerPixel: number } {
|
||||
const statePerPixel = totalValue / availableSpace;
|
||||
if (statePerPixel > this._statePerPixel) {
|
||||
this._statePerPixel = statePerPixel;
|
||||
}
|
||||
let deficitHeight = 0;
|
||||
const result = nodes.map((node) => {
|
||||
if (node.size === MIN_SIZE) {
|
||||
return node;
|
||||
}
|
||||
let size = Math.floor(node.value / this._statePerPixel);
|
||||
if (size < MIN_SIZE) {
|
||||
deficitHeight += MIN_SIZE - size;
|
||||
size = MIN_SIZE;
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
size,
|
||||
};
|
||||
});
|
||||
if (deficitHeight > 0) {
|
||||
return this._setNodeSizes(
|
||||
result,
|
||||
availableSpace - deficitHeight,
|
||||
totalValue
|
||||
);
|
||||
}
|
||||
return { nodes: result, statePerPixel: this._statePerPixel };
|
||||
}
|
||||
|
||||
private _getSectionFlexSize(nodesPerSection: Node[][]): number {
|
||||
const fullSize = this.vertical
|
||||
? this._sizeController.value!.height
|
||||
: this._sizeController.value!.width;
|
||||
if (nodesPerSection.length < 2) {
|
||||
return fullSize;
|
||||
}
|
||||
let lastSectionFlexSize: number;
|
||||
if (this.vertical) {
|
||||
lastSectionFlexSize = FONT_SIZE * 2 + NODE_WIDTH; // estimated based on the font size + some margin
|
||||
} else {
|
||||
// Estimate the width needed for the last section based on label length
|
||||
const lastIndex = nodesPerSection.length - 1;
|
||||
const lastSectionNodes = nodesPerSection[lastIndex];
|
||||
const TEXT_PADDING = 5; // Padding between node and text
|
||||
lastSectionFlexSize =
|
||||
lastSectionNodes.length > 0
|
||||
? Math.max(
|
||||
...lastSectionNodes.map(
|
||||
(node) =>
|
||||
NODE_WIDTH +
|
||||
TEXT_PADDING +
|
||||
(node.label ? measureTextWidth(node.label, FONT_SIZE) : 0)
|
||||
)
|
||||
)
|
||||
: 0;
|
||||
}
|
||||
// Calculate the flex size for other sections
|
||||
const remainingSize = fullSize - lastSectionFlexSize;
|
||||
const flexSize = remainingSize / (nodesPerSection.length - 1);
|
||||
// if the last section is bigger than the others, we make them all the same size
|
||||
// this is to prevent the last section from squishing the others
|
||||
return lastSectionFlexSize < flexSize
|
||||
? flexSize
|
||||
: fullSize / nodesPerSection.length;
|
||||
}
|
||||
|
||||
private _getVerticalLabelFontSize(label: string, labelWidth: number): number {
|
||||
// reduce the label font size so the longest word fits on one line
|
||||
const longestWord = label
|
||||
.split(" ")
|
||||
.reduce(
|
||||
(longest, current) =>
|
||||
longest.length > current.length ? longest : current,
|
||||
""
|
||||
);
|
||||
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
|
||||
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
|
||||
return links;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
background: var(--ha-card-background, var(--card-background-color, #000));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--ha-card-background, var(--card-background-color));
|
||||
}
|
||||
svg {
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
}
|
||||
.node-label {
|
||||
font-size: ${FONT_SIZE}px;
|
||||
fill: var(--primary-text-color, white);
|
||||
}
|
||||
.node-label.vertical {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
ha-chart-base {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ import { property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -18,10 +17,10 @@ import {
|
||||
getNumberFormatOptions,
|
||||
formatNumber,
|
||||
} from "../../common/number/format_number";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -64,6 +63,9 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _entityIds: string[] = [];
|
||||
@@ -72,8 +74,12 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
@state() private _visualMap?: VisualMapComponentOption[];
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -84,49 +90,104 @@ export class StateHistoryChartLine extends LitElement {
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
.expandLegend=${this.expandLegend}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip(params) {
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
let value = `${formatNumber(
|
||||
param.value[1] as number,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._entityIds[param.seriesIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
this._hiddenStats.has(dataset.name as string)
|
||||
)
|
||||
return;
|
||||
const param = params.find(
|
||||
(p: Record<string, any>) => p.seriesIndex === index
|
||||
);
|
||||
if (param) {
|
||||
datapoints.push(param);
|
||||
return;
|
||||
}
|
||||
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||
let lastData: any;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
if (point && point[0] <= time && point[1]) {
|
||||
lastData = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lastData) return;
|
||||
datapoints.push({
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
// HTML copied from echarts. May change based on options
|
||||
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
return (
|
||||
title +
|
||||
datapoints
|
||||
.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
let value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
||||
`;
|
||||
})
|
||||
.join("<br>");
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
this._hiddenStats.delete(ev.detail.name);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
@@ -152,58 +213,67 @@ export class StateHistoryChartLine extends LitElement {
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("_chartData") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_visualMap") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
||||
const rtl = computeRTL(this.hass);
|
||||
const splitLineStyle = this.hass.themes?.darkMode
|
||||
? { opacity: 0.15 }
|
||||
: {};
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: this.startTime,
|
||||
max: this.endTime,
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
},
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
min: this.fitYData ? this.minYAxis : undefined,
|
||||
max: this.fitYData ? this.maxYAxis : undefined,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
position: rtl ? "right" : "left",
|
||||
scale: true,
|
||||
nameGap: 2,
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 5,
|
||||
formatter: (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale);
|
||||
const formatOptions =
|
||||
value >= 1 || value <= -1
|
||||
? undefined
|
||||
: {
|
||||
// show the first significant digit for tiny values
|
||||
maximumFractionDigits: Math.max(
|
||||
2,
|
||||
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
|
||||
),
|
||||
};
|
||||
const label = formatNumber(
|
||||
value,
|
||||
this.hass.locale,
|
||||
formatOptions
|
||||
);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
@@ -218,46 +288,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
} as YAXisOption,
|
||||
legend: {
|
||||
show: this.showNames,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
},
|
||||
grid: {
|
||||
...(this.showNames ? {} : { top: 30 }), // undefined is the same as 0
|
||||
top: 15,
|
||||
left: rtl ? 1 : Math.max(this.paddingYAxis, this._yWidth),
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 30,
|
||||
bottom: 20,
|
||||
},
|
||||
visualMap: this._chartData
|
||||
.map((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
return {
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as VisualMapComponentOption[],
|
||||
visualMap: this._visualMap,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -307,21 +349,28 @@ export class StateHistoryChartLine extends LitElement {
|
||||
prevValues = datavalues;
|
||||
};
|
||||
|
||||
const addDataSet = (nameY: string, color?: string, fill = false) => {
|
||||
const addDataSet = (
|
||||
id: string,
|
||||
nameY: string,
|
||||
color?: string,
|
||||
fill = false
|
||||
) => {
|
||||
if (!color) {
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
}
|
||||
data.push({
|
||||
id: nameY,
|
||||
id,
|
||||
data: [],
|
||||
type: "line",
|
||||
cursor: "default",
|
||||
name: nameY,
|
||||
color,
|
||||
symbol: "circle",
|
||||
step: "end",
|
||||
symbolSize: 1,
|
||||
step: "end",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: fill ? 0 : 1.5,
|
||||
},
|
||||
@@ -375,13 +424,23 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.target_temp_low
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-current_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.current_temperature", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
if (hasHeat) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
||||
states.entity_id + "-heating",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
true
|
||||
);
|
||||
@@ -390,7 +449,12 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
@@ -400,22 +464,40 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
if (hasTargetRange) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})}`
|
||||
states.entity_id + "-target_temperature_mode",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.high"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_high.name"
|
||||
)
|
||||
);
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})}`
|
||||
states.entity_id + "-target_temperature_mode_low",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||
name: name,
|
||||
mode: this.hass.localize("ui.card.climate.low"),
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.target_temp_low.name"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-target_temperature",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.climate.target_temperature_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.temperature.name"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -468,19 +550,29 @@ export class StateHistoryChartLine extends LitElement {
|
||||
);
|
||||
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})}`
|
||||
states.entity_id + "-target_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.humidity.name"
|
||||
)
|
||||
);
|
||||
|
||||
if (hasCurrent) {
|
||||
addDataSet(
|
||||
`${this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)}`
|
||||
states.entity_id + "-current_humidity",
|
||||
this.showNames
|
||||
? this.hass.localize(
|
||||
"ui.card.humidifier.current_humidity_entity",
|
||||
{
|
||||
name: name,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -488,25 +580,40 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// If action attribute is not available, we shade the area when the device is on
|
||||
if (hasHumidifying) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-humidifying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.humidifying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else if (hasDrying) {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-drying",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.drying", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state_attributes.action.state.drying"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
addDataSet(
|
||||
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})}`,
|
||||
states.entity_id + "-on",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.humidifier.on_entity", {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.humidifier.entity_component._.state.on"
|
||||
),
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
@@ -539,7 +646,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
addDataSet(name);
|
||||
addDataSet(states.entity_id, name);
|
||||
|
||||
let lastValue: number;
|
||||
let lastDate: Date;
|
||||
@@ -608,6 +715,46 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartData = datasets;
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
const visualMap: VisualMapComponentOption[] = [];
|
||||
this._chartData.forEach((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
visualMap.push({
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
@@ -8,7 +8,6 @@ import type {
|
||||
TooltipFormatterCallback,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -22,7 +21,6 @@ import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@@ -67,7 +65,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${this.data.length * 30 + 30}px`}
|
||||
.data=${this._chartData}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
@@ -129,10 +127,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||
(params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, marker } = Array.isArray(params)
|
||||
const { value, name, marker, seriesName } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${value![0]}</h4>`;
|
||||
const title = seriesName
|
||||
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: "";
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
@@ -183,13 +183,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
private _createOptions() {
|
||||
const narrow = this.narrow;
|
||||
const showNames = this.chunked || this.showNames;
|
||||
const maxInternalLabelWidth = narrow ? 70 : 165;
|
||||
const maxInternalLabelWidth = narrow ? 105 : 185;
|
||||
const labelWidth = showNames
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(this.hass);
|
||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
@@ -197,21 +196,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
max: this.endTime,
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
},
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
@@ -226,14 +214,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
},
|
||||
axisLabel: {
|
||||
show: showNames,
|
||||
width: labelWidth - labelMargin,
|
||||
width: labelWidth,
|
||||
overflow: "truncate",
|
||||
margin: labelMargin,
|
||||
formatter: (label: string) => {
|
||||
const width = Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
);
|
||||
formatter: (id: string) => {
|
||||
const label = this._chartData.find((d) => d.id === id)
|
||||
?.name as string;
|
||||
const width = label
|
||||
? Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
)
|
||||
: 0;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
@@ -278,8 +270,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
let prevState: string | null = null;
|
||||
let locState: string | null = null;
|
||||
let prevLastChanged = startTime;
|
||||
const entityDisplay: string =
|
||||
names[stateInfo.entity_id] || stateInfo.name;
|
||||
const entityDisplay: string = this.showNames
|
||||
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
|
||||
: "";
|
||||
|
||||
const dataRow: unknown[] = [];
|
||||
stateInfo.data.forEach((entityState) => {
|
||||
@@ -307,7 +300,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
);
|
||||
dataRow.push({
|
||||
value: [
|
||||
entityDisplay,
|
||||
stateInfo.entity_id,
|
||||
prevLastChanged,
|
||||
newLastChanged,
|
||||
locState,
|
||||
@@ -333,7 +326,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
);
|
||||
dataRow.push({
|
||||
value: [
|
||||
entityDisplay,
|
||||
stateInfo.entity_id,
|
||||
prevLastChanged,
|
||||
endTime,
|
||||
locState,
|
||||
@@ -346,9 +339,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
});
|
||||
}
|
||||
datasets.push({
|
||||
id: stateInfo.entity_id,
|
||||
data: dataRow,
|
||||
name: entityDisplay,
|
||||
dimensions: ["index", "start", "end", "name", "color", "textColor"],
|
||||
dimensions: ["id", "start", "end", "name", "color", "textColor"],
|
||||
type: "custom",
|
||||
encode: {
|
||||
x: [1, 2],
|
||||
@@ -364,10 +358,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
||||
if (e.detail.targetType === "axisLabel") {
|
||||
const dataset = this.data[e.detail.dataIndex];
|
||||
const dataset = this._chartData[e.detail.dataIndex];
|
||||
if (dataset) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: dataset.entity_id,
|
||||
entityId: dataset.id as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -71,6 +71,9 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
private _computedEndTime!: Date;
|
||||
@@ -135,7 +138,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
if (!Array.isArray(item)) {
|
||||
return html`<div class="entry-container">
|
||||
return html`<div class="entry-container line">
|
||||
<state-history-chart-line
|
||||
.hass=${this.hass}
|
||||
.unit=${item.unit}
|
||||
@@ -154,10 +157,11 @@ export class StateHistoryCharts extends LitElement {
|
||||
.fitYData=${this.fitYData}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
return html`<div class="entry-container">
|
||||
return html`<div class="entry-container timeline">
|
||||
<state-history-chart-timeline
|
||||
.hass=${this.hass}
|
||||
.data=${item}
|
||||
@@ -299,7 +303,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-container.line {
|
||||
flex: 1;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
@@ -313,6 +322,10 @@ export class StateHistoryCharts extends LitElement {
|
||||
padding-inline-end: 1px;
|
||||
}
|
||||
|
||||
.entry-container.timeline:first-child {
|
||||
margin-top: var(--timeline-top-margin);
|
||||
}
|
||||
|
||||
.entry-container:not(:first-child) {
|
||||
border-top: 2px solid var(--divider-color);
|
||||
margin-top: 16px;
|
||||
|
@@ -1,15 +1,23 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
LineSeriesOption,
|
||||
ZRColor,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
@@ -21,16 +29,9 @@ import {
|
||||
getStatisticMetadata,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../common/number/format_number";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@@ -56,6 +57,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: false }) public startTime?: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@@ -88,6 +91,9 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: string[] = [];
|
||||
@@ -124,7 +130,10 @@ export class StatisticsChart extends LitElement {
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("hideLegend") ||
|
||||
changedProps.has("_legendData")
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("_legendData") ||
|
||||
changedProps.has("_chartData")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -164,9 +173,9 @@ export class StatisticsChart extends LitElement {
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
style=${styleMap({ height: this.height })}
|
||||
external-hidden
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
.expandLegend=${this.expandLegend}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
@@ -181,18 +190,31 @@ export class StatisticsChart extends LitElement {
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) =>
|
||||
params
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesName]) return "";
|
||||
rendered[param.seriesName] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
(param.value[2] ?? param.value[1]) as number,
|
||||
rawValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._statisticIds[param.seriesIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
@@ -202,36 +224,70 @@ export class StatisticsChart extends LitElement {
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
||||
`;
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {};
|
||||
const dayDifference = this.daysToShow ?? 1;
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
this.maxYAxis;
|
||||
if (typeof minYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
}
|
||||
const endTime = this.endTime ?? new Date();
|
||||
let startTime = this.startTime;
|
||||
|
||||
if (!startTime) {
|
||||
// set start time to the earliest point in the chart data
|
||||
this._chartData.forEach((series) => {
|
||||
if (!Array.isArray(series.data) || !series.data[0]) return;
|
||||
const firstPoint = series.data[0] as any;
|
||||
const timestamp = Array.isArray(firstPoint)
|
||||
? firstPoint[0]
|
||||
: firstPoint.value?.[0];
|
||||
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
|
||||
startTime = new Date(timestamp);
|
||||
}
|
||||
});
|
||||
|
||||
if (!startTime) {
|
||||
// Calculate default start time based on dayDifference
|
||||
startTime = new Date(
|
||||
endTime.getTime() - dayDifference * 24 * 3600 * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
axisLabel: getTimeAxisLabelConfig(
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
dayDifference
|
||||
),
|
||||
axisLine: {
|
||||
xAxis: [
|
||||
{
|
||||
id: "xAxis",
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
type: "time",
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
minInterval:
|
||||
dayDifference >= 89 // quarter
|
||||
? 28 * 3600 * 24 * 1000
|
||||
: dayDifference > 2
|
||||
? 3600 * 24 * 1000
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
yAxis: {
|
||||
type: this.logarithmicScale ? "log" : "value",
|
||||
name: this.unit,
|
||||
@@ -240,24 +296,20 @@ export class StatisticsChart extends LitElement {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
// @ts-ignore
|
||||
scale: this.chartType !== "bar",
|
||||
min: this.fitYData ? undefined : this.minYAxis,
|
||||
max: this.fitYData ? undefined : this.maxYAxis,
|
||||
scale: true,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: splitLineStyle,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: !this.hideLegend,
|
||||
icon: "circle",
|
||||
padding: [20, 0],
|
||||
data: this._legendData,
|
||||
},
|
||||
grid: {
|
||||
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
|
||||
left: 20,
|
||||
top: 15,
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
@@ -296,7 +348,11 @@ export class StatisticsChart extends LitElement {
|
||||
let colorIndex = 0;
|
||||
const statisticsData = Object.entries(this.statisticsData);
|
||||
const totalDataSets: typeof this._chartData = [];
|
||||
const legendData: { name: string; color: string }[] = [];
|
||||
const legendData: {
|
||||
name: string;
|
||||
color?: ZRColor;
|
||||
borderColor?: ZRColor;
|
||||
}[] = [];
|
||||
const statisticIds: string[] = [];
|
||||
let endTime: Date;
|
||||
|
||||
@@ -347,7 +403,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
// The datasets for the current statistic
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: { name: string; color: string }[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
@@ -369,10 +425,12 @@ export class StatisticsChart extends LitElement {
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push(
|
||||
this._transformDataValue([prevEndTime, ...prevValues[i]!])
|
||||
);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
@@ -411,19 +469,15 @@ export class StatisticsChart extends LitElement {
|
||||
sortedTypes.forEach((type) => {
|
||||
if (statisticsHaveType(stats, type)) {
|
||||
const band = drawBands && (type === "min" || type === "max");
|
||||
if (!this.hideLegend) {
|
||||
const showLegend = hasMean
|
||||
? type === "mean"
|
||||
: displayedLegend === false;
|
||||
if (showLegend) {
|
||||
statLegendData.push({ name, color });
|
||||
}
|
||||
displayedLegend = displayedLegend || showLegend;
|
||||
}
|
||||
statTypes.push(type);
|
||||
const borderColor =
|
||||
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
|
||||
const backgroundColor = band ? color + "3F" : color + "7F";
|
||||
const series: LineSeriesOption | BarSeriesOption = {
|
||||
id: `${statistic_id}-${type}`,
|
||||
type: this.chartType,
|
||||
smooth: this.chartType === "line" ? 0.4 : false,
|
||||
smoothMonotone: "x",
|
||||
cursor: "default",
|
||||
data: [],
|
||||
name: name
|
||||
@@ -433,8 +487,9 @@ export class StatisticsChart extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
symbol: "none",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
},
|
||||
@@ -442,27 +497,34 @@ export class StatisticsChart extends LitElement {
|
||||
this.chartType === "bar"
|
||||
? {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
borderColor:
|
||||
band && hasMean
|
||||
? color + (this.hideLegend ? "00" : "7F")
|
||||
: color,
|
||||
borderColor,
|
||||
borderWidth: 1.5,
|
||||
}
|
||||
: undefined,
|
||||
color: band ? color + "3F" : color + "7F",
|
||||
color: this.chartType === "bar" ? backgroundColor : borderColor,
|
||||
};
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
(series as LineSeriesOption).symbol = "none";
|
||||
(series as LineSeriesOption).lineStyle = {
|
||||
opacity: 0,
|
||||
};
|
||||
series.stackStrategy = "all";
|
||||
if (drawBands && type === "max") {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!this.hideLegend) {
|
||||
const showLegend = hasMean
|
||||
? type === "mean"
|
||||
: displayedLegend === false;
|
||||
if (showLegend) {
|
||||
statLegendData.push({
|
||||
name,
|
||||
color: series.color as ZRColor,
|
||||
borderColor: series.itemStyle?.borderColor,
|
||||
});
|
||||
}
|
||||
displayedLegend = displayedLegend || showLegend;
|
||||
}
|
||||
statDataSets.push(series);
|
||||
statisticIds.push(statistic_id);
|
||||
}
|
||||
@@ -489,7 +551,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
} else if (type === "max" && this.chartType === "line") {
|
||||
const max = stat.max || 0;
|
||||
val.push(max - (stat.min || 0));
|
||||
val.push(Math.abs(max - (stat.min || 0)));
|
||||
val.push(max);
|
||||
} else {
|
||||
val.push(stat[type] ?? null);
|
||||
@@ -510,14 +572,18 @@ export class StatisticsChart extends LitElement {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
legendData.forEach(({ name, color }) => {
|
||||
legendData.forEach(({ name, color, borderColor }) => {
|
||||
// Add an empty series for the legend
|
||||
totalDataSets.push({
|
||||
id: name + "-legend",
|
||||
name: name,
|
||||
color,
|
||||
itemStyle: {
|
||||
borderColor,
|
||||
},
|
||||
type: this.chartType,
|
||||
data: [],
|
||||
xAxisIndex: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -529,6 +595,26 @@ export class StatisticsChart extends LitElement {
|
||||
this._statisticIds = statisticIds;
|
||||
}
|
||||
|
||||
private _transformDataValue(val: [Date, ...(number | null)[]]) {
|
||||
if (this.chartType === "bar" && val[1] && val[1] < 0) {
|
||||
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@customElement("ha-data-table-icon")
|
||||
class HaDataTableIcon extends LitElement {
|
||||
@@ -9,30 +10,14 @@ class HaDataTableIcon extends LitElement {
|
||||
|
||||
@property() public path!: string;
|
||||
|
||||
@state() private _hovered = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._hovered ? html`<div>${this.tooltip}</div>` : ""}
|
||||
<ha-svg-icon .path=${this.path}></ha-svg-icon>
|
||||
<ha-tooltip .content=${this.tooltip}>
|
||||
<ha-svg-icon .path=${this.path}></ha-svg-icon>
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
const show = () => {
|
||||
this._hovered = true;
|
||||
};
|
||||
const hide = () => {
|
||||
this._hovered = false;
|
||||
};
|
||||
this.addEventListener("mouseenter", show);
|
||||
this.addEventListener("focus", show);
|
||||
this.addEventListener("mouseleave", hide);
|
||||
this.addEventListener("blur", hide);
|
||||
this.addEventListener("tap", hide);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
@@ -41,20 +26,6 @@ class HaDataTableIcon extends LitElement {
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
inset-inline-end: 28px;
|
||||
inset-inline-start: initial;
|
||||
z-index: 1002;
|
||||
outline: none;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
background-color: var(--simple-tooltip-background, #616161);
|
||||
color: var(--simple-tooltip-text-color, white);
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -448,6 +448,7 @@ export class HaDataTable extends LitElement {
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
title=${ifDefined(column.title)}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
|
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
(this._comboBox as any).items = [
|
||||
...(this.extraOptions ?? []),
|
||||
...(this.entityId && stateObj
|
||||
? getStates(stateObj, this.attribute).map((key) => ({
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -6,6 +5,7 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@@ -36,13 +36,13 @@ class StateInfo extends LitElement {
|
||||
</div>
|
||||
${this.inDialog
|
||||
? html`<div class="time-ago">
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<simple-tooltip animation-delay="0">
|
||||
<div>
|
||||
<ha-tooltip>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<div slot="content">
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
@@ -68,7 +68,7 @@ class StateInfo extends LitElement {
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</simple-tooltip>
|
||||
</ha-tooltip>
|
||||
</div>`
|
||||
: html`<div class="extra-info"><slot></slot></div>`}
|
||||
</div>`;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -8,6 +7,7 @@ import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
|
||||
@@ -67,22 +67,21 @@ export class HaAnalytics extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
<ha-tooltip
|
||||
content=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
placement="right"
|
||||
?disabled=${baseEnabled}
|
||||
>
|
||||
</ha-switch>
|
||||
${!baseEnabled
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
<ha-switch
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-tooltip>
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
|
@@ -295,10 +295,12 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
|
||||
const hassMessage: AssistMessage = {
|
||||
let hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
@@ -328,6 +330,43 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
@@ -435,28 +474,71 @@ export class HaAssistChat extends LitElement {
|
||||
this._processing = true;
|
||||
this._audio?.pause();
|
||||
this._addMessage({ who: "user", text });
|
||||
const message: AssistMessage = {
|
||||
let hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
let currentDeltaRole = "";
|
||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||
this._addMessage(message);
|
||||
this._addMessage(hassMessage);
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
(event) => {
|
||||
if (event.type === "intent-progress") {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message and previous message has content
|
||||
if (delta.role) {
|
||||
// If currentDeltaRole exists, it means we're receiving our
|
||||
// second or later message. Let's add it to the chat.
|
||||
if (
|
||||
currentDeltaRole &&
|
||||
delta.role === "assistant" &&
|
||||
hassMessage.text !== "…"
|
||||
) {
|
||||
// Remove progress indicator of previous message
|
||||
hassMessage.text = hassMessage.text.substring(
|
||||
0,
|
||||
hassMessage.text.length - 1
|
||||
);
|
||||
|
||||
hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
error: false,
|
||||
};
|
||||
this._addMessage(hassMessage);
|
||||
}
|
||||
currentDeltaRole = delta.role;
|
||||
}
|
||||
|
||||
if (
|
||||
currentDeltaRole === "assistant" &&
|
||||
"content" in delta &&
|
||||
delta.content
|
||||
) {
|
||||
hassMessage.text =
|
||||
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||
delta.content +
|
||||
"…";
|
||||
this.requestUpdate("_conversation");
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
message.text = plain.speech;
|
||||
hassMessage.text = plain.speech;
|
||||
}
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
if (event.type === "error") {
|
||||
message.text = event.data.message;
|
||||
message.error = true;
|
||||
hassMessage.text = event.data.message;
|
||||
hassMessage.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
unsub();
|
||||
}
|
||||
@@ -470,8 +552,8 @@ export class HaAssistChat extends LitElement {
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
message.error = true;
|
||||
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||
hassMessage.error = true;
|
||||
this.requestUpdate("_conversation");
|
||||
} finally {
|
||||
this._processing = false;
|
||||
|
@@ -329,15 +329,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.time-input-wrap-wrap {
|
||||
display: flex;
|
||||
}
|
||||
.time-input-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex: var(--time-input-flex, unset);
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
@@ -28,6 +28,7 @@ export class HaControlButton extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
--control-button-focus-color: var(--secondary-text-color);
|
||||
--control-button-icon-color: var(--primary-text-color);
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
@@ -66,9 +67,13 @@ export class HaControlButton extends LitElement {
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
color 180ms ease-in-out;
|
||||
color: var(--control-button-icon-color);
|
||||
}
|
||||
.button:focus-visible {
|
||||
--control-button-background-opacity: 0.4;
|
||||
box-shadow: 0 0 0 2px var(--control-button-focus-color);
|
||||
}
|
||||
.button::before {
|
||||
content: "";
|
||||
@@ -85,10 +90,6 @@ export class HaControlButton extends LitElement {
|
||||
pointer-events: none;
|
||||
white-space: normal;
|
||||
}
|
||||
.button {
|
||||
transition: color 180ms ease-in-out;
|
||||
color: var(--control-button-icon-color);
|
||||
}
|
||||
.button ::slotted(*) {
|
||||
pointer-events: none;
|
||||
opacity: 0.95;
|
||||
|
@@ -184,7 +184,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
--control-number-buttons-focus-color: var(--primary-color);
|
||||
--control-number-buttons-focus-color: var(--secondary-text-color);
|
||||
--control-number-buttons-background-color: var(--disabled-color);
|
||||
--control-number-buttons-background-opacity: 0.2;
|
||||
--control-number-buttons-border-radius: 10px;
|
||||
@@ -228,6 +228,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
outline: none;
|
||||
}
|
||||
.value::before {
|
||||
@@ -253,7 +254,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
width: 35px;
|
||||
height: 40px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
@@ -137,6 +137,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
--control-select-menu-focus-color: var(--secondary-text-color);
|
||||
--control-select-menu-text-color: var(--primary-text-color);
|
||||
--control-select-menu-background-color: var(--disabled-color);
|
||||
--control-select-menu-background-opacity: 0.2;
|
||||
@@ -167,7 +168,9 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
background: none;
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
transition: color 180ms ease-in-out;
|
||||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
color 180ms ease-in-out;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
@@ -205,7 +208,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
.select-anchor:focus-visible {
|
||||
--control-select-menu-background-opacity: 0.4;
|
||||
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
|
||||
}
|
||||
|
||||
.select-anchor::before {
|
||||
|
110
src/components/ha-copy-textfield.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-copy-textfield")
|
||||
export class HaCopyTextfield extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "value" }) public value!: string;
|
||||
|
||||
@property({ attribute: "masked-value" }) public maskedValue?: string;
|
||||
|
||||
@property({ attribute: "label" }) public label?: string;
|
||||
|
||||
@state() private _showMasked = true;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._showMasked && this.maskedValue
|
||||
? this.maskedValue
|
||||
: this.value}
|
||||
readonly
|
||||
.suffix=${this.maskedValue
|
||||
? html`<div style="width: 24px"></div>`
|
||||
: nothing}
|
||||
@click=${this._focusInput}
|
||||
></ha-textfield>
|
||||
${this.maskedValue
|
||||
? html`<ha-icon-button
|
||||
class="toggle-unmasked"
|
||||
.label=${this.hass.localize(
|
||||
`ui.common.${this._showMasked ? "show" : "hide"}`
|
||||
)}
|
||||
@click=${this._toggleMasked}
|
||||
.path=${this._showMasked ? mdiEye : mdiEyeOff}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button @click=${this._copy} unelevated>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.label || this.hass.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _focusInput(ev) {
|
||||
const inputElement = ev.currentTarget as HaTextField;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
private _toggleMasked(): void {
|
||||
this._showMasked = !this._showMasked;
|
||||
}
|
||||
|
||||
private async _copy(): Promise<void> {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-unmasked {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-copy-textfield": HaCopyTextfield;
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -22,16 +24,18 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import {
|
||||
formatShortDateTimeWithYear,
|
||||
formatShortDateTime,
|
||||
formatShortDateTimeWithYear,
|
||||
} from "../common/datetime/format_date_time";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textarea";
|
||||
import "./ha-icon-button-next";
|
||||
import "./ha-icon-button-prev";
|
||||
import "./ha-textarea";
|
||||
|
||||
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
||||
|
||||
@@ -175,6 +179,96 @@ export class HaDateRangePicker extends LitElement {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-1h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
1
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-12h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
12
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-24h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-7d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 7
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-30d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 30
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -197,14 +291,15 @@ export class HaDateRangePicker extends LitElement {
|
||||
?auto-apply=${this.autoApply}
|
||||
time-picker=${this.timePicker}
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this.startDate.toISOString()}
|
||||
end-date=${this.endDate.toISOString()}
|
||||
start-date=${this._formatDate(this.startDate)}
|
||||
end-date=${this._formatDate(this.endDate)}
|
||||
?ranges=${this.ranges !== false}
|
||||
opening-direction=${ifDefined(
|
||||
this.openingDirection || this._calcedOpeningDirection
|
||||
)}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
language=${this.hass.locale.language}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||
${!this.minimal
|
||||
@@ -325,9 +420,31 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _applyDateRange() {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
|
||||
const startDate = fromZonedTime(
|
||||
dateRangePicker.start,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const endDate = fromZonedTime(
|
||||
dateRangePicker.end,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
|
||||
dateRangePicker.clickRange([startDate, endDate]);
|
||||
}
|
||||
|
||||
this._dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
private _formatDate(date: Date): string {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
return toZonedTime(date, this.hass.config.time_zone).toISOString();
|
||||
}
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
private get _dateRangePicker() {
|
||||
const dateRangePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker"
|
||||
@@ -358,45 +475,66 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const startDate = ev.detail.startDate;
|
||||
const endDate = ev.detail.endDate;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { startDate, endDate },
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 940px) and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
overflow: auto;
|
||||
max-height: calc(70vh - 330px);
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
:host([header-position]) .date-range-ranges {
|
||||
max-height: calc(90vh - 430px);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -16,7 +16,7 @@ export const createCloseHeading = (
|
||||
) => html`
|
||||
<div class="header_title">
|
||||
<ha-icon-button
|
||||
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
|
||||
.label=${hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
|
@@ -11,6 +11,7 @@ import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -23,6 +24,8 @@ declare global {
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||
|
||||
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
<div class="uploading">
|
||||
<span class="header"
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>${this.uploadingLabel || this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
localize("ui.components.file-upload.secondary")}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel ||
|
||||
localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
|
@@ -38,6 +38,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
<ha-textfield
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
|
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
if (!this.isPassword) return nothing;
|
||||
return html`
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.localize?.(
|
||||
`${this.localizeBaseKey}.${
|
||||
this.unmaskedPassword ? "hide_password" : "show_password"
|
||||
|
@@ -1,25 +1,33 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-help-tooltip")
|
||||
export class HaHelpTooltip extends LitElement {
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public position = "top";
|
||||
@property() public position:
|
||||
| "top"
|
||||
| "bottom"
|
||||
| "left"
|
||||
| "right"
|
||||
| "top-start"
|
||||
| "top-end"
|
||||
| "right-start"
|
||||
| "right-end"
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left-start"
|
||||
| "left-end" = "top";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<simple-tooltip
|
||||
offset="4"
|
||||
.position=${this.position}
|
||||
.fitToVisibleBounds=${true}
|
||||
>${this.label}</simple-tooltip
|
||||
>
|
||||
<ha-tooltip .placement=${this.position} .content=${this.label}>
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyle } from "../resources/styles";
|
||||
@@ -10,6 +9,7 @@ import "./ha-button-menu";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
@@ -70,25 +70,20 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
<!-- Icon representation for big screens -->
|
||||
${this.items.map((item) =>
|
||||
item.narrowOnly
|
||||
? ""
|
||||
? nothing
|
||||
: item.divider
|
||||
? html`<div role="separator"></div>`
|
||||
: html`<div>
|
||||
${item.tooltip
|
||||
? html`<simple-tooltip
|
||||
animation-delay="0"
|
||||
position="left"
|
||||
>
|
||||
${item.tooltip}
|
||||
</simple-tooltip>`
|
||||
: ""}
|
||||
: html`<ha-tooltip
|
||||
.disabled=${!item.tooltip}
|
||||
.content=${item.tooltip ?? ""}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${item.action}
|
||||
.label=${item.label}
|
||||
.path=${item.path}
|
||||
?disabled=${item.disabled}
|
||||
></ha-icon-button>
|
||||
</div> `
|
||||
</ha-tooltip>`
|
||||
)}
|
||||
`}
|
||||
`;
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
reType:
|
||||
@@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
markdownCache.set(key, this.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -37,6 +52,24 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash({
|
||||
content: this.content,
|
||||
allowSvg: this.allowSvg,
|
||||
breaks: this.breaks,
|
||||
});
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
|
@@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
|
||||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { mdiStar } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -64,9 +63,13 @@ export class HaNetwork extends LitElement {
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
|
||||
<span slot="heading" data-for="auto_configure">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.auto_configure"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="auto_configure">
|
||||
Detected:
|
||||
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
@@ -85,18 +88,21 @@ export class HaNetwork extends LitElement {
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
<span slot="heading">
|
||||
Adapter: ${adapter.name}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.adapter"
|
||||
)}:
|
||||
${adapter.name}
|
||||
${adapter.default
|
||||
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
||||
(Default)`
|
||||
: ""}
|
||||
(${this.hass.localize("ui.common.default")})`
|
||||
: nothing}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
)
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
|
||||
@change=${this._handleChangeEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
|
@@ -250,6 +250,11 @@ export class HaPictureUpload extends LitElement {
|
||||
max-height: 200px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: var(--file-upload-image-border-radius);
|
||||
transition: opacity 0.3s;
|
||||
opacity: var(--picture-opacity, 1);
|
||||
}
|
||||
img:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
||||
// The following import makes a better implementation available that is based on a
|
||||
// WebAssembly port of ZXing:
|
||||
import { setZXingModuleOverrides } from "barcode-detector";
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
@@ -21,12 +21,14 @@ import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|