mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-11 22:19:44 +00:00
Compare commits
584 Commits
20190917.0
...
fix-more-i
Author | SHA1 | Date | |
---|---|---|---|
![]() |
77b37bce7e | ||
![]() |
2e4c73c087 | ||
![]() |
c7f7ef28bf | ||
![]() |
aac7dbab58 | ||
![]() |
8518f774d4 | ||
![]() |
cb0d91d124 | ||
![]() |
107f428dd3 | ||
![]() |
7758ddba56 | ||
![]() |
e0376c803f | ||
![]() |
788c490bbc | ||
![]() |
cdf6e9eb75 | ||
![]() |
4aa49f66bc | ||
![]() |
1bf82f216a | ||
![]() |
004ff58c21 | ||
![]() |
f1a1654371 | ||
![]() |
862044ca23 | ||
![]() |
c54b474838 | ||
![]() |
42cbe863bb | ||
![]() |
ccc42dad79 | ||
![]() |
82ff444cec | ||
![]() |
24c591fbf3 | ||
![]() |
ad676d7fd3 | ||
![]() |
cbe4782d78 | ||
![]() |
3fdcc1c0ea | ||
![]() |
f9d64e51c4 | ||
![]() |
b082828a75 | ||
![]() |
25f5bf0042 | ||
![]() |
f5dec3c6d5 | ||
![]() |
3215437bb8 | ||
![]() |
33176d8f3d | ||
![]() |
f82b62f45c | ||
![]() |
edfdd0da89 | ||
![]() |
33d9bf4660 | ||
![]() |
1912bda60d | ||
![]() |
2e25db4d1b | ||
![]() |
ec08b2ef65 | ||
![]() |
2c740cedb8 | ||
![]() |
e3984d7bf9 | ||
![]() |
2f86b6ec3d | ||
![]() |
d10045eac1 | ||
![]() |
41da68290e | ||
![]() |
593a2de07b | ||
![]() |
59540bdf63 | ||
![]() |
616df070a4 | ||
![]() |
ef62f1956b | ||
![]() |
b41f25ef12 | ||
![]() |
3ed538276e | ||
![]() |
c1a29e8091 | ||
![]() |
4e8bf434f1 | ||
![]() |
dd8c568a2c | ||
![]() |
7ab9257f5e | ||
![]() |
7021fd5809 | ||
![]() |
adec2fc2df | ||
![]() |
27ebcc1bda | ||
![]() |
2fb7a31c76 | ||
![]() |
65994e7280 | ||
![]() |
036eedc69d | ||
![]() |
7937714ce6 | ||
![]() |
cdbd51f6f7 | ||
![]() |
d5a8105718 | ||
![]() |
1c9eab7ca0 | ||
![]() |
6e624b394b | ||
![]() |
1cb10694e7 | ||
![]() |
d496b9742f | ||
![]() |
72e5375795 | ||
![]() |
04f8f0f74f | ||
![]() |
82fb622904 | ||
![]() |
95ba1fd0cb | ||
![]() |
c7b3a517e8 | ||
![]() |
523dc881bb | ||
![]() |
1123adc584 | ||
![]() |
15be1688ad | ||
![]() |
0c0e82a3ba | ||
![]() |
8abe8d7615 | ||
![]() |
f95ba4c04c | ||
![]() |
6874788cc0 | ||
![]() |
f77bd79387 | ||
![]() |
67a91b7c19 | ||
![]() |
30211fe61d | ||
![]() |
9de80b2947 | ||
![]() |
b7a3fe6e91 | ||
![]() |
1f38d13b3b | ||
![]() |
ef6e468a7f | ||
![]() |
729a5e385f | ||
![]() |
32fd7a51f4 | ||
![]() |
f3f32c800e | ||
![]() |
1f44b4b5a9 | ||
![]() |
971538e9c2 | ||
![]() |
db9924bd87 | ||
![]() |
74c6b9077a | ||
![]() |
23865c31e6 | ||
![]() |
8641a701cb | ||
![]() |
3ca99e5c90 | ||
![]() |
753804f463 | ||
![]() |
9aedeab4fa | ||
![]() |
fc4e3e90b2 | ||
![]() |
ae8a9940ed | ||
![]() |
a544295167 | ||
![]() |
8a9e149d33 | ||
![]() |
49611e285f | ||
![]() |
def0c51669 | ||
![]() |
572215b359 | ||
![]() |
0f8cf574d3 | ||
![]() |
312f1df368 | ||
![]() |
5bfacb3bf0 | ||
![]() |
2103866c48 | ||
![]() |
61cf1bf1eb | ||
![]() |
9c407caf2c | ||
![]() |
323cf72be3 | ||
![]() |
eeced628b3 | ||
![]() |
38be488f86 | ||
![]() |
83756a338a | ||
![]() |
b9415cb5f0 | ||
![]() |
fd49e26120 | ||
![]() |
3474e92eb7 | ||
![]() |
913299998c | ||
![]() |
d9e522e4d7 | ||
![]() |
22c8e4a455 | ||
![]() |
02fe5144d8 | ||
![]() |
9d333fb557 | ||
![]() |
ef2ca4a07f | ||
![]() |
f74ee76ae2 | ||
![]() |
6fb9a7636d | ||
![]() |
56249110d6 | ||
![]() |
1a2ebabd22 | ||
![]() |
28511d0cbf | ||
![]() |
b01bdec973 | ||
![]() |
ecee5980af | ||
![]() |
3e1b85e6b5 | ||
![]() |
ce94d1ea7c | ||
![]() |
62654ec598 | ||
![]() |
fbe4550c78 | ||
![]() |
a082667c24 | ||
![]() |
fc29b519ae | ||
![]() |
c391b19c0e | ||
![]() |
8c49e3c4ef | ||
![]() |
67022b9ffc | ||
![]() |
eb9023595d | ||
![]() |
8262d1617f | ||
![]() |
581a803cc4 | ||
![]() |
5ff8fe68ba | ||
![]() |
a2a039ebc5 | ||
![]() |
1064aed1b0 | ||
![]() |
7025592e8e | ||
![]() |
4966354b62 | ||
![]() |
68d6faf4af | ||
![]() |
e3346483b9 | ||
![]() |
e8fb79e5ce | ||
![]() |
d612162ab1 | ||
![]() |
86f8ef3a70 | ||
![]() |
0e43435362 | ||
![]() |
aaefe0b09f | ||
![]() |
bc731a9dc3 | ||
![]() |
da25701dca | ||
![]() |
21ae483dc9 | ||
![]() |
38b6e9ca10 | ||
![]() |
d31245866c | ||
![]() |
4e08d8f3b3 | ||
![]() |
1e717ab33e | ||
![]() |
995fb4974e | ||
![]() |
ffb76132f8 | ||
![]() |
acba3af54b | ||
![]() |
40ac456937 | ||
![]() |
5c32413bf7 | ||
![]() |
22792c70c5 | ||
![]() |
a8ed87298a | ||
![]() |
b15270dfe2 | ||
![]() |
58ad949bc8 | ||
![]() |
adce40de56 | ||
![]() |
0f487ae4bf | ||
![]() |
2848e3a63b | ||
![]() |
5a172a64c5 | ||
![]() |
433aa16ea6 | ||
![]() |
50cb8cf3cc | ||
![]() |
4e5406b27b | ||
![]() |
80eb80619a | ||
![]() |
bf71b3a869 | ||
![]() |
ff270c4b7d | ||
![]() |
8b659498b6 | ||
![]() |
5415068917 | ||
![]() |
357a67c00d | ||
![]() |
cbe4269320 | ||
![]() |
fbd5185ce2 | ||
![]() |
a33cf97e2c | ||
![]() |
7e7da26543 | ||
![]() |
79058e893b | ||
![]() |
e9231fc17e | ||
![]() |
2eb548bb74 | ||
![]() |
08baf8a757 | ||
![]() |
f02fa6a94b | ||
![]() |
2ed6d0e73c | ||
![]() |
35d9b2ac3c | ||
![]() |
18d09c6f04 | ||
![]() |
70b81de49d | ||
![]() |
f0808c1f54 | ||
![]() |
e779f0747e | ||
![]() |
bdd18775c3 | ||
![]() |
711d51c022 | ||
![]() |
1b0d8bba29 | ||
![]() |
2988cc512f | ||
![]() |
a2f8e5f3e7 | ||
![]() |
680bf06a4b | ||
![]() |
ff0b1881e2 | ||
![]() |
de653e1f7b | ||
![]() |
bb41170765 | ||
![]() |
0ed2bc93aa | ||
![]() |
04770f8ee2 | ||
![]() |
15a2790b9f | ||
![]() |
83880791b1 | ||
![]() |
4dca3289f6 | ||
![]() |
083a3ebfc4 | ||
![]() |
6117c4e989 | ||
![]() |
609763e658 | ||
![]() |
2c57ab60f1 | ||
![]() |
dd17a153d2 | ||
![]() |
c2d551bb7c | ||
![]() |
e0b1921108 | ||
![]() |
fcf39ceb96 | ||
![]() |
3cc979a077 | ||
![]() |
9972973774 | ||
![]() |
20ae32bc26 | ||
![]() |
a29892023b | ||
![]() |
b283fec482 | ||
![]() |
e0116a8236 | ||
![]() |
d1990a4bac | ||
![]() |
cbba1849e2 | ||
![]() |
43393d1647 | ||
![]() |
b47ee1051c | ||
![]() |
393adacc9e | ||
![]() |
073428849e | ||
![]() |
e6ac0258e3 | ||
![]() |
d7e7798a55 | ||
![]() |
2557414b11 | ||
![]() |
f7065fbce9 | ||
![]() |
016564eee9 | ||
![]() |
ff3087c39c | ||
![]() |
239438ee5d | ||
![]() |
5458cda31f | ||
![]() |
36f49e66fd | ||
![]() |
2bafd38ea8 | ||
![]() |
73b3262491 | ||
![]() |
808cde033f | ||
![]() |
fa8f6b7b91 | ||
![]() |
94c120cdb1 | ||
![]() |
7b2be54f8f | ||
![]() |
4b56db5255 | ||
![]() |
93165c9111 | ||
![]() |
caa604d5ca | ||
![]() |
e7e9e2cf85 | ||
![]() |
daa04e9973 | ||
![]() |
5355269f5d | ||
![]() |
2665a75250 | ||
![]() |
8a39d18323 | ||
![]() |
b8a026397b | ||
![]() |
bd5fe302eb | ||
![]() |
de0f1b2b65 | ||
![]() |
defaa2b276 | ||
![]() |
60efe00a1f | ||
![]() |
fe93b993db | ||
![]() |
f6afc92d3c | ||
![]() |
e4c635c855 | ||
![]() |
a3e59e168f | ||
![]() |
e56355b406 | ||
![]() |
8ef15c50b4 | ||
![]() |
81588469b8 | ||
![]() |
70a920af3c | ||
![]() |
1329e60c89 | ||
![]() |
9b7c095080 | ||
![]() |
654ff99cd1 | ||
![]() |
0511bc360e | ||
![]() |
ea9e8cc392 | ||
![]() |
8433678371 | ||
![]() |
757bc00854 | ||
![]() |
2551393821 | ||
![]() |
0acd41b7f0 | ||
![]() |
85ca73db84 | ||
![]() |
444cbd00d9 | ||
![]() |
15b500886c | ||
![]() |
3aac834e72 | ||
![]() |
6edf23b91f | ||
![]() |
e445251b02 | ||
![]() |
693151b590 | ||
![]() |
1249c0eea9 | ||
![]() |
3133118870 | ||
![]() |
de5c1a0545 | ||
![]() |
c61e2fb459 | ||
![]() |
64a2a19da3 | ||
![]() |
74fe1f820c | ||
![]() |
69929f5dc3 | ||
![]() |
fcd793fc9e | ||
![]() |
8a3b1d76a1 | ||
![]() |
9f520d7628 | ||
![]() |
258cfddc3f | ||
![]() |
3697500402 | ||
![]() |
b4942ad27e | ||
![]() |
1e217e8d2f | ||
![]() |
0056237d85 | ||
![]() |
920ee741f3 | ||
![]() |
6ecc60423f | ||
![]() |
09e7638c89 | ||
![]() |
b82b4a639e | ||
![]() |
d08aa51c16 | ||
![]() |
385ffe6d8f | ||
![]() |
564e6d4073 | ||
![]() |
a4bd816eb5 | ||
![]() |
13c18a9bb7 | ||
![]() |
562d7a7cf4 | ||
![]() |
89f33a1730 | ||
![]() |
ff7309f5c4 | ||
![]() |
1c614c855f | ||
![]() |
6a3238951d | ||
![]() |
0dab5828fb | ||
![]() |
d0b9c09f8f | ||
![]() |
55f4629256 | ||
![]() |
004565217e | ||
![]() |
c07b39ebde | ||
![]() |
8b17b6ed1c | ||
![]() |
1d16bdbe54 | ||
![]() |
9e2a0c77d5 | ||
![]() |
4f41508110 | ||
![]() |
eaedb2e5ae | ||
![]() |
75ad1f51a9 | ||
![]() |
142175c6ab | ||
![]() |
f1980d6bcf | ||
![]() |
5a7b5200fe | ||
![]() |
d284d53b93 | ||
![]() |
bc01df42d8 | ||
![]() |
901752bec3 | ||
![]() |
e3ef3cfae1 | ||
![]() |
ab476d2f1b | ||
![]() |
5ca82fd39c | ||
![]() |
da35c263d2 | ||
![]() |
2a617a9639 | ||
![]() |
c730aab28f | ||
![]() |
274c2016c0 | ||
![]() |
9b3891f778 | ||
![]() |
b705de956e | ||
![]() |
e37201f84f | ||
![]() |
f53eea81c4 | ||
![]() |
0fa8db1682 | ||
![]() |
46f5224e70 | ||
![]() |
12be2a9775 | ||
![]() |
6196bbdc5e | ||
![]() |
b41f4777d4 | ||
![]() |
f2812bc706 | ||
![]() |
04500bc237 | ||
![]() |
2a6b877cf1 | ||
![]() |
c3896a4613 | ||
![]() |
c6fb896fe4 | ||
![]() |
669fbb7e77 | ||
![]() |
971865e4f9 | ||
![]() |
9078e41855 | ||
![]() |
466c48a7d0 | ||
![]() |
31a047ce9e | ||
![]() |
bd24ffa5d0 | ||
![]() |
99f4bd7398 | ||
![]() |
417177b097 | ||
![]() |
c407cab501 | ||
![]() |
044cf22f47 | ||
![]() |
75aa940d44 | ||
![]() |
7be8080726 | ||
![]() |
13fbc813cd | ||
![]() |
44d1458229 | ||
![]() |
f06f3ee2e5 | ||
![]() |
a889a02e15 | ||
![]() |
6bf3d6a689 | ||
![]() |
1d7dcca495 | ||
![]() |
ad8f049570 | ||
![]() |
73c56a68b6 | ||
![]() |
b34b52f305 | ||
![]() |
39d052273d | ||
![]() |
e435b9153b | ||
![]() |
0792278927 | ||
![]() |
06d59b3cde | ||
![]() |
1e7497ad33 | ||
![]() |
49d0f2359b | ||
![]() |
bb73039205 | ||
![]() |
d4d6b7e2ce | ||
![]() |
7b5201599d | ||
![]() |
11c08e9a69 | ||
![]() |
731bb176f7 | ||
![]() |
b0fce93de8 | ||
![]() |
fdbe89e87e | ||
![]() |
a8d0a2293f | ||
![]() |
8ac278bc59 | ||
![]() |
70d6c6b902 | ||
![]() |
0621218e16 | ||
![]() |
2424376fba | ||
![]() |
3973374f3f | ||
![]() |
c25a38b82f | ||
![]() |
3c0ba1d7eb | ||
![]() |
be678b02c5 | ||
![]() |
0078b48e3c | ||
![]() |
540f1d9bce | ||
![]() |
5e3cb812ec | ||
![]() |
6d10a5dd4c | ||
![]() |
96d14b7ab7 | ||
![]() |
b96b026905 | ||
![]() |
c25f2d3941 | ||
![]() |
785453aa79 | ||
![]() |
4dbf5327bd | ||
![]() |
603240c467 | ||
![]() |
bbc3e7d93f | ||
![]() |
fbee4937a0 | ||
![]() |
0a77728652 | ||
![]() |
e3ed0cf436 | ||
![]() |
d05dc2e4dc | ||
![]() |
c437cd3865 | ||
![]() |
442171169b | ||
![]() |
cc12dbb6ee | ||
![]() |
60b3a960ae | ||
![]() |
5a957c3c9e | ||
![]() |
be4d431dc3 | ||
![]() |
0005c75091 | ||
![]() |
880b382a16 | ||
![]() |
d012512a79 | ||
![]() |
e2ac842690 | ||
![]() |
67d8d48855 | ||
![]() |
00f2d36cb5 | ||
![]() |
035057b185 | ||
![]() |
982966c8d9 | ||
![]() |
f5e3a9ad40 | ||
![]() |
141c3f1ea4 | ||
![]() |
4ea483e3de | ||
![]() |
8eca956cd1 | ||
![]() |
c9242a5075 | ||
![]() |
df29a5becb | ||
![]() |
fb589337f8 | ||
![]() |
ea5ee6189d | ||
![]() |
a39e47cced | ||
![]() |
49d69f65ad | ||
![]() |
424d677bcb | ||
![]() |
59e4cdc62a | ||
![]() |
9d3dfad98c | ||
![]() |
555b746f4b | ||
![]() |
ce6a97d065 | ||
![]() |
88567df36d | ||
![]() |
f55cbd9e9a | ||
![]() |
7d00cc1eff | ||
![]() |
29301ddee7 | ||
![]() |
978b773968 | ||
![]() |
4f30cae6aa | ||
![]() |
5f29b66a8d | ||
![]() |
b94da1bd19 | ||
![]() |
f9b0a0fc13 | ||
![]() |
300ffdae04 | ||
![]() |
476525e0d4 | ||
![]() |
edecf9d58f | ||
![]() |
38bf2e116b | ||
![]() |
0719c4d1ae | ||
![]() |
12840231be | ||
![]() |
4728c12225 | ||
![]() |
90526ac563 | ||
![]() |
6f7ea03e35 | ||
![]() |
78900e05ad | ||
![]() |
495f4aa19c | ||
![]() |
88c480759f | ||
![]() |
ab75365636 | ||
![]() |
0266617c71 | ||
![]() |
aef45c5043 | ||
![]() |
deeb0146c7 | ||
![]() |
5dea674f20 | ||
![]() |
646fe34d09 | ||
![]() |
c67907aa58 | ||
![]() |
e78f4c5ace | ||
![]() |
e891fdc3eb | ||
![]() |
95a258c2a5 | ||
![]() |
f1fabd09a6 | ||
![]() |
0d77bdaf32 | ||
![]() |
320be2e5d9 | ||
![]() |
6a098ad0b5 | ||
![]() |
e895e91a11 | ||
![]() |
fc3f7ca4b2 | ||
![]() |
1f09d848c5 | ||
![]() |
4d794f6088 | ||
![]() |
9ad7f0dbac | ||
![]() |
9f39610153 | ||
![]() |
12d8a04c15 | ||
![]() |
73b0f5949e | ||
![]() |
0f7a3887a7 | ||
![]() |
ac75ce038a | ||
![]() |
8de9a73741 | ||
![]() |
ef51f29e28 | ||
![]() |
b61bbee35a | ||
![]() |
64dd8c463d | ||
![]() |
d2a95e9f06 | ||
![]() |
0cb0525516 | ||
![]() |
dcaf4fdfe2 | ||
![]() |
0c13757910 | ||
![]() |
0cdcd74c9d | ||
![]() |
db3968399f | ||
![]() |
7494a49238 | ||
![]() |
55d2a3c8b1 | ||
![]() |
be4e45c22c | ||
![]() |
efb28d337a | ||
![]() |
edd77e1f32 | ||
![]() |
848dd7e071 | ||
![]() |
59d4a4247a | ||
![]() |
ba79633758 | ||
![]() |
860973bdbd | ||
![]() |
d4d897e79e | ||
![]() |
4850f3d588 | ||
![]() |
8bc53c235f | ||
![]() |
c74793b1d5 | ||
![]() |
56bac8a8c1 | ||
![]() |
184575fd54 | ||
![]() |
e148559d3e | ||
![]() |
95b76dbb85 | ||
![]() |
496bb9dc39 | ||
![]() |
351ba3e701 | ||
![]() |
260f428bc6 | ||
![]() |
3622514131 | ||
![]() |
391b2dcf6a | ||
![]() |
a02bf1fd48 | ||
![]() |
4cf9472bf4 | ||
![]() |
74d1de7313 | ||
![]() |
cd6fd6a46c | ||
![]() |
a6dda90b13 | ||
![]() |
7add8a2ea0 | ||
![]() |
b927a3ef29 | ||
![]() |
76d3218130 | ||
![]() |
8b6d8f9086 | ||
![]() |
ffaecb29b7 | ||
![]() |
fa74295c0b | ||
![]() |
ea50d486da | ||
![]() |
3cf4b890b6 | ||
![]() |
313b984a53 | ||
![]() |
7d09e29d60 | ||
![]() |
7e979f0cf1 | ||
![]() |
64366dc99a | ||
![]() |
c69585db98 | ||
![]() |
2dd5cd586b | ||
![]() |
05a258c886 | ||
![]() |
f4bd42dfd4 | ||
![]() |
41e5e7c1ae | ||
![]() |
95dfcafce3 | ||
![]() |
111d1afc21 | ||
![]() |
886c6dd88c | ||
![]() |
38b817bd67 | ||
![]() |
c59b6626f2 | ||
![]() |
2cc196e3fb | ||
![]() |
a08884fed6 | ||
![]() |
2fe4a02b6b | ||
![]() |
7c793c1cdb | ||
![]() |
a0b848acc4 | ||
![]() |
a1b9a092d0 | ||
![]() |
993d390ea5 | ||
![]() |
1f4d359050 | ||
![]() |
f871387fa6 | ||
![]() |
9a92ed31f6 | ||
![]() |
37129adfab | ||
![]() |
ec52e71c71 | ||
![]() |
5e28e1b320 | ||
![]() |
9a7eb3d406 | ||
![]() |
eee0c2e53f | ||
![]() |
145259e82f | ||
![]() |
d0cc4c2715 | ||
![]() |
4d97a47e08 | ||
![]() |
4ef5a8da70 | ||
![]() |
cdfd0afdf4 | ||
![]() |
d250a931e6 | ||
![]() |
cd2b92a449 | ||
![]() |
c617cb5b12 | ||
![]() |
e7ac95e314 | ||
![]() |
7bab9cb464 | ||
![]() |
bb5ab958c1 | ||
![]() |
2d92ffaa4d | ||
![]() |
7a7a0f772a | ||
![]() |
c898db5010 | ||
![]() |
b6fbf4da3a | ||
![]() |
0bfc61629e | ||
![]() |
e594fcfc42 | ||
![]() |
84ed6d8fb3 | ||
![]() |
31ae115062 | ||
![]() |
fca885a17a | ||
![]() |
29dff42de4 | ||
![]() |
f5b3a82922 | ||
![]() |
54beaad7e5 | ||
![]() |
e30f9d4a66 | ||
![]() |
27264b27a9 | ||
![]() |
2b1f9460a8 | ||
![]() |
4641cd65ca | ||
![]() |
1f6fe5dfcf | ||
![]() |
058b4ba658 |
37
.github/ISSUE_TEMPLATE.md
vendored
37
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,37 +0,0 @@
|
|||||||
<!-- READ THIS FIRST:
|
|
||||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
|
||||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
|
||||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
|
||||||
- Provide as many details as possible. Do not delete any text from this template!
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Home Assistant release with the issue:**
|
|
||||||
<!--
|
|
||||||
- Frontend -> Developer tools -> Info
|
|
||||||
- Or use this command: hass --version
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Last working Home Assistant release (if known):**
|
|
||||||
|
|
||||||
**UI (States or Lovelace UI?):**
|
|
||||||
<!--
|
|
||||||
- Frontend -> Developer tools -> Info
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Browser and Operating System:**
|
|
||||||
<!--
|
|
||||||
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Description of problem:**
|
|
||||||
<!--
|
|
||||||
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
**Javascript errors shown in the web inspector (if applicable):**
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
**Additional information:**
|
|
85
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
Normal file
85
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
name: Report a bug with the UI, Frontend or Lovelace
|
||||||
|
about: Report an issue related to the Home Assistant frontend.
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
<!-- READ THIS FIRST:
|
||||||
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||||
|
- Do not report issues for custom Lovelace cards.
|
||||||
|
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||||
|
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||||
|
-->
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have updated to the latest available Home Assistant version.
|
||||||
|
- [ ] I have cleared the cache of my browser.
|
||||||
|
- [ ] I have tried a different browser to see if it is related to my browser.
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
<!--
|
||||||
|
Describe the issue you are experiencing here to communicate to the
|
||||||
|
maintainers. Tell us about the current behavior.
|
||||||
|
If possible provide a screenshot with a description.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Expected behavior
|
||||||
|
<!--
|
||||||
|
Describe what you expected to happen or it should look/behave.
|
||||||
|
If possible provide a screenshot with a description.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
<!--
|
||||||
|
Provide steps for us, that helps reproducing your issue.
|
||||||
|
For example:
|
||||||
|
1. Add a climate integration
|
||||||
|
2. Navigate to Lovelace
|
||||||
|
3. Click more info of the climate entity
|
||||||
|
4. Set the HVAC action to heat
|
||||||
|
5. Set the temperature higher than the current temperature
|
||||||
|
6. Set the HVAC action to cool
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
<!--
|
||||||
|
Provide details about the versions you are using, which helps us reproducing
|
||||||
|
and finding the issue quicker. Version information is found in the
|
||||||
|
Home Assistant frontend: Developer tools -> Info.
|
||||||
|
|
||||||
|
Browser version and operating system is important! Please try to replicate
|
||||||
|
your issue in a different browser and be sure to include your findings.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- Home Assistant release with the issue:
|
||||||
|
- Last working Home Assistant release (if known):
|
||||||
|
- UI Type (States or Lovelace):
|
||||||
|
- Browser and browser version:
|
||||||
|
- Operating system:
|
||||||
|
|
||||||
|
## Problem-relevant configuration
|
||||||
|
<!--
|
||||||
|
An example configuration that caused the problem for you. Fill this out even
|
||||||
|
if it seems unimportant to you. Please be sure to remove personal information
|
||||||
|
like passwords, private URLs and other credentials.
|
||||||
|
-->
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Javascript errors shown in your browser console/inspector
|
||||||
|
<!--
|
||||||
|
If you come across any javascript or other error logs, e.g., in your browser
|
||||||
|
console/inspector please provide them.
|
||||||
|
-->
|
||||||
|
|
||||||
|
```txt
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
25
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: Request a feature for the UI, Frontend or Lovelace
|
||||||
|
about: Request an new feature for the Home Assistant frontend.
|
||||||
|
labels: feature request
|
||||||
|
---
|
||||||
|
<!--
|
||||||
|
DO NOT DELETE ANY TEXT from this template!
|
||||||
|
Otherwise, your request may be closed without comment.
|
||||||
|
-->
|
||||||
|
## The request
|
||||||
|
<!--
|
||||||
|
Describe to our maintainers, the feature you would like to be added.
|
||||||
|
Please be clear and concise and, if possible, provide a screenshot or mockup.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## The alternatives
|
||||||
|
<!--
|
||||||
|
Are you currently using, or have you considered alternatives?
|
||||||
|
If so, could you please describe those?
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||||
|
url: https://github.com/home-assistant/home-assistant/issues
|
||||||
|
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||||
|
- name: Report incorrect or missing information on our website
|
||||||
|
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||||
|
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
||||||
|
- name: I have a question or need support
|
||||||
|
url: https://www.home-assistant.io/help
|
||||||
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
|
- name: I'm unsure where to go
|
||||||
|
url: https://www.home-assistant.io/join-chat
|
||||||
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
77
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
77
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<!--
|
||||||
|
You are amazing! Thanks for contributing to our project!
|
||||||
|
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
|
||||||
|
-->
|
||||||
|
## Breaking change
|
||||||
|
<!--
|
||||||
|
If your PR contains a breaking change for existing users, it is important
|
||||||
|
to tell them what breaks, how to make it work again and why we did this.
|
||||||
|
This piece of text is published with the release notes, so it helps if you
|
||||||
|
write it towards our users, not us.
|
||||||
|
Note: Remove this section if this PR is NOT a breaking change.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Proposed change
|
||||||
|
<!--
|
||||||
|
Describe the big picture of your changes here to communicate to the
|
||||||
|
maintainers why we should accept this pull request. If it fixes a bug
|
||||||
|
or resolves a feature request, be sure to link to that issue in the
|
||||||
|
additional information section.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
<!--
|
||||||
|
What type of change does your PR introduce to the Home Assistant frontend?
|
||||||
|
NOTE: Please, check only 1! box!
|
||||||
|
If your PR requires multiple boxes to be checked, you'll most likely need to
|
||||||
|
split it into multiple PRs. This makes things easier and faster to code review.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] Dependency upgrade
|
||||||
|
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (thank you!)
|
||||||
|
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||||
|
- [ ] Code quality improvements to existing code or addition of tests
|
||||||
|
|
||||||
|
## Example configuration
|
||||||
|
<!--
|
||||||
|
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||||
|
your PR.
|
||||||
|
-->
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
<!--
|
||||||
|
Details are important, and help maintainers processing your PR.
|
||||||
|
Please be sure to fill out additional details, if applicable.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- This PR fixes or closes issue: fixes #
|
||||||
|
- This PR is related to issue:
|
||||||
|
- Link to documentation pull request:
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
<!--
|
||||||
|
Put an `x` in the boxes that apply. You can also fill these out after
|
||||||
|
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||||
|
We're here to help! This is simply a reminder of what we are going to look
|
||||||
|
for before merging your code.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] The code change is tested and works locally.
|
||||||
|
- [ ] There is no commented out code in this PR.
|
||||||
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
|
|
||||||
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
|
|
||||||
|
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for contributing <3
|
||||||
|
-->
|
||||||
|
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
27
.github/lock.yml
vendored
Normal file
27
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
||||||
|
|
||||||
|
# Number of days of inactivity before a closed issue or pull request is locked
|
||||||
|
daysUntilLock: 1
|
||||||
|
|
||||||
|
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||||
|
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||||
|
skipCreatedBefore: 2020-01-01
|
||||||
|
|
||||||
|
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||||
|
exemptLabels: []
|
||||||
|
|
||||||
|
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||||
|
lockLabel: false
|
||||||
|
|
||||||
|
# Comment to post before locking. Set to `false` to disable
|
||||||
|
lockComment: false
|
||||||
|
|
||||||
|
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||||
|
setLockReason: false
|
||||||
|
|
||||||
|
# Limit to only `issues` or `pulls`
|
||||||
|
only: pulls
|
||||||
|
|
||||||
|
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||||
|
issues:
|
||||||
|
daysUntilLock: 30
|
56
.github/stale.yml
vendored
Normal file
56
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Configuration for probot-stale - https://github.com/probot/stale
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||||
|
daysUntilStale: 90
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||||
|
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||||
|
daysUntilClose: 7
|
||||||
|
|
||||||
|
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||||
|
onlyLabels: []
|
||||||
|
|
||||||
|
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||||
|
exemptLabels:
|
||||||
|
- feature request
|
||||||
|
- Help wanted
|
||||||
|
- to do
|
||||||
|
|
||||||
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
|
exemptProjects: true
|
||||||
|
|
||||||
|
# Set to true to ignore issues in a milestone (defaults to false)
|
||||||
|
exemptMilestones: true
|
||||||
|
|
||||||
|
# Set to true to ignore issues with an assignee (defaults to false)
|
||||||
|
exemptAssignees: false
|
||||||
|
|
||||||
|
# Label to use when marking as stale
|
||||||
|
staleLabel: stale
|
||||||
|
|
||||||
|
# Comment to post when marking as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
There hasn't been any activity on this issue recently. Due to the high number
|
||||||
|
of incoming GitHub notifications, we have to clean some of the old issues,
|
||||||
|
as many of them have already been resolved with the latest updates.
|
||||||
|
|
||||||
|
Please make sure to update to the latest Home Assistant version and check
|
||||||
|
if that solves the issue. Let us know if that works for you by adding a
|
||||||
|
comment 👍
|
||||||
|
|
||||||
|
This issue now has been marked as stale and will be closed if no further
|
||||||
|
activity occurs. Thank you for your contributions.
|
||||||
|
|
||||||
|
# Comment to post when removing the stale label.
|
||||||
|
# unmarkComment: >
|
||||||
|
# Your comment here.
|
||||||
|
|
||||||
|
# Comment to post when closing a stale Issue or Pull Request.
|
||||||
|
# closeComment: >
|
||||||
|
# Your comment here.
|
||||||
|
|
||||||
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
|
limitPerRun: 30
|
||||||
|
|
||||||
|
# Limit to only `issues` or `pulls`
|
||||||
|
only: issues
|
12
.travis.yml
12
.travis.yml
@@ -8,19 +8,11 @@ install: yarn install
|
|||||||
script:
|
script:
|
||||||
- npm run build
|
- npm run build
|
||||||
- hassio/script/build_hassio
|
- hassio/script/build_hassio
|
||||||
|
# Because else eslint fails because hassio has cleaned that build
|
||||||
|
- ./node_modules/.bin/gulp gen-icons-app
|
||||||
- npm run test
|
- npm run test
|
||||||
# - xvfb-run wct --module-resolution=node --npm
|
# - xvfb-run wct --module-resolution=node --npm
|
||||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
before_deploy:
|
|
||||||
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
|
|
||||||
deploy:
|
|
||||||
provider: script
|
|
||||||
script: script/travis_deploy
|
|
||||||
'on':
|
|
||||||
branch: master
|
|
||||||
dist: trusty
|
dist: trusty
|
||||||
addons:
|
addons:
|
||||||
sauce_connect: true
|
sauce_connect: true
|
||||||
|
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||||
|
|
||||||
[](https://home-assistant.io/demo/)
|
[](https://demo.home-assistant.io/)
|
||||||
|
|
||||||
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
|
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||||
- [More information about Home Assistant](https://home-assistant.io)
|
- [More information about Home Assistant](https://home-assistant.io)
|
||||||
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
|
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
|
||||||
|
|
||||||
@@ -31,3 +31,5 @@ It is possible to compile the project and/or run commands in the development env
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||||
|
|
||||||
|
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.
|
||||||
|
27
azure-pipelines-netlify.yml
Normal file
27
azure-pipelines-netlify.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# https://dev.azure.com/home-assistant
|
||||||
|
|
||||||
|
trigger: none
|
||||||
|
pr: none
|
||||||
|
schedules:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
displayName: "build preview"
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- dev
|
||||||
|
always: false
|
||||||
|
variables:
|
||||||
|
- group: netlify
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
- job: 'Netlify_preview'
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- script: |
|
||||||
|
# Cast
|
||||||
|
curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_CAST}
|
||||||
|
|
||||||
|
# Demo
|
||||||
|
curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_DEMO}
|
||||||
|
displayName: 'Trigger netlify build preview'
|
70
azure-pipelines-translation.yml
Normal file
70
azure-pipelines-translation.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# https://dev.azure.com/home-assistant
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
batch: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- translations/en.json
|
||||||
|
pr: none
|
||||||
|
schedules:
|
||||||
|
- cron: "30 0 * * *"
|
||||||
|
displayName: "translation update"
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- dev
|
||||||
|
always: true
|
||||||
|
variables:
|
||||||
|
- group: translation
|
||||||
|
resources:
|
||||||
|
repositories:
|
||||||
|
- repository: azure
|
||||||
|
type: github
|
||||||
|
name: 'home-assistant/ci-azure'
|
||||||
|
endpoint: 'home-assistant'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
- job: 'Upload'
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
displayName: 'Use Node 12.x'
|
||||||
|
inputs:
|
||||||
|
versionSpec: '12.x'
|
||||||
|
- script: |
|
||||||
|
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||||
|
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||||
|
|
||||||
|
./script/translations_upload_base
|
||||||
|
displayName: 'Upload Translation'
|
||||||
|
|
||||||
|
- job: 'Download'
|
||||||
|
dependsOn:
|
||||||
|
- 'Upload'
|
||||||
|
condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual'))
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
displayName: 'Use Node 12.x'
|
||||||
|
inputs:
|
||||||
|
versionSpec: '12.x'
|
||||||
|
- template: templates/azp-step-git-init.yaml@azure
|
||||||
|
- script: |
|
||||||
|
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||||
|
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||||
|
|
||||||
|
npm install
|
||||||
|
./script/translations_download
|
||||||
|
displayName: 'Download Translation'
|
||||||
|
- script: |
|
||||||
|
git checkout dev
|
||||||
|
git add translation
|
||||||
|
git commit -am "[ci skip] Translation update"
|
||||||
|
git push
|
||||||
|
displayName: 'Update translation'
|
49
build-scripts/babel.js
Normal file
49
build-scripts/babel.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||||
|
if (latestBuild === undefined) {
|
||||||
|
throw Error("latestBuild not defined for babel loader config");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
test: /\.m?js$|\.tsx?$/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
!latestBuild && [
|
||||||
|
require("@babel/preset-env").default,
|
||||||
|
{ modules: false },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require("@babel/preset-typescript").default,
|
||||||
|
{
|
||||||
|
jsxPragma: "h",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
].filter(Boolean),
|
||||||
|
plugins: [
|
||||||
|
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||||
|
[
|
||||||
|
"@babel/plugin-proposal-object-rest-spread",
|
||||||
|
{ loose: true, useBuiltIns: true },
|
||||||
|
],
|
||||||
|
// Only support the syntax, Webpack will handle it.
|
||||||
|
"@babel/syntax-dynamic-import",
|
||||||
|
[
|
||||||
|
"@babel/transform-react-jsx",
|
||||||
|
{
|
||||||
|
pragma: "h",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@babel/plugin-proposal-optional-chaining",
|
||||||
|
[
|
||||||
|
require("@babel/plugin-proposal-decorators").default,
|
||||||
|
{ decoratorsBeforeExport: true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require("@babel/plugin-proposal-class-properties").default,
|
||||||
|
{ loose: true },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
6
build-scripts/env.js
Normal file
6
build-scripts/env.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
isProdBuild: process.env.NODE_ENV === "production",
|
||||||
|
isStatsBuild: process.env.STATS === "1",
|
||||||
|
isTravis: process.env.TRAVIS === "true",
|
||||||
|
isNetlify: process.env.NETLIFY === "true",
|
||||||
|
};
|
@@ -1,10 +1,13 @@
|
|||||||
// Run HA develop mode
|
// Run HA develop mode
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const envVars = require("../env");
|
||||||
|
|
||||||
require("./clean.js");
|
require("./clean.js");
|
||||||
require("./translations.js");
|
require("./translations.js");
|
||||||
require("./gen-icons.js");
|
require("./gen-icons.js");
|
||||||
require("./gather-static.js");
|
require("./gather-static.js");
|
||||||
|
require("./compress.js");
|
||||||
require("./webpack.js");
|
require("./webpack.js");
|
||||||
require("./service-worker.js");
|
require("./service-worker.js");
|
||||||
require("./entry-html.js");
|
require("./entry-html.js");
|
||||||
@@ -18,7 +21,7 @@ gulp.task(
|
|||||||
"clean",
|
"clean",
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-service-worker-dev",
|
"gen-service-worker-dev",
|
||||||
"gen-icons",
|
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||||
"gen-pages-dev",
|
"gen-pages-dev",
|
||||||
"gen-index-app-dev",
|
"gen-index-app-dev",
|
||||||
gulp.series("create-test-translation", "build-translations")
|
gulp.series("create-test-translation", "build-translations")
|
||||||
@@ -35,13 +38,11 @@ gulp.task(
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
},
|
},
|
||||||
"clean",
|
"clean",
|
||||||
gulp.parallel("gen-icons", "build-translations"),
|
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||||
"copy-static",
|
"copy-static",
|
||||||
gulp.parallel(
|
"webpack-prod-app",
|
||||||
"webpack-prod-app",
|
...// Don't compress running tests
|
||||||
// Do not compress static files in CI, it's SLOW.
|
(envVars.isTravis ? [] : ["compress-app"]),
|
||||||
...(process.env.CI === "true" ? [] : ["compress-static"])
|
|
||||||
),
|
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-pages-prod",
|
"gen-pages-prod",
|
||||||
"gen-index-app-prod",
|
"gen-index-app-prod",
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
// Run cast develop mode
|
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
|
|
||||||
require("./clean.js");
|
require("./clean.js");
|
||||||
@@ -16,7 +15,12 @@ gulp.task(
|
|||||||
process.env.NODE_ENV = "development";
|
process.env.NODE_ENV = "development";
|
||||||
},
|
},
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
|
gulp.parallel(
|
||||||
|
"gen-icons-app",
|
||||||
|
"gen-icons-mdi",
|
||||||
|
"gen-index-cast-dev",
|
||||||
|
"build-translations"
|
||||||
|
),
|
||||||
"copy-static-cast",
|
"copy-static-cast",
|
||||||
"webpack-dev-server-cast"
|
"webpack-dev-server-cast"
|
||||||
)
|
)
|
||||||
@@ -29,7 +33,7 @@ gulp.task(
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
},
|
},
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("gen-icons", "build-translations"),
|
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||||
"copy-static-cast",
|
"copy-static-cast",
|
||||||
"webpack-prod-cast",
|
"webpack-prod-cast",
|
||||||
"gen-index-cast-prod"
|
"gen-index-cast-prod"
|
||||||
|
@@ -9,15 +9,31 @@ gulp.task(
|
|||||||
return del([config.root, config.build_dir]);
|
return del([config.root, config.build_dir]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||||
return del([config.demo_root, config.build_dir]);
|
return del([config.demo_root, config.build_dir]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"clean-cast",
|
"clean-cast",
|
||||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||||
return del([config.cast_root, config.build_dir]);
|
return del([config.cast_root, config.build_dir]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-hassio",
|
||||||
|
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||||
|
return del([config.hassio_root, config.build_dir]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"clean-gallery",
|
||||||
|
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||||
|
return del([config.gallery_root, config.build_dir]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
38
build-scripts/gulp/compress.js
Normal file
38
build-scripts/gulp/compress.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Tasks to compress
|
||||||
|
|
||||||
|
const gulp = require("gulp");
|
||||||
|
const zopfli = require("gulp-zopfli-green");
|
||||||
|
const merge = require("merge-stream");
|
||||||
|
const path = require("path");
|
||||||
|
const paths = require("../paths");
|
||||||
|
|
||||||
|
gulp.task("compress-app", function compressApp() {
|
||||||
|
const jsLatest = gulp
|
||||||
|
.src(path.resolve(paths.output, "**/*.js"))
|
||||||
|
.pipe(zopfli())
|
||||||
|
.pipe(gulp.dest(paths.output));
|
||||||
|
|
||||||
|
const jsEs5 = gulp
|
||||||
|
.src(path.resolve(paths.output_es5, "**/*.js"))
|
||||||
|
.pipe(zopfli())
|
||||||
|
.pipe(gulp.dest(paths.output_es5));
|
||||||
|
|
||||||
|
const polyfills = gulp
|
||||||
|
.src(path.resolve(paths.static, "polyfills/*.js"))
|
||||||
|
.pipe(zopfli())
|
||||||
|
.pipe(gulp.dest(path.resolve(paths.static, "polyfills")));
|
||||||
|
|
||||||
|
const translations = gulp
|
||||||
|
.src(path.resolve(paths.static, "translations/*.json"))
|
||||||
|
.pipe(zopfli())
|
||||||
|
.pipe(gulp.dest(path.resolve(paths.static, "translations")));
|
||||||
|
|
||||||
|
return merge(jsLatest, jsEs5, polyfills, translations);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("compress-hassio", function compressApp() {
|
||||||
|
return gulp
|
||||||
|
.src(path.resolve(paths.hassio_root, "**/*.js"))
|
||||||
|
.pipe(zopfli())
|
||||||
|
.pipe(gulp.dest(paths.hassio_root));
|
||||||
|
});
|
@@ -17,7 +17,8 @@ gulp.task(
|
|||||||
},
|
},
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"gen-icons",
|
"gen-icons-app",
|
||||||
|
"gen-icons-mdi",
|
||||||
"gen-icons-demo",
|
"gen-icons-demo",
|
||||||
"gen-index-demo-dev",
|
"gen-index-demo-dev",
|
||||||
"build-translations"
|
"build-translations"
|
||||||
@@ -34,7 +35,12 @@ gulp.task(
|
|||||||
process.env.NODE_ENV = "production";
|
process.env.NODE_ENV = "production";
|
||||||
},
|
},
|
||||||
"clean-demo",
|
"clean-demo",
|
||||||
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
|
gulp.parallel(
|
||||||
|
"gen-icons-app",
|
||||||
|
"gen-icons-mdi",
|
||||||
|
"gen-icons-demo",
|
||||||
|
"build-translations"
|
||||||
|
),
|
||||||
"copy-static-demo",
|
"copy-static-demo",
|
||||||
"webpack-prod-demo",
|
"webpack-prod-demo",
|
||||||
"gen-index-demo-prod"
|
"gen-index-demo-prod"
|
||||||
|
@@ -14,7 +14,7 @@ function hasHtml(data) {
|
|||||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||||
Object.keys(data).forEach(function(key) {
|
Object.keys(data).forEach(function(key) {
|
||||||
if (typeof data[key] === "object") {
|
if (typeof data[key] === "object") {
|
||||||
nextRecKey = recKey ? `${recKey}.${key}` : key;
|
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||||
} else if (hasHtml(data[key])) {
|
} else if (hasHtml(data[key])) {
|
||||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||||
@@ -23,7 +23,7 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkHtml() {
|
function checkHtml() {
|
||||||
let errors = [];
|
const errors = [];
|
||||||
|
|
||||||
return mapStream(function(file, cb) {
|
return mapStream(function(file, cb) {
|
||||||
const content = file.contents;
|
const content = file.contents;
|
||||||
|
@@ -11,12 +11,6 @@ const config = require("../paths.js");
|
|||||||
const templatePath = (tpl) =>
|
const templatePath = (tpl) =>
|
||||||
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
|
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||||
|
|
||||||
const demoTemplatePath = (tpl) =>
|
|
||||||
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
|
|
||||||
|
|
||||||
const castTemplatePath = (tpl) =>
|
|
||||||
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
|
|
||||||
|
|
||||||
const readFile = (pth) => fs.readFileSync(pth).toString();
|
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||||
|
|
||||||
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||||
@@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDemoTemplate = (pth, data = {}) =>
|
const renderDemoTemplate = (pth, data = {}) =>
|
||||||
renderTemplate(pth, data, demoTemplatePath);
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
const renderCastTemplate = (pth, data = {}) =>
|
const renderCastTemplate = (pth, data = {}) =>
|
||||||
renderTemplate(pth, data, castTemplatePath);
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGalleryTemplate = (pth, data = {}) =>
|
||||||
|
renderTemplate(pth, data, (tpl) =>
|
||||||
|
path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`)
|
||||||
|
);
|
||||||
|
|
||||||
const minifyHtml = (content) =>
|
const minifyHtml = (content) =>
|
||||||
minify(content, {
|
minify(content, {
|
||||||
@@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
|
|||||||
es5Compatibility: es5Manifest["compatibility.js"],
|
es5Compatibility: es5Manifest["compatibility.js"],
|
||||||
es5DemoJS: es5Manifest["main.js"],
|
es5DemoJS: es5Manifest["main.js"],
|
||||||
});
|
});
|
||||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
|
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-gallery-dev", (done) => {
|
||||||
|
// In dev mode we don't mangle names, so we hardcode urls. That way we can
|
||||||
|
// run webpack as last in watch mode, which blocks output.
|
||||||
|
const content = renderGalleryTemplate("index", {
|
||||||
|
latestGalleryJS: "./entrypoint.js",
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-index-gallery-prod", (done) => {
|
||||||
|
const latestManifest = require(path.resolve(
|
||||||
|
config.gallery_output,
|
||||||
|
"manifest.json"
|
||||||
|
));
|
||||||
|
const content = renderGalleryTemplate("index", {
|
||||||
|
latestGalleryJS: latestManifest["entrypoint.js"],
|
||||||
|
});
|
||||||
|
const minified = minifyHtml(content);
|
||||||
|
|
||||||
|
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
38
build-scripts/gulp/gallery.js
Normal file
38
build-scripts/gulp/gallery.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Run demo develop mode
|
||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
require("./clean.js");
|
||||||
|
require("./translations.js");
|
||||||
|
require("./gen-icons.js");
|
||||||
|
require("./gather-static.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./service-worker.js");
|
||||||
|
require("./entry-html.js");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-gallery",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-gallery",
|
||||||
|
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
|
||||||
|
"copy-static-gallery",
|
||||||
|
"gen-index-gallery-dev",
|
||||||
|
"webpack-dev-server-gallery"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-gallery",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-gallery",
|
||||||
|
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||||
|
"copy-static-gallery",
|
||||||
|
"webpack-prod-gallery",
|
||||||
|
"gen-index-gallery-prod"
|
||||||
|
)
|
||||||
|
);
|
@@ -4,8 +4,6 @@ const gulp = require("gulp");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const cpx = require("cpx");
|
const cpx = require("cpx");
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const zopfli = require("gulp-zopfli-green");
|
|
||||||
const merge = require("merge-stream");
|
|
||||||
const paths = require("../paths");
|
const paths = require("../paths");
|
||||||
|
|
||||||
const npmPath = (...parts) =>
|
const npmPath = (...parts) =>
|
||||||
@@ -67,20 +65,6 @@ function copyMapPanel(staticDir) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compressStatic(staticDir) {
|
|
||||||
const staticPath = genStaticPath(staticDir);
|
|
||||||
const polyfills = gulp
|
|
||||||
.src(staticPath("polyfills/*.js"))
|
|
||||||
.pipe(zopfli())
|
|
||||||
.pipe(gulp.dest(staticPath("polyfills")));
|
|
||||||
const translations = gulp
|
|
||||||
.src(staticPath("translations/*.json"))
|
|
||||||
.pipe(zopfli())
|
|
||||||
.pipe(gulp.dest(staticPath("translations")));
|
|
||||||
|
|
||||||
return merge(polyfills, translations);
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task("copy-static", (done) => {
|
gulp.task("copy-static", (done) => {
|
||||||
const staticDir = paths.static;
|
const staticDir = paths.static;
|
||||||
const staticPath = genStaticPath(paths.static);
|
const staticPath = genStaticPath(paths.static);
|
||||||
@@ -100,11 +84,12 @@ gulp.task("copy-static", (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("compress-static", () => compressStatic(paths.static));
|
|
||||||
|
|
||||||
gulp.task("copy-static-demo", (done) => {
|
gulp.task("copy-static-demo", (done) => {
|
||||||
// Copy app static files
|
// Copy app static files
|
||||||
fs.copySync(polyPath("public"), paths.demo_root);
|
fs.copySync(
|
||||||
|
polyPath("public/static"),
|
||||||
|
path.resolve(paths.demo_root, "static")
|
||||||
|
);
|
||||||
// Copy demo static files
|
// Copy demo static files
|
||||||
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
|
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
|
||||||
|
|
||||||
@@ -126,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
|
|||||||
copyTranslations(paths.cast_static);
|
copyTranslations(paths.cast_static);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task("copy-static-gallery", (done) => {
|
||||||
|
// Copy app static files
|
||||||
|
fs.copySync(polyPath("public/static"), paths.gallery_static);
|
||||||
|
// Copy gallery static files
|
||||||
|
fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root);
|
||||||
|
|
||||||
|
copyMapPanel(paths.gallery_static);
|
||||||
|
copyFonts(paths.gallery_static);
|
||||||
|
copyTranslations(paths.gallery_static);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
@@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
|
|||||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the full MDI iconset
|
|
||||||
function genMDIIcons() {
|
|
||||||
const meta = JSON.parse(
|
|
||||||
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
|
||||||
);
|
|
||||||
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
||||||
fs.mkdirSync(OUTPUT_DIR);
|
|
||||||
}
|
|
||||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to map recursively over files in a folder and it's subfolders
|
// Helper function to map recursively over files in a folder and it's subfolders
|
||||||
function mapFiles(startPath, filter, mapFunc) {
|
function mapFiles(startPath, filter, mapFunc) {
|
||||||
const files = fs.readdirSync(startPath);
|
const files = fs.readdirSync(startPath);
|
||||||
@@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
|
|||||||
return icons;
|
return icons;
|
||||||
}
|
}
|
||||||
|
|
||||||
function genHassIcons() {
|
gulp.task("gen-icons-mdi", (done) => {
|
||||||
|
const meta = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
||||||
|
);
|
||||||
|
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
||||||
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||||
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("gen-icons-app", (done) => {
|
||||||
const iconNames = findIcons("./src", "hass");
|
const iconNames = findIcons("./src", "hass");
|
||||||
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
||||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||||
fs.mkdirSync(OUTPUT_DIR);
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task("gen-icons-mdi", (done) => {
|
|
||||||
genMDIIcons();
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
gulp.task("gen-icons-hass", (done) => {
|
|
||||||
genHassIcons();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
|
|
||||||
|
|
||||||
gulp.task("gen-icons-demo", (done) => {
|
gulp.task("gen-icons-demo", (done) => {
|
||||||
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
||||||
@@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
gulp.task("gen-icons-hassio", (done) => {
|
||||||
findIcons,
|
const iconNames = findIcons(
|
||||||
generateIconset,
|
path.resolve(paths.hassio_dir, "./src"),
|
||||||
genMDIIcons,
|
"hassio"
|
||||||
};
|
);
|
||||||
|
// Find hassio icons inside HA main repo.
|
||||||
|
for (const item of findIcons(
|
||||||
|
path.resolve(paths.polymer_dir, "./src"),
|
||||||
|
"hassio"
|
||||||
|
)) {
|
||||||
|
iconNames.add(item);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(paths.hassio_dir, "hassio-icons.html"),
|
||||||
|
generateIconset("hassio", iconNames)
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
34
build-scripts/gulp/hassio.js
Normal file
34
build-scripts/gulp/hassio.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const gulp = require("gulp");
|
||||||
|
|
||||||
|
const envVars = require("../env");
|
||||||
|
|
||||||
|
require("./clean.js");
|
||||||
|
require("./gen-icons.js");
|
||||||
|
require("./webpack.js");
|
||||||
|
require("./compress.js");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"develop-hassio",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
},
|
||||||
|
"clean-hassio",
|
||||||
|
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||||
|
"webpack-watch-hassio"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"build-hassio",
|
||||||
|
gulp.series(
|
||||||
|
async function setEnv() {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
},
|
||||||
|
"clean-hassio",
|
||||||
|
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||||
|
"webpack-prod-hassio",
|
||||||
|
...// Don't compress running tests
|
||||||
|
(envVars.isTravis ? [] : ["compress-hassio"])
|
||||||
|
)
|
||||||
|
);
|
@@ -45,11 +45,10 @@ function recursiveFlatten(prefix, data) {
|
|||||||
let output = {};
|
let output = {};
|
||||||
Object.keys(data).forEach(function(key) {
|
Object.keys(data).forEach(function(key) {
|
||||||
if (typeof data[key] === "object") {
|
if (typeof data[key] === "object") {
|
||||||
output = Object.assign(
|
output = {
|
||||||
{},
|
...output,
|
||||||
output,
|
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||||
recursiveFlatten(prefix + key + ".", data[key])
|
};
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
output[prefix + key] = data[key];
|
output[prefix + key] = data[key];
|
||||||
}
|
}
|
||||||
@@ -99,18 +98,16 @@ function recursiveEmpty(data) {
|
|||||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||||
*/
|
*/
|
||||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||||
function lokalise_transform(data, original) {
|
function lokaliseTransform(data, original, file) {
|
||||||
const output = {};
|
const output = {};
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
if (value instanceof Object) {
|
if (value instanceof Object) {
|
||||||
output[key] = lokalise_transform(value, original);
|
output[key] = lokaliseTransform(value, original, file);
|
||||||
} else {
|
} else {
|
||||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
output[key] = value.replace(re_key_reference, (match, key) => {
|
||||||
const replace = key.split("::").reduce((tr, k) => tr[k], original);
|
const replace = key.split("::").reduce((tr, k) => tr[k], original);
|
||||||
if (typeof replace !== "string") {
|
if (typeof replace !== "string") {
|
||||||
throw Error(
|
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
||||||
`Invalid key placeholder ${key} in src/translations/en.json`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return replace;
|
return replace;
|
||||||
});
|
});
|
||||||
@@ -183,7 +180,7 @@ gulp.task(
|
|||||||
.src("src/translations/en.json")
|
.src("src/translations/en.json")
|
||||||
.pipe(
|
.pipe(
|
||||||
transform(function(data, file) {
|
transform(function(data, file) {
|
||||||
return lokalise_transform(data, data);
|
return lokaliseTransform(data, data, file);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(rename("translationMaster.json"))
|
.pipe(rename("translationMaster.json"))
|
||||||
@@ -198,6 +195,11 @@ gulp.task(
|
|||||||
gulp.series("build-master-translation", function() {
|
gulp.series("build-master-translation", function() {
|
||||||
return gulp
|
return gulp
|
||||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||||
|
.pipe(
|
||||||
|
transform(function(data, file) {
|
||||||
|
return lokaliseTransform(data, data, file);
|
||||||
|
})
|
||||||
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
foreach(function(stream, file) {
|
foreach(function(stream, file) {
|
||||||
// For each language generate a merged json file. It begins with the master
|
// For each language generate a merged json file. It begins with the master
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
// Tasks to run webpack.
|
// Tasks to run webpack.
|
||||||
const gulp = require("gulp");
|
const gulp = require("gulp");
|
||||||
const path = require("path");
|
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const WebpackDevServer = require("webpack-dev-server");
|
const WebpackDevServer = require("webpack-dev-server");
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
@@ -9,8 +8,33 @@ const {
|
|||||||
createAppConfig,
|
createAppConfig,
|
||||||
createDemoConfig,
|
createDemoConfig,
|
||||||
createCastConfig,
|
createCastConfig,
|
||||||
|
createHassioConfig,
|
||||||
|
createGalleryConfig,
|
||||||
} = require("../webpack");
|
} = require("../webpack");
|
||||||
|
|
||||||
|
const bothBuilds = (createConfigFunc, params) => [
|
||||||
|
createConfigFunc({ ...params, latestBuild: true }),
|
||||||
|
createConfigFunc({ ...params, latestBuild: false }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const runDevServer = ({
|
||||||
|
compiler,
|
||||||
|
contentBase,
|
||||||
|
port,
|
||||||
|
listenHost = "localhost",
|
||||||
|
}) =>
|
||||||
|
new WebpackDevServer(compiler, {
|
||||||
|
open: true,
|
||||||
|
watchContentBase: true,
|
||||||
|
contentBase,
|
||||||
|
}).listen(port, listenHost, function(err) {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// Server listening
|
||||||
|
log("[webpack-dev-server]", `http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
const handler = (done) => (err, stats) => {
|
const handler = (done) => (err, stats) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err.stack || err);
|
console.log(err.stack || err);
|
||||||
@@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
gulp.task("webpack-watch-app", () => {
|
gulp.task("webpack-watch-app", () => {
|
||||||
const compiler = webpack([
|
|
||||||
createAppConfig({
|
|
||||||
isProdBuild: false,
|
|
||||||
latestBuild: true,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
createAppConfig({
|
|
||||||
isProdBuild: false,
|
|
||||||
latestBuild: false,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
compiler.watch({}, handler());
|
|
||||||
// we are not calling done, so this command will run forever
|
// we are not calling done, so this command will run forever
|
||||||
|
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
|
||||||
|
{},
|
||||||
|
handler()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
@@ -53,47 +68,17 @@ gulp.task(
|
|||||||
() =>
|
() =>
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
webpack(
|
webpack(
|
||||||
[
|
bothBuilds(createAppConfig, { isProdBuild: true }),
|
||||||
createAppConfig({
|
|
||||||
isProdBuild: true,
|
|
||||||
latestBuild: true,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
createAppConfig({
|
|
||||||
isProdBuild: true,
|
|
||||||
latestBuild: false,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
handler(resolve)
|
handler(resolve)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task("webpack-dev-server-demo", () => {
|
gulp.task("webpack-dev-server-demo", () => {
|
||||||
const compiler = webpack([
|
runDevServer({
|
||||||
createDemoConfig({
|
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
|
||||||
isProdBuild: false,
|
contentBase: paths.demo_root,
|
||||||
latestBuild: false,
|
port: 8090,
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
createDemoConfig({
|
|
||||||
isProdBuild: false,
|
|
||||||
latestBuild: true,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
new WebpackDevServer(compiler, {
|
|
||||||
open: true,
|
|
||||||
watchContentBase: true,
|
|
||||||
contentBase: path.resolve(paths.demo_dir, "dist"),
|
|
||||||
}).listen(8090, "localhost", function(err) {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
// Server listening
|
|
||||||
log("[webpack-dev-server]", "http://localhost:8090");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,51 +87,22 @@ gulp.task(
|
|||||||
() =>
|
() =>
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
webpack(
|
webpack(
|
||||||
[
|
bothBuilds(createDemoConfig, {
|
||||||
createDemoConfig({
|
isProdBuild: true,
|
||||||
isProdBuild: true,
|
}),
|
||||||
latestBuild: false,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
createDemoConfig({
|
|
||||||
isProdBuild: true,
|
|
||||||
latestBuild: true,
|
|
||||||
isStatsBuild: false,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
handler(resolve)
|
handler(resolve)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task("webpack-dev-server-cast", () => {
|
gulp.task("webpack-dev-server-cast", () => {
|
||||||
const compiler = webpack([
|
runDevServer({
|
||||||
createCastConfig({
|
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
|
||||||
isProdBuild: false,
|
contentBase: paths.cast_root,
|
||||||
latestBuild: false,
|
port: 8080,
|
||||||
}),
|
|
||||||
createCastConfig({
|
|
||||||
isProdBuild: false,
|
|
||||||
latestBuild: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
new WebpackDevServer(compiler, {
|
|
||||||
open: true,
|
|
||||||
watchContentBase: true,
|
|
||||||
contentBase: path.resolve(paths.cast_dir, "dist"),
|
|
||||||
}).listen(
|
|
||||||
8080,
|
|
||||||
// Accessible from the network, because that's how Cast hits it.
|
// Accessible from the network, because that's how Cast hits it.
|
||||||
"0.0.0.0",
|
listenHost: "0.0.0.0",
|
||||||
function(err) {
|
});
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
// Server listening
|
|
||||||
log("[webpack-dev-server]", "http://localhost:8080");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
@@ -154,16 +110,59 @@ gulp.task(
|
|||||||
() =>
|
() =>
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
webpack(
|
webpack(
|
||||||
[
|
bothBuilds(createCastConfig, {
|
||||||
createCastConfig({
|
isProdBuild: true,
|
||||||
isProdBuild: true,
|
}),
|
||||||
latestBuild: false,
|
|
||||||
}),
|
handler(resolve)
|
||||||
createCastConfig({
|
)
|
||||||
isProdBuild: true,
|
)
|
||||||
latestBuild: true,
|
);
|
||||||
}),
|
|
||||||
],
|
gulp.task("webpack-watch-hassio", () => {
|
||||||
|
// we are not calling done, so this command will run forever
|
||||||
|
webpack(
|
||||||
|
createHassioConfig({
|
||||||
|
isProdBuild: false,
|
||||||
|
latestBuild: false,
|
||||||
|
})
|
||||||
|
).watch({}, handler());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"webpack-prod-hassio",
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
webpack(
|
||||||
|
createHassioConfig({
|
||||||
|
isProdBuild: true,
|
||||||
|
latestBuild: false,
|
||||||
|
}),
|
||||||
|
handler(resolve)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("webpack-dev-server-gallery", () => {
|
||||||
|
runDevServer({
|
||||||
|
compiler: webpack(
|
||||||
|
createGalleryConfig({ latestBuild: true, isProdBuild: false })
|
||||||
|
),
|
||||||
|
contentBase: paths.gallery_root,
|
||||||
|
port: 8100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"webpack-prod-gallery",
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
webpack(
|
||||||
|
createGalleryConfig({
|
||||||
|
isProdBuild: true,
|
||||||
|
latestBuild: true,
|
||||||
|
}),
|
||||||
|
|
||||||
handler(resolve)
|
handler(resolve)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@@ -20,4 +20,13 @@ module.exports = {
|
|||||||
cast_static: path.resolve(__dirname, "../cast/dist/static"),
|
cast_static: path.resolve(__dirname, "../cast/dist/static"),
|
||||||
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
|
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
|
||||||
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
|
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
|
||||||
|
|
||||||
|
gallery_dir: path.resolve(__dirname, "../gallery"),
|
||||||
|
gallery_root: path.resolve(__dirname, "../gallery/dist"),
|
||||||
|
gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"),
|
||||||
|
gallery_static: path.resolve(__dirname, "../gallery/dist/static"),
|
||||||
|
|
||||||
|
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||||
|
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||||
|
hassio_publicPath: "/api/hassio/app/",
|
||||||
};
|
};
|
||||||
|
@@ -3,10 +3,9 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||||
const CompressionPlugin = require("compression-webpack-plugin");
|
|
||||||
const zopfli = require("@gfx/zopfli");
|
|
||||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||||
const paths = require("./paths.js");
|
const paths = require("./paths.js");
|
||||||
|
const { babelLoaderConfig } = require("./babel.js");
|
||||||
|
|
||||||
let version = fs
|
let version = fs
|
||||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||||
@@ -16,273 +15,249 @@ if (!version) {
|
|||||||
}
|
}
|
||||||
version = version[0];
|
version = version[0];
|
||||||
|
|
||||||
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
|
const createWebpackConfig = ({
|
||||||
const genDevTool = (isProdBuild) =>
|
entry,
|
||||||
isProdBuild ? "source-map" : "inline-cheap-module-source-map";
|
outputRoot,
|
||||||
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
|
defineOverlay,
|
||||||
if (!isProdBuild || dontHash.has(chunk.name)) {
|
isProdBuild,
|
||||||
return `${chunk.name}.js`;
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
dontHash,
|
||||||
|
}) => {
|
||||||
|
if (!dontHash) {
|
||||||
|
dontHash = new Set();
|
||||||
}
|
}
|
||||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
|
||||||
};
|
|
||||||
const genChunkFilename = (isProdBuild, isStatsBuild) =>
|
|
||||||
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
|
||||||
|
|
||||||
const resolve = {
|
|
||||||
extensions: [".ts", ".js", ".json", ".tsx"],
|
|
||||||
alias: {
|
|
||||||
react: "preact-compat",
|
|
||||||
"react-dom": "preact-compat",
|
|
||||||
// Not necessary unless you consume a module using `createClass`
|
|
||||||
"create-react-class": "preact-compat/lib/create-react-class",
|
|
||||||
// Not necessary unless you consume a module requiring `react-dom-factories`
|
|
||||||
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tsLoader = (latestBuild) => ({
|
|
||||||
test: /\.ts|tsx$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: "ts-loader",
|
|
||||||
options: {
|
|
||||||
compilerOptions: latestBuild
|
|
||||||
? { noEmit: false }
|
|
||||||
: { target: "es5", noEmit: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const cssLoader = {
|
|
||||||
test: /\.css$/,
|
|
||||||
use: "raw-loader",
|
|
||||||
};
|
|
||||||
const htmlLoader = {
|
|
||||||
test: /\.(html)$/,
|
|
||||||
use: {
|
|
||||||
loader: "html-loader",
|
|
||||||
options: {
|
|
||||||
exportAsEs6Default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const plugins = [
|
|
||||||
// Ignore moment.js locales
|
|
||||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
|
||||||
// Color.js is bloated, it contains all color definitions for all material color sets.
|
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
|
||||||
/@polymer\/paper-styles\/color\.js$/,
|
|
||||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
|
||||||
),
|
|
||||||
// Ignore roboto pointing at CDN. We use local font-roboto-local.
|
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
|
||||||
/@polymer\/font-roboto\/roboto\.js$/,
|
|
||||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
|
||||||
),
|
|
||||||
// Ignore mwc icons pointing at CDN.
|
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
|
||||||
/@material\/mwc-icon\/mwc-icon-font\.js$/,
|
|
||||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const optimization = (latestBuild) => ({
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
cache: true,
|
|
||||||
parallel: true,
|
|
||||||
extractComments: true,
|
|
||||||
sourceMap: true,
|
|
||||||
terserOptions: {
|
|
||||||
safari10: true,
|
|
||||||
ecma: latestBuild ? undefined : 5,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
|
||||||
const isCI = process.env.CI === "true";
|
|
||||||
|
|
||||||
// Create an object mapping browser urls to their paths during build
|
|
||||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
|
||||||
const workBoxTranslationsTemplatedURLs = {};
|
|
||||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
|
||||||
Object.keys(englishFP).forEach((key) => {
|
|
||||||
workBoxTranslationsTemplatedURLs[
|
|
||||||
`/static/translations/${englishFP[key]}`
|
|
||||||
] = `build-translations/output/${key}.json`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const entry = {
|
|
||||||
app: "./src/entrypoints/app.ts",
|
|
||||||
authorize: "./src/entrypoints/authorize.ts",
|
|
||||||
onboarding: "./src/entrypoints/onboarding.ts",
|
|
||||||
core: "./src/entrypoints/core.ts",
|
|
||||||
compatibility: "./src/entrypoints/compatibility.ts",
|
|
||||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
|
||||||
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode: genMode(isProdBuild),
|
mode: isProdBuild ? "production" : "development",
|
||||||
devtool: genDevTool(isProdBuild),
|
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
|
||||||
entry,
|
entry,
|
||||||
module: {
|
module: {
|
||||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
rules: [
|
||||||
|
babelLoaderConfig({ latestBuild }),
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: "raw-loader",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(html)$/,
|
||||||
|
use: {
|
||||||
|
loader: "html-loader",
|
||||||
|
options: {
|
||||||
|
exportAsEs6Default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
cache: true,
|
||||||
|
parallel: true,
|
||||||
|
extractComments: true,
|
||||||
|
sourceMap: true,
|
||||||
|
terserOptions: {
|
||||||
|
safari10: true,
|
||||||
|
ecma: latestBuild ? undefined : 5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
optimization: optimization(latestBuild),
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new ManifestPlugin(),
|
new ManifestPlugin(),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
__DEV__: JSON.stringify(!isProdBuild),
|
__DEV__: !isProdBuild,
|
||||||
__DEMO__: false,
|
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||||
__VERSION__: JSON.stringify(version),
|
__VERSION__: JSON.stringify(version),
|
||||||
|
__DEMO__: false,
|
||||||
__STATIC_PATH__: "/static/",
|
__STATIC_PATH__: "/static/",
|
||||||
"process.env.NODE_ENV": JSON.stringify(
|
"process.env.NODE_ENV": JSON.stringify(
|
||||||
isProdBuild ? "production" : "development"
|
isProdBuild ? "production" : "development"
|
||||||
),
|
),
|
||||||
|
...defineOverlay,
|
||||||
}),
|
}),
|
||||||
...plugins,
|
// Ignore moment.js locales
|
||||||
isProdBuild &&
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
!isCI &&
|
// Color.js is bloated, it contains all color definitions for all material color sets.
|
||||||
!isStatsBuild &&
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
new CompressionPlugin({
|
/@polymer\/paper-styles\/color\.js$/,
|
||||||
cache: true,
|
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
),
|
||||||
algorithm(input, compressionOptions, callback) {
|
// Ignore roboto pointing at CDN. We use local font-roboto-local.
|
||||||
return zopfli.gzip(input, compressionOptions, callback);
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
},
|
/@polymer\/font-roboto\/roboto\.js$/,
|
||||||
}),
|
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||||
latestBuild &&
|
),
|
||||||
new WorkboxPlugin.InjectManifest({
|
// Ignore mwc icons pointing at CDN.
|
||||||
swSrc: "./src/entrypoints/service-worker-hass.js",
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
swDest: "service_worker.js",
|
/@material\/mwc-icon\/mwc-icon-font\.js$/,
|
||||||
importWorkboxFrom: "local",
|
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||||
include: [/\.js$/],
|
),
|
||||||
templatedURLs: {
|
|
||||||
...workBoxTranslationsTemplatedURLs,
|
|
||||||
"/static/icons/favicon-192x192.png":
|
|
||||||
"public/icons/favicon-192x192.png",
|
|
||||||
"/static/fonts/roboto/Roboto-Light.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Medium.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Regular.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
|
||||||
"/static/fonts/roboto/Roboto-Bold.woff2":
|
|
||||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".js", ".json"],
|
||||||
|
alias: {
|
||||||
|
react: "preact-compat",
|
||||||
|
"react-dom": "preact-compat",
|
||||||
|
// Not necessary unless you consume a module using `createClass`
|
||||||
|
"create-react-class": "preact-compat/lib/create-react-class",
|
||||||
|
// Not necessary unless you consume a module requiring `react-dom-factories`
|
||||||
|
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
||||||
|
},
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: genFilename(isProdBuild),
|
filename: ({ chunk }) => {
|
||||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
if (!isProdBuild || dontHash.has(chunk.name)) {
|
||||||
path: latestBuild ? paths.output : paths.output_es5,
|
return `${chunk.name}.js`;
|
||||||
|
}
|
||||||
|
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||||
|
},
|
||||||
|
chunkFilename:
|
||||||
|
isProdBuild && !isStatsBuild
|
||||||
|
? "chunk.[chunkhash].js"
|
||||||
|
: "[name].chunk.js",
|
||||||
|
path: path.resolve(
|
||||||
|
outputRoot,
|
||||||
|
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||||
|
),
|
||||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||||
// For workerize loader
|
// For workerize loader
|
||||||
globalObject: "self",
|
globalObject: "self",
|
||||||
},
|
},
|
||||||
resolve,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||||
|
const config = createWebpackConfig({
|
||||||
|
entry: {
|
||||||
|
app: "./src/entrypoints/app.ts",
|
||||||
|
authorize: "./src/entrypoints/authorize.ts",
|
||||||
|
onboarding: "./src/entrypoints/onboarding.ts",
|
||||||
|
core: "./src/entrypoints/core.ts",
|
||||||
|
compatibility: "./src/entrypoints/compatibility.ts",
|
||||||
|
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||||
|
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
||||||
|
},
|
||||||
|
outputRoot: paths.root,
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (latestBuild) {
|
||||||
|
// Create an object mapping browser urls to their paths during build
|
||||||
|
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||||
|
const workBoxTranslationsTemplatedURLs = {};
|
||||||
|
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||||
|
Object.keys(englishFP).forEach((key) => {
|
||||||
|
workBoxTranslationsTemplatedURLs[
|
||||||
|
`/static/translations/${englishFP[key]}`
|
||||||
|
] = `build-translations/output/${key}.json`;
|
||||||
|
});
|
||||||
|
|
||||||
|
config.plugins.push(
|
||||||
|
new WorkboxPlugin.InjectManifest({
|
||||||
|
swSrc: "./src/entrypoints/service-worker-hass.js",
|
||||||
|
swDest: "service_worker.js",
|
||||||
|
importWorkboxFrom: "local",
|
||||||
|
include: [/\.js$/],
|
||||||
|
templatedURLs: {
|
||||||
|
...workBoxTranslationsTemplatedURLs,
|
||||||
|
"/static/icons/favicon-192x192.png":
|
||||||
|
"public/icons/favicon-192x192.png",
|
||||||
|
"/static/fonts/roboto/Roboto-Light.woff2":
|
||||||
|
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
||||||
|
"/static/fonts/roboto/Roboto-Medium.woff2":
|
||||||
|
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
||||||
|
"/static/fonts/roboto/Roboto-Regular.woff2":
|
||||||
|
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
||||||
|
"/static/fonts/roboto/Roboto-Bold.woff2":
|
||||||
|
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||||
return {
|
return createWebpackConfig({
|
||||||
mode: genMode(isProdBuild),
|
|
||||||
devtool: genDevTool(isProdBuild),
|
|
||||||
entry: {
|
entry: {
|
||||||
main: "./demo/src/entrypoint.ts",
|
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
||||||
compatibility: "./src/entrypoints/compatibility.ts",
|
compatibility: path.resolve(
|
||||||
},
|
paths.polymer_dir,
|
||||||
module: {
|
"src/entrypoints/compatibility.ts"
|
||||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
|
||||||
},
|
|
||||||
optimization: optimization(latestBuild),
|
|
||||||
plugins: [
|
|
||||||
new ManifestPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
__DEV__: !isProdBuild,
|
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
|
||||||
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
|
||||||
__DEMO__: true,
|
|
||||||
__STATIC_PATH__: "/static/",
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(
|
|
||||||
isProdBuild ? "production" : "development"
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
...plugins,
|
|
||||||
].filter(Boolean),
|
|
||||||
resolve,
|
|
||||||
output: {
|
|
||||||
filename: genFilename(isProdBuild),
|
|
||||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
|
||||||
path: path.resolve(
|
|
||||||
paths.demo_root,
|
|
||||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
|
||||||
),
|
),
|
||||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
|
||||||
// For workerize loader
|
|
||||||
globalObject: "self",
|
|
||||||
},
|
},
|
||||||
};
|
outputRoot: paths.demo_root,
|
||||||
|
defineOverlay: {
|
||||||
|
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
||||||
|
__DEMO__: true,
|
||||||
|
},
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
isStatsBuild,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createCastConfig = ({ isProdBuild, latestBuild }) => {
|
const createCastConfig = ({ isProdBuild, latestBuild }) => {
|
||||||
const isStatsBuild = false;
|
|
||||||
const entry = {
|
const entry = {
|
||||||
launcher: "./cast/src/launcher/entrypoint.ts",
|
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (latestBuild) {
|
if (latestBuild) {
|
||||||
entry.receiver = "./cast/src/receiver/entrypoint.ts";
|
entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return createWebpackConfig({
|
||||||
mode: genMode(isProdBuild),
|
|
||||||
devtool: genDevTool(isProdBuild),
|
|
||||||
entry,
|
entry,
|
||||||
module: {
|
outputRoot: paths.cast_root,
|
||||||
rules: [tsLoader(latestBuild), cssLoader, htmlLoader],
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHassioConfig = ({ isProdBuild, latestBuild }) => {
|
||||||
|
if (latestBuild) {
|
||||||
|
throw new Error("Hass.io does not support latest build!");
|
||||||
|
}
|
||||||
|
const config = createWebpackConfig({
|
||||||
|
entry: {
|
||||||
|
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
||||||
},
|
},
|
||||||
optimization: optimization(latestBuild),
|
outputRoot: "",
|
||||||
plugins: [
|
isProdBuild,
|
||||||
new ManifestPlugin(),
|
latestBuild,
|
||||||
new webpack.DefinePlugin({
|
dontHash: new Set(["entrypoint"]),
|
||||||
__DEV__: !isProdBuild,
|
});
|
||||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
|
||||||
__VERSION__: JSON.stringify(version),
|
config.output.path = paths.hassio_root;
|
||||||
__DEMO__: false,
|
config.output.publicPath = paths.hassio_publicPath;
|
||||||
__STATIC_PATH__: "/static/",
|
|
||||||
"process.env.NODE_ENV": JSON.stringify(
|
return config;
|
||||||
isProdBuild ? "production" : "development"
|
};
|
||||||
),
|
|
||||||
}),
|
const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
|
||||||
...plugins,
|
if (!latestBuild) {
|
||||||
].filter(Boolean),
|
throw new Error("Gallery only supports latest build!");
|
||||||
resolve,
|
}
|
||||||
output: {
|
const config = createWebpackConfig({
|
||||||
filename: genFilename(isProdBuild),
|
entry: {
|
||||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
||||||
path: path.resolve(
|
|
||||||
paths.cast_root,
|
|
||||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
|
||||||
),
|
|
||||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
|
||||||
// For workerize loader
|
|
||||||
globalObject: "self",
|
|
||||||
},
|
},
|
||||||
};
|
outputRoot: paths.gallery_root,
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
resolve,
|
|
||||||
plugins,
|
|
||||||
optimization,
|
|
||||||
createAppConfig,
|
createAppConfig,
|
||||||
createDemoConfig,
|
createDemoConfig,
|
||||||
createCastConfig,
|
createCastConfig,
|
||||||
|
createHassioConfig,
|
||||||
|
createGalleryConfig,
|
||||||
};
|
};
|
||||||
|
20
cast/public/_headers
Normal file
20
cast/public/_headers
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
|
||||||
|
Content-Security-Policy: form-action https:
|
||||||
|
Feature-Policy: vibrate 'none'; geolocation 'none'; midi 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; payment 'none'
|
||||||
|
Referrer-Policy: no-referrer-when-downgrade
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
|
||||||
|
/images/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||||
|
|
||||||
|
/manifest.json
|
||||||
|
Cache-Control: public, max-age: 3600, s-maxage=3600
|
||||||
|
|
||||||
|
/frontend_es5/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||||
|
|
||||||
|
/frontend_latest/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
@@ -39,7 +39,7 @@ class HcCast extends LitElement {
|
|||||||
@property() private askWrite = false;
|
@property() private askWrite = false;
|
||||||
@property() private lovelaceConfig?: LovelaceConfig | null;
|
@property() private lovelaceConfig?: LovelaceConfig | null;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (this.lovelaceConfig === undefined) {
|
if (this.lovelaceConfig === undefined) {
|
||||||
return html`
|
return html`
|
||||||
<loading-screen></loading-screen>>
|
<loading-screen></loading-screen>>
|
||||||
|
@@ -70,7 +70,7 @@ export class HcConnect extends LitElement {
|
|||||||
@property() private castManager?: CastManager | null;
|
@property() private castManager?: CastManager | null;
|
||||||
private openDemo = false;
|
private openDemo = false;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (this.cannotConnect) {
|
if (this.cannotConnect) {
|
||||||
const tokens = loadTokens();
|
const tokens = loadTokens();
|
||||||
return html`
|
return html`
|
||||||
|
@@ -22,7 +22,7 @@ class HcLayout extends LitElement {
|
|||||||
@property() public connection?: Connection;
|
@property() public connection?: Connection;
|
||||||
@property() public user?: HassUser;
|
@property() public user?: HassUser;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
@@ -50,13 +50,12 @@ class HcLayout extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="./faq.html">Frequently Asked Questions</a> – Found a bug? Let
|
<a href="./faq.html">Frequently Asked Questions</a> – Found a bug?
|
||||||
@balloob know
|
<a
|
||||||
<!-- <a
|
|
||||||
href="https://github.com/home-assistant/home-assistant-polymer/issues"
|
href="https://github.com/home-assistant/home-assistant-polymer/issues"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>Let us know!</a
|
>Let us know!</a
|
||||||
> -->
|
>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ class HcDemo extends HassElement {
|
|||||||
@property() public lovelacePath!: string;
|
@property() public lovelacePath!: string;
|
||||||
@property() private _lovelaceConfig?: LovelaceConfig;
|
@property() private _lovelaceConfig?: LovelaceConfig;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (!this._lovelaceConfig) {
|
if (!this._lovelaceConfig) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ class HcLaunchScreen extends LitElement {
|
|||||||
@property() public hass?: HomeAssistant;
|
@property() public hass?: HomeAssistant;
|
||||||
@property() public error?: string;
|
@property() public error?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img
|
<img
|
||||||
|
@@ -8,7 +8,8 @@ import {
|
|||||||
property,
|
property,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||||
import "../../../../src/panels/lovelace/hui-view";
|
import "../../../../src/panels/lovelace/views/hui-view";
|
||||||
|
import "../../../../src/panels/lovelace/views/hui-panel-view";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||||
import "./hc-launch-screen";
|
import "./hc-launch-screen";
|
||||||
@@ -21,7 +22,7 @@ class HcLovelace extends LitElement {
|
|||||||
|
|
||||||
@property() public viewPath?: string | number;
|
@property() public viewPath?: string | number;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
const index = this._viewIndex;
|
const index = this._viewIndex;
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
return html`
|
return html`
|
||||||
@@ -38,16 +39,24 @@ class HcLovelace extends LitElement {
|
|||||||
mode: "storage",
|
mode: "storage",
|
||||||
language: "en",
|
language: "en",
|
||||||
saveConfig: async () => undefined,
|
saveConfig: async () => undefined,
|
||||||
|
deleteConfig: async () => undefined,
|
||||||
setEditMode: () => undefined,
|
setEditMode: () => undefined,
|
||||||
};
|
};
|
||||||
return html`
|
return this.lovelaceConfig.views[index].panel
|
||||||
<hui-view
|
? html`
|
||||||
.hass=${this.hass}
|
<hui-panel-view
|
||||||
.lovelace=${lovelace}
|
.hass=${this.hass}
|
||||||
.index=${index}
|
.config=${this.lovelaceConfig.views[index]}
|
||||||
columns="2"
|
></hui-panel-view>
|
||||||
></hui-view>
|
`
|
||||||
`;
|
: html`
|
||||||
|
<hui-view
|
||||||
|
.hass=${this.hass}
|
||||||
|
.lovelace=${lovelace}
|
||||||
|
.index=${index}
|
||||||
|
columns="2"
|
||||||
|
></hui-view>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps) {
|
protected updated(changedProps) {
|
||||||
@@ -62,7 +71,9 @@ class HcLovelace extends LitElement {
|
|||||||
this.lovelaceConfig.background;
|
this.lovelaceConfig.background;
|
||||||
|
|
||||||
if (configBackground) {
|
if (configBackground) {
|
||||||
this.shadowRoot!.querySelector("hui-view")!.style.setProperty(
|
(this.shadowRoot!.querySelector(
|
||||||
|
"hui-view, hui-panel-view"
|
||||||
|
) as HTMLElement)!.style.setProperty(
|
||||||
"--lovelace-background",
|
"--lovelace-background",
|
||||||
configBackground
|
configBackground
|
||||||
);
|
);
|
||||||
@@ -94,7 +105,7 @@ class HcLovelace extends LitElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--primary-background-color);
|
background: var(--primary-background-color);
|
||||||
}
|
}
|
||||||
hui-view {
|
:host > * {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -50,7 +50,7 @@ export class HcMain extends HassElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (this._showDemo) {
|
if (this._showDemo) {
|
||||||
return html`
|
return html`
|
||||||
<hc-demo .lovelacePath=${this._lovelacePath}></hc-demo>
|
<hc-demo .lovelacePath=${this._lovelacePath}></hc-demo>
|
||||||
@@ -137,14 +137,14 @@ export class HcMain extends HassElement {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err;
|
this._error = this._getErrorMessage(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let connection;
|
let connection;
|
||||||
try {
|
try {
|
||||||
connection = await createConnection({ auth });
|
connection = await createConnection({ auth });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err;
|
this._error = this._getErrorMessage(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.hass) {
|
if (this.hass) {
|
||||||
@@ -175,9 +175,9 @@ export class HcMain extends HassElement {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Generate a Lovelace config.
|
// Generate a Lovelace config.
|
||||||
this._unsubLovelace = () => undefined;
|
this._unsubLovelace = () => undefined;
|
||||||
const {
|
const { generateLovelaceConfigFromHass } = await import(
|
||||||
generateLovelaceConfigFromHass,
|
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||||
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
|
);
|
||||||
this._handleNewLovelaceConfig(
|
this._handleNewLovelaceConfig(
|
||||||
await generateLovelaceConfigFromHass(this.hass!)
|
await generateLovelaceConfigFromHass(this.hass!)
|
||||||
);
|
);
|
||||||
@@ -213,6 +213,23 @@ export class HcMain extends HassElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getErrorMessage(error: number): string {
|
||||||
|
switch (error) {
|
||||||
|
case 1:
|
||||||
|
return "Unable to connect to the Home Assistant websocket API.";
|
||||||
|
case 2:
|
||||||
|
return "The supplied authentication is invalid.";
|
||||||
|
case 3:
|
||||||
|
return "The connection to Home Assistant was lost.";
|
||||||
|
case 4:
|
||||||
|
return "Missing hassUrl. This is required.";
|
||||||
|
case 5:
|
||||||
|
return "Home Assistant needs to be served over https:// to use with Home Assistant Cast.";
|
||||||
|
default:
|
||||||
|
return "Unknown Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _breakFree() {
|
private _breakFree() {
|
||||||
const controls = document.body.querySelector("touch-controls");
|
const controls = document.body.querySelector("touch-controls");
|
||||||
if (controls) {
|
if (controls) {
|
||||||
|
11
cast/webpack.config.js
Normal file
11
cast/webpack.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { createCastConfig } = require("../build-scripts/webpack.js");
|
||||||
|
const { isProdBuild } = require("../build-scripts/env.js");
|
||||||
|
|
||||||
|
// File just used for stats builds
|
||||||
|
|
||||||
|
const latestBuild = true;
|
||||||
|
|
||||||
|
module.exports = createCastConfig({
|
||||||
|
isProdBuild,
|
||||||
|
latestBuild,
|
||||||
|
});
|
18
demo/public/_headers
Normal file
18
demo/public/_headers
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
|
||||||
|
Content-Security-Policy: form-action https:
|
||||||
|
Referrer-Policy: no-referrer-when-downgrade
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
|
||||||
|
/api/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||||
|
|
||||||
|
/assets/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||||
|
|
||||||
|
/frontend_es5/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||||
|
|
||||||
|
/frontend_latest/*
|
||||||
|
Cache-Control: public, max-age: 604800, s-maxage=604800
|
@@ -7,22 +7,26 @@
|
|||||||
{
|
{
|
||||||
"src": "/static/icons/favicon-192x192.png",
|
"src": "/static/icons/favicon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/favicon-384x384.png",
|
"src": "/static/icons/favicon-384x384.png",
|
||||||
"sizes": "384x384",
|
"sizes": "384x384",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/favicon-512x512.png",
|
"src": "/static/icons/favicon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/favicon-1024x1024.png",
|
"src": "/static/icons/favicon-1024x1024.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "maskable any"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
|
@@ -115,10 +115,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "Abode Updates",
|
friendly_name: "Abode Updates",
|
||||||
icon: "hademo:security",
|
icon: "hademo:security",
|
||||||
templates: {
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_boolean.tv": {
|
"input_boolean.tv": {
|
||||||
@@ -127,10 +123,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "TV",
|
friendly_name: "TV",
|
||||||
icon: "hademo:television",
|
icon: "hademo:television",
|
||||||
templates: {
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_boolean.homeautomation": {
|
"input_boolean.homeautomation": {
|
||||||
@@ -139,10 +131,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "Home Automation",
|
friendly_name: "Home Automation",
|
||||||
icon: "hass:home-automation",
|
icon: "hass:home-automation",
|
||||||
templates: {
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_boolean.tvtime": {
|
"input_boolean.tvtime": {
|
||||||
@@ -151,12 +139,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "TV Time",
|
friendly_name: "TV Time",
|
||||||
icon: "hademo:television-guide",
|
icon: "hademo:television-guide",
|
||||||
templates: {
|
|
||||||
icon:
|
|
||||||
"if (state === 'on') return 'hademo:television-classic'; return 'hademo:television-classic-off';\n",
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"input_select.livingroomharmony": {
|
"input_select.livingroomharmony": {
|
||||||
@@ -235,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
icon: "hademo:currency-usd",
|
icon: "hademo:currency-usd",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"sensor.study_temp": {
|
||||||
|
entity_id: "sensor.study_temp",
|
||||||
|
state: "20.9",
|
||||||
|
attributes: {
|
||||||
|
unit_of_measurement: "°C",
|
||||||
|
device_class: "temperature",
|
||||||
|
friendly_name: localize(
|
||||||
|
"ui.panel.page-demo.config.arsaboo.names.temperature_study"
|
||||||
|
),
|
||||||
|
icon: "hademo:thermometer",
|
||||||
|
},
|
||||||
|
},
|
||||||
"cover.garagedoor": {
|
"cover.garagedoor": {
|
||||||
entity_id: "cover.garagedoor",
|
entity_id: "cover.garagedoor",
|
||||||
state: "closed",
|
state: "closed",
|
||||||
@@ -560,12 +554,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
state: "off",
|
state: "off",
|
||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "Driveway Light",
|
friendly_name: "Driveway Light",
|
||||||
templates: {
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
icon:
|
|
||||||
"if (state === 'on') return 'hademo:lightbulb-on'; return 'hademo:lightbulb';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"switch.wemoporch": {
|
"switch.wemoporch": {
|
||||||
@@ -573,12 +561,6 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
|||||||
state: "off",
|
state: "off",
|
||||||
attributes: {
|
attributes: {
|
||||||
friendly_name: "Porch Lights",
|
friendly_name: "Porch Lights",
|
||||||
templates: {
|
|
||||||
icon_color:
|
|
||||||
"if (state === 'on') return 'rgb(251, 210, 41)'; return 'rgb(54, 95, 140)';\n",
|
|
||||||
icon:
|
|
||||||
"if (state === 'on') return 'hademo:lightbulb-on'; return 'hademo:lightbulb';\n",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
|
|||||||
"script.tv_off",
|
"script.tv_off",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "sensor",
|
||||||
|
entity: "sensor.study_temp",
|
||||||
|
graph: "line",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "entities",
|
type: "entities",
|
||||||
title: "Doorbell",
|
title: "Doorbell",
|
||||||
|
@@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
|
|||||||
"paper-listbox-background-color": "#2E333A",
|
"paper-listbox-background-color": "#2E333A",
|
||||||
"table-row-background-color": "#353840",
|
"table-row-background-color": "#353840",
|
||||||
"paper-grey-50": "var(--primary-text-color)",
|
"paper-grey-50": "var(--primary-text-color)",
|
||||||
"paper-toggle-button-checked-button-color": "var(--accent-color)",
|
"switch-checked-color": "var(--accent-color)",
|
||||||
"paper-dialog-background-color": "#434954",
|
"paper-dialog-background-color": "#434954",
|
||||||
"secondary-text-color": "#5294E2",
|
"secondary-text-color": "#5294E2",
|
||||||
"google-red-500": "#E45E65",
|
"google-red-500": "#E45E65",
|
||||||
"divider-color": "rgba(0, 0, 0, .12)",
|
"divider-color": "rgba(0, 0, 0, .12)",
|
||||||
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
|
|
||||||
"google-green-500": "#39E949",
|
"google-green-500": "#39E949",
|
||||||
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
|
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||||
"label-badge-border-color": "green",
|
"label-badge-border-color": "green",
|
||||||
"paper-listbox-color": "var(--primary-color)",
|
"paper-listbox-color": "var(--primary-color)",
|
||||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||||
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
|
|
||||||
"paper-card-background-color": "#434954",
|
"paper-card-background-color": "#434954",
|
||||||
"label-badge-text-color": "var(--primary-text-color)",
|
"label-badge-text-color": "var(--primary-text-color)",
|
||||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||||
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
|
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||||
"dark-primary-color": "var(--accent-color)",
|
"dark-primary-color": "var(--accent-color)",
|
||||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||||
"paper-slider-pin-color": "var(--accent-color)",
|
"paper-slider-pin-color": "var(--accent-color)",
|
||||||
"paper-item-icon-active-color": "#F9C536",
|
"paper-item-icon-active-color": "#F9C536",
|
||||||
"accent-color": "#E45E65",
|
"accent-color": "#E45E65",
|
||||||
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
|
|
||||||
"table-row-alternative-background-color": "#3E424B",
|
"table-row-alternative-background-color": "#3E424B",
|
||||||
});
|
});
|
||||||
|
@@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
|
|||||||
"paper-listbox-background-color": "#141414",
|
"paper-listbox-background-color": "#141414",
|
||||||
"table-row-background-color": "#292929",
|
"table-row-background-color": "#292929",
|
||||||
"paper-grey-50": "var(--primary-text-color)",
|
"paper-grey-50": "var(--primary-text-color)",
|
||||||
"paper-toggle-button-checked-button-color": "var(--accent-color)",
|
"switch-checked-color": "var(--accent-color)",
|
||||||
"paper-dialog-background-color": "#292929",
|
"paper-dialog-background-color": "#292929",
|
||||||
"secondary-text-color": "#b58e31",
|
"secondary-text-color": "#b58e31",
|
||||||
"google-red-500": "#b58e31",
|
"google-red-500": "#b58e31",
|
||||||
"divider-color": "rgba(0, 0, 0, .12)",
|
"divider-color": "rgba(0, 0, 0, .12)",
|
||||||
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
|
|
||||||
"google-green-500": "#2980b9",
|
"google-green-500": "#2980b9",
|
||||||
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
|
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||||
"label-badge-border-color": "green",
|
"label-badge-border-color": "green",
|
||||||
"paper-listbox-color": "#777777",
|
"paper-listbox-color": "#777777",
|
||||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||||
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
|
|
||||||
"paper-card-background-color": "#292929",
|
"paper-card-background-color": "#292929",
|
||||||
"label-badge-text-color": "var(--primary-text-color)",
|
"label-badge-text-color": "var(--primary-text-color)",
|
||||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||||
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
|
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||||
"dark-primary-color": "var(--accent-color)",
|
"dark-primary-color": "var(--accent-color)",
|
||||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||||
"paper-slider-pin-color": "var(--accent-color)",
|
"paper-slider-pin-color": "var(--accent-color)",
|
||||||
"paper-item-icon-active-color": "#b58e31",
|
"paper-item-icon-active-color": "#b58e31",
|
||||||
"accent-color": "#2980b9",
|
"accent-color": "#2980b9",
|
||||||
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
|
|
||||||
"table-row-alternative-background-color": "#292929",
|
"table-row-alternative-background-color": "#292929",
|
||||||
});
|
});
|
||||||
|
@@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
|
|||||||
"paper-slider-knob-color": "var(--primary-color)",
|
"paper-slider-knob-color": "var(--primary-color)",
|
||||||
"paper-listbox-color": "#FFFFFF",
|
"paper-listbox-color": "#FFFFFF",
|
||||||
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
|
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
|
||||||
"paper-toggle-button-checked-ink-color": "var(--dark-primary-color)",
|
"switch-unchecked-track-color": "var(--primary-text-color)",
|
||||||
"paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
|
|
||||||
"paper-card-background-color": "#4e4e4e",
|
"paper-card-background-color": "#4e4e4e",
|
||||||
"label-badge-text-color": "var(--text-primary-color)",
|
"label-badge-text-color": "var(--text-primary-color)",
|
||||||
"primary-background-color": "#303030",
|
"primary-background-color": "#303030",
|
||||||
@@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
|
|||||||
"secondary-background-color": "#2b2b2b",
|
"secondary-background-color": "#2b2b2b",
|
||||||
"paper-slider-knob-start-color": "var(--primary-color)",
|
"paper-slider-knob-start-color": "var(--primary-color)",
|
||||||
"paper-item-icon-active-color": "#d8bf50",
|
"paper-item-icon-active-color": "#d8bf50",
|
||||||
"paper-toggle-button-checked-button-color": "var(--primary-color)",
|
"switch-checked-color": "var(--primary-color)",
|
||||||
"secondary-text-color": "#389638",
|
"secondary-text-color": "#389638",
|
||||||
"disabled-text-color": "#545454",
|
"disabled-text-color": "#545454",
|
||||||
"paper-item-icon_-_color": "var(--primary-text-color)",
|
"paper-item-icon_-_color": "var(--primary-text-color)",
|
||||||
|
@@ -53,7 +53,7 @@ class CardModder extends LitElement {
|
|||||||
for (var k in this._config.style) {
|
for (var k in this._config.style) {
|
||||||
if (window.cardTools.hasTemplate(this._config.style[k]))
|
if (window.cardTools.hasTemplate(this._config.style[k]))
|
||||||
this.templated.push(k);
|
this.templated.push(k);
|
||||||
this.card.style.setProperty(k, '');
|
this.card.style.setProperty(k, "");
|
||||||
target.style.setProperty(
|
target.style.setProperty(
|
||||||
k,
|
k,
|
||||||
window.cardTools.parseTemplate(this._config.style[k])
|
window.cardTools.parseTemplate(this._config.style[k])
|
||||||
|
@@ -161,8 +161,8 @@ if (!window.cardTools) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cardTools.longpress = (element) => {
|
cardTools.longpress = (element) => {
|
||||||
customElements.whenDefined("long-press").then(() => {
|
customElements.whenDefined("action-handler").then(() => {
|
||||||
const longpress = document.body.querySelector("long-press");
|
const longpress = document.body.querySelector("action-handler");
|
||||||
longpress.bind(element);
|
longpress.bind(element);
|
||||||
});
|
});
|
||||||
return element;
|
return element;
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
import "../../../src/components/ha-icon";
|
import "../../../src/components/ha-icon";
|
||||||
import {
|
import {
|
||||||
EntityRow,
|
LovelaceRow,
|
||||||
CastConfig,
|
CastConfig,
|
||||||
} from "../../../src/panels/lovelace/entity-rows/types";
|
} from "../../../src/panels/lovelace/entity-rows/types";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
@@ -18,7 +18,7 @@ import { CastManager } from "../../../src/cast/cast_manager";
|
|||||||
import { castSendShowDemo } from "../../../src/cast/receiver_messages";
|
import { castSendShowDemo } from "../../../src/cast/receiver_messages";
|
||||||
|
|
||||||
@customElement("cast-demo-row")
|
@customElement("cast-demo-row")
|
||||||
class CastDemoRow extends LitElement implements EntityRow {
|
class CastDemoRow extends LitElement implements LovelaceRow {
|
||||||
public hass!: HomeAssistant;
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() private _castManager?: CastManager | null;
|
@property() private _castManager?: CastManager | null;
|
||||||
@@ -27,7 +27,7 @@ class CastDemoRow extends LitElement implements EntityRow {
|
|||||||
// No config possible.
|
// No config possible.
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (
|
if (
|
||||||
!this._castManager ||
|
!this._castManager ||
|
||||||
this._castManager.castState === "NO_DEVICES_AVAILABLE"
|
this._castManager.castState === "NO_DEVICES_AVAILABLE"
|
||||||
|
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { LitElement, html, CSSResult, css, property } from "lit-element";
|
||||||
LitElement,
|
|
||||||
html,
|
|
||||||
CSSResult,
|
|
||||||
css,
|
|
||||||
PropertyDeclarations,
|
|
||||||
} from "lit-element";
|
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||||
@@ -20,19 +14,11 @@ import {
|
|||||||
} from "../configs/demo-configs";
|
} from "../configs/demo-configs";
|
||||||
|
|
||||||
export class HADemoCard extends LitElement implements LovelaceCard {
|
export class HADemoCard extends LitElement implements LovelaceCard {
|
||||||
public lovelace?: Lovelace;
|
@property() public lovelace?: Lovelace;
|
||||||
public hass!: MockHomeAssistant;
|
@property() public hass!: MockHomeAssistant;
|
||||||
private _switching?: boolean;
|
@property() private _switching?: boolean;
|
||||||
private _hidden = localStorage.hide_demo_card;
|
private _hidden = localStorage.hide_demo_card;
|
||||||
|
|
||||||
static get properties(): PropertyDeclarations {
|
|
||||||
return {
|
|
||||||
lovelace: {},
|
|
||||||
hass: {},
|
|
||||||
_switching: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardSize() {
|
public getCardSize() {
|
||||||
return this._hidden ? 0 : 2;
|
return this._hidden ? 0 : 2;
|
||||||
}
|
}
|
||||||
|
@@ -12,5 +12,7 @@ import "./resources/hademo-icons";
|
|||||||
|
|
||||||
/* polyfill for paper-dropdown */
|
/* polyfill for paper-dropdown */
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
|
import(
|
||||||
|
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||||
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
@@ -65,74 +65,79 @@ const generateHistory = (state, deltas) => {
|
|||||||
const incrementalUnits = ["clients", "queries", "ads"];
|
const incrementalUnits = ["clients", "queries", "ads"];
|
||||||
|
|
||||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||||
mockHass.mockAPI(new RegExp("history/period/.+"), (
|
mockHass.mockAPI(
|
||||||
hass,
|
new RegExp("history/period/.+"),
|
||||||
// @ts-ignore
|
(
|
||||||
method,
|
hass,
|
||||||
path,
|
// @ts-ignore
|
||||||
// @ts-ignore
|
method,
|
||||||
parameters
|
path,
|
||||||
) => {
|
// @ts-ignore
|
||||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
parameters
|
||||||
const entities = params.filter_entity_id.split(",");
|
) => {
|
||||||
|
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||||
|
const entities = params.filter_entity_id.split(",");
|
||||||
|
|
||||||
const results: HassEntity[][] = [];
|
const results: HassEntity[][] = [];
|
||||||
|
|
||||||
for (const entityId of entities) {
|
for (const entityId of entities) {
|
||||||
const state = hass.states[entityId];
|
const state = hass.states[entityId];
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.attributes.unit_of_measurement) {
|
if (!state.attributes.unit_of_measurement) {
|
||||||
results.push(generateHistory(state, [state.state]));
|
results.push(generateHistory(state, [state.state]));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberState = Number(state.state);
|
const numberState = Number(state.state);
|
||||||
|
|
||||||
if (isNaN(numberState)) {
|
if (isNaN(numberState)) {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
console.log(
|
console.log(
|
||||||
"Ignoring state with unparsable state but with a unit",
|
"Ignoring state with unparsable state but with a unit",
|
||||||
entityId,
|
entityId,
|
||||||
state
|
state
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statesToGenerate = 15;
|
||||||
|
let genFunc;
|
||||||
|
|
||||||
|
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
||||||
|
let initial = Math.floor(
|
||||||
|
numberState * 0.4 + numberState * Math.random() * 0.2
|
||||||
|
);
|
||||||
|
const diff = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor((numberState - initial) / statesToGenerate)
|
||||||
|
);
|
||||||
|
genFunc = () => {
|
||||||
|
initial += diff;
|
||||||
|
return Math.min(numberState, initial);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const diff = Math.floor(
|
||||||
|
numberState * (numberState > 80 ? 0.05 : 0.5)
|
||||||
|
);
|
||||||
|
genFunc = () =>
|
||||||
|
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(
|
||||||
|
generateHistory(
|
||||||
|
{
|
||||||
|
entity_id: state.entity_id,
|
||||||
|
attributes: state.attributes,
|
||||||
|
},
|
||||||
|
Array.from({ length: statesToGenerate }, genFunc)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
const statesToGenerate = 15;
|
|
||||||
let genFunc;
|
|
||||||
|
|
||||||
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
|
||||||
let initial = Math.floor(
|
|
||||||
numberState * 0.4 + numberState * Math.random() * 0.2
|
|
||||||
);
|
|
||||||
const diff = Math.max(
|
|
||||||
1,
|
|
||||||
Math.floor((numberState - initial) / statesToGenerate)
|
|
||||||
);
|
|
||||||
genFunc = () => {
|
|
||||||
initial += diff;
|
|
||||||
return Math.min(numberState, initial);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const diff = Math.floor(numberState * (numberState > 80 ? 0.05 : 0.5));
|
|
||||||
genFunc = () =>
|
|
||||||
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(
|
|
||||||
generateHistory(
|
|
||||||
{
|
|
||||||
entity_id: state.entity_id,
|
|
||||||
attributes: state.attributes,
|
|
||||||
},
|
|
||||||
Array.from({ length: statesToGenerate }, genFunc)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return results;
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@@ -12,9 +12,10 @@ export const mockLovelace = (
|
|||||||
localizePromise: Promise<LocalizeFunc>
|
localizePromise: Promise<LocalizeFunc>
|
||||||
) => {
|
) => {
|
||||||
hass.mockWS("lovelace/config", () =>
|
hass.mockWS("lovelace/config", () =>
|
||||||
Promise.all([selectedDemoConfig, localizePromise]).then(
|
Promise.all([
|
||||||
([config, localize]) => config.lovelace(localize)
|
selectedDemoConfig,
|
||||||
)
|
localizePromise,
|
||||||
|
]).then(([config, localize]) => config.lovelace(localize))
|
||||||
);
|
);
|
||||||
|
|
||||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||||
|
@@ -97,7 +97,7 @@ export const mockTranslations = (hass: MockHomeAssistant) => {
|
|||||||
"component.nest.config.abort.authorize_url_timeout":
|
"component.nest.config.abort.authorize_url_timeout":
|
||||||
"Timeout generating authorize url.",
|
"Timeout generating authorize url.",
|
||||||
"component.nest.config.abort.no_flows":
|
"component.nest.config.abort.no_flows":
|
||||||
"You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/).",
|
"You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/nest/).",
|
||||||
"component.nest.config.error.internal_error":
|
"component.nest.config.error.internal_error":
|
||||||
"Internal error validating code",
|
"Internal error validating code",
|
||||||
"component.nest.config.error.invalid_code": "Invalid code",
|
"component.nest.config.error.invalid_code": "Invalid code",
|
||||||
@@ -199,7 +199,7 @@ export const mockTranslations = (hass: MockHomeAssistant) => {
|
|||||||
"component.point.config.abort.external_setup":
|
"component.point.config.abort.external_setup":
|
||||||
"Point successfully configured from another flow.",
|
"Point successfully configured from another flow.",
|
||||||
"component.point.config.abort.no_flows":
|
"component.point.config.abort.no_flows":
|
||||||
"You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/).",
|
"You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/).",
|
||||||
"component.point.config.create_entry.default":
|
"component.point.config.create_entry.default":
|
||||||
"Successfully authenticated with Minut for your Point device(s)",
|
"Successfully authenticated with Minut for your Point device(s)",
|
||||||
"component.point.config.error.follow_link":
|
"component.point.config.error.follow_link":
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
const { createDemoConfig } = require("../build-scripts/webpack.js");
|
const { createDemoConfig } = require("../build-scripts/webpack.js");
|
||||||
|
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
|
||||||
|
|
||||||
// This file exists because we haven't migrated the stats script yet
|
// File just used for stats builds
|
||||||
|
|
||||||
const isProdBuild = process.env.NODE_ENV === "production";
|
const latestBuild = true;
|
||||||
const isStatsBuild = process.env.STATS === "1";
|
|
||||||
const latestBuild = false;
|
|
||||||
|
|
||||||
module.exports = createDemoConfig({
|
module.exports = createDemoConfig({
|
||||||
isProdBuild,
|
isProdBuild,
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
<!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="#2157BC">
|
|
||||||
<title>HAGallery</title>
|
|
||||||
<script src='./main.js' async></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Roboto, Noto, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
@@ -4,14 +4,6 @@
|
|||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
OUTPUT_DIR=dist
|
./node_modules/.bin/gulp build-gallery
|
||||||
|
|
||||||
rm -rf $OUTPUT_DIR
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
./node_modules/.bin/gulp build-translations gen-icons
|
|
||||||
cd gallery
|
|
||||||
|
|
||||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
|
|
||||||
|
@@ -4,10 +4,6 @@
|
|||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
cd ..
|
./node_modules/.bin/gulp develop-gallery
|
||||||
./node_modules/.bin/gulp build-translations gen-icons
|
|
||||||
cd gallery
|
|
||||||
|
|
||||||
../node_modules/.bin/webpack-dev-server
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import JsYaml from "js-yaml";
|
import { safeLoad } from "js-yaml";
|
||||||
|
|
||||||
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
|
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
|
||||||
|
|
||||||
class DemoCard extends PolymerElement {
|
class DemoCard extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
|
|||||||
card.removeChild(card.lastChild);
|
card.removeChild(card.lastChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = createCardElement(JsYaml.safeLoad(config.config)[0]);
|
const el = createCardElement(safeLoad(config.config)[0]);
|
||||||
el.hass = this.hass;
|
el.hass = this.hass;
|
||||||
card.appendChild(el);
|
card.appendChild(el);
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
|
||||||
|
|
||||||
import "./demo-card";
|
import "./demo-card";
|
||||||
|
import "../../../src/components/ha-switch";
|
||||||
|
|
||||||
class DemoCards extends PolymerElement {
|
class DemoCards extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@@ -26,9 +26,9 @@ class DemoCards extends PolymerElement {
|
|||||||
</style>
|
</style>
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<paper-toggle-button checked="{{_showConfig}}"
|
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||||
>Show config</paper-toggle-button
|
Show config
|
||||||
>
|
</ha-switch>
|
||||||
</div>
|
</div>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
@@ -53,6 +53,10 @@ class DemoCards extends PolymerElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_showConfigToggled(ev) {
|
||||||
|
this._showConfig = ev.target.checked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("demo-cards", DemoCards);
|
customElements.define("demo-cards", DemoCards);
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
|
||||||
|
|
||||||
import "./demo-more-info";
|
import "./demo-more-info";
|
||||||
|
import "../../../src/components/ha-switch";
|
||||||
|
|
||||||
class DemoMoreInfos extends PolymerElement {
|
class DemoMoreInfos extends PolymerElement {
|
||||||
static get template() {
|
static get template() {
|
||||||
@@ -26,9 +26,7 @@ class DemoMoreInfos extends PolymerElement {
|
|||||||
</style>
|
</style>
|
||||||
<app-toolbar>
|
<app-toolbar>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<paper-toggle-button checked="{{_showConfig}}"
|
<ha-switch checked="{{_showConfig}}">Show entity</ha-switch>
|
||||||
>Show entity</paper-toggle-button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</app-toolbar>
|
</app-toolbar>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
|
@@ -36,7 +36,7 @@ export default {
|
|||||||
attributes: {
|
attributes: {
|
||||||
title: "Welcome Home!",
|
title: "Welcome Home!",
|
||||||
message:
|
message:
|
||||||
"Here are some resources to get started:\n\n - [Configuring Home Assistant](https://home-assistant.io/getting-started/configuration/)\n - [Available components](https://home-assistant.io/components/)\n - [Troubleshooting your configuration](https://home-assistant.io/docs/configuration/troubleshooting/)\n - [Getting help](https://home-assistant.io/help/)\n\nTo not see this card popup in the future, edit your config in\n`configuration.yaml` and disable the `introduction` component.",
|
"Here are some resources to get started:\n\n - [Configuring Home Assistant](https://home-assistant.io/getting-started/configuration/)\n - [Available integrations](https://home-assistant.io/integrations/)\n - [Troubleshooting your configuration](https://home-assistant.io/docs/configuration/troubleshooting/)\n - [Getting help](https://home-assistant.io/help/)\n\nTo not see this card popup in the future, edit your config in\n`configuration.yaml` and disable the `introduction` integration.",
|
||||||
},
|
},
|
||||||
last_changed: "2018-07-19T10:44:45.922241+00:00",
|
last_changed: "2018-07-19T10:44:45.922241+00:00",
|
||||||
last_updated: "2018-07-19T10:44:45.922241+00:00",
|
last_updated: "2018-07-19T10:44:45.922241+00:00",
|
||||||
|
@@ -2,19 +2,19 @@ import { html, LitElement, TemplateResult } from "lit-element";
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
|
|
||||||
import "../../../src/components/ha-card";
|
import "../../../src/components/ha-card";
|
||||||
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
|
import { actionHandler } from "../../../src/panels/lovelace/common/directives/action-handler-directive";
|
||||||
|
import { ActionHandlerEvent } from "../../../src/data/lovelace";
|
||||||
|
|
||||||
export class DemoUtilLongPress extends LitElement {
|
export class DemoUtilLongPress extends LitElement {
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
${[1, 2, 3].map(
|
${[1, 2, 3].map(
|
||||||
() => html`
|
() => html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@ha-click="${this._handleTap}"
|
@action=${this._handleAction}
|
||||||
@ha-hold="${this._handleHold}"
|
.actionHandler=${actionHandler({})}
|
||||||
.longPress="${longPress()}"
|
|
||||||
>
|
>
|
||||||
(long) press me!
|
(long) press me!
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
@@ -28,12 +28,8 @@ export class DemoUtilLongPress extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleTap(ev: Event) {
|
private _handleAction(ev: ActionHandlerEvent) {
|
||||||
this._addValue(ev, "tap");
|
this._addValue(ev, ev.detail.action!);
|
||||||
}
|
|
||||||
|
|
||||||
private _handleHold(ev: Event) {
|
|
||||||
this._addValue(ev, "hold");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addValue(ev: Event, value: string) {
|
private _addValue(ev: Event, value: string) {
|
||||||
|
22
gallery/src/html/index.html.template
Normal file
22
gallery/src/html/index.html.template
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!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="#2157BC" />
|
||||||
|
<title>HAGallery</title>
|
||||||
|
|
||||||
|
<script type="module" src="<%= latestGalleryJS %>"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Roboto, Noto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
@@ -1,6 +1,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const webpackBase = require("../build-scripts/webpack.js");
|
const { createGalleryConfig } = require("../build-scripts/webpack.js");
|
||||||
|
const { babelLoaderConfig } = require("../build-scripts/babel.js");
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||||
@@ -8,71 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
|
|||||||
const publicPath = isProd ? "./" : "http://localhost:8080/";
|
const publicPath = isProd ? "./" : "http://localhost:8080/";
|
||||||
const latestBuild = true;
|
const latestBuild = true;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = createGalleryConfig({
|
||||||
mode: isProd ? "production" : "development",
|
latestBuild: true,
|
||||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
});
|
||||||
// Was source-map
|
|
||||||
devtool: isProd ? "none" : "inline-source-map",
|
const bla = () => {
|
||||||
entry: "./src/entrypoint.js",
|
const oldExports = {
|
||||||
module: {
|
mode: isProd ? "production" : "development",
|
||||||
rules: [
|
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||||
{
|
// Was source-map
|
||||||
test: /\.ts$/,
|
devtool: isProd ? "none" : "inline-source-map",
|
||||||
exclude: /node_modules/,
|
entry: "./src/entrypoint.js",
|
||||||
use: [
|
module: {
|
||||||
{
|
rules: [
|
||||||
loader: "ts-loader",
|
babelLoaderConfig({ latestBuild }),
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: "raw-loader",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(html)$/,
|
||||||
|
use: {
|
||||||
|
loader: "html-loader",
|
||||||
options: {
|
options: {
|
||||||
compilerOptions: latestBuild
|
exportAsEs6Default: true,
|
||||||
? { noEmit: false }
|
|
||||||
: { target: "es5", noEmit: false },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: "raw-loader",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(html)$/,
|
|
||||||
use: {
|
|
||||||
loader: "html-loader",
|
|
||||||
options: {
|
|
||||||
exportAsEs6Default: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
optimization: webpackBase.optimization(latestBuild),
|
||||||
optimization: webpackBase.optimization(latestBuild),
|
plugins: [
|
||||||
plugins: [
|
new CopyWebpackPlugin([
|
||||||
new CopyWebpackPlugin([
|
"public",
|
||||||
"public",
|
{ from: "../public", to: "static" },
|
||||||
{ from: "../public", to: "static" },
|
{ from: "../build-translations/output", to: "static/translations" },
|
||||||
{ from: "../build-translations/output", to: "static/translations" },
|
{
|
||||||
{
|
from: "../node_modules/leaflet/dist/leaflet.css",
|
||||||
from: "../node_modules/leaflet/dist/leaflet.css",
|
to: "static/images/leaflet/",
|
||||||
to: "static/images/leaflet/",
|
},
|
||||||
},
|
{
|
||||||
{
|
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
|
||||||
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
|
to: "static/fonts/roboto/",
|
||||||
to: "static/fonts/roboto/",
|
},
|
||||||
},
|
{
|
||||||
{
|
from: "../node_modules/leaflet/dist/images",
|
||||||
from: "../node_modules/leaflet/dist/images",
|
to: "static/images/leaflet/",
|
||||||
to: "static/images/leaflet/",
|
},
|
||||||
},
|
]),
|
||||||
]),
|
].filter(Boolean),
|
||||||
].filter(Boolean),
|
resolve: webpackBase.resolve,
|
||||||
resolve: webpackBase.resolve,
|
output: {
|
||||||
output: {
|
filename: "[name].js",
|
||||||
filename: "[name].js",
|
chunkFilename: chunkFilename,
|
||||||
chunkFilename: chunkFilename,
|
path: buildPath,
|
||||||
path: buildPath,
|
publicPath,
|
||||||
publicPath,
|
},
|
||||||
},
|
devServer: {
|
||||||
devServer: {
|
contentBase: "./public",
|
||||||
contentBase: "./public",
|
},
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// Target directory for the build.
|
// Target directory for the build.
|
||||||
buildDir: path.resolve(__dirname, "build"),
|
buildDir: path.resolve(__dirname, "build"),
|
||||||
|
nodeDir: path.resolve(__dirname, "../node_modules"),
|
||||||
// Path where the Hass.io frontend will be publicly available.
|
// Path where the Hass.io frontend will be publicly available.
|
||||||
publicPath: "/api/hassio/app",
|
publicPath: "/api/hassio/app",
|
||||||
};
|
};
|
||||||
|
@@ -4,11 +4,6 @@
|
|||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
OUTPUT_DIR=build
|
./node_modules/.bin/gulp build-hassio
|
||||||
|
|
||||||
rm -rf $OUTPUT_DIR
|
|
||||||
|
|
||||||
node script/gen-icons.js
|
|
||||||
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
|
|
||||||
|
@@ -4,11 +4,6 @@
|
|||||||
# Stop on errors
|
# Stop on errors
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
OUTPUT_DIR=build
|
./node_modules/.bin/gulp develop-hassio
|
||||||
|
|
||||||
rm -rf $OUTPUT_DIR
|
|
||||||
mkdir $OUTPUT_DIR
|
|
||||||
node script/gen-icons.js
|
|
||||||
../node_modules/.bin/webpack --watch --progress
|
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
const fs = require("fs");
|
|
||||||
const {
|
|
||||||
findIcons,
|
|
||||||
generateIconset,
|
|
||||||
genMDIIcons,
|
|
||||||
} = require("../../build-scripts/gulp/gen-icons.js");
|
|
||||||
|
|
||||||
function genHassioIcons() {
|
|
||||||
const iconNames = findIcons("./src", "hassio");
|
|
||||||
|
|
||||||
for (const item of findIcons("../src", "hassio")) {
|
|
||||||
iconNames.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
genMDIIcons();
|
|
||||||
genHassioIcons();
|
|
@@ -15,7 +15,7 @@ import { HomeAssistant } from "../../../src/types";
|
|||||||
import {
|
import {
|
||||||
HassioAddonInfo,
|
HassioAddonInfo,
|
||||||
HassioAddonRepository,
|
HassioAddonRepository,
|
||||||
} from "../../../src/data/hassio";
|
} from "../../../src/data/hassio/addon";
|
||||||
import { navigate } from "../../../src/common/navigate";
|
import { navigate } from "../../../src/common/navigate";
|
||||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||||
|
|
||||||
@@ -36,84 +36,90 @@ class HassioAddonRepositoryEl extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
const repo = this.repo;
|
const repo = this.repo;
|
||||||
const addons = this._getAddons(this.addons, this.filter);
|
const addons = this._getAddons(this.addons, this.filter);
|
||||||
|
const ha105pluss = this._computeHA105plus;
|
||||||
|
|
||||||
if (this.filter && addons.length < 1) {
|
if (this.filter && addons.length < 1) {
|
||||||
return html`
|
return html`
|
||||||
<div class="card-group">
|
<div class="content">
|
||||||
<div class="title">
|
<p class="description">
|
||||||
<div class="description">
|
No results found in "${repo.name}"
|
||||||
No results found in "${repo.name}"
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div class="card-group">
|
<div class="content">
|
||||||
<div class="title">
|
<h1>
|
||||||
${repo.name}
|
${repo.name}
|
||||||
<div class="description">
|
</h1>
|
||||||
Maintained by ${repo.maintainer}<br />
|
<p class="description">
|
||||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
Maintained by ${repo.maintainer}<br />
|
||||||
</div>
|
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
||||||
|
</p>
|
||||||
|
<div class="card-group">
|
||||||
|
${addons.map(
|
||||||
|
(addon) => html`
|
||||||
|
<paper-card
|
||||||
|
.addon=${addon}
|
||||||
|
class=${addon.available ? "" : "not_available"}
|
||||||
|
@click=${this._addonTapped}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<hassio-card-content
|
||||||
|
.hass=${this.hass}
|
||||||
|
.title=${addon.name}
|
||||||
|
.description=${addon.description}
|
||||||
|
.available=${addon.available}
|
||||||
|
.icon=${addon.installed && addon.installed !== addon.version
|
||||||
|
? "hassio:arrow-up-bold-circle"
|
||||||
|
: "hassio:puzzle"}
|
||||||
|
.iconTitle=${addon.installed
|
||||||
|
? addon.installed !== addon.version
|
||||||
|
? "New version available"
|
||||||
|
: "Add-on is installed"
|
||||||
|
: addon.available
|
||||||
|
? "Add-on is not installed"
|
||||||
|
: "Add-on is not available on your system"}
|
||||||
|
.iconClass=${addon.installed
|
||||||
|
? addon.installed !== addon.version
|
||||||
|
? "update"
|
||||||
|
: "installed"
|
||||||
|
: !addon.available
|
||||||
|
? "not_available"
|
||||||
|
: ""}
|
||||||
|
.iconImage=${ha105pluss && addon.icon
|
||||||
|
? `/api/hassio/addons/${addon.slug}/icon`
|
||||||
|
: undefined}
|
||||||
|
.showTopbar=${addon.installed || !addon.available}
|
||||||
|
.topbarClass=${addon.installed
|
||||||
|
? addon.installed !== addon.version
|
||||||
|
? "update"
|
||||||
|
: "installed"
|
||||||
|
: !addon.available
|
||||||
|
? "unavailable"
|
||||||
|
: ""}
|
||||||
|
></hassio-card-content>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${addons.map(
|
|
||||||
(addon) => html`
|
|
||||||
<paper-card
|
|
||||||
.addon=${addon}
|
|
||||||
class=${addon.available ? "" : "not_available"}
|
|
||||||
@click=${this.addonTapped}
|
|
||||||
>
|
|
||||||
<div class="card-content">
|
|
||||||
<hassio-card-content
|
|
||||||
.hass=${this.hass}
|
|
||||||
.title=${addon.name}
|
|
||||||
.description=${addon.description}
|
|
||||||
.available=${addon.available}
|
|
||||||
.icon=${this.computeIcon(addon)}
|
|
||||||
.iconTitle=${this.computeIconTitle(addon)}
|
|
||||||
.iconClass=${this.computeIconClass(addon)}
|
|
||||||
></hassio-card-content>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeIcon(addon) {
|
private _addonTapped(ev) {
|
||||||
return addon.installed && addon.installed !== addon.version
|
|
||||||
? "hassio:arrow-up-bold-circle"
|
|
||||||
: "hassio:puzzle";
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeIconTitle(addon) {
|
|
||||||
if (addon.installed) {
|
|
||||||
return addon.installed !== addon.version
|
|
||||||
? "New version available"
|
|
||||||
: "Add-on is installed";
|
|
||||||
}
|
|
||||||
return addon.available
|
|
||||||
? "Add-on is not installed"
|
|
||||||
: "Add-on is not available on your system";
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeIconClass(addon) {
|
|
||||||
if (addon.installed) {
|
|
||||||
return addon.installed !== addon.version ? "update" : "installed";
|
|
||||||
}
|
|
||||||
return !addon.available ? "not_available" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private addonTapped(ev) {
|
|
||||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _computeHA105plus(): boolean {
|
||||||
|
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||||
|
return Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultArray {
|
static get styles(): CSSResultArray {
|
||||||
return [
|
return [
|
||||||
hassioStyle,
|
hassioStyle,
|
||||||
|
@@ -14,7 +14,7 @@ import {
|
|||||||
HassioAddonInfo,
|
HassioAddonInfo,
|
||||||
fetchHassioAddonsInfo,
|
fetchHassioAddonsInfo,
|
||||||
reloadHassioAddons,
|
reloadHassioAddons,
|
||||||
} from "../../../src/data/hassio";
|
} from "../../../src/data/hassio/addon";
|
||||||
import "../../../src/layouts/loading-screen";
|
import "../../../src/layouts/loading-screen";
|
||||||
import "../components/hassio-search-input";
|
import "../components/hassio-search-input";
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class HassioAddonStore extends LitElement {
|
|||||||
await this._loadData();
|
await this._loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (!this._addons || !this._repos) {
|
if (!this._addons || !this._repos) {
|
||||||
return html`
|
return html`
|
||||||
<loading-screen></loading-screen>
|
<loading-screen></loading-screen>
|
||||||
|
@@ -17,7 +17,7 @@ import "../../../src/components/buttons/ha-call-api-button";
|
|||||||
import "../components/hassio-card-content";
|
import "../components/hassio-card-content";
|
||||||
import { hassioStyle } from "../resources/hassio-style";
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
import { HassioAddonRepository } from "../../../src/data/hassio";
|
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||||
import { repeat } from "lit-html/directives/repeat";
|
import { repeat } from "lit-html/directives/repeat";
|
||||||
|
|
||||||
@@ -33,64 +33,66 @@ class HassioRepositoriesEditor extends LitElement {
|
|||||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
const repos = this._sortedRepos(this.repos);
|
const repos = this._sortedRepos(this.repos);
|
||||||
return html`
|
return html`
|
||||||
<div class="card-group">
|
<div class="content">
|
||||||
<div class="title">
|
<h1>
|
||||||
Repositories
|
Repositories
|
||||||
<div class="description">
|
</h1>
|
||||||
Configure which add-on repositories to fetch data from:
|
<p class="description">
|
||||||
</div>
|
Configure which add-on repositories to fetch data from:
|
||||||
</div>
|
</p>
|
||||||
${// Use repeat so that the fade-out from call-service-api-button
|
<div class="card-group">
|
||||||
// stays with the correct repo after we add/delete one.
|
${// Use repeat so that the fade-out from call-service-api-button
|
||||||
repeat(
|
// stays with the correct repo after we add/delete one.
|
||||||
repos,
|
repeat(
|
||||||
(repo) => repo.slug,
|
repos,
|
||||||
(repo) => html`
|
(repo) => repo.slug,
|
||||||
<paper-card>
|
(repo) => html`
|
||||||
<div class="card-content">
|
<paper-card>
|
||||||
<hassio-card-content
|
<div class="card-content">
|
||||||
.hass=${this.hass}
|
<hassio-card-content
|
||||||
.title=${repo.name}
|
.hass=${this.hass}
|
||||||
.description=${repo.url}
|
.title=${repo.name}
|
||||||
icon="hassio:github-circle"
|
.description=${repo.url}
|
||||||
></hassio-card-content>
|
icon="hassio:github-circle"
|
||||||
</div>
|
></hassio-card-content>
|
||||||
<div class="card-actions">
|
</div>
|
||||||
<ha-call-api-button
|
<div class="card-actions">
|
||||||
path="hassio/supervisor/options"
|
<ha-call-api-button
|
||||||
.hass=${this.hass}
|
path="hassio/supervisor/options"
|
||||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
.hass=${this.hass}
|
||||||
class="warning"
|
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||||
>
|
class="warning"
|
||||||
Remove
|
>
|
||||||
</ha-call-api-button>
|
Remove
|
||||||
</div>
|
</ha-call-api-button>
|
||||||
</paper-card>
|
</div>
|
||||||
`
|
</paper-card>
|
||||||
)}
|
`
|
||||||
|
)}
|
||||||
|
|
||||||
<paper-card>
|
<paper-card>
|
||||||
<div class="card-content add">
|
<div class="card-content add">
|
||||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||||
<paper-input
|
<paper-input
|
||||||
label="Add new repository by URL"
|
label="Add new repository by URL"
|
||||||
.value=${this._repoUrl}
|
.value=${this._repoUrl}
|
||||||
@value-changed=${this._urlChanged}
|
@value-changed=${this._urlChanged}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-call-api-button
|
<ha-call-api-button
|
||||||
path="hassio/supervisor/options"
|
path="hassio/supervisor/options"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</ha-call-api-button>
|
</ha-call-api-button>
|
||||||
</div>
|
</div>
|
||||||
</paper-card>
|
</paper-card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -1,142 +0,0 @@
|
|||||||
import "web-animations-js/web-animations-next-lite.min";
|
|
||||||
|
|
||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
|
||||||
import "@polymer/paper-item/paper-item";
|
|
||||||
import "@polymer/paper-listbox/paper-listbox";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../src/resources/ha-style";
|
|
||||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
|
||||||
|
|
||||||
class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host,
|
|
||||||
paper-card,
|
|
||||||
paper-dropdown-menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
paper-item {
|
|
||||||
width: 450px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<paper-card heading="Audio">
|
|
||||||
<div class="card-content">
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<div class="errors">[[error]]</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<paper-dropdown-menu label="Input">
|
|
||||||
<paper-listbox
|
|
||||||
slot="dropdown-content"
|
|
||||||
attr-for-selected="device"
|
|
||||||
selected="{{selectedInput}}"
|
|
||||||
>
|
|
||||||
<template is="dom-repeat" items="[[inputDevices]]">
|
|
||||||
<paper-item device\$="[[item.device]]"
|
|
||||||
>[[item.name]]</paper-item
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
<paper-dropdown-menu label="Output">
|
|
||||||
<paper-listbox
|
|
||||||
slot="dropdown-content"
|
|
||||||
attr-for-selected="device"
|
|
||||||
selected="{{selectedOutput}}"
|
|
||||||
>
|
|
||||||
<template is="dom-repeat" items="[[outputDevices]]">
|
|
||||||
<paper-item device\$="[[item.device]]"
|
|
||||||
>[[item.name]]</paper-item
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<mwc-button on-click="_saveSettings">Save</mwc-button>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addon: {
|
|
||||||
type: Object,
|
|
||||||
observer: "addonChanged",
|
|
||||||
},
|
|
||||||
inputDevices: Array,
|
|
||||||
outputDevices: Array,
|
|
||||||
selectedInput: String,
|
|
||||||
selectedOutput: String,
|
|
||||||
error: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addonChanged(addon) {
|
|
||||||
this.setProperties({
|
|
||||||
selectedInput: addon.audio_input || "null",
|
|
||||||
selectedOutput: addon.audio_output || "null",
|
|
||||||
});
|
|
||||||
if (this.outputDevices) return;
|
|
||||||
|
|
||||||
const noDevice = [{ device: "null", name: "-" }];
|
|
||||||
this.hass.callApi("get", "hassio/hardware/audio").then(
|
|
||||||
(resp) => {
|
|
||||||
const dev = resp.data.audio;
|
|
||||||
const input = Object.keys(dev.input).map((key) => ({
|
|
||||||
device: key,
|
|
||||||
name: dev.input[key],
|
|
||||||
}));
|
|
||||||
const output = Object.keys(dev.output).map((key) => ({
|
|
||||||
device: key,
|
|
||||||
name: dev.output[key],
|
|
||||||
}));
|
|
||||||
this.setProperties({
|
|
||||||
inputDevices: noDevice.concat(input),
|
|
||||||
outputDevices: noDevice.concat(output),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.setProperties({
|
|
||||||
inputDevices: noDevice,
|
|
||||||
outputDevices: noDevice,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_saveSettings() {
|
|
||||||
this.error = null;
|
|
||||||
const path = `hassio/addons/${this.addon.slug}/options`;
|
|
||||||
this.hass
|
|
||||||
.callApi("post", path, {
|
|
||||||
audio_input: this.selectedInput === "null" ? null : this.selectedInput,
|
|
||||||
audio_output:
|
|
||||||
this.selectedOutput === "null" ? null : this.selectedOutput,
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
this.fire("hass-api-called", { success: true, path: path });
|
|
||||||
},
|
|
||||||
(resp) => {
|
|
||||||
this.error = resp.body.message;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addon-audio", HassioAddonAudio);
|
|
186
hassio/src/addon-view/hassio-addon-audio.ts
Normal file
186
hassio/src/addon-view/hassio-addon-audio.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import "web-animations-js/web-animations-next-lite.min";
|
||||||
|
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
setHassioAddonOption,
|
||||||
|
HassioAddonSetOptionParams,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import {
|
||||||
|
HassioHardwareAudioDevice,
|
||||||
|
fetchHassioHardwareAudio,
|
||||||
|
} from "../../../src/data/hassio/hardware";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
|
||||||
|
@customElement("hassio-addon-audio")
|
||||||
|
class HassioAddonAudio extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addon!: HassioAddonDetails;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@property() private _inputDevices?: HassioHardwareAudioDevice[];
|
||||||
|
@property() private _outputDevices?: HassioHardwareAudioDevice[];
|
||||||
|
@property() private _selectedInput!: null | string;
|
||||||
|
@property() private _selectedOutput!: null | string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<paper-card heading="Audio">
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<paper-dropdown-menu
|
||||||
|
label="Input"
|
||||||
|
@iron-select=${this._setInputDevice}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
attr-for-selected="device"
|
||||||
|
.selected=${this._selectedInput}
|
||||||
|
>
|
||||||
|
${this._inputDevices &&
|
||||||
|
this._inputDevices.map((item) => {
|
||||||
|
return html`
|
||||||
|
<paper-item device=${item.device || ""}
|
||||||
|
>${item.name}</paper-item
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
label="Output"
|
||||||
|
@iron-select=${this._setOutputDevice}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
attr-for-selected="device"
|
||||||
|
.selected=${this._selectedOutput}
|
||||||
|
>
|
||||||
|
${this._outputDevices &&
|
||||||
|
this._outputDevices.map((item) => {
|
||||||
|
return html`
|
||||||
|
<paper-item device=${item.device || ""}
|
||||||
|
>${item.name}</paper-item
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host,
|
||||||
|
paper-card,
|
||||||
|
paper-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
paper-item {
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(changedProperties: PropertyValues): void {
|
||||||
|
super.update(changedProperties);
|
||||||
|
if (changedProperties.has("addon")) {
|
||||||
|
this._addonChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setInputDevice(ev): void {
|
||||||
|
const device = ev.detail.item.getAttribute("device");
|
||||||
|
this._selectedInput = device || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setOutputDevice(ev): void {
|
||||||
|
const device = ev.detail.item.getAttribute("device");
|
||||||
|
this._selectedOutput = device || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addonChanged(): Promise<void> {
|
||||||
|
this._selectedInput = this.addon.audio_input;
|
||||||
|
this._selectedOutput = this.addon.audio_output;
|
||||||
|
if (this._outputDevices) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
||||||
|
const input = Object.keys(audio.input).map((key) => ({
|
||||||
|
device: key,
|
||||||
|
name: audio.input[key],
|
||||||
|
}));
|
||||||
|
const output = Object.keys(audio.output).map((key) => ({
|
||||||
|
device: key,
|
||||||
|
name: audio.output[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
this._inputDevices = [noDevice, ...input];
|
||||||
|
this._outputDevices = [noDevice, ...output];
|
||||||
|
} catch {
|
||||||
|
this._error = "Failed to fetch audio hardware";
|
||||||
|
this._inputDevices = [noDevice];
|
||||||
|
this._outputDevices = [noDevice];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveSettings(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
audio_input: this._selectedInput || null,
|
||||||
|
audio_output: this._selectedOutput || null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
} catch {
|
||||||
|
this._error = "Failed to set addon audio device";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-audio": HassioAddonAudio;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,111 +0,0 @@
|
|||||||
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
|
||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
|
||||||
|
|
||||||
class HassioAddonConfig extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
paper-card {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
@apply --layout;
|
|
||||||
@apply --layout-justified;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
iron-autogrow-textarea {
|
|
||||||
width: 100%;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
.syntaxerror {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<paper-card heading="Config">
|
|
||||||
<div class="card-content">
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<div class="errors">[[error]]</div>
|
|
||||||
</template>
|
|
||||||
<iron-autogrow-textarea
|
|
||||||
id="config"
|
|
||||||
value="{{config}}"
|
|
||||||
></iron-autogrow-textarea>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/options"
|
|
||||||
data="[[resetData]]"
|
|
||||||
>Reset to defaults</ha-call-api-button
|
|
||||||
>
|
|
||||||
<mwc-button on-click="saveTapped" disabled="[[!configParsed]]"
|
|
||||||
>Save</mwc-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addon: {
|
|
||||||
type: Object,
|
|
||||||
observer: "addonChanged",
|
|
||||||
},
|
|
||||||
addonSlug: String,
|
|
||||||
config: {
|
|
||||||
type: String,
|
|
||||||
observer: "configChanged",
|
|
||||||
},
|
|
||||||
configParsed: Object,
|
|
||||||
error: String,
|
|
||||||
resetData: {
|
|
||||||
type: Object,
|
|
||||||
value: {
|
|
||||||
options: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addonChanged(addon) {
|
|
||||||
this.config = addon ? JSON.stringify(addon.options, null, 2) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
configChanged(config) {
|
|
||||||
try {
|
|
||||||
this.$.config.classList.remove("syntaxerror");
|
|
||||||
this.configParsed = JSON.parse(config);
|
|
||||||
} catch (err) {
|
|
||||||
this.$.config.classList.add("syntaxerror");
|
|
||||||
this.configParsed = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTapped() {
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
this.hass
|
|
||||||
.callApi("post", `hassio/addons/${this.addonSlug}/options`, {
|
|
||||||
options: this.configParsed,
|
|
||||||
})
|
|
||||||
.catch((resp) => {
|
|
||||||
this.error = resp.body.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addon-config", HassioAddonConfig);
|
|
179
hassio/src/addon-view/hassio-addon-config.ts
Normal file
179
hassio/src/addon-view/hassio-addon-config.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
setHassioAddonOption,
|
||||||
|
HassioAddonSetOptionParams,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
import "../../../src/components/ha-yaml-editor";
|
||||||
|
// tslint:disable-next-line: no-duplicate-imports
|
||||||
|
import { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
|
||||||
|
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
|
|
||||||
|
@customElement("hassio-addon-config")
|
||||||
|
class HassioAddonConfig extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addon!: HassioAddonDetails;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@property({ type: Boolean }) private _configHasChanged = false;
|
||||||
|
|
||||||
|
@query("ha-yaml-editor") private _editor!: HaYamlEditor;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const editor = this._editor;
|
||||||
|
// If editor not rendered, don't show the error.
|
||||||
|
const valid = editor ? editor.isValid : true;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<paper-card heading="Config">
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-yaml-editor
|
||||||
|
@value-changed=${this._configChanged}
|
||||||
|
></ha-yaml-editor>
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${valid
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<div class="errors">Invalid YAML</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||||
|
Reset to defaults
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._saveTapped}
|
||||||
|
.disabled=${!this._configHasChanged || !valid}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
paper-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
iron-autogrow-textarea {
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.syntaxerror {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has("addon")) {
|
||||||
|
this._editor.setValue(this.addon.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _configChanged(): void {
|
||||||
|
this._configHasChanged = true;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _resetTapped(): Promise<void> {
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.addon.name,
|
||||||
|
text: "Are you sure you want to reset all your options?",
|
||||||
|
confirmText: "reset options",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
options: null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "options",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to reset addon configuration, ${err.body?.message ||
|
||||||
|
err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveTapped(): Promise<void> {
|
||||||
|
let data: HassioAddonSetOptionParams;
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
data = {
|
||||||
|
options: this._editor.value,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this._error = err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
this._configHasChanged = false;
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "options",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to save addon configuration, ${err.body?.message ||
|
||||||
|
err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-config": HassioAddonConfig;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,615 +0,0 @@
|
|||||||
import "@polymer/iron-icon/iron-icon";
|
|
||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
|
||||||
import "@polymer/paper-toggle-button/paper-toggle-button";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../src/components/ha-label-badge";
|
|
||||||
import "../../../src/components/ha-markdown";
|
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
|
||||||
import "../../../src/resources/ha-style";
|
|
||||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
|
||||||
import { navigate } from "../../../src/common/navigate";
|
|
||||||
|
|
||||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
|
||||||
import "../components/hassio-card-content";
|
|
||||||
|
|
||||||
const PERMIS_DESC = {
|
|
||||||
rating: {
|
|
||||||
title: "Add-on Security Rating",
|
|
||||||
description:
|
|
||||||
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
|
|
||||||
},
|
|
||||||
host_network: {
|
|
||||||
title: "Host Network",
|
|
||||||
description:
|
|
||||||
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
|
|
||||||
},
|
|
||||||
homeassistant_api: {
|
|
||||||
title: "Home Assistant API Access",
|
|
||||||
description:
|
|
||||||
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
|
|
||||||
},
|
|
||||||
full_access: {
|
|
||||||
title: "Full Hardware Access",
|
|
||||||
description:
|
|
||||||
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
|
||||||
},
|
|
||||||
hassio_api: {
|
|
||||||
title: "Hass.io API Access",
|
|
||||||
description:
|
|
||||||
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
|
|
||||||
},
|
|
||||||
docker_api: {
|
|
||||||
title: "Full Docker Access",
|
|
||||||
description:
|
|
||||||
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
|
||||||
},
|
|
||||||
host_pid: {
|
|
||||||
title: "Host Processes Namespace",
|
|
||||||
description:
|
|
||||||
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
|
||||||
},
|
|
||||||
apparmor: {
|
|
||||||
title: "AppArmor",
|
|
||||||
description:
|
|
||||||
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
|
|
||||||
},
|
|
||||||
auth_api: {
|
|
||||||
title: "Home Assistant Authentication",
|
|
||||||
description:
|
|
||||||
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
|
|
||||||
},
|
|
||||||
ingress: {
|
|
||||||
title: "Ingress",
|
|
||||||
description:
|
|
||||||
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
paper-card {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
paper-card.warning {
|
|
||||||
background-color: var(--google-red-500);
|
|
||||||
color: white;
|
|
||||||
--paper-card-header-color: white;
|
|
||||||
}
|
|
||||||
paper-card.warning mwc-button {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
}
|
|
||||||
.addon-header {
|
|
||||||
@apply --paper-font-headline;
|
|
||||||
}
|
|
||||||
.light-color {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.addon-version {
|
|
||||||
float: right;
|
|
||||||
font-size: 15px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.logo img {
|
|
||||||
max-height: 60px;
|
|
||||||
margin: 16px 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.state div {
|
|
||||||
width: 180px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.state iron-icon {
|
|
||||||
width: 16px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
paper-toggle-button {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
iron-icon.running {
|
|
||||||
color: var(--paper-green-400);
|
|
||||||
}
|
|
||||||
iron-icon.stopped {
|
|
||||||
color: var(--google-red-300);
|
|
||||||
}
|
|
||||||
ha-call-api-button {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
ha-markdown img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.red {
|
|
||||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
|
||||||
}
|
|
||||||
.blue {
|
|
||||||
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
|
||||||
}
|
|
||||||
.green {
|
|
||||||
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
|
||||||
}
|
|
||||||
.yellow {
|
|
||||||
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
|
||||||
}
|
|
||||||
.security {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.security h3 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.security ha-label-badge {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 4px;
|
|
||||||
--iron-icon-height: 45px;
|
|
||||||
}
|
|
||||||
.protection-enable mwc-button {
|
|
||||||
--mdc-theme-primary: white;
|
|
||||||
}
|
|
||||||
.description a, ha-markdown a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
|
|
||||||
<paper-card heading="Update available! 🎉">
|
|
||||||
<div class="card-content">
|
|
||||||
<hassio-card-content
|
|
||||||
hass="[[hass]]"
|
|
||||||
title="[[addon.name]] [[addon.last_version]] is available"
|
|
||||||
description="You are currently running version [[addon.version]]"
|
|
||||||
icon="hassio:arrow-up-bold-circle"
|
|
||||||
icon-class="update"
|
|
||||||
></hassio-card-content>
|
|
||||||
<template is="dom-if" if="[[!addon.available]]">
|
|
||||||
<p>This update is no longer compatible with your system.</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<ha-call-api-button
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/update"
|
|
||||||
disabled="[[!addon.available]]"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ha-call-api-button
|
|
||||||
>
|
|
||||||
<template is="dom-if" if="[[addon.changelog]]">
|
|
||||||
<mwc-button on-click="openChangelog">Changelog</mwc-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[!addon.protected]]">
|
|
||||||
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
|
|
||||||
<div class="card-content">
|
|
||||||
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
|
|
||||||
</div>
|
|
||||||
<div class="card-actions protection-enable">
|
|
||||||
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<paper-card>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="addon-header">
|
|
||||||
[[addon.name]]
|
|
||||||
<div class="addon-version light-color">
|
|
||||||
<template is="dom-if" if="[[addon.version]]">
|
|
||||||
[[addon.version]]
|
|
||||||
<template is="dom-if" if="[[isRunning]]">
|
|
||||||
<iron-icon
|
|
||||||
title="Add-on is running"
|
|
||||||
class="running"
|
|
||||||
icon="hassio:circle"
|
|
||||||
></iron-icon>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!isRunning]]">
|
|
||||||
<iron-icon
|
|
||||||
title="Add-on is stopped"
|
|
||||||
class="stopped"
|
|
||||||
icon="hassio:circle"
|
|
||||||
></iron-icon>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!addon.version]]">
|
|
||||||
[[addon.last_version]]
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="description light-color">
|
|
||||||
[[addon.description]].<br />
|
|
||||||
Visit
|
|
||||||
<a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for
|
|
||||||
details.
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[addon.logo]]">
|
|
||||||
<a href="[[addon.url]]" target="_blank" class="logo">
|
|
||||||
<img src="/api/hassio/addons/[[addonSlug]]/logo" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<div class="security">
|
|
||||||
<ha-label-badge
|
|
||||||
class$="[[computeSecurityClassName(addon.rating)]]"
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="rating"
|
|
||||||
value="[[addon.rating]]"
|
|
||||||
label="rating"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
<template is="dom-if" if="[[addon.host_network]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="host_network"
|
|
||||||
icon="hassio:network"
|
|
||||||
label="host"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.full_access]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="full_access"
|
|
||||||
icon="hassio:chip"
|
|
||||||
label="hardware"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.homeassistant_api]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="homeassistant_api"
|
|
||||||
icon="hassio:home-assistant"
|
|
||||||
label="hass"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[computeHassioApi(addon)]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="hassio_api"
|
|
||||||
icon="hassio:home-assistant"
|
|
||||||
label="hassio"
|
|
||||||
description="[[addon.hassio_role]]"
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.docker_api]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="docker_api"
|
|
||||||
icon="hassio:docker"
|
|
||||||
label="docker"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.host_pid]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="host_pid"
|
|
||||||
icon="hassio:pound"
|
|
||||||
label="host pid"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.apparmor]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
class$="[[computeApparmorClassName(addon.apparmor)]]"
|
|
||||||
id="apparmor"
|
|
||||||
icon="hassio:shield"
|
|
||||||
label="apparmor"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.auth_api]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="auth_api"
|
|
||||||
icon="hassio:key"
|
|
||||||
label="auth"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[addon.ingress]]">
|
|
||||||
<ha-label-badge
|
|
||||||
on-click="showMoreInfo"
|
|
||||||
id="ingress"
|
|
||||||
icon="hassio:cursor-default-click-outline"
|
|
||||||
label="ingress"
|
|
||||||
description=""
|
|
||||||
></ha-label-badge>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[addon.version]]">
|
|
||||||
<div class="state">
|
|
||||||
<div>Start on boot</div>
|
|
||||||
<paper-toggle-button
|
|
||||||
on-change="startOnBootToggled"
|
|
||||||
checked="[[computeStartOnBoot(addon.boot)]]"
|
|
||||||
></paper-toggle-button>
|
|
||||||
</div>
|
|
||||||
<div class="state">
|
|
||||||
<div>Auto update</div>
|
|
||||||
<paper-toggle-button
|
|
||||||
on-change="autoUpdateToggled"
|
|
||||||
checked="[[addon.auto_update]]"
|
|
||||||
></paper-toggle-button>
|
|
||||||
</div>
|
|
||||||
<template is="dom-if" if="[[addon.ingress]]">
|
|
||||||
<div class="state">
|
|
||||||
<div>Show in sidebar</div>
|
|
||||||
<paper-toggle-button
|
|
||||||
on-change="panelToggled"
|
|
||||||
checked="[[addon.ingress_panel]]"
|
|
||||||
disabled="[[_computeCannotIngressSidebar(hass, addon)]]"
|
|
||||||
></paper-toggle-button>
|
|
||||||
<template is="dom-if" if="[[_computeCannotIngressSidebar(hass, addon)]]">
|
|
||||||
<span>This option requires Home Assistant 0.92 or later.</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="state">
|
|
||||||
<div>
|
|
||||||
Protection mode
|
|
||||||
<span>
|
|
||||||
<iron-icon icon="hassio:information"></iron-icon>
|
|
||||||
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<paper-toggle-button
|
|
||||||
on-change="protectionToggled"
|
|
||||||
checked="[[addon.protected]]"
|
|
||||||
></paper-toggle-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<template is="dom-if" if="[[addon.version]]">
|
|
||||||
<mwc-button class="warning" on-click="_unistallClicked"
|
|
||||||
>Uninstall</mwc-button
|
|
||||||
>
|
|
||||||
<template is="dom-if" if="[[addon.build]]">
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/rebuild"
|
|
||||||
>Rebuild</ha-call-api-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[isRunning]]">
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/restart"
|
|
||||||
>Restart</ha-call-api-button
|
|
||||||
>
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/stop"
|
|
||||||
>Stop</ha-call-api-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!isRunning]]">
|
|
||||||
<ha-call-api-button
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/start"
|
|
||||||
>Start</ha-call-api-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="[[pathWebui(addon.webui)]]"
|
|
||||||
tabindex="-1"
|
|
||||||
target="_blank"
|
|
||||||
class="right"
|
|
||||||
><mwc-button>Open web UI</mwc-button></a
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[computeShowIngressUI(addon.ingress, isRunning)]]"
|
|
||||||
>
|
|
||||||
<mwc-button
|
|
||||||
tabindex="-1"
|
|
||||||
class="right"
|
|
||||||
on-click="openIngress"
|
|
||||||
>Open web UI</mwc-button>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!addon.version]]">
|
|
||||||
<template is="dom-if" if="[[!addon.available]]">
|
|
||||||
<p class="warning">This add-on is not available on your system.</p>
|
|
||||||
</template>
|
|
||||||
<ha-call-api-button
|
|
||||||
disabled="[[!addon.available]]"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/install"
|
|
||||||
>Install</ha-call-api-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
<template is="dom-if" if="[[addon.long_description]]">
|
|
||||||
<paper-card>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-markdown content="[[addon.long_description]]"></ha-markdown>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</template>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addon: Object,
|
|
||||||
addonSlug: String,
|
|
||||||
isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
computeIsRunning(addon) {
|
|
||||||
return addon && addon.state === "started";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeUpdateAvailable(addon) {
|
|
||||||
return (
|
|
||||||
addon &&
|
|
||||||
!addon.detached &&
|
|
||||||
addon.version &&
|
|
||||||
addon.version !== addon.last_version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeHassioApi(addon) {
|
|
||||||
return (
|
|
||||||
addon.hassio_api &&
|
|
||||||
(addon.hassio_role === "manager" || addon.hassio_role === "admin")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeApparmorClassName(apparmor) {
|
|
||||||
if (apparmor === "profile") {
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
if (apparmor === "disable") {
|
|
||||||
return "red";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pathWebui(webui) {
|
|
||||||
return webui && webui.replace("[HOST]", document.location.hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeShowWebUI(ingress, webui, isRunning) {
|
|
||||||
return !ingress && webui && isRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
openIngress() {
|
|
||||||
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeShowIngressUI(ingress, isRunning) {
|
|
||||||
return ingress && isRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeStartOnBoot(state) {
|
|
||||||
return state === "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeSecurityClassName(rating) {
|
|
||||||
if (rating > 4) {
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
if (rating > 2) {
|
|
||||||
return "yellow";
|
|
||||||
}
|
|
||||||
return "red";
|
|
||||||
}
|
|
||||||
|
|
||||||
startOnBootToggled() {
|
|
||||||
const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" };
|
|
||||||
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateToggled() {
|
|
||||||
const data = { auto_update: !this.addon.auto_update };
|
|
||||||
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
protectionToggled() {
|
|
||||||
const data = { protected: !this.addon.protected };
|
|
||||||
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data);
|
|
||||||
this.set("addon.protected", !this.addon.protected);
|
|
||||||
}
|
|
||||||
|
|
||||||
panelToggled() {
|
|
||||||
const data = { ingress_panel: !this.addon.ingress_panel };
|
|
||||||
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
showMoreInfo(e) {
|
|
||||||
const id = e.target.getAttribute("id");
|
|
||||||
showHassioMarkdownDialog(this, {
|
|
||||||
title: PERMIS_DESC[id].title,
|
|
||||||
content: PERMIS_DESC[id].description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openChangelog() {
|
|
||||||
this.hass
|
|
||||||
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
|
|
||||||
.then((resp) => resp, () => "Error getting changelog")
|
|
||||||
.then((content) => {
|
|
||||||
showHassioMarkdownDialog(this, {
|
|
||||||
title: "Changelog",
|
|
||||||
content: content,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_unistallClicked() {
|
|
||||||
if (!confirm("Are you sure you want to uninstall this add-on?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const path = `hassio/addons/${this.addonSlug}/uninstall`;
|
|
||||||
const eventData = {
|
|
||||||
path: path,
|
|
||||||
};
|
|
||||||
this.hass
|
|
||||||
.callApi("post", path)
|
|
||||||
.then(
|
|
||||||
(resp) => {
|
|
||||||
eventData.success = true;
|
|
||||||
eventData.response = resp;
|
|
||||||
},
|
|
||||||
(resp) => {
|
|
||||||
eventData.success = false;
|
|
||||||
eventData.response = resp;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
this.fire("hass-api-called", eventData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeCannotIngressSidebar(hass, addon) {
|
|
||||||
return !addon.ingress || !this._computeHA92plus(hass);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeHA92plus(hass) {
|
|
||||||
const [major, minor] = hass.config.version.split(".", 2);
|
|
||||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define("hassio-addon-info", HassioAddonInfo);
|
|
799
hassio/src/addon-view/hassio-addon-info.ts
Normal file
799
hassio/src/addon-view/hassio-addon-info.ts
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/iron-icon/iron-icon";
|
||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
|
||||||
|
import "../../../src/components/buttons/ha-call-api-button";
|
||||||
|
import "../../../src/components/buttons/ha-progress-button";
|
||||||
|
import "../../../src/components/ha-label-badge";
|
||||||
|
import "../../../src/components/ha-markdown";
|
||||||
|
import "../../../src/components/ha-switch";
|
||||||
|
import "../components/hassio-card-content";
|
||||||
|
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
HassioAddonSetOptionParams,
|
||||||
|
HassioAddonSetSecurityParams,
|
||||||
|
setHassioAddonOption,
|
||||||
|
setHassioAddonSecurity,
|
||||||
|
uninstallHassioAddon,
|
||||||
|
installHassioAddon,
|
||||||
|
fetchHassioAddonChangelog,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { navigate } from "../../../src/common/navigate";
|
||||||
|
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||||
|
|
||||||
|
const PERMIS_DESC = {
|
||||||
|
rating: {
|
||||||
|
title: "Add-on Security Rating",
|
||||||
|
description:
|
||||||
|
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
|
||||||
|
},
|
||||||
|
host_network: {
|
||||||
|
title: "Host Network",
|
||||||
|
description:
|
||||||
|
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
|
||||||
|
},
|
||||||
|
homeassistant_api: {
|
||||||
|
title: "Home Assistant API Access",
|
||||||
|
description:
|
||||||
|
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
|
||||||
|
},
|
||||||
|
full_access: {
|
||||||
|
title: "Full Hardware Access",
|
||||||
|
description:
|
||||||
|
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||||
|
},
|
||||||
|
hassio_api: {
|
||||||
|
title: "Hass.io API Access",
|
||||||
|
description:
|
||||||
|
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
|
||||||
|
},
|
||||||
|
docker_api: {
|
||||||
|
title: "Full Docker Access",
|
||||||
|
description:
|
||||||
|
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||||
|
},
|
||||||
|
host_pid: {
|
||||||
|
title: "Host Processes Namespace",
|
||||||
|
description:
|
||||||
|
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||||
|
},
|
||||||
|
apparmor: {
|
||||||
|
title: "AppArmor",
|
||||||
|
description:
|
||||||
|
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
|
||||||
|
},
|
||||||
|
auth_api: {
|
||||||
|
title: "Home Assistant Authentication",
|
||||||
|
description:
|
||||||
|
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
|
||||||
|
},
|
||||||
|
ingress: {
|
||||||
|
title: "Ingress",
|
||||||
|
description:
|
||||||
|
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hassio-addon-info")
|
||||||
|
class HassioAddonInfo extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addon!: HassioAddonDetails;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@property({ type: Boolean }) private _installing = false;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${this._computeUpdateAvailable
|
||||||
|
? html`
|
||||||
|
<paper-card heading="Update available! 🎉">
|
||||||
|
<div class="card-content">
|
||||||
|
<hassio-card-content
|
||||||
|
.hass=${this.hass}
|
||||||
|
.title="${this.addon.name} ${this.addon
|
||||||
|
.last_version} is available"
|
||||||
|
.description="You are currently running version ${this.addon
|
||||||
|
.version}"
|
||||||
|
icon="hassio:arrow-up-bold-circle"
|
||||||
|
iconClass="update"
|
||||||
|
></hassio-card-content>
|
||||||
|
${!this.addon.available
|
||||||
|
? html`
|
||||||
|
<p>
|
||||||
|
This update is no longer compatible with your system.
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<ha-call-api-button
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${!this.addon.available}
|
||||||
|
path="hassio/addons/${this.addon.slug}/update"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ha-call-api-button>
|
||||||
|
${this.addon.changelog
|
||||||
|
? html`
|
||||||
|
<mwc-button @click=${this._openChangelog}>
|
||||||
|
Changelog
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${!this.addon.protected
|
||||||
|
? html`
|
||||||
|
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
|
||||||
|
<div class="card-content">
|
||||||
|
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
|
||||||
|
</div>
|
||||||
|
<div class="card-actions protection-enable">
|
||||||
|
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<paper-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="addon-header">
|
||||||
|
${this.addon.name}
|
||||||
|
<div class="addon-version light-color">
|
||||||
|
${this.addon.version
|
||||||
|
? html`
|
||||||
|
${this.addon.version}
|
||||||
|
${this._computeIsRunning
|
||||||
|
? html`
|
||||||
|
<iron-icon
|
||||||
|
title="Add-on is running"
|
||||||
|
class="running"
|
||||||
|
icon="hassio:circle"
|
||||||
|
></iron-icon>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<iron-icon
|
||||||
|
title="Add-on is stopped"
|
||||||
|
class="stopped"
|
||||||
|
icon="hassio:circle"
|
||||||
|
></iron-icon>
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
${this.addon.last_version}
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="description light-color">
|
||||||
|
${this.addon.description}.<br />
|
||||||
|
Visit
|
||||||
|
<a href="${this.addon.url}" target="_blank">
|
||||||
|
${this.addon.name} page</a
|
||||||
|
>
|
||||||
|
for details.
|
||||||
|
</div>
|
||||||
|
${this.addon.logo
|
||||||
|
? html`
|
||||||
|
<a href="${this.addon.url}" target="_blank" class="logo">
|
||||||
|
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="security">
|
||||||
|
<ha-label-badge
|
||||||
|
class=${classMap({
|
||||||
|
green: [5, 6].includes(Number(this.addon.rating)),
|
||||||
|
yellow: [3, 4].includes(Number(this.addon.rating)),
|
||||||
|
red: [1, 2].includes(Number(this.addon.rating)),
|
||||||
|
})}
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="rating"
|
||||||
|
.value=${this.addon.rating}
|
||||||
|
label="rating"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
${this.addon.host_network
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="host_network"
|
||||||
|
icon="hassio:network"
|
||||||
|
label="host"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.full_access
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="full_access"
|
||||||
|
icon="hassio:chip"
|
||||||
|
label="hardware"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.homeassistant_api
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="homeassistant_api"
|
||||||
|
icon="hassio:home-assistant"
|
||||||
|
label="hass"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._computeHassioApi
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="hassio_api"
|
||||||
|
icon="hassio:home-assistant"
|
||||||
|
label="hassio"
|
||||||
|
.description=${this.addon.hassio_role}
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.docker_api
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="docker_api"
|
||||||
|
icon="hassio:docker"
|
||||||
|
label="docker"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.host_pid
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="host_pid"
|
||||||
|
icon="hassio:pound"
|
||||||
|
label="host pid"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.apparmor
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
class=${this._computeApparmorClassName}
|
||||||
|
id="apparmor"
|
||||||
|
icon="hassio:shield"
|
||||||
|
label="apparmor"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.auth_api
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="auth_api"
|
||||||
|
icon="hassio:key"
|
||||||
|
label="auth"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.ingress
|
||||||
|
? html`
|
||||||
|
<ha-label-badge
|
||||||
|
@click=${this._showMoreInfo}
|
||||||
|
id="ingress"
|
||||||
|
icon="hassio:cursor-default-click-outline"
|
||||||
|
label="ingress"
|
||||||
|
description=""
|
||||||
|
></ha-label-badge>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.addon.version
|
||||||
|
? html`
|
||||||
|
<div class="state">
|
||||||
|
<div>Start on boot</div>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._startOnBootToggled}
|
||||||
|
.checked=${this.addon.boot === "auto"}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
<div class="state">
|
||||||
|
<div>Auto update</div>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._autoUpdateToggled}
|
||||||
|
.checked=${this.addon.auto_update}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
${this.addon.ingress
|
||||||
|
? html`
|
||||||
|
<div class="state">
|
||||||
|
<div>Show in sidebar</div>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._panelToggled}
|
||||||
|
.checked=${this.addon.ingress_panel}
|
||||||
|
.disabled=${this._computeCannotIngressSidebar}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
${this._computeCannotIngressSidebar
|
||||||
|
? html`
|
||||||
|
<span>
|
||||||
|
This option requires Home Assistant 0.92 or
|
||||||
|
later.
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._computeUsesProtectedOptions
|
||||||
|
? html`
|
||||||
|
<div class="state">
|
||||||
|
<div>
|
||||||
|
Protection mode
|
||||||
|
<span>
|
||||||
|
<iron-icon icon="hassio:information"></iron-icon>
|
||||||
|
<paper-tooltip>
|
||||||
|
Grant the add-on elevated system access.
|
||||||
|
</paper-tooltip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._protectionToggled}
|
||||||
|
.checked=${this.addon.protected}
|
||||||
|
haptic
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
${this.addon.version
|
||||||
|
? html`
|
||||||
|
<mwc-button class="warning" @click=${this._uninstallClicked}>
|
||||||
|
Uninstall
|
||||||
|
</mwc-button>
|
||||||
|
${this.addon.build
|
||||||
|
? html`
|
||||||
|
<ha-call-api-button
|
||||||
|
class="warning"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path="hassio/addons/${this.addon.slug}/rebuild"
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
</ha-call-api-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._computeIsRunning
|
||||||
|
? html`
|
||||||
|
<ha-call-api-button
|
||||||
|
class="warning"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path="hassio/addons/${this.addon.slug}/restart"
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</ha-call-api-button>
|
||||||
|
<ha-call-api-button
|
||||||
|
class="warning"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path="hassio/addons/${this.addon.slug}/stop"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</ha-call-api-button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-call-api-button
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path="hassio/addons/${this.addon.slug}/start"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</ha-call-api-button>
|
||||||
|
`}
|
||||||
|
${this._computeShowWebUI
|
||||||
|
? html`
|
||||||
|
<a
|
||||||
|
.href=${this._pathWebui}
|
||||||
|
tabindex="-1"
|
||||||
|
target="_blank"
|
||||||
|
class="right"
|
||||||
|
>
|
||||||
|
<mwc-button>
|
||||||
|
Open web UI
|
||||||
|
</mwc-button>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._computeShowIngressUI
|
||||||
|
? html`
|
||||||
|
<mwc-button class="right" @click=${this._openIngress}>
|
||||||
|
Open web UI
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
${!this.addon.available
|
||||||
|
? html`
|
||||||
|
<p class="warning">
|
||||||
|
This add-on is not available on your system.
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<ha-progress-button
|
||||||
|
.disabled=${!this.addon.available}
|
||||||
|
.progress=${this._installing}
|
||||||
|
@click=${this._installClicked}
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</ha-progress-button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
|
||||||
|
${this.addon.long_description
|
||||||
|
? html`
|
||||||
|
<paper-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-markdown
|
||||||
|
.content=${this.addon.long_description}
|
||||||
|
></ha-markdown>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
paper-card {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
paper-card.warning {
|
||||||
|
background-color: var(--google-red-500);
|
||||||
|
color: white;
|
||||||
|
--paper-card-header-color: white;
|
||||||
|
}
|
||||||
|
paper-card.warning mwc-button {
|
||||||
|
--mdc-theme-primary: white !important;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
--mdc-theme-primary: var(--google-red-500);
|
||||||
|
}
|
||||||
|
.light-color {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.addon-header {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--paper-card-header-color, --primary-text-color);
|
||||||
|
}
|
||||||
|
.addon-version {
|
||||||
|
float: right;
|
||||||
|
font-size: 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.logo img {
|
||||||
|
max-height: 60px;
|
||||||
|
margin: 16px 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
margin: 33px 0;
|
||||||
|
}
|
||||||
|
.state div {
|
||||||
|
width: 180px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.state iron-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
ha-switch {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
iron-icon.running {
|
||||||
|
color: var(--paper-green-400);
|
||||||
|
}
|
||||||
|
iron-icon.stopped {
|
||||||
|
color: var(--google-red-300);
|
||||||
|
}
|
||||||
|
ha-call-api-button {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
ha-markdown img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
protection-enable mwc-button {
|
||||||
|
--mdc-theme-primary: white;
|
||||||
|
}
|
||||||
|
.description a,
|
||||||
|
ha-markdown a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
--ha-label-badge-color: var(--label-badge-blue, #039be5);
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
--ha-label-badge-color: var(--label-badge-green, #0da035);
|
||||||
|
}
|
||||||
|
.yellow {
|
||||||
|
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
|
||||||
|
}
|
||||||
|
.security {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flow-root;
|
||||||
|
}
|
||||||
|
.security h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.security ha-label-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
|
--iron-icon-height: 45px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeHassioApi(): boolean {
|
||||||
|
return (
|
||||||
|
this.addon.hassio_api &&
|
||||||
|
(this.addon.hassio_role === "manager" ||
|
||||||
|
this.addon.hassio_role === "admin")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeApparmorClassName(): string {
|
||||||
|
if (this.addon.apparmor === "profile") {
|
||||||
|
return "green";
|
||||||
|
}
|
||||||
|
if (this.addon.apparmor === "disable") {
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showMoreInfo(ev): void {
|
||||||
|
const id = ev.target.getAttribute("id");
|
||||||
|
showHassioMarkdownDialog(this, {
|
||||||
|
title: PERMIS_DESC[id].title,
|
||||||
|
content: PERMIS_DESC[id].description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeIsRunning(): boolean {
|
||||||
|
return this.addon?.state === "started";
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeUpdateAvailable(): boolean | "" {
|
||||||
|
return (
|
||||||
|
this.addon &&
|
||||||
|
!this.addon.detached &&
|
||||||
|
this.addon.version &&
|
||||||
|
this.addon.version !== this.addon.last_version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _pathWebui(): string | null {
|
||||||
|
return (
|
||||||
|
this.addon.webui &&
|
||||||
|
this.addon.webui.replace("[HOST]", document.location.hostname)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeShowWebUI(): boolean | "" | null {
|
||||||
|
return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openIngress(): void {
|
||||||
|
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeShowIngressUI(): boolean {
|
||||||
|
return this.addon.ingress && this._computeIsRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeCannotIngressSidebar(): boolean {
|
||||||
|
return !this.addon.ingress || !this._computeHA92plus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeUsesProtectedOptions(): boolean {
|
||||||
|
return (
|
||||||
|
this.addon.docker_api || this.addon.full_access || this.addon.host_pid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _computeHA92plus(): boolean {
|
||||||
|
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||||
|
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startOnBootToggled(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
boot: this.addon.boot === "auto" ? "manual" : "auto",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _autoUpdateToggled(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
auto_update: !this.addon.auto_update,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _protectionToggled(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetSecurityParams = {
|
||||||
|
protected: !this.addon.protected,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonSecurity(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "security",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon security option, ${err.body?.message ||
|
||||||
|
err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _panelToggled(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
ingress_panel: !this.addon.ingress_panel,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon option, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openChangelog(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
const content = await fetchHassioAddonChangelog(
|
||||||
|
this.hass,
|
||||||
|
this.addon.slug
|
||||||
|
);
|
||||||
|
showHassioMarkdownDialog(this, {
|
||||||
|
title: "Changelog",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to get addon changelog, ${err.body?.message ||
|
||||||
|
err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _installClicked(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
this._installing = true;
|
||||||
|
try {
|
||||||
|
await installHassioAddon(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "install",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to install addon, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
this._installing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _uninstallClicked(): Promise<void> {
|
||||||
|
if (!confirm("Are you sure you want to uninstall this add-on?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
await uninstallHassioAddon(this.hass, this.addon.slug);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "uninstall",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-info": HassioAddonInfo;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,66 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
|
||||||
|
|
||||||
import "../../../src/resources/ha-style";
|
|
||||||
|
|
||||||
class HassioAddonLogs extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host,
|
|
||||||
paper-card {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
${ANSI_HTML_STYLE}
|
|
||||||
<paper-card heading="Log">
|
|
||||||
<div class="card-content" id="content"></div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<mwc-button on-click="refresh">Refresh</mwc-button>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addonSlug: {
|
|
||||||
type: String,
|
|
||||||
observer: "addonSlugChanged",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addonSlugChanged(slug) {
|
|
||||||
if (!this.hass) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.addonChanged(slug);
|
|
||||||
}, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.hass
|
|
||||||
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
|
|
||||||
.then((text) => {
|
|
||||||
while (this.$.content.lastChild) {
|
|
||||||
this.$.content.removeChild(this.$.content.lastChild);
|
|
||||||
}
|
|
||||||
this.$.content.appendChild(parseTextToColoredPre(text));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addon-logs", HassioAddonLogs);
|
|
95
hassio/src/addon-view/hassio-addon-logs.ts
Normal file
95
hassio/src/addon-view/hassio-addon-logs.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
fetchHassioAddonLogs,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
|
||||||
|
@customElement("hassio-addon-logs")
|
||||||
|
class HassioAddonLogs extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addon!: HassioAddonDetails;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@query("#content") private _logContent!: any;
|
||||||
|
|
||||||
|
public async connectedCallback(): Promise<void> {
|
||||||
|
super.connectedCallback();
|
||||||
|
await this._loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<paper-card heading="Log">
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="card-content" id="content"></div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
ANSI_HTML_STYLE,
|
||||||
|
css`
|
||||||
|
:host,
|
||||||
|
paper-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadData(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
const content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||||
|
while (this._logContent.lastChild) {
|
||||||
|
this._logContent.removeChild(this._logContent.lastChild as Node);
|
||||||
|
}
|
||||||
|
this._logContent.appendChild(parseTextToColoredPre(content));
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _refresh(): Promise<void> {
|
||||||
|
await this._loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-logs": HassioAddonLogs;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,129 +0,0 @@
|
|||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import "@polymer/paper-input/paper-input";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
|
||||||
import "../../../src/resources/ha-style";
|
|
||||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
|
||||||
|
|
||||||
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style">
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
paper-card {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
@apply --layout;
|
|
||||||
@apply --layout-justified;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<paper-card heading="Network">
|
|
||||||
<div class="card-content">
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<div class="errors">[[error]]</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>Container</th>
|
|
||||||
<th>Host</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
<template is="dom-repeat" items="[[config]]">
|
|
||||||
<tr>
|
|
||||||
<td>[[item.container]]</td>
|
|
||||||
<td>
|
|
||||||
<paper-input
|
|
||||||
placeholder="disabled"
|
|
||||||
value="{{item.host}}"
|
|
||||||
no-label-float=""
|
|
||||||
></paper-input>
|
|
||||||
</td>
|
|
||||||
<td>[[item.description]]</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<ha-call-api-button
|
|
||||||
class="warning"
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/addons/[[addonSlug]]/options"
|
|
||||||
data="[[resetData]]"
|
|
||||||
>Reset to defaults</ha-call-api-button
|
|
||||||
>
|
|
||||||
<mwc-button on-click="saveTapped">Save</mwc-button>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addonSlug: String,
|
|
||||||
config: Object,
|
|
||||||
addon: {
|
|
||||||
type: Object,
|
|
||||||
observer: "addonChanged",
|
|
||||||
},
|
|
||||||
error: String,
|
|
||||||
resetData: {
|
|
||||||
type: Object,
|
|
||||||
value: {
|
|
||||||
network: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addonChanged(addon) {
|
|
||||||
if (!addon) return;
|
|
||||||
|
|
||||||
const network = addon.network || {};
|
|
||||||
const description = addon.network_description || {};
|
|
||||||
const items = Object.keys(network).map((key) => ({
|
|
||||||
container: key,
|
|
||||||
host: network[key],
|
|
||||||
description: description[key],
|
|
||||||
}));
|
|
||||||
this.config = items.sort(function(el1, el2) {
|
|
||||||
return el1.host - el2.host;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTapped() {
|
|
||||||
this.error = null;
|
|
||||||
const data = {};
|
|
||||||
this.config.forEach(function(item) {
|
|
||||||
data[item.container] = parseInt(item.host);
|
|
||||||
});
|
|
||||||
const path = `hassio/addons/${this.addonSlug}/options`;
|
|
||||||
|
|
||||||
this.hass
|
|
||||||
.callApi("post", path, {
|
|
||||||
network: data,
|
|
||||||
})
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
this.fire("hass-api-called", { success: true, path: path });
|
|
||||||
},
|
|
||||||
(resp) => {
|
|
||||||
this.error = resp.body.message;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addon-network", HassioAddonNetwork);
|
|
202
hassio/src/addon-view/hassio-addon-network.ts
Normal file
202
hassio/src/addon-view/hassio-addon-network.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
|
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
HassioAddonSetOptionParams,
|
||||||
|
setHassioAddonOption,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
|
||||||
|
interface NetworkItem {
|
||||||
|
description: string;
|
||||||
|
container: string;
|
||||||
|
host: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetworkItemInput extends PaperInputElement {
|
||||||
|
container: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("hassio-addon-network")
|
||||||
|
class HassioAddonNetwork extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addon!: HassioAddonDetails;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@property() private _config?: NetworkItem[];
|
||||||
|
|
||||||
|
public connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setNetworkConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._config) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<paper-card heading="Network">
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="errors">${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Container</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
${this._config!.map((item) => {
|
||||||
|
return html`
|
||||||
|
<tr>
|
||||||
|
<td>${item.container}</td>
|
||||||
|
<td>
|
||||||
|
<paper-input
|
||||||
|
@value-changed=${this._configChanged}
|
||||||
|
placeholder="disabled"
|
||||||
|
.value=${item.host}
|
||||||
|
.container=${item.container}
|
||||||
|
no-label-float
|
||||||
|
></paper-input>
|
||||||
|
</td>
|
||||||
|
<td>${item.description}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<mwc-button class="warning" @click=${this._resetTapped}>
|
||||||
|
Reset to defaults
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
paper-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(changedProperties: PropertyValues): void {
|
||||||
|
super.update(changedProperties);
|
||||||
|
if (changedProperties.has("addon")) {
|
||||||
|
this._setNetworkConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setNetworkConfig(): void {
|
||||||
|
const network = this.addon.network || {};
|
||||||
|
const description = this.addon.network_description || {};
|
||||||
|
const items: NetworkItem[] = Object.keys(network).map((key) => {
|
||||||
|
return {
|
||||||
|
container: key,
|
||||||
|
host: network[key],
|
||||||
|
description: description[key],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _configChanged(ev: Event): Promise<void> {
|
||||||
|
const target = ev.target as NetworkItemInput;
|
||||||
|
this._config!.forEach((item) => {
|
||||||
|
if (
|
||||||
|
item.container === target.container &&
|
||||||
|
item.host !== parseInt(String(target.value), 10)
|
||||||
|
) {
|
||||||
|
item.host = target.value ? parseInt(String(target.value), 10) : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _resetTapped(): Promise<void> {
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
network: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon network configuration, ${err.body
|
||||||
|
?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveTapped(): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const networkconfiguration = {};
|
||||||
|
this._config!.forEach((item) => {
|
||||||
|
networkconfiguration[item.container] = parseInt(String(item.host), 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: HassioAddonSetOptionParams = {
|
||||||
|
network: networkconfiguration,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "option",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} catch (err) {
|
||||||
|
this._error = `Failed to set addon network configuration, ${err.body
|
||||||
|
?.message || err}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-network": HassioAddonNetwork;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,139 +0,0 @@
|
|||||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
|
||||||
import "@polymer/app-layout/app-header/app-header";
|
|
||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "./hassio-addon-audio";
|
|
||||||
import "./hassio-addon-config";
|
|
||||||
import "./hassio-addon-info";
|
|
||||||
import "./hassio-addon-logs";
|
|
||||||
import "./hassio-addon-network";
|
|
||||||
|
|
||||||
class HassioAddonView extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
--paper-card-header-color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: 24px 0 32px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
hassio-addon-info,
|
|
||||||
hassio-addon-network,
|
|
||||||
hassio-addon-audio,
|
|
||||||
hassio-addon-config {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
hassio-addon-logs {
|
|
||||||
max-width: calc(100% - 8px);
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 600px) {
|
|
||||||
hassio-addon-info,
|
|
||||||
hassio-addon-network,
|
|
||||||
hassio-addon-audio,
|
|
||||||
hassio-addon-config,
|
|
||||||
hassio-addon-logs {
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<hass-subpage header="Hass.io: add-on details" hassio>
|
|
||||||
<div class="content">
|
|
||||||
<hassio-addon-info
|
|
||||||
hass="[[hass]]"
|
|
||||||
addon="[[addon]]"
|
|
||||||
addon-slug="[[addonSlug]]"
|
|
||||||
></hassio-addon-info>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[addon.version]]">
|
|
||||||
<hassio-addon-config
|
|
||||||
hass="[[hass]]"
|
|
||||||
addon="[[addon]]"
|
|
||||||
addon-slug="[[addonSlug]]"
|
|
||||||
></hassio-addon-config>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[addon.audio]]">
|
|
||||||
<hassio-addon-audio
|
|
||||||
hass="[[hass]]"
|
|
||||||
addon="[[addon]]"
|
|
||||||
></hassio-addon-audio>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[addon.network]]">
|
|
||||||
<hassio-addon-network
|
|
||||||
hass="[[hass]]"
|
|
||||||
addon="[[addon]]"
|
|
||||||
addon-slug="[[addonSlug]]"
|
|
||||||
></hassio-addon-network>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<hassio-addon-logs
|
|
||||||
hass="[[hass]]"
|
|
||||||
addon-slug="[[addonSlug]]"
|
|
||||||
></hassio-addon-logs>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</hass-subpage>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
route: {
|
|
||||||
type: Object,
|
|
||||||
observer: "routeDataChanged",
|
|
||||||
},
|
|
||||||
addonSlug: {
|
|
||||||
type: String,
|
|
||||||
computed: "_computeSlug(route)",
|
|
||||||
},
|
|
||||||
addon: Object,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
|
||||||
}
|
|
||||||
|
|
||||||
apiCalled(ev) {
|
|
||||||
const path = ev.detail.path;
|
|
||||||
|
|
||||||
if (!path) return;
|
|
||||||
|
|
||||||
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
|
|
||||||
history.back();
|
|
||||||
} else {
|
|
||||||
this.routeDataChanged(this.route);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
routeDataChanged(routeData) {
|
|
||||||
const addon = routeData.path.substr(1);
|
|
||||||
this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
|
|
||||||
(info) => {
|
|
||||||
this.addon = info.data;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.addon = null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_computeSlug(route) {
|
|
||||||
return route.path.substr(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addon-view", HassioAddonView);
|
|
159
hassio/src/addon-view/hassio-addon-view.ts
Normal file
159
hassio/src/addon-view/hassio-addon-view.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||||
|
import "@polymer/app-layout/app-header/app-header";
|
||||||
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
|
import {
|
||||||
|
HassioAddonDetails,
|
||||||
|
fetchHassioAddonInfo,
|
||||||
|
} from "../../../src/data/hassio/addon";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
|
||||||
|
import "./hassio-addon-audio";
|
||||||
|
import "./hassio-addon-config";
|
||||||
|
import "./hassio-addon-info";
|
||||||
|
import "./hassio-addon-logs";
|
||||||
|
import "./hassio-addon-network";
|
||||||
|
|
||||||
|
@customElement("hassio-addon-view")
|
||||||
|
class HassioAddonView extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public route!: Route;
|
||||||
|
@property() public addon?: HassioAddonDetails;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.addon) {
|
||||||
|
return html`
|
||||||
|
<paper-spinner-lite active></paper-spinner-lite>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<hass-subpage header="Hass.io: add-on details" hassio>
|
||||||
|
<div class="content">
|
||||||
|
<hassio-addon-info
|
||||||
|
.hass=${this.hass}
|
||||||
|
.addon=${this.addon}
|
||||||
|
></hassio-addon-info>
|
||||||
|
|
||||||
|
${this.addon && this.addon.version
|
||||||
|
? html`
|
||||||
|
<hassio-addon-config
|
||||||
|
.hass=${this.hass}
|
||||||
|
.addon=${this.addon}
|
||||||
|
></hassio-addon-config>
|
||||||
|
|
||||||
|
${this.addon.audio
|
||||||
|
? html`
|
||||||
|
<hassio-addon-audio
|
||||||
|
.hass=${this.hass}
|
||||||
|
.addon=${this.addon}
|
||||||
|
></hassio-addon-audio>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.addon.network
|
||||||
|
? html`
|
||||||
|
<hassio-addon-network
|
||||||
|
.hass=${this.hass}
|
||||||
|
.addon=${this.addon}
|
||||||
|
></hassio-addon-network>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<hassio-addon-logs
|
||||||
|
.hass=${this.hass}
|
||||||
|
.addon=${this.addon}
|
||||||
|
></hassio-addon-logs>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
--paper-card-header-color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 24px 0 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
hassio-addon-info,
|
||||||
|
hassio-addon-network,
|
||||||
|
hassio-addon-audio,
|
||||||
|
hassio-addon-config {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
hassio-addon-logs {
|
||||||
|
max-width: calc(100% - 8px);
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
hassio-addon-info,
|
||||||
|
hassio-addon-network,
|
||||||
|
hassio-addon-audio,
|
||||||
|
hassio-addon-config,
|
||||||
|
hassio-addon-logs {
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async firstUpdated(): Promise<void> {
|
||||||
|
await this._routeDataChanged(this.route);
|
||||||
|
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _apiCalled(ev): Promise<void> {
|
||||||
|
const path: string = ev.detail.path;
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === "uninstall") {
|
||||||
|
history.back();
|
||||||
|
} else {
|
||||||
|
await this._routeDataChanged(this.route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _routeDataChanged(routeData: Route): Promise<void> {
|
||||||
|
const addon = routeData.path.substr(1);
|
||||||
|
try {
|
||||||
|
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||||
|
this.addon = addoninfo;
|
||||||
|
} catch {
|
||||||
|
this.addon = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addon-view": HassioAddonView;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,68 +1,75 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
import { css } from "lit-element";
|
||||||
|
|
||||||
export const ANSI_HTML_STYLE = html`
|
interface State {
|
||||||
<style>
|
bold: boolean;
|
||||||
.bold {
|
italic: boolean;
|
||||||
font-weight: bold;
|
underline: boolean;
|
||||||
}
|
strikethrough: boolean;
|
||||||
.italic {
|
foregroundColor: null | string;
|
||||||
font-style: italic;
|
backgroundColor: null | string;
|
||||||
}
|
}
|
||||||
.underline {
|
|
||||||
text-decoration: underline;
|
export const ANSI_HTML_STYLE = css`
|
||||||
}
|
.bold {
|
||||||
.strikethrough {
|
font-weight: bold;
|
||||||
text-decoration: line-through;
|
}
|
||||||
}
|
.italic {
|
||||||
.underline.strikethrough {
|
font-style: italic;
|
||||||
text-decoration: underline line-through;
|
}
|
||||||
}
|
.underline {
|
||||||
.fg-red {
|
text-decoration: underline;
|
||||||
color: rgb(222, 56, 43);
|
}
|
||||||
}
|
.strikethrough {
|
||||||
.fg-green {
|
text-decoration: line-through;
|
||||||
color: rgb(57, 181, 74);
|
}
|
||||||
}
|
.underline.strikethrough {
|
||||||
.fg-yellow {
|
text-decoration: underline line-through;
|
||||||
color: rgb(255, 199, 6);
|
}
|
||||||
}
|
.fg-red {
|
||||||
.fg-blue {
|
color: rgb(222, 56, 43);
|
||||||
color: rgb(0, 111, 184);
|
}
|
||||||
}
|
.fg-green {
|
||||||
.fg-magenta {
|
color: rgb(57, 181, 74);
|
||||||
color: rgb(118, 38, 113);
|
}
|
||||||
}
|
.fg-yellow {
|
||||||
.fg-cyan {
|
color: rgb(255, 199, 6);
|
||||||
color: rgb(44, 181, 233);
|
}
|
||||||
}
|
.fg-blue {
|
||||||
.fg-white {
|
color: rgb(0, 111, 184);
|
||||||
color: rgb(204, 204, 204);
|
}
|
||||||
}
|
.fg-magenta {
|
||||||
.bg-black {
|
color: rgb(118, 38, 113);
|
||||||
background-color: rgb(0, 0, 0);
|
}
|
||||||
}
|
.fg-cyan {
|
||||||
.bg-red {
|
color: rgb(44, 181, 233);
|
||||||
background-color: rgb(222, 56, 43);
|
}
|
||||||
}
|
.fg-white {
|
||||||
.bg-green {
|
color: rgb(204, 204, 204);
|
||||||
background-color: rgb(57, 181, 74);
|
}
|
||||||
}
|
.bg-black {
|
||||||
.bg-yellow {
|
background-color: rgb(0, 0, 0);
|
||||||
background-color: rgb(255, 199, 6);
|
}
|
||||||
}
|
.bg-red {
|
||||||
.bg-blue {
|
background-color: rgb(222, 56, 43);
|
||||||
background-color: rgb(0, 111, 184);
|
}
|
||||||
}
|
.bg-green {
|
||||||
.bg-magenta {
|
background-color: rgb(57, 181, 74);
|
||||||
background-color: rgb(118, 38, 113);
|
}
|
||||||
}
|
.bg-yellow {
|
||||||
.bg-cyan {
|
background-color: rgb(255, 199, 6);
|
||||||
background-color: rgb(44, 181, 233);
|
}
|
||||||
}
|
.bg-blue {
|
||||||
.bg-white {
|
background-color: rgb(0, 111, 184);
|
||||||
background-color: rgb(204, 204, 204);
|
}
|
||||||
}
|
.bg-magenta {
|
||||||
</style>
|
background-color: rgb(118, 38, 113);
|
||||||
|
}
|
||||||
|
.bg-cyan {
|
||||||
|
background-color: rgb(44, 181, 233);
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: rgb(204, 204, 204);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function parseTextToColoredPre(text) {
|
export function parseTextToColoredPre(text) {
|
||||||
@@ -70,7 +77,7 @@ export function parseTextToColoredPre(text) {
|
|||||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
const state = {
|
const state: State = {
|
||||||
bold: false,
|
bold: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
underline: false,
|
underline: false,
|
||||||
@@ -81,29 +88,42 @@ export function parseTextToColoredPre(text) {
|
|||||||
|
|
||||||
const addSpan = (content) => {
|
const addSpan = (content) => {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
if (state.bold) span.classList.add("bold");
|
if (state.bold) {
|
||||||
if (state.italic) span.classList.add("italic");
|
span.classList.add("bold");
|
||||||
if (state.underline) span.classList.add("underline");
|
}
|
||||||
if (state.strikethrough) span.classList.add("strikethrough");
|
if (state.italic) {
|
||||||
if (state.foregroundColor !== null)
|
span.classList.add("italic");
|
||||||
|
}
|
||||||
|
if (state.underline) {
|
||||||
|
span.classList.add("underline");
|
||||||
|
}
|
||||||
|
if (state.strikethrough) {
|
||||||
|
span.classList.add("strikethrough");
|
||||||
|
}
|
||||||
|
if (state.foregroundColor !== null) {
|
||||||
span.classList.add(`fg-${state.foregroundColor}`);
|
span.classList.add(`fg-${state.foregroundColor}`);
|
||||||
if (state.backgroundColor !== null)
|
}
|
||||||
|
if (state.backgroundColor !== null) {
|
||||||
span.classList.add(`bg-${state.backgroundColor}`);
|
span.classList.add(`bg-${state.backgroundColor}`);
|
||||||
|
}
|
||||||
span.appendChild(document.createTextNode(content));
|
span.appendChild(document.createTextNode(content));
|
||||||
pre.appendChild(span);
|
pre.appendChild(span);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable no-cond-assign */
|
/* eslint-disable no-cond-assign */
|
||||||
let match;
|
let match;
|
||||||
|
// tslint:disable-next-line
|
||||||
while ((match = re.exec(text)) !== null) {
|
while ((match = re.exec(text)) !== null) {
|
||||||
const j = match.index;
|
const j = match!.index;
|
||||||
addSpan(text.substring(i, j));
|
addSpan(text.substring(i, j));
|
||||||
i = j + match[0].length;
|
i = j + match[0].length;
|
||||||
|
|
||||||
if (match[1] === undefined) continue;
|
if (match[1] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match[1].split(";").forEach((colorCode) => {
|
match[1].split(";").forEach((colorCode: string) => {
|
||||||
switch (parseInt(colorCode)) {
|
switch (parseInt(colorCode, 10)) {
|
||||||
case 0:
|
case 0:
|
||||||
// reset
|
// reset
|
||||||
state.bold = false;
|
state.bold = false;
|
@@ -17,21 +17,40 @@ class HassioCardContent extends LitElement {
|
|||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
@property() public title!: string;
|
@property() public title!: string;
|
||||||
@property() public description?: string;
|
@property() public description?: string;
|
||||||
@property({ type: Boolean }) public available?: boolean;
|
@property({ type: Boolean }) public available: boolean = true;
|
||||||
|
@property({ type: Boolean }) public showTopbar: boolean = false;
|
||||||
|
@property() public topbarClass?: string;
|
||||||
@property() public datetime?: string;
|
@property() public datetime?: string;
|
||||||
@property() public iconTitle?: string;
|
@property() public iconTitle?: string;
|
||||||
@property() public iconClass?: string;
|
@property() public iconClass?: string;
|
||||||
@property() public icon = "hass:help-circle";
|
@property() public icon = "hass:help-circle";
|
||||||
|
@property() public iconImage?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<iron-icon
|
${this.showTopbar
|
||||||
class=${this.iconClass}
|
? html`
|
||||||
.icon=${this.icon}
|
<div class="topbar ${this.topbarClass}"></div>
|
||||||
.title=${this.iconTitle}
|
`
|
||||||
></iron-icon>
|
: ""}
|
||||||
|
${this.iconImage
|
||||||
|
? html`
|
||||||
|
<div class="icon_image ${this.iconClass}">
|
||||||
|
<img src="${this.iconImage}" title="${this.iconTitle}" />
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<iron-icon
|
||||||
|
class=${this.iconClass}
|
||||||
|
.icon=${this.icon}
|
||||||
|
.title=${this.iconTitle}
|
||||||
|
></iron-icon>
|
||||||
|
`}
|
||||||
<div>
|
<div>
|
||||||
<div class="title">${this.title}</div>
|
<div class="title">
|
||||||
|
${this.title}
|
||||||
|
</div>
|
||||||
<div class="addition">
|
<div class="addition">
|
||||||
${this.description}
|
${this.description}
|
||||||
${/* treat as available when undefined */
|
${/* treat as available when undefined */
|
||||||
@@ -53,8 +72,9 @@ class HassioCardContent extends LitElement {
|
|||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
iron-icon {
|
iron-icon {
|
||||||
margin-right: 16px;
|
margin-right: 24px;
|
||||||
margin-top: 16px;
|
margin-left: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
float: left;
|
float: left;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
@@ -88,6 +108,44 @@ class HassioCardContent extends LitElement {
|
|||||||
ha-relative-time {
|
ha-relative-time {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.icon_image img {
|
||||||
|
max-height: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-right: 16px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.icon_image.stopped,
|
||||||
|
.icon_image.not_available {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--paper-orange-400);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.topbar.installed {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.topbar.update {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.topbar.unavailable {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { HassioAddonInfo } from "../../../src/data/hassio";
|
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||||
import * as Fuse from "fuse.js";
|
import * as Fuse from "fuse.js";
|
||||||
|
|
||||||
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
|
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
|
||||||
|
@@ -16,7 +16,7 @@ import "@material/mwc-button";
|
|||||||
class HassioSearchInput extends LitElement {
|
class HassioSearchInput extends LitElement {
|
||||||
@property() private filter?: string;
|
@property() private filter?: string;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<paper-input
|
<paper-input
|
||||||
|
@@ -1,92 +0,0 @@
|
|||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../components/hassio-card-content";
|
|
||||||
import "../resources/hassio-style";
|
|
||||||
import NavigateMixin from "../../../src/mixins/navigate-mixin";
|
|
||||||
|
|
||||||
class HassioAddons extends NavigateMixin(PolymerElement) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style hassio-style">
|
|
||||||
paper-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="content card-group">
|
|
||||||
<div class="title">Add-ons</div>
|
|
||||||
<template is="dom-if" if="[[!addons.length]]">
|
|
||||||
<paper-card>
|
|
||||||
<div class="card-content">
|
|
||||||
You don't have any add-ons installed yet. Head over to
|
|
||||||
<a href="#" on-click="openStore">the add-on store</a> to get
|
|
||||||
started!
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-repeat"
|
|
||||||
items="[[addons]]"
|
|
||||||
as="addon"
|
|
||||||
sort="sortAddons"
|
|
||||||
>
|
|
||||||
<paper-card on-click="addonTapped">
|
|
||||||
<div class="card-content">
|
|
||||||
<hassio-card-content
|
|
||||||
hass="[[hass]]"
|
|
||||||
title="[[addon.name]]"
|
|
||||||
description="[[addon.description]]"
|
|
||||||
available="[[addon.available]]"
|
|
||||||
icon="[[computeIcon(addon)]]"
|
|
||||||
icon-title="[[computeIconTitle(addon)]]"
|
|
||||||
icon-class="[[computeIconClass(addon)]]"
|
|
||||||
></hassio-card-content>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
addons: Array,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
sortAddons(a, b) {
|
|
||||||
return a.name < b.name ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeIcon(addon) {
|
|
||||||
return addon.installed !== addon.version
|
|
||||||
? "hassio:arrow-up-bold-circle"
|
|
||||||
: "hassio:puzzle";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeIconTitle(addon) {
|
|
||||||
if (addon.installed !== addon.version) return "New version available";
|
|
||||||
return addon.state === "started"
|
|
||||||
? "Add-on is running"
|
|
||||||
: "Add-on is stopped";
|
|
||||||
}
|
|
||||||
|
|
||||||
computeIconClass(addon) {
|
|
||||||
if (addon.installed !== addon.version) return "update";
|
|
||||||
return addon.state === "started" ? "running" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
addonTapped(ev) {
|
|
||||||
this.navigate("/hassio/addon/" + ev.model.addon.slug);
|
|
||||||
ev.target.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
openStore(ev) {
|
|
||||||
this.navigate("/hassio/store");
|
|
||||||
ev.target.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-addons", HassioAddons);
|
|
109
hassio/src/dashboard/hassio-addons.ts
Normal file
109
hassio/src/dashboard/hassio-addons.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||||
|
import { navigate } from "../../../src/common/navigate";
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
import "../components/hassio-card-content";
|
||||||
|
|
||||||
|
@customElement("hassio-addons")
|
||||||
|
class HassioAddons extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public addons?: HassioAddonInfo[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||||
|
const ha105pluss =
|
||||||
|
Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
||||||
|
return html`
|
||||||
|
<div class="content">
|
||||||
|
<h1>Add-ons</h1>
|
||||||
|
<div class="card-group">
|
||||||
|
${!this.addons
|
||||||
|
? html`
|
||||||
|
<paper-card>
|
||||||
|
<div class="card-content">
|
||||||
|
You don't have any add-ons installed yet. Head over to
|
||||||
|
<a href="#" @click=${this._openStore}>the add-on store</a>
|
||||||
|
to get started!
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
: this.addons
|
||||||
|
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||||
|
.map(
|
||||||
|
(addon) => html`
|
||||||
|
<paper-card .addon=${addon} @click=${this._addonTapped}>
|
||||||
|
<div class="card-content">
|
||||||
|
<hassio-card-content
|
||||||
|
.hass=${this.hass}
|
||||||
|
.title=${addon.name}
|
||||||
|
.description=${addon.description}
|
||||||
|
available
|
||||||
|
.showTopbar=${addon.installed !== addon.version}
|
||||||
|
topbarClass="update"
|
||||||
|
.icon=${addon.installed !== addon.version
|
||||||
|
? "hassio:arrow-up-bold-circle"
|
||||||
|
: "hassio:puzzle"}
|
||||||
|
.iconTitle=${addon.state !== "started"
|
||||||
|
? "Add-on is stopped"
|
||||||
|
: addon.installed !== addon.version
|
||||||
|
? "New version available"
|
||||||
|
: "Add-on is running"}
|
||||||
|
.iconClass=${addon.installed &&
|
||||||
|
addon.installed !== addon.version
|
||||||
|
? addon.state === "started"
|
||||||
|
? "update"
|
||||||
|
: "update stopped"
|
||||||
|
: addon.installed && addon.state === "started"
|
||||||
|
? "running"
|
||||||
|
: "stopped"}
|
||||||
|
.iconImage=${ha105pluss && addon.icon
|
||||||
|
? `/api/hassio/addons/${addon.slug}/icon`
|
||||||
|
: undefined}
|
||||||
|
></hassio-card-content>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
paper-card {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addonTapped(ev: any): void {
|
||||||
|
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openStore(): void {
|
||||||
|
navigate(this, "/hassio/store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hassio-addons": HassioAddons;
|
||||||
|
}
|
||||||
|
}
|
@@ -8,26 +8,31 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "./hassio-addons";
|
import "./hassio-addons";
|
||||||
import "./hassio-hass-update";
|
import "./hassio-update";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant } from "../../../src/types";
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
HassioHomeAssistantInfo,
|
HassioHomeAssistantInfo,
|
||||||
} from "../../../src/data/hassio";
|
} from "../../../src/data/hassio/supervisor";
|
||||||
|
|
||||||
@customElement("hassio-dashboard")
|
@customElement("hassio-dashboard")
|
||||||
class HassioDashboard extends LitElement {
|
class HassioDashboard extends LitElement {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||||
|
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<hassio-hass-update
|
<hassio-update
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.hassInfo=${this.hassInfo}
|
.hassInfo=${this.hassInfo}
|
||||||
></hassio-hass-update>
|
.supervisorInfo=${this.supervisorInfo}
|
||||||
|
.hassOsInfo=${this.hassOsInfo}
|
||||||
|
></hassio-update>
|
||||||
<hassio-addons
|
<hassio-addons
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.addons=${this.supervisorInfo.addons}
|
.addons=${this.supervisorInfo.addons}
|
||||||
@@ -36,12 +41,15 @@ class HassioDashboard extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult[] {
|
||||||
return css`
|
return [
|
||||||
.content {
|
haStyle,
|
||||||
margin: 0 auto;
|
css`
|
||||||
}
|
.content {
|
||||||
`;
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,96 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "@polymer/paper-card/paper-card";
|
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../src/components/buttons/ha-call-api-button";
|
|
||||||
import "../components/hassio-card-content";
|
|
||||||
import "../resources/hassio-style";
|
|
||||||
|
|
||||||
class HassioHassUpdate extends PolymerElement {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style include="ha-style hassio-style">
|
|
||||||
paper-card {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
.errors {
|
|
||||||
color: var(--google-red-500);
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
|
|
||||||
<div class="content">
|
|
||||||
<div class="card-group">
|
|
||||||
<paper-card heading="Update available! 🎉">
|
|
||||||
<div class="card-content">
|
|
||||||
Home Assistant [[hassInfo.last_version]] is available and you
|
|
||||||
are currently running Home Assistant [[hassInfo.version]].
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<div class="error">Error: [[error]]</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<ha-call-api-button
|
|
||||||
hass="[[hass]]"
|
|
||||||
path="hassio/homeassistant/update"
|
|
||||||
>Update</ha-call-api-button
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="[[computeReleaseNotesUrl(hassInfo.version)]]"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<mwc-button>Release notes</mwc-button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</paper-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: Object,
|
|
||||||
hassInfo: Object,
|
|
||||||
error: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
|
||||||
}
|
|
||||||
|
|
||||||
apiCalled(ev) {
|
|
||||||
if (ev.detail.success) {
|
|
||||||
this.errors = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = ev.detail.response;
|
|
||||||
|
|
||||||
if (typeof response.body === "object") {
|
|
||||||
this.errors = response.body.message || "Unknown error";
|
|
||||||
} else {
|
|
||||||
this.errors = response.body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeUpdateAvailable(hassInfo) {
|
|
||||||
return hassInfo.version !== hassInfo.last_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeReleaseNotesUrl(version) {
|
|
||||||
return `https://${
|
|
||||||
version.includes("b") ? "rc" : "www"
|
|
||||||
}.home-assistant.io/latest-release-notes/`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hassio-hass-update", HassioHassUpdate);
|
|
188
hassio/src/dashboard/hassio-update.ts
Normal file
188
hassio/src/dashboard/hassio-update.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
property,
|
||||||
|
customElement,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@polymer/iron-icon/iron-icon";
|
||||||
|
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||||
|
import {
|
||||||
|
HassioHomeAssistantInfo,
|
||||||
|
HassioSupervisorInfo,
|
||||||
|
} from "../../../src/data/hassio/supervisor";
|
||||||
|
|
||||||
|
import { hassioStyle } from "../resources/hassio-style";
|
||||||
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
|
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-card/paper-card";
|
||||||
|
import "../../../src/components/buttons/ha-call-api-button";
|
||||||
|
import "../components/hassio-card-content";
|
||||||
|
|
||||||
|
@customElement("hassio-update")
|
||||||
|
export class HassioUpdate extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public hassInfo: HassioHomeAssistantInfo;
|
||||||
|
@property() public hassOsInfo?: HassioHassOSInfo;
|
||||||
|
@property() public supervisorInfo: HassioSupervisorInfo;
|
||||||
|
@property() private _error?: string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const updatesAvailable: number = [
|
||||||
|
this.hassInfo,
|
||||||
|
this.supervisorInfo,
|
||||||
|
this.hassOsInfo,
|
||||||
|
].filter((value) => {
|
||||||
|
return (
|
||||||
|
!!value &&
|
||||||
|
(value.last_version
|
||||||
|
? value.version !== value.last_version
|
||||||
|
: value.version !== value.version_latest)
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (!updatesAvailable) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="content">
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<div class="error">Error: ${this._error}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<h1>
|
||||||
|
${updatesAvailable > 1
|
||||||
|
? "Updates Available 🎉"
|
||||||
|
: "Update Available 🎉"}
|
||||||
|
</h1>
|
||||||
|
<div class="card-group">
|
||||||
|
${this._renderUpdateCard(
|
||||||
|
"Home Assistant Core",
|
||||||
|
this.hassInfo.version,
|
||||||
|
this.hassInfo.last_version,
|
||||||
|
"hassio/homeassistant/update",
|
||||||
|
`https://${
|
||||||
|
this.hassInfo.last_version.includes("b") ? "rc" : "www"
|
||||||
|
}.home-assistant.io/latest-release-notes/`,
|
||||||
|
"hassio:home-assistant"
|
||||||
|
)}
|
||||||
|
${this._renderUpdateCard(
|
||||||
|
"Supervisor",
|
||||||
|
this.supervisorInfo.version,
|
||||||
|
this.supervisorInfo.last_version,
|
||||||
|
"hassio/supervisor/update",
|
||||||
|
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
|
||||||
|
)}
|
||||||
|
${this.hassOsInfo
|
||||||
|
? this._renderUpdateCard(
|
||||||
|
"Operating System",
|
||||||
|
this.hassOsInfo.version,
|
||||||
|
this.hassOsInfo.version_latest,
|
||||||
|
"hassio/hassos/update",
|
||||||
|
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderUpdateCard(
|
||||||
|
name: string,
|
||||||
|
curVersion: string,
|
||||||
|
lastVersion: string,
|
||||||
|
apiPath: string,
|
||||||
|
releaseNotesUrl: string,
|
||||||
|
icon?: string
|
||||||
|
): TemplateResult {
|
||||||
|
if (lastVersion === curVersion) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<paper-card>
|
||||||
|
<div class="card-content">
|
||||||
|
${icon
|
||||||
|
? html`
|
||||||
|
<div class="icon">
|
||||||
|
<iron-icon .icon="${icon}" />
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="update-heading">${name} ${lastVersion}</div>
|
||||||
|
<div class="warning">
|
||||||
|
You are currently running version ${curVersion}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="${releaseNotesUrl}" target="_blank">
|
||||||
|
<mwc-button>Release notes</mwc-button>
|
||||||
|
</a>
|
||||||
|
<ha-call-api-button
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path=${apiPath}
|
||||||
|
@hass-api-called=${this._apiCalled}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ha-call-api-button>
|
||||||
|
</div>
|
||||||
|
</paper-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _apiCalled(ev) {
|
||||||
|
if (ev.detail.success) {
|
||||||
|
this._error = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = ev.detail.response;
|
||||||
|
|
||||||
|
typeof response.body === "object"
|
||||||
|
? (this._error = response.body.message || "Unknown error")
|
||||||
|
: (this._error = response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
|
.icon {
|
||||||
|
--iron-icon-height: 48px;
|
||||||
|
--iron-icon-width: 48px;
|
||||||
|
float: right;
|
||||||
|
margin: 0 0 2px 10px;
|
||||||
|
}
|
||||||
|
.update-heading {
|
||||||
|
font-size: var(--paper-font-subhead_-_font-size);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
height: calc(100% - 47px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.errors {
|
||||||
|
color: var(--google-red-500);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,20 +1,59 @@
|
|||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../../src/components/ha-markdown";
|
|
||||||
import "../../../../src/resources/ha-style";
|
|
||||||
import "../../../../src/components/dialog/ha-paper-dialog";
|
|
||||||
import { customElement } from "lit-element";
|
|
||||||
import { PaperDialogElement } from "@polymer/paper-dialog";
|
import { PaperDialogElement } from "@polymer/paper-dialog";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import { hassioStyle } from "../../resources/hassio-style";
|
||||||
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
|
import { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
|
||||||
|
|
||||||
|
import "../../../../src/components/dialog/ha-paper-dialog";
|
||||||
|
import "../../../../src/components/ha-markdown";
|
||||||
|
|
||||||
@customElement("dialog-hassio-markdown")
|
@customElement("dialog-hassio-markdown")
|
||||||
class HassioMarkdownDialog extends PolymerElement {
|
class HassioMarkdownDialog extends LitElement {
|
||||||
static get template() {
|
@property() public title!: string;
|
||||||
|
@property() public content!: string;
|
||||||
|
@query("#dialog") private _dialog!: PaperDialogElement;
|
||||||
|
|
||||||
|
public showDialog(params: HassioMarkdownDialogParams) {
|
||||||
|
this.title = params.title;
|
||||||
|
this.content = params.content;
|
||||||
|
this._dialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<style include="ha-style-dialog">
|
<ha-paper-dialog id="dialog" with-backdrop="">
|
||||||
|
<app-toolbar>
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hassio:close"
|
||||||
|
dialog-dismiss=""
|
||||||
|
></paper-icon-button>
|
||||||
|
<div main-title="">${this.title}</div>
|
||||||
|
</app-toolbar>
|
||||||
|
<paper-dialog-scrollable>
|
||||||
|
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
</ha-paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
hassioStyle,
|
||||||
|
css`
|
||||||
ha-paper-dialog {
|
ha-paper-dialog {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -52,32 +91,8 @@ class HassioMarkdownDialog extends PolymerElement {
|
|||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
`,
|
||||||
<ha-paper-dialog id="dialog" with-backdrop="">
|
];
|
||||||
<app-toolbar>
|
|
||||||
<paper-icon-button
|
|
||||||
icon="hassio:close"
|
|
||||||
dialog-dismiss=""
|
|
||||||
></paper-icon-button>
|
|
||||||
<div main-title="">[[title]]</div>
|
|
||||||
</app-toolbar>
|
|
||||||
<paper-dialog-scrollable>
|
|
||||||
<ha-markdown content="[[content]]"></ha-markdown>
|
|
||||||
</paper-dialog-scrollable>
|
|
||||||
</ha-paper-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
title: String,
|
|
||||||
content: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public showDialog(params) {
|
|
||||||
this.setProperties(params);
|
|
||||||
(this.$.dialog as PaperDialogElement).open();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,7 +12,9 @@ export const showHassioMarkdownDialog = (
|
|||||||
fireEvent(element, "show-dialog", {
|
fireEvent(element, "show-dialog", {
|
||||||
dialogTag: "dialog-hassio-markdown",
|
dialogTag: "dialog-hassio-markdown",
|
||||||
dialogImport: () =>
|
dialogImport: () =>
|
||||||
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
|
import(
|
||||||
|
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
|
||||||
|
),
|
||||||
dialogParams,
|
dialogParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
474
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
Normal file → Executable file
474
hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
Normal file → Executable file
@@ -1,19 +1,33 @@
|
|||||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@polymer/paper-checkbox/paper-checkbox";
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||||
|
import "@polymer/iron-icon/iron-icon";
|
||||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||||
import "@polymer/paper-icon-button/paper-icon-button";
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
import "@polymer/paper-input/paper-input";
|
import "@polymer/paper-input/paper-input";
|
||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
import { getSignedPath } from "../../../../src/data/auth";
|
|
||||||
|
|
||||||
import "../../../../src/resources/ha-style";
|
|
||||||
import "../../../../src/components/dialog/ha-paper-dialog";
|
|
||||||
import { customElement } from "lit-element";
|
|
||||||
import { PaperDialogElement } from "@polymer/paper-dialog";
|
import { PaperDialogElement } from "@polymer/paper-dialog";
|
||||||
|
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
query,
|
||||||
|
} from "lit-element";
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchHassioSnapshotInfo,
|
||||||
|
HassioSnapshotDetail,
|
||||||
|
} from "../../../../src/data/hassio/snapshot";
|
||||||
|
import { getSignedPath } from "../../../../src/data/auth";
|
||||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||||
import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio";
|
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
|
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||||
|
|
||||||
|
import "../../../../src/components/dialog/ha-paper-dialog";
|
||||||
|
|
||||||
const _computeFolders = (folders) => {
|
const _computeFolders = (folders) => {
|
||||||
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
|
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
|
||||||
@@ -45,21 +59,179 @@ const _computeAddons = (addons) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("dialog-hassio-snapshot")
|
interface AddonItem {
|
||||||
class HassioSnapshotDialog extends PolymerElement {
|
slug: string;
|
||||||
// Commented out because it breaks Polymer! Kept around for when we migrate
|
name: string;
|
||||||
// to Lit. Now just putting ts-ignore everywhere because we need this out.
|
version: string;
|
||||||
// Sorry future developer.
|
checked: boolean | null | undefined;
|
||||||
// public hass!: HomeAssistant;
|
}
|
||||||
// protected error?: string;
|
|
||||||
// private snapshot?: any;
|
|
||||||
// private dialogParams?: HassioSnapshotDialogParams;
|
|
||||||
// private restoreHass!: boolean;
|
|
||||||
// private snapshotPassword!: string;
|
|
||||||
|
|
||||||
static get template() {
|
interface FolderItem {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
checked: boolean | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("dialog-hassio-snapshot")
|
||||||
|
class HassioSnapshotDialog extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() private _error?: string;
|
||||||
|
@property() private snapshot?: HassioSnapshotDetail;
|
||||||
|
@property() private _folders!: FolderItem[];
|
||||||
|
@property() private _addons!: AddonItem[];
|
||||||
|
@property() private _dialogParams?: HassioSnapshotDialogParams;
|
||||||
|
@property() private _snapshotPassword!: string;
|
||||||
|
@property() private _restoreHass: boolean | null | undefined = true;
|
||||||
|
@query("#dialog") private _dialog!: PaperDialogElement;
|
||||||
|
|
||||||
|
public async showDialog(params: HassioSnapshotDialogParams) {
|
||||||
|
this.snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
||||||
|
this._folders = _computeFolders(
|
||||||
|
this.snapshot.folders
|
||||||
|
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
|
||||||
|
this._addons = _computeAddons(
|
||||||
|
this.snapshot.addons
|
||||||
|
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
|
||||||
|
|
||||||
|
this._dialogParams = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._dialog.open();
|
||||||
|
} catch {
|
||||||
|
await this.showDialog(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.snapshot) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<style include="ha-style-dialog">
|
<ha-paper-dialog
|
||||||
|
id="dialog"
|
||||||
|
with-backdrop=""
|
||||||
|
.on-iron-overlay-closed=${this._dialogClosed}
|
||||||
|
>
|
||||||
|
<app-toolbar>
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hassio:close"
|
||||||
|
dialog-dismiss=""
|
||||||
|
></paper-icon-button>
|
||||||
|
<div main-title="">${this._computeName}</div>
|
||||||
|
</app-toolbar>
|
||||||
|
<div class="details">
|
||||||
|
${this.snapshot.type === "full"
|
||||||
|
? "Full snapshot"
|
||||||
|
: "Partial snapshot"}
|
||||||
|
(${this._computeSize})<br />
|
||||||
|
${this._formatDatetime(this.snapshot.date)}
|
||||||
|
</div>
|
||||||
|
<div>Home Assistant:</div>
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${this._restoreHass}
|
||||||
|
@change="${(ev: Event) =>
|
||||||
|
(this._restoreHass = (ev.target as PaperCheckboxElement).checked)}"
|
||||||
|
>
|
||||||
|
Home Assistant ${this.snapshot.homeassistant}
|
||||||
|
</paper-checkbox>
|
||||||
|
${this._folders.length
|
||||||
|
? html`
|
||||||
|
<div>Folders:</div>
|
||||||
|
<paper-dialog-scrollable class="no-margin-top">
|
||||||
|
${this._folders.map((item) => {
|
||||||
|
return html`
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${item.checked}
|
||||||
|
@change="${(ev: Event) =>
|
||||||
|
this._updateFolders(
|
||||||
|
item,
|
||||||
|
(ev.target as PaperCheckboxElement).checked
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${item.name}
|
||||||
|
</paper-checkbox>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._addons.length
|
||||||
|
? html`
|
||||||
|
<div>Add-on:</div>
|
||||||
|
<paper-dialog-scrollable class="no-margin-top">
|
||||||
|
${this._addons.map((item) => {
|
||||||
|
return html`
|
||||||
|
<paper-checkbox
|
||||||
|
.checked=${item.checked}
|
||||||
|
@change="${(ev: Event) =>
|
||||||
|
this._updateAddons(
|
||||||
|
item,
|
||||||
|
(ev.target as PaperCheckboxElement).checked
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${item.name}
|
||||||
|
</paper-checkbox>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.snapshot.protected
|
||||||
|
? html`
|
||||||
|
<paper-input
|
||||||
|
autofocus=""
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
@value-changed=${this._passwordInput}
|
||||||
|
.value=${this._snapshotPassword}
|
||||||
|
></paper-input>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<p class="error">Error: ${this._error}</p>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<div>Actions:</div>
|
||||||
|
<ul class="buttons">
|
||||||
|
<li>
|
||||||
|
<mwc-button @click=${this._downloadClicked}>
|
||||||
|
<iron-icon icon="hassio:download" class="icon"></iron-icon>
|
||||||
|
Download Snapshot
|
||||||
|
</mwc-button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<mwc-button @click=${this._partialRestoreClicked}>
|
||||||
|
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||||
|
Restore Selected
|
||||||
|
</mwc-button>
|
||||||
|
</li>
|
||||||
|
${this.snapshot.type === "full"
|
||||||
|
? html`
|
||||||
|
<li>
|
||||||
|
<mwc-button @click=${this._fullRestoreClicked}>
|
||||||
|
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||||
|
Wipe & restore
|
||||||
|
</mwc-button>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<li>
|
||||||
|
<mwc-button @click=${this._deleteClicked}>
|
||||||
|
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
|
||||||
|
<span class="warning">Delete Snapshot</span>
|
||||||
|
</mwc-button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ha-paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
ha-paper-dialog {
|
ha-paper-dialog {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -94,259 +266,172 @@ class HassioSnapshotDialog extends PolymerElement {
|
|||||||
.details {
|
.details {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.download {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.warning,
|
.warning,
|
||||||
.error {
|
.error {
|
||||||
color: var(--google-red-500);
|
color: var(--google-red-500);
|
||||||
}
|
}
|
||||||
</style>
|
.buttons {
|
||||||
<ha-paper-dialog
|
display: flex;
|
||||||
id="dialog"
|
flex-direction: column;
|
||||||
with-backdrop=""
|
}
|
||||||
on-iron-overlay-closed="_dialogClosed"
|
.buttons li {
|
||||||
>
|
list-style-type: none;
|
||||||
<app-toolbar>
|
}
|
||||||
<paper-icon-button
|
.buttons .icon {
|
||||||
icon="hassio:close"
|
margin-right: 16px;
|
||||||
dialog-dismiss=""
|
}
|
||||||
></paper-icon-button>
|
.no-margin-top {
|
||||||
<div main-title="">[[_computeName(snapshot)]]</div>
|
margin-top: 0;
|
||||||
</app-toolbar>
|
}
|
||||||
<div class="details">
|
`,
|
||||||
[[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br />
|
];
|
||||||
[[_formatDatetime(snapshot.date)]]
|
|
||||||
</div>
|
|
||||||
<div>Home Assistant:</div>
|
|
||||||
<paper-checkbox checked="{{restoreHass}}">
|
|
||||||
Home Assistant [[snapshot.homeassistant]]
|
|
||||||
</paper-checkbox>
|
|
||||||
<template is="dom-if" if="[[_folders.length]]">
|
|
||||||
<div>Folders:</div>
|
|
||||||
<template is="dom-repeat" items="[[_folders]]">
|
|
||||||
<paper-checkbox checked="{{item.checked}}">
|
|
||||||
[[item.name]]
|
|
||||||
</paper-checkbox>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_addons.length]]">
|
|
||||||
<div>Add-ons:</div>
|
|
||||||
<paper-dialog-scrollable>
|
|
||||||
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
|
|
||||||
<paper-checkbox checked="{{item.checked}}">
|
|
||||||
[[item.name]] <span class="details">([[item.version]])</span>
|
|
||||||
</paper-checkbox>
|
|
||||||
</template>
|
|
||||||
</paper-dialog-scrollable>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[snapshot.protected]]">
|
|
||||||
<paper-input
|
|
||||||
autofocus=""
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
value="{{snapshotPassword}}"
|
|
||||||
></paper-input>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<p class="error">Error: [[error]]</p>
|
|
||||||
</template>
|
|
||||||
<div class="buttons">
|
|
||||||
<paper-icon-button
|
|
||||||
icon="hassio:delete"
|
|
||||||
on-click="_deleteClicked"
|
|
||||||
class="warning"
|
|
||||||
title="Delete snapshot"
|
|
||||||
></paper-icon-button>
|
|
||||||
<paper-icon-button
|
|
||||||
on-click="_downloadClicked"
|
|
||||||
icon="hassio:download"
|
|
||||||
class="download"
|
|
||||||
title="Download snapshot"
|
|
||||||
></paper-icon-button>
|
|
||||||
<mwc-button on-click="_partialRestoreClicked"
|
|
||||||
>Restore selected</mwc-button
|
|
||||||
>
|
|
||||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
|
||||||
<mwc-button on-click="_fullRestoreClicked"
|
|
||||||
>Wipe & restore</mwc-button
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</ha-paper-dialog>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
|
||||||
return {
|
this._folders = this._folders.map((folder) => {
|
||||||
hass: Object,
|
if (folder.slug === item.slug) {
|
||||||
dialogParams: Object,
|
folder.checked = value;
|
||||||
snapshot: Object,
|
}
|
||||||
_folders: Object,
|
return folder;
|
||||||
_addons: Object,
|
|
||||||
restoreHass: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
snapshotPassword: String,
|
|
||||||
error: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async showDialog(params: HassioSnapshotDialogParams) {
|
|
||||||
// @ts-ignore
|
|
||||||
const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
|
||||||
this.setProperties({
|
|
||||||
dialogParams: params,
|
|
||||||
snapshot,
|
|
||||||
_folders: _computeFolders(snapshot.folders),
|
|
||||||
_addons: _computeAddons(snapshot.addons),
|
|
||||||
});
|
});
|
||||||
(this.$.dialog as PaperDialogElement).open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _isFullSnapshot(type) {
|
private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
|
||||||
return type === "full";
|
this._addons = this._addons.map((addon) => {
|
||||||
|
if (addon.slug === item.slug) {
|
||||||
|
addon.checked = value;
|
||||||
|
}
|
||||||
|
return addon;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _partialRestoreClicked() {
|
private _passwordInput(ev: PolymerChangedEvent<string>) {
|
||||||
|
this._snapshotPassword = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _partialRestoreClicked() {
|
||||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
const addons = this._addons
|
const addons = this._addons
|
||||||
.filter((addon) => addon.checked)
|
.filter((addon) => addon.checked)
|
||||||
.map((addon) => addon.slug);
|
.map((addon) => addon.slug);
|
||||||
// @ts-ignore
|
|
||||||
const folders = this._folders
|
const folders = this._folders
|
||||||
.filter((folder) => folder.checked)
|
.filter((folder) => folder.checked)
|
||||||
.map((folder) => folder.slug);
|
.map((folder) => folder.slug);
|
||||||
|
|
||||||
const data = {
|
const data: {
|
||||||
// @ts-ignore
|
homeassistant: boolean | null | undefined;
|
||||||
homeassistant: this.restoreHass,
|
addons: any;
|
||||||
|
folders: any;
|
||||||
|
password?: string;
|
||||||
|
} = {
|
||||||
|
homeassistant: this._restoreHass,
|
||||||
addons,
|
addons,
|
||||||
folders,
|
folders,
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
|
||||||
if (this.snapshot.protected) {
|
if (this.snapshot!.protected) {
|
||||||
// @ts-ignore
|
data.password = this._snapshotPassword;
|
||||||
data.password = this.snapshotPassword;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.hass
|
this.hass
|
||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
// @ts-ignore
|
|
||||||
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
|
`hassio/snapshots/${this.snapshot!.slug}/restore/partial`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
alert("Snapshot restored!");
|
alert("Snapshot restored!");
|
||||||
(this.$.dialog as PaperDialogElement).close();
|
this._dialog.close();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// @ts-ignore
|
this._error = error.body.message;
|
||||||
this.error = error.body.message;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _fullRestoreClicked() {
|
private _fullRestoreClicked() {
|
||||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
const data = this.snapshot.protected
|
const data = this.snapshot!.protected
|
||||||
? {
|
? { password: this._snapshotPassword }
|
||||||
password:
|
|
||||||
// @ts-ignore
|
|
||||||
this.snapshotPassword,
|
|
||||||
}
|
|
||||||
: undefined;
|
: undefined;
|
||||||
// @ts-ignore
|
|
||||||
this.hass
|
this.hass
|
||||||
.callApi(
|
.callApi(
|
||||||
"POST",
|
"POST",
|
||||||
// @ts-ignore
|
`hassio/snapshots/${this.snapshot!.slug}/restore/full`,
|
||||||
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
|
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
alert("Snapshot restored!");
|
alert("Snapshot restored!");
|
||||||
(this.$.dialog as PaperDialogElement).close();
|
this._dialog.close();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// @ts-ignore
|
this._error = error.body.message;
|
||||||
this.error = error.body.message;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _deleteClicked() {
|
private _deleteClicked() {
|
||||||
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
this.hass
|
this.hass
|
||||||
// @ts-ignore
|
|
||||||
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
|
.callApi("POST", `hassio/snapshots/${this.snapshot!.slug}/remove`)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
(this.$.dialog as PaperDialogElement).close();
|
this._dialog.close();
|
||||||
// @ts-ignore
|
this._dialogParams!.onDelete();
|
||||||
this.dialogParams!.onDelete();
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// @ts-ignore
|
this._error = error.body.message;
|
||||||
this.error = error.body.message;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _downloadClicked() {
|
private async _downloadClicked() {
|
||||||
let signedPath;
|
let signedPath: { path: string };
|
||||||
try {
|
try {
|
||||||
signedPath = await getSignedPath(
|
signedPath = await getSignedPath(
|
||||||
// @ts-ignore
|
|
||||||
this.hass,
|
this.hass,
|
||||||
// @ts-ignore
|
`/api/hassio/snapshots/${this.snapshot!.slug}/download`
|
||||||
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
|
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = signedPath.path;
|
a.href = signedPath.path;
|
||||||
a.download = `Hass_io_${name}.tar`;
|
a.download = `Hass_io_${name}.tar`;
|
||||||
this.$.dialog.appendChild(a);
|
this._dialog.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
this.$.dialog.removeChild(a);
|
this._dialog.removeChild(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _computeName(snapshot) {
|
private get _computeName() {
|
||||||
return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
|
return this.snapshot
|
||||||
|
? this.snapshot.name || this.snapshot.slug
|
||||||
|
: "Unnamed snapshot";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _computeType(type) {
|
private get _computeSize() {
|
||||||
return type === "full" ? "Full snapshot" : "Partial snapshot";
|
return Math.ceil(this.snapshot!.size * 10) / 10 + " MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _computeSize(size) {
|
private _formatDatetime(datetime) {
|
||||||
return Math.ceil(size * 10) / 10 + " MB";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _sortAddons(a, b) {
|
|
||||||
return a.name < b.name ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _formatDatetime(datetime) {
|
|
||||||
return new Date(datetime).toLocaleDateString(navigator.language, {
|
return new Date(datetime).toLocaleDateString(navigator.language, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -357,13 +442,12 @@ class HassioSnapshotDialog extends PolymerElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _dialogClosed() {
|
private _dialogClosed() {
|
||||||
this.setProperties({
|
this._dialogParams = undefined;
|
||||||
dialogParams: undefined,
|
this.snapshot = undefined;
|
||||||
snapshot: undefined,
|
this._snapshotPassword = "";
|
||||||
_addons: [],
|
this._folders = [];
|
||||||
_folders: [],
|
this._addons = [];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,7 +12,9 @@ export const showHassioSnapshotDialog = (
|
|||||||
fireEvent(element, "show-dialog", {
|
fireEvent(element, "show-dialog", {
|
||||||
dialogTag: "dialog-hassio-snapshot",
|
dialogTag: "dialog-hassio-snapshot",
|
||||||
dialogImport: () =>
|
dialogImport: () =>
|
||||||
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
|
import(
|
||||||
|
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
|
||||||
|
),
|
||||||
dialogParams,
|
dialogParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
window.loadES5Adapter().then(() => {
|
window.loadES5Adapter().then(() => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
|
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
|
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
|
||||||
});
|
});
|
||||||
|
|
||||||
const styleEl = document.createElement("style");
|
const styleEl = document.createElement("style");
|
||||||
styleEl.innerHTML = `
|
styleEl.innerHTML = `
|
||||||
body {
|
body {
|
@@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer";
|
|||||||
import "@polymer/paper-icon-button";
|
import "@polymer/paper-icon-button";
|
||||||
|
|
||||||
import "../../src/resources/ha-style";
|
import "../../src/resources/ha-style";
|
||||||
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||||
import {
|
import {
|
||||||
HassRouterPage,
|
HassRouterPage,
|
||||||
@@ -12,19 +12,28 @@ import {
|
|||||||
import { HomeAssistant } from "../../src/types";
|
import { HomeAssistant } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
fetchHassioSupervisorInfo,
|
fetchHassioSupervisorInfo,
|
||||||
fetchHassioHostInfo,
|
|
||||||
fetchHassioHomeAssistantInfo,
|
fetchHassioHomeAssistantInfo,
|
||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
HassioHostInfo,
|
|
||||||
HassioHomeAssistantInfo,
|
HassioHomeAssistantInfo,
|
||||||
fetchHassioAddonInfo,
|
|
||||||
createHassioSession,
|
createHassioSession,
|
||||||
HassioPanelInfo,
|
HassioPanelInfo,
|
||||||
} from "../../src/data/hassio";
|
} from "../../src/data/hassio/supervisor";
|
||||||
|
import {
|
||||||
|
fetchHassioHostInfo,
|
||||||
|
fetchHassioHassOsInfo,
|
||||||
|
HassioHostInfo,
|
||||||
|
HassioHassOSInfo,
|
||||||
|
} from "../../src/data/hassio/host";
|
||||||
|
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
|
||||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||||
// Don't codesplit it, that way the dashboard always loads fast.
|
// Don't codesplit it, that way the dashboard always loads fast.
|
||||||
import "./hassio-pages-with-tabs";
|
import "./hassio-pages-with-tabs";
|
||||||
|
import { navigate } from "../../src/common/navigate";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
AlertDialogParams,
|
||||||
|
} from "../../src/dialogs/generic/show-dialog-box";
|
||||||
|
|
||||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||||
@@ -54,24 +63,33 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
addon: {
|
addon: {
|
||||||
tag: "hassio-addon-view",
|
tag: "hassio-addon-view",
|
||||||
load: () =>
|
load: () =>
|
||||||
import(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
|
import(
|
||||||
|
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
ingress: {
|
ingress: {
|
||||||
tag: "hassio-ingress-view",
|
tag: "hassio-ingress-view",
|
||||||
load: () =>
|
load: () =>
|
||||||
import(/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"),
|
import(
|
||||||
|
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@property() private _supervisorInfo: HassioSupervisorInfo;
|
@property() private _supervisorInfo: HassioSupervisorInfo;
|
||||||
@property() private _hostInfo: HassioHostInfo;
|
@property() private _hostInfo: HassioHostInfo;
|
||||||
|
@property() private _hassOsInfo?: HassioHassOSInfo;
|
||||||
@property() private _hassInfo: HassioHomeAssistantInfo;
|
@property() private _hassInfo: HassioHomeAssistantInfo;
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
|
applyThemesOnElement(
|
||||||
|
this.parentElement,
|
||||||
|
this.hass.themes,
|
||||||
|
this.hass.selectedTheme,
|
||||||
|
true
|
||||||
|
);
|
||||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||||
// Paulus - March 17, 2019
|
// Paulus - March 17, 2019
|
||||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||||
@@ -97,6 +115,14 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Forward haptic events to parent window.
|
||||||
|
window.addEventListener("haptic", (ev) => {
|
||||||
|
// @ts-ignore
|
||||||
|
fireEvent(window.parent, ev.type, ev.detail, {
|
||||||
|
bubbles: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
makeDialogManager(this, document.body);
|
makeDialogManager(this, document.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +139,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
supervisorInfo: this._supervisorInfo,
|
supervisorInfo: this._supervisorInfo,
|
||||||
hostInfo: this._hostInfo,
|
hostInfo: this._hostInfo,
|
||||||
hassInfo: this._hassInfo,
|
hassInfo: this._hassInfo,
|
||||||
|
hassOsInfo: this._hassOsInfo,
|
||||||
route,
|
route,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -121,6 +148,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
el.supervisorInfo = this._supervisorInfo;
|
el.supervisorInfo = this._supervisorInfo;
|
||||||
el.hostInfo = this._hostInfo;
|
el.hostInfo = this._hostInfo;
|
||||||
el.hassInfo = this._hassInfo;
|
el.hassInfo = this._hassInfo;
|
||||||
|
el.hassOsInfo = this._hassOsInfo;
|
||||||
el.route = route;
|
el.route = route;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,28 +167,88 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
|||||||
this._supervisorInfo = supervisorInfo;
|
this._supervisorInfo = supervisorInfo;
|
||||||
this._hostInfo = hostInfo;
|
this._hostInfo = hostInfo;
|
||||||
this._hassInfo = hassInfo;
|
this._hassInfo = hassInfo;
|
||||||
|
|
||||||
|
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
|
||||||
|
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _redirectIngress(addonSlug: string) {
|
private async _redirectIngress(addonSlug: string) {
|
||||||
|
// When we trigger a navigation, we sleep to make sure we don't
|
||||||
|
// show the hassio dashboard before navigating away.
|
||||||
|
const awaitAlert = async (
|
||||||
|
alertParams: AlertDialogParams,
|
||||||
|
action: () => void
|
||||||
|
) => {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
alertParams.confirm = resolve;
|
||||||
|
showAlertDialog(this, alertParams);
|
||||||
|
});
|
||||||
|
action();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSessionPromise = createHassioSession(this.hass).then(
|
||||||
|
() => true,
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
let addon;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [addon] = await Promise.all([
|
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||||
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
|
|
||||||
throw new Error("Failed to fetch add-on info");
|
|
||||||
}),
|
|
||||||
createHassioSession(this.hass).catch(() => {
|
|
||||||
throw new Error("Failed to create an ingress session");
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
if (!addon.ingress_url) {
|
|
||||||
throw new Error("Add-on does not support Ingress");
|
|
||||||
}
|
|
||||||
location.assign(addon.ingress_url);
|
|
||||||
// await a promise that doesn't resolve, so we show the loading screen
|
|
||||||
// while we load the next page.
|
|
||||||
await new Promise(() => undefined);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Unable to open ingress connection `);
|
await awaitAlert(
|
||||||
|
{
|
||||||
|
text: "Unable to fetch add-on info to start Ingress",
|
||||||
|
title: "Hass.io",
|
||||||
|
},
|
||||||
|
() => history.back()
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!addon.ingress_url) {
|
||||||
|
await awaitAlert(
|
||||||
|
{
|
||||||
|
text: "Add-on does not support Ingress",
|
||||||
|
title: addon.name,
|
||||||
|
},
|
||||||
|
() => history.back()
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addon.state !== "started") {
|
||||||
|
await awaitAlert(
|
||||||
|
{
|
||||||
|
text: "Add-on is not running. Please start it first",
|
||||||
|
title: addon.name,
|
||||||
|
},
|
||||||
|
() => navigate(this, `/hassio/addon/${addon.slug}`, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await createSessionPromise)) {
|
||||||
|
await awaitAlert(
|
||||||
|
{
|
||||||
|
text: "Unable to create an Ingress session",
|
||||||
|
title: addon.name,
|
||||||
|
},
|
||||||
|
() => history.back()
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location.assign(addon.ingress_url);
|
||||||
|
// await a promise that doesn't resolve, so we show the loading screen
|
||||||
|
// while we load the next page.
|
||||||
|
await new Promise(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _apiCalled(ev) {
|
private _apiCalled(ev) {
|
||||||
|
@@ -23,11 +23,11 @@ import scrollToTarget from "../../src/common/dom/scroll-to-target";
|
|||||||
import { haStyle } from "../../src/resources/styles";
|
import { haStyle } from "../../src/resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../src/types";
|
import { HomeAssistant, Route } from "../../src/types";
|
||||||
import { navigate } from "../../src/common/navigate";
|
import { navigate } from "../../src/common/navigate";
|
||||||
|
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
HassioHostInfo,
|
|
||||||
HassioHomeAssistantInfo,
|
HassioHomeAssistantInfo,
|
||||||
} from "../../src/data/hassio";
|
} from "../../src/data/hassio/supervisor";
|
||||||
|
|
||||||
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
|
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
|
||||||
|
|
||||||
@@ -39,8 +39,9 @@ class HassioPagesWithTabs extends LitElement {
|
|||||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||||
@property() public hostInfo!: HassioHostInfo;
|
@property() public hostInfo!: HassioHostInfo;
|
||||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||||
|
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
const page = this._page;
|
const page = this._page;
|
||||||
return html`
|
return html`
|
||||||
<app-header-layout has-scrolling-region>
|
<app-header-layout has-scrolling-region>
|
||||||
@@ -51,7 +52,7 @@ class HassioPagesWithTabs extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
hassio
|
hassio
|
||||||
></ha-menu-button>
|
></ha-menu-button>
|
||||||
<div main-title>Hass.io</div>
|
<div main-title>Supervisor</div>
|
||||||
${HAS_REFRESH_BUTTON.includes(page)
|
${HAS_REFRESH_BUTTON.includes(page)
|
||||||
? html`
|
? html`
|
||||||
<paper-icon-button
|
<paper-icon-button
|
||||||
@@ -79,6 +80,7 @@ class HassioPagesWithTabs extends LitElement {
|
|||||||
.supervisorInfo=${this.supervisorInfo}
|
.supervisorInfo=${this.supervisorInfo}
|
||||||
.hostInfo=${this.hostInfo}
|
.hostInfo=${this.hostInfo}
|
||||||
.hassInfo=${this.hassInfo}
|
.hassInfo=${this.hassInfo}
|
||||||
|
.hassOsInfo=${this.hassOsInfo}
|
||||||
></hassio-tabs-router>
|
></hassio-tabs-router>
|
||||||
</app-header-layout>
|
</app-header-layout>
|
||||||
`;
|
`;
|
||||||
@@ -121,7 +123,7 @@ class HassioPagesWithTabs extends LitElement {
|
|||||||
}
|
}
|
||||||
paper-tabs {
|
paper-tabs {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
--paper-tabs-selection-bar-color: #fff;
|
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@@ -11,11 +11,11 @@ import "./dashboard/hassio-dashboard";
|
|||||||
import "./snapshots/hassio-snapshots";
|
import "./snapshots/hassio-snapshots";
|
||||||
import "./addon-store/hassio-addon-store";
|
import "./addon-store/hassio-addon-store";
|
||||||
import "./system/hassio-system";
|
import "./system/hassio-system";
|
||||||
|
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
|
||||||
import {
|
import {
|
||||||
HassioSupervisorInfo,
|
HassioSupervisorInfo,
|
||||||
HassioHostInfo,
|
|
||||||
HassioHomeAssistantInfo,
|
HassioHomeAssistantInfo,
|
||||||
} from "../../src/data/hassio";
|
} from "../../src/data/hassio/supervisor";
|
||||||
|
|
||||||
@customElement("hassio-tabs-router")
|
@customElement("hassio-tabs-router")
|
||||||
class HassioTabsRouter extends HassRouterPage {
|
class HassioTabsRouter extends HassRouterPage {
|
||||||
@@ -23,6 +23,7 @@ class HassioTabsRouter extends HassRouterPage {
|
|||||||
@property() public supervisorInfo: HassioSupervisorInfo;
|
@property() public supervisorInfo: HassioSupervisorInfo;
|
||||||
@property() public hostInfo: HassioHostInfo;
|
@property() public hostInfo: HassioHostInfo;
|
||||||
@property() public hassInfo: HassioHomeAssistantInfo;
|
@property() public hassInfo: HassioHomeAssistantInfo;
|
||||||
|
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||||
|
|
||||||
protected routerOptions: RouterOptions = {
|
protected routerOptions: RouterOptions = {
|
||||||
routes: {
|
routes: {
|
||||||
@@ -49,12 +50,14 @@ class HassioTabsRouter extends HassRouterPage {
|
|||||||
supervisorInfo: this.supervisorInfo,
|
supervisorInfo: this.supervisorInfo,
|
||||||
hostInfo: this.hostInfo,
|
hostInfo: this.hostInfo,
|
||||||
hassInfo: this.hassInfo,
|
hassInfo: this.hassInfo,
|
||||||
|
hassOsInfo: this.hassOsInfo,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
el.hass = this.hass;
|
el.hass = this.hass;
|
||||||
el.supervisorInfo = this.supervisorInfo;
|
el.supervisorInfo = this.supervisorInfo;
|
||||||
el.hostInfo = this.hostInfo;
|
el.hostInfo = this.hostInfo;
|
||||||
el.hassInfo = this.hassInfo;
|
el.hassInfo = this.hassInfo;
|
||||||
|
el.hassOsInfo = this.hassOsInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,11 +9,11 @@ import {
|
|||||||
css,
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
|
import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
||||||
import {
|
import {
|
||||||
createHassioSession,
|
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
fetchHassioAddonInfo,
|
fetchHassioAddonInfo,
|
||||||
} from "../../../src/data/hassio";
|
} from "../../../src/data/hassio/addon";
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../src/layouts/hass-subpage";
|
import "../../../src/layouts/hass-subpage";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class HassioIngressView extends LitElement {
|
|||||||
@property() public route!: Route;
|
@property() public route!: Route;
|
||||||
@property() private _addon?: HassioAddonDetails;
|
@property() private _addon?: HassioAddonDetails;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult {
|
||||||
if (!this._addon) {
|
if (!this._addon) {
|
||||||
return html`
|
return html`
|
||||||
<hass-loading-screen></hass-loading-screen>
|
<hass-loading-screen></hass-loading-screen>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user