mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 19:09:48 +00:00
Compare commits
802 Commits
feature/ca
...
logs-front
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1afcfcd32 | ||
|
|
29e6ada90e | ||
|
|
ff1745fccc | ||
|
|
59aaf86104 | ||
|
|
52bc692c79 | ||
|
|
29de405912 | ||
|
|
a723171ef2 | ||
|
|
cfe193cf60 | ||
|
|
a8e6557b09 | ||
|
|
a517aabdd8 | ||
|
|
10c8828aa5 | ||
|
|
5801ce944c | ||
|
|
79ad9dbf44 | ||
|
|
9814533d47 | ||
|
|
bdb6c684c0 | ||
|
|
0046167f5c | ||
|
|
5266f0d761 | ||
|
|
cc5c0d04c4 | ||
|
|
7f3d5f557f | ||
|
|
9b48cd7737 | ||
|
|
11c6f6ec78 | ||
|
|
cb0f59b26d | ||
|
|
c89fc35578 | ||
|
|
f03cd9c239 | ||
|
|
19a4e37933 | ||
|
|
76514babd5 | ||
|
|
b6abbdafb8 | ||
|
|
d35e6c0092 | ||
|
|
5c2ee54dec | ||
|
|
1dfca76c81 | ||
|
|
fd7f028fbf | ||
|
|
3f7283b1af | ||
|
|
d35323ac52 | ||
|
|
06475382e8 | ||
|
|
b60dd7f15d | ||
|
|
b77e65fabd | ||
|
|
cea691a04e | ||
|
|
50df2a34cd | ||
|
|
e6c0a84994 | ||
|
|
b03fa4bdc5 | ||
|
|
058cecc124 | ||
|
|
a5f058a7eb | ||
|
|
655c2ff3c2 | ||
|
|
e1b0a3e737 | ||
|
|
d59d436080 | ||
|
|
0fe0bf12f2 | ||
|
|
f5a3877f47 | ||
|
|
31d04f5338 | ||
|
|
4f7d223aa7 | ||
|
|
484c60073d | ||
|
|
0e1ab1a60c | ||
|
|
cef11e0c18 | ||
|
|
55e75e80d2 | ||
|
|
126a78ec8a | ||
|
|
063af39f0f | ||
|
|
132c4c8201 | ||
|
|
4c08e960f1 | ||
|
|
a8020256de | ||
|
|
2ea57c33ae | ||
|
|
db1408666c | ||
|
|
260288a061 | ||
|
|
45fd685913 | ||
|
|
896d76b218 | ||
|
|
cec24117dc | ||
|
|
34006d268b | ||
|
|
54c03d91df | ||
|
|
52a56a1c4e | ||
|
|
e49feeb4aa | ||
|
|
a0c30e433a | ||
|
|
354ce027eb | ||
|
|
5c224a942d | ||
|
|
0efa4f81d4 | ||
|
|
3ad2f35f29 | ||
|
|
7a21d5f7bc | ||
|
|
33226587e6 | ||
|
|
bd2673f311 | ||
|
|
cecadde497 | ||
|
|
494b8811d0 | ||
|
|
4e0a49b3da | ||
|
|
3145fed5dc | ||
|
|
3dd040fdc7 | ||
|
|
e3abe9736c | ||
|
|
fe41e72774 | ||
|
|
7078ef52d4 | ||
|
|
f1c9802ee3 | ||
|
|
35697e3f94 | ||
|
|
8ea7ad3026 | ||
|
|
73747fbedc | ||
|
|
aaa92bd354 | ||
|
|
5f75fc5bcb | ||
|
|
5fa44548c3 | ||
|
|
1945c11621 | ||
|
|
930575d292 | ||
|
|
0147dbab00 | ||
|
|
13ace24b83 | ||
|
|
616333591a | ||
|
|
8f5875c30f | ||
|
|
517cd49f35 | ||
|
|
25d9fc94b2 | ||
|
|
7b188759e3 | ||
|
|
76772d1098 | ||
|
|
6052745ca0 | ||
|
|
89b9780345 | ||
|
|
a607edca96 | ||
|
|
52eb3d8063 | ||
|
|
1361fc36bf | ||
|
|
505ef2bd11 | ||
|
|
c0cc66c1ab | ||
|
|
7cfbc521c7 | ||
|
|
e064ce56cc | ||
|
|
8d688aa3a9 | ||
|
|
d122483449 | ||
|
|
f17bbc3f79 | ||
|
|
c88f8fcce0 | ||
|
|
8efabde916 | ||
|
|
e821e1ec83 | ||
|
|
dc7516da94 | ||
|
|
a545a377a7 | ||
|
|
3634dbcbbf | ||
|
|
75af4f939e | ||
|
|
453a2ac7f3 | ||
|
|
8fbd0226fc | ||
|
|
2a8d935601 | ||
|
|
a6328fb6d7 | ||
|
|
a78b61006f | ||
|
|
d506aa23b6 | ||
|
|
48b4df43ab | ||
|
|
8cdcd9cb55 | ||
|
|
a1e2ac1d99 | ||
|
|
8ecddbc42c | ||
|
|
6f70ef52a5 | ||
|
|
7dff02d7c8 | ||
|
|
8bbd7a6a06 | ||
|
|
5c73a06f76 | ||
|
|
9943dae82c | ||
|
|
70bf049df0 | ||
|
|
f9d9fbb7f0 | ||
|
|
9cb84d3f37 | ||
|
|
c1bcf27cf8 | ||
|
|
164ec2a9b5 | ||
|
|
20001a551c | ||
|
|
b7f85bf733 | ||
|
|
b303e9441b | ||
|
|
8f4bd0f620 | ||
|
|
596346bf59 | ||
|
|
769cea92aa | ||
|
|
f825016514 | ||
|
|
c6fd45bd6a | ||
|
|
6c4f4af75c | ||
|
|
cd5c3ef2f6 | ||
|
|
636a6fa02e | ||
|
|
21b83426d6 | ||
|
|
c139ec22f9 | ||
|
|
a6ef3a26da | ||
|
|
221ca56121 | ||
|
|
4e6e3629a8 | ||
|
|
fe94ae0243 | ||
|
|
8a1a22d4bd | ||
|
|
153a578986 | ||
|
|
04bb10d0a2 | ||
|
|
35e52de2c1 | ||
|
|
b0862fddaa | ||
|
|
77735f5310 | ||
|
|
e388756533 | ||
|
|
e9ca9bb781 | ||
|
|
e48918442c | ||
|
|
52f37f41f0 | ||
|
|
4687006fec | ||
|
|
aca4ca3066 | ||
|
|
3a2c00622a | ||
|
|
699c25a6c3 | ||
|
|
1ad226d608 | ||
|
|
992a4cd98a | ||
|
|
fd217f8ea5 | ||
|
|
dede14e578 | ||
|
|
fa7aca67e5 | ||
|
|
6abdfa6d5c | ||
|
|
0a70e2abda | ||
|
|
1ec589e9b6 | ||
|
|
2d2b5633c4 | ||
|
|
76df75c306 | ||
|
|
027ded61c2 | ||
|
|
a718589ba0 | ||
|
|
5b5dc9d853 | ||
|
|
2a49b5e15a | ||
|
|
fa4dd1c5ea | ||
|
|
37a3af2e8b | ||
|
|
fbfcef1573 | ||
|
|
4eecd37aaf | ||
|
|
c798521ab8 | ||
|
|
e432f0a8ee | ||
|
|
e3a1d0abe2 | ||
|
|
8080ba696c | ||
|
|
7bd8f321a4 | ||
|
|
4e958302b4 | ||
|
|
8a42d15bde | ||
|
|
ef0da0a7ee | ||
|
|
ae053c20b0 | ||
|
|
5f71938d60 | ||
|
|
82ac26b326 | ||
|
|
80b92b9813 | ||
|
|
904a083f61 | ||
|
|
d75ee09d55 | ||
|
|
a8e0d506b6 | ||
|
|
01dd731622 | ||
|
|
dc20702d36 | ||
|
|
f32ca9be29 | ||
|
|
8c4c4157a8 | ||
|
|
c8419d4c3d | ||
|
|
089316b8ae | ||
|
|
8d03ac5f64 | ||
|
|
e0e1f6f920 | ||
|
|
d4c98cae3a | ||
|
|
46d0eb4f44 | ||
|
|
07812f8d84 | ||
|
|
96f54d348f | ||
|
|
6084ab116f | ||
|
|
6b7acd8d3b | ||
|
|
e35b155c66 | ||
|
|
437d02c12f | ||
|
|
9cd74fbff8 | ||
|
|
33a7aacd83 | ||
|
|
39546615bb | ||
|
|
be51cbc944 | ||
|
|
77874aa2d7 | ||
|
|
4808463d5f | ||
|
|
5fb3cab247 | ||
|
|
d1093b187f | ||
|
|
fd7f0d3841 | ||
|
|
36aa74e4a5 | ||
|
|
938128d1c3 | ||
|
|
2a5d4ac578 | ||
|
|
be63ff7702 | ||
|
|
132c68bf20 | ||
|
|
16499bbd6b | ||
|
|
c7eddfed8f | ||
|
|
150842e431 | ||
|
|
9eb5360a68 | ||
|
|
e9e32c7d91 | ||
|
|
c83d760e82 | ||
|
|
489b7f9227 | ||
|
|
ad2ba63155 | ||
|
|
29bc894dbd | ||
|
|
faf6cb6333 | ||
|
|
a2e1e6362b | ||
|
|
3212ab6f3b | ||
|
|
3d27daad80 | ||
|
|
b679f1ce60 | ||
|
|
6b0a5d783b | ||
|
|
23e2f94d11 | ||
|
|
c250777858 | ||
|
|
c35d0da9bd | ||
|
|
794aa45a2b | ||
|
|
d0b85d0c0b | ||
|
|
23b6a3a1a9 | ||
|
|
3e749ec085 | ||
|
|
ee2ec00069 | ||
|
|
0aa2941868 | ||
|
|
46cd1d5156 | ||
|
|
07a5c41fd4 | ||
|
|
4ad3c553d5 | ||
|
|
d40cc448a5 | ||
|
|
43a23e6cdd | ||
|
|
aa4dd1cf29 | ||
|
|
0ae55c39cc | ||
|
|
0bfacacc9e | ||
|
|
c2f21c19af | ||
|
|
e2f3f9d348 | ||
|
|
98d44950f8 | ||
|
|
8ae9edb1ef | ||
|
|
84c4396c13 | ||
|
|
6653333c38 | ||
|
|
8c19e080be | ||
|
|
c649b1015a | ||
|
|
2b937a30e3 | ||
|
|
b7815bfd86 | ||
|
|
d94fa03411 | ||
|
|
0a7007ef9e | ||
|
|
dd12136dee | ||
|
|
6e2f89fe3d | ||
|
|
092085b9af | ||
|
|
1c06eb8661 | ||
|
|
c7e87b06b5 | ||
|
|
38c738c199 | ||
|
|
e899587307 | ||
|
|
c9feb0b75f | ||
|
|
10718c35d1 | ||
|
|
4dc6a37bad | ||
|
|
ac49fc7aba | ||
|
|
e4f008800b | ||
|
|
1b6c33efd4 | ||
|
|
5cfc34b020 | ||
|
|
1e7647b214 | ||
|
|
cef3a7ef99 | ||
|
|
14d0028426 | ||
|
|
28032d9d0d | ||
|
|
6c1995ba1b | ||
|
|
b68464c5d5 | ||
|
|
31ccf114a6 | ||
|
|
1b932ae4a2 | ||
|
|
0df6019b95 | ||
|
|
94fb03d2e2 | ||
|
|
6dc165ebf8 | ||
|
|
f2c5b91def | ||
|
|
b312cca050 | ||
|
|
ac14733bff | ||
|
|
a2d4165511 | ||
|
|
b87ffbd4f7 | ||
|
|
a8f8d197f8 | ||
|
|
4fcac79047 | ||
|
|
42ddacd41a | ||
|
|
ebc9981289 | ||
|
|
23deab253b | ||
|
|
ab172abe02 | ||
|
|
10d5d8b15d | ||
|
|
c9e472dab7 | ||
|
|
1e13b2b812 | ||
|
|
e04a04632a | ||
|
|
04bc5fba63 | ||
|
|
e66724ca9e | ||
|
|
bcfe5add33 | ||
|
|
7cc116dd07 | ||
|
|
ee93f31220 | ||
|
|
b7cc19f12e | ||
|
|
f70edf9311 | ||
|
|
0fa7c2face | ||
|
|
7b3a265a70 | ||
|
|
5d9aae3ad5 | ||
|
|
5de84ac0d8 | ||
|
|
98c4ec91d6 | ||
|
|
972b9cb758 | ||
|
|
ac621af811 | ||
|
|
7eb97bb58f | ||
|
|
d37af0f488 | ||
|
|
0d3b340228 | ||
|
|
288e03775b | ||
|
|
df36e9d205 | ||
|
|
15a0b35866 | ||
|
|
aa7522f681 | ||
|
|
c09e97a561 | ||
|
|
733be8e5a3 | ||
|
|
d107ac7d4c | ||
|
|
efc5bacb97 | ||
|
|
430e52efe3 | ||
|
|
6b4c4a9cf8 | ||
|
|
e5b1acc2c3 | ||
|
|
c89f476d67 | ||
|
|
e68afead17 | ||
|
|
c4651c0bc0 | ||
|
|
6d95b7af11 | ||
|
|
3e74cf3ada | ||
|
|
859ee98abb | ||
|
|
dd3e5e3724 | ||
|
|
2e3ab4d64f | ||
|
|
63cbeca820 | ||
|
|
1057ff314c | ||
|
|
5b946f1048 | ||
|
|
fdd66b5cec | ||
|
|
76c9723c71 | ||
|
|
b02368b9c6 | ||
|
|
0bcb7897c9 | ||
|
|
786bbb3850 | ||
|
|
e8ead84fe5 | ||
|
|
428e7fb332 | ||
|
|
ad9e8d5a52 | ||
|
|
e3cf04b3d1 | ||
|
|
10c3042db1 | ||
|
|
25f6b7de2f | ||
|
|
ca1cda4824 | ||
|
|
8c4a67315b | ||
|
|
c18de97b32 | ||
|
|
23a3ca3ed7 | ||
|
|
69457b4e85 | ||
|
|
2e096c23e0 | ||
|
|
552691e200 | ||
|
|
91258c86c1 | ||
|
|
3750a378cd | ||
|
|
12d3304c72 | ||
|
|
246100809d | ||
|
|
6efca93186 | ||
|
|
6280647b9a | ||
|
|
0b0ffd7bab | ||
|
|
dfa77526a2 | ||
|
|
9a3bd6c613 | ||
|
|
1161de5746 | ||
|
|
9df8e20391 | ||
|
|
11047a9c95 | ||
|
|
18fa66f61c | ||
|
|
758a048f34 | ||
|
|
ee0fc360b0 | ||
|
|
4012f95ec1 | ||
|
|
2ff52c6c29 | ||
|
|
d038e11170 | ||
|
|
8925b39fe5 | ||
|
|
beeef65506 | ||
|
|
994c1b5751 | ||
|
|
6823c647b6 | ||
|
|
866b478dc0 | ||
|
|
d746dc5752 | ||
|
|
5f53e1e71c | ||
|
|
3da82df093 | ||
|
|
4cedfffb71 | ||
|
|
1e1514e7da | ||
|
|
60e07075bc | ||
|
|
c998086474 | ||
|
|
53be0a3fa2 | ||
|
|
d69c46c80c | ||
|
|
0c2a7bfed0 | ||
|
|
afdd232e38 | ||
|
|
179751a135 | ||
|
|
52f6024306 | ||
|
|
7c7a4e61f2 | ||
|
|
facce7b016 | ||
|
|
e546cb3374 | ||
|
|
a0d2e7312b | ||
|
|
c0a9dadcbe | ||
|
|
e1edf7fb98 | ||
|
|
6d5c165bd2 | ||
|
|
54177a16e9 | ||
|
|
c814b8e888 | ||
|
|
33a0b32cc5 | ||
|
|
7dae13bf57 | ||
|
|
0a3fe6e0fb | ||
|
|
0336ce4606 | ||
|
|
9ba36ab7e2 | ||
|
|
fe7a08a1b0 | ||
|
|
87a8f9cedc | ||
|
|
01df7e20ca | ||
|
|
d181219522 | ||
|
|
6ae24b8135 | ||
|
|
8e009f24f9 | ||
|
|
53031f44ac | ||
|
|
af5a988457 | ||
|
|
e0348e4da7 | ||
|
|
d53f3ec898 | ||
|
|
e422547d93 | ||
|
|
d91a3fbe85 | ||
|
|
01d7130f22 | ||
|
|
c57851e4df | ||
|
|
6f1f13acb0 | ||
|
|
a8abd00809 | ||
|
|
e053978dbe | ||
|
|
6e57f726a3 | ||
|
|
b7cabadbe1 | ||
|
|
d920217374 | ||
|
|
1630263276 | ||
|
|
bab0391a19 | ||
|
|
444123c47e | ||
|
|
f123d34046 | ||
|
|
1b40f99f68 | ||
|
|
b314b3ed2b | ||
|
|
59b8932969 | ||
|
|
107af753ec | ||
|
|
1f0acb3046 | ||
|
|
5680c742be | ||
|
|
2aeb9cf0ef | ||
|
|
c9931b3a3c | ||
|
|
fbf7ebdfe4 | ||
|
|
52ccb03de5 | ||
|
|
900236ac07 | ||
|
|
28940c930d | ||
|
|
e278b463fd | ||
|
|
db2acd4e39 | ||
|
|
6dcc52cd44 | ||
|
|
431e533929 | ||
|
|
02c845cbc6 | ||
|
|
628111ed20 | ||
|
|
e825a9c02f | ||
|
|
7a35bddf36 | ||
|
|
ad69270af8 | ||
|
|
404edf9483 | ||
|
|
a166b4e9b6 | ||
|
|
981db50826 | ||
|
|
09683863a7 | ||
|
|
8c78f931dc | ||
|
|
40ce3c1e31 | ||
|
|
e430a1b1be | ||
|
|
a2c6116417 | ||
|
|
3239273f3e | ||
|
|
7a285f11db | ||
|
|
e42c5a3254 | ||
|
|
df7a6297b0 | ||
|
|
e4ca478d01 | ||
|
|
7be2c59295 | ||
|
|
99d9c67492 | ||
|
|
8f781e53e3 | ||
|
|
3c92826e71 | ||
|
|
151a879e0a | ||
|
|
f3a8529ed7 | ||
|
|
d2cc7856d1 | ||
|
|
d5cb815bbd | ||
|
|
7f88d863e9 | ||
|
|
88ac56ac0b | ||
|
|
3d173ad03e | ||
|
|
3889d71768 | ||
|
|
8872adf2ed | ||
|
|
969e655fff | ||
|
|
cdc913d878 | ||
|
|
4ac1215def | ||
|
|
b2376fba56 | ||
|
|
f14d9198ac | ||
|
|
f4e583b302 | ||
|
|
2c602aecee | ||
|
|
cbf96898fe | ||
|
|
6760f4a2ae | ||
|
|
3481f7e8be | ||
|
|
95a0fe335f | ||
|
|
1e2d144d26 | ||
|
|
6aa89cb532 | ||
|
|
1b0ed7017f | ||
|
|
1cc7e387da | ||
|
|
41bf935f6e | ||
|
|
b08ea36a1e | ||
|
|
4f52a46725 | ||
|
|
f8a82563b0 | ||
|
|
a1672ccdfb | ||
|
|
bde851e5a4 | ||
|
|
a6d3041d59 | ||
|
|
f64edfa305 | ||
|
|
067b321d84 | ||
|
|
33efe395c8 | ||
|
|
db26b1041f | ||
|
|
6e9b4637bb | ||
|
|
0e30e5e0f4 | ||
|
|
283da74e2d | ||
|
|
034afd1375 | ||
|
|
912d710ae4 | ||
|
|
86b99d931a | ||
|
|
35cfa9aa0d | ||
|
|
6a23dbf204 | ||
|
|
cef8fc1d38 | ||
|
|
7c06e33b50 | ||
|
|
cb365d4635 | ||
|
|
525102678b | ||
|
|
dfc4b0bba2 | ||
|
|
846692bc8a | ||
|
|
3b90b5fcb1 | ||
|
|
cac978344f | ||
|
|
6a40631e6d | ||
|
|
48f5b6dfd3 | ||
|
|
04b01d2cd9 | ||
|
|
0e8e054db1 | ||
|
|
477a893193 | ||
|
|
bd0822f09f | ||
|
|
07c3ffb55d | ||
|
|
fbfb4709d2 | ||
|
|
0a5b31e328 | ||
|
|
8cf0d8d2c3 | ||
|
|
61c16ce020 | ||
|
|
6bede4ddca | ||
|
|
bd88b91071 | ||
|
|
29b02a3c99 | ||
|
|
ac87e2280d | ||
|
|
98c4e34a23 | ||
|
|
3d005c8316 | ||
|
|
af31b5add3 | ||
|
|
9d02a1d391 | ||
|
|
98e6f32fe8 | ||
|
|
2726c6a849 | ||
|
|
c09ec54c76 | ||
|
|
9f045538a2 | ||
|
|
c6c4f91b0e | ||
|
|
f71d8f4367 | ||
|
|
68c1a38231 | ||
|
|
a9796e4216 | ||
|
|
bf6eefb692 | ||
|
|
7ec3b08444 | ||
|
|
f3355671d1 | ||
|
|
c0e240a3bf | ||
|
|
00fd4753e4 | ||
|
|
08ac873e3b | ||
|
|
d12b8d1b1b | ||
|
|
977207dde4 | ||
|
|
87a5f1a315 | ||
|
|
acab2d5ead | ||
|
|
046fc00f73 | ||
|
|
05775c411b | ||
|
|
d64acca598 | ||
|
|
59571d03a6 | ||
|
|
28c515bbac | ||
|
|
27db5b3b02 | ||
|
|
1922db0474 | ||
|
|
c8c74a9744 | ||
|
|
2c676baa99 | ||
|
|
3e41474faa | ||
|
|
5f9c69ac21 | ||
|
|
8b45ccaaba | ||
|
|
455925f637 | ||
|
|
9fba7427f8 | ||
|
|
21aae02652 | ||
|
|
24e3fbf622 | ||
|
|
dcbc8b627f | ||
|
|
0d8d18617c | ||
|
|
7eb87c78cc | ||
|
|
0eaf9ead9e | ||
|
|
7082646fe5 | ||
|
|
96d364b3bd | ||
|
|
e726eb7370 | ||
|
|
e6f587da78 | ||
|
|
c595392abe | ||
|
|
5bcffd0dbe | ||
|
|
df801833fc | ||
|
|
5ba5c00c70 | ||
|
|
dcea227f4a | ||
|
|
1abedcd5f0 | ||
|
|
9e29693293 | ||
|
|
3bfafc794f | ||
|
|
89c43b2b33 | ||
|
|
466115d916 | ||
|
|
a34ca3c085 | ||
|
|
9a8ca36047 | ||
|
|
b454e89613 | ||
|
|
0b76109272 | ||
|
|
942d264693 | ||
|
|
b10fdf8438 | ||
|
|
bee8980192 | ||
|
|
61487565db | ||
|
|
cc70eb46c9 | ||
|
|
dec9d304da | ||
|
|
7f8e856102 | ||
|
|
4bd60a1366 | ||
|
|
e9ca1758a0 | ||
|
|
dff3b82f0d | ||
|
|
1b630e7b66 | ||
|
|
f4238bf291 | ||
|
|
ef8cb8b393 | ||
|
|
bed161d485 | ||
|
|
22e0ef4308 | ||
|
|
eb355d110d | ||
|
|
c041c295d5 | ||
|
|
c582896574 | ||
|
|
3e6b59fe1e | ||
|
|
62714b2b68 | ||
|
|
07fdd5b7af | ||
|
|
720f435987 | ||
|
|
52061d6c1a | ||
|
|
ae35164a57 | ||
|
|
d1c814bd6b | ||
|
|
bb50512c89 | ||
|
|
0fae45edc9 | ||
|
|
0a8d3cc8fa | ||
|
|
db09947a67 | ||
|
|
5eb600726f | ||
|
|
17a2e6e1f6 | ||
|
|
53e7959d54 | ||
|
|
5f140c5fc4 | ||
|
|
688b8e5229 | ||
|
|
34b50b45a3 | ||
|
|
c17c9c6cc9 | ||
|
|
c9d72b5253 | ||
|
|
f5dbb28fb2 | ||
|
|
9acfe5c1cc | ||
|
|
701cbcfbad | ||
|
|
38685127d2 | ||
|
|
4275f6c6b6 | ||
|
|
bfc186b612 | ||
|
|
2cbcf1a689 | ||
|
|
1c1c0d70c5 | ||
|
|
a66f5fb573 | ||
|
|
9affeab755 | ||
|
|
2bfaf77908 | ||
|
|
bc4caae796 | ||
|
|
8746acd329 | ||
|
|
96ecf16da2 | ||
|
|
1e95a0f3ef | ||
|
|
a164d793b1 | ||
|
|
cb4d92ccf4 | ||
|
|
1dc7256fb5 | ||
|
|
012e710e45 | ||
|
|
5abb7d0286 | ||
|
|
ce74946706 | ||
|
|
bf351d67e9 | ||
|
|
b75fa013d2 | ||
|
|
2601b0d89c | ||
|
|
ef8410e121 | ||
|
|
37610703c8 | ||
|
|
4efd9bba8a | ||
|
|
e1fe7976d8 | ||
|
|
53b96107d9 | ||
|
|
510fc71b40 | ||
|
|
2a6a3edb77 | ||
|
|
c7a8796a47 | ||
|
|
dcbad9e798 | ||
|
|
26b3212c7e | ||
|
|
9d40fa5f2b | ||
|
|
8f2a023775 | ||
|
|
989b0b34fe | ||
|
|
f3f4bcfe45 | ||
|
|
cf94e71215 | ||
|
|
49896f3fa6 | ||
|
|
7cfa9de75f | ||
|
|
fc4b7674b1 | ||
|
|
04c9f32539 | ||
|
|
21e3fc9bb9 | ||
|
|
4b78eb7656 | ||
|
|
b66dc8894d | ||
|
|
14a7813ab0 | ||
|
|
70a2ca281f | ||
|
|
d982f042fc | ||
|
|
e60f9e326b | ||
|
|
e6f91aef8e | ||
|
|
8f99f86c8b | ||
|
|
b7eff547c7 | ||
|
|
ba39d189e7 | ||
|
|
78867b2cd9 | ||
|
|
ceb6b64152 | ||
|
|
d253041376 | ||
|
|
cb0aa81f89 | ||
|
|
42061b2f8c | ||
|
|
69bfb89a65 | ||
|
|
e0307f9688 | ||
|
|
1cf353461f | ||
|
|
1786235c86 | ||
|
|
645ba3f9c1 | ||
|
|
b65f6f46e1 | ||
|
|
84ad521b3d | ||
|
|
dfb9c662e7 | ||
|
|
5ac42e17b0 | ||
|
|
be2f19637e | ||
|
|
1dff42dc00 | ||
|
|
0c9b3a0765 | ||
|
|
5a109c0ba8 | ||
|
|
f3b214c30a | ||
|
|
c49d2a0be6 | ||
|
|
c6c3170c1b | ||
|
|
0abb958aea | ||
|
|
9d55843629 | ||
|
|
b70d309297 | ||
|
|
5961b71562 | ||
|
|
6942626f60 | ||
|
|
069c0acdff | ||
|
|
1f0d83190d | ||
|
|
b7a6ee3792 | ||
|
|
1fb2f0c989 | ||
|
|
7c6c92c856 | ||
|
|
b4ad411e6f | ||
|
|
5d76a92f73 | ||
|
|
beee09491a | ||
|
|
ee5aabdddf | ||
|
|
ec80f6a6f1 | ||
|
|
9845f0b47c | ||
|
|
cd294ba619 | ||
|
|
61e27cb1ea | ||
|
|
8d6295e8e8 | ||
|
|
b0e95699f7 | ||
|
|
c8e1e7b8a8 | ||
|
|
d2cea159af | ||
|
|
eb5d1c79c8 | ||
|
|
65ab6848ab | ||
|
|
7a1d934e8d | ||
|
|
cbacde12fa | ||
|
|
eff352cde1 | ||
|
|
62a75c188c | ||
|
|
4ffa6b6186 | ||
|
|
25173cf605 | ||
|
|
3277d8e80b | ||
|
|
55864fdc82 | ||
|
|
d4d662ba46 | ||
|
|
3ea5f508bb | ||
|
|
902a5dd678 | ||
|
|
4a3ed62583 | ||
|
|
b4223e9e92 | ||
|
|
99955d7818 | ||
|
|
f66768726c | ||
|
|
0e4be02b2c | ||
|
|
6daea23b3c | ||
|
|
e21ddcb1e5 | ||
|
|
ded85d9f27 | ||
|
|
eea43494da | ||
|
|
9cf9ef927d | ||
|
|
779ec4f583 | ||
|
|
c541831cd2 | ||
|
|
fd20c2a554 | ||
|
|
14fd29808c | ||
|
|
7132ee157f | ||
|
|
1596b313d5 | ||
|
|
4c33618e05 | ||
|
|
3837b3e630 | ||
|
|
7c15633f6d | ||
|
|
f7ec8650eb | ||
|
|
7674eee0fb | ||
|
|
f494a6453a | ||
|
|
37f3682ffa | ||
|
|
8055286a1f | ||
|
|
70cd68ded7 | ||
|
|
cc91a6185e | ||
|
|
1fd7c84583 | ||
|
|
0269540ee9 | ||
|
|
0bdd213761 | ||
|
|
810b43760e | ||
|
|
424d71c55a | ||
|
|
176924241c | ||
|
|
da08aa7fb0 | ||
|
|
6047227648 | ||
|
|
fc71fd6bc3 | ||
|
|
90a1b135e1 | ||
|
|
e19413b6ca | ||
|
|
0dfc10af5f | ||
|
|
bbbc419bea | ||
|
|
50ad5e376f | ||
|
|
a9f2254bbc | ||
|
|
a8836404d4 |
12
.github/workflows/cast_deployment.yaml
vendored
12
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=cast/dist --alias dev
|
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=cast/dist --prod
|
npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||||
|
|||||||
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@v4.2.4
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -58,9 +58,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -76,9 +76,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@@ -100,9 +100,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -36,14 +36,14 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||||
|
|||||||
12
.github/workflows/demo_deployment.yaml
vendored
12
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=demo/dist --prod
|
npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
||||||
|
|||||||
6
.github/workflows/design_deployment.yaml
vendored
6
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Deploy to Netlify
|
- name: Deploy to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=gallery/dist --prod
|
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
||||||
|
|||||||
6
.github/workflows/design_preview.yaml
vendored
6
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Deploy preview to Netlify
|
- name: Deploy preview to Netlify
|
||||||
id: deploy
|
id: deploy
|
||||||
run: |
|
run: |
|
||||||
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
|
||||||
--json > deploy_output.json
|
--json > deploy_output.json
|
||||||
env:
|
env:
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/labeler.yaml
vendored
2
.github/workflows/labeler.yaml
vendored
@@ -10,6 +10,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Apply labels
|
- name: Apply labels
|
||||||
uses: actions/labeler@v5.0.0
|
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5.0.1
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
process-only: "issues, prs"
|
process-only: "issues, prs"
|
||||||
|
|||||||
10
.github/workflows/nightly.yaml
vendored
10
.github/workflows/nightly.yaml
vendored
@@ -20,15 +20,15 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
|||||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
- name: Send bundle stats and build information to RelativeCI
|
||||||
uses: relative-ci/agent-action@v3.0.1
|
uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
|
||||||
with:
|
with:
|
||||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|||||||
2
.github/workflows/release-drafter.yaml
vendored
2
.github/workflows/release-drafter.yaml
vendored
@@ -18,6 +18,6 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@v6.1.0
|
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
23
.github/workflows/release.yaml
vendored
23
.github/workflows/release.yaml
vendored
@@ -23,10 +23,10 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@v2.3.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
@@ -73,8 +73,9 @@ jobs:
|
|||||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||||
|
|
||||||
|
# home-assistant/wheels doesn't support SHA pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.07.0
|
uses: home-assistant/wheels@2025.10.0
|
||||||
with:
|
with:
|
||||||
abi: cp313
|
abi: cp313
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -90,9 +91,9 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -107,7 +108,7 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@v2.3.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||||
|
|
||||||
@@ -119,9 +120,9 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.4.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -136,6 +137,6 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@v2.3.2
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||||
|
|||||||
2
.github/workflows/restrict-task-creation.yml
vendored
2
.github/workflows/restrict-task-creation.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
if: github.event.issue.type.name == 'Task'
|
if: github.event.issue.type.name == 'Task'
|
||||||
steps:
|
steps:
|
||||||
- name: Check if user is authorized
|
- name: Check if user is authorized
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
const issueAuthor = context.payload.issue.user.login;
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 90 days stale policy
|
- name: 90 days stale policy
|
||||||
uses: actions/stale@v9.1.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
|
|||||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
/hass_frontend/
|
/hass_frontend/
|
||||||
|
/logs/dist/
|
||||||
/translations/
|
/translations/
|
||||||
|
|
||||||
# yarn
|
# yarn
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
|
|||||||
module.exports.ignorePackages = () => [];
|
module.exports.ignorePackages = () => [];
|
||||||
|
|
||||||
// Files from NPM packages that we should replace with empty file
|
// Files from NPM packages that we should replace with empty file
|
||||||
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||||
[
|
[
|
||||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||||
isHassioBuild &&
|
(isHassioBuild || isLandingPageBuild) &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||||
),
|
),
|
||||||
isHassioBuild &&
|
(isHassioBuild || isLandingPageBuild) &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||||
),
|
),
|
||||||
@@ -183,7 +183,6 @@ module.exports.babelOptions = ({
|
|||||||
include: /\/node_modules\//,
|
include: /\/node_modules\//,
|
||||||
exclude: [
|
exclude: [
|
||||||
"element-internals-polyfill",
|
"element-internals-polyfill",
|
||||||
"@shoelace-style",
|
|
||||||
"@?lit(?:-labs|-element|-html)?",
|
"@?lit(?:-labs|-element|-html)?",
|
||||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||||
},
|
},
|
||||||
@@ -328,6 +327,20 @@ module.exports.config = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
logs({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||||
|
return {
|
||||||
|
name: "logs" + nameSuffix(latestBuild),
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.logs_dir, "src/entrypoint.ts"),
|
||||||
|
},
|
||||||
|
outputPath: outputPath(paths.logs_output_root, latestBuild),
|
||||||
|
publicPath: publicPath(latestBuild),
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
landingPage({ isProdBuild, latestBuild }) {
|
landingPage({ isProdBuild, latestBuild }) {
|
||||||
return {
|
return {
|
||||||
name: "landing-page" + nameSuffix(latestBuild),
|
name: "landing-page" + nameSuffix(latestBuild),
|
||||||
@@ -338,6 +351,7 @@ module.exports.config = {
|
|||||||
publicPath: publicPath(latestBuild),
|
publicPath: publicPath(latestBuild),
|
||||||
isProdBuild,
|
isProdBuild,
|
||||||
latestBuild,
|
latestBuild,
|
||||||
|
isLandingPageBuild: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ gulp.task(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel("clean-translations", async () =>
|
||||||
|
deleteSync([paths.logs_output_root, paths.build_dir])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-landing-page",
|
"clean-landing-page",
|
||||||
gulp.parallel("clean-translations", async () =>
|
gulp.parallel("clean-translations", async () =>
|
||||||
|
|||||||
@@ -245,6 +245,24 @@ gulp.task(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const LOGS_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"gen-pages-logs-dev",
|
||||||
|
genPagesDevTask(LOGS_PAGE_ENTRIES, paths.logs_dir, paths.logs_output_root)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"gen-pages-logs-prod",
|
||||||
|
genPagesProdTask(
|
||||||
|
LOGS_PAGE_ENTRIES,
|
||||||
|
paths.logs_dir,
|
||||||
|
paths.logs_output_root,
|
||||||
|
paths.logs_output_latest,
|
||||||
|
paths.logs_output_es5
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ gulp.task("copy-static-gallery", async () => {
|
|||||||
copyMdiIcons(paths.gallery_output_static);
|
copyMdiIcons(paths.gallery_output_static);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task("copy-static-logs", async () => {
|
||||||
|
// Copy app static files
|
||||||
|
fs.copySync(polyPath("public/static"), paths.logs_output_static);
|
||||||
|
|
||||||
|
copyFonts(paths.logs_output_static);
|
||||||
|
copyTranslations(paths.logs_output_static);
|
||||||
|
copyLocaleData(paths.logs_output_static);
|
||||||
|
copyMdiIcons(paths.logs_output_static);
|
||||||
|
});
|
||||||
|
|
||||||
gulp.task("copy-static-landing-page", async () => {
|
gulp.task("copy-static-landing-page", async () => {
|
||||||
// Copy landing-page static files
|
// Copy landing-page static files
|
||||||
fs.copySync(
|
fs.copySync(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import "./download-translations.js";
|
|||||||
import "./entry-html.js";
|
import "./entry-html.js";
|
||||||
import "./fetch-nightly-translations.js";
|
import "./fetch-nightly-translations.js";
|
||||||
import "./gallery.js";
|
import "./gallery.js";
|
||||||
|
import "./logs.js";
|
||||||
import "./gather-static.js";
|
import "./gather-static.js";
|
||||||
import "./gen-icons-json.js";
|
import "./gen-icons-json.js";
|
||||||
import "./hassio.js";
|
import "./hassio.js";
|
||||||
|
|||||||
39
build-scripts/gulp/logs.js
Normal file
39
build-scripts/gulp/logs.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import gulp from "gulp";
|
||||||
|
import "./clean.js";
|
||||||
|
import "./entry-html.js";
|
||||||
|
import "./gather-static.js";
|
||||||
|
import "./gen-icons-json.js";
|
||||||
|
import "./translations.js";
|
||||||
|
import "./rspack.js";
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-logs",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel(
|
||||||
|
"gen-icons-json",
|
||||||
|
"gen-pages-logs-dev",
|
||||||
|
"build-translations",
|
||||||
|
"build-locale-data"
|
||||||
|
),
|
||||||
|
"copy-static-logs",
|
||||||
|
"rspack-dev-server-logs"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-logs",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-logs",
|
||||||
|
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||||
|
"copy-static-logs",
|
||||||
|
"rspack-prod-logs",
|
||||||
|
"gen-pages-logs-prod"
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createLandingPageConfig,
|
createLandingPageConfig,
|
||||||
|
createLogsConfig,
|
||||||
} from "../rspack.cjs";
|
} from "../rspack.cjs";
|
||||||
|
|
||||||
const bothBuilds = (createConfigFunc, params) => [
|
const bothBuilds = (createConfigFunc, params) => [
|
||||||
@@ -204,6 +205,25 @@ gulp.task("rspack-prod-gallery", () =>
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task("rspack-dev-server-logs", () =>
|
||||||
|
runDevServer({
|
||||||
|
compiler: rspack(
|
||||||
|
createLogsConfig({ isProdBuild: false, latestBuild: true })
|
||||||
|
),
|
||||||
|
contentBase: paths.logs_output_root,
|
||||||
|
port: 5647,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("rspack-prod-logs", () =>
|
||||||
|
prodBuild(
|
||||||
|
bothBuilds(createLogsConfig, {
|
||||||
|
isProdBuild: true,
|
||||||
|
isStatsBuild: env.isStatsBuild(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task("rspack-watch-landing-page", () => {
|
gulp.task("rspack-watch-landing-page", () => {
|
||||||
// This command will run forever because we don't close compiler
|
// This command will run forever because we don't close compiler
|
||||||
rspack(
|
rspack(
|
||||||
|
|||||||
@@ -59,5 +59,11 @@ module.exports = {
|
|||||||
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
||||||
hassio_publicPath: "/api/hassio/app",
|
hassio_publicPath: "/api/hassio/app",
|
||||||
|
|
||||||
|
logs_dir: path.resolve(__dirname, "../logs"),
|
||||||
|
logs_output_root: path.resolve(__dirname, "../logs/dist"),
|
||||||
|
logs_output_static: path.resolve(__dirname, "../logs/dist/static"),
|
||||||
|
logs_output_latest: path.resolve(__dirname, "../logs/dist/frontend_latest"),
|
||||||
|
logs_output_es5: path.resolve(__dirname, "../logs/dist/frontend_es5"),
|
||||||
|
|
||||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const createRspackConfig = ({
|
|||||||
isStatsBuild,
|
isStatsBuild,
|
||||||
isTestBuild,
|
isTestBuild,
|
||||||
isHassioBuild,
|
isHassioBuild,
|
||||||
|
isLandingPageBuild,
|
||||||
dontHash,
|
dontHash,
|
||||||
}) => {
|
}) => {
|
||||||
if (!dontHash) {
|
if (!dontHash) {
|
||||||
@@ -168,7 +169,9 @@ const createRspackConfig = ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new rspack.NormalModuleReplacementPlugin(
|
new rspack.NormalModuleReplacementPlugin(
|
||||||
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
new RegExp(
|
||||||
|
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||||
|
),
|
||||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||||
),
|
),
|
||||||
!isProdBuild && new LogStartCompilePlugin(),
|
!isProdBuild && new LogStartCompilePlugin(),
|
||||||
@@ -299,6 +302,11 @@ const createHassioConfig = ({
|
|||||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
|
const createLogsConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||||
|
createRspackConfig(
|
||||||
|
bundle.config.logs({ isProdBuild, latestBuild, isStatsBuild })
|
||||||
|
);
|
||||||
|
|
||||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||||
|
|
||||||
@@ -308,6 +316,7 @@ module.exports = {
|
|||||||
createCastConfig,
|
createCastConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
|
createLogsConfig,
|
||||||
createRspackConfig,
|
createRspackConfig,
|
||||||
createLandingPageConfig,
|
createLandingPageConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
} from "../../../../src/common/auth/token_storage";
|
} from "../../../../src/common/auth/token_storage";
|
||||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||||
|
import "../../../../src/components/ha-button";
|
||||||
import "../../../../src/components/ha-icon";
|
import "../../../../src/components/ha-icon";
|
||||||
import "../../../../src/components/ha-list";
|
import "../../../../src/components/ha-list";
|
||||||
import "../../../../src/components/ha-button";
|
|
||||||
import "../../../../src/components/ha-list-item";
|
import "../../../../src/components/ha-list-item";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
|
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
|
||||||
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
||||||
import "../../../../src/layouts/hass-loading-screen";
|
import "../../../../src/layouts/hass-loading-screen";
|
||||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
|
||||||
import "./hc-layout";
|
import "./hc-layout";
|
||||||
|
|
||||||
@customElement("hc-cast")
|
@customElement("hc-cast")
|
||||||
@@ -96,7 +95,9 @@ class HcCast extends LitElement {
|
|||||||
<ha-list @action=${this._handlePickView} activatable>
|
<ha-list @action=${this._handlePickView} activatable>
|
||||||
${(
|
${(
|
||||||
this.lovelaceViews ?? [
|
this.lovelaceViews ?? [
|
||||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
{
|
||||||
|
title: "Home",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
).map(
|
).map(
|
||||||
(view, idx) => html`
|
(view, idx) => html`
|
||||||
@@ -242,7 +243,7 @@ class HcCast extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.question:before {
|
.question:before {
|
||||||
border-radius: 4px;
|
border-radius: var(--ha-border-radius-sm);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ class HcLayout extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
|
||||||
|
var(--ha-border-radius-square) var(--ha-border-radius-square);
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: var(--ha-font-size-m);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
|||||||
longitude: 4.8903147,
|
longitude: 4.8903147,
|
||||||
radius: 100,
|
radius: 100,
|
||||||
friendly_name: "Home",
|
friendly_name: "Home",
|
||||||
icon: "hass:home",
|
icon: "mdi:home",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_number.harmonyvolume": {
|
"input_number.harmonyvolume": {
|
||||||
@@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () =>
|
|||||||
step: 1,
|
step: 1,
|
||||||
mode: "slider",
|
mode: "slider",
|
||||||
friendly_name: "Volume",
|
friendly_name: "Volume",
|
||||||
icon: "hass:volume-high",
|
icon: "mdi:volume-high",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"climate.upstairs": {
|
"climate.upstairs": {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
|||||||
type: "weblink",
|
type: "weblink",
|
||||||
url: "/lovelace/climate",
|
url: "/lovelace/climate",
|
||||||
name: "Climate controls",
|
name: "Climate controls",
|
||||||
icon: "hass:arrow-right",
|
icon: "mdi:arrow-right",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
|
|||||||
type: "weblink",
|
type: "weblink",
|
||||||
url: "/lovelace/overview",
|
url: "/lovelace/overview",
|
||||||
name: "Back",
|
name: "Back",
|
||||||
icon: "hass:arrow-left",
|
icon: "mdi:arrow-left",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
state: "on",
|
state: "on",
|
||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "Home Automation",
|
friendly_name: "Home Automation",
|
||||||
icon: "hass:home-automation",
|
icon: "mdi:home-automation",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_boolean.tvtime": {
|
"input_boolean.tvtime": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
|
|||||||
title: "Home Assistant",
|
title: "Home Assistant",
|
||||||
views: [
|
views: [
|
||||||
{
|
{
|
||||||
icon: "hass:home-assistant",
|
icon: "mdi:home-assistant",
|
||||||
id: "home",
|
id: "home",
|
||||||
title: "Home",
|
title: "Home",
|
||||||
cards: [
|
cards: [
|
||||||
|
|||||||
@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
path: "security",
|
path: "security",
|
||||||
icon: "hass:shield-home",
|
icon: "mdi:shield-home",
|
||||||
name: "Security",
|
name: "Security",
|
||||||
background:
|
background:
|
||||||
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',
|
'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
|
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
|
||||||
padding-top: 48px;
|
padding-top: 48px;
|
||||||
}
|
}
|
||||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import "../../../src/dialogs/more-info/more-info-content";
|
import "../../../src/dialogs/more-info/more-info-content";
|
||||||
import "../../../src/state-summary/state-card-content";
|
import "../../../src/state-summary/state-card-content";
|
||||||
import "../ha-demo-options";
|
import "../ha-demo-options";
|
||||||
import type { HomeAssistant } from "../../../src/types";
|
import type { HomeAssistant } from "../../../src/types";
|
||||||
|
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
|
||||||
|
|
||||||
@customElement("demo-more-info")
|
@customElement("demo-more-info")
|
||||||
class DemoMoreInfo extends LitElement {
|
class DemoMoreInfo extends LitElement {
|
||||||
@@ -21,11 +22,13 @@ class DemoMoreInfo extends LitElement {
|
|||||||
<div class="root">
|
<div class="root">
|
||||||
<div id="card">
|
<div id="card">
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<state-card-content
|
${!computeShowNewMoreInfo(state)
|
||||||
|
? html`<state-card-content
|
||||||
.stateObj=${state}
|
.stateObj=${state}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
in-dialog
|
in-dialog
|
||||||
></state-card-content>
|
></state-card-content>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<more-info-content
|
<more-info-content
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
|||||||
@@ -1106,7 +1106,7 @@ export default {
|
|||||||
friendly_name: "Philips Hue",
|
friendly_name: "Philips Hue",
|
||||||
entity_picture: null,
|
entity_picture: null,
|
||||||
description:
|
description:
|
||||||
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n",
|
"Press the button on the bridge to register Philips Hue with Home Assistant.",
|
||||||
submit_caption: "I have pressed the button",
|
submit_caption: "I have pressed the button",
|
||||||
},
|
},
|
||||||
last_changed: "2018-07-19T10:44:46.515160+00:00",
|
last_changed: "2018-07-19T10:44:46.515160+00:00",
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export const createMediaPlayerEntities = () => [
|
|||||||
new Date().getTime() - 23000
|
new Date().getTime() - 23000
|
||||||
).toISOString(),
|
).toISOString(),
|
||||||
volume_level: 0.5,
|
volume_level: 0.5,
|
||||||
|
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||||
|
source: "AirPlay",
|
||||||
|
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||||
|
sound_mode: "Music",
|
||||||
}),
|
}),
|
||||||
getEntity("media_player", "music_playing", "playing", {
|
getEntity("media_player", "music_playing", "playing", {
|
||||||
friendly_name: "Playing The Music",
|
friendly_name: "Playing The Music",
|
||||||
@@ -24,8 +28,8 @@ export const createMediaPlayerEntities = () => [
|
|||||||
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
|
||||||
media_artist: "Technohead",
|
media_artist: "Technohead",
|
||||||
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
|
||||||
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
|
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping
|
||||||
supported_features: 195135,
|
supported_features: 784959,
|
||||||
entity_picture: "/images/album_cover.jpg",
|
entity_picture: "/images/album_cover.jpg",
|
||||||
media_duration: 300,
|
media_duration: 300,
|
||||||
media_position: 0,
|
media_position: 0,
|
||||||
@@ -34,6 +38,9 @@ export const createMediaPlayerEntities = () => [
|
|||||||
new Date().getTime() - 23000
|
new Date().getTime() - 23000
|
||||||
).toISOString(),
|
).toISOString(),
|
||||||
volume_level: 0.5,
|
volume_level: 0.5,
|
||||||
|
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||||
|
sound_mode: "Music",
|
||||||
|
group_members: ["media_player.playing", "media_player.stream_playing"],
|
||||||
}),
|
}),
|
||||||
getEntity("media_player", "stream_playing", "playing", {
|
getEntity("media_player", "stream_playing", "playing", {
|
||||||
friendly_name: "Playing the Stream",
|
friendly_name: "Playing the Stream",
|
||||||
@@ -149,15 +156,18 @@ export const createMediaPlayerEntities = () => [
|
|||||||
}),
|
}),
|
||||||
getEntity("media_player", "receiver_on", "on", {
|
getEntity("media_player", "receiver_on", "on", {
|
||||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||||
|
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||||
volume_level: 0.63,
|
volume_level: 0.63,
|
||||||
is_volume_muted: false,
|
is_volume_muted: false,
|
||||||
source: "TV",
|
source: "TV",
|
||||||
|
sound_mode: "Movie",
|
||||||
friendly_name: "Receiver (selectable sources)",
|
friendly_name: "Receiver (selectable sources)",
|
||||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||||
supported_features: 84364,
|
supported_features: 84364,
|
||||||
}),
|
}),
|
||||||
getEntity("media_player", "receiver_off", "off", {
|
getEntity("media_player", "receiver_off", "off", {
|
||||||
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
|
||||||
|
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
|
||||||
friendly_name: "Receiver (selectable sources)",
|
friendly_name: "Receiver (selectable sources)",
|
||||||
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
|
||||||
supported_features: 84364,
|
supported_features: 84364,
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ class HaGallery extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar a[active]::before {
|
.sidebar a[active]::before {
|
||||||
border-radius: 12px;
|
border-radius: var(--ha-border-radius-lg);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
@@ -241,7 +241,7 @@ class HaGallery extends LitElement {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: var(--ha-border-radius-lg);
|
||||||
background-color: var(--primary-background-color);
|
background-color: var(--primary-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
.card-content {
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: var(--ha-space-6);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,11 +155,11 @@ export class DemoHaButton extends LitElement {
|
|||||||
.card-content {
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: var(--ha-space-6);
|
||||||
}
|
}
|
||||||
.card-content div {
|
.card-content div {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: var(--ha-space-2);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import { css, html, LitElement } from "lit";
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import "../../../../src/components/ha-control-button";
|
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-svg-icon";
|
import "../../../../src/components/ha-control-button";
|
||||||
import "../../../../src/components/ha-control-button-group";
|
import "../../../../src/components/ha-control-button-group";
|
||||||
|
import "../../../../src/components/ha-svg-icon";
|
||||||
|
|
||||||
interface Button {
|
interface Button {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement {
|
|||||||
--control-button-icon-color: var(--primary-color);
|
--control-button-icon-color: var(--primary-color);
|
||||||
--control-button-background-color: var(--primary-color);
|
--control-button-background-color: var(--primary-color);
|
||||||
--control-button-background-opacity: 0.2;
|
--control-button-background-opacity: 0.2;
|
||||||
--control-button-border-radius: 18px;
|
--control-button-border-radius: var(--ha-border-radius-xl);
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
.custom-group {
|
.custom-group {
|
||||||
--control-button-group-thickness: 100px;
|
--control-button-group-thickness: 100px;
|
||||||
--control-button-group-border-radius: 36px;
|
--control-button-group-border-radius: var(--ha-border-radius-6xl);
|
||||||
--control-button-group-spacing: 20px;
|
--control-button-group-spacing: 20px;
|
||||||
}
|
}
|
||||||
.custom-group ha-control-button {
|
.custom-group ha-control-button {
|
||||||
--control-button-border-radius: 18px;
|
--control-button-border-radius: var(--ha-border-radius-xl);
|
||||||
--mdc-icon-size: 32px;
|
--mdc-icon-size: 32px;
|
||||||
}
|
}
|
||||||
.vertical-buttons {
|
.vertical-buttons {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-control-number-buttons";
|
import "../../../../src/components/ha-control-number-buttons";
|
||||||
import { repeat } from "lit/directives/repeat";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
|
|
||||||
const buttons: {
|
const buttons: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
|
|||||||
--control-number-buttons-background-color: #2196f3;
|
--control-number-buttons-background-color: #2196f3;
|
||||||
--control-number-buttons-background-opacity: 0.1;
|
--control-number-buttons-background-opacity: 0.1;
|
||||||
--control-number-buttons-thickness: 100px;
|
--control-number-buttons-thickness: 100px;
|
||||||
--control-number-buttons-border-radius: 36px;
|
--control-number-buttons-border-radius: var(--ha-border-radius-6xl);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export class DemoHaControlSelectMenu extends LitElement {
|
|||||||
--control-button-icon-color: var(--primary-color);
|
--control-button-icon-color: var(--primary-color);
|
||||||
--control-button-background-color: var(--primary-color);
|
--control-button-background-color: var(--primary-color);
|
||||||
--control-button-background-opacity: 0.2;
|
--control-button-background-opacity: 0.2;
|
||||||
--control-button-border-radius: 18px;
|
--control-button-border-radius: var(--ha-border-radius-xl);
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
|
|||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
--control-select-color: var(--state-fan-active-color);
|
--control-select-color: var(--state-fan-active-color);
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 36px;
|
--control-select-border-radius: var(--ha-border-radius-6xl);
|
||||||
}
|
}
|
||||||
.vertical-selects {
|
.vertical-selects {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
|
|||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import "../../../../src/components/ha-control-slider";
|
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-slider";
|
||||||
|
|
||||||
const sliders: {
|
const sliders: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
--control-slider-background: #ffcf4c;
|
--control-slider-background: #ffcf4c;
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 36px;
|
--control-slider-border-radius: var(--ha-border-radius-6xl);
|
||||||
}
|
}
|
||||||
.vertical-sliders {
|
.vertical-sliders {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit";
|
|||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import "../../../../src/components/ha-control-switch";
|
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-switch";
|
||||||
|
|
||||||
const switches: {
|
const switches: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
|
|||||||
--control-switch-on-color: var(--green-color);
|
--control-switch-on-color: var(--green-color);
|
||||||
--control-switch-off-color: var(--red-color);
|
--control-switch-off-color: var(--red-color);
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 36px;
|
--control-switch-border-radius: var(--ha-border-radius-6xl);
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
|
|||||||
|
|
||||||
# Material Design 3
|
# Material Design 3
|
||||||
|
|
||||||
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
||||||
|
|
||||||
# Guidelines
|
# Guidelines
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
|
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
|
||||||
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
|
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
|
||||||
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
|
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
|
||||||
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
|
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
|
||||||
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
|
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
|
||||||
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
|
|||||||
|
|
||||||
- A best practice is to always use a title, even if it is optional by Material guidelines.
|
- A best practice is to always use a title, even if it is optional by Material guidelines.
|
||||||
- People mainly read the title and a button. Put the most important information in those two.
|
- People mainly read the title and a button. Put the most important information in those two.
|
||||||
- Try to avoid user generated content in the title, this could make the title unreadable long.
|
- Try to avoid user generated content in the title, this could make the title unreadably long.
|
||||||
- If users become unsure, they read the description. Make sure this explains what will happen.
|
- If users become unsure, they read the description. Make sure this explains what will happen.
|
||||||
- Strive for minimalism.
|
- Strive for minimalism.
|
||||||
|
|
||||||
|
|||||||
37
gallery/src/pages/components/ha-marquee-text.markdown
Normal file
37
gallery/src/pages/components/ha-marquee-text.markdown
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Marquee Text
|
||||||
|
---
|
||||||
|
|
||||||
|
# Marquee Text `<ha-marquee-text>`
|
||||||
|
|
||||||
|
Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
<ha-marquee-text style="width: 200px;">
|
||||||
|
This is a long text that will scroll horizontally if it overflows the container.
|
||||||
|
</ha-marquee-text>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ha-marquee-text style="width: 200px;">
|
||||||
|
This is a long text that will scroll horizontally if it overflows the
|
||||||
|
container.
|
||||||
|
</ha-marquee-text>
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
**Slots**
|
||||||
|
|
||||||
|
- default slot: The text content to be displayed and scrolled.
|
||||||
|
- no default
|
||||||
|
|
||||||
|
**Properties/Attributes**
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------------- | ------- | ------- | ---------------------------------------------------------------------------- |
|
||||||
|
| speed | number | `15` | The speed of the scrolling animation. Higher values result in faster scroll. |
|
||||||
|
| pause-on-hover | boolean | `true` | Whether to pause the scrolling animation when |
|
||||||
|
| pause-duration | number | `1000` | The delay in milliseconds before the scrolling animation starts/restarts. |
|
||||||
25
gallery/src/pages/components/ha-marquee-text.ts
Normal file
25
gallery/src/pages/components/ha-marquee-text.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { css, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-marquee-text";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-marquee-text")
|
||||||
|
export class DemoHaMarqueeText extends LitElement {
|
||||||
|
static styles = css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-marquee-text": DemoHaMarqueeText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,11 +123,11 @@ export class DemoHaProgressButton extends LitElement {
|
|||||||
.card-content {
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: var(--ha-space-6);
|
||||||
}
|
}
|
||||||
.card-content div {
|
.card-content div {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: var(--ha-space-2);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement {
|
|||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
--control-select-color: var(--state-fan-active-color);
|
--control-select-color: var(--state-fan-active-color);
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 36px;
|
--control-select-border-radius: var(--ha-border-radius-6xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
p.title {
|
p.title {
|
||||||
|
|||||||
38
gallery/src/pages/components/ha-slider.markdown
Normal file
38
gallery/src/pages/components/ha-slider.markdown
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: Slider
|
||||||
|
subtitle: A slider component for selecting a value from a range.
|
||||||
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
# Slider `<ha-slider>`
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
|
||||||
|
<ha-slider size="medium"></ha-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider>
|
||||||
|
<ha-slider size="medium"></ha-slider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
This component is based on the webawesome slider component.
|
||||||
|
Check the [webawesome documentation](https://webawesome.com/docs/components/slider/) for more details.
|
||||||
|
|
||||||
|
**CSS Custom Properties**
|
||||||
|
|
||||||
|
- `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`.
|
||||||
|
- `--ha-slider-thumb-color` - Color of the slider thumb. Defaults to `var(--primary-color)`.
|
||||||
|
- `--ha-slider-indicator-color` - Color of the filled portion of the slider track. Defaults to `var(--primary-color)`.
|
||||||
100
gallery/src/pages/components/ha-slider.ts
Normal file
100
gallery/src/pages/components/ha-slider.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||||
|
import "../../../../src/components/ha-bar";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-spinner";
|
||||||
|
import "../../../../src/components/ha-slider";
|
||||||
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-slider")
|
||||||
|
export class DemoHaSlider extends LitElement {
|
||||||
|
@property({ attribute: false }) hass!: HomeAssistant;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${["light", "dark"].map(
|
||||||
|
(mode) => html`
|
||||||
|
<div class=${mode}>
|
||||||
|
<ha-card header="ha-slider ${mode} demo">
|
||||||
|
<div class="card-content">
|
||||||
|
<span>Default (disabled)</span>
|
||||||
|
<ha-slider
|
||||||
|
disabled
|
||||||
|
min="0"
|
||||||
|
max="8"
|
||||||
|
value="4"
|
||||||
|
with-markers
|
||||||
|
></ha-slider>
|
||||||
|
<span>Small</span>
|
||||||
|
<ha-slider
|
||||||
|
size="small"
|
||||||
|
min="0"
|
||||||
|
max="8"
|
||||||
|
value="4"
|
||||||
|
with-markers
|
||||||
|
></ha-slider>
|
||||||
|
<span>Medium</span>
|
||||||
|
<ha-slider
|
||||||
|
size="medium"
|
||||||
|
min="0"
|
||||||
|
max="8"
|
||||||
|
value="4"
|
||||||
|
with-markers
|
||||||
|
></ha-slider>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
applyThemesOnElement(
|
||||||
|
this.shadowRoot!.querySelector(".dark"),
|
||||||
|
{
|
||||||
|
default_theme: "default",
|
||||||
|
default_dark_theme: "default",
|
||||||
|
themes: {},
|
||||||
|
darkMode: true,
|
||||||
|
theme: "default",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.dark,
|
||||||
|
.light {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
padding: 0 50px;
|
||||||
|
margin: 16px;
|
||||||
|
border-radius: var(--ha-border-radius-md);
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--ha-space-6);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-slider": DemoHaSlider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement {
|
|||||||
background-color: var(--primary-background-color);
|
background-color: var(--primary-background-color);
|
||||||
padding: 0 50px;
|
padding: 0 50px;
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
border-radius: 8px;
|
border-radius: var(--ha-border-radius-md);
|
||||||
}
|
}
|
||||||
ha-card {
|
ha-card {
|
||||||
margin: 24px auto;
|
margin: 24px auto;
|
||||||
@@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: var(--ha-space-6);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,23 @@ A tooltip's target is its _first child element_, so you should only wrap one ele
|
|||||||
|
|
||||||
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
|
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 id="hover">Hover Me</ha-button>
|
||||||
<ha-button>Hover Me</ha-button>
|
<ha-tooltip for="hover">
|
||||||
|
This is a tooltip
|
||||||
</ha-tooltip>
|
</ha-tooltip>
|
||||||
|
|
||||||
```
|
```
|
||||||
<ha-tooltip content="This is a tooltip">
|
<ha-button id="hover">Hover Me</ha-button>
|
||||||
<ha-button>Hover Me</ha-button>
|
<ha-tooltip for="hover">
|
||||||
|
This is a tooltip
|
||||||
</ha-tooltip>
|
</ha-tooltip>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
This element is based on shoelace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation.
|
This element is based on webawesome `wa-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>
|
<a href="https://webawesome.com/docs/components/tooltip/" target="_blank" rel="noopener noreferrer">Webawesome documentation</a>
|
||||||
|
|
||||||
### HA style tokens
|
### HA style tokens
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ In your theme settings use this without the prefixed `--`.
|
|||||||
|
|
||||||
- `--ha-tooltip-border-radius` (Default: 4px)
|
- `--ha-tooltip-border-radius` (Default: 4px)
|
||||||
- `--ha-tooltip-arrow-size` (Default: 8px)
|
- `--ha-tooltip-arrow-size` (Default: 8px)
|
||||||
- `--sl-tooltip-font-family` (Default: `var(--ha-font-family-body)`)
|
- `--wa-tooltip-font-family` (Default: `var(--ha-font-family-body)`)
|
||||||
- `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`)
|
- `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`)
|
||||||
- `--sl-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`)
|
- `--wa-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`)
|
||||||
- `--sl-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`)
|
- `--wa-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`)
|
||||||
|
|||||||
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Dialog (ha-wa-dialog)
|
||||||
|
---
|
||||||
523
gallery/src/pages/components/ha-wa-dialog.ts
Normal file
523
gallery/src/pages/components/ha-wa-dialog.ts
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import { mdiCog, mdiHelp } from "@mdi/js";
|
||||||
|
import "../../../../src/components/ha-button";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-dialog-footer";
|
||||||
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
|
import "../../../../src/components/ha-icon-button";
|
||||||
|
import "../../../../src/components/ha-wa-dialog";
|
||||||
|
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||||
|
|
||||||
|
const SCHEMA: HaFormSchema[] = [
|
||||||
|
{ type: "string", name: "Name", default: "", autofocus: true },
|
||||||
|
{ type: "string", name: "Email", default: "" },
|
||||||
|
];
|
||||||
|
|
||||||
|
type DialogType =
|
||||||
|
| false
|
||||||
|
| "basic"
|
||||||
|
| "basic-subtitle-below"
|
||||||
|
| "basic-subtitle-above"
|
||||||
|
| "form"
|
||||||
|
| "actions";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-wa-dialog")
|
||||||
|
export class DemoHaWaDialog extends LitElement {
|
||||||
|
@state() private _openDialog: DialogType = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div class="content">
|
||||||
|
<h1>Dialog <code><ha-wa-dialog></code></h1>
|
||||||
|
|
||||||
|
<p class="subtitle">Dialog component built with WebAwesome.</p>
|
||||||
|
|
||||||
|
<h2>Demos</h2>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<ha-button @click=${this._handleOpenDialog("basic")}
|
||||||
|
>Basic dialog</ha-button
|
||||||
|
>
|
||||||
|
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
|
||||||
|
>Basic dialog with subtitle below</ha-button
|
||||||
|
>
|
||||||
|
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
|
||||||
|
>Basic dialog with subtitle above</ha-button
|
||||||
|
>
|
||||||
|
<ha-button @click=${this._handleOpenDialog("form")}
|
||||||
|
>Dialog with form</ha-button
|
||||||
|
>
|
||||||
|
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||||
|
>Dialog with actions</ha-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-wa-dialog
|
||||||
|
.open=${this._openDialog === "basic"}
|
||||||
|
header-title="Basic dialog"
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<div>Dialog content</div>
|
||||||
|
</ha-wa-dialog>
|
||||||
|
|
||||||
|
<ha-wa-dialog
|
||||||
|
.open=${this._openDialog === "basic-subtitle-below"}
|
||||||
|
header-title="Basic dialog with subtitle"
|
||||||
|
header-subtitle="This is a basic dialog with a subtitle below"
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<div>Dialog content</div>
|
||||||
|
</ha-wa-dialog>
|
||||||
|
|
||||||
|
<ha-wa-dialog
|
||||||
|
.open=${this._openDialog === "basic-subtitle-above"}
|
||||||
|
header-title="Dialog with subtitle above"
|
||||||
|
header-subtitle="This is a basic dialog with a subtitle above"
|
||||||
|
header-subtitle-position="above"
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<div>Dialog content</div>
|
||||||
|
</ha-wa-dialog>
|
||||||
|
|
||||||
|
<ha-wa-dialog
|
||||||
|
.open=${this._openDialog === "form"}
|
||||||
|
header-title="Dialog with form"
|
||||||
|
header-subtitle="This is a dialog with a form and a footer"
|
||||||
|
prevent-scrim-close
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||||
|
<ha-dialog-footer slot="footer">
|
||||||
|
<ha-button
|
||||||
|
data-dialog="close"
|
||||||
|
slot="secondaryAction"
|
||||||
|
variant="plain"
|
||||||
|
>Cancel</ha-button
|
||||||
|
>
|
||||||
|
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
|
||||||
|
>Submit</ha-button
|
||||||
|
>
|
||||||
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog>
|
||||||
|
|
||||||
|
<ha-wa-dialog
|
||||||
|
.open=${this._openDialog === "actions"}
|
||||||
|
header-title="Dialog with actions"
|
||||||
|
header-subtitle="This is a dialog with header actions"
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<div slot="headerActionItems">
|
||||||
|
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
|
||||||
|
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Dialog content</div>
|
||||||
|
</ha-wa-dialog>
|
||||||
|
|
||||||
|
<h2>Design</h2>
|
||||||
|
|
||||||
|
<h3>Width</h3>
|
||||||
|
|
||||||
|
<p>There are multiple widths available for the dialog.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>small</code></td>
|
||||||
|
<td><code>min(320px, var(--full-width))</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>medium</code></td>
|
||||||
|
<td><code>min(580px, var(--full-width))</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>large</code></td>
|
||||||
|
<td><code>min(720px, var(--full-width))</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>full</code></td>
|
||||||
|
<td><code>var(--full-width)</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<code>--full-width</code> is calculated based on the available width
|
||||||
|
of the screen. 95vw is the maximum width of the dialog on a large
|
||||||
|
screen, while on a small screen it is 100vw minus the safe area
|
||||||
|
insets.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Dialogs have a default width of <code>medium</code>.</p>
|
||||||
|
|
||||||
|
<h3>Prevent scrim close</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can prevent the dialog from being closed by clicking the
|
||||||
|
scrim/overlay. This is allowed by default.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Header</h3>
|
||||||
|
|
||||||
|
<p>The header contains a title, a subtitle and action items.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Slot</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>header</code></td>
|
||||||
|
<td>The entire header area.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>headerTitle</code></td>
|
||||||
|
<td>The header title text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>headerSubtitle</code></td>
|
||||||
|
<td>The header subtitle text.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>headerActionItems</code></td>
|
||||||
|
<td>The header action items.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>Header title</h4>
|
||||||
|
|
||||||
|
<p>The header title is a text string.</p>
|
||||||
|
|
||||||
|
<h4>Header subtitle</h4>
|
||||||
|
|
||||||
|
<p>The header subtitle is a text string.</p>
|
||||||
|
|
||||||
|
<h4>Header action items</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The header action items usually containing icon buttons and/or menu
|
||||||
|
buttons.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Body</h3>
|
||||||
|
|
||||||
|
<p>The body is the content of the dialog.</p>
|
||||||
|
|
||||||
|
<h3>Footer</h3>
|
||||||
|
|
||||||
|
<p>The footer is the footer of the dialog.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
It is recommended to use the <code>ha-dialog-footer</code> component
|
||||||
|
for the footer and to style the buttons inside the footer as so:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Slot</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Variant to use</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>secondaryAction</code></td>
|
||||||
|
<td>The secondary action button(s).</td>
|
||||||
|
<td><code>plain</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>primaryAction</code></td>
|
||||||
|
<td>The primary action button(s).</td>
|
||||||
|
<td><code>accent</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Implementation</h2>
|
||||||
|
|
||||||
|
<h3>Example Usage</h3>
|
||||||
|
|
||||||
|
<pre><code><ha-wa-dialog
|
||||||
|
open
|
||||||
|
header-title="Dialog title"
|
||||||
|
header-subtitle="Dialog subtitle"
|
||||||
|
prevent-scrim-close
|
||||||
|
>
|
||||||
|
<div slot="headerActionItems">
|
||||||
|
<ha-icon-button label="Settings" path="mdiCog"></ha-icon-button>
|
||||||
|
<ha-icon-button label="Help" path="mdiHelp"></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
<div>Dialog content</div>
|
||||||
|
<ha-dialog-footer slot="footer">
|
||||||
|
<ha-button data-dialog="close" slot="secondaryAction" variant="plain"
|
||||||
|
>Cancel</ha-button
|
||||||
|
>
|
||||||
|
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||||
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog></code></pre>
|
||||||
|
|
||||||
|
<h3>API</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This component is based on the webawesome dialog component. Check the
|
||||||
|
<a
|
||||||
|
href="https://webawesome.com/docs/components/dialog/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>webawesome documentation</a
|
||||||
|
>
|
||||||
|
for more details.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Attributes</h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Attribute</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Default</th>
|
||||||
|
<th>Options</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>open</code></td>
|
||||||
|
<td>Controls the dialog open state.</td>
|
||||||
|
<td><code>false</code></td>
|
||||||
|
<td><code>false</code>, <code>true</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>width</code></td>
|
||||||
|
<td>Preferred dialog width preset.</td>
|
||||||
|
<td><code>medium</code></td>
|
||||||
|
<td>
|
||||||
|
<code>small</code>, <code>medium</code>, <code>large</code>,
|
||||||
|
<code>full</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>prevent-scrim-close</code></td>
|
||||||
|
<td>
|
||||||
|
Prevents closing the dialog by clicking the scrim/overlay.
|
||||||
|
</td>
|
||||||
|
<td><code>false</code></td>
|
||||||
|
<td><code>true</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>header-title</code></td>
|
||||||
|
<td>Header title text when no custom title slot is provided.</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>header-subtitle</code></td>
|
||||||
|
<td>
|
||||||
|
Header subtitle text when no custom subtitle slot is provided.
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>header-subtitle-position</code></td>
|
||||||
|
<td>Position of the subtitle relative to the title.</td>
|
||||||
|
<td><code>below</code></td>
|
||||||
|
<td><code>above</code>, <code>below</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>flexcontent</code></td>
|
||||||
|
<td>
|
||||||
|
Makes the dialog body a flex container for flexible layouts.
|
||||||
|
</td>
|
||||||
|
<td><code>false</code></td>
|
||||||
|
<td><code>false</code>, <code>true</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>CSS Custom Properties</h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>CSS Property</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>--dialog-content-padding</code></td>
|
||||||
|
<td>Padding for dialog content sections.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--ha-dialog-show-duration</code></td>
|
||||||
|
<td>Show animation duration.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--ha-dialog-hide-duration</code></td>
|
||||||
|
<td>Hide animation duration.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--ha-dialog-surface-background</code></td>
|
||||||
|
<td>Dialog background color.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--ha-dialog-border-radius</code></td>
|
||||||
|
<td>Border radius of the dialog surface.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--dialog-z-index</code></td>
|
||||||
|
<td>Z-index for the dialog.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--dialog-surface-position</code></td>
|
||||||
|
<td>CSS position of the dialog surface.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>--dialog-surface-margin-top</code></td>
|
||||||
|
<td>Top margin for the dialog surface.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>Events</h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>opened</code></td>
|
||||||
|
<td>Fired when the dialog is shown.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>closed</code></td>
|
||||||
|
<td>Fired after the dialog is hidden.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleOpenDialog = (dialog: DialogType) => () => {
|
||||||
|
this._openDialog = dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _handleClosed = () => {
|
||||||
|
this._openDialog = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
padding: var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--ha-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: var(--ha-space-6);
|
||||||
|
margin-bottom: var(--ha-space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
margin-top: var(--ha-space-4);
|
||||||
|
margin-bottom: var(--ha-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: var(--ha-space-2) 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: var(--ha-space-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
text-align: left;
|
||||||
|
padding: var(--ha-space-2);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
padding: var(--ha-space-3);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: var(--ha-space-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ha-space-2);
|
||||||
|
margin: var(--ha-space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-wa-dialog": DemoHaWaDialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,13 @@ import type {
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||||
import { computeDomain } from "../../../../src/common/entity/compute_domain";
|
import { computeDomain } from "../../../../src/common/entity/compute_domain";
|
||||||
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
|
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
|
||||||
import "../../../../src/components/data-table/ha-data-table";
|
import "../../../../src/components/data-table/ha-data-table";
|
||||||
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
|
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
|
||||||
import "../../../../src/components/entity/state-badge";
|
import "../../../../src/components/entity/state-badge";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
|
|
||||||
const SENSOR_DEVICE_CLASSES = [
|
const SENSOR_DEVICE_CLASSES = [
|
||||||
@@ -39,6 +39,7 @@ const SENSOR_DEVICE_CLASSES = [
|
|||||||
"pm1",
|
"pm1",
|
||||||
"pm10",
|
"pm10",
|
||||||
"pm25",
|
"pm25",
|
||||||
|
"pm4",
|
||||||
"power_factor",
|
"power_factor",
|
||||||
"power",
|
"power",
|
||||||
"precipitation",
|
"precipitation",
|
||||||
@@ -434,7 +435,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 10px;
|
border-radius: var(--ha-border-radius-md);
|
||||||
background-color: rgb(--color);
|
background-color: rgb(--color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import "../../../../src/components/ha-alert";
|
|||||||
import "../../../../src/components/ha-button-menu";
|
import "../../../../src/components/ha-button-menu";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-form/ha-form";
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
import type {
|
||||||
|
HaFormSchema,
|
||||||
|
HaFormDataContainer,
|
||||||
|
} from "../../../../src/components/ha-form/types";
|
||||||
import "../../../../src/components/ha-formfield";
|
import "../../../../src/components/ha-formfield";
|
||||||
import "../../../../src/components/ha-icon-button";
|
import "../../../../src/components/ha-icon-button";
|
||||||
import "../../../../src/components/ha-list-item";
|
import "../../../../src/components/ha-list-item";
|
||||||
@@ -33,6 +36,7 @@ import { haStyle } from "../../../../src/resources/styles";
|
|||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||||
import { hassioStyle } from "../../resources/hassio-style";
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
|
||||||
|
|
||||||
const SUPPORTED_UI_TYPES = [
|
const SUPPORTED_UI_TYPES = [
|
||||||
"string",
|
"string",
|
||||||
@@ -78,41 +82,83 @@ class HassioAddonConfig extends LitElement {
|
|||||||
|
|
||||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||||
|
|
||||||
public computeLabel = (entry: HaFormSchema): string =>
|
private _getTranslationEntry(
|
||||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
language: string,
|
||||||
?.name ||
|
entry: HaFormSchema,
|
||||||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
|
options?: { path?: string[] }
|
||||||
|
) {
|
||||||
|
let parent = this.addon.translations[language]?.configuration;
|
||||||
|
if (!parent) return undefined;
|
||||||
|
if (options?.path) {
|
||||||
|
for (const key of options.path) {
|
||||||
|
parent = parent[key]?.fields;
|
||||||
|
if (!parent) return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parent[entry.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public computeLabel = (
|
||||||
|
entry: HaFormSchema,
|
||||||
|
_data: HaFormDataContainer,
|
||||||
|
options?: { path?: string[] }
|
||||||
|
): string =>
|
||||||
|
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
|
||||||
|
this._getTranslationEntry("en", entry, options)?.name ||
|
||||||
entry.name;
|
entry.name;
|
||||||
|
|
||||||
public computeHelper = (entry: HaFormSchema): string =>
|
public computeHelper = (
|
||||||
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
|
entry: HaFormSchema,
|
||||||
|
options?: { path?: string[] }
|
||||||
|
): string =>
|
||||||
|
this._getTranslationEntry(this.hass.language, entry, options)
|
||||||
?.description ||
|
?.description ||
|
||||||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
|
this._getTranslationEntry("en", entry, options)?.description ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
private _convertSchema = memoizeOne(
|
private _convertSchema = memoizeOne(
|
||||||
// Convert supervisor schema to selectors
|
// Convert supervisor schema to selectors
|
||||||
(schema: Record<string, any>): HaFormSchema[] =>
|
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
|
||||||
schema.map((entry) =>
|
this._convertSchemaElements(schema)
|
||||||
entry.type === "select"
|
);
|
||||||
? {
|
|
||||||
|
private _convertSchemaElements(
|
||||||
|
schema: readonly HaFormSchema[]
|
||||||
|
): HaFormSchema[] {
|
||||||
|
return schema.map((entry) => this._convertSchemaElement(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _convertSchemaElement(entry: any): HaFormSchema {
|
||||||
|
if (entry.type === "schema" && !entry.multiple) {
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
type: "expandable",
|
||||||
|
required: entry.required,
|
||||||
|
schema: this._convertSchemaElements(entry.schema),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const selector = this._convertSchemaElementToSelector(entry, false);
|
||||||
|
if (selector) {
|
||||||
|
return {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
required: entry.required,
|
required: entry.required,
|
||||||
selector: { select: { options: entry.options } },
|
selector,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
: entry.type === "string"
|
return entry;
|
||||||
? entry.multiple
|
|
||||||
? {
|
|
||||||
name: entry.name,
|
|
||||||
required: entry.required,
|
|
||||||
selector: {
|
|
||||||
select: { options: [], multiple: true, custom_value: true },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _convertSchemaElementToSelector(
|
||||||
|
entry: any,
|
||||||
|
force: boolean
|
||||||
|
): Selector | null {
|
||||||
|
if (entry.type === "select") {
|
||||||
|
return { select: { options: entry.options } };
|
||||||
|
}
|
||||||
|
if (entry.type === "string") {
|
||||||
|
return entry.multiple
|
||||||
|
? { select: { options: [], multiple: true, custom_value: true } }
|
||||||
: {
|
: {
|
||||||
name: entry.name,
|
|
||||||
required: entry.required,
|
|
||||||
selector: {
|
|
||||||
text: {
|
text: {
|
||||||
type: entry.format
|
type: entry.format
|
||||||
? entry.format
|
? entry.format
|
||||||
@@ -120,36 +166,41 @@ class HassioAddonConfig extends LitElement {
|
|||||||
? "password"
|
? "password"
|
||||||
: "text",
|
: "text",
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (entry.type === "boolean") {
|
||||||
|
return { boolean: {} };
|
||||||
|
}
|
||||||
|
if (entry.type === "schema") {
|
||||||
|
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
|
||||||
|
for (const child_entry of entry.schema) {
|
||||||
|
fields[child_entry.name] = {
|
||||||
|
required: child_entry.required,
|
||||||
|
selector: this._convertSchemaElementToSelector(child_entry, true)!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
object: {
|
||||||
|
multiple: entry.multiple,
|
||||||
|
fields,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
: entry.type === "boolean"
|
if (entry.type === "float" || entry.type === "integer") {
|
||||||
? {
|
return {
|
||||||
name: entry.name,
|
|
||||||
required: entry.required,
|
|
||||||
selector: { boolean: {} },
|
|
||||||
}
|
|
||||||
: entry.type === "schema"
|
|
||||||
? {
|
|
||||||
name: entry.name,
|
|
||||||
required: entry.required,
|
|
||||||
selector: { object: {} },
|
|
||||||
}
|
|
||||||
: entry.type === "float" || entry.type === "integer"
|
|
||||||
? {
|
|
||||||
name: entry.name,
|
|
||||||
required: entry.required,
|
|
||||||
selector: {
|
|
||||||
number: {
|
number: {
|
||||||
mode: "box",
|
mode: "box",
|
||||||
step: entry.type === "float" ? "any" : undefined,
|
step: entry.type === "float" ? "any" : undefined,
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
if (force) {
|
||||||
|
return { object: {} };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
: entry
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private _filteredShchema = memoizeOne(
|
private _filteredSchema = memoizeOne(
|
||||||
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
|
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
|
||||||
schema.filter((entry) => entry.name in options || entry.required)
|
schema.filter((entry) => entry.name in options || entry.required)
|
||||||
);
|
);
|
||||||
@@ -161,7 +212,7 @@ class HassioAddonConfig extends LitElement {
|
|||||||
showForm &&
|
showForm &&
|
||||||
JSON.stringify(this.addon.schema) !==
|
JSON.stringify(this.addon.schema) !==
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
this._filteredShchema(this.addon.options, this.addon.schema!)
|
this._filteredSchema(this.addon.options, this.addon.schema!)
|
||||||
);
|
);
|
||||||
return html`
|
return html`
|
||||||
<h1>${this.addon.name}</h1>
|
<h1>${this.addon.name}</h1>
|
||||||
@@ -199,6 +250,7 @@ class HassioAddonConfig extends LitElement {
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${showForm
|
${showForm
|
||||||
? html`<ha-form
|
? html`<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.data=${this._options!}
|
.data=${this._options!}
|
||||||
@value-changed=${this._configChanged}
|
@value-changed=${this._configChanged}
|
||||||
@@ -207,7 +259,7 @@ class HassioAddonConfig extends LitElement {
|
|||||||
.schema=${this._convertSchema(
|
.schema=${this._convertSchema(
|
||||||
this._showOptional
|
this._showOptional
|
||||||
? this.addon.schema!
|
? this.addon.schema!
|
||||||
: this._filteredShchema(
|
: this._filteredSchema(
|
||||||
this.addon.options,
|
this.addon.options,
|
||||||
this.addon.schema!
|
this.addon.schema!
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -781,7 +781,7 @@ class HassioAddonInfo extends LitElement {
|
|||||||
|
|
||||||
${this.addon.long_description
|
${this.addon.long_description
|
||||||
? html`
|
? html`
|
||||||
<ha-card outlined>
|
<ha-card class="long-description" outlined>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<ha-markdown
|
<ha-markdown
|
||||||
.content=${this.addon.long_description}
|
.content=${this.addon.long_description}
|
||||||
@@ -1333,6 +1333,9 @@ class HassioAddonInfo extends LitElement {
|
|||||||
.description a {
|
.description a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
.long-description {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
ha-assist-chip {
|
ha-assist-chip {
|
||||||
--md-sys-color-primary: var(--text-primary-color);
|
--md-sys-color-primary: var(--text-primary-color);
|
||||||
--md-sys-color-on-surface: var(--text-primary-color);
|
--md-sys-color-on-surface: var(--text-primary-color);
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class HassioCardContent extends LitElement {
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
border-radius: 50%;
|
border-radius: var(--ha-border-radius-circle);
|
||||||
}
|
}
|
||||||
.topbar {
|
.topbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class HassioHardwareDialog extends LitElement {
|
|||||||
pre,
|
pre,
|
||||||
code {
|
code {
|
||||||
background-color: var(--markdown-code-background-color, none);
|
background-color: var(--markdown-code-background-color, none);
|
||||||
border-radius: 3px;
|
border-radius: var(--ha-border-radius-sm);
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import "../../../../src/components/ha-list";
|
|||||||
import "../../../../src/components/ha-list-item";
|
import "../../../../src/components/ha-list-item";
|
||||||
import "../../../../src/components/ha-password-field";
|
import "../../../../src/components/ha-password-field";
|
||||||
import "../../../../src/components/ha-radio";
|
import "../../../../src/components/ha-radio";
|
||||||
|
import "../../../../src/components/ha-tab-group";
|
||||||
|
import "../../../../src/components/ha-tab-group-tab";
|
||||||
import "../../../../src/components/ha-textfield";
|
import "../../../../src/components/ha-textfield";
|
||||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||||
@@ -36,7 +38,6 @@ import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
|||||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import type { HassioNetworkDialogParams } from "./show-dialog-network";
|
import type { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||||
import "../../../../src/components/sl-tab-group";
|
|
||||||
|
|
||||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||||
|
|
||||||
@@ -114,19 +115,19 @@ export class DialogHassioNetwork
|
|||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
${this._interfaces.length > 1
|
${this._interfaces.length > 1
|
||||||
? html`<sl-tab-group @sl-tab-show=${this._handleTabActivated}
|
? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated}
|
||||||
>${this._interfaces.map(
|
>${this._interfaces.map(
|
||||||
(device, index) =>
|
(device, index) =>
|
||||||
html`<sl-tab
|
html`<ha-tab-group-tab
|
||||||
slot="nav"
|
slot="nav"
|
||||||
.id=${device.interface}
|
.id=${device.interface}
|
||||||
.panel=${index.toString()}
|
.panel=${index.toString()}
|
||||||
.active=${this._curTabIndex === index}
|
.active=${this._curTabIndex === index}
|
||||||
>
|
>
|
||||||
${device.interface}
|
${device.interface}
|
||||||
</sl-tab>`
|
</ha-tab-group-tab>`
|
||||||
)}
|
)}
|
||||||
</sl-tab-group>`
|
</ha-tab-group>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
${cache(this._renderTab())}
|
${cache(this._renderTab())}
|
||||||
@@ -627,10 +628,10 @@ export class DialogHassioNetwork
|
|||||||
--mdc-list-side-padding: 10px;
|
--mdc-list-side-padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
sl-tab {
|
ha-tab-group-tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
sl-tab::part(base) {
|
ha-tab-group-tab::part(base) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class HassioRegistriesDialog extends LitElement {
|
|||||||
css`
|
css`
|
||||||
.registry {
|
.registry {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 4px;
|
border-radius: var(--ha-border-radius-sm);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.action {
|
.action {
|
||||||
|
|||||||
@@ -119,15 +119,17 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
<div>${repo.url}</div>
|
<div>${repo.url}</div>
|
||||||
</div>
|
</div>
|
||||||
<ha-tooltip
|
<ha-tooltip
|
||||||
|
.for="icon-button-${repo.slug}"
|
||||||
class="delete"
|
class="delete"
|
||||||
slot="end"
|
slot="end"
|
||||||
.content=${this._dialogParams!.supervisor.localize(
|
>
|
||||||
|
${this._dialogParams!.supervisor.localize(
|
||||||
usedRepositories.includes(repo.slug)
|
usedRepositories.includes(repo.slug)
|
||||||
? "dialog.repositories.used"
|
? "dialog.repositories.used"
|
||||||
: "dialog.repositories.remove"
|
: "dialog.repositories.remove"
|
||||||
)}
|
)}
|
||||||
>
|
</ha-tooltip>
|
||||||
<div>
|
<div .id="icon-button-${repo.slug}">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.disabled=${usedRepositories.includes(repo.slug)}
|
.disabled=${usedRepositories.includes(repo.slug)}
|
||||||
.slug=${repo.slug}
|
.slug=${repo.slug}
|
||||||
@@ -138,7 +140,6 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-tooltip>
|
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -192,7 +193,7 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
.option {
|
.option {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 4px;
|
border-radius: var(--ha-border-radius-sm);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
ha-button {
|
ha-button {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class HassioSystemManagedDialog extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: var(--ha-space-4);
|
||||||
--mdc-icon-size: 48px;
|
--mdc-icon-size: 48px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
|||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
import { navigate } from "../../../src/common/navigate";
|
import { goBack, navigate } from "../../../src/common/navigate";
|
||||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||||
import { nextRender } from "../../../src/common/util/render-status";
|
import { nextRender } from "../../../src/common/util/render-status";
|
||||||
import "../../../src/components/ha-icon-button";
|
import "../../../src/components/ha-icon-button";
|
||||||
@@ -193,7 +193,7 @@ class HassioIngressView extends LitElement {
|
|||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
history.back();
|
goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ class HassioIngressView extends LitElement {
|
|||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
history.back();
|
goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const hassioStyle = css`
|
|||||||
.card-group {
|
.card-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
grid-gap: 8px;
|
grid-gap: var(--ha-space-2);
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 640px) {
|
@media screen and (min-width: 640px) {
|
||||||
.card-group {
|
.card-group {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
|
|||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
|
import { goBack } from "../../../src/common/navigate";
|
||||||
import "../../../src/layouts/hass-subpage";
|
import "../../../src/layouts/hass-subpage";
|
||||||
import type { HomeAssistant, Route } from "../../../src/types";
|
import type { HomeAssistant, Route } from "../../../src/types";
|
||||||
import "./update-available-card";
|
import "./update-available-card";
|
||||||
@@ -35,7 +36,7 @@ class UpdateAvailableDashboard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateComplete() {
|
private _updateComplete() {
|
||||||
history.back();
|
goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ class LandingPageLogs extends LitElement {
|
|||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 4px;
|
border-radius: var(--ha-border-radius-sm);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import "@material/mwc-linear-progress";
|
import "@material/mwc-linear-progress";
|
||||||
import { type PropertyValues, css, html, nothing } from "lit";
|
import { mdiOpenInNew } from "@mdi/js";
|
||||||
|
import { css, html, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||||
import "../../src/components/ha-alert";
|
import "../../src/components/ha-alert";
|
||||||
|
import "../../src/components/ha-button";
|
||||||
import "../../src/components/ha-fade-in";
|
import "../../src/components/ha-fade-in";
|
||||||
import "../../src/components/ha-spinner";
|
import "../../src/components/ha-spinner";
|
||||||
import { haStyle } from "../../src/resources/styles";
|
import "../../src/components/ha-svg-icon";
|
||||||
import "../../src/onboarding/onboarding-welcome-links";
|
|
||||||
import "./components/landing-page-network";
|
|
||||||
import "./components/landing-page-logs";
|
|
||||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
|
||||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
|
||||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
import "../../src/onboarding/onboarding-welcome-links";
|
||||||
|
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||||
|
import { haStyle } from "../../src/resources/styles";
|
||||||
|
import "./components/landing-page-logs";
|
||||||
|
import "./components/landing-page-network";
|
||||||
import {
|
import {
|
||||||
getSupervisorNetworkInfo,
|
getSupervisorNetworkInfo,
|
||||||
pingSupervisor,
|
pingSupervisor,
|
||||||
type NetworkInfo,
|
type NetworkInfo,
|
||||||
} from "./data/supervisor";
|
} from "./data/supervisor";
|
||||||
|
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||||
|
|
||||||
export const ASSUME_CORE_START_SECONDS = 60;
|
export const ASSUME_CORE_START_SECONDS = 60;
|
||||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||||
@@ -94,16 +97,21 @@ class HaLandingPage extends LandingPageBaseElement {
|
|||||||
<ha-language-picker
|
<ha-language-picker
|
||||||
.value=${this.language}
|
.value=${this.language}
|
||||||
.label=${""}
|
.label=${""}
|
||||||
|
button-style
|
||||||
native-name
|
native-name
|
||||||
@value-changed=${this._languageChanged}
|
@value-changed=${this._languageChanged}
|
||||||
inline-arrow
|
inline-arrow
|
||||||
></ha-language-picker>
|
></ha-language-picker>
|
||||||
<a
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
variant="neutral"
|
||||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>${this.localize("ui.panel.page-onboarding.help")}</a
|
|
||||||
>
|
>
|
||||||
|
${this.localize("ui.panel.page-onboarding.help")}
|
||||||
|
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -213,31 +221,13 @@ class HaLandingPage extends LandingPageBaseElement {
|
|||||||
ha-card .card-content {
|
ha-card .card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: var(--ha-space-4);
|
||||||
}
|
}
|
||||||
ha-alert p {
|
ha-alert p {
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
}
|
}
|
||||||
ha-language-picker {
|
.footer ha-svg-icon {
|
||||||
display: block;
|
--mdc-icon-size: var(--ha-space-5);
|
||||||
width: 200px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
--ha-select-height: 40px;
|
|
||||||
--mdc-select-fill-color: none;
|
|
||||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-select-idle-line-color: transparent;
|
|
||||||
--mdc-select-hover-line-color: transparent;
|
|
||||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-shape-small: 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
margin-right: 16px;
|
|
||||||
margin-inline-end: 16px;
|
|
||||||
margin-inline-start: initial;
|
|
||||||
}
|
}
|
||||||
ha-fade-in {
|
ha-fade-in {
|
||||||
min-height: calc(100vh - 64px - 88px);
|
min-height: calc(100vh - 64px - 88px);
|
||||||
|
|||||||
@@ -19,8 +19,9 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
padding: 32px 0;
|
padding: 32px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
max-width: 560px;
|
max-width: min(560px, calc(100vw - var(--safe-area-inset-right, 0px) - var(--safe-area-inset-left, 0px)));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|||||||
9
logs/.dockerignore
Normal file
9
logs/.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
dist/
|
||||||
|
src/
|
||||||
|
node_modules/
|
||||||
|
*.md
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
docker-compose.yaml
|
||||||
|
backend/ha-logs-proxy
|
||||||
|
backend/README.md
|
||||||
47
logs/Dockerfile
Normal file
47
logs/Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
ARG BUILD_FROM
|
||||||
|
FROM $BUILD_FROM AS base
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
jq \
|
||||||
|
curl \
|
||||||
|
go
|
||||||
|
|
||||||
|
# Install Home Assistant CLI
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
ARG CLI_VERSION
|
||||||
|
RUN curl -Lso /usr/bin/ha \
|
||||||
|
"https://github.com/home-assistant/cli/releases/download/${CLI_VERSION}/ha_${BUILD_ARCH}" \
|
||||||
|
&& chmod a+x /usr/bin/ha
|
||||||
|
|
||||||
|
# Build Go backend
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/go.mod backend/go.sum* ./
|
||||||
|
RUN go mod download || true
|
||||||
|
|
||||||
|
COPY backend/*.go ./
|
||||||
|
RUN CGO_ENABLED=0 go build -o ha-logs-proxy .
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
jq \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# Copy HA CLI from base
|
||||||
|
COPY --from=base /usr/bin/ha /usr/bin/ha
|
||||||
|
|
||||||
|
# Copy Go backend
|
||||||
|
COPY --from=base /app/ha-logs-proxy /usr/bin/ha-logs-proxy
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
|
# Expose port for backend (5642 = LOGB)
|
||||||
|
EXPOSE 5642
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["/usr/bin/ha-logs-proxy"]
|
||||||
113
logs/README.md
Normal file
113
logs/README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Home Assistant CLI Docker Container
|
||||||
|
|
||||||
|
A simple multi-architecture Docker container with the Home Assistant CLI installed.
|
||||||
|
|
||||||
|
## Development Usage
|
||||||
|
|
||||||
|
The CLI container is integrated into the `script/develop-logs` workflow. Both flags are required:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server with CLI container (requires remote_api add-on)
|
||||||
|
script/develop-logs -c http://192.168.1.2 -t your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
When started with credentials, the container runs a Go backend that proxies HA CLI logs commands. The backend API is available at `http://localhost:5642`.
|
||||||
|
|
||||||
|
**Frontend Features:**
|
||||||
|
- Dropdown menu to select log provider (core, supervisor, host, audio, dns, multicast)
|
||||||
|
- Follow mode with WebSocket streaming (`ha core logs --follow`)
|
||||||
|
- Manual refresh to fetch latest logs
|
||||||
|
- Download logs as text file
|
||||||
|
- Line wrapping toggle
|
||||||
|
- Auto-scroll to bottom when following
|
||||||
|
- Error display with retry
|
||||||
|
|
||||||
|
**API Endpoints:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all endpoints
|
||||||
|
curl http://localhost:5642/api/logs
|
||||||
|
|
||||||
|
# Get static logs
|
||||||
|
curl http://localhost:5642/api/logs/core
|
||||||
|
curl http://localhost:5642/api/logs/supervisor
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:5642/health
|
||||||
|
|
||||||
|
# WebSocket streaming (requires websocat or browser)
|
||||||
|
websocat ws://localhost:5642/api/logs/core/follow
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also execute HA CLI commands directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it ha-cli-dev ha info
|
||||||
|
docker exec -it ha-cli-dev ha supervisor info
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop everything with Ctrl+C (both the dev server and backend will stop automatically).
|
||||||
|
|
||||||
|
### Getting API Token
|
||||||
|
|
||||||
|
1. Install the [remote_api add-on](https://github.com/home-assistant/addons/tree/master/remote_api) in Home Assistant
|
||||||
|
2. Check the add-on logs for the generated token
|
||||||
|
3. Use the token with the `-t` flag
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Local Build (Single Architecture)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build \
|
||||||
|
--build-arg BUILD_FROM=alpine:3.22 \
|
||||||
|
--build-arg BUILD_ARCH=amd64 \
|
||||||
|
--build-arg CLI_VERSION=4.42.0 \
|
||||||
|
-t ha-cli:local \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Architecture Build
|
||||||
|
|
||||||
|
The `build.yaml` configuration is designed for use with Home Assistant's build system. For local multi-arch builds, use Docker Buildx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
--build-arg BUILD_FROM=alpine:3.22 \
|
||||||
|
--build-arg CLI_VERSION=4.42.0 \
|
||||||
|
-t ha-cli:latest \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Run CLI Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm ha-cli:local ha help
|
||||||
|
docker run --rm ha-cli:local ha supervisor info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm ha-cli:local
|
||||||
|
# Then run: ha <command>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm ha-cli ha help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update CLI Version
|
||||||
|
|
||||||
|
Edit the `CLI_VERSION` in `build.yaml` or pass it as a build argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg CLI_VERSION=4.43.0 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for latest versions at: https://github.com/home-assistant/cli/releases
|
||||||
1
logs/backend/.gitignore
vendored
Normal file
1
logs/backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ha-logs-proxy
|
||||||
108
logs/backend/README.md
Normal file
108
logs/backend/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# HA Logs Proxy Backend
|
||||||
|
|
||||||
|
A Go backend that proxies Home Assistant CLI logs commands through a secure HTTP API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Only allows `logs` commands (security-restricted)
|
||||||
|
- GET endpoints for static logs
|
||||||
|
- WebSocket endpoints for streaming logs (follow mode)
|
||||||
|
- CORS enabled for frontend integration
|
||||||
|
- Simple JSON API
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api/logs
|
||||||
|
|
||||||
|
List all available endpoints.
|
||||||
|
|
||||||
|
### GET /api/logs/core
|
||||||
|
|
||||||
|
Get Home Assistant core logs.
|
||||||
|
|
||||||
|
### GET /api/logs/supervisor
|
||||||
|
|
||||||
|
Get Supervisor logs.
|
||||||
|
|
||||||
|
### GET /api/logs/host
|
||||||
|
|
||||||
|
Get host system logs.
|
||||||
|
|
||||||
|
### GET /api/logs/audio
|
||||||
|
|
||||||
|
Get audio logs.
|
||||||
|
|
||||||
|
### GET /api/logs/dns
|
||||||
|
|
||||||
|
Get DNS logs.
|
||||||
|
|
||||||
|
### GET /api/logs/multicast
|
||||||
|
|
||||||
|
Get multicast logs.
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
Health check endpoint.
|
||||||
|
|
||||||
|
**Response format (all log endpoints):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"output": "log content here...",
|
||||||
|
"error": "error message if any"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WS /api/logs/*/follow
|
||||||
|
|
||||||
|
WebSocket endpoints for streaming logs in real-time.
|
||||||
|
|
||||||
|
Available endpoints:
|
||||||
|
- `WS /api/logs/core/follow` - Stream core logs
|
||||||
|
- `WS /api/logs/supervisor/follow` - Stream supervisor logs
|
||||||
|
- `WS /api/logs/host/follow` - Stream host logs
|
||||||
|
- `WS /api/logs/audio/follow` - Stream audio logs
|
||||||
|
- `WS /api/logs/dns/follow` - Stream DNS logs
|
||||||
|
- `WS /api/logs/multicast/follow` - Stream multicast logs
|
||||||
|
|
||||||
|
Each WebSocket message contains a single log line as plain text. The connection streams output from `ha {component} logs --follow` command.
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
The server starts on port 5642 (LOGB) by default. Override with `PORT` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=3000 go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o ha-logs-proxy
|
||||||
|
./ha-logs-proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List endpoints
|
||||||
|
curl http://localhost:5642/api/logs
|
||||||
|
|
||||||
|
# Get core logs
|
||||||
|
curl http://localhost:5642/api/logs/core
|
||||||
|
|
||||||
|
# Get supervisor logs
|
||||||
|
curl http://localhost:5642/api/logs/supervisor
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:5642/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Integration
|
||||||
|
|
||||||
|
The backend is designed to run in the same container as the HA CLI, sharing access to the `ha` command.
|
||||||
5
logs/backend/go.mod
Normal file
5
logs/backend/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/home-assistant/frontend/logs/backend
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.5.3
|
||||||
2
logs/backend/go.sum
Normal file
2
logs/backend/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
372
logs/backend/main.go
Normal file
372
logs/backend/main.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogsResponse struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // Allow all origins (CORS)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHACommand(args []string) (string, error) {
|
||||||
|
cmd := exec.Command("ha", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamHACommandToWS(conn *websocket.Conn, args []string) error {
|
||||||
|
cmd := exec.Command("ha", args...)
|
||||||
|
|
||||||
|
// Get stdout pipe
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and send output line by line
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if err := conn.WriteMessage(websocket.TextMessage, []byte(line)); err != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for command to finish
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCoreLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute ha core logs command
|
||||||
|
output, err := executeHACommand([]string{"core", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
log.Printf("Error encoding response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSupervisorLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"supervisor", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHostLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"host", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAudioLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"audio", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDNSLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"dns", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMulticastLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeHACommand([]string{"multicast", "logs"})
|
||||||
|
|
||||||
|
response := LogsResponse{
|
||||||
|
Output: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCoreLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to core logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"core", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming core logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from core logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSupervisorLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to supervisor logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"supervisor", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming supervisor logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from supervisor logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHostLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to host logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"host", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming host logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from host logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAudioLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to audio logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"audio", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming audio logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from audio logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDNSLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to dns logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"dns", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming dns logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from dns logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMulticastLogsFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WebSocket upgrade failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
log.Println("Client connected to multicast logs follow")
|
||||||
|
|
||||||
|
if err := streamHACommandToWS(conn, []string{"multicast", "logs", "--follow"}); err != nil {
|
||||||
|
log.Printf("Error streaming multicast logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Client disconnected from multicast logs follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := map[string]string{
|
||||||
|
"core": "/api/logs/core",
|
||||||
|
"supervisor": "/api/logs/supervisor",
|
||||||
|
"host": "/api/logs/host",
|
||||||
|
"audio": "/api/logs/audio",
|
||||||
|
"dns": "/api/logs/dns",
|
||||||
|
"multicast": "/api/logs/multicast",
|
||||||
|
"health": "/health",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
json.NewEncoder(w).Encode(endpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "5642"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register handlers
|
||||||
|
http.HandleFunc("/api/logs", listEndpoints)
|
||||||
|
http.HandleFunc("/api/logs/core", handleCoreLogs)
|
||||||
|
http.HandleFunc("/api/logs/supervisor", handleSupervisorLogs)
|
||||||
|
http.HandleFunc("/api/logs/host", handleHostLogs)
|
||||||
|
http.HandleFunc("/api/logs/audio", handleAudioLogs)
|
||||||
|
http.HandleFunc("/api/logs/dns", handleDNSLogs)
|
||||||
|
http.HandleFunc("/api/logs/multicast", handleMulticastLogs)
|
||||||
|
|
||||||
|
// WebSocket follow endpoints
|
||||||
|
http.HandleFunc("/api/logs/core/follow", handleCoreLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/supervisor/follow", handleSupervisorLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/host/follow", handleHostLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/audio/follow", handleAudioLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/dns/follow", handleDNSLogsFollow)
|
||||||
|
http.HandleFunc("/api/logs/multicast/follow", handleMulticastLogsFollow)
|
||||||
|
|
||||||
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Starting HA Logs Proxy on port %s", port)
|
||||||
|
log.Printf("Available endpoints:")
|
||||||
|
log.Printf(" GET /api/logs - List all endpoints")
|
||||||
|
log.Printf(" GET /api/logs/core - Core logs")
|
||||||
|
log.Printf(" GET /api/logs/supervisor - Supervisor logs")
|
||||||
|
log.Printf(" GET /api/logs/host - Host logs")
|
||||||
|
log.Printf(" GET /api/logs/audio - Audio logs")
|
||||||
|
log.Printf(" GET /api/logs/dns - DNS logs")
|
||||||
|
log.Printf(" GET /api/logs/multicast - Multicast logs")
|
||||||
|
log.Printf(" WS /api/logs/*/follow - Stream logs (WebSocket)")
|
||||||
|
log.Printf(" GET /health - Health check")
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||||
|
log.Fatalf("Server failed to start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
logs/build.yaml
Normal file
19
logs/build.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
image: ghcr.io/home-assistant/{arch}-hassio-cli
|
||||||
|
build_from:
|
||||||
|
aarch64: ghcr.io/home-assistant/aarch64-base:3.22
|
||||||
|
armhf: ghcr.io/home-assistant/armhf-base:3.22
|
||||||
|
armv7: ghcr.io/home-assistant/armv7-base:3.22
|
||||||
|
amd64: ghcr.io/home-assistant/amd64-base:3.22
|
||||||
|
i386: ghcr.io/home-assistant/i386-base:3.22
|
||||||
|
args:
|
||||||
|
CLI_VERSION: 4.42.0
|
||||||
|
cosign:
|
||||||
|
enabled: true
|
||||||
|
repository_name: home-assistant/cli
|
||||||
|
repository_owner: home-assistant
|
||||||
|
labels:
|
||||||
|
org.opencontainers.image.title: Home Assistant CLI
|
||||||
|
org.opencontainers.image.description: Home Assistant CLI for container environments
|
||||||
|
org.opencontainers.image.source: https://github.com/home-assistant/cli
|
||||||
|
org.opencontainers.image.licenses: Apache-2.0
|
||||||
16
logs/docker-compose.yaml
Normal file
16
logs/docker-compose.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
ha-cli:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
BUILD_FROM: alpine:3.22
|
||||||
|
BUILD_ARCH: amd64
|
||||||
|
CLI_VERSION: 4.42.0
|
||||||
|
image: ha-cli:local
|
||||||
|
ports:
|
||||||
|
- "5642:5642"
|
||||||
|
environment:
|
||||||
|
- PORT=5642
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
34
logs/src/auto-theme.ts
Normal file
34
logs/src/auto-theme.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { darkSemanticColorStyles } from "../../src/resources/theme/color/semantic.globals";
|
||||||
|
import { darkColorStyles } from "../../src/resources/theme/color/color.globals";
|
||||||
|
|
||||||
|
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
function applyTheme(dark: boolean) {
|
||||||
|
const el = document.documentElement;
|
||||||
|
if (dark) {
|
||||||
|
el.setAttribute("dark", "");
|
||||||
|
} else {
|
||||||
|
el.removeAttribute("dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dark theme styles wrapped in media query
|
||||||
|
// This runs after append-ha-style has loaded the base theme
|
||||||
|
const styleElement = document.createElement("style");
|
||||||
|
styleElement.id = "auto-theme-dark";
|
||||||
|
styleElement.textContent = `
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
${darkSemanticColorStyles.cssText}
|
||||||
|
${darkColorStyles.cssText}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
// Append to head to ensure it comes after base styles
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
// Apply theme on initial load
|
||||||
|
applyTheme(mql.matches);
|
||||||
|
|
||||||
|
// Listen for theme changes
|
||||||
|
mql.addEventListener("change", (e) => {
|
||||||
|
applyTheme(e.matches);
|
||||||
|
});
|
||||||
8
logs/src/entrypoint.ts
Normal file
8
logs/src/entrypoint.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import "./logs-app";
|
||||||
|
|
||||||
|
// Load base styles first, then apply theme
|
||||||
|
import("../../src/resources/append-ha-style").then(() => {
|
||||||
|
import("./auto-theme");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(document.createElement("logs-app"));
|
||||||
37
logs/src/html/index.html.template
Normal file
37
logs/src/html/index.html.template
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#03a9f4" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<title>Home Assistant Logs</title>
|
||||||
|
<% for (const entry of latestEntryJS) { %>
|
||||||
|
<script type="module" src="<%= entry %>"></script>
|
||||||
|
<% } %>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
|
color: var(--primary-text-color, #212121);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
background-color: var(--primary-background-color, #111111);
|
||||||
|
color: var(--primary-text-color, #e1e1e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Roboto, Noto, sans-serif;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
24
logs/src/logs-app.ts
Normal file
24
logs/src/logs-app.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { LitElement, css, html } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "./logs-viewer";
|
||||||
|
|
||||||
|
@customElement("logs-app")
|
||||||
|
class LogsApp extends LitElement {
|
||||||
|
render() {
|
||||||
|
return html`<logs-viewer></logs-viewer>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"logs-app": LogsApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
538
logs/src/logs-viewer.ts
Normal file
538
logs/src/logs-viewer.ts
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
import {
|
||||||
|
mdiArrowCollapseDown,
|
||||||
|
mdiChevronDown,
|
||||||
|
mdiCircle,
|
||||||
|
mdiDownload,
|
||||||
|
mdiRefresh,
|
||||||
|
mdiWrap,
|
||||||
|
mdiWrapDisabled,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||||
|
import "../../src/components/ha-ansi-to-html";
|
||||||
|
import type { HaAnsiToHtml } from "../../src/components/ha-ansi-to-html";
|
||||||
|
import "../../src/components/ha-button";
|
||||||
|
import "../../src/components/ha-button-menu";
|
||||||
|
import "../../src/components/ha-card";
|
||||||
|
import "../../src/components/ha-icon-button";
|
||||||
|
import "../../src/components/ha-list-item";
|
||||||
|
import "../../src/components/ha-spinner";
|
||||||
|
import "../../src/components/ha-svg-icon";
|
||||||
|
|
||||||
|
// Data types
|
||||||
|
interface LogProvider {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("logs-viewer")
|
||||||
|
export class LogsViewer extends LitElement {
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _selectedLogProvider?: string;
|
||||||
|
|
||||||
|
@state() private _logProviders: LogProvider[] = [];
|
||||||
|
|
||||||
|
@state() private _loading = false;
|
||||||
|
|
||||||
|
@state() private _wrapLines = true;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _newLogsIndicator?: boolean;
|
||||||
|
|
||||||
|
@query(".error-log") private _logElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("#scroll-top-marker") private _scrollTopMarkerElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("#scroll-bottom-marker")
|
||||||
|
private _scrollBottomMarkerElement?: HTMLElement;
|
||||||
|
|
||||||
|
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
|
||||||
|
|
||||||
|
@state() private _scrolledToBottomController =
|
||||||
|
new IntersectionController<boolean>(this, {
|
||||||
|
callback(this: IntersectionController<boolean>, entries) {
|
||||||
|
return entries[0].isIntersecting;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
@state() private _scrolledToTopController =
|
||||||
|
new IntersectionController<boolean>(this, {});
|
||||||
|
|
||||||
|
private _ws: WebSocket | null = null;
|
||||||
|
|
||||||
|
private _apiUrl = `http://${window.location.hostname}:5642`;
|
||||||
|
|
||||||
|
private async _fetchLogs(): Promise<void> {
|
||||||
|
if (!this._selectedLogProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loading = true;
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
|
// Stop any existing websocket
|
||||||
|
this._stopFollowing();
|
||||||
|
|
||||||
|
// Clear existing logs
|
||||||
|
this._ansiToHtmlElement?.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, fetch the latest logs
|
||||||
|
const response = await fetch(
|
||||||
|
`${this._apiUrl}/api/logs/${this._selectedLogProvider}`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logText = data.output || "";
|
||||||
|
|
||||||
|
// Parse and display initial logs
|
||||||
|
if (logText.trim()) {
|
||||||
|
this._ansiToHtmlElement?.parseTextToColoredPre(logText);
|
||||||
|
|
||||||
|
// Add divider line
|
||||||
|
this._ansiToHtmlElement?.parseLineToColoredPre(
|
||||||
|
"--- Live logs start here ---"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loading = false;
|
||||||
|
|
||||||
|
// Scroll to bottom after loading
|
||||||
|
this._scrollToBottom();
|
||||||
|
|
||||||
|
// Start streaming
|
||||||
|
this._startFollowing();
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Error loading logs: ${err}`;
|
||||||
|
this._loading = false;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error fetching logs:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchLogProviders(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this._apiUrl}/api/logs`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const providers = await response.json();
|
||||||
|
|
||||||
|
// Define the order (matching backend registration order)
|
||||||
|
const order = ["core", "supervisor", "host", "audio", "dns", "multicast"];
|
||||||
|
|
||||||
|
// Convert object to array of providers, filter out health endpoint, and sort
|
||||||
|
this._logProviders = Object.entries(providers)
|
||||||
|
.filter(([key]) => key !== "health")
|
||||||
|
.map(([key]) => ({
|
||||||
|
key,
|
||||||
|
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key));
|
||||||
|
|
||||||
|
// Set default provider once loaded
|
||||||
|
if (this._logProviders.length > 0 && !this._selectedLogProvider) {
|
||||||
|
this._selectedLogProvider = this._logProviders[0].key;
|
||||||
|
await this._fetchLogs();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to load log providers: ${err}`;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error fetching log providers:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._fetchLogProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._stopFollowing();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
|
||||||
|
this._scrolledToTopController.observe(this._scrollTopMarkerElement!);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated() {
|
||||||
|
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
|
||||||
|
this._newLogsIndicator = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectProvider(ev: Event) {
|
||||||
|
const target = ev.currentTarget as any;
|
||||||
|
this._selectedLogProvider = target.provider;
|
||||||
|
this._fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _refresh() {
|
||||||
|
this._fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleLineWrap() {
|
||||||
|
this._wrapLines = !this._wrapLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scrollToBottom(): void {
|
||||||
|
if (this._logElement) {
|
||||||
|
this._newLogsIndicator = false;
|
||||||
|
this._logElement.scrollTo(0, this._logElement.scrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startFollowing() {
|
||||||
|
if (!this._selectedLogProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._stopFollowing();
|
||||||
|
this._error = undefined;
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${wsProtocol}//${window.location.hostname}:5642/api/logs/${this._selectedLogProvider}/follow`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this._ws.onopen = () => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log("WebSocket connected");
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onmessage = (event) => {
|
||||||
|
const scrolledToBottom = this._scrolledToBottomController.value;
|
||||||
|
|
||||||
|
// Add the new line to the display
|
||||||
|
this._ansiToHtmlElement?.parseLineToColoredPre(event.data);
|
||||||
|
|
||||||
|
// Auto-scroll if user is at bottom
|
||||||
|
if (scrolledToBottom && this._logElement) {
|
||||||
|
this._scrollToBottom();
|
||||||
|
} else {
|
||||||
|
this._newLogsIndicator = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onerror = (error) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
this._error = "WebSocket connection error";
|
||||||
|
};
|
||||||
|
|
||||||
|
this._ws.onclose = () => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log("WebSocket disconnected");
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to start following logs: ${err}`;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.error("Error starting WebSocket:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopFollowing() {
|
||||||
|
if (this._ws) {
|
||||||
|
this._ws.close();
|
||||||
|
this._ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _downloadLogs() {
|
||||||
|
if (!this._selectedLogProvider || !this._ansiToHtmlElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the text content from the logs
|
||||||
|
const logText =
|
||||||
|
this._ansiToHtmlElement.shadowRoot?.querySelector("pre")?.textContent ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (!logText.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blob from log text
|
||||||
|
const blob = new Blob([logText], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create download link and trigger it
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${this._selectedLogProvider}-logs-${Date.now()}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const currentProvider = this._logProviders.find(
|
||||||
|
(p) => p.key === this._selectedLogProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="toolbar">
|
||||||
|
<ha-button-menu>
|
||||||
|
<ha-button slot="trigger" appearance="filled">
|
||||||
|
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
|
||||||
|
${currentProvider?.name || "Select Provider"}
|
||||||
|
</ha-button>
|
||||||
|
${this._logProviders.map(
|
||||||
|
(provider) => html`
|
||||||
|
<ha-list-item
|
||||||
|
?selected=${provider.key === this._selectedLogProvider}
|
||||||
|
.provider=${provider.key}
|
||||||
|
@click=${this._selectProvider}
|
||||||
|
>
|
||||||
|
${provider.name}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="error-log-intro">
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="card-header">${currentProvider?.name || "Logs"}</h1>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDownload}
|
||||||
|
@click=${this._downloadLogs}
|
||||||
|
.label=${"Download logs"}
|
||||||
|
.disabled=${!this._ansiToHtmlElement}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${this._wrapLines ? mdiWrapDisabled : mdiWrap}
|
||||||
|
@click=${this._toggleLineWrap}
|
||||||
|
.label=${this._wrapLines ? "Full width" : "Wrap lines"}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiRefresh}
|
||||||
|
@click=${this._refresh}
|
||||||
|
.label=${"Refresh"}
|
||||||
|
.disabled=${!this._selectedLogProvider}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content error-log">
|
||||||
|
<div id="scroll-top-marker"></div>
|
||||||
|
${this._loading
|
||||||
|
? html`<div>Loading logs...</div>`
|
||||||
|
: this._error
|
||||||
|
? html`<div class="error">${this._error}</div>`
|
||||||
|
: nothing}
|
||||||
|
<ha-ansi-to-html
|
||||||
|
?wrap-disabled=${!this._wrapLines}
|
||||||
|
></ha-ansi-to-html>
|
||||||
|
<div id="scroll-bottom-marker"></div>
|
||||||
|
</div>
|
||||||
|
<ha-button
|
||||||
|
class="new-logs-indicator ${classMap({
|
||||||
|
visible:
|
||||||
|
(this._newLogsIndicator &&
|
||||||
|
!this._scrolledToBottomController.value) ||
|
||||||
|
false,
|
||||||
|
})}"
|
||||||
|
size="small"
|
||||||
|
appearance="filled"
|
||||||
|
@click=${this._scrollToBottom}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiArrowCollapseDown}
|
||||||
|
slot="start"
|
||||||
|
></ha-svg-icon>
|
||||||
|
Scroll down
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiArrowCollapseDown}
|
||||||
|
slot="end"
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
|
${this._ws && !this._error
|
||||||
|
? html`<div class="live-indicator">
|
||||||
|
<ha-svg-icon .path=${mdiCircle}></ha-svg-icon>
|
||||||
|
Live
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles: CSSResultGroup = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
padding: var(--ha-space-2) var(--ha-space-4);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--ha-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-card {
|
||||||
|
padding-top: var(--ha-space-2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
|
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
line-height: var(--ha-line-height-expanded);
|
||||||
|
display: block;
|
||||||
|
margin-block-start: 0px;
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: calc(100% - 150px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log {
|
||||||
|
position: relative;
|
||||||
|
font-family: var(--ha-font-family-code);
|
||||||
|
clear: both;
|
||||||
|
text-align: start;
|
||||||
|
padding-top: var(--ha-space-4);
|
||||||
|
padding-bottom: var(--ha-space-4);
|
||||||
|
overflow-y: scroll;
|
||||||
|
min-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||||
|
max-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-log > div {
|
||||||
|
padding: 0 var(--ha-space-4);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
padding: var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-logs-indicator {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--ha-space-1);
|
||||||
|
left: var(--ha-space-1);
|
||||||
|
height: 0;
|
||||||
|
transition: height 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-logs-indicator.visible {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes breathe {
|
||||||
|
from {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
inset-inline-end: var(--ha-space-4);
|
||||||
|
border-top-right-radius: var(--ha-space-2);
|
||||||
|
border-top-left-radius: var(--ha-space-2);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-indicator ha-svg-icon {
|
||||||
|
animation: breathe 1s cubic-bezier(0.5, 0, 1, 1) infinite alternate;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 870px) {
|
||||||
|
.error-log {
|
||||||
|
min-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||||
|
max-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button-menu {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button::part(label) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-list-item[selected] {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"logs-viewer": LogsViewer;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
package.json
127
package.json
@@ -26,33 +26,34 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@awesome.me/webawesome": "3.0.0-beta.4",
|
"@babel/runtime": "7.28.4",
|
||||||
"@babel/runtime": "7.28.3",
|
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.19.1",
|
||||||
"@codemirror/commands": "6.8.1",
|
"@codemirror/commands": "6.10.0",
|
||||||
"@codemirror/language": "6.11.3",
|
"@codemirror/language": "6.11.3",
|
||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.2",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.38.1",
|
"@codemirror/view": "6.38.6",
|
||||||
|
"@date-fns/tz": "1.4.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||||
"@formatjs/intl-displaynames": "6.8.11",
|
"@formatjs/intl-displaynames": "6.8.13",
|
||||||
"@formatjs/intl-durationformat": "0.7.4",
|
"@formatjs/intl-durationformat": "0.7.6",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.5.5",
|
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
||||||
"@formatjs/intl-listformat": "7.7.11",
|
"@formatjs/intl-listformat": "7.7.13",
|
||||||
"@formatjs/intl-locale": "4.2.11",
|
"@formatjs/intl-locale": "4.2.13",
|
||||||
"@formatjs/intl-numberformat": "8.15.4",
|
"@formatjs/intl-numberformat": "8.15.6",
|
||||||
"@formatjs/intl-pluralrules": "5.4.4",
|
"@formatjs/intl-pluralrules": "5.4.6",
|
||||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
"@formatjs/intl-relativetimeformat": "11.4.13",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"@fullcalendar/interaction": "6.1.19",
|
"@fullcalendar/interaction": "6.1.19",
|
||||||
"@fullcalendar/list": "6.1.19",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/luxon3": "6.1.19",
|
"@fullcalendar/luxon3": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@lezer/highlight": "1.2.1",
|
"@home-assistant/webawesome": "3.0.0-beta.6.ha.7",
|
||||||
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
"@lit-labs/virtualizer": "2.1.1",
|
"@lit-labs/virtualizer": "2.1.1",
|
||||||
@@ -80,30 +81,28 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "2.4.0",
|
"@material/web": "2.4.1",
|
||||||
"@mdi/js": "7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
"@mdi/svg": "7.4.47",
|
"@mdi/svg": "7.4.47",
|
||||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||||
"@shoelace-style/shoelace": "2.20.1",
|
|
||||||
"@swc/helpers": "0.5.17",
|
"@swc/helpers": "0.5.17",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.9.1",
|
"@tsparticles/engine": "3.9.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.8.6",
|
"@vaadin/combo-box": "24.9.4",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.6",
|
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"barcode-detector": "3.0.5",
|
"barcode-detector": "3.0.6",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.2",
|
||||||
"comlink": "4.4.2",
|
"comlink": "4.4.2",
|
||||||
"core-js": "3.45.1",
|
"core-js": "3.46.0",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"culori": "4.0.2",
|
"culori": "4.0.2",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"date-fns-tz": "3.2.0",
|
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"dialog-polyfill": "0.5.6",
|
"dialog-polyfill": "0.5.6",
|
||||||
@@ -112,18 +111,18 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.11",
|
"hls.js": "1.6.14",
|
||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.16",
|
"intl-messageformat": "10.7.18",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
"leaflet.markercluster": "1.5.3",
|
"leaflet.markercluster": "1.5.3",
|
||||||
"lit": "3.3.1",
|
"lit": "3.3.1",
|
||||||
"lit-html": "3.3.1",
|
"lit-html": "3.3.1",
|
||||||
"luxon": "3.7.1",
|
"luxon": "3.7.2",
|
||||||
"marked": "16.2.0",
|
"marked": "16.4.1",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@@ -136,7 +135,7 @@
|
|||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"ua-parser-js": "2.0.4",
|
"ua-parser-js": "2.0.6",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
"weekstart": "2.0.0",
|
"weekstart": "2.0.0",
|
||||||
@@ -149,52 +148,52 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.3",
|
"@babel/core": "7.28.5",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||||
"@babel/plugin-transform-runtime": "7.28.3",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.3",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.3",
|
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
||||||
"@lokalise/node-api": "15.2.1",
|
"@lokalise/node-api": "15.3.1",
|
||||||
"@octokit/auth-oauth-device": "8.0.1",
|
"@octokit/auth-oauth-device": "8.0.3",
|
||||||
"@octokit/plugin-retry": "8.0.1",
|
"@octokit/plugin-retry": "8.0.3",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.1",
|
||||||
"@rsdoctor/rspack-plugin": "1.2.3",
|
"@rsdoctor/rspack-plugin": "1.3.7",
|
||||||
"@rspack/core": "1.4.11",
|
"@rspack/core": "1.6.0",
|
||||||
"@rspack/dev-server": "1.1.4",
|
"@rspack/dev-server": "1.1.4",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.22",
|
"@types/chromecast-caf-receiver": "6.0.22",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/culori": "4.0.0",
|
"@types/culori": "4.0.1",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.20",
|
"@types/leaflet": "1.9.21",
|
||||||
"@types/leaflet-draw": "1.0.12",
|
"@types/leaflet-draw": "1.0.13",
|
||||||
"@types/leaflet.markercluster": "1.5.5",
|
"@types/leaflet.markercluster": "1.5.6",
|
||||||
"@types/lodash.merge": "4.6.9",
|
"@types/lodash.merge": "4.6.9",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/mocha": "10.0.10",
|
"@types/mocha": "10.0.10",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.6",
|
||||||
"@types/sortablejs": "1.15.8",
|
"@types/sortablejs": "1.15.9",
|
||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "4.0.6",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.0",
|
"del": "8.0.1",
|
||||||
"eslint": "9.34.0",
|
"eslint": "9.39.1",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-lit": "2.1.1",
|
"eslint-plugin-lit": "2.1.1",
|
||||||
"eslint-plugin-lit-a11y": "5.1.1",
|
"eslint-plugin-lit-a11y": "5.1.1",
|
||||||
"eslint-plugin-unused-imports": "4.2.0",
|
"eslint-plugin-unused-imports": "4.3.0",
|
||||||
"eslint-plugin-wc": "3.0.1",
|
"eslint-plugin-wc": "3.0.2",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.1",
|
"fs-extra": "11.3.2",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"gulp": "5.0.1",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
@@ -202,25 +201,25 @@
|
|||||||
"gulp-rename": "2.1.0",
|
"gulp-rename": "2.1.0",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "27.1.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "16.1.5",
|
"lint-staged": "16.2.6",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"rspack-manifest-plugin": "5.0.3",
|
"rspack-manifest-plugin": "5.1.0",
|
||||||
"serve": "14.2.4",
|
"serve": "14.2.5",
|
||||||
"sinon": "21.0.0",
|
"sinon": "21.0.0",
|
||||||
"tar": "7.4.3",
|
"tar": "7.5.2",
|
||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.41.0",
|
"typescript-eslint": "8.46.3",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.2.4",
|
"vitest": "4.0.6",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@@ -232,10 +231,12 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "2.1.1",
|
"@lit/reactive-element": "2.1.1",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"globals": "16.3.0",
|
"globals": "16.5.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||||
"@vaadin/vaadin-themable-mixin": "24.8.6"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.4"
|
"packageManager": "yarn@4.10.3",
|
||||||
|
"volta": {
|
||||||
|
"node": "22.21.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
export default {
|
export default {
|
||||||
trailingComma: "es5",
|
trailingComma: "es5",
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: "*.globals.ts",
|
||||||
|
options: {
|
||||||
|
printWidth: 9999, // Effectively disables line wrapping for these files
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20250827.0"
|
version = "20251029.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*"]
|
license-files = ["LICENSE*"]
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
":semanticCommitsDisabled",
|
":semanticCommitsDisabled",
|
||||||
"group:monorepos",
|
"group:monorepos",
|
||||||
"group:recommended",
|
"group:recommended",
|
||||||
"npm:unpublishSafe"
|
"security:minimumReleaseAgeNpm"
|
||||||
],
|
],
|
||||||
"enabledManagers": ["npm", "nvm"],
|
"enabledManagers": ["npm", "nvm"],
|
||||||
"postUpdateOptions": ["yarnDedupeHighest"],
|
"postUpdateOptions": ["yarnDedupeHighest"],
|
||||||
|
|||||||
96
script/develop-logs
Executable file
96
script/develop-logs
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run the logs frontend development server
|
||||||
|
|
||||||
|
# Stop on errors
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
SUPERVISOR_ENDPOINT=""
|
||||||
|
SUPERVISOR_API_TOKEN=""
|
||||||
|
|
||||||
|
while getopts "c:t:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
c)
|
||||||
|
SUPERVISOR_ENDPOINT="$OPTARG"
|
||||||
|
;;
|
||||||
|
t)
|
||||||
|
SUPERVISOR_API_TOKEN="$OPTARG"
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
echo "Usage: $0 -c SUPERVISOR_ENDPOINT -t SUPERVISOR_API_TOKEN"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -c SUPERVISOR_ENDPOINT (e.g., http://192.168.1.2) [required]"
|
||||||
|
echo " -t SUPERVISOR_API_TOKEN (from remote_api add-on) [required]"
|
||||||
|
echo " -h Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 -c http://192.168.1.2 -t your_token_here"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
\?)
|
||||||
|
echo "Invalid option: -$OPTARG" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate that both -c and -t are provided
|
||||||
|
if [ -z "$SUPERVISOR_ENDPOINT" ] || [ -z "$SUPERVISOR_API_TOKEN" ]; then
|
||||||
|
echo "Error: Both -c and -t are required" >&2
|
||||||
|
echo "Use -h for help"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Shutting down..."
|
||||||
|
echo "Stopping HA CLI container..."
|
||||||
|
docker stop ha-cli-dev 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to cleanup on exit
|
||||||
|
trap cleanup INT TERM EXIT
|
||||||
|
|
||||||
|
# Run HA CLI container
|
||||||
|
echo "Starting HA CLI container..."
|
||||||
|
|
||||||
|
# Build the container if needed
|
||||||
|
if ! docker images | grep -q "ha-cli:local"; then
|
||||||
|
echo "Building HA CLI container..."
|
||||||
|
(cd logs && docker compose build)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up any existing container
|
||||||
|
docker stop ha-cli-dev 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run the container in background (not detached, so it shares stdout)
|
||||||
|
docker run \
|
||||||
|
--name ha-cli-dev \
|
||||||
|
--rm \
|
||||||
|
-p 5642:5642 \
|
||||||
|
-e SUPERVISOR_ENDPOINT="$SUPERVISOR_ENDPOINT" \
|
||||||
|
-e SUPERVISOR_API_TOKEN="$SUPERVISOR_API_TOKEN" \
|
||||||
|
-e PORT=5642 \
|
||||||
|
ha-cli:local &
|
||||||
|
|
||||||
|
# Store the docker process ID
|
||||||
|
DOCKER_PID=$!
|
||||||
|
|
||||||
|
# Wait a moment for container to start
|
||||||
|
sleep 2
|
||||||
|
echo ""
|
||||||
|
echo "HA Logs Backend API: http://localhost:5642"
|
||||||
|
echo " GET /api/logs - List endpoints"
|
||||||
|
echo " GET /api/logs/core - Core logs"
|
||||||
|
echo " GET /api/logs/supervisor - Supervisor logs"
|
||||||
|
echo " GET /health - Health check"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run gulp (this will block until Ctrl+C)
|
||||||
|
./node_modules/.bin/gulp develop-logs
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable lit/prefer-static-styles */
|
/* eslint-disable lit/prefer-static-styles */
|
||||||
|
import { mdiOpenInNew } from "@mdi/js";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@@ -6,6 +7,8 @@ import punycode from "punycode";
|
|||||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
|
import "../components/ha-button";
|
||||||
|
import "../components/ha-svg-icon";
|
||||||
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
|
||||||
import { fetchAuthProviders } from "../data/auth";
|
import { fetchAuthProviders } from "../data/auth";
|
||||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||||
@@ -103,7 +106,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
box-shadow: var(--ha-card-box-shadow, none);
|
box-shadow: var(--ha-card-box-shadow, none);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
border-radius: var(
|
||||||
|
--ha-card-border-radius,
|
||||||
|
var(--ha-border-radius-lg)
|
||||||
|
);
|
||||||
border-width: var(--ha-card-border-width, 1px);
|
border-width: var(--ha-card-border-width, 1px);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: var(
|
border-color: var(
|
||||||
@@ -130,25 +136,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
ha-language-picker {
|
.footer ha-svg-icon {
|
||||||
width: 200px;
|
--mdc-icon-size: var(--ha-space-5);
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
--ha-select-height: 40px;
|
|
||||||
--mdc-select-fill-color: none;
|
|
||||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-select-idle-line-color: transparent;
|
|
||||||
--mdc-select-hover-line-color: transparent;
|
|
||||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
|
||||||
--mdc-shape-small: 0;
|
|
||||||
}
|
|
||||||
.footer a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
margin-right: 16px;
|
|
||||||
margin-inline-end: 16px;
|
|
||||||
margin-inline-start: initial;
|
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--ha-font-size-3xl);
|
font-size: var(--ha-font-size-3xl);
|
||||||
@@ -202,16 +191,21 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
<ha-language-picker
|
<ha-language-picker
|
||||||
.value=${this.language}
|
.value=${this.language}
|
||||||
.label=${""}
|
.label=${""}
|
||||||
|
button-style
|
||||||
native-name
|
native-name
|
||||||
@value-changed=${this._languageChanged}
|
@value-changed=${this._languageChanged}
|
||||||
inline-arrow
|
inline-arrow
|
||||||
></ha-language-picker>
|
></ha-language-picker>
|
||||||
<a
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
variant="neutral"
|
||||||
href="https://www.home-assistant.io/docs/authentication/"
|
href="https://www.home-assistant.io/docs/authentication/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>${this.localize("ui.panel.page-authorize.help")}</a
|
|
||||||
>
|
>
|
||||||
|
${this.localize("ui.panel.page-authorize.help")}
|
||||||
|
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
|
import { formatHex, parse } from "culori";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands a 3-digit hex color to a 6-digit hex color.
|
||||||
|
* @param hex - The hex color to expand.
|
||||||
|
* @returns The expanded hex color.
|
||||||
|
* @throws If the hex color is invalid.
|
||||||
|
*/
|
||||||
export const expandHex = (hex: string): string => {
|
export const expandHex = (hex: string): string => {
|
||||||
hex = hex.replace("#", "");
|
const color = parse(hex);
|
||||||
if (hex.length === 6) return hex;
|
if (!color) {
|
||||||
let result = "";
|
throw new Error(`Invalid hex color: ${hex}`);
|
||||||
for (const val of hex) {
|
|
||||||
result += val + val;
|
|
||||||
}
|
}
|
||||||
return result;
|
const formattedColor = formatHex(color);
|
||||||
|
if (!formattedColor) {
|
||||||
|
throw new Error(`Could not format hex color: ${hex}`);
|
||||||
|
}
|
||||||
|
return formattedColor.replace("#", "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
|
/**
|
||||||
|
* Blends two hex colors. c1 is placed over c2, blend is c1's opacity.
|
||||||
|
* @param c1 - The first hex color.
|
||||||
|
* @param c2 - The second hex color.
|
||||||
|
* @param blend - The blend percentage (0-100).
|
||||||
|
* @returns The blended hex color.
|
||||||
|
*/
|
||||||
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
|
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
|
||||||
let color = "";
|
|
||||||
c1 = expandHex(c1);
|
c1 = expandHex(c1);
|
||||||
c2 = expandHex(c2);
|
c2 = expandHex(c2);
|
||||||
|
let color = "";
|
||||||
for (let i = 0; i <= 5; i += 2) {
|
for (let i = 0; i <= 5; i += 2) {
|
||||||
const h1 = parseInt(c1.substring(i, i + 2), 16);
|
const h1 = parseInt(c1.substring(i, i + 2), 16);
|
||||||
const h2 = parseInt(c2.substring(i, i + 2), 16);
|
const h2 = parseInt(c2.substring(i, i + 2), 16);
|
||||||
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
|
const hex = Math.floor(h2 + (h1 - h2) * (blend / 100))
|
||||||
while (hex.length < 2) hex = "0" + hex;
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
color += hex;
|
color += hex;
|
||||||
}
|
}
|
||||||
return `#${color}`;
|
return `#${color}`;
|
||||||
|
|||||||
@@ -1,28 +1,49 @@
|
|||||||
export const luminosity = (rgb: [number, number, number]): number => {
|
import { wcagLuminance, wcagContrast } from "culori";
|
||||||
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
|
|
||||||
const lum: [number, number, number] = [0, 0, 0];
|
|
||||||
for (let i = 0; i < rgb.length; i++) {
|
|
||||||
const chan = rgb[i] / 255;
|
|
||||||
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
/**
|
||||||
};
|
* Calculates the luminosity of an RGB color.
|
||||||
|
* @param rgb - The RGB color to calculate the luminosity of.
|
||||||
|
* @returns The luminosity of the color.
|
||||||
|
*/
|
||||||
|
export const luminosity = (rgb: [number, number, number]): number =>
|
||||||
|
wcagLuminance({
|
||||||
|
mode: "rgb",
|
||||||
|
r: rgb[0] / 255,
|
||||||
|
g: rgb[1] / 255,
|
||||||
|
b: rgb[2] / 255,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the contrast ratio between two RGB colors.
|
||||||
|
* @param color1 - The first color to calculate the contrast ratio of.
|
||||||
|
* @param color2 - The second color to calculate the contrast ratio of.
|
||||||
|
* @returns The contrast ratio between the two colors.
|
||||||
|
*/
|
||||||
export const rgbContrast = (
|
export const rgbContrast = (
|
||||||
color1: [number, number, number],
|
color1: [number, number, number],
|
||||||
color2: [number, number, number]
|
color2: [number, number, number]
|
||||||
) => {
|
) =>
|
||||||
const lum1 = luminosity(color1);
|
wcagContrast(
|
||||||
const lum2 = luminosity(color2);
|
{
|
||||||
|
mode: "rgb",
|
||||||
if (lum1 > lum2) {
|
r: color1[0] / 255,
|
||||||
return (lum1 + 0.05) / (lum2 + 0.05);
|
g: color1[1] / 255,
|
||||||
|
b: color1[2] / 255,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "rgb",
|
||||||
|
r: color2[0] / 255,
|
||||||
|
g: color2[1] / 255,
|
||||||
|
b: color2[2] / 255,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (lum2 + 0.05) / (lum1 + 0.05);
|
/**
|
||||||
};
|
* Calculates the contrast ratio between two RGB colors.
|
||||||
|
* @param rgb1 - The first color to calculate the contrast ratio of.
|
||||||
|
* @param rgb2 - The second color to calculate the contrast ratio of.
|
||||||
|
* @returns The contrast ratio between the two colors.
|
||||||
|
*/
|
||||||
export const getRGBContrastRatio = (
|
export const getRGBContrastRatio = (
|
||||||
rgb1: [number, number, number],
|
rgb1: [number, number, number],
|
||||||
rgb2: [number, number, number]
|
rgb2: [number, number, number]
|
||||||
|
|||||||
141
src/common/controllers/undo-redo-controller.ts
Normal file
141
src/common/controllers/undo-redo-controller.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import type {
|
||||||
|
ReactiveController,
|
||||||
|
ReactiveControllerHost,
|
||||||
|
} from "@lit/reactive-element/reactive-controller";
|
||||||
|
|
||||||
|
const UNDO_REDO_STACK_LIMIT = 75;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the UndoRedoController.
|
||||||
|
*
|
||||||
|
* @template ConfigType The type of configuration to manage.
|
||||||
|
*/
|
||||||
|
export interface UndoRedoControllerConfig<ConfigType> {
|
||||||
|
stackLimit?: number;
|
||||||
|
currentConfig: () => ConfigType;
|
||||||
|
apply: (config: ConfigType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller to manage undo and redo operations for a given configuration type.
|
||||||
|
*
|
||||||
|
* @template ConfigType The type of configuration to manage.
|
||||||
|
*/
|
||||||
|
export class UndoRedoController<ConfigType> implements ReactiveController {
|
||||||
|
private _host: ReactiveControllerHost;
|
||||||
|
|
||||||
|
private _undoStack: ConfigType[] = [];
|
||||||
|
|
||||||
|
private _redoStack: ConfigType[] = [];
|
||||||
|
|
||||||
|
private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT;
|
||||||
|
|
||||||
|
private readonly _apply: (config: ConfigType) => void = () => {
|
||||||
|
throw new Error("No apply function provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _currentConfig: () => ConfigType = () => {
|
||||||
|
throw new Error("No currentConfig function provided");
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: ReactiveControllerHost,
|
||||||
|
options: UndoRedoControllerConfig<ConfigType>
|
||||||
|
) {
|
||||||
|
if (options.stackLimit !== undefined) {
|
||||||
|
this._stackLimit = options.stackLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._apply = options.apply;
|
||||||
|
this._currentConfig = options.currentConfig;
|
||||||
|
this._host = host;
|
||||||
|
host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConnected() {
|
||||||
|
window.addEventListener("undo-change", this._onUndoChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDisconnected() {
|
||||||
|
window.removeEventListener("undo-change", this._onUndoChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onUndoChange = (ev: Event) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.undo();
|
||||||
|
this._host.requestUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether there are actions available to undo.
|
||||||
|
*
|
||||||
|
* @returns `true` if there are actions to undo, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public get canUndo(): boolean {
|
||||||
|
return this._undoStack.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether there are actions available to redo.
|
||||||
|
*
|
||||||
|
* @returns `true` if there are actions to redo, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public get canRedo(): boolean {
|
||||||
|
return this._redoStack.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits the current configuration to the undo stack and clears the redo stack.
|
||||||
|
*
|
||||||
|
* @param config The current configuration to commit.
|
||||||
|
*/
|
||||||
|
public commit(config: ConfigType) {
|
||||||
|
if (this._undoStack.length >= this._stackLimit) {
|
||||||
|
this._undoStack.shift();
|
||||||
|
}
|
||||||
|
this._undoStack.push({ ...config });
|
||||||
|
this._redoStack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undoes the last action and applies the previous configuration
|
||||||
|
* while saving the current configuration to the redo stack.
|
||||||
|
*/
|
||||||
|
public undo() {
|
||||||
|
if (this._undoStack.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._redoStack.push({ ...this._currentConfig() });
|
||||||
|
const config = this._undoStack.pop()!;
|
||||||
|
this._apply(config);
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redoes the last undone action and reapplies the configuration
|
||||||
|
* while saving the current configuration to the undo stack.
|
||||||
|
*/
|
||||||
|
public redo() {
|
||||||
|
if (this._redoStack.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._undoStack.push({ ...this._currentConfig() });
|
||||||
|
const config = this._redoStack.pop()!;
|
||||||
|
this._apply(config);
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the undo and redo stacks, clearing all history.
|
||||||
|
*/
|
||||||
|
public reset() {
|
||||||
|
this._undoStack = [];
|
||||||
|
this._redoStack = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"undo-change": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
differenceInDays,
|
differenceInDays,
|
||||||
addDays,
|
addDays,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
import { TZDate } from "@date-fns/tz";
|
||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { TimeZone } from "../../data/translation";
|
import { TimeZone } from "../../data/translation";
|
||||||
@@ -22,12 +22,13 @@ const calcZonedDate = (
|
|||||||
fn: (date: Date, options?: any) => Date | number | boolean,
|
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||||
options?
|
options?
|
||||||
) => {
|
) => {
|
||||||
const inputZoned = toZonedTime(date, tz);
|
const tzDate = new TZDate(date, tz);
|
||||||
const fnZoned = fn(inputZoned, options);
|
const fnResult = fn(tzDate, options);
|
||||||
if (fnZoned instanceof Date) {
|
if (fnResult instanceof Date) {
|
||||||
return fromZonedTime(fnZoned, tz) as Date;
|
// Convert back to regular Date in the specified timezone
|
||||||
|
return new Date(fnResult.getTime());
|
||||||
}
|
}
|
||||||
return fnZoned;
|
return fnResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calcDate = (
|
export const calcDate = (
|
||||||
@@ -65,7 +66,7 @@ export const calcDateDifferenceProperty = (
|
|||||||
locale,
|
locale,
|
||||||
config,
|
config,
|
||||||
locale.time_zone === TimeZone.server
|
locale.time_zone === TimeZone.server
|
||||||
? toZonedTime(startDate, config.time_zone)
|
? new TZDate(startDate, config.time_zone)
|
||||||
: startDate
|
: startDate
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,3 +145,36 @@ export const shiftDateRange = (
|
|||||||
}
|
}
|
||||||
return { start, end };
|
return { start, end };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Parses a date in browser display timezone
|
||||||
|
* @param date - The date to parse
|
||||||
|
* @param timezone - The timezone to parse the date in
|
||||||
|
* @returns The parsed date as a Date object
|
||||||
|
*/
|
||||||
|
export const parseDate = (date: string, timezone: string): Date => {
|
||||||
|
const tzDate = new TZDate(date, timezone);
|
||||||
|
return new Date(tzDate.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Formats a date in browser display timezone
|
||||||
|
* @param date - The date to format
|
||||||
|
* @param timezone - The timezone to format the date in
|
||||||
|
* @returns The formatted date in YYYY-MM-DD format
|
||||||
|
*/
|
||||||
|
export const formatDate = (date: Date, timezone: string): string => {
|
||||||
|
const tzDate = new TZDate(date, timezone);
|
||||||
|
return tzDate.toISOString().split("T")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Formats a time in browser display timezone
|
||||||
|
* @param date - The date to format
|
||||||
|
* @param timezone - The timezone to format the time in
|
||||||
|
* @returns The formatted time in HH:mm:ss format
|
||||||
|
*/
|
||||||
|
export const formatTime = (date: Date, timezone: string): string => {
|
||||||
|
const tzDate = new TZDate(date, timezone);
|
||||||
|
return tzDate.toISOString().split("T")[1].split(".")[0];
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user