Compare commits
495 Commits
20190130.1
...
20190601.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3a05b1124a | ||
![]() |
d14c6125da | ||
![]() |
3ee357178e | ||
![]() |
8f6fdea4eb | ||
![]() |
be6b25f5be | ||
![]() |
be4dd5b20b | ||
![]() |
fe4811b278 | ||
![]() |
d376457cec | ||
![]() |
35e82a8e26 | ||
![]() |
03735f0539 | ||
![]() |
4cc812c1bf | ||
![]() |
bdacd05fab | ||
![]() |
ab157fdbff | ||
![]() |
d94223a61e | ||
![]() |
ebe3198c27 | ||
![]() |
2b2d2effd2 | ||
![]() |
8092e24af8 | ||
![]() |
f019bb095d | ||
![]() |
1ad9d2e54c | ||
![]() |
b2b18cb814 | ||
![]() |
e595637a10 | ||
![]() |
d10a0b3b6c | ||
![]() |
c24f8a2115 | ||
![]() |
7691e3f2c2 | ||
![]() |
6bbe8ff39f | ||
![]() |
a1e9b4938f | ||
![]() |
c826596529 | ||
![]() |
7f47079750 | ||
![]() |
642ba1adc3 | ||
![]() |
fe80c7fe0e | ||
![]() |
9309c5a1b6 | ||
![]() |
575eb22608 | ||
![]() |
be0bef3f1b | ||
![]() |
970286bbba | ||
![]() |
087c3b9c0e | ||
![]() |
b12d1b13ca | ||
![]() |
d0410e0884 | ||
![]() |
a1bf06ceb2 | ||
![]() |
d99744e054 | ||
![]() |
e02d11a51f | ||
![]() |
1b50100b6c | ||
![]() |
309fecc9f3 | ||
![]() |
46f3add520 | ||
![]() |
a89f0bd1cd | ||
![]() |
8408b8d41f | ||
![]() |
13761a20c5 | ||
![]() |
03d17a9761 | ||
![]() |
5501cccc67 | ||
![]() |
9340d9068e | ||
![]() |
bbdaa4b7c1 | ||
![]() |
c87c782b2c | ||
![]() |
f0b1cd9032 | ||
![]() |
2ed532e055 | ||
![]() |
f70dafa192 | ||
![]() |
af6ade8eb6 | ||
![]() |
d77ae840d8 | ||
![]() |
968eae7727 | ||
![]() |
97d8a68455 | ||
![]() |
7827cec212 | ||
![]() |
746ad588ef | ||
![]() |
95e918b6ac | ||
![]() |
1e82cc22e4 | ||
![]() |
fb2e1e5ebb | ||
![]() |
fe2ae965b3 | ||
![]() |
8924a5f043 | ||
![]() |
32e68c1a4b | ||
![]() |
89a35a0062 | ||
![]() |
484b1c8444 | ||
![]() |
cd5e274ffa | ||
![]() |
f466a53ed4 | ||
![]() |
1d40d94774 | ||
![]() |
82e8ca2754 | ||
![]() |
8c904fb012 | ||
![]() |
fa13b95498 | ||
![]() |
289611363e | ||
![]() |
cb7048db23 | ||
![]() |
b9f86f735b | ||
![]() |
0e044acaa9 | ||
![]() |
1223766523 | ||
![]() |
db65af9c22 | ||
![]() |
fcdb1b48a2 | ||
![]() |
8729410dce | ||
![]() |
adb92e1708 | ||
![]() |
81088e0d07 | ||
![]() |
34129cc7cb | ||
![]() |
530be9155b | ||
![]() |
aa33b00a1f | ||
![]() |
57b917f297 | ||
![]() |
aad7dc5d7d | ||
![]() |
6c41c7b1ab | ||
![]() |
8b98f375c2 | ||
![]() |
8a86dd8426 | ||
![]() |
5b12ca94e9 | ||
![]() |
652cd10483 | ||
![]() |
ca0ded8587 | ||
![]() |
f943393ade | ||
![]() |
d8f21d99af | ||
![]() |
73ef03e33f | ||
![]() |
c34dde815c | ||
![]() |
1e85880d7b | ||
![]() |
57abd4ae07 | ||
![]() |
2624c1544b | ||
![]() |
1e72ffc0c2 | ||
![]() |
8ca70ace4c | ||
![]() |
d66cf3f787 | ||
![]() |
44df0f698c | ||
![]() |
981dd5df63 | ||
![]() |
cd6250c495 | ||
![]() |
2f36304f06 | ||
![]() |
30471b7cfb | ||
![]() |
ff2f573dd0 | ||
![]() |
38ddbf45c2 | ||
![]() |
d79bf5e07e | ||
![]() |
d05b1ef9cc | ||
![]() |
c260591d4d | ||
![]() |
87a7e63e31 | ||
![]() |
a5dd3755e1 | ||
![]() |
ad40d9927b | ||
![]() |
f4cfbc6678 | ||
![]() |
b3c1bead39 | ||
![]() |
d220e56239 | ||
![]() |
f967b4940a | ||
![]() |
f44d5dca1c | ||
![]() |
a9ed4e7943 | ||
![]() |
a404acbf44 | ||
![]() |
eaa2ce1462 | ||
![]() |
bdd8699709 | ||
![]() |
9f0b20634a | ||
![]() |
a70d9195db | ||
![]() |
d86253d582 | ||
![]() |
d5a313445f | ||
![]() |
f979febb76 | ||
![]() |
a1a2a78531 | ||
![]() |
6ed2d288e6 | ||
![]() |
5c8e5d3539 | ||
![]() |
bbae3291e1 | ||
![]() |
5dbd5c7395 | ||
![]() |
038f7b43d5 | ||
![]() |
671e564037 | ||
![]() |
8298d810a8 | ||
![]() |
7428479f6b | ||
![]() |
6b85910cdb | ||
![]() |
4d7bb0df7d | ||
![]() |
26a39b1bb8 | ||
![]() |
e23f046c4d | ||
![]() |
fe73213643 | ||
![]() |
cbe5355d38 | ||
![]() |
81b232f01e | ||
![]() |
3e6be45f1f | ||
![]() |
d26ed6fdb6 | ||
![]() |
eda168247c | ||
![]() |
4d2390daf4 | ||
![]() |
5b861bb4c6 | ||
![]() |
be6d89bb7a | ||
![]() |
1c17210948 | ||
![]() |
5257715145 | ||
![]() |
8df9ac9dfa | ||
![]() |
559164e159 | ||
![]() |
70072786a1 | ||
![]() |
cda29fcd07 | ||
![]() |
31e351c75c | ||
![]() |
cadcd845cc | ||
![]() |
b07f95f956 | ||
![]() |
7f99f1d9be | ||
![]() |
8c7cdda3d3 | ||
![]() |
8c222bb467 | ||
![]() |
4dfdebb00a | ||
![]() |
3947adbab4 | ||
![]() |
81eab0bf1b | ||
![]() |
0c406335f5 | ||
![]() |
109c40b2d3 | ||
![]() |
a362b08113 | ||
![]() |
438d155c45 | ||
![]() |
75f5325048 | ||
![]() |
8f5f14fada | ||
![]() |
8e290be9e7 | ||
![]() |
9f97b583a8 | ||
![]() |
8993e39c38 | ||
![]() |
dc61a62149 | ||
![]() |
22fdac4189 | ||
![]() |
c52f437ee6 | ||
![]() |
549db23ff5 | ||
![]() |
6775a094c9 | ||
![]() |
74a255add1 | ||
![]() |
a77c951d55 | ||
![]() |
e3896c359a | ||
![]() |
56e3514e40 | ||
![]() |
f4319d9b13 | ||
![]() |
c134464f6a | ||
![]() |
7b821aa363 | ||
![]() |
4e6d00cf5c | ||
![]() |
22e5792a8f | ||
![]() |
e3e0d4618e | ||
![]() |
aa1ac8f339 | ||
![]() |
40863db138 | ||
![]() |
eac37af18c | ||
![]() |
7f8f99a414 | ||
![]() |
a743a2c46b | ||
![]() |
adc63e1e5a | ||
![]() |
1d24b83e5c | ||
![]() |
b3f9432ae1 | ||
![]() |
c95a44c570 | ||
![]() |
5080f4c2db | ||
![]() |
44eaa3abad | ||
![]() |
9a4215b5d5 | ||
![]() |
004892e11a | ||
![]() |
669358bf1a | ||
![]() |
435b7d9cee | ||
![]() |
9a2207b5cb | ||
![]() |
324f0bb8a2 | ||
![]() |
3b8f8f8189 | ||
![]() |
702c17d658 | ||
![]() |
e2a9cf0d3c | ||
![]() |
8aa501b7bd | ||
![]() |
45189c9163 | ||
![]() |
86940f4d42 | ||
![]() |
812c1362a6 | ||
![]() |
6bf9ea5699 | ||
![]() |
20ee3452dc | ||
![]() |
ef18f9eac9 | ||
![]() |
47faf2768c | ||
![]() |
a2bed3dd90 | ||
![]() |
4fab0b9717 | ||
![]() |
06b70e2653 | ||
![]() |
48aa9a2ad7 | ||
![]() |
93d971f72b | ||
![]() |
7e69df44d7 | ||
![]() |
c743a48cf9 | ||
![]() |
b82a1c75c4 | ||
![]() |
be9402bd05 | ||
![]() |
ebae469e7d | ||
![]() |
d0d293fe21 | ||
![]() |
bd6d082555 | ||
![]() |
39190dda20 | ||
![]() |
89a8e3da36 | ||
![]() |
49f90671fb | ||
![]() |
c4ece5e451 | ||
![]() |
799bd973ca | ||
![]() |
03dffa9905 | ||
![]() |
1d1c981601 | ||
![]() |
40025d44c2 | ||
![]() |
42117fcba0 | ||
![]() |
dc16abd637 | ||
![]() |
8c71746952 | ||
![]() |
6e504020bf | ||
![]() |
7caf37275d | ||
![]() |
c3f094eb9e | ||
![]() |
feb3be1d17 | ||
![]() |
2fe0398f37 | ||
![]() |
42c7879c4d | ||
![]() |
2586590bd9 | ||
![]() |
59ee160f96 | ||
![]() |
d4bc4bf7bc | ||
![]() |
e2a182acee | ||
![]() |
88131ade23 | ||
![]() |
fb16156f8d | ||
![]() |
6ba77b4fa5 | ||
![]() |
c55291dd18 | ||
![]() |
27b61776e8 | ||
![]() |
baa13a1b6c | ||
![]() |
23ca1b972d | ||
![]() |
2d75e797c7 | ||
![]() |
117ea32586 | ||
![]() |
68909c80ff | ||
![]() |
7aa296e774 | ||
![]() |
18df636573 | ||
![]() |
e1540e45f9 | ||
![]() |
d0b0284200 | ||
![]() |
915c441a94 | ||
![]() |
2aec877310 | ||
![]() |
1e291e80b7 | ||
![]() |
7d92eede1f | ||
![]() |
9fc8c0764c | ||
![]() |
4ff2d941c3 | ||
![]() |
2349e2f251 | ||
![]() |
8785b03fd8 | ||
![]() |
92e6c5adfd | ||
![]() |
6015eff8a2 | ||
![]() |
1451a78dd3 | ||
![]() |
094f558556 | ||
![]() |
cd466df42c | ||
![]() |
a626961ae5 | ||
![]() |
bbc32278d8 | ||
![]() |
e2ed1a9fd9 | ||
![]() |
cd94442455 | ||
![]() |
c9eea4acc1 | ||
![]() |
e55ca54509 | ||
![]() |
4118497978 | ||
![]() |
882dc38b12 | ||
![]() |
bcade77075 | ||
![]() |
8c13e524b9 | ||
![]() |
ffa47ccf34 | ||
![]() |
1e22d13588 | ||
![]() |
19804a713d | ||
![]() |
eeaaecd5b7 | ||
![]() |
9a00c65e3b | ||
![]() |
c026c65d53 | ||
![]() |
e9c245015c | ||
![]() |
cdde6f6f4c | ||
![]() |
262537c287 | ||
![]() |
ec04c80413 | ||
![]() |
86548052e5 | ||
![]() |
1890dd8683 | ||
![]() |
2908eb693a | ||
![]() |
7fe4084073 | ||
![]() |
ee948302ed | ||
![]() |
f809bf0550 | ||
![]() |
ed9dff99d3 | ||
![]() |
f5d0162aec | ||
![]() |
32682a2be0 | ||
![]() |
836844a312 | ||
![]() |
8b82fa940e | ||
![]() |
d4be171df9 | ||
![]() |
57be7ac873 | ||
![]() |
a9cecb55ac | ||
![]() |
1c6bf8b94a | ||
![]() |
1c6235546a | ||
![]() |
daaaef96b0 | ||
![]() |
aa3b6343ed | ||
![]() |
3e5d372bbe | ||
![]() |
3bab8686c8 | ||
![]() |
8c0af2c140 | ||
![]() |
f008f8f41a | ||
![]() |
587a7c3b66 | ||
![]() |
b25fff9852 | ||
![]() |
45e5f7d0ff | ||
![]() |
19c44f7c7b | ||
![]() |
241d7345d0 | ||
![]() |
34c6356a47 | ||
![]() |
9383d80354 | ||
![]() |
178e4de452 | ||
![]() |
787abc4611 | ||
![]() |
c2948638d6 | ||
![]() |
1db93a4f7b | ||
![]() |
8f4d24b6da | ||
![]() |
03d4a648f5 | ||
![]() |
8dba463dd4 | ||
![]() |
7c21a07a66 | ||
![]() |
82189ab3c6 | ||
![]() |
c0896d173d | ||
![]() |
5032b6e63b | ||
![]() |
9bf06ca0af | ||
![]() |
4fa1c3e883 | ||
![]() |
339be43eea | ||
![]() |
00c08a09db | ||
![]() |
bed257a4eb | ||
![]() |
b73a2e838a | ||
![]() |
6580d4ce92 | ||
![]() |
8c23674683 | ||
![]() |
3e28b6f2e2 | ||
![]() |
6d2e480ed5 | ||
![]() |
90a1f7e51c | ||
![]() |
63e6506510 | ||
![]() |
220fec6dc9 | ||
![]() |
534b18ee30 | ||
![]() |
e406a50b50 | ||
![]() |
b764e87a00 | ||
![]() |
b7f62c5822 | ||
![]() |
dede819a12 | ||
![]() |
083f0d16ef | ||
![]() |
c7500a504d | ||
![]() |
3348405518 | ||
![]() |
70b2ff3365 | ||
![]() |
a259a12eab | ||
![]() |
979025539e | ||
![]() |
6da311078a | ||
![]() |
7d1991ac78 | ||
![]() |
2c2199fb84 | ||
![]() |
e12da05d4e | ||
![]() |
25d10cf092 | ||
![]() |
1cdaebd92f | ||
![]() |
f5d3f1c042 | ||
![]() |
4073238103 | ||
![]() |
513eaea4f4 | ||
![]() |
b4ac3ddfbd | ||
![]() |
0a269c9e26 | ||
![]() |
1f2371641e | ||
![]() |
8b582f3fcf | ||
![]() |
1afb8f109e | ||
![]() |
5824e0b706 | ||
![]() |
97deed9299 | ||
![]() |
9efcca002d | ||
![]() |
7904483272 | ||
![]() |
8a9594d918 | ||
![]() |
90c09e967a | ||
![]() |
5ba1cc5075 | ||
![]() |
12064a086b | ||
![]() |
32d0e8bf1d | ||
![]() |
79a5947587 | ||
![]() |
c8cda3c817 | ||
![]() |
197cf0f8cc | ||
![]() |
392af26503 | ||
![]() |
41343c9774 | ||
![]() |
4afce7600b | ||
![]() |
e4b4a94a5f | ||
![]() |
3db79607b7 | ||
![]() |
2ada32be02 | ||
![]() |
a4ec8719f9 | ||
![]() |
5e6b28d965 | ||
![]() |
7d8f790708 | ||
![]() |
b6b224be77 | ||
![]() |
3b008b6359 | ||
![]() |
da80bfa3c7 | ||
![]() |
fcd06a9000 | ||
![]() |
762908207f | ||
![]() |
b40b5b95f1 | ||
![]() |
fe176f2752 | ||
![]() |
c7796e9557 | ||
![]() |
679457e36a | ||
![]() |
2d3d4db4dd | ||
![]() |
f127bbc64d | ||
![]() |
bdaf96b114 | ||
![]() |
ad55bae212 | ||
![]() |
ea8958adae | ||
![]() |
e7b664a2ff | ||
![]() |
541d1a5380 | ||
![]() |
ac179f5b45 | ||
![]() |
34f36c6179 | ||
![]() |
56c1920cc1 | ||
![]() |
456880c7cf | ||
![]() |
08222dfbec | ||
![]() |
e8d84e8ba5 | ||
![]() |
c570ce9720 | ||
![]() |
f8b66a78fa | ||
![]() |
e2fc98526b | ||
![]() |
856a393531 | ||
![]() |
8bf2a2f8db | ||
![]() |
3f6bbffcd6 | ||
![]() |
4d5087bd8d | ||
![]() |
f9663143a6 | ||
![]() |
c4aac72e68 | ||
![]() |
f4048bf4ba | ||
![]() |
b384d17fd3 | ||
![]() |
2ae30ac024 | ||
![]() |
9b9b2f0710 | ||
![]() |
5d58dfab3e | ||
![]() |
38ba6058be | ||
![]() |
0f779dd7f8 | ||
![]() |
3ca842187a | ||
![]() |
e36dada843 | ||
![]() |
1b8c567fd7 | ||
![]() |
e1c2cf770a | ||
![]() |
ab6cd578e8 | ||
![]() |
4058a0c8d0 | ||
![]() |
421e5bb169 | ||
![]() |
6faea73c9f | ||
![]() |
3a644621fe | ||
![]() |
abbfea0b6a | ||
![]() |
2f2cdad16b | ||
![]() |
aae3c26a64 | ||
![]() |
0f680bcfd6 | ||
![]() |
f71612d6cf | ||
![]() |
5cc23ab084 | ||
![]() |
9d6c0773c5 | ||
![]() |
44dca3b86d | ||
![]() |
310b81de04 | ||
![]() |
f23258eb8c | ||
![]() |
039bc587cc | ||
![]() |
46e1139946 | ||
![]() |
8938ad8f8d | ||
![]() |
102cb06d28 | ||
![]() |
17d0ae003a | ||
![]() |
7a16961387 | ||
![]() |
d3bdbce0d0 | ||
![]() |
504e4987b7 | ||
![]() |
f00de454d1 | ||
![]() |
ce35416284 | ||
![]() |
7773589e2c | ||
![]() |
5d900f9ced | ||
![]() |
7a344c865f | ||
![]() |
bd0bc2047d | ||
![]() |
2482d78a06 | ||
![]() |
18fc0d0342 | ||
![]() |
024ce5c379 | ||
![]() |
f74fe5718e | ||
![]() |
ef395d4c9f | ||
![]() |
5d42e4f68d | ||
![]() |
acce6f0b2f | ||
![]() |
dadb5f92ee | ||
![]() |
69aff1e204 | ||
![]() |
810fd802b5 | ||
![]() |
cf1b9e5067 | ||
![]() |
83aaf4699c | ||
![]() |
72aa98fe5c | ||
![]() |
86b353e627 | ||
![]() |
79183bb6ea | ||
![]() |
4921686bdf | ||
![]() |
a5bdf096dc | ||
![]() |
bfee69e7ff | ||
![]() |
db53d37493 | ||
![]() |
c294124b8d | ||
![]() |
2afc8607c6 | ||
![]() |
e2ff51f425 | ||
![]() |
25a579f7ed | ||
![]() |
ecd33fd93c |
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -13,6 +13,10 @@
|
||||
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
**UI (States or Lovelace UI?):**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
-->
|
||||
|
||||
**Browser and Operating System:**
|
||||
<!--
|
||||
|
1
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
hass_frontend_es5/*
|
||||
.reify-cache
|
||||
demo/hademo-icons.html
|
||||
|
||||
|
13
.vscode/extensions.json
vendored
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eg2.tslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html"
|
||||
]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"runem.lit-plugin"
|
||||
]
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
throw Error("latestBuild not defined for babel loader config");
|
||||
}
|
||||
return {
|
||||
test: /\.m?js$|\.ts$/,
|
||||
test: /\.m?js$|\.tsx?$/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
@@ -12,7 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
require("@babel/preset-env").default,
|
||||
{ modules: false },
|
||||
],
|
||||
require("@babel/preset-typescript").default,
|
||||
[
|
||||
require("@babel/preset-typescript").default,
|
||||
{
|
||||
jsxPragma: "h",
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
plugins: [
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
@@ -28,6 +33,14 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
pragma: "h",
|
||||
},
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-class-properties").default,
|
||||
{ loose: true },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
51
build-scripts/gulp/app.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Run HA 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-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-service-worker-dev",
|
||||
"gen-icons",
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
),
|
||||
"copy-static",
|
||||
"webpack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel("gen-icons", "build-translations"),
|
||||
"copy-static",
|
||||
gulp.parallel(
|
||||
"webpack-prod-app",
|
||||
// Do not compress static files in CI, it's SLOW.
|
||||
...(process.env.CI === "true" ? [] : ["compress-static"])
|
||||
),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
"gen-service-worker-prod"
|
||||
)
|
||||
)
|
||||
);
|
6
build-scripts/gulp/clean.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const config = require("../paths");
|
||||
|
||||
gulp.task("clean", () => del([config.root, config.build_dir]));
|
||||
gulp.task("clean-demo", () => del([config.demo_root, config.build_dir]));
|
42
build-scripts/gulp/demo.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Run HA 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-demo",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel(
|
||||
"gen-icons",
|
||||
"gen-icons-demo",
|
||||
"gen-index-demo-dev",
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static-demo",
|
||||
"webpack-dev-server-demo"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-demo",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
|
||||
"copy-static-demo",
|
||||
"webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
)
|
||||
);
|
163
build-scripts/gulp/entry-html.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// Tasks to generate entry HTML
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const template = require("lodash.template");
|
||||
const minify = require("html-minifier").minify;
|
||||
const config = require("../paths.js");
|
||||
|
||||
const templatePath = (tpl) =>
|
||||
path.resolve(config.polymer_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const demoTemplatePath = (tpl) =>
|
||||
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||
|
||||
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
const compiled = template(readFile(pathFunc(pth)));
|
||||
return compiled({ ...data, renderTemplate });
|
||||
};
|
||||
|
||||
const renderDemoTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, demoTemplatePath);
|
||||
|
||||
const minifyHtml = (content) =>
|
||||
minify(content, {
|
||||
collapseWhitespace: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
removeComments: true,
|
||||
});
|
||||
|
||||
const PAGES = ["onboarding", "authorize"];
|
||||
|
||||
gulp.task("gen-pages-dev", (done) => {
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: `/frontend_latest/${page}.js`,
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5PageJS: `/frontend_es5/${page}.js`,
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, `${page}.html`), content);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-pages-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(config.output, "manifest.json"));
|
||||
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
|
||||
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: latestManifest[`${page}.js`],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5PageJS: es5Manifest[`${page}.js`],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
|
||||
fs.outputFileSync(
|
||||
path.resolve(config.root, `${page}.html`),
|
||||
minifyHtml(content)
|
||||
);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-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 = renderTemplate("index", {
|
||||
latestAppJS: "/frontend_latest/app.js",
|
||||
latestCoreJS: "/frontend_latest/core.js",
|
||||
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5AppJS: "/frontend_es5/app.js",
|
||||
es5CoreJS: "/frontend_es5/core.js",
|
||||
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-app-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(config.output, "manifest.json"));
|
||||
const es5Manifest = require(path.resolve(config.output_es5, "manifest.json"));
|
||||
const content = renderTemplate("index", {
|
||||
latestAppJS: latestManifest["app.js"],
|
||||
latestCoreJS: latestManifest["core.js"],
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5AppJS: es5Manifest["app.js"],
|
||||
es5CoreJS: es5Manifest["core.js"],
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, "index.html"), minified);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-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 = renderDemoTemplate("index", {
|
||||
latestDemoJS: "/frontend_latest/main.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5DemoJS: "/frontend_es5/main.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-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 = renderDemoTemplate("index", {
|
||||
latestDemoJS: "/frontend_latest/main.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5DemoJS: "/frontend_es5/main.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-demo-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(
|
||||
config.demo_output,
|
||||
"manifest.json"
|
||||
));
|
||||
const es5Manifest = require(path.resolve(
|
||||
config.demo_output_es5,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderDemoTemplate("index", {
|
||||
latestDemoJS: latestManifest["main.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5DemoJS: es5Manifest["main.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
|
||||
done();
|
||||
});
|
110
build-scripts/gulp/gather-static.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// Gulp task to gather all static files.
|
||||
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const zopfli = require("gulp-zopfli-green");
|
||||
const merge = require("merge-stream");
|
||||
const paths = require("../paths");
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.polymer_dir, "node_modules", ...parts);
|
||||
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
|
||||
|
||||
const copyFileDir = (fromFile, toDir) =>
|
||||
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
|
||||
|
||||
const genStaticPath = (staticDir) => (...parts) =>
|
||||
path.resolve(staticDir, ...parts);
|
||||
|
||||
function copyTranslations(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Translation output
|
||||
fs.copySync(
|
||||
polyPath("build-translations/output"),
|
||||
staticPath("translations")
|
||||
);
|
||||
}
|
||||
|
||||
function copyPolyfills(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Web Component polyfills and adapters
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
}
|
||||
|
||||
function copyFonts(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
// Local fonts
|
||||
fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts"));
|
||||
}
|
||||
|
||||
function compressStatic(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
const fonts = gulp
|
||||
.src(staticPath("fonts/**/*.ttf"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(staticPath("fonts")));
|
||||
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(fonts, polyfills, translations);
|
||||
}
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
// Basic static files
|
||||
fs.copySync(polyPath("public"), paths.root);
|
||||
|
||||
copyPolyfills(staticDir);
|
||||
copyFonts(staticDir);
|
||||
copyTranslations(staticDir);
|
||||
|
||||
// Panel assets
|
||||
copyFileDir(
|
||||
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
|
||||
staticPath("panels/calendar/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("compress-static", () => compressStatic(paths.static));
|
||||
|
||||
gulp.task("copy-static-demo", (done) => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public"), paths.demo_root);
|
||||
// Copy demo static files
|
||||
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
|
||||
|
||||
copyPolyfills(paths.demo_static);
|
||||
copyFonts(paths.demo_static);
|
||||
copyTranslations(paths.demo_static);
|
||||
done();
|
||||
});
|
@@ -1,7 +1,7 @@
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const config = require("../config");
|
||||
const paths = require("../paths");
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(
|
||||
__dirname,
|
||||
@@ -38,13 +38,13 @@ function loadIcon(name) {
|
||||
function transformXMLtoPolymer(name, xml) {
|
||||
const start = xml.indexOf("><path") + 1;
|
||||
const end = xml.length - start - 6;
|
||||
const path = xml.substr(start, end);
|
||||
return `<g id="${name}">${path}</g>`;
|
||||
const pth = xml.substr(start, end);
|
||||
return `<g id="${name}">${pth}</g>`;
|
||||
}
|
||||
|
||||
// Given an iconset name and icon names, generate a polymer iconset
|
||||
function generateIconset(name, iconNames) {
|
||||
const iconDefs = iconNames
|
||||
function generateIconset(iconsetName, iconNames) {
|
||||
const iconDefs = Array.from(iconNames)
|
||||
.map((name) => {
|
||||
const iconDef = loadIcon(name);
|
||||
if (!iconDef) {
|
||||
@@ -53,7 +53,7 @@ function generateIconset(name, iconNames) {
|
||||
return transformXMLtoPolymer(name, iconDef);
|
||||
})
|
||||
.join("");
|
||||
return `<ha-iconset-svg name="${name}" 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
|
||||
@@ -62,7 +62,9 @@ function genMDIIcons() {
|
||||
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
||||
);
|
||||
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
||||
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
||||
}
|
||||
|
||||
@@ -81,7 +83,7 @@ function mapFiles(startPath, filter, mapFunc) {
|
||||
}
|
||||
|
||||
// Find all icons used by the project.
|
||||
function findIcons(path, iconsetName) {
|
||||
function findIcons(searchPath, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||
const icons = new Set();
|
||||
function processFile(filename) {
|
||||
@@ -93,20 +95,38 @@ function findIcons(path, iconsetName) {
|
||||
icons.add(match[0].substr(iconsetName.length + 1));
|
||||
}
|
||||
}
|
||||
mapFiles(path, ".js", processFile);
|
||||
mapFiles(path, ".ts", processFile);
|
||||
return Array.from(icons);
|
||||
mapFiles(searchPath, ".js", processFile);
|
||||
mapFiles(searchPath, ".ts", processFile);
|
||||
return icons;
|
||||
}
|
||||
|
||||
function genHassIcons() {
|
||||
const iconNames = findIcons("./src", "hass").concat(BUILT_IN_PANEL_ICONS);
|
||||
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR);
|
||||
const iconNames = findIcons("./src", "hass");
|
||||
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
||||
}
|
||||
|
||||
gulp.task("gen-icons-mdi", () => genMDIIcons());
|
||||
gulp.task("gen-icons-hass", () => genHassIcons());
|
||||
gulp.task("gen-icons", ["gen-icons-hass", "gen-icons-mdi"], () => {});
|
||||
gulp.task("gen-icons-mdi", (done) => {
|
||||
genMDIIcons();
|
||||
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) => {
|
||||
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.demo_dir, "hademo-icons.html"),
|
||||
generateIconset("hademo", iconNames)
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
findIcons,
|
29
build-scripts/gulp/service-worker.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Generate service worker.
|
||||
// Based on manifest, create a file with the content as service_worker.js
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable global-require */
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const config = require("../paths.js");
|
||||
|
||||
const swPath = path.resolve(config.root, "service_worker.js");
|
||||
|
||||
const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n");
|
||||
|
||||
gulp.task("gen-service-worker-dev", (done) => {
|
||||
writeSW(
|
||||
`
|
||||
console.debug('Service worker disabled in development');
|
||||
`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-service-worker-prod", (done) => {
|
||||
fs.copySync(
|
||||
path.resolve(config.output, "service_worker.js"),
|
||||
path.resolve(config.root, "service_worker.js")
|
||||
);
|
||||
done();
|
||||
});
|
392
build-scripts/gulp/translations.js
Executable file
@@ -0,0 +1,392 @@
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const hash = require("gulp-hash");
|
||||
const hashFilename = require("gulp-hash-filename");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
|
||||
String.prototype.rsplit = function(sep, maxsplit) {
|
||||
var split = this.split(sep);
|
||||
return maxsplit
|
||||
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
|
||||
: split;
|
||||
};
|
||||
|
||||
// Panel translations which should be split from the core translations. These
|
||||
// should mirror the fragment definitions in polymer.json, so that we load
|
||||
// additional resources at equivalent points.
|
||||
const TRANSLATION_FRAGMENTS = [
|
||||
"config",
|
||||
"history",
|
||||
"logbook",
|
||||
"mailbox",
|
||||
"profile",
|
||||
"shopping-list",
|
||||
"page-authorize",
|
||||
"page-onboarding",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
output = Object.assign(
|
||||
{},
|
||||
output,
|
||||
recursiveFlatten(prefix + key + ".", data[key])
|
||||
);
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function flatten(data) {
|
||||
return recursiveFlatten("", data);
|
||||
}
|
||||
|
||||
function emptyFilter(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = emptyFilter(data[key]);
|
||||
} else {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
function recursiveEmpty(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = recursiveEmpty(data[key]);
|
||||
} else {
|
||||
newData[key] = "TRANSLATED";
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Lokalise key placeholders with their actual values.
|
||||
*
|
||||
* We duplicate the behavior of Lokalise here so that placeholders can
|
||||
* be included in src/translations/en.json, but still be usable while
|
||||
* developing locally.
|
||||
*
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokalise_transform(data, original) {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokalise_transform(value, original);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
||||
const replace = key.split("::").reduce((tr, k) => tr[k], original);
|
||||
if (typeof replace !== "string") {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${key} in src/translations/en.json`
|
||||
);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
let taskName = "clean-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${outDir}/**/*.json`]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "create-test-metadata";
|
||||
gulp.task(taskName, function(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "create-test-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("create-test-metadata", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return recursiveEmpty(data);
|
||||
})
|
||||
)
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
* all languages. This starts with src/translations/en.json, and replaces all
|
||||
* Lokalise key placeholders with their target values. Under normal circumstances,
|
||||
* this will be the same as translations/en.json However, we build it here to
|
||||
* facilitate both making changes in development mode, and to ensure that the
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
taskName = "build-master-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("clean-translations", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokalise_transform(data, data);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-master-translation", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.pipe(
|
||||
foreach(function(stream, file) {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = "build-flattened-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series(...splitTasks, function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(hashFilename())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translation-fingerprints";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-flattened-translations", function() {
|
||||
return gulp
|
||||
.src(outDir + "/**/*.json")
|
||||
.pipe(
|
||||
rename({
|
||||
extname: "",
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
hash({
|
||||
algorithm: "md5",
|
||||
hashLength: 32,
|
||||
template: "<%= name %>.json",
|
||||
})
|
||||
)
|
||||
.pipe(hash.manifest("translationFingerprints.json"))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// After generating fingerprints of our translation files, consolidate
|
||||
// all translation fragment fingerprints under the translation name key
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const [path, _md5] = key.rsplit("-", 1);
|
||||
// let translation = key;
|
||||
let translation = path;
|
||||
const parts = translation.split("/");
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[path] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-translation-fingerprints", function() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
"src/translations/translationMetadata.json",
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
module.exports = tasks;
|
116
build-scripts/gulp/webpack.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Tasks to run webpack.
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const log = require("fancy-log");
|
||||
const paths = require("../paths");
|
||||
const { createAppConfig, createDemoConfig } = require("../webpack");
|
||||
|
||||
const handler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
console.log(err.stack || err);
|
||||
if (err.details) {
|
||||
console.log(err.details);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
|
||||
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-app",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-demo", () => {
|
||||
const compiler = webpack([
|
||||
createDemoConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createDemoConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase: path.resolve(paths.demo_dir, "dist"),
|
||||
}).listen(8080, "localhost", function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", "http://localhost:8080");
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-demo",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
17
build-scripts/paths.js
Normal file
@@ -0,0 +1,17 @@
|
||||
var path = require("path");
|
||||
|
||||
module.exports = {
|
||||
polymer_dir: path.resolve(__dirname, ".."),
|
||||
|
||||
build_dir: path.resolve(__dirname, "../build"),
|
||||
root: path.resolve(__dirname, "../hass_frontend"),
|
||||
static: path.resolve(__dirname, "../hass_frontend/static"),
|
||||
output: path.resolve(__dirname, "../hass_frontend/frontend_latest"),
|
||||
output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
|
||||
|
||||
demo_dir: path.resolve(__dirname, "../demo"),
|
||||
demo_root: path.resolve(__dirname, "../demo/dist"),
|
||||
demo_static: path.resolve(__dirname, "../demo/dist/static"),
|
||||
demo_output: path.resolve(__dirname, "../demo/dist/frontend_latest"),
|
||||
demo_output_es5: path.resolve(__dirname, "../demo/dist/frontend_es5"),
|
||||
};
|
222
build-scripts/webpack.js
Normal file
@@ -0,0 +1,222 @@
|
||||
const webpack = require("webpack");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-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 paths = require("./paths.js");
|
||||
const { babelLoaderConfig } = require("./babel.js");
|
||||
|
||||
let version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||
.match(/\d{8}\.\d+/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
version = version[0];
|
||||
|
||||
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
|
||||
const genDevTool = (isProdBuild) =>
|
||||
isProdBuild ? "cheap-source-map" : "inline-cheap-module-source-map";
|
||||
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
|
||||
if (!isProdBuild || dontHash.has(chunk.name)) {
|
||||
return `${chunk.name}.js`;
|
||||
}
|
||||
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 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,
|
||||
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 {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
entry,
|
||||
module: {
|
||||
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__DEMO__: false,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
...plugins,
|
||||
isProdBuild &&
|
||||
!isCI &&
|
||||
!isStatsBuild &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
algorithm(input, compressionOptions, callback) {
|
||||
return zopfli.gzip(input, compressionOptions, callback);
|
||||
},
|
||||
}),
|
||||
latestBuild &&
|
||||
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.ttf":
|
||||
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf",
|
||||
"/static/fonts/roboto/Roboto-Medium.ttf":
|
||||
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf",
|
||||
"/static/fonts/roboto/Roboto-Regular.ttf":
|
||||
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf",
|
||||
"/static/fonts/roboto/Roboto-Bold.ttf":
|
||||
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf",
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
output: {
|
||||
filename: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: latestBuild ? paths.output : paths.output_es5,
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
},
|
||||
resolve,
|
||||
};
|
||||
};
|
||||
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
entry: {
|
||||
main: "./demo/src/entrypoint.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
},
|
||||
module: {
|
||||
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify("DEMO"),
|
||||
__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/",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolve,
|
||||
plugins,
|
||||
optimization,
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
};
|
@@ -1,29 +0,0 @@
|
||||
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
|
||||
|
||||
module.exports.minimizer = [
|
||||
// Took options from Polymer build tool
|
||||
// https://github.com/Polymer/tools/blob/master/packages/build/src/js-transform.ts
|
||||
new BabelMinifyPlugin(
|
||||
{
|
||||
// Disable the minify-constant-folding plugin because it has a bug relating
|
||||
// to invalid substitution of constant values into export specifiers:
|
||||
// https://github.com/babel/minify/issues/820
|
||||
evaluate: false,
|
||||
|
||||
// TODO(aomarks) Find out why we disabled this plugin.
|
||||
simplifyComparisons: false,
|
||||
|
||||
// Prevent removal of things that babel thinks are unreachable, but sometimes
|
||||
// gets wrong: https://github.com/Polymer/tools/issues/724
|
||||
deadcode: false,
|
||||
|
||||
// Disable the simplify plugin because it can eat some statements preceeding
|
||||
// loops. https://github.com/babel/minify/issues/824
|
||||
simplify: false,
|
||||
|
||||
// This is breaking ES6 output. https://github.com/Polymer/tools/issues/261
|
||||
mangle: false,
|
||||
},
|
||||
{}
|
||||
),
|
||||
];
|
0
demo/public/service_worker.js
Normal file
@@ -4,16 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUTPUT_DIR=dist
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
mkdir $OUTPUT_DIR
|
||||
node script/gen-icons.js
|
||||
|
||||
cd ..
|
||||
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
|
||||
cd demo
|
||||
|
||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
|
||||
./node_modules/.bin/gulp build-demo
|
||||
|
@@ -4,12 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
node script/gen-icons.js
|
||||
|
||||
cd ..
|
||||
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
|
||||
cd demo
|
||||
|
||||
../node_modules/.bin/webpack-dev-server
|
||||
./node_modules/.bin/gulp develop-demo
|
||||
|
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
} = require("../../gulp/tasks/gen-icons.js");
|
||||
|
||||
function genHademoIcons() {
|
||||
const iconNames = findIcons("./src", "hademo");
|
||||
fs.writeFileSync("./hademo-icons.html", generateIconset("hademo", iconNames));
|
||||
}
|
||||
|
||||
genMDIIcons();
|
||||
genHademoIcons();
|
11
demo/script/size_stats
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Analyze stats
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
|
||||
npx webpack-bundle-analyzer compilation-stats.json dist
|
||||
rm compilation-stats.json
|
@@ -528,7 +528,7 @@ export const demoLovelaceArsaboo: () => LovelaceConfig = () => ({
|
||||
{
|
||||
type: "iframe",
|
||||
aspect_ratio: "90%",
|
||||
url: "https://embed.windy.com/embed2.html?rain,32.487,-84.023,5",
|
||||
url: "https://embed.windy.com/embed2.html?lat=32.487&lon=-84.023&zoom=5&level=surface&overlay=rain&menu=&message=&marker=&calendar=&pressure=&type=map&location=coordinates&detail=&detailLat=32.487&detailLon=--84.023&metricWind=default&metricTemp=default&radarRange=-1",
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
|
@@ -7,9 +7,11 @@ import {
|
||||
} from "lit-element";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import "@polymer/paper-icon-button";
|
||||
import "@polymer/paper-button";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-paper-icon-button-next";
|
||||
import "../../../src/components/ha-paper-icon-button-prev";
|
||||
import { LovelaceCard, Lovelace } from "../../../src/panels/lovelace/types";
|
||||
import { LovelaceCardConfig } from "../../../src/data/lovelace";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
@@ -47,12 +49,10 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="picker">
|
||||
<paper-icon-button
|
||||
<ha-paper-icon-button-prev
|
||||
@click=${this._prevConfig}
|
||||
icon="hass:chevron-right"
|
||||
style="transform: rotate(180deg)"
|
||||
.disabled=${this._switching}
|
||||
></paper-icon-button>
|
||||
></ha-paper-icon-button-prev>
|
||||
<div>
|
||||
${this._switching
|
||||
? html`
|
||||
@@ -73,11 +73,10 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<paper-icon-button
|
||||
<ha-paper-icon-button-next
|
||||
@click=${this._nextConfig}
|
||||
icon="hass:chevron-right"
|
||||
.disabled=${this._switching}
|
||||
></paper-icon-button>
|
||||
></ha-paper-icon-button-next>
|
||||
</div>
|
||||
<div class="content">
|
||||
Welcome home! You've reached the Home Assistant demo where we showcase
|
||||
@@ -85,7 +84,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="https://www.home-assistant.io" target="_blank">
|
||||
<paper-button>Learn more about Home Assistant</paper-button>
|
||||
<mwc-button>Learn more about Home Assistant</mwc-button>
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -146,12 +145,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.actions paper-button {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
padding-left: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/app/home-assistant";
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import {
|
||||
provideHass,
|
||||
MockHomeAssistant,
|
||||
@@ -15,9 +15,10 @@ import { mockTemplate } from "./stubs/template";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
|
||||
class HaDemo extends HomeAssistantAppEl {
|
||||
protected async _handleConnProm() {
|
||||
protected async _initialize() {
|
||||
const initial: Partial<MockHomeAssistant> = {
|
||||
panelUrl: (this as any).panelUrl,
|
||||
// Override updateHass so that the correct hass lifecycle methods are called
|
||||
@@ -25,7 +26,7 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
this._updateHass(hassUpdate),
|
||||
};
|
||||
|
||||
const hass = provideHass(this, initial);
|
||||
const hass = (this.hass = provideHass(this, initial));
|
||||
mockLovelace(hass);
|
||||
mockAuth(hass);
|
||||
mockTranslations(hass);
|
||||
@@ -35,6 +36,7 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
mockTemplate(hass);
|
||||
mockEvents(hass);
|
||||
mockMediaPlayer(hass);
|
||||
mockFrontend(hass);
|
||||
selectedDemoConfig.then((conf) => {
|
||||
hass.addEntities(conf.entities());
|
||||
if (conf.theme) {
|
||||
|
@@ -74,9 +74,6 @@
|
||||
content="https://www.home-assistant.io/images/default-social.png"
|
||||
/>
|
||||
<title>Home Assistant Demo</title>
|
||||
<script src="./custom-elements-es5-adapter.js"></script>
|
||||
<script src="./compatibility.js"></script>
|
||||
<script src="./main.js" async></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
@@ -96,7 +93,23 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ha-demo><div id="ha-init-skeleton"></div></ha-demo>
|
||||
<div id="ha-init-skeleton"></div>
|
||||
<ha-demo></ha-demo>
|
||||
<%= renderTemplate('_js_base') %>
|
||||
|
||||
<script type="module" src="<%= latestDemoJS %>"></script>
|
||||
|
||||
<script nomodule>
|
||||
(function() {
|
||||
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
|
||||
if (!isS101) {
|
||||
_ls("/static/polyfills/custom-elements-es5-adapter.js");
|
||||
_ls("<%= es5Compatibility %>");
|
||||
_ls("<%= es5DemoJS %>");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
|
||||
(function(d, t) {
|
7
demo/src/stubs/frontend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
};
|
@@ -3,7 +3,6 @@ import "../custom-cards/ha-demo-card";
|
||||
// tslint:disable-next-line
|
||||
import { HADemoCard } from "../custom-cards/ha-demo-card";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { HUIView } from "../../../src/panels/lovelace/hui-view";
|
||||
import { selectedDemoConfig } from "../configs/demo-configs";
|
||||
|
||||
export const mockLovelace = (hass: MockHomeAssistant) => {
|
||||
@@ -16,13 +15,17 @@ export const mockLovelace = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
};
|
||||
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView.prototype.createCardElement;
|
||||
customElements.whenDefined("hui-view").then(() => {
|
||||
// tslint:disable-next-line
|
||||
const HUIView = customElements.get("hui-view");
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView.prototype.createCardElement;
|
||||
|
||||
HUIView.prototype.createCardElement = function(config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (el.tagName === "HA-DEMO-CARD") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
HUIView.prototype.createCardElement = function(config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (el.tagName === "HA-DEMO-CARD") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
});
|
||||
|
@@ -1,100 +1,13 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||
const { babelLoaderConfig } = require("../config/babel.js");
|
||||
const { minimizer } = require("../config/babel.js");
|
||||
const { createDemoConfig } = require("../build-scripts/webpack.js");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const buildPath = path.resolve(__dirname, "dist");
|
||||
const publicPath = "/";
|
||||
// This file exists because we haven't migrated the stats script yet
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const isStatsBuild = process.env.STATS === "1";
|
||||
const latestBuild = false;
|
||||
|
||||
module.exports = {
|
||||
mode: isProd ? "production" : "development",
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProd ? "none" : "inline-source-map",
|
||||
entry: {
|
||||
main: "./src/entrypoint.ts",
|
||||
compatibility: "../src/entrypoints/compatibility.js",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: false,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify("DEMO"),
|
||||
__DEMO__: true,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProd ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
"public",
|
||||
"../node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
|
||||
{ from: "../public", to: "static" },
|
||||
{ from: "../build-translations/output", to: "static/translations" },
|
||||
{
|
||||
from: "../node_modules/leaflet/dist/leaflet.css",
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
{
|
||||
from: "../node_modules/@polymer/font-roboto-local/fonts",
|
||||
to: "static/fonts",
|
||||
},
|
||||
{
|
||||
from: "../node_modules/leaflet/dist/images",
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
]),
|
||||
isProd &&
|
||||
new WorkboxPlugin.GenerateSW({
|
||||
swDest: "service_worker_es5.js",
|
||||
importWorkboxFrom: "local",
|
||||
}),
|
||||
].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: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
path: buildPath,
|
||||
publicPath,
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./public",
|
||||
},
|
||||
};
|
||||
module.exports = createDemoConfig({
|
||||
isProdBuild,
|
||||
isStatsBuild,
|
||||
latestBuild,
|
||||
});
|
||||
|
@@ -2,6 +2,19 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("sensor", "brightness", "12", {}),
|
||||
getEntity("plant", "bonsai", "ok", {}),
|
||||
getEntity("sensor", "outside_humidity", "54", {
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
getEntity("sensor", "outside_temperature", "15.6", {
|
||||
unit_of_measurement: "°C",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@@ -66,7 +79,7 @@ const CONFIGS = [
|
||||
class DemoGaugeEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -78,6 +91,12 @@ class DemoGaugeEntity extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-gauge-card", DemoGaugeEntity);
|
||||
|
@@ -2,6 +2,17 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "kitchen_lights", "on", {
|
||||
friendly_name: "Kitchen Lights",
|
||||
}),
|
||||
getEntity("light", "bed_light", "off", {
|
||||
friendly_name: "Bed Light",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@@ -10,6 +21,8 @@ const CONFIGS = [
|
||||
- type: picture-entity
|
||||
image: /images/kitchen.png
|
||||
entity: light.kitchen_lights
|
||||
tap_action:
|
||||
action: toggle
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -18,6 +31,8 @@ const CONFIGS = [
|
||||
- type: picture-entity
|
||||
image: /images/bed.png
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
action: toggle
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -68,7 +83,7 @@ const CONFIGS = [
|
||||
class DemoPicEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,6 +95,12 @@ class DemoPicEntity extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-entity-card", DemoPicEntity);
|
||||
|
@@ -2,6 +2,25 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/demo-cards";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("switch", "decorative_lights", "on", {
|
||||
friendly_name: "Decorative Lights",
|
||||
}),
|
||||
getEntity("light", "ceiling_lights", "on", {
|
||||
friendly_name: "Ceiling Lights",
|
||||
}),
|
||||
getEntity("binary_sensor", "movement_backyard", "on", {
|
||||
friendly_name: "Movement Backyard",
|
||||
device_class: "moving",
|
||||
}),
|
||||
getEntity("binary_sensor", "basement_floor_wet", "off", {
|
||||
friendly_name: "Basement Floor Wet",
|
||||
device_class: "moisture",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
@@ -105,7 +124,7 @@ const CONFIGS = [
|
||||
class DemoPicGlance extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,6 +136,12 @@ class DemoPicGlance extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-picture-glance-card", DemoPicGlance);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { html, LitElement, TemplateResult } from "lit-element";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@material/mwc-button";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
|
||||
@@ -11,13 +11,13 @@ export class DemoUtilLongPress extends LitElement {
|
||||
${[1, 2, 3].map(
|
||||
() => html`
|
||||
<ha-card>
|
||||
<paper-button
|
||||
<mwc-button
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
>
|
||||
(long) press me!
|
||||
</paper-button>
|
||||
</mwc-button>
|
||||
|
||||
<textarea></textarea>
|
||||
|
||||
@@ -60,11 +60,6 @@ export class DemoUtilLongPress extends LitElement {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 50px;
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ 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/iron-icon/iron-icon";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
@@ -10,6 +9,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../src/managers/notification-manager";
|
||||
import "../../src/components/ha-card";
|
||||
|
||||
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
|
||||
|
||||
@@ -38,13 +38,13 @@ class HaGallery extends PolymerElement {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pickers paper-card {
|
||||
.pickers ha-card {
|
||||
width: 400px;
|
||||
display: block;
|
||||
margin: 16px 8px;
|
||||
}
|
||||
|
||||
.pickers paper-card:last-child {
|
||||
.pickers ha-card:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class HaGallery extends PolymerElement {
|
||||
<div id='demo'></div>
|
||||
<template is='dom-if' if='[[!_demo]]'>
|
||||
<div class='pickers'>
|
||||
<paper-card heading="Lovelace card demos">
|
||||
<ha-card header="Lovelace card demos">
|
||||
<div class='card-content intro'>
|
||||
<p>
|
||||
Lovelace has many different cards. Each card allows the user to tell a different story about what is going on in their house. These cards are very customizable, as no household is the same.
|
||||
@@ -101,9 +101,9 @@ class HaGallery extends PolymerElement {
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</paper-card>
|
||||
</ha-card>
|
||||
|
||||
<paper-card heading="More Info demos">
|
||||
<ha-card header="More Info demos">
|
||||
<div class='card-content intro'>
|
||||
<p>
|
||||
More info screens show up when an entity is clicked.
|
||||
@@ -117,9 +117,9 @@ class HaGallery extends PolymerElement {
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</paper-card>
|
||||
</ha-card>
|
||||
|
||||
<paper-card heading="Util demos">
|
||||
<ha-card header="Util demos">
|
||||
<div class='card-content intro'>
|
||||
<p>
|
||||
Test pages for our utility functions.
|
||||
@@ -133,7 +133,7 @@ class HaGallery extends PolymerElement {
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</paper-card>
|
||||
</ha-card>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@@ -1,12 +1,13 @@
|
||||
const path = require("path");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const { babelLoaderConfig } = require("../config/babel.js");
|
||||
const { minimizer } = require("../config/babel.js");
|
||||
const { babelLoaderConfig } = require("../build-scripts/babel.js");
|
||||
const webpackBase = require("../build-scripts/webpack.js");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const buildPath = path.resolve(__dirname, "dist");
|
||||
const publicPath = isProd ? "./" : "http://localhost:8080/";
|
||||
const latestBuild = true;
|
||||
|
||||
module.exports = {
|
||||
mode: isProd ? "production" : "development",
|
||||
@@ -16,7 +17,7 @@ module.exports = {
|
||||
entry: "./src/entrypoint.js",
|
||||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild: true }),
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
@@ -32,9 +33,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer,
|
||||
},
|
||||
optimization: webpackBase.optimization(latestBuild),
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
"public",
|
||||
@@ -53,19 +52,8 @@ module.exports = {
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
]),
|
||||
isProd &&
|
||||
new UglifyJsPlugin({
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
uglifyOptions: {
|
||||
// Disabling because it broke output
|
||||
mangle: false,
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
},
|
||||
resolve: webpackBase.resolve,
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
|
@@ -1,8 +0,0 @@
|
||||
var path = require("path");
|
||||
|
||||
module.exports = {
|
||||
polymer_dir: path.resolve(__dirname, ".."),
|
||||
build_dir: path.resolve(__dirname, "../build"),
|
||||
output: path.resolve(__dirname, "../hass_frontend"),
|
||||
output_es5: path.resolve(__dirname, "../hass_frontend_es5"),
|
||||
};
|
@@ -1,299 +0,0 @@
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const foreach = require("gulp-foreach");
|
||||
const hash = require("gulp-hash");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
|
||||
const isDemo = process.env.DEMO === "1";
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
|
||||
// Panel translations which should be split from the core translations. These
|
||||
// should mirror the fragment definitions in polymer.json, so that we load
|
||||
// additional resources at equivalent points.
|
||||
const TRANSLATION_FRAGMENTS = [
|
||||
"config",
|
||||
"history",
|
||||
"logbook",
|
||||
"mailbox",
|
||||
"profile",
|
||||
"shopping-list",
|
||||
"page-authorize",
|
||||
"page-onboarding",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
output = Object.assign(
|
||||
{},
|
||||
output,
|
||||
recursiveFlatten(prefix + key + ".", data[key])
|
||||
);
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function flatten(data) {
|
||||
return recursiveFlatten("", data);
|
||||
}
|
||||
|
||||
function emptyFilter(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = emptyFilter(data[key]);
|
||||
} else {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace Lokalise key placeholders with their actual values.
|
||||
*
|
||||
* We duplicate the behavior of Lokalise here so that placeholders can
|
||||
* be included in src/translations/en.json, but still be usable while
|
||||
* developing locally.
|
||||
*
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokalise_transform(data, original) {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokalise_transform(value, original);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
||||
const replace = key.split("::").reduce((tr, k) => tr[k], original);
|
||||
if (typeof replace !== "string") {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${key} in src/translations/en.json`
|
||||
);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
* all languages. This starts with src/translations/en.json, and replaces all
|
||||
* Lokalise key placeholders with their target values. Under normal circumstances,
|
||||
* this will be the same as translations/en.json However, we build it here to
|
||||
* facilitate both making changes in development mode, and to ensure that the
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
let taskName = "build-master-translation";
|
||||
gulp.task(taskName, function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokalise_transform(data, data);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(taskName, ["build-master-translation"], function() {
|
||||
return gulp.src(inDir + "/*.json").pipe(
|
||||
foreach(function(stream, file) {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
return gulp
|
||||
.src(src)
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(taskName, ["build-merged-translations"], function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(taskName, ["build-merged-translations"], function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = "build-flattened-translations";
|
||||
gulp.task(taskName, splitTasks, function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translation-fingerprints";
|
||||
gulp.task(taskName, ["build-flattened-translations"], function() {
|
||||
return gulp
|
||||
.src(outDir + "/**/*.json")
|
||||
.pipe(
|
||||
rename({
|
||||
extname: "",
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
hash({
|
||||
algorithm: "md5",
|
||||
hashLength: 32,
|
||||
template: isDemo ? "<%= name %>.json" : "<%= name %>-<%= hash %>.json",
|
||||
})
|
||||
)
|
||||
.pipe(hash.manifest("translationFingerprints.json"))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// After generating fingerprints of our translation files, consolidate
|
||||
// all translation fragment fingerprints under the translation name key
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const parts = key.split("/");
|
||||
let translation = key;
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(taskName, ["build-translation-fingerprints"], function() {
|
||||
return gulp
|
||||
.src([
|
||||
"src/translations/translationMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
])
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
module.exports = tasks;
|
@@ -1,3 +1,3 @@
|
||||
var requireDir = require('require-dir');
|
||||
var requireDir = require("require-dir");
|
||||
|
||||
requireDir('./gulp/tasks/');
|
||||
requireDir("./build-scripts/gulp/");
|
||||
|
@@ -4,12 +4,15 @@ const {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
} = require("../../gulp/tasks/gen-icons.js");
|
||||
|
||||
const MENU_BUTTON_ICON = "menu";
|
||||
} = require("../../build-scripts/gulp/gen-icons.js");
|
||||
|
||||
function genHassioIcons() {
|
||||
const iconNames = findIcons("./src", "hassio").concat(MENU_BUTTON_ICON);
|
||||
const iconNames = findIcons("./src", "hassio");
|
||||
|
||||
for (const item of findIcons("../src", "hassio")) {
|
||||
iconNames.add(item);
|
||||
}
|
||||
|
||||
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
|
||||
}
|
||||
|
||||
|
@@ -1,103 +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 HassioAddonRepository extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style hassio-style">
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
a.repo {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[addons.length]]">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
[[repo.name]]
|
||||
<div class="description">
|
||||
Maintained by [[repo.maintainer]]
|
||||
<a class="repo" href="[[repo.url]]" target="_blank"
|
||||
>[[repo.url]]</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[addons]]"
|
||||
as="addon"
|
||||
sort="sortAddons"
|
||||
>
|
||||
<paper-card class$="[[computeClass(addon)]]" 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>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
repo: Object,
|
||||
addons: Array,
|
||||
};
|
||||
}
|
||||
|
||||
sortAddons(a, b) {
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
}
|
||||
|
||||
computeIcon(addon) {
|
||||
return addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
computeIconClass(addon) {
|
||||
if (addon.installed)
|
||||
return addon.installed !== addon.version ? "update" : "installed";
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
computeClass(addon) {
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
addonTapped(ev) {
|
||||
this.navigate(`/hassio/addon/${ev.model.addon.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-repository", HassioAddonRepository);
|
135
hassio/src/addon-store/hassio-addon-repository.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
css,
|
||||
TemplateResult,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
CSSResultArray,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../src/data/hassio";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public repo!: HassioAddonRepository;
|
||||
@property() public addons!: HassioAddonInfo[];
|
||||
@property() public filter!: string;
|
||||
|
||||
private _getAddons = memoizeOne(
|
||||
(addons: HassioAddonInfo[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const repo = this.repo;
|
||||
const addons = this._getAddons(this.addons, this.filter);
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
<div class="description">
|
||||
No results found in "${repo.name}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
${repo.name}
|
||||
<div class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
private computeIcon(addon) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
hassioStyle,
|
||||
css`
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
a.repo {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-repository", HassioAddonRepositoryEl);
|
@@ -1,92 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hassio-addon-repository";
|
||||
import "./hassio-repositories-editor";
|
||||
|
||||
class HassioAddonStore extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
<hassio-repositories-editor
|
||||
hass="[[hass]]"
|
||||
repos="[[repos]]"
|
||||
></hassio-repositories-editor>
|
||||
|
||||
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
|
||||
<hassio-addon-repository
|
||||
hass="[[hass]]"
|
||||
repo="[[repo]]"
|
||||
addons="[[computeAddons(repo.slug)]]"
|
||||
></hassio-addon-repository>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
addons: Array,
|
||||
repos: Array,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
sortRepos(a, b) {
|
||||
if (a.slug === "local") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "local") {
|
||||
return 1;
|
||||
}
|
||||
if (a.slug === "core") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "core") {
|
||||
return 1;
|
||||
}
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
}
|
||||
|
||||
computeAddons(repo) {
|
||||
return this.addons.filter(function(addon) {
|
||||
return addon.repository === repo;
|
||||
});
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.hass.callApi("get", "hassio/addons").then(
|
||||
(info) => {
|
||||
this.addons = info.data.addons;
|
||||
this.repos = info.data.repositories;
|
||||
},
|
||||
() => {
|
||||
this.addons = [];
|
||||
this.repos = [];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.hass.callApi("post", "hassio/addons/reload").then(() => {
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-store", HassioAddonStore);
|
129
hassio/src/addon-store/hassio-addon-store.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import "./hassio-addon-repository";
|
||||
import "./hassio-repositories-editor";
|
||||
import { TemplateResult, html } from "lit-html";
|
||||
import {
|
||||
LitElement,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
HassioAddonRepository,
|
||||
HassioAddonInfo,
|
||||
fetchHassioAddonsInfo,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio";
|
||||
import "../../../src/layouts/loading-screen";
|
||||
import "../components/hassio-search-input";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "local") {
|
||||
return 1;
|
||||
}
|
||||
if (a.slug === "core") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "core") {
|
||||
return 1;
|
||||
}
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
};
|
||||
|
||||
class HassioAddonStore extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _addons?: HassioAddonInfo[];
|
||||
@property() private _repos?: HassioAddonRepository[];
|
||||
@property() private _filter?: string;
|
||||
|
||||
public async refreshData() {
|
||||
this._repos = undefined;
|
||||
this._addons = undefined;
|
||||
this._filter = undefined;
|
||||
await reloadHassioAddons(this.hass);
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._addons || !this._repos) {
|
||||
return html`
|
||||
<loading-screen></loading-screen>
|
||||
`;
|
||||
}
|
||||
const repos: TemplateResult[] = [];
|
||||
|
||||
for (const repo of this._repos) {
|
||||
const addons = this._addons!.filter(
|
||||
(addon) => addon.repository === repo.slug
|
||||
);
|
||||
|
||||
if (addons.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
repos.push(html`
|
||||
<hassio-addon-repository
|
||||
.hass=${this.hass}
|
||||
.repo=${repo}
|
||||
.addons=${addons}
|
||||
.filter=${this._filter}
|
||||
></hassio-addon-repository>
|
||||
`);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hassio-repositories-editor
|
||||
.hass=${this.hass}
|
||||
.repos=${this._repos}
|
||||
></hassio-repositories-editor>
|
||||
|
||||
<hassio-search-input
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></hassio-search-input>
|
||||
|
||||
${repos}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData() {
|
||||
try {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._repos = addonsInfo.repositories;
|
||||
this._repos.sort(sortRepos);
|
||||
this._addons = addonsInfo.addons;
|
||||
} catch (err) {
|
||||
alert("Failed to fetch add-on info");
|
||||
}
|
||||
}
|
||||
|
||||
private async _filterChanged(e) {
|
||||
this._filter = e.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-addon-store", HassioAddonStore);
|
@@ -1,120 +0,0 @@
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
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 "../components/hassio-card-content";
|
||||
import "../resources/hassio-style";
|
||||
|
||||
class HassioRepositoriesEditor extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
id="list"
|
||||
is="dom-repeat"
|
||||
items="[[repoList]]"
|
||||
as="repo"
|
||||
sort="sortRepos"
|
||||
>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="[[repo.name]]"
|
||||
description="[[repo.url]]"
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/supervisor/options"
|
||||
data="[[computeRemoveRepoData(repoList, repo.url)]]"
|
||||
class="warning"
|
||||
>Remove</ha-call-api-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
value="{{repoUrl}}"
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/supervisor/options"
|
||||
data="[[computeAddRepoData(repoList, repoUrl)]]"
|
||||
>Add</ha-call-api-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
repos: {
|
||||
type: Array,
|
||||
observer: "reposChanged",
|
||||
},
|
||||
repoList: Array,
|
||||
repoUrl: String,
|
||||
};
|
||||
}
|
||||
|
||||
reposChanged(repos) {
|
||||
this.repoList = repos.filter(
|
||||
(repo) => repo.slug !== "core" && repo.slug !== "local"
|
||||
);
|
||||
this.repoUrl = "";
|
||||
}
|
||||
|
||||
sortRepos(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
computeRemoveRepoData(repoList, url) {
|
||||
const list = repoList
|
||||
.filter((repo) => repo.url !== url)
|
||||
.map((repo) => repo.url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
computeAddRepoData(repoList, url) {
|
||||
const list = repoList ? repoList.map((repo) => repo.url) : [];
|
||||
list.push(url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-repositories-editor", HassioRepositoriesEditor);
|
149
hassio/src/addon-store/hassio-repositories-editor.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
CSSResultArray,
|
||||
css,
|
||||
property,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { HassioAddonRepository } from "../../../src/data/hassio";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { repeat } from "lit-html/directives/repeat";
|
||||
|
||||
@customElement("hassio-repositories-editor")
|
||||
class HassioRepositoriesEditor extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public repos!: HassioAddonRepository[];
|
||||
@property() private _repoUrl = "";
|
||||
|
||||
private _sortedRepos = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const repos = this._sortedRepos(this.repos);
|
||||
return html`
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("repos")) {
|
||||
this._repoUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
private _urlChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._repoUrl = ev.detail.value;
|
||||
}
|
||||
|
||||
private computeRemoveRepoData(repoList, url) {
|
||||
const list = repoList
|
||||
.filter((repo) => repo.url !== url)
|
||||
.map((repo) => repo.source);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
private computeAddRepoData(repoList, url) {
|
||||
const list = repoList ? repoList.map((repo) => repo.source) : [];
|
||||
list.push(url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
hassioStyle,
|
||||
css`
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-repositories-editor": HassioRepositoriesEditor;
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
@@ -9,7 +9,7 @@ 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";
|
||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
||||
|
||||
class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
@@ -65,7 +65,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="_saveSettings">Save</paper-button>
|
||||
<mwc-button on-click="_saveSettings">Save</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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";
|
||||
@@ -50,8 +50,8 @@ class HassioAddonConfig extends PolymerElement {
|
||||
data="[[resetData]]"
|
||||
>Reset to defaults</ha-call-api-button
|
||||
>
|
||||
<paper-button on-click="saveTapped" disabled="[[!configParsed]]"
|
||||
>Save</paper-button
|
||||
<mwc-button on-click="saveTapped" disabled="[[!configParsed]]"
|
||||
>Save</mwc-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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";
|
||||
@@ -9,55 +10,62 @@ 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 { 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: "Addon Security 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 addon 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).",
|
||||
"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 addon full access to the network capabilities of the host machine. This gives the addon 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 addon.",
|
||||
"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 addon as well, which enables an addon to interact with Home Assistant without the need for additional authentication tokens.",
|
||||
"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 addon is given full access to the hardware of your system, by request of the addon author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
"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 addon was given access to the Hass.io API, by request of the addon author. By default, the addon can access general version information of your system. When the addon 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.",
|
||||
"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 addon author has requested the addon to have management access to the Docker instance running on your system. This mode gives the addon 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 addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
"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 addon runs, are isolated from all other system processes. The addon author has requested the addon to have access to the system processes running on the host system instance, and allow the addon to spawn processes on the host system as well. This mode gives the addon 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 addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
"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 addons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAddon authors can provide their security profiles, optimized for the addon, 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 addon.",
|
||||
"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 addon 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.",
|
||||
"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.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,7 +85,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
color: white;
|
||||
--paper-card-header-color: white;
|
||||
}
|
||||
paper-card.warning paper-button {
|
||||
paper-card.warning mwc-button {
|
||||
color: white !important;
|
||||
}
|
||||
.warning {
|
||||
@@ -102,10 +110,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.state {
|
||||
display: flex;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.state div {
|
||||
width: 150px;
|
||||
width: 180px;
|
||||
display: inline-block;
|
||||
}
|
||||
.state iron-icon {
|
||||
width: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
paper-toggle-button {
|
||||
display: inline;
|
||||
}
|
||||
@@ -149,6 +165,9 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
margin-right: 4px;
|
||||
--iron-icon-height: 45px;
|
||||
}
|
||||
.protection-enable mwc-button {
|
||||
--mdc-theme-primary: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
|
||||
@@ -161,20 +180,38 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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"
|
||||
>Update</ha-call-api-button
|
||||
disabled="[[!addon.available]]"
|
||||
>
|
||||
Update
|
||||
</ha-call-api-button
|
||||
>
|
||||
<template is="dom-if" if="[[addon.changelog]]">
|
||||
<paper-button on-click="openChangelog">Changelog</paper-button>
|
||||
<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">
|
||||
@@ -213,22 +250,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
<img src="/api/hassio/addons/[[addonSlug]]/logo" />
|
||||
</a>
|
||||
</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 addon 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 addon.
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="protectionToggled">Enable Protection mode</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<div class="security">
|
||||
<h3>Addon Security Rating</h3>
|
||||
<div class="description light-color">
|
||||
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 addon requires on your system, the lower the score, thus raising the possible security risks.
|
||||
</div>
|
||||
<ha-label-badge
|
||||
class$="[[computeSecurityClassName(addon.rating)]]"
|
||||
on-click="showMoreInfo"
|
||||
@@ -250,7 +272,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
<ha-label-badge
|
||||
on-click="showMoreInfo"
|
||||
id="full_access"
|
||||
icon="hassio:chip"
|
||||
icon="hassio:chip"
|
||||
label="hardware"
|
||||
description=""
|
||||
></ha-label-badge>
|
||||
@@ -298,7 +320,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
id="apparmor"
|
||||
icon="hassio:shield"
|
||||
label="apparmor"
|
||||
description="[[addon.apparmor]]"
|
||||
description=""
|
||||
></ha-label-badge>
|
||||
</template>
|
||||
<template is="dom-if" if="[[addon.auth_api]]">
|
||||
@@ -310,6 +332,15 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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">
|
||||
@@ -326,8 +357,27 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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</div>
|
||||
<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]]"
|
||||
@@ -337,8 +387,8 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<paper-button class="warning" on-click="_unistallClicked"
|
||||
>Uninstall</paper-button
|
||||
<mwc-button class="warning" on-click="_unistallClicked"
|
||||
>Uninstall</mwc-button
|
||||
>
|
||||
<template is="dom-if" if="[[addon.build]]">
|
||||
<ha-call-api-button
|
||||
@@ -371,20 +421,30 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
</template>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[computeShowWebUI(addon.webui, isRunning)]]"
|
||||
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
|
||||
>
|
||||
<a
|
||||
href="[[pathWebui(addon.webui)]]"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
><paper-button>Open web UI</paper-button></a
|
||||
><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 addon is not available on your system.</p>
|
||||
<p class="warning">This add-on is not available on your system.</p>
|
||||
</template>
|
||||
<ha-call-api-button
|
||||
disabled="[[!addon.available]]"
|
||||
@@ -448,8 +508,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
return webui && webui.replace("[HOST]", document.location.hostname);
|
||||
}
|
||||
|
||||
computeShowWebUI(webui, isRunning) {
|
||||
return webui && isRunning;
|
||||
computeShowWebUI(ingress, webui, isRunning) {
|
||||
return !ingress && webui && isRunning;
|
||||
}
|
||||
|
||||
openIngress() {
|
||||
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
||||
}
|
||||
|
||||
computeShowIngressUI(ingress, isRunning) {
|
||||
return ingress && isRunning;
|
||||
}
|
||||
|
||||
computeStartOnBoot(state) {
|
||||
@@ -482,9 +550,14 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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");
|
||||
this.fire("hassio-markdown-dialog", {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: PERMIS_DESC[id].title,
|
||||
content: PERMIS_DESC[id].description,
|
||||
});
|
||||
@@ -495,7 +568,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
|
||||
.then((resp) => resp, () => "Error getting changelog")
|
||||
.then((content) => {
|
||||
this.fire("hassio-markdown-dialog", {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Changelog",
|
||||
content: content,
|
||||
});
|
||||
@@ -526,5 +599,14 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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";
|
||||
@@ -24,7 +24,7 @@ class HassioAddonLogs extends PolymerElement {
|
||||
<paper-card heading="Log">
|
||||
<div class="card-content" id="content"></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
<mwc-button on-click="refresh">Refresh</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
|
@@ -5,7 +5,7 @@ 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";
|
||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
||||
|
||||
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
@@ -37,16 +37,19 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
||||
<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>
|
||||
@@ -60,7 +63,7 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
||||
data="[[resetData]]"
|
||||
>Reset to defaults</ha-call-api-button
|
||||
>
|
||||
<paper-button on-click="saveTapped">Save</paper-button>
|
||||
<mwc-button on-click="saveTapped">Save</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
@@ -89,9 +92,11 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
|
||||
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;
|
||||
|
@@ -1,14 +1,10 @@
|
||||
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/app-route/app-route";
|
||||
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-menu-button";
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../hassio-markdown-dialog";
|
||||
import "./hassio-addon-audio";
|
||||
import "./hassio-addon-config";
|
||||
import "./hassio-addon-info";
|
||||
@@ -18,7 +14,7 @@ import "./hassio-addon-network";
|
||||
class HassioAddonView extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
<style>
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
@@ -51,39 +47,19 @@ class HassioAddonView extends PolymerElement {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<app-route
|
||||
route="[[route]]"
|
||||
pattern="/addon/:slug"
|
||||
data="{{routeData}}"
|
||||
active="{{routeMatches}}"
|
||||
></app-route>
|
||||
<app-header-layout has-scrolling-region="">
|
||||
<app-header fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
hassio
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></ha-menu-button>
|
||||
<paper-icon-button
|
||||
icon="hassio:arrow-left"
|
||||
on-click="backTapped"
|
||||
></paper-icon-button>
|
||||
<div main-title="">Hass.io: add-on details</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<hass-subpage header="Hass.io: add-on details" hassio>
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
hass="[[hass]]"
|
||||
addon="[[addon]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
addon-slug="[[addonSlug]]"
|
||||
></hassio-addon-info>
|
||||
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<hassio-addon-config
|
||||
hass="[[hass]]"
|
||||
addon="[[addon]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
addon-slug="[[addonSlug]]"
|
||||
></hassio-addon-config>
|
||||
|
||||
<template is="dom-if" if="[[addon.audio]]">
|
||||
@@ -97,52 +73,38 @@ class HassioAddonView extends PolymerElement {
|
||||
<hassio-addon-network
|
||||
hass="[[hass]]"
|
||||
addon="[[addon]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
addon-slug="[[addonSlug]]"
|
||||
></hassio-addon-network>
|
||||
</template>
|
||||
|
||||
<hassio-addon-logs
|
||||
hass="[[hass]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
addon-slug="[[addonSlug]]"
|
||||
></hassio-addon-logs>
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog
|
||||
title="[[markdownTitle]]"
|
||||
content="[[markdownContent]]"
|
||||
></hassio-markdown-dialog>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
showMenu: Boolean,
|
||||
narrow: Boolean,
|
||||
route: Object,
|
||||
routeData: {
|
||||
route: {
|
||||
type: Object,
|
||||
observer: "routeDataChanged",
|
||||
},
|
||||
routeMatches: Boolean,
|
||||
addon: Object,
|
||||
|
||||
markdownTitle: String,
|
||||
markdownContent: {
|
||||
addonSlug: {
|
||||
type: String,
|
||||
value: "",
|
||||
computed: "_computeSlug(route)",
|
||||
},
|
||||
addon: Object,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
||||
this.addEventListener("hassio-markdown-dialog", (ev) =>
|
||||
this.openMarkdown(ev)
|
||||
);
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
@@ -151,15 +113,15 @@ class HassioAddonView extends PolymerElement {
|
||||
if (!path) return;
|
||||
|
||||
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
|
||||
this.backTapped();
|
||||
history.back();
|
||||
} else {
|
||||
this.routeDataChanged(this.routeData);
|
||||
this.routeDataChanged(this.route);
|
||||
}
|
||||
}
|
||||
|
||||
routeDataChanged(routeData) {
|
||||
if (!this.routeMatches || !routeData || !routeData.slug) return;
|
||||
this.hass.callApi("get", `hassio/addons/${routeData.slug}/info`).then(
|
||||
const addon = routeData.path.substr(1);
|
||||
this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
|
||||
(info) => {
|
||||
this.addon = info.data;
|
||||
},
|
||||
@@ -169,16 +131,8 @@ class HassioAddonView extends PolymerElement {
|
||||
);
|
||||
}
|
||||
|
||||
backTapped() {
|
||||
history.back();
|
||||
}
|
||||
|
||||
openMarkdown(ev) {
|
||||
this.setProperties({
|
||||
markdownTitle: ev.detail.title,
|
||||
markdownContent: ev.detail.content,
|
||||
});
|
||||
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
|
||||
_computeSlug(route) {
|
||||
return route.path.substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,90 +0,0 @@
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../src/components/ha-relative-time";
|
||||
|
||||
class HassioCardContent extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
iron-icon.not_available {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<iron-icon
|
||||
icon="[[icon]]"
|
||||
class\$="[[iconClass]]"
|
||||
title="[[iconTitle]]"
|
||||
></iron-icon>
|
||||
<div>
|
||||
<div class="title">[[title]]</div>
|
||||
<div class="addition">
|
||||
<template is="dom-if" if="[[description]]">
|
||||
[[description]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[!available]]">
|
||||
(Not available)
|
||||
</template>
|
||||
<template is="dom-if" if="[[datetime]]">
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
class="addition"
|
||||
datetime="[[datetime]]"
|
||||
></ha-relative-time>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
title: String,
|
||||
description: String,
|
||||
available: Boolean,
|
||||
datetime: String,
|
||||
icon: {
|
||||
type: String,
|
||||
value: "hass:help-circle",
|
||||
},
|
||||
iconTitle: String,
|
||||
iconClass: String,
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define("hassio-card-content", HassioCardContent);
|
99
hassio/src/components/hassio-card-content.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
|
||||
import "../../../src/components/ha-relative-time";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("hassio-card-content")
|
||||
class HassioCardContent extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public title!: string;
|
||||
@property() public description?: string;
|
||||
@property({ type: Boolean }) public available?: boolean;
|
||||
@property() public datetime?: string;
|
||||
@property() public iconTitle?: string;
|
||||
@property() public iconClass?: string;
|
||||
@property() public icon = "hass:help-circle";
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<iron-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
<div>
|
||||
<div class="title">${this.title}</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${/* treat as available when undefined */
|
||||
this.available === false ? " (Not available)" : ""}
|
||||
${this.datetime
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
class="addition"
|
||||
.datetime=${this.datetime}
|
||||
></ha-relative-time>
|
||||
`
|
||||
: undefined}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
iron-icon.not_available {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-card-content": HassioCardContent;
|
||||
}
|
||||
}
|
13
hassio/src/components/hassio-filter-addons.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { HassioAddonInfo } from "../../../src/data/hassio";
|
||||
import * as Fuse from "fuse.js";
|
||||
|
||||
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
|
||||
const options: Fuse.FuseOptions<HassioAddonInfo> = {
|
||||
keys: ["name", "description", "slug"],
|
||||
caseSensitive: false,
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.2,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(filter);
|
||||
}
|
82
hassio/src/components/hassio-search-input.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { TemplateResult, html } from "lit-html";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@material/mwc-button";
|
||||
|
||||
@customElement("hassio-search-input")
|
||||
class HassioSearchInput extends LitElement {
|
||||
@property() private filter?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="search-container">
|
||||
<paper-input
|
||||
label="Search"
|
||||
.value=${this.filter}
|
||||
@value-changed=${this._filterInputChanged}
|
||||
>
|
||||
<iron-icon
|
||||
icon="hassio:magnify"
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
></iron-icon>
|
||||
${this.filter &&
|
||||
html`
|
||||
<paper-icon-button
|
||||
slot="suffix"
|
||||
class="suffix"
|
||||
@click=${this._clearSearch}
|
||||
icon="hassio:close"
|
||||
alt="Clear"
|
||||
title="Clear"
|
||||
></paper-icon-button>
|
||||
`}
|
||||
</paper-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 16px;
|
||||
}
|
||||
.search-container {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.prefix {
|
||||
margin: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-search-input": HassioSearchInput;
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hassio-addons";
|
||||
import "./hassio-hass-update";
|
||||
import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
|
||||
class HassioDashboard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<hassio-hass-update
|
||||
hass="[[hass]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
></hassio-hass-update>
|
||||
<hassio-addons
|
||||
hass="[[hass]]"
|
||||
addons="[[supervisorInfo.addons]]"
|
||||
></hassio-addons>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
supervisorInfo: Object,
|
||||
hassInfo: Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-dashboard", HassioDashboard);
|
52
hassio/src/dashboard/hassio-dashboard.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "./hassio-addons";
|
||||
import "./hassio-hass-update";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
HassioSupervisorInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
} from "../../../src/data/hassio";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-hass-update
|
||||
.hass=${this.hass}
|
||||
.hassInfo=${this.hassInfo}
|
||||
></hassio-hass-update>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.addons=${this.supervisorInfo.addons}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-dashboard": HassioDashboard;
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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";
|
||||
@@ -26,26 +26,13 @@ class HassioHassUpdate extends PolymerElement {
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">Update available! 🎉</div>
|
||||
<paper-card>
|
||||
<paper-card heading="Update available! 🎉">
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="Home Assistant [[hassInfo.last_version]] is available"
|
||||
description="You are currently running version [[hassInfo.version]]"
|
||||
icon="hassio:home-assistant"
|
||||
icon-class="hassupdate"
|
||||
></hassio-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>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.home-assistant.io/latest-release-notes/"
|
||||
target="_blank"
|
||||
>Read the release notes</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
@@ -54,9 +41,9 @@ class HassioHassUpdate extends PolymerElement {
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant/releases"
|
||||
href="https://www.home-assistant.io/latest-release-notes/"
|
||||
target="_blank"
|
||||
><paper-button>Release notes</paper-button></a
|
||||
><mwc-button>Release notes</mwc-button></a
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
|
@@ -1,18 +1,21 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
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/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";
|
||||
|
||||
@customElement("dialog-hassio-markdown")
|
||||
class HassioMarkdownDialog extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
ha-paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
@@ -31,10 +34,10 @@ class HassioMarkdownDialog extends PolymerElement {
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
ha-paper-dialog {
|
||||
max-height: 100%;
|
||||
}
|
||||
paper-dialog::before {
|
||||
ha-paper-dialog::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
@@ -50,7 +53,7 @@ class HassioMarkdownDialog extends PolymerElement {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop="">
|
||||
<ha-paper-dialog id="dialog" with-backdrop="">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon="hassio:close"
|
||||
@@ -61,7 +64,7 @@ class HassioMarkdownDialog extends PolymerElement {
|
||||
<paper-dialog-scrollable>
|
||||
<ha-markdown content="[[content]]"></ha-markdown>
|
||||
</paper-dialog-scrollable>
|
||||
</paper-dialog>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -72,8 +75,14 @@ class HassioMarkdownDialog extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
this.$.dialog.open();
|
||||
public showDialog(params) {
|
||||
this.setProperties(params);
|
||||
(this.$.dialog as PaperDialogElement).open();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-markdown": HassioMarkdownDialog;
|
||||
}
|
||||
}
|
||||
customElements.define("hassio-markdown-dialog", HassioMarkdownDialog);
|
18
hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
|
||||
export interface HassioMarkdownDialogParams {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const showHassioMarkdownDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioMarkdownDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,21 +1,66 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
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/auth/data";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../../src/resources/ha-style";
|
||||
import "../../../../src/components/dialog/ha-paper-dialog";
|
||||
import { customElement } from "lit-element";
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog";
|
||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||
import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio";
|
||||
|
||||
const _computeFolders = (folders) => {
|
||||
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
|
||||
if (folders.includes("homeassistant")) {
|
||||
list.push({
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: true,
|
||||
});
|
||||
}
|
||||
if (folders.includes("ssl")) {
|
||||
list.push({ slug: "ssl", name: "SSL", checked: true });
|
||||
}
|
||||
if (folders.includes("share")) {
|
||||
list.push({ slug: "share", name: "Share", checked: true });
|
||||
}
|
||||
if (folders.includes("addons/local")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: true });
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
const _computeAddons = (addons) => {
|
||||
return addons.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: true,
|
||||
}));
|
||||
};
|
||||
|
||||
@customElement("dialog-hassio-snapshot")
|
||||
class HassioSnapshotDialog extends PolymerElement {
|
||||
// Commented out because it breaks Polymer! Kept around for when we migrate
|
||||
// to Lit. Now just putting ts-ignore everywhere because we need this out.
|
||||
// Sorry future developer.
|
||||
// public hass!: HomeAssistant;
|
||||
// protected error?: string;
|
||||
// private snapshot?: any;
|
||||
// private dialogParams?: HassioSnapshotDialogParams;
|
||||
// private restoreHass!: boolean;
|
||||
// private snapshotPassword!: string;
|
||||
|
||||
class HassioSnapshot extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
ha-paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
@@ -29,7 +74,7 @@ class HassioSnapshot extends PolymerElement {
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-dialog-scrollable {
|
||||
ha-paper-dialog-scrollable {
|
||||
margin: 0;
|
||||
}
|
||||
paper-checkbox {
|
||||
@@ -37,7 +82,7 @@ class HassioSnapshot extends PolymerElement {
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
ha-paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -57,7 +102,7 @@ class HassioSnapshot extends PolymerElement {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
<paper-dialog
|
||||
<ha-paper-dialog
|
||||
id="dialog"
|
||||
with-backdrop=""
|
||||
on-iron-overlay-closed="_dialogClosed"
|
||||
@@ -77,22 +122,18 @@ class HassioSnapshot extends PolymerElement {
|
||||
<paper-checkbox checked="{{restoreHass}}">
|
||||
Home Assistant [[snapshot.homeassistant]]
|
||||
</paper-checkbox>
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<template is="dom-if" if="[[_folders.length]]">
|
||||
<div>Folders:</div>
|
||||
<template is="dom-repeat" items="[[snapshot.folders]]">
|
||||
<template is="dom-repeat" items="[[_folders]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<template is="dom-if" if="[[_addons.length]]">
|
||||
<div>Add-ons:</div>
|
||||
<paper-dialog-scrollable>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[snapshot.addons]]"
|
||||
sort="_sortAddons"
|
||||
>
|
||||
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]] <span class="details">([[item.version]])</span>
|
||||
</paper-checkbox>
|
||||
@@ -123,32 +164,26 @@ class HassioSnapshot extends PolymerElement {
|
||||
class="download"
|
||||
title="Download snapshot"
|
||||
></paper-icon-button>
|
||||
<paper-button on-click="_partialRestoreClicked"
|
||||
>Restore selected</paper-button
|
||||
<mwc-button on-click="_partialRestoreClicked"
|
||||
>Restore selected</mwc-button
|
||||
>
|
||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
||||
<paper-button on-click="_fullRestoreClicked"
|
||||
>Wipe & restore</paper-button
|
||||
<mwc-button on-click="_fullRestoreClicked"
|
||||
>Wipe & restore</mwc-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
snapshotSlug: {
|
||||
type: String,
|
||||
notify: true,
|
||||
observer: "_snapshotSlugChanged",
|
||||
},
|
||||
snapshotDeleted: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
},
|
||||
dialogParams: Object,
|
||||
snapshot: Object,
|
||||
_folders: Object,
|
||||
_addons: Object,
|
||||
restoreHass: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
@@ -158,140 +193,136 @@ class HassioSnapshot extends PolymerElement {
|
||||
};
|
||||
}
|
||||
|
||||
_snapshotSlugChanged(snapshotSlug) {
|
||||
if (!snapshotSlug || snapshotSlug === "update") return;
|
||||
this.hass.callApi("get", `hassio/snapshots/${snapshotSlug}/info`).then(
|
||||
(info) => {
|
||||
info.data.folders = this._computeFolders(info.data.folders);
|
||||
info.data.addons = this._computeAddons(info.data.addons);
|
||||
this.snapshot = info.data;
|
||||
this.$.dialog.open();
|
||||
},
|
||||
() => {
|
||||
this.snapshot = null;
|
||||
}
|
||||
);
|
||||
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();
|
||||
}
|
||||
|
||||
_computeFolders(folders) {
|
||||
const list = [];
|
||||
if (folders.includes("homeassistant"))
|
||||
list.push({
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: true,
|
||||
});
|
||||
if (folders.includes("ssl"))
|
||||
list.push({ slug: "ssl", name: "SSL", checked: true });
|
||||
if (folders.includes("share"))
|
||||
list.push({ slug: "share", name: "Share", checked: true });
|
||||
if (folders.includes("addons/local"))
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: true });
|
||||
return list;
|
||||
}
|
||||
|
||||
_computeAddons(addons) {
|
||||
return addons.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: true,
|
||||
}));
|
||||
}
|
||||
|
||||
_isFullSnapshot(type) {
|
||||
protected _isFullSnapshot(type) {
|
||||
return type === "full";
|
||||
}
|
||||
|
||||
_partialRestoreClicked() {
|
||||
protected _partialRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
const addons = this.snapshot.addons
|
||||
// @ts-ignore
|
||||
const addons = this._addons
|
||||
.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this.snapshot.folders
|
||||
// @ts-ignore
|
||||
const folders = this._folders
|
||||
.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
const data = {
|
||||
// @ts-ignore
|
||||
homeassistant: this.restoreHass,
|
||||
addons: addons,
|
||||
folders: folders,
|
||||
addons,
|
||||
folders,
|
||||
};
|
||||
if (this.snapshot.protected) data.password = this.snapshotPassword;
|
||||
// @ts-ignore
|
||||
if (this.snapshot.protected) {
|
||||
// @ts-ignore
|
||||
data.password = this.snapshotPassword;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.hass
|
||||
.callApi(
|
||||
"post",
|
||||
`hassio/snapshots/${this.snapshotSlug}/restore/partial`,
|
||||
"POST",
|
||||
// @ts-ignore
|
||||
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this.$.dialog.close();
|
||||
(this.$.dialog as PaperDialogElement).close();
|
||||
},
|
||||
(error) => {
|
||||
// @ts-ignore
|
||||
this.error = error.body.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_fullRestoreClicked() {
|
||||
protected _fullRestoreClicked() {
|
||||
if (!confirm("Are you sure you want to restore this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const data = this.snapshot.protected
|
||||
? { password: this.snapshotPassword }
|
||||
: null;
|
||||
? {
|
||||
password:
|
||||
// @ts-ignore
|
||||
this.snapshotPassword,
|
||||
}
|
||||
: undefined;
|
||||
// @ts-ignore
|
||||
this.hass
|
||||
.callApi(
|
||||
"post",
|
||||
`hassio/snapshots/${this.snapshotSlug}/restore/full`,
|
||||
"POST",
|
||||
// @ts-ignore
|
||||
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this.$.dialog.close();
|
||||
(this.$.dialog as PaperDialogElement).close();
|
||||
},
|
||||
(error) => {
|
||||
// @ts-ignore
|
||||
this.error = error.body.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_deleteClicked() {
|
||||
protected _deleteClicked() {
|
||||
if (!confirm("Are you sure you want to delete this snapshot?")) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
this.hass
|
||||
.callApi("post", `hassio/snapshots/${this.snapshotSlug}/remove`)
|
||||
// @ts-ignore
|
||||
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
|
||||
.then(
|
||||
() => {
|
||||
this.$.dialog.close();
|
||||
this.snapshotDeleted = true;
|
||||
(this.$.dialog as PaperDialogElement).close();
|
||||
// @ts-ignore
|
||||
this.dialogParams!.onDelete();
|
||||
},
|
||||
(error) => {
|
||||
// @ts-ignore
|
||||
this.error = error.body.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _downloadClicked() {
|
||||
protected async _downloadClicked() {
|
||||
let signedPath;
|
||||
try {
|
||||
signedPath = await getSignedPath(
|
||||
// @ts-ignore
|
||||
this.hass,
|
||||
`/api/hassio/snapshots/${this.snapshotSlug}/download`
|
||||
// @ts-ignore
|
||||
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
|
||||
);
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
|
||||
const a = document.createElement("A");
|
||||
const a = document.createElement("a");
|
||||
a.href = signedPath.path;
|
||||
a.download = `Hass_io_${name}.tar`;
|
||||
this.$.dialog.appendChild(a);
|
||||
@@ -299,23 +330,23 @@ class HassioSnapshot extends PolymerElement {
|
||||
this.$.dialog.removeChild(a);
|
||||
}
|
||||
|
||||
_computeName(snapshot) {
|
||||
return snapshot.name || snapshot.slug;
|
||||
protected _computeName(snapshot) {
|
||||
return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
|
||||
}
|
||||
|
||||
_computeType(type) {
|
||||
protected _computeType(type) {
|
||||
return type === "full" ? "Full snapshot" : "Partial snapshot";
|
||||
}
|
||||
|
||||
_computeSize(size) {
|
||||
protected _computeSize(size) {
|
||||
return Math.ceil(size * 10) / 10 + " MB";
|
||||
}
|
||||
|
||||
_sortAddons(a, b) {
|
||||
protected _sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
_formatDatetime(datetime) {
|
||||
protected _formatDatetime(datetime) {
|
||||
return new Date(datetime).toLocaleDateString(navigator.language, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
@@ -326,8 +357,18 @@ class HassioSnapshot extends PolymerElement {
|
||||
});
|
||||
}
|
||||
|
||||
_dialogClosed() {
|
||||
this.snapshotSlug = null;
|
||||
protected _dialogClosed() {
|
||||
this.setProperties({
|
||||
dialogParams: undefined,
|
||||
snapshot: undefined,
|
||||
_addons: [],
|
||||
_folders: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-snapshot": HassioSnapshotDialog;
|
||||
}
|
||||
}
|
||||
customElements.define("hassio-snapshot", HassioSnapshot);
|
18
hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
|
||||
export interface HassioSnapshotDialogParams {
|
||||
slug: string;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const showHassioSnapshotDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioSnapshotDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,4 +1,17 @@
|
||||
window.loadES5Adapter().then(() => {
|
||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js");
|
||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main.js");
|
||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
|
||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
|
||||
});
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
body {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
|
@@ -1,51 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./hassio-main";
|
||||
import "./resources/hassio-icons";
|
||||
|
||||
class HassioApp extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[hass]]">
|
||||
<hassio-main
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
route="[[route]]"
|
||||
></hassio-main>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
route: Object,
|
||||
hassioPanel: {
|
||||
type: Object,
|
||||
value: window.parent.hassioPanel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
window.setProperties = this.setProperties.bind(this);
|
||||
this.addEventListener("location-changed", () => this._locationChanged());
|
||||
this.addEventListener("hass-open-menu", () => this._menuEvent(true));
|
||||
this.addEventListener("hass-close-menu", () => this._menuEvent(false));
|
||||
}
|
||||
|
||||
_menuEvent(shouldOpen) {
|
||||
this.hassioPanel.fire(shouldOpen ? "hass-open-menu" : "hass-close-menu");
|
||||
}
|
||||
|
||||
_locationChanged() {
|
||||
this.hassioPanel.navigate(window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-app", HassioApp);
|
@@ -1,59 +0,0 @@
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
class HassioData extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
supervisor: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
host: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
homeassistant: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
return Promise.all([
|
||||
this.fetchSupervisorInfo(),
|
||||
this.fetchHostInfo(),
|
||||
this.fetchHassInfo(),
|
||||
]);
|
||||
}
|
||||
|
||||
fetchSupervisorInfo() {
|
||||
return this.hass.callApi("get", "hassio/supervisor/info").then((info) => {
|
||||
this.supervisor = info.data;
|
||||
});
|
||||
}
|
||||
|
||||
fetchHostInfo() {
|
||||
return this.hass.callApi("get", "hassio/host/info").then((info) => {
|
||||
this.host = info.data;
|
||||
});
|
||||
}
|
||||
|
||||
fetchHassInfo() {
|
||||
return this.hass
|
||||
.callApi("get", "hassio/homeassistant/info")
|
||||
.then((info) => {
|
||||
this.homeassistant = info.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-data", HassioData);
|
@@ -1,129 +0,0 @@
|
||||
import "@polymer/app-route/app-route";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import "./addon-view/hassio-addon-view";
|
||||
import "./hassio-data";
|
||||
import "./hassio-pages-with-tabs";
|
||||
|
||||
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
|
||||
import EventsMixin from "../../src/mixins/events-mixin";
|
||||
import NavigateMixin from "../../src/mixins/navigate-mixin";
|
||||
|
||||
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route
|
||||
route="[[route]]"
|
||||
pattern="/:page"
|
||||
data="{{routeData}}"
|
||||
></app-route>
|
||||
<hassio-data
|
||||
id="data"
|
||||
hass="[[hass]]"
|
||||
supervisor="{{supervisorInfo}}"
|
||||
homeassistant="{{hassInfo}}"
|
||||
host="{{hostInfo}}"
|
||||
></hassio-data>
|
||||
|
||||
<template is="dom-if" if="[[!loaded]]">
|
||||
<hass-loading-screen
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></hass-loading-screen>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[loaded]]">
|
||||
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
|
||||
<hassio-pages-with-tabs
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
page="[[routeData.page]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
host-info="[[hostInfo]]"
|
||||
></hassio-pages-with-tabs>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
|
||||
<hassio-addon-view
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
route="[[route]]"
|
||||
></hassio-addon-view>
|
||||
</template>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
narrow: Boolean,
|
||||
showMenu: Boolean,
|
||||
route: {
|
||||
type: Object,
|
||||
// Fake route object
|
||||
value: {
|
||||
prefix: "/hassio",
|
||||
path: "/dashboard",
|
||||
__queryParams: {},
|
||||
},
|
||||
observer: "routeChanged",
|
||||
},
|
||||
routeData: Object,
|
||||
supervisorInfo: Object,
|
||||
hostInfo: Object,
|
||||
hassInfo: Object,
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
computed: "computeIsLoaded(supervisorInfo, hostInfo, hassInfo)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
|
||||
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.routeChanged(this.route);
|
||||
}
|
||||
|
||||
apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this.$.data.refresh().catch(function() {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
computeIsLoaded(supervisorInfo, hostInfo, hassInfo) {
|
||||
return supervisorInfo !== null && hostInfo !== null && hassInfo !== null;
|
||||
}
|
||||
|
||||
routeChanged(route) {
|
||||
if (route.path === "" && route.prefix === "/hassio") {
|
||||
this.navigate("/hassio/dashboard", true);
|
||||
}
|
||||
this.fire("iron-resize");
|
||||
}
|
||||
|
||||
equalsAddon(page) {
|
||||
return page && page === "addon";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-main", HassioMain);
|
183
hassio/src/hassio-main.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { customElement, PropertyValues, property } from "lit-element";
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import "@polymer/paper-icon-button";
|
||||
|
||||
import "../../src/resources/ha-style";
|
||||
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
fetchHassioHostInfo,
|
||||
fetchHassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
HassioHostInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
fetchHassioAddonInfo,
|
||||
createHassioSession,
|
||||
HassioPanelInfo,
|
||||
} from "../../src/data/hassio";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-pages-with-tabs";
|
||||
|
||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||
// causing an exception when added to DOM. When transpiled to ES5, this will
|
||||
// break the build.
|
||||
customElements.get("paper-icon-button").prototype._keyBindings = {};
|
||||
|
||||
@customElement("hassio-main")
|
||||
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public panel!: HassioPanelInfo;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
initialLoad: () => this._fetchData(),
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-pages-with-tabs",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-view",
|
||||
load: () => import("./addon-view/hassio-addon-view"),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () => import("./ingress-view/hassio-ingress-view"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@property() private _supervisorInfo: HassioSupervisorInfo;
|
||||
@property() private _hostInfo: HassioHostInfo;
|
||||
@property() private _hassInfo: HassioHomeAssistantInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
// supervisor UI can also run under older versions of Home Assistant.
|
||||
// So here we are going to translate toggle events into the appropriate
|
||||
// open and close events. These events are a no-op in newer versions of
|
||||
// Home Assistant.
|
||||
this.addEventListener("hass-toggle-menu", () => {
|
||||
fireEvent(
|
||||
(window.parent as any).customPanel,
|
||||
// @ts-ignore
|
||||
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
|
||||
);
|
||||
});
|
||||
// Paulus - March 19, 2019
|
||||
// We changed the navigate event to fire directly on the window, as that's
|
||||
// where we are listening for it. However, the older panel_custom will
|
||||
// listen on this element for navigation events, so we need to forward them.
|
||||
window.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
|
||||
makeDialogManager(this, document.body);
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route =
|
||||
el.nodeName === "HASSIO-PAGES-WITH-TABS" ? this.route : this.routeTail;
|
||||
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
(el as PolymerElement).setProperties({
|
||||
hass: this.hass,
|
||||
supervisorInfo: this._supervisorInfo,
|
||||
hostInfo: this._hostInfo,
|
||||
hassInfo: this._hassInfo,
|
||||
route,
|
||||
});
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
el.supervisorInfo = this._supervisorInfo;
|
||||
el.hostInfo = this._hostInfo;
|
||||
el.hassInfo = this._hassInfo;
|
||||
el.route = route;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.panel.config && this.panel.config.ingress) {
|
||||
await this._redirectIngress(this.panel.config.ingress);
|
||||
return;
|
||||
}
|
||||
|
||||
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
]);
|
||||
this._supervisorInfo = supervisorInfo;
|
||||
this._hostInfo = hostInfo;
|
||||
this._hassInfo = hassInfo;
|
||||
}
|
||||
|
||||
private async _redirectIngress(addonSlug: string) {
|
||||
try {
|
||||
const [addon] = await Promise.all([
|
||||
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) {
|
||||
alert(`Unable to open ingress connection `);
|
||||
}
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tries = 1;
|
||||
|
||||
const tryUpdate = () => {
|
||||
this._fetchData().catch(() => {
|
||||
tries += 1;
|
||||
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
tryUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-main": HassioMain;
|
||||
}
|
||||
}
|
@@ -1,167 +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 "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../src/components/ha-menu-button";
|
||||
import "../../src/resources/ha-style";
|
||||
import "./addon-store/hassio-addon-store";
|
||||
import "./dashboard/hassio-dashboard";
|
||||
import "./hassio-markdown-dialog";
|
||||
import "./snapshots/hassio-snapshot";
|
||||
import "./snapshots/hassio-snapshots";
|
||||
import "./system/hassio-system";
|
||||
|
||||
import scrollToTarget from "../../src/common/dom/scroll-to-target";
|
||||
|
||||
import NavigateMixin from "../../src/mixins/navigate-mixin";
|
||||
|
||||
class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-header-layout id="layout" has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
hassio
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
<template is="dom-if" if="[[showRefreshButton(page)]]">
|
||||
<paper-icon-button
|
||||
icon="hassio:refresh"
|
||||
on-click="refreshClicked"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</app-toolbar>
|
||||
<paper-tabs
|
||||
scrollable=""
|
||||
selected="[[page]]"
|
||||
attr-for-selected="page-name"
|
||||
on-iron-activate="handlePageSelected"
|
||||
>
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<template is="dom-if" if='[[equals(page, "dashboard")]]'>
|
||||
<hassio-dashboard
|
||||
hass="[[hass]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
></hassio-dashboard>
|
||||
</template>
|
||||
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
|
||||
<hassio-snapshots
|
||||
hass="[[hass]]"
|
||||
installed-addons="[[supervisorInfo.addons]]"
|
||||
snapshot-slug="{{snapshotSlug}}"
|
||||
snapshot-deleted="{{snapshotDeleted}}"
|
||||
></hassio-snapshots>
|
||||
</template>
|
||||
<template is="dom-if" if='[[equals(page, "store")]]'>
|
||||
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
|
||||
</template>
|
||||
<template is="dom-if" if='[[equals(page, "system")]]'>
|
||||
<hassio-system
|
||||
hass="[[hass]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
host-info="[[hostInfo]]"
|
||||
></hassio-system>
|
||||
</template>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog
|
||||
title="[[markdownTitle]]"
|
||||
content="[[markdownContent]]"
|
||||
></hassio-markdown-dialog>
|
||||
|
||||
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
|
||||
<hassio-snapshot
|
||||
hass="[[hass]]"
|
||||
snapshot-slug="{{snapshotSlug}}"
|
||||
snapshot-deleted="{{snapshotDeleted}}"
|
||||
></hassio-snapshot>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
showMenu: Boolean,
|
||||
narrow: Boolean,
|
||||
page: String,
|
||||
supervisorInfo: Object,
|
||||
hostInfo: Object,
|
||||
hassInfo: Object,
|
||||
snapshotSlug: String,
|
||||
snapshotDeleted: Boolean,
|
||||
|
||||
markdownTitle: String,
|
||||
markdownContent: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("hassio-markdown-dialog", (ev) =>
|
||||
this.openMarkdown(ev)
|
||||
);
|
||||
}
|
||||
|
||||
handlePageSelected(ev) {
|
||||
const newPage = ev.detail.item.getAttribute("page-name");
|
||||
if (newPage !== this.page) {
|
||||
this.navigate(`/hassio/${newPage}`);
|
||||
}
|
||||
scrollToTarget(this, this.$.layout.header.scrollTarget);
|
||||
}
|
||||
|
||||
equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
showRefreshButton(page) {
|
||||
return page === "store" || page === "snapshots";
|
||||
}
|
||||
|
||||
refreshClicked() {
|
||||
if (this.page === "snapshots") {
|
||||
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
|
||||
} else {
|
||||
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
openMarkdown(ev) {
|
||||
this.setProperties({
|
||||
markdownTitle: ev.detail.title,
|
||||
markdownContent: ev.detail.content,
|
||||
});
|
||||
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-pages-with-tabs", HassioPagesWithTabs);
|
131
hassio/src/hassio-pages-with-tabs.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResultArray,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
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-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
|
||||
import "../../src/components/ha-menu-button";
|
||||
import "../../src/resources/ha-style";
|
||||
import "./hassio-tabs-router";
|
||||
|
||||
import scrollToTarget from "../../src/common/dom/scroll-to-target";
|
||||
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
HassioSupervisorInfo,
|
||||
HassioHostInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
} from "../../src/data/hassio";
|
||||
|
||||
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
|
||||
|
||||
@customElement("hassio-pages-with-tabs")
|
||||
class HassioPagesWithTabs extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public route!: Route;
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property() public hostInfo!: HassioHostInfo;
|
||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const page = this._page;
|
||||
return html`
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button hassio></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
${HAS_REFRESH_BUTTON.includes(page)
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hassio:refresh"
|
||||
@click=${this.refreshClicked}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: undefined}
|
||||
</app-toolbar>
|
||||
<paper-tabs
|
||||
scrollable
|
||||
attr-for-selected="page-name"
|
||||
.selected=${page}
|
||||
@iron-activate=${this.handlePageSelected}
|
||||
>
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<hassio-tabs-router
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassInfo=${this.hassInfo}
|
||||
></hassio-tabs-router>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
private handlePageSelected(ev) {
|
||||
const newPage = ev.detail.item.getAttribute("page-name");
|
||||
if (newPage !== this._page) {
|
||||
navigate(this, `/hassio/${newPage}`);
|
||||
}
|
||||
|
||||
scrollToTarget(
|
||||
this,
|
||||
// @ts-ignore
|
||||
this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget
|
||||
);
|
||||
}
|
||||
|
||||
private refreshClicked() {
|
||||
if (this._page === "snapshots") {
|
||||
// @ts-ignore
|
||||
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private get _page() {
|
||||
return this.route.path.substr(1);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-pages-with-tabs": HassioPagesWithTabs;
|
||||
}
|
||||
}
|
66
hassio/src/hassio-tabs-router.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./dashboard/hassio-dashboard";
|
||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||
import "./snapshots/hassio-snapshots";
|
||||
import "./addon-store/hassio-addon-store";
|
||||
import "./system/hassio-system";
|
||||
import {
|
||||
HassioSupervisorInfo,
|
||||
HassioHostInfo,
|
||||
HassioHomeAssistantInfo,
|
||||
} from "../../src/data/hassio";
|
||||
|
||||
@customElement("hassio-tabs-router")
|
||||
class HassioTabsRouter extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public supervisorInfo: HassioSupervisorInfo;
|
||||
@property() public hostInfo: HassioHostInfo;
|
||||
@property() public hassInfo: HassioHomeAssistantInfo;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-dashboard",
|
||||
},
|
||||
snapshots: {
|
||||
tag: "hassio-snapshots",
|
||||
},
|
||||
store: {
|
||||
tag: "hassio-addon-store",
|
||||
},
|
||||
system: {
|
||||
tag: "hassio-system",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
(el as PolymerElement).setProperties({
|
||||
hass: this.hass,
|
||||
supervisorInfo: this.supervisorInfo,
|
||||
hostInfo: this.hostInfo,
|
||||
hassInfo: this.hassInfo,
|
||||
});
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
el.supervisorInfo = this.supervisorInfo;
|
||||
el.hostInfo = this.hostInfo;
|
||||
el.hassInfo = this.hassInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-tabs-router": HassioTabsRouter;
|
||||
}
|
||||
}
|
100
hassio/src/ingress-view/hassio-ingress-view.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
LitElement,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
html,
|
||||
PropertyValues,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import {
|
||||
createHassioSession,
|
||||
HassioAddonDetails,
|
||||
fetchHassioAddonInfo,
|
||||
} from "../../../src/data/hassio";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
|
||||
@customElement("hassio-ingress-view")
|
||||
class HassioIngressView extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public route!: Route;
|
||||
@property() private _addon?: HassioAddonDetails;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._addon) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage .header=${this._addon.name} hassio>
|
||||
<iframe src=${this._addon.ingress_url}></iframe>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addon = this.route.path.substr(1);
|
||||
|
||||
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
|
||||
|
||||
if (addon && addon !== oldAddon) {
|
||||
this._fetchData(addon);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
try {
|
||||
const [addon] = await Promise.all([
|
||||
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) {
|
||||
throw new Error("This add-on does not support ingress");
|
||||
}
|
||||
|
||||
this._addon = addon;
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line
|
||||
console.error(err);
|
||||
alert(err.message || "Unknown error starting ingress.");
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ingress-view": HassioIngressView;
|
||||
}
|
||||
}
|
@@ -1,56 +1,64 @@
|
||||
import { css } from "lit-element";
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
|
||||
export const hassioStyle = css`
|
||||
.card-group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.card-group .title {
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-group .description {
|
||||
font-size: 0.5em;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 4;
|
||||
width: calc(
|
||||
(100% - 12px * var(--card-group-columns)) / var(--card-group-columns)
|
||||
);
|
||||
margin: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@media screen and (max-width: 1200px) and (min-width: 901px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 3;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) and (min-width: 601px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 2;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 0) {
|
||||
.card-group paper-card {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
documentContainer.innerHTML = `<dom-module id="hassio-style">
|
||||
<template>
|
||||
<style>
|
||||
.card-group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.card-group .title {
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-group .description {
|
||||
font-size: 0.5em;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 4;
|
||||
width: calc((100% - 12px * var(--card-group-columns)) / var(--card-group-columns));
|
||||
margin: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@media screen and (max-width: 1200px) and (min-width: 901px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 3;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) and (min-width: 601px) {
|
||||
.card-group paper-card {
|
||||
--card-group-columns: 2;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 0) {
|
||||
.card-group paper-card {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
ha-call-api-button {
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
${hassioStyle.toString()}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>`;
|
||||
|
@@ -1,311 +0,0 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-radio-button/paper-radio-button";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
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 EventsMixin from "../../../src/mixins/events-mixin";
|
||||
|
||||
class HassioSnapshots extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-radio-group {
|
||||
display: block;
|
||||
}
|
||||
paper-radio-button {
|
||||
padding: 0 0 2px 2px;
|
||||
}
|
||||
paper-radio-button,
|
||||
paper-checkbox,
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 48px;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Create snapshot
|
||||
<div class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your
|
||||
Hass.io instance.
|
||||
</div>
|
||||
</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
autofocus=""
|
||||
label="Name"
|
||||
value="{{snapshotName}}"
|
||||
></paper-input>
|
||||
Type:
|
||||
<paper-radio-group selected="{{snapshotType}}">
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
|
||||
Folders:
|
||||
<template is="dom-repeat" items="[[folderList]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
Add-ons:
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[addonList]]"
|
||||
sort="_sortAddons"
|
||||
>
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
Security:
|
||||
<paper-checkbox checked="{{snapshotHasPassword}}"
|
||||
>Password protection</paper-checkbox
|
||||
>
|
||||
<template is="dom-if" if="[[snapshotHasPassword]]">
|
||||
<paper-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="{{snapshotPassword}}"
|
||||
></paper-input>
|
||||
</template>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">[[error]]</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button
|
||||
disabled="[[creatingSnapshot]]"
|
||||
on-click="_createSnapshot"
|
||||
>Create</paper-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
<template is="dom-if" if="[[!snapshots.length]]">
|
||||
<paper-card>
|
||||
<div class="card-content">You don't have any snapshots yet.</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[snapshots]]"
|
||||
as="snapshot"
|
||||
sort="_sortSnapshots"
|
||||
>
|
||||
<paper-card class="pointer" on-click="_snapshotClicked">
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="[[_computeName(snapshot)]]"
|
||||
description="[[_computeDetails(snapshot)]]"
|
||||
datetime="[[snapshot.date]]"
|
||||
icon="[[_computeIcon(snapshot.type)]]"
|
||||
icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
snapshotName: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
snapshotPassword: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
snapshotHasPassword: Boolean,
|
||||
snapshotType: {
|
||||
type: String,
|
||||
value: "full",
|
||||
},
|
||||
snapshots: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
installedAddons: {
|
||||
type: Array,
|
||||
observer: "_installedAddonsChanged",
|
||||
},
|
||||
addonList: Array,
|
||||
folderList: {
|
||||
type: Array,
|
||||
value: [
|
||||
{
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: true,
|
||||
},
|
||||
{ slug: "ssl", name: "SSL", checked: true },
|
||||
{ slug: "share", name: "Share", checked: true },
|
||||
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
||||
],
|
||||
},
|
||||
snapshotSlug: {
|
||||
type: String,
|
||||
notify: true,
|
||||
},
|
||||
snapshotDeleted: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
observer: "_snapshotDeletedChanged",
|
||||
},
|
||||
creatingSnapshot: Boolean,
|
||||
dialogOpened: Boolean,
|
||||
error: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._updateSnapshots();
|
||||
}
|
||||
|
||||
_apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._updateSnapshots();
|
||||
}
|
||||
}
|
||||
|
||||
_updateSnapshots() {
|
||||
this.hass.callApi("get", "hassio/snapshots").then(
|
||||
(result) => {
|
||||
this.snapshots = result.data.snapshots;
|
||||
},
|
||||
(error) => {
|
||||
this.error = error.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_createSnapshot() {
|
||||
this.error = "";
|
||||
if (this.snapshotHasPassword && !this.snapshotPassword.length) {
|
||||
this.error = "Please enter a password.";
|
||||
return;
|
||||
}
|
||||
this.creatingSnapshot = true;
|
||||
let name = this.snapshotName;
|
||||
if (!name.length) {
|
||||
name = new Date().toLocaleDateString(navigator.language, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
let data;
|
||||
let path;
|
||||
if (this.snapshotType === "full") {
|
||||
data = { name: name };
|
||||
path = "hassio/snapshots/new/full";
|
||||
} else {
|
||||
const addons = this.addonList
|
||||
.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this.folderList
|
||||
.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
data = { name: name, folders: folders, addons: addons };
|
||||
path = "hassio/snapshots/new/partial";
|
||||
}
|
||||
if (this.snapshotHasPassword) {
|
||||
data.password = this.snapshotPassword;
|
||||
}
|
||||
|
||||
this.hass.callApi("post", path, data).then(
|
||||
() => {
|
||||
this.creatingSnapshot = false;
|
||||
this.fire("hass-api-called", { success: true });
|
||||
},
|
||||
(error) => {
|
||||
this.creatingSnapshot = false;
|
||||
this.error = error.message;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_installedAddonsChanged(addons) {
|
||||
this.addonList = addons.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
checked: true,
|
||||
}));
|
||||
}
|
||||
|
||||
_sortAddons(a, b) {
|
||||
return a.name < b.name ? -1 : 1;
|
||||
}
|
||||
|
||||
_sortSnapshots(a, b) {
|
||||
return a.date < b.date ? 1 : -1;
|
||||
}
|
||||
|
||||
_computeName(snapshot) {
|
||||
return snapshot.name || snapshot.slug;
|
||||
}
|
||||
|
||||
_computeDetails(snapshot) {
|
||||
const type =
|
||||
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
|
||||
return snapshot.protected ? `${type}, password protected` : type;
|
||||
}
|
||||
|
||||
_computeIcon(type) {
|
||||
return type === "full"
|
||||
? "hassio:package-variant-closed"
|
||||
: "hassio:package-variant";
|
||||
}
|
||||
|
||||
_snapshotClicked(ev) {
|
||||
this.snapshotSlug = ev.model.snapshot.slug;
|
||||
}
|
||||
|
||||
_fullSelected(type) {
|
||||
return type === "full";
|
||||
}
|
||||
|
||||
_snapshotDeletedChanged(snapshotDeleted) {
|
||||
if (snapshotDeleted) {
|
||||
this._updateSnapshots();
|
||||
this.snapshotDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.hass.callApi("post", "hassio/snapshots/reload").then(() => {
|
||||
this._updateSnapshots();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("hassio-snapshots", HassioSnapshots);
|
363
hassio/src/snapshots/hassio-snapshots.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResultArray,
|
||||
css,
|
||||
property,
|
||||
PropertyValues,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-radio-button/paper-radio-button";
|
||||
import "@polymer/paper-radio-group/paper-radio-group";
|
||||
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
HassioSnapshot,
|
||||
HassioSupervisorInfo,
|
||||
fetchHassioSnapshots,
|
||||
reloadHassioSnapshots,
|
||||
HassioFullSnapshotCreateParams,
|
||||
HassioPartialSnapshotCreateParams,
|
||||
createHassioFullSnapshot,
|
||||
createHassioPartialSnapshot,
|
||||
} from "../../../src/data/hassio";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
// Not duplicate, used for typing
|
||||
// tslint:disable-next-line
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
// tslint:disable-next-line
|
||||
import { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group";
|
||||
// tslint:disable-next-line
|
||||
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
name: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
@customElement("hassio-snapshots")
|
||||
class HassioSnapshots extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property() private _snapshotName = "";
|
||||
@property() private _snapshotPassword = "";
|
||||
@property() private _snapshotHasPassword = false;
|
||||
@property() private _snapshotType: HassioSnapshot["type"] = "full";
|
||||
@property() private _snapshots?: HassioSnapshot[] = [];
|
||||
@property() private _addonList: CheckboxItem[] = [];
|
||||
@property() private _folderList: CheckboxItem[] = [
|
||||
{
|
||||
slug: "homeassistant",
|
||||
name: "Home Assistant configuration",
|
||||
checked: true,
|
||||
},
|
||||
{ slug: "ssl", name: "SSL", checked: true },
|
||||
{ slug: "share", name: "Share", checked: true },
|
||||
{ slug: "addons/local", name: "Local add-ons", checked: true },
|
||||
];
|
||||
@property() private _creatingSnapshot = false;
|
||||
@property() private _error = "";
|
||||
|
||||
public async refreshData() {
|
||||
await reloadHassioSnapshots(this.hass);
|
||||
await this._updateSnapshots();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Create snapshot
|
||||
<div class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your
|
||||
Hass.io instance.
|
||||
</div>
|
||||
</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
autofocus
|
||||
label="Name"
|
||||
name="snapshotName"
|
||||
.value=${this._snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
Type:
|
||||
<paper-radio-group
|
||||
name="snapshotType"
|
||||
.selected=${this._snapshotType}
|
||||
@selected-changed=${this._handleRadioValueChanged}
|
||||
>
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
${this._snapshotType === "full"
|
||||
? undefined
|
||||
: html`
|
||||
Folders:
|
||||
${this._folderList.map(
|
||||
(folder, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${folder.checked}
|
||||
@checked-changed=${this._folderChecked}
|
||||
>
|
||||
${folder.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
Add-ons:
|
||||
${this._addonList.map(
|
||||
(addon, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked="{{item.checked}}"
|
||||
@checked-changed=${this._addonChecked}
|
||||
>
|
||||
${addon.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
`}
|
||||
Security:
|
||||
<paper-checkbox
|
||||
name="snapshotHasPassword"
|
||||
.checked=${this._snapshotHasPassword}
|
||||
@checked-changed=${this._handleCheckboxValueChanged}
|
||||
>
|
||||
Password protection
|
||||
</paper-checkbox>
|
||||
${this._snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
label="Password"
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this._snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
`
|
||||
: undefined}
|
||||
${this._error !== ""
|
||||
? html`
|
||||
<p class="error">${this._error}</p>
|
||||
`
|
||||
: undefined}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
Create
|
||||
</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
${this._snapshots === undefined
|
||||
? undefined
|
||||
: this._snapshots.length === 0
|
||||
? html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
You don't have any snapshots yet.
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
: this._snapshots.map(
|
||||
(snapshot) => html`
|
||||
<paper-card
|
||||
class="pointer"
|
||||
.snapshot=${snapshot}
|
||||
@click=${this._snapshotClicked}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${snapshot.name || snapshot.slug}
|
||||
.description=${this._computeDetails(snapshot)}
|
||||
.datetime=${snapshot.date}
|
||||
.icon=${snapshot.type === "full"
|
||||
? "hassio:package-variant-closed"
|
||||
: "hassio:package-variant"}
|
||||
.
|
||||
.icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._updateSnapshots();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("supervisorInfo")) {
|
||||
this._addonList = this.supervisorInfo.addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
checked: true,
|
||||
}))
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1));
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperInputElement;
|
||||
this[`_${input.name}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCheckboxValueChanged(ev) {
|
||||
const input = ev.currentTarget as PaperCheckboxElement;
|
||||
this[`_${input.name}`] = input.checked;
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperRadioGroupElement;
|
||||
this[`_${input.getAttribute("name")}`] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _folderChecked(ev) {
|
||||
const { idx, checked } = ev.currentTarget!;
|
||||
this._folderList = this._folderList.map((folder, curIdx) =>
|
||||
curIdx === idx ? { ...folder, checked } : folder
|
||||
);
|
||||
}
|
||||
|
||||
private _addonChecked(ev) {
|
||||
const { idx, checked } = ev.currentTarget!;
|
||||
this._addonList = this._addonList.map((addon, curIdx) =>
|
||||
curIdx === idx ? { ...addon, checked } : addon
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateSnapshots() {
|
||||
try {
|
||||
this._snapshots = await fetchHassioSnapshots(this.hass);
|
||||
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSnapshot() {
|
||||
this._error = "";
|
||||
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
|
||||
this._error = "Please enter a password.";
|
||||
return;
|
||||
}
|
||||
this._creatingSnapshot = true;
|
||||
await this.updateComplete;
|
||||
|
||||
const name =
|
||||
this._snapshotName ||
|
||||
new Date().toLocaleDateString(navigator.language, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
try {
|
||||
if (this._snapshotType === "full") {
|
||||
const data: HassioFullSnapshotCreateParams = { name };
|
||||
if (this._snapshotHasPassword) {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
await createHassioFullSnapshot(this.hass, data);
|
||||
} else {
|
||||
const addons = this._addonList
|
||||
.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this._folderList
|
||||
.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
const data: HassioPartialSnapshotCreateParams = {
|
||||
name,
|
||||
folders,
|
||||
addons,
|
||||
};
|
||||
if (this._snapshotHasPassword) {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
await createHassioPartialSnapshot(this.hass, data);
|
||||
}
|
||||
this._updateSnapshots();
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
this._creatingSnapshot = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeDetails(snapshot: HassioSnapshot) {
|
||||
const type =
|
||||
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
|
||||
return snapshot.protected ? `${type}, password protected` : type;
|
||||
}
|
||||
|
||||
private _snapshotClicked(ev) {
|
||||
showHassioSnapshotDialog(this, {
|
||||
slug: ev.currentTarget!.snapshot.slug,
|
||||
onDelete: () => this._updateSnapshots(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
hassioStyle,
|
||||
css`
|
||||
paper-radio-group {
|
||||
display: block;
|
||||
}
|
||||
paper-radio-button {
|
||||
padding: 0 0 2px 2px;
|
||||
}
|
||||
paper-radio-button,
|
||||
paper-checkbox,
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 48px;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-snapshots": HassioSnapshots;
|
||||
}
|
||||
}
|
@@ -1,15 +1,17 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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 EventsMixin from "../../../src/mixins/events-mixin";
|
||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
||||
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
|
||||
class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
<style>
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
@@ -39,7 +41,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
paper-button.info {
|
||||
mwc-button.info {
|
||||
max-width: calc(50% - 12px);
|
||||
}
|
||||
table.info {
|
||||
@@ -67,13 +69,13 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<paper-button raised on-click="_showHardware" class="info">
|
||||
<mwc-button raised on-click="_showHardware" class="info">
|
||||
Hardware
|
||||
</paper-button>
|
||||
</mwc-button>
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
|
||||
<paper-button raised on-click="_changeHostnameClicked" class="info">
|
||||
<mwc-button raised on-click="_changeHostnameClicked" class="info">
|
||||
Change hostname
|
||||
</paper-button>
|
||||
</mwc-button>
|
||||
</template>
|
||||
<template is="dom-if" if="[[errors]]">
|
||||
<div class="errors">Error: [[errors]]</div>
|
||||
@@ -173,7 +175,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
() => "Error getting hardware info"
|
||||
)
|
||||
.then((content) => {
|
||||
this.fire("hassio-markdown-dialog", {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Hardware",
|
||||
content: content,
|
||||
});
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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 EventsMixin from "../../../src/mixins/events-mixin";
|
||||
import { EventsMixin } from "../../../src/mixins/events-mixin";
|
||||
|
||||
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
<style>
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
@@ -80,11 +80,11 @@ class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
|
||||
>
|
||||
</template>
|
||||
<template is="dom-if" if='[[_equals(data.channel, "stable")]]'>
|
||||
<paper-button
|
||||
<mwc-button
|
||||
on-click="_joinBeta"
|
||||
class="warning"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>Join beta channel</paper-button
|
||||
>Join beta channel</mwc-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "@polymer/paper-button/paper-button";
|
||||
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";
|
||||
@@ -24,7 +24,7 @@ class HassioSupervisorLog extends PolymerElement {
|
||||
<paper-card>
|
||||
<div class="card-content" id="content"></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
<mwc-button on-click="refresh">Refresh</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
@@ -9,9 +8,10 @@ import "./hassio-supervisor-log";
|
||||
class HassioSystem extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
<style>
|
||||
.content {
|
||||
margin: 4px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
|
@@ -1,11 +1,15 @@
|
||||
const webpack = require("webpack");
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
const zopfli = require("@gfx/zopfli");
|
||||
|
||||
const config = require("./config.js");
|
||||
const { babelLoaderConfig } = require("../config/babel.js");
|
||||
const { minimizer } = require("../config/babel.js");
|
||||
const { babelLoaderConfig } = require("../build-scripts/babel.js");
|
||||
const webpackBase = require("../build-scripts/webpack.js");
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const isCI = process.env.CI === "true";
|
||||
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const latestBuild = false;
|
||||
|
||||
module.exports = {
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
@@ -15,7 +19,7 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild: false }),
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
@@ -27,15 +31,24 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer,
|
||||
},
|
||||
optimization: webpackBase.optimization(latestBuild),
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__DEMO__: false,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
isProdBuild &&
|
||||
!isCI &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
algorithm(input, compressionOptions, callback) {
|
||||
return zopfli.gzip(input, compressionOptions, callback);
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
|
164
package.json
@@ -17,8 +17,10 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mdi/svg": "^3.0.39",
|
||||
"@polymer/app-layout": "^3.0.1",
|
||||
"@material/mwc-button": "^0.5.0",
|
||||
"@material/mwc-ripple": "^0.5.0",
|
||||
"@mdi/svg": "3.5.95",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
@@ -32,19 +34,19 @@
|
||||
"@polymer/iron-input": "^3.0.1",
|
||||
"@polymer/iron-label": "^3.0.1",
|
||||
"@polymer/iron-media-query": "^3.0.1",
|
||||
"@polymer/iron-overlay-behavior": "^3.0.2",
|
||||
"@polymer/iron-pages": "^3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||
"@polymer/neon-animation": "^3.0.1",
|
||||
"@polymer/paper-button": "^3.0.1",
|
||||
"@polymer/paper-card": "^3.0.1",
|
||||
"@polymer/paper-checkbox": "^3.0.1",
|
||||
"@polymer/paper-checkbox": "^3.1.0",
|
||||
"@polymer/paper-dialog": "^3.0.1",
|
||||
"@polymer/paper-dialog-behavior": "^3.0.1",
|
||||
"@polymer/paper-dialog-scrollable": "^3.0.1",
|
||||
"@polymer/paper-drawer-panel": "^3.0.1",
|
||||
"@polymer/paper-dropdown-menu": "^3.0.1",
|
||||
"@polymer/paper-fab": "^3.0.1",
|
||||
"@polymer/paper-icon-button": "^3.0.1",
|
||||
"@polymer/paper-icon-button": "^3.0.2",
|
||||
"@polymer/paper-input": "^3.0.1",
|
||||
"@polymer/paper-item": "^3.0.1",
|
||||
"@polymer/paper-listbox": "^3.0.1",
|
||||
@@ -55,110 +57,120 @@
|
||||
"@polymer/paper-ripple": "^3.0.1",
|
||||
"@polymer/paper-scroll-header-panel": "^3.0.1",
|
||||
"@polymer/paper-slider": "^3.0.1",
|
||||
"@polymer/paper-spinner": "^3.0.1",
|
||||
"@polymer/paper-spinner": "^3.0.2",
|
||||
"@polymer/paper-styles": "^3.0.1",
|
||||
"@polymer/paper-tabs": "^3.0.1",
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-toggle-button": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "^3.0.5",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.0",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.0",
|
||||
"chart.js": "~2.7.2",
|
||||
"chartjs-chart-timeline": "^0.2.1",
|
||||
"codemirror": "^5.43.0",
|
||||
"@polymer/polymer": "^3.2.0",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.8",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.3",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
"chartjs-chart-timeline": "^0.3.0",
|
||||
"codemirror": "^5.45.0",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.1",
|
||||
"fecha": "^3.0.0",
|
||||
"home-assistant-js-websocket": "^3.2.4",
|
||||
"fecha": "^3.0.2",
|
||||
"fuse.js": "^3.4.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "^4.2.1",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.12.0",
|
||||
"leaflet": "^1.3.4",
|
||||
"lit-element": "2.0.0-rc.5",
|
||||
"lit-html": "1.0.0-rc.2",
|
||||
"marked": "^0.6.0",
|
||||
"mdn-polyfills": "^5.12.0",
|
||||
"moment": "^2.22.2",
|
||||
"preact": "^8.3.1",
|
||||
"js-yaml": "^3.13.0",
|
||||
"leaflet": "^1.4.0",
|
||||
"lit-element": "^2.1.0",
|
||||
"lit-html": "^1.0.0",
|
||||
"marked": "^0.6.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"moment": "^2.24.0",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.18.4",
|
||||
"react-big-calendar": "^0.19.2",
|
||||
"regenerator-runtime": "^0.12.1",
|
||||
"round-slider": "^1.3.2",
|
||||
"superstruct": "^0.6.0",
|
||||
"unfetch": "^4.0.1",
|
||||
"react-big-calendar": "^0.20.4",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"round-slider": "^1.3.3",
|
||||
"superstruct": "^0.6.1",
|
||||
"unfetch": "^4.1.0",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"xss": "^1.0.3"
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-external-helpers": "^7.0.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@gfx/zopfli": "^1.0.9",
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-external-helpers": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/preset-env": "^7.4.2",
|
||||
"@babel/preset-typescript": "^7.3.3",
|
||||
"@gfx/zopfli": "^1.0.11",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/codemirror": "^0.0.71",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"babel-eslint": "^10",
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-minify-webpack-plugin": "^0.3.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"chai": "^4.2.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^5.6.0",
|
||||
"copy-webpack-plugin": "^5.0.2",
|
||||
"del": "^4.0.0",
|
||||
"eslint": "^5.15.3",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-prettier": "^3.0.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^3.9.1",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.11.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"fs-extra": "^7.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.2.2",
|
||||
"gulp-hash-filename": "^2.0.1",
|
||||
"gulp-insert": "^0.5.0",
|
||||
"gulp-json-transform": "^0.4.5",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^1.1.0",
|
||||
"lint-staged": "^8.0.2",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.5",
|
||||
"lodash.template": "^4.4.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha": "^6.0.2",
|
||||
"parse5": "^5.1.0",
|
||||
"polymer-cli": "^1.8.0",
|
||||
"prettier": "^1.14.3",
|
||||
"raw-loader": "^0.5.1",
|
||||
"polymer-cli": "^1.9.7",
|
||||
"prettier": "^1.16.4",
|
||||
"raw-loader": "^2.0.0",
|
||||
"reify": "^0.18.1",
|
||||
"require-dir": "^1.0.0",
|
||||
"sinon": "^7.1.1",
|
||||
"ts-mocha": "^2.0.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-prettier": "^1.15.0",
|
||||
"require-dir": "^1.2.0",
|
||||
"sinon": "^7.3.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"tslint": "^5.14.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.1.4",
|
||||
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||
"wct-browser-legacy": "^1.0.1",
|
||||
"web-component-tester": "^6.8.0",
|
||||
"webpack": "^4.19.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.1.8",
|
||||
"workbox-webpack-plugin": "^3.5.0"
|
||||
"typescript": "^3.4.1",
|
||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
||||
"wct-browser-legacy": "^1.0.2",
|
||||
"web-component-tester": "^6.9.2",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-cli": "^3.3.0",
|
||||
"webpack-dev-server": "^3.2.1",
|
||||
"webpack-manifest-plugin": "^2.0.4",
|
||||
"workbox-webpack-plugin": "^4.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@webcomponents/webcomponentsjs": "2.2.1",
|
||||
"@webcomponents/shadycss": "^1.6.0",
|
||||
"@vaadin/vaadin-overlay": "3.2.2",
|
||||
"@vaadin/vaadin-lumo-styles": "1.3.0"
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"@vaadin/vaadin-lumo-styles": "^1.4.2"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
@@ -1,30 +1,7 @@
|
||||
"""Frontend for Home Assistant."""
|
||||
import os
|
||||
from user_agents import parse
|
||||
|
||||
FAMILY_MIN_VERSION = {
|
||||
'Chrome': 55, # Async/await
|
||||
'Chrome Mobile': 55,
|
||||
'Firefox': 52, # Async/await
|
||||
'Firefox Mobile': 52,
|
||||
'Opera': 42, # Async/await
|
||||
'Edge': 15, # Async/await
|
||||
'Safari': 10.1, # Async/await
|
||||
}
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def where():
|
||||
"""Return path to the frontend."""
|
||||
return os.path.dirname(__file__)
|
||||
|
||||
|
||||
def version(useragent):
|
||||
"""Get the version for given user agent."""
|
||||
useragent = parse(useragent)
|
||||
|
||||
# on iOS every browser uses the Safari engine
|
||||
if useragent.os.family == 'iOS':
|
||||
return useragent.os.version[0] >= FAMILY_MIN_VERSION['Safari']
|
||||
|
||||
version = FAMILY_MIN_VERSION.get(useragent.browser.family)
|
||||
return version and useragent.browser.version[0] >= version
|
||||
return Path(__file__).parent
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 824 B |
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |