Compare commits

..

416 Commits

Author SHA1 Message Date
Paulus Schoutsen
c7f7e72340 Merge pull request #3377 from home-assistant/dev
20190717.1
2019-07-17 15:10:22 -07:00
Paulus Schoutsen
1205322342 Bumped version to 20190717.1 2019-07-17 15:09:26 -07:00
Joakim Plate
4cefb9715c Make sure sliderType is set first (#3376)
* Make sure sliderType is set first

* No need for a separate name
2019-07-17 15:08:51 -07:00
Paulus Schoutsen
35b38db57f Merge branch 'master' into dev 2019-07-17 15:08:31 -07:00
Paulus Schoutsen
4f72eb5416 Remove check 2019-07-17 13:01:12 -07:00
Paulus Schoutsen
f3d1a421f4 Update pipeline 2019-07-17 13:00:34 -07:00
Paulus Schoutsen
f3c24dc0b3 Remove tsc check 2019-07-17 12:41:07 -07:00
Paulus Schoutsen
e4cbdc29a2 Update azure-pipelines-release.yml 2019-07-17 12:33:46 -07:00
Paulus Schoutsen
c985977efc Update pipeline 2019-07-17 12:17:52 -07:00
Paulus Schoutsen
8fb991c5ce Merge remote-tracking branch 'origin/dev' 2019-07-17 12:15:52 -07:00
Paulus Schoutsen
210c63ad14 Bumped version to 20190717.0 2019-07-17 12:15:13 -07:00
Paulus Schoutsen
8167b05cad Update translations 2019-07-17 12:15:12 -07:00
Paulus Schoutsen
e5a916032a Merge pull request #3375 from home-assistant/dev
20190717.0
2019-07-17 12:09:36 -07:00
Paulus Schoutsen
56745b3723 Set up CI with Azure Pipelines
[skip ci]
2019-07-17 11:50:07 -07:00
Paulus Schoutsen
ddf2c6cc0f Add Azure pipelines for release 2019-07-17 11:48:50 -07:00
Joakim Plate
84df2bd531 Make sure slider type updates with changes to temperatures (#3374) 2019-07-17 11:32:22 -07:00
Paulus Schoutsen
42c3e3e46c Differentiate Heat/Cool (#3371) 2019-07-16 23:51:15 -07:00
Paulus Schoutsen
5141e0e923 Merge pull request #3370 from home-assistant/fixes-yo
Fixes yo
2019-07-16 22:56:10 -07:00
Paulus Schoutsen
b87c94e395 Fix typing 2019-07-16 22:41:47 -07:00
Paulus Schoutsen
55aa5a0d12 Scroll device list when goes out of screen. Fixes #3343 2019-07-16 21:43:15 -07:00
Paulus Schoutsen
eaaeb10c6d Store width before searching to avoid jumping 2019-07-16 21:34:12 -07:00
Paulus Schoutsen
567769be5a Use hass icons for search input 2019-07-16 21:33:09 -07:00
Paulus Schoutsen
3ebb30bd48 Only show advanced mode toggle for admins. Fixes #3369 2019-07-16 20:43:05 -07:00
Paulus Schoutsen
09a19d2e7f Allow scrolling notification drawer. Fixes 3366 2019-07-16 20:38:35 -07:00
Paulus Schoutsen
fabc49d17e Fix theme color in dev index.html 2019-07-16 20:36:40 -07:00
Paulus Schoutsen
00e9155546 Merge pull request #3368 from home-assistant/dev
20190715.0
2019-07-15 13:50:25 -07:00
Paulus Schoutsen
8238b700b0 Update translations uno mas 2019-07-15 13:38:44 -07:00
Paulus Schoutsen
5ff33224ed Remove unused keys 2019-07-15 13:15:50 -07:00
Paulus Schoutsen
07dee9c5bb Update translations 2019-07-15 13:11:37 -07:00
Paulus Schoutsen
9eaeafdd6a Bumped version to 20190715.0 2019-07-15 13:05:11 -07:00
Paulus Schoutsen
beb1fe1e64 Update translations 2019-07-15 13:05:06 -07:00
Timmo
cdb2a1a424 Integrations - Add Search (#3361)
*  Add search to flow picker

* 🔨 Autofocus

* 🔨 squash extra space

* Update src/dialogs/config-flow/step-flow-pick-handler.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Update src/dialogs/config-flow/step-flow-pick-handler.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>
2019-07-14 13:47:33 -07:00
Paulus Schoutsen
8bbc442b7e Correctly check frontend version (#3365) 2019-07-14 13:30:50 -07:00
Sean Mooney
7a12cbf96e Add link to Alexa skill on Cloud page (#3364)
This adds a direct link to the Home Assistant Alexa skill, which is more convenient than telling the user to search for it themselves. On mobile devices it can open the skill directly in the Alexa app which is nice.

Also tweaked the wording from "activate" to "enable" since that is what Amazon uses.
2019-07-12 23:45:27 -07:00
Paulus Schoutsen
e36454f08f Fix token card not showing (#3362) 2019-07-12 22:36:36 -07:00
Timmo
3865c1943c 🔨 Fix mqtt dev tools missing class (#3359) 2019-07-12 15:31:38 -07:00
Paulus Schoutsen
e7e3edfd97 Merge pull request #3358 from home-assistant/dev
20190712.0
2019-07-12 14:58:22 -07:00
Paulus Schoutsen
4bdc82f0ed Bumped version to 20190712.0 2019-07-12 14:44:22 -07:00
Paulus Schoutsen
8e3b41885d Update translations 2019-07-12 14:44:07 -07:00
Paulus Schoutsen
8f3d5fdb7d Fix scrollbar on Firefox (#3357) 2019-07-12 14:42:56 -07:00
Timmo
f258aa2818 Replace standard browser scrollbar for overview and sidebar (#3350)
* 📜 Replace standard browser scrollbar for overview and sidebar

* 🔨 Remove from main
2019-07-12 13:08:51 -07:00
Charles Garwood
b4dd971829 Z-Wave Config Panel Updates (#3349)
* Display network status, hide buttons if network is stopped.

* travis/lint updates

* Review comments

* Add translations

* lint

* Missed a translation

* lint again...

* Fix unsubscribe function?

* lint again

* Remove state_str

* Code review comments

* fix for lit re-rendering & possible undefined value
2019-07-12 13:07:50 -07:00
Sean Mooney
e99d6f8e6a Fix incorrect tooltip on Add Person button (#3355)
The tooltip for the add person button incorrectly says "Create Area" when hovered. This should fix it, I think.
2019-07-12 12:07:46 -07:00
Paulus Schoutsen
cc969e547c Add frontend version to info (#3354) 2019-07-12 11:18:01 -07:00
Paulus Schoutsen
0e1ae3926b Merge pull request #3348 from home-assistant/dev
20190710.0
2019-07-10 10:35:46 -07:00
Charles Garwood
60c2bcc483 Convert Z-Wave panel to TS/Lit (#3345)
* Convert zwave-network to typescript/lit

* Convert zwave-network to ts/lit

* Updates per code review

* missed one (hass? => hass!)

* Remove constructor
2019-07-10 10:26:17 -07:00
Paulus Schoutsen
5d8e34e8be Show menu button when Hassio supervisor UI runs on old HA (#3347) 2019-07-10 10:10:56 -07:00
Paulus Schoutsen
14a430a059 Bumped version to 20190710.0 2019-07-10 09:36:44 -07:00
Paulus Schoutsen
4ae347949a Update translations 2019-07-10 09:36:35 -07:00
Paulus Schoutsen
cdd007cc54 Ensure load order (#3346) 2019-07-10 09:27:25 -07:00
Paulus Schoutsen
2929db5ba4 Fix fonts precaching (#3338)
* Fix font urls

* Don't zopfli nonexisting fonts
2019-07-10 08:18:04 -07:00
Paulus Schoutsen
628692b2e9 Do not cache onboarding page (#3337) 2019-07-10 08:17:51 -07:00
Paulus Schoutsen
7cfdc24a8c Fix test translations (#3339) 2019-07-10 08:17:41 -07:00
Paulus Schoutsen
1c69aa122b Merge pull request #3334 from home-assistant/dev
20190705.0
2019-07-05 16:09:33 -07:00
Paulus Schoutsen
25afb73ed7 Bumped version to 20190705.0 2019-07-05 15:25:01 -07:00
Paulus Schoutsen
5b5384032d Update translations 2019-07-05 15:25:01 -07:00
Jérôme W
a9d221147f Fix timer stops updating when changing views in LL (#3248)
* Fix timer stops updating when changing views in LL

* Test if _config exists in connectedCallback()

* Update hui-timer-entity-row.ts
2019-07-05 15:22:29 -07:00
Paulus Schoutsen
4fdbec93b3 Migrate for climate 1.0 (#3333)
* Migrate for climate 1.0

* Update demo

* Fix gallery

* Add preset to thermostat card

* Fix climate entity row
2019-07-05 15:13:53 -07:00
Thomas Lovén
0a8703ad0a Allow selecting text in error-card (#3330)
* Allow selecting text in error-card

* Still use default cursor
2019-07-02 20:28:39 -07:00
Paulus Schoutsen
ddc11c1b12 Merge pull request #3329 from home-assistant/dev
20190702.0
2019-07-02 10:33:40 -07:00
Paulus Schoutsen
317f43277e Bumped version to 20190702.0 2019-07-02 10:32:43 -07:00
Paulus Schoutsen
bf90642c9b Update translations 2019-07-02 10:32:40 -07:00
Paulus Schoutsen
6f77992387 Random fixes (#3328)
* Fix scroll into view on first load

* Do not crash when deleting script/automation

* Disable swipe on notification drawer
2019-07-02 10:31:48 -07:00
David F. Mulcahey
deaccd6cd4 Add info to ZHA device card (#3327)
* add zha domain icon

* update device card
2019-07-02 10:13:59 -07:00
Paulus Schoutsen
d7371ace6a Sidebar improvements (#3325)
* Do not contract sidebar when undocking sidebar

* Do not hide text until fully contracted

* Cancel hover expanding on tablets

* Open notifications on the left

* Set property before opening

* Fix check for support scroll if needed
2019-07-01 10:35:10 -07:00
Paulus Schoutsen
453b1000c1 Merge branch 'dev' 2019-06-30 23:03:58 -07:00
Paulus Schoutsen
8c1aff7505 Check supported func properly 2019-06-30 23:03:51 -07:00
Paulus Schoutsen
adf002c154 Merge branch 'dev' 2019-06-30 23:02:58 -07:00
Paulus Schoutsen
ed7b81e7a4 Hide scrollbars on sidebar during expanding 2019-06-30 23:02:48 -07:00
Paulus Schoutsen
c0f6ee6a32 Merge branch 'dev' 2019-06-30 22:52:44 -07:00
Paulus Schoutsen
9408df6099 Fix Firefox 2019-06-30 22:52:31 -07:00
Paulus Schoutsen
157bfd6f80 Space out sidebar 2019-06-30 22:21:37 -07:00
Paulus Schoutsen
99da7ebfe6 Merge branch 'dev' 2019-06-30 21:58:32 -07:00
Paulus Schoutsen
6911df9ac4 Fix sidebar in Safari 2019-06-30 21:58:02 -07:00
Paulus Schoutsen
8daeaab40b Merge pull request #3324 from home-assistant/dev
20190630.0
2019-06-30 15:20:33 -07:00
Paulus Schoutsen
45c3c78b31 Bumped version to 20190630.0 2019-06-30 15:15:56 -07:00
Paulus Schoutsen
a64a35b861 Update translations 2019-06-30 15:15:52 -07:00
Paulus Schoutsen
5a25627219 Stop playing video when more info is closed (#3318) 2019-06-30 15:14:42 -07:00
Paulus Schoutsen
203b14613f Show a notification dot on toggle menu button in narrow mode (#3323)
* Show a notification dot on toggle menu button in narrow mode

* Fix lint

* Move menu button to sidebar

* Fix height sidebar
2019-06-30 15:02:53 -07:00
Paulus Schoutsen
0a7cb39500 Add transition delay to sidebar expand 2019-06-28 14:46:54 -07:00
Paulus Schoutsen
42e75e7cdf Move notifications to the sidebar (#3317)
* Move notifications to the sidebar

* Close when navigating

* Lint
2019-06-28 14:23:29 -07:00
Paulus Schoutsen
58e6be12af Add developer tools panel (#3313) 2019-06-28 08:34:29 -07:00
Paulus Schoutsen
618d25ce48 Fix person showing value in badge 2019-06-28 08:28:52 -07:00
Paulus Schoutsen
9974510067 Merge pull request #3311 from home-assistant/dev
20190627.0
2019-06-27 17:56:22 -07:00
Paulus Schoutsen
2faa0c5979 Bumped version to 20190627.0 2019-06-27 17:48:44 -07:00
Paulus Schoutsen
1479647062 Update translations 2019-06-27 17:48:38 -07:00
Paulus Schoutsen
3becefaf8b Add a couple of labels (#3310)
* Add a couple of labels

* Add some more labels
2019-06-27 17:47:19 -07:00
Paulus Schoutsen
e804e62e66 Add advanced mode (#3298)
* Add advanced mode

* Move advanced mode to profile

* Add promo for advanced mode
2019-06-27 16:17:32 -07:00
Paulus Schoutsen
2c3cc1fbc7 experimental sidebar (#3306)
* experimental sidebar

* Change default docked sidebar to true

* remove delay

* Push things down

* Speed up animation

* Always open on big screens

* Move things around

* Final tweaks

* Lint

* Don't open on hover logo
2019-06-27 15:23:05 -07:00
Paulus Schoutsen
58cc76ab5a Merge pull request #3305 from home-assistant/dev
20190626.0
2019-06-26 09:10:32 -07:00
Paulus Schoutsen
5783cdb0d2 Bumped version to 20190626.0 2019-06-26 09:05:00 -07:00
Paulus Schoutsen
4f07caebc6 Update translations 2019-06-26 09:04:56 -07:00
Paulus Schoutsen
c4b75b4534 Fix typo in font definition (#3304) 2019-06-25 23:39:50 -07:00
Paulus Schoutsen
ae82eabaec Fix link colors in hassio addon view 2019-06-25 13:08:11 -07:00
Paulus Schoutsen
f8d3e55fe0 Fix home zone so it doesn't overlap the city name 2019-06-25 10:17:26 -07:00
Paulus Schoutsen
1462db0a76 Merge pull request #3303 from home-assistant/dev
20190624.1
2019-06-24 22:15:47 -07:00
Paulus Schoutsen
86b36fb76b Bumped version to 20190624.1 2019-06-24 22:06:35 -07:00
Paulus Schoutsen
c6194622b1 Update translations 2019-06-24 22:06:14 -07:00
Paulus Schoutsen
be5c3efb23 Fix source maps 2019-06-24 22:05:34 -07:00
Paulus Schoutsen
999c243c94 Better error report on Alexa (#3302) 2019-06-24 17:36:12 -07:00
Paulus Schoutsen
483f82e554 Add demo sw (#3301) 2019-06-24 10:39:27 -07:00
Paulus Schoutsen
e91f4567c2 Merge pull request #3300 from home-assistant/dev
20190624.0
2019-06-24 08:26:03 -07:00
Paulus Schoutsen
029467139d Bumped version to 20190624.0 2019-06-24 08:14:17 -07:00
Paulus Schoutsen
29649abe3d Update translations 2019-06-24 08:14:12 -07:00
Mauricio Bonani
266c80320b Upgrade mdi to 3.7.95 (#3299)
* Update yarn.lock

* Update package.json
2019-06-22 18:18:28 -07:00
Paulus Schoutsen
ae51300446 Revert Polymer to 3.1 (#3297)
* Revert Polymer to 3.1

* Only install 1 polymer
2019-06-22 12:36:30 -07:00
Thomas Lovén
cbdb222f72 Import extra_html_url using js instead of deprecated link tags (#3288)
* import extra_html_url using js instead of deprecated link tags

* Import modules instead

* Address comment

* Rename variables for es5 scripts

* Address comments
2019-06-21 20:55:53 -07:00
Paulus Schoutsen
98c419ff03 Switch to woff2 fonts (#3296) 2019-06-21 12:48:20 -07:00
Paulus Schoutsen
88b9348a81 Add map to demo (#3295) 2019-06-21 11:05:01 -07:00
Paulus Schoutsen
3e8606781e Fix typo 2019-06-21 09:31:15 -07:00
Paulus Schoutsen
875afbd7ae Merge pull request #3294 from home-assistant/dev
20190620.0
2019-06-20 23:52:08 -07:00
Paulus Schoutsen
3139b914d7 Bumped version to 20190620.0 2019-06-20 23:43:45 -07:00
Paulus Schoutsen
212a44b6ae Update translations 2019-06-20 23:43:39 -07:00
Paulus Schoutsen
60551168a2 Update demo (#3292)
* Update demo

* Kill homebridge hidden

* Add missing air translation

* Hide demo card with localStorage.hide_demo_card

* adjust size of demo card if hidden

* disable lint rule
2019-06-20 23:40:56 -07:00
Robbie Trencheny
32d9a6884f Add some aria-labels for use with fastlane snapshot (#3293) 2019-06-20 23:16:50 -07:00
Mauricio Bonani
7002ab27c0 Upgrade mdi to 3.6.95 (#3291)
* Update package.json

* Update yarn.lock
2019-06-20 14:45:43 -07:00
Bram Kragten
15c101109e Update hui-map-card.ts (#3289) 2019-06-20 08:34:34 -07:00
Paulus Schoutsen
316fed953a Merge pull request #3286 from home-assistant/dev
20190619.0
2019-06-19 16:21:38 -07:00
Paulus Schoutsen
93934449c0 Bumped version to 20190619.0 2019-06-19 16:11:45 -07:00
Paulus Schoutsen
894a25c98e Update translations 2019-06-19 16:11:41 -07:00
Paulus Schoutsen
90f0d9fa00 Add button for Alexa to sync entities (#3284)
* Add button for Alexa to sync entities

* Lint
2019-06-19 11:43:17 -07:00
Paulus Schoutsen
83889a8fd7 Filter met during onboarding (#3285)
* Filter met during onboarding

* Hide temp values with no value
2019-06-19 11:42:46 -07:00
Paulus Schoutsen
4cfc429e75 Hide unused entities in the demo 2019-06-18 11:59:51 -07:00
Paulus Schoutsen
2df829b79d Hide unused entities in the demo 2019-06-18 11:57:21 -07:00
Paulus Schoutsen
7baf6382ac Merge pull request #3282 from home-assistant/dev
20190618.0
2019-06-18 11:47:21 -07:00
Paulus Schoutsen
7d1f689ed9 Bumped version to 20190618.0 2019-06-18 11:21:56 -07:00
Paulus Schoutsen
4f448553f6 Update translations 2019-06-18 11:21:52 -07:00
David F. Mulcahey
dd56671974 Add NWK to ZHA device card and fix search again (#3278)
* add nwk to device info

* fix search again
2019-06-18 11:14:37 -07:00
Paulus Schoutsen
d8e0fd0ba5 Limit demo card mobile (#3281)
* Limit demo card on mobile

* Remove unused deps

* Remove tabs from ARS demo

* Set title to Home Assistant
2019-06-18 10:44:11 -07:00
Paulus Schoutsen
a9320d4baf Fix import 2019-06-17 20:12:29 -07:00
Kevin Cooper
42475becf1 Fix for more-info-alarm_control_panel when using code_arm_required (#3052)
* Fix for more-infoalarm_control_panel when using code_arm_required

* Update more-info-alarm_control_panel.js
2019-06-17 15:08:14 -07:00
Justin Bassett
c30aca8484 Dark Mode for Map Card (#3250)
* Ability to change tile set from light_all to dark_all through card config.

* Use correct boolean.

* Fix possible undefined.

* Use correct value.
2019-06-14 20:37:34 -07:00
Paulus Schoutsen
25bdf50737 Use users location 2019-06-14 15:25:41 -07:00
Paulus Schoutsen
4a60479b74 Merge branch 'dev' 2019-06-14 15:09:54 -07:00
Paulus Schoutsen
f6d651304c Fix demo 2019-06-14 15:09:42 -07:00
Paulus Schoutsen
85990c20ed Merge pull request #3275 from home-assistant/dev
20190614.0
2019-06-14 14:40:40 -07:00
Paulus Schoutsen
8ea98023a5 Bumped version to 20190614.0 2019-06-14 13:41:39 -07:00
Paulus Schoutsen
acceaea410 Update translations 2019-06-14 13:41:33 -07:00
Paulus Schoutsen
d609155022 Make setView not async (#3274) 2019-06-14 13:40:16 -07:00
Paulus Schoutsen
1add5077af Add Alexa report state (#3272) 2019-06-14 13:30:35 -07:00
Paulus Schoutsen
a9cac343b0 Preload LL (#3273)
* Preload LL

* Remove using observer
2019-06-14 13:30:20 -07:00
Paulus Schoutsen
1b441a752e Manage Alexa entities (#3269)
* Reorg cloud components

* Allow managing Alexa entities in the UI

* Use observer

* Update mwc version

* Tweak some UI
2019-06-13 11:57:56 -07:00
Ofek Ashery
03fee95f68 Allow to copy text in the notifications panel (#3257) 2019-06-12 21:01:14 -07:00
Paulus Schoutsen
7fa4b18843 Group managed entities (#3268) 2019-06-12 21:00:41 -07:00
Paulus Schoutsen
7b0fb949fd Upgrade some deps (#3266)
* Upgrade some deps

* Fix linting
2019-06-12 17:29:16 -07:00
Paulus Schoutsen
df10cff842 Add loading screen when external step done (#3261) 2019-06-11 15:19:12 -07:00
Penny Wood
8b93af1b56 Via Hub Rename (#3254) 2019-06-11 08:36:48 -07:00
Tor Arne Vestbø
a396a4e666 Pass credentials (cookies e.g.) when loading JavaScript assets (#3259)
The behavior of 'crossorigin' without a value is the same as
anonymous, which means user credentials such as cookies,
client-side SSL certificates or HTTP authentication will
not be passed on.

We want the preload links to work even when they are behind
a proxy that requires an authentication cookie, such as
Cloudflare Access (CF_Authorization), so we need to explicitly
send credentials with the "use-credentials" value.

ES modules are always fetched with CORS, with anonymous being
the default. Some browsers (Chromium) will realize that the
request is to the same origin, and send credentials anyways,
while others (Safari) will not, so we need to explicitly send
credentials to make sure they load in all cases.

See https://jakearchibald.com/2017/es-modules-in-browsers/
2019-06-10 13:41:59 -07:00
Paulus Schoutsen
8f278ec4bc Merge pull request #3247 from home-assistant/dev
20190604.0
2019-06-04 08:50:01 -07:00
Paulus Schoutsen
032ebce0bc Bumped version to 20190604.0 2019-06-04 08:48:41 -07:00
Paulus Schoutsen
bb60b42f98 Update translations 2019-06-04 08:48:37 -07:00
Paulus Schoutsen
21ed717287 Link to beta release notes for beta release in hassio (#3243) 2019-06-04 08:47:40 -07:00
Paulus Schoutsen
2d056bad81 Allow picking location on a map (#3244)
* Allow picking location on a map

* Add some better defaults

* Close connection before navigation
2019-06-04 08:47:02 -07:00
Paulus Schoutsen
8297e9e215 Merge pull request #3242 from home-assistant/dev
20190602.0
2019-06-02 13:52:11 -07:00
Paulus Schoutsen
4ccf450ad4 Bumped version to 20190602.0 2019-06-02 13:45:20 -07:00
Paulus Schoutsen
fc056869a7 Update translations 2019-06-02 13:45:16 -07:00
Paulus Schoutsen
0bd5ff34d4 Google banner background to support incorrect themes (#3241) 2019-06-02 13:44:01 -07:00
Paulus Schoutsen
ffd272d3fe Fix refreshing cloud prefs after changing Google prefs (#3240) 2019-06-02 13:37:55 -07:00
Paulus Schoutsen
1eee186e79 Fix toast on Firefox (#3239) 2019-06-02 16:13:10 +02:00
Paulus Schoutsen
3a05b1124a Merge branch 'dev' 2019-06-01 14:26:26 -07:00
Paulus Schoutsen
d14c6125da Bumped version to 20190601.0 2019-06-01 14:26:13 -07:00
Paulus Schoutsen
3ee357178e Update translations 2019-06-01 14:26:12 -07:00
Paulus Schoutsen
8f6fdea4eb Merge pull request #3238 from home-assistant/dev
20190601.0
2019-06-01 14:25:00 -07:00
Paulus Schoutsen
be6b25f5be Google Entities updates (#3237)
* Match Google Expose default with backend for when no config value set

* Allow toggling domains
2019-06-01 14:23:32 -07:00
Paulus Schoutsen
be4dd5b20b Fix Google manage entity button on small screens (#3234) 2019-05-31 21:26:47 -07:00
Paulus Schoutsen
fe4811b278 Allow saving unit system (#3235) 2019-05-31 15:51:12 -07:00
Paulus Schoutsen
d376457cec Merge pull request #3231 from home-assistant/dev
20190530.0
2019-05-30 09:32:06 -07:00
Paulus Schoutsen
35e82a8e26 Bumped version to 20190530.0 2019-05-30 09:31:29 -07:00
Paulus Schoutsen
03735f0539 Update translations 2019-05-30 09:31:23 -07:00
Paulus Schoutsen
4cc812c1bf Always have a gauge base unit (#3229) 2019-05-30 08:42:02 -07:00
Paulus Schoutsen
bdacd05fab Pass connection instead of hass (#3228) 2019-05-30 08:41:52 -07:00
Paulus Schoutsen
ab157fdbff Correctly warn if Google entities defined in YAML (#3230) 2019-05-30 08:41:44 -07:00
Paulus Schoutsen
d94223a61e Dynamic update panels (#3227) 2019-05-30 08:41:32 -07:00
Paulus Schoutsen
ebe3198c27 Merge pull request #3225 from home-assistant/dev
20190529.0
2019-05-29 08:55:07 -07:00
Paulus Schoutsen
2b2d2effd2 Bumped version to 20190529.0 2019-05-29 08:42:17 -07:00
Paulus Schoutsen
8092e24af8 Update translations 2019-05-29 08:42:06 -07:00
Paulus Schoutsen
f019bb095d Allow edit default config (#3220) 2019-05-29 08:39:38 -07:00
Paulus Schoutsen
1ad9d2e54c Add UI to manage Google Entities exposed to Cloud (#3224)
* Add UI to manage Google Entities exposed to Cloud

* Add selected count
2019-05-29 08:38:52 -07:00
Paulus Schoutsen
b2b18cb814 Convert cloud dashboard to use Lit router (#3223) 2019-05-28 14:31:51 -07:00
Bram Kragten
e595637a10 Show toast when Lovelace config was updated from a different place (#3218)
* Refresh other lovelace UI's when making a change

* Move to toast with refresh button

* Change to `hass-notification`

* Reload on reconnect

- Fix for duration = 0
- Reload on reconnect

* Listen to ready of connection

* Update src/managers/notification-manager.ts

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* use showToast, listen connection-status,  noCancelOnOutsideClick -> option

* Remove unused import
2019-05-27 14:54:14 -07:00
Bram Kragten
d10a0b3b6c Update translations.js (#3219) 2019-05-26 20:59:52 -07:00
Penny Wood
c24f8a2115 Allow editing of device name (#3209)
* Allow editing of device name

* Patches

* Update dialog-device-registry-detail.ts
2019-05-26 07:27:33 -07:00
Paulus Schoutsen
7691e3f2c2 Fix duplicate tab when regen LL (#3217) 2019-05-25 12:30:44 -07:00
Paulus Schoutsen
6bbe8ff39f Merge pull request #3214 from home-assistant/dev
20190523.0
2019-05-23 13:31:26 -07:00
Paulus Schoutsen
a1e9b4938f Bumped version to 20190523.0 2019-05-23 13:20:59 -07:00
Paulus Schoutsen
c826596529 Update translations 2019-05-23 13:20:55 -07:00
Paulus Schoutsen
7f47079750 Notify about remote portal (#3211)
* Notify about remote portal

* Update text
2019-05-23 13:16:50 -07:00
Bram Kragten
642ba1adc3 Create dummy translation on dev (#3213) 2019-05-23 13:16:24 -07:00
Paulus Schoutsen
fe80c7fe0e Warn that only automations in automations.yaml are editable (#3200) 2019-05-23 12:28:17 -07:00
Paulus Schoutsen
9309c5a1b6 Update Lit-HTML to 1.1 (#3210) 2019-05-22 19:36:07 -07:00
Paulus Schoutsen
575eb22608 Add UI to set/update core config (#3208)
* Add UI to set/update core config

* Types

* Disable editor in config.yaml mode

* Fix type
2019-05-21 20:12:07 -07:00
Paulus Schoutsen
be0bef3f1b Allow automation/script delete (#3194) 2019-05-16 17:44:46 +02:00
Paulus Schoutsen
970286bbba Merge pull request #3197 from home-assistant/dev
20190514.0
2019-05-14 07:06:58 +02:00
Paulus Schoutsen
087c3b9c0e Bumped version to 20190514.0 2019-05-14 06:55:20 +02:00
Paulus Schoutsen
b12d1b13ca Update translations 2019-05-14 06:55:14 +02:00
Paulus Schoutsen
d0410e0884 Fix Safari 10 (#3196) 2019-05-14 06:52:20 +02:00
Paulus Schoutsen
a1bf06ceb2 Fix CSS 2019-05-14 06:15:50 +02:00
Thomas Lovén
d99744e054 ha-card migration - step #2 (#3187)
* Convert profile settings to ha-card

* Convert dev- panels to ha-card

* Convert empty-state-card to ha-card

* Convert zha config to ha-card - UNTESTED

* Convert zwave config to ha-card - UNTESTED

* Convert various panels to ha-card - UNTESTED

* Convert gallery to ha-card
2019-05-13 01:24:43 -07:00
Paulus Schoutsen
e02d11a51f Pimp script editor (#3192)
* Pimp script editor

* Use property for define

* Show toasts
2019-05-12 21:28:25 -07:00
Paulus Schoutsen
1b50100b6c Pimp automation picker (#3193) 2019-05-12 20:30:49 -07:00
Timmo
309fecc9f3 Support icon_height for entity button (#2800)
* 🎨 Add support for icon_height

* 🎨 Add icon_height to config ui

* 🔨 Don't expose css for icon_height

* 🔨 Post rebase changes and allow advanced css height
2019-05-12 20:25:48 -07:00
Timmo
46f3add520 🔨 Fix hassio new add-on repository height (#3191) 2019-05-12 11:14:18 +02:00
Timmo
a89f0bd1cd Add Search to Hassio add-on store (#3108)
*  Add search to hassio add-ons

* 👕 Fix linter error

* 👕 Lint fixes

* 🔥 Remove search from dashboard for this PR

* 🔥 Remove search from dasboard in this PR

* 🔨 Suggested changes

* 🔨 Change to fireEvent

* 🔨 Convert definition

* 🔥 Fix imports

* 🔥 Revert styling test

* 🔨 Fix search

* 🔨 CSS fix

* 🔨 Add smaller message to show no results found in repo

* 🔨 Fixes

* 🔨 CSS fixes

* 🔨 Add types

* 🎨 Max width

* 🔨 Fix margin jump

* 🔨 Add working memoizeOne

* 👕 Fix linting / error on build
2019-05-12 11:13:16 +02:00
Paulus Schoutsen
8408b8d41f Merge pull request #3190 from home-assistant/dev
20190510.0
2019-05-10 14:21:52 -07:00
Paulus Schoutsen
13761a20c5 Bumped version to 20190510.0 2019-05-10 14:13:20 -07:00
Paulus Schoutsen
03d17a9761 Update translations 2019-05-10 14:13:15 -07:00
Paulus Schoutsen
5501cccc67 Fix custom panel paths (#3188) 2019-05-10 14:12:08 -07:00
Paulus Schoutsen
9340d9068e Hash demo files in index.html (#3185) 2019-05-09 20:19:15 -07:00
Paulus Schoutsen
bbdaa4b7c1 Merge pull request #3184 from home-assistant/dev
20190509.0
2019-05-09 15:48:22 -07:00
Paulus Schoutsen
c87c782b2c Merge remote-tracking branch 'origin/master' into dev 2019-05-09 15:40:33 -07:00
Paulus Schoutsen
f0b1cd9032 Bumped version to 20190509.0 2019-05-09 15:38:37 -07:00
Paulus Schoutsen
2ed532e055 Update translations 2019-05-09 15:38:33 -07:00
Paulus Schoutsen
f70dafa192 Autofocus the first element in the auth page (#3177) 2019-05-09 15:36:43 -07:00
Paulus Schoutsen
af6ade8eb6 Fix editing wait actions in script editor (#3181) 2019-05-09 15:36:29 -07:00
Paulus Schoutsen
d77ae840d8 Fix the ES5 adapter for custom panel (#3182)
* Fix the ES5 adapter for custom panel

* Update custom-panel.ts

* Update custom-panel.ts

* Update custom-panel.ts
2019-05-09 15:36:16 -07:00
Paulus Schoutsen
968eae7727 Add external step (#3183)
* Add external step

* Automatically open external step
2019-05-09 15:36:05 -07:00
Paulus Schoutsen
97d8a68455 Unused entities to check picture-element cards (#3180)
* Check picture element cards for unused entities

* Remove unused entities from Arsaboo config

* Remove unused entities Isa

* Remove unused entities kernehed

* Remove unused entities jimpower
2019-05-09 09:53:29 -07:00
Paulus Schoutsen
7827cec212 Fix demo camera images (#3179) 2019-05-09 09:43:52 -07:00
Paulus Schoutsen
746ad588ef 20190508.0 (#3176)
* Prevent default form action (#3172)

* Use areas when generating Lovelace config (#3175)

* Use areas when generating Lovelace config

* Add missing return type

* Convert auth-flow to TypeScript/Lit (#3174)

* Change ha-pick-auth-provider to typescript

* Convert auth-flow to TypeScript/Lit

* Make sure we block emulated mouse events when touch is used (#3173)

* Trim text

* Update translations

* Bumped version to 20190508.0
2019-05-08 20:12:05 -07:00
Paulus Schoutsen
95e918b6ac Bumped version to 20190508.0 2019-05-08 19:58:30 -07:00
Paulus Schoutsen
1e82cc22e4 Update translations 2019-05-08 19:58:26 -07:00
Paulus Schoutsen
fb2e1e5ebb Trim text 2019-05-08 19:57:03 -07:00
Joakim Plate
fe2ae965b3 Make sure we block emulated mouse events when touch is used (#3173) 2019-05-08 19:53:34 -07:00
Jason Hu
8924a5f043 Convert auth-flow to TypeScript/Lit (#3174)
* Change ha-pick-auth-provider to typescript

* Convert auth-flow to TypeScript/Lit
2019-05-08 19:52:55 -07:00
Paulus Schoutsen
32e68c1a4b Use areas when generating Lovelace config (#3175)
* Use areas when generating Lovelace config

* Add missing return type
2019-05-08 16:34:53 -07:00
Jason Hu
89a35a0062 Prevent default form action (#3172) 2019-05-08 07:48:33 -07:00
Paulus Schoutsen
484b1c8444 Merge pull request #3171 from home-assistant/dev
20190507.0
2019-05-07 22:48:16 -07:00
Paulus Schoutsen
cd5e274ffa Fix demo 2019-05-07 22:39:00 -07:00
Paulus Schoutsen
f466a53ed4 Bumped version to 20190507.0 2019-05-07 22:31:15 -07:00
Paulus Schoutsen
1d40d94774 Update translations 2019-05-07 22:31:10 -07:00
Paulus Schoutsen
82e8ca2754 Alow setting up integrations during onboarding (#3163)
* Allow setting up integrations during onboarding

* Fix compress static

* Don't compress static files in CI

* Remove unused file

* Fix static compress disabled in CI build

* Work with new integration step

* Import fix

* Lint

* Upgrade HAWS to 4.1.1
2019-05-07 22:27:10 -07:00
Paulus Schoutsen
8c904fb012 Sort config flow picker (#3170) 2019-05-07 21:07:59 -07:00
Paulus Schoutsen
fa13b95498 Use collections for registries (#3168)
* Use collections

* Fix bugs

* Lint
2019-05-07 20:57:23 -07:00
Paulus Schoutsen
289611363e Use named export for events mixin (#3166) 2019-05-07 17:47:44 -07:00
Paulus Schoutsen
cb7048db23 Type debounce (#3167) 2019-05-07 17:47:31 -07:00
Paulus Schoutsen
b9f86f735b Use named export for compare (#3169) 2019-05-07 17:47:19 -07:00
Thomas Lovén
0e044acaa9 ha-card migration Step #1 Configuration (#3161)
* Improvements to ha-card. Better paper-card compatibility

* Remove named slots

* Tweak distance between header and contents

* Further css tweaking

* Convert config dashboard to ha-card

* Convert cloud configuration to ha-card.

* Convert integrations configuration to ha-card.

* Convert user configuration to ha-card.

* Convert core configuration to ha-card

* Convert person, entity and area config to ha-card

* Convert automation and script editor to ha-card

* Convert customization editor to ha-card
2019-05-07 09:26:56 -07:00
Thomas Lovén
1223766523 ha-card migration. Step #0: improvements to ha-card. (#3144)
* Improvements to ha-card. Better paper-card compatibility

* Remove named slots

* Tweak distance between header and contents

* Further css tweaking
2019-05-07 09:24:39 -07:00
Paulus Schoutsen
db65af9c22 Split up onboarding code (#3158) 2019-05-04 11:59:43 -07:00
Paulus Schoutsen
fcdb1b48a2 Isolate hass state from base el (#3157) 2019-05-03 20:26:01 -07:00
Thomas Lovén
8729410dce Remove spinners in firefox (#3150) 2019-05-03 09:38:28 -07:00
Sean Mooney
adb92e1708 add target="_blank" to match all other links on page (#3154)
* add target="_blank" to match all other links on page

* adds target="_blank" to ensure link opens in new tab

* adds target="_blank" to ensure link opens in new tab

* attempting to fix travis error

* resolve prettier errors hopefully

* Update ha-automation-picker.js

* Update ha-automation-picker.js

* Lint

* Lint
2019-05-03 09:37:06 -07:00
Joakim Plate
81088e0d07 Simplify list selection 2 (#3156)
* light: use attr-for-selected for effect

* vacuum: use attr-for-selection for speed
2019-05-03 08:54:38 -07:00
Paulus Schoutsen
34129cc7cb Migrate demo to gulp (#3152)
* Migrate demo to gulp

* Tweak demo migration to gulp

* Feature detect demo

* Gen icons
2019-05-02 22:41:58 -07:00
Paulus Schoutsen
530be9155b Dont cache requests to auth APIs (#3151) 2019-05-02 19:49:39 -07:00
Paulus Schoutsen
aa33b00a1f Merge pull request #3149 from home-assistant/dev
20190502.0
2019-05-02 11:51:22 -07:00
Paulus Schoutsen
57b917f297 Bumped version to 20190502.0 2019-05-02 11:45:03 -07:00
Paulus Schoutsen
aad7dc5d7d Update translations 2019-05-02 11:44:58 -07:00
Paulus Schoutsen
6c41c7b1ab Gulpify build pipeline (#3145)
* Gulpify build pipeline

* Update build frontend script

* Fixes

* Limit service worker to latest build

* Use shorthand

* Fix hassio build
2019-05-02 11:35:46 -07:00
Yosi Levy
8b98f375c2 Added tooltip to state badge element (#3137) 2019-05-02 11:31:14 -07:00
Joakim Plate
8a86dd8426 Simplify list selection (#3148)
* water_heater: Use attr-for-selected for operation mode

* climate: Use attr-for-selected for operation, fan, swing mode

* fan: Use attr-for-selected for speed

* fan: skip extra property

* climate: drop extra property

* water_heater: avoid extra property

* media_player: drop extra variable for source and sound_mode

* water_heater: missed change
2019-05-02 07:09:06 -07:00
Robbie Trencheny
5b12ca94e9 Add missing key for app configuration in sidebar (#3146)
* Add missing key for app configuration in sidebar

* avoid lokalise round trip
2019-05-01 22:49:33 -07:00
Paulus Schoutsen
652cd10483 Use Node 12 (#3141)
* Use Node 12

* Remove tests that don't work in Node
2019-04-30 12:16:41 -07:00
Paulus Schoutsen
ca0ded8587 Fix webpack chunkname 2019-04-30 11:38:17 -07:00
Joakim Plate
f943393ade Switch source selection to same logic as SoundMode with using (#3136)
actual value as selected item instead of index.

This avoids the bug with selected:
https://github.com/PolymerElements/paper-dropdown-menu/issues/197
https://github.com/PolymerElements/paper-dropdown-menu/issues/114

Fixes: #3022

Side note: it actually mainly hides the issue. If we should allow a key,
value setup with source being a key and the display value being a
localized value it likely would return.
2019-04-30 10:21:43 -07:00
Thomas Lovén
d8f21d99af Use named slots to have advanced ha-card headers (#3127)
* Use named slots to have advanced ha-card headers

* Fix header text color
2019-04-29 11:31:27 -07:00
Paulus Schoutsen
73ef03e33f Use signed path for camera snapshot (#3138) 2019-04-29 11:27:40 -07:00
Paulus Schoutsen
c34dde815c Merge pull request #3134 from home-assistant/dev
20190427.0
2019-04-27 22:16:54 -07:00
Paulus Schoutsen
1e85880d7b Bumped version to 20190427.0 2019-04-27 22:08:20 -07:00
Paulus Schoutsen
57abd4ae07 Update translations 2019-04-27 22:08:15 -07:00
Paulus Schoutsen
2624c1544b Disable the show in sidebar toggle when HA < 0.92 (#3124)
* Hide the show in sidebar toggle on 0.92

* Update hassio/src/addon-view/hassio-addon-info.js

Co-Authored-By: balloob <paulus@home-assistant.io>
2019-04-27 22:02:47 -07:00
Paulus Schoutsen
1e72ffc0c2 Fix input select more info (#3132)
* Fix input select more info

* Lint
2019-04-27 22:02:26 -07:00
Yosi Levy
8ca70ace4c Tooltip picture elements (#3111)
* Added noTitle option to supress tooltip

* Additional strings

* Updated name

* refactored name

* Refactored strings

* Refactored to allow null in title
2019-04-27 13:03:31 -07:00
Thomas Lovén
d66cf3f787 Allow changing state-icon element icon (#3122) 2019-04-27 10:37:57 -07:00
Paulus Schoutsen
44df0f698c Conditionally attach external message bus (#3131) 2019-04-27 10:37:15 -07:00
Thomas Lovén
981dd5df63 Basic input-datetime entity row (#3121)
* Basic input-datetime entity row

* Address review comments

* Fix imports
2019-04-25 21:47:46 -07:00
Paulus Schoutsen
cd6250c495 Don't proxy external accessible covers (#3120) 2019-04-24 21:05:38 -07:00
Paulus Schoutsen
2f36304f06 Move picking new integration into dialog (#3110) 2019-04-24 12:51:41 -07:00
Paulus Schoutsen
30471b7cfb Merge pull request #3119 from home-assistant/dev
20190424.0
2019-04-24 11:13:42 -07:00
Paulus Schoutsen
ff2f573dd0 Bumped version to 20190424.0 2019-04-24 11:07:34 -07:00
Paulus Schoutsen
38ddbf45c2 Update translations 2019-04-24 11:07:29 -07:00
Robbie Trencheny
d79bf5e07e Forward haptic events to external apps (#3116)
* Forward haptic events to external apps

* Fix types
2019-04-24 11:03:59 -07:00
Robbie Trencheny
d05b1ef9cc Add connection events to bus (#3117)
* Add connection events

* Fix types

* Fix order
2019-04-24 10:56:53 -07:00
Pascal Vizeli
c260591d4d Hass.io update labels (#3114)
* Hass.io update labels

* Update hassio-addon-info.js
2019-04-24 08:58:35 +02:00
Paulus Schoutsen
87a7e63e31 Fix tests 2019-04-23 20:37:56 -07:00
Paulus Schoutsen
a5dd3755e1 Add external app message bus (#3112)
* Add support for a app configuration button in the sidebar

* Add event to types

* Fire connection events so that app knows when to hide its fallback settings button

* Add external message bus

* Fixes

* Update external_config.ts

* Remove icon from gen-icons

* Add fireMessagE

* msgId -> id

* Rename to externalBus

* Log messages in dev

* Add should update to ha-sidebar


Co-authored-by: Robbie Trencheny <me@robbiet.us>
2019-04-23 20:23:56 -07:00
Pascal Vizeli
ad40d9927b Hass.io: Support automated panel integration (#3113)
* Hass.io: Support automated panel integration

* Update hassio-addon-info.js

* fix lint
2019-04-23 12:24:06 +02:00
Robbie Trencheny
f4cfbc6678 Initial haptics support (#3099)
* Initial haptics support

* Move window stuff into types.ts

* Fire haptic events instead of expecting a messageHandler

* Style fixes, linting fixes

* Only allow whitelisted haptics

* Make requested changes
2019-04-22 09:24:30 -07:00
Jc2k
b3c1bead39 Allow 'Discovered' flows to have title placeholders (#3106)
* Allow 'Discovered' flows to have title placeholders

* Feedback from review
2019-04-21 10:58:51 -07:00
Paulus Schoutsen
d220e56239 Merge pull request #3104 from home-assistant/dev
20190419.0
2019-04-19 17:01:05 -07:00
Paulus Schoutsen
f967b4940a Bumped version to 20190419.0 2019-04-19 16:57:51 -07:00
Paulus Schoutsen
f44d5dca1c Update translations 2019-04-19 16:57:50 -07:00
Paulus Schoutsen
a9ed4e7943 Regenerate LL when attached (#3102)
* Regenerate when attached

* Fix lint
2019-04-19 14:53:26 -07:00
Paulus Schoutsen
a404acbf44 Add support for secure devices pin (#3101) 2019-04-19 14:53:16 -07:00
Robbie Trencheny
eaa2ce1462 Ask users if they want to install iOS app (#3100) 2019-04-18 20:54:52 -07:00
Joakim Plate
bdd8699709 Add device_class for switches (#3096) 2019-04-18 15:12:17 -07:00
Paulus Schoutsen
9f0b20634a Merge pull request #3098 from home-assistant/dev
20190417.0
2019-04-17 09:39:05 -07:00
Paulus Schoutsen
a70d9195db Bumped version to 20190417.0 2019-04-17 09:31:40 -07:00
Paulus Schoutsen
d86253d582 Update translations 2019-04-17 09:31:23 -07:00
Paulus Schoutsen
d5a313445f Update user-agents to 2.0.0 (#3087) 2019-04-15 19:56:31 -07:00
Joakim Plate
f979febb76 Match list of classes for cover to backend (#3090)
1d2e9b6915/homeassistant/components/cover/__init__.py (L52)

Fixes #3089
2019-04-15 19:56:23 -07:00
Jason Hunter
a1a2a78531 Add Stream Element (#3086)
* initial commit for stream element

* lit elements are apparently not self closing

* add disconnectedCallback to teardown on unload

* refactor stream element to UpdatingElement and bundle MJPEG handling with it

* attach video element for HLS native

* update hui-image to optionally show a live camera view (video or mjpeg)

* fix playing inline video on iOS

* implement review feedback

* Fix update bugs

* Tweaks

* Fix stateObj changed
2019-04-15 19:55:13 -07:00
Paulus Schoutsen
6ed2d288e6 addon -> add-on (#3094) 2019-04-15 10:53:53 -07:00
ktnrg45
5c8e5d3539 Added padding definition for type game (#3059) 2019-04-13 20:35:24 -07:00
Paulus Schoutsen
bbae3291e1 Support ingress custom panels (#3085)
* Support ingress custom panels

* Fix types

* Add disabled placeholder to network card
2019-04-11 11:42:52 -07:00
Paulus Schoutsen
5dbd5c7395 Fix hassio unavailable dashboard & update card 2019-04-10 15:38:33 -07:00
Paulus Schoutsen
038f7b43d5 Merge pull request #3080 from home-assistant/dev
20190410.0
2019-04-10 15:00:36 -07:00
Paulus Schoutsen
671e564037 Fix hassio dashboard available maybe 2019-04-10 15:00:19 -07:00
Paulus Schoutsen
8298d810a8 Bumped version to 20190410.0 2019-04-10 14:46:23 -07:00
Paulus Schoutsen
7428479f6b Update translations 2019-04-10 14:46:16 -07:00
Paulus Schoutsen
6b85910cdb Fix classname 2019-04-10 14:45:39 -07:00
Paulus Schoutsen
4d7bb0df7d Fix hassio loading 2019-04-10 12:46:42 -07:00
Paulus Schoutsen
26a39b1bb8 Hassio fixes 2019-04-10 12:32:21 -07:00
Ian Richardson
e23f046c4d 🕶 convert hui-persisten-notification-item to TypeScript/LitElement (#3032) 2019-04-09 22:25:34 -05:00
Paulus Schoutsen
fe73213643 Fix hassio 2019-04-09 16:31:07 -07:00
Paulus Schoutsen
cbe5355d38 Small fixes 2019-04-09 14:26:30 -07:00
Paulus Schoutsen
81b232f01e Stop experimenting in dev for hassio 2019-04-09 14:26:18 -07:00
Paulus Schoutsen
3e6be45f1f Revert webpack upgrade for hassio fix? 2019-04-09 13:58:48 -07:00
Paulus Schoutsen
d26ed6fdb6 fix sorting and use user given name if available (#3072) 2019-04-09 13:53:44 -07:00
Pascal Vizeli
eda168247c Add description support to UI (#3079) 2019-04-09 22:53:10 +02:00
Paulus Schoutsen
4d2390daf4 Hass.io snapshots -> Lit (#3078)
* Hass.io snapshots

* Fix rootnav
2019-04-09 13:05:56 -07:00
Paulus Schoutsen
5b861bb4c6 Fix hassio in prod? 2019-04-09 00:17:34 -07:00
Paulus Schoutsen
be6d89bb7a Revert a tsconfig change (#3075) 2019-04-08 17:08:59 -07:00
Paulus Schoutsen
1c17210948 Clean up even more (#3074) 2019-04-09 00:15:46 +02:00
David Mulcahey
5257715145 fix sorting and use user given name if available 2019-04-08 07:31:33 -04:00
Paulus Schoutsen
8df9ac9dfa Fix paper-icon-button fail (#3069) 2019-04-07 23:50:58 -07:00
Paulus Schoutsen
559164e159 fix showing edit entity icon in more info dialog (#3066)
* fix showing cog

* Remove unused value

* Lint
2019-04-07 12:17:06 -07:00
Paulus Schoutsen
70072786a1 Clean up hassio tabs page (#3068)
* Clean up hassio tabs page

* Make load optional

* Fix bug
2019-04-07 11:58:51 -07:00
Paulus Schoutsen
cda29fcd07 Clean up hassio panel (#3067)
* Clean up hassio panel

* Extract dialog manager code

* Convert markdown dialog to show-dialog

* Extract snapshot dialog
2019-04-07 17:45:56 +02:00
Paulus Schoutsen
31e351c75c Show features translation 2019-04-06 22:36:30 -07:00
Paulus Schoutsen
cadcd845cc Add guard for addon 2019-04-06 12:32:45 -07:00
Paulus Schoutsen
b07f95f956 Add hassio ingress support (#3062)
* Add hassio ingress support

* Remove logging

* Better integrate

* Add badge

* FIx type
2019-04-06 09:28:08 +02:00
Markus Jankowski
7f99f1d9be add device_class_signal_strength (#3058) 2019-04-04 21:40:27 -04:00
Yosi Levy
8c7cdda3d3 Converted paper-dialog to ha-paper-dialog (#3055)
* Converted paper-dialog to ha-paper-dialog

* Fixed paths

* Fixed double import

* Fixed orphan tags

* Moved to /components and renamed

* Fixed hassio

* Fix travis issue
2019-04-04 15:11:43 -07:00
Paulus Schoutsen
8c222bb467 Remove unnecessary resolutions (#3056)
* Remove vaadin-lumo-styles resolution

* Remove iron-overlay-behavior resolution

* Remove polymer and shadycss resolutions

* Add lumo-styles back to resolution
2019-04-03 23:07:35 -07:00
Markus Jankowski
4dfdebb00a Add device_class_power to sensor (#3057) 2019-04-03 21:55:14 -07:00
Paulus Schoutsen
3947adbab4 Upgrade workbox to v4 (#3053)
* Upgrade workbox to v4

* Update dmeo config
2019-04-02 15:38:14 -07:00
Paulus Schoutsen
81eab0bf1b Fix arsaboo demo unit (#3050)
* Fix arsaboo demo unit

* Migrate to C
2019-04-02 12:25:35 -07:00
Paulus Schoutsen
0c406335f5 Upgrade deps (#3038)
* Upgrade deps

* Revert workbox back to 3

* Fix var name
2019-04-02 12:14:10 -07:00
yosilevy
109c40b2d3 RTL fix for drop downs (#3047)
* RTL fix for drop downs

* Added new file
2019-04-02 11:53:00 -07:00
David F. Mulcahey
a362b08113 buffer time to prevent edge misses (#3049) 2019-04-02 11:52:01 -07:00
Paulus Schoutsen
438d155c45 Fix imports (#3040) 2019-04-02 11:50:57 -07:00
Paulus Schoutsen
75f5325048 Simplify hass subpage (#3039) 2019-04-02 11:50:50 -07:00
David F. Mulcahey
8f5f14fada Add targeted joins to ZHA config panel (#3048)
* initial targeted add

* mains powered devices only

* fix prop reference

* import

* fix targeted join
2019-04-02 11:48:06 -07:00
Paulus Schoutsen
8e290be9e7 Merge pull request #3044 from home-assistant/dev
20190331.0
2019-03-31 19:52:11 -07:00
Paulus Schoutsen
9f97b583a8 Bumped version to 20190331.0 2019-03-31 19:51:21 -07:00
Paulus Schoutsen
8993e39c38 Update translations 2019-03-31 19:51:17 -07:00
Paulus Schoutsen
dc61a62149 Edit card fixes (#3043) 2019-03-31 19:38:11 -07:00
Paulus Schoutsen
22fdac4189 Reset camera prefs (#3042) 2019-03-31 19:38:01 -07:00
Paulus Schoutsen
c52f437ee6 lint 2019-03-29 16:46:58 -07:00
Paulus Schoutsen
549db23ff5 lint 2019-03-29 16:46:48 -07:00
Paulus Schoutsen
6775a094c9 Merge pull request #3037 from home-assistant/dev
20190329.0
2019-03-29 16:45:26 -07:00
Paulus Schoutsen
74a255add1 Bumped version to 20190329.0 2019-03-29 16:44:32 -07:00
Paulus Schoutsen
a77c951d55 Update translations 2019-03-29 16:44:27 -07:00
Paulus Schoutsen
e3896c359a Register service worker during login (#3036) 2019-03-29 16:43:42 -07:00
Paulus Schoutsen
56e3514e40 Allow changing camera prefs (#3035)
* Check camera supported_features before streaming

* Allow mutating camera prefs

* Move when we fetch prefs
2019-03-29 16:43:32 -07:00
Paulus Schoutsen
f4319d9b13 Fix custom panel/hass.io navigation (#3034)
* Hass.io: use correct function for firing evenet

* Fix navigation from custom panel
2019-03-29 16:40:28 -07:00
David F. Mulcahey
c134464f6a fix area and user given name display (#3033) 2019-03-29 14:02:13 -07:00
Ian Richardson
7b821aa363 🕶 convert hui-notification-item-template to TypeScript/LitElement (#3029)
* 🕶 convert hui-notification-item-template to TypeScript/LitElement

* address review comments
2019-03-27 22:05:07 -07:00
Paulus Schoutsen
4e6d00cf5c Merge pull request #3030 from home-assistant/dev
20190327.0
2019-03-27 21:31:53 -07:00
Paulus Schoutsen
22e5792a8f Upgrade mwc (#3031) 2019-03-27 21:24:46 -07:00
Paulus Schoutsen
e3e0d4618e Bumped version to 20190327.0 2019-03-27 21:21:20 -07:00
Paulus Schoutsen
aa1ac8f339 Update translations 2019-03-27 21:21:15 -07:00
Paulus Schoutsen
40863db138 Typo 2019-03-27 21:19:32 -07:00
Ian Richardson
eac37af18c 🕶 convert hui-notification-item to TypeScript/LitElement (#3028) 2019-03-27 21:14:27 -07:00
Ian Richardson
7f8f99a414 🕶 convert hui-configurator-notification-item to TypeScript/LitElement (#3027) 2019-03-27 21:12:56 -07:00
Ian Richardson
a743a2c46b 🛠 Fix button icon/name (#3026) 2019-03-27 21:12:20 -07:00
Jason Hu
adc63e1e5a Fix login form missing abort reason (#3024) 2019-03-27 21:11:07 -07:00
Ian Richardson
1d24b83e5c Align configs (#3019)
* Align configurations

* cleanup

* fix imports
2019-03-27 21:10:55 -07:00
yosilevy
b3f9432ae1 Tab fix in yaml-editor edit card (#3008)
* Fixed tabs not working in yaml editor in edit card

* Improved docs

* Fixed comments

* Added dependencies

* Added typescript mapping

* Fixed data type issue + removed depednency since it breaks the UI. Non final.

* Added iron-overlay-behavior package

* Added dependency

* Update iron-overlay-behavior

* Lint
2019-03-27 21:10:07 -07:00
Ian Richardson
c95a44c570 Consider "on" as valid media state (#3020) 2019-03-27 21:07:04 -07:00
Jason Hu
5080f4c2db Allow auth provider bypass login form (#3025) 2019-03-27 20:54:10 -07:00
Thomas Lovén
44eaa3abad A bit of cleanup in the card editor (#2984)
* edit-card shouldn't need to know about the path

* fix

* Store config as object at all times, convert when necessary

* Hidden is not a property of mwc-button. No need to hide anyway...
2019-03-26 15:31:43 -07:00
Ian Richardson
9a4215b5d5 Upgrade mdi to 3.5.92 (#3007) 2019-03-26 14:59:37 -07:00
Ian Richardson
004892e11a 🔧 Remove unnecessary re-renders (#3014)
* 🔧 Remove unnecessary re-renders

* address review comments

* address review comments
2019-03-26 00:18:16 -05:00
David F. Mulcahey
669358bf1a ZHA add devices page (#2969)
* zha add device page

add device join dialog stub

update dialog stub

fix spinner

add messages and devices to dialog

dialog updates

update dialog

update dialog

add debug info

fix reference

add header

update dialog

test zha gateway message subscription

add device join dialog stub

add messages and devices to dialog

dialog updates

update dialog

add debug info

update dialog

start transitioning to a page instead of a dialog

fix import

subpage

update router

remove old dialog handle

remove dialog parts

make add button call navigate

change extract page

add devices page

cleanup

* update device join page

* auto scroll log

* update css and add device page layout

* fix padding

* fix missing imports

* fix imports

* add -> permit

* left justify device cards to prevent jumping

* conditionally display entity ids

* cleanup

* fix vertical alignment

* review comments

* fix manufacturer overrides
2019-03-25 22:26:32 -05:00
Petro31
435b7d9cee Fix for vertical button spacing on alarm card (#3017)
Vertical spacing on button card doesn't have padding when 3 or more buttons are present and the card size is small.  Small PR.
2019-03-25 22:15:38 -05:00
Ian Richardson
9a2207b5cb badge warning (#3009) 2019-03-25 07:40:04 -07:00
Ian Richardson
324f0bb8a2 warning-element (#3006)
* warning-element

* add warning-element to picture-glance

* add glance-card
2019-03-23 23:10:55 -07:00
yosilevy
3b8f8f8189 Climate RTL fixes (#3002) 2019-03-23 20:30:26 -07:00
Paulus Schoutsen
702c17d658 Convert custom panel to typescript (#2991)
* Convert custom panel to typescript

* Address comments
2019-03-23 11:41:36 -07:00
Ian Richardson
e2a9cf0d3c Handle unavailable entity in conditional-card (#2996)
* Handle unavailable entity in conditional-card

* cleanup
2019-03-23 11:09:30 -07:00
Jason Hu
8aa501b7bd Add Icelandic :flag_is: support (#3003) 2019-03-23 11:08:00 -07:00
yosilevy
45189c9163 Media player RTL fixes (#3001) 2019-03-23 11:06:35 -07:00
heckler
86940f4d42 Edited the delete message on removeEntry to match the resource type (#3000) 2019-03-22 18:06:17 -07:00
Ian Richardson
812c1362a6 Fix typo (#2999)
Fixes https://github.com/home-assistant/home-assistant-polymer/issues/2997
2019-03-22 14:42:30 -05:00
Ian Richardson
6bf9ea5699 entity-button show/hide icon/name (#2936) 2019-03-22 11:55:52 -07:00
Paulus Schoutsen
20ee3452dc Fix sidebar when user is slow to load (#2993) 2019-03-22 10:38:40 -07:00
Paulus Schoutsen
ef18f9eac9 Upgrade lit element to 2.1.0 (#2990)
* UPgrade lit element

* Fix yarn resolving

* Upgrade mwc
2019-03-21 14:56:57 -07:00
Paulus Schoutsen
47faf2768c Merge pull request #2989 from home-assistant/dev
20190321.0
2019-03-21 12:24:23 -07:00
Paulus Schoutsen
a2bed3dd90 Bumped version to 20190321.0 2019-03-21 12:23:15 -07:00
Paulus Schoutsen
4fab0b9717 Update translations 2019-03-21 12:23:11 -07:00
Paulus Schoutsen
06b70e2653 Fix route changing on every hass change (#2988) 2019-03-21 12:22:32 -07:00
Paulus Schoutsen
48aa9a2ad7 Fix blank tabs in Hass.io 2019-03-20 13:46:53 -07:00
Paulus Schoutsen
93d971f72b Merge pull request #2981 from home-assistant/dev
20190320.0
2019-03-20 07:43:56 -07:00
Paulus Schoutsen
7e69df44d7 Bumped version to 20190320.0 2019-03-20 07:42:46 -07:00
Paulus Schoutsen
c743a48cf9 Update translations 2019-03-20 07:42:45 -07:00
Paulus Schoutsen
b82a1c75c4 Fix custom panel doctype (#2977) 2019-03-20 07:36:28 -07:00
Bram Kragten
be9402bd05 Remove console.log (#2979)
Remove console.log probably forgotten?
2019-03-20 07:36:06 -07:00
Paulus Schoutsen
ebae469e7d Warn when remote UI cannot be turned on (#2978)
* Warn when remote UI cannot be turned on

* Lint
2019-03-20 07:35:44 -07:00
Paulus Schoutsen
d0d293fe21 Merge pull request #2976 from home-assistant/dev
20190319.1
2019-03-19 14:03:45 -07:00
Paulus Schoutsen
bd6d082555 Exclude google fonts (#2975) 2019-03-19 14:03:16 -07:00
Paulus Schoutsen
39190dda20 Bumped version to 20190319.1 2019-03-19 14:02:22 -07:00
Paulus Schoutsen
89a8e3da36 Fix property on ha-panel-lovelace 2019-03-19 14:02:15 -07:00
Paulus Schoutsen
49f90671fb Merge pull request #2973 from home-assistant/dev
20190319.0
2019-03-19 11:28:43 -07:00
Paulus Schoutsen
c4ece5e451 Bumped version to 20190319.0 2019-03-19 11:27:18 -07:00
Paulus Schoutsen
799bd973ca Pass narrow to hui-root 2019-03-19 11:26:45 -07:00
Paulus Schoutsen
03dffa9905 Fix hassio panel nav on <0.90 HA 2019-03-19 10:10:26 -07:00
Paulus Schoutsen
1d1c981601 Upgrade HAWS" (#2967) 2019-03-18 19:42:38 -07:00
Paulus Schoutsen
40025d44c2 Add if replace was used when sending navigation events (#2970) 2019-03-18 19:42:27 -07:00
Paulus Schoutsen
42117fcba0 Merge pull request #2971 from home-assistant/dev
20190318.0
2019-03-18 16:54:04 -07:00
Paulus Schoutsen
dc16abd637 Bumped version to 20190318.0 2019-03-18 16:53:14 -07:00
Paulus Schoutsen
8c71746952 Update translations 2019-03-18 16:53:06 -07:00
Paulus Schoutsen
6e504020bf Fix panels race 2019-03-18 10:50:45 -07:00
Paulus Schoutsen
7caf37275d Fix hassio repo editing (#2965) 2019-03-18 09:14:34 -07:00
Paulus Schoutsen
c3f094eb9e Fix hassio nav 2019-03-18 09:05:22 -07:00
Paulus Schoutsen
feb3be1d17 Fix hassio build 2019-03-18 07:41:40 -07:00
Paulus Schoutsen
2fe0398f37 Make Hass.io menu toggle button work in pre and post 90 release (#2959) 2019-03-18 08:53:25 +01:00
David F. Mulcahey
42c7879c4d change integrations link (#2955) 2019-03-17 19:28:41 -07:00
Paulus Schoutsen
2586590bd9 Merge pull request #2953 from home-assistant/dev
20190316.0
2019-03-16 23:22:26 -07:00
Paulus Schoutsen
59ee160f96 Reload data when opening integrations.
Fixes #2952
2019-03-16 23:20:45 -07:00
Paulus Schoutsen
d4bc4bf7bc Bumped version to 20190316.0 2019-03-16 23:16:47 -07:00
Paulus Schoutsen
e2a182acee Update translations 2019-03-16 23:16:42 -07:00
Paulus Schoutsen
88131ade23 Add area ID to area ID modal 2019-03-16 23:15:20 -07:00
Paulus Schoutsen
fb16156f8d Fix routetail + config subrouting (#2951)
* Fix routetail + config subrouting

* Do not update panel when loading a new one

* Fix init skeleton not removed during loading
2019-03-16 23:15:00 -07:00
Jason Hunter
6ba77b4fa5 Fix HLS on Android 9.0 (#2950)
* make sure can play type is "probably"

* check hls.js first and then native
2019-03-16 21:22:42 -07:00
599 changed files with 31541 additions and 54808 deletions

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ node_modules/*
npm-debug.log
.DS_Store
hass_frontend/*
hass_frontend_es5/*
.reify-cache
demo/hademo-icons.html

2
.nvmrc
View File

@@ -1 +1 @@
8.11.1
12.1

View File

@@ -0,0 +1,57 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
tags:
include:
- "*"
pr: none
variables:
- name: versionBuilder
value: "5.2"
- group: github
- group: twine
stages:
- stage: "Validate"
jobs:
- job: "VersionValidate"
pool:
vmImage: "ubuntu-latest"
steps:
- task: UsePythonVersion@0
displayName: "Use Python 3.7"
inputs:
versionSpec: "3.7"
- script: |
setup_version="$(python setup.py -V)"
branch_version="$(Build.SourceBranchName)"
if [ "${setup_version}" != "${branch_version}" ]; then
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
exit 1
fi
displayName: "Check version of branch/tag"
- stage: "Build"
jobs:
- job: "ReleasePython"
pool:
vmImage: "ubuntu-latest"
steps:
- task: UsePythonVersion@0
displayName: "Use Python 3.7"
inputs:
versionSpec: "3.7"
- task: NodeTool@0
displayName: "Use Node 12.1"
inputs:
versionSpec: "12.1"
- script: pip install twine wheel
displayName: "Install tools"
- script: |
export TWINE_USERNAME="$(twineUser)"
export TWINE_PASSWORD="$(twinePassword)"
script/release
displayName: "Build and release package"

51
build-scripts/gulp/app.js Normal file
View 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"
)
)
);

View File

@@ -0,0 +1,17 @@
const del = require("del");
const gulp = require("gulp");
const config = require("../paths");
require("./translations");
gulp.task(
"clean",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.root, config.build_dir]);
})
);
gulp.task(
"clean-demo",
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
return del([config.demo_root, config.build_dir]);
})
);

View 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"
)
);

View 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",
}).replace(/#THEMEC/g, "{{ theme_color }}");
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();
});

View File

@@ -0,0 +1,116 @@
// Gulp task to gather all static files.
const gulp = require("gulp");
const path = require("path");
const cpx = require("cpx");
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
cpx.copySync(
npmPath("roboto-fontface/fonts/roboto/*.woff2"),
staticPath("fonts/roboto")
);
}
function copyMapPanel(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
);
}
function compressStatic(staticDir) {
const staticPath = genStaticPath(staticDir);
const polyfills = gulp
.src(staticPath("polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("polyfills")));
const translations = gulp
.src(staticPath("translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("translations")));
return merge(polyfills, translations);
}
gulp.task("copy-static", (done) => {
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/")
);
copyMapPanel(staticDir);
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);
copyMapPanel(paths.demo_static);
copyFonts(paths.demo_static);
copyTranslations(paths.demo_static);
done();
});

View File

@@ -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,
@@ -22,6 +22,7 @@ const BUILT_IN_PANEL_ICONS = [
"mailbox", // Mailbox
"tooltip-account", // Map
"cart", // Shopping List
"hammer", // developer-tools
];
// Given an icon name, load the SVG file
@@ -38,13 +39,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 +54,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 +63,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 +84,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 +96,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,

View File

@@ -0,0 +1,33 @@
// 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');
self.addEventListener('install', (event) => {
self.skipWaiting();
});
`
);
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();
});

View File

@@ -0,0 +1,404 @@
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-demo",
"page-onboarding",
"developer-tools",
];
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);
gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) {
fs.mkdirSync(workDir);
}
done();
});
taskName = "create-test-metadata";
gulp.task(
taskName,
gulp.series("ensure-translations-build-dir", function writeTestMetaData(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;

View 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
View 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"),
};

223
build-scripts/webpack.js Normal file
View File

@@ -0,0 +1,223 @@
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 ? "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,
sourceMap: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
});
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const isCI = process.env.CI === "true";
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata["translations"]["en"]["fingerprints"];
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
return {
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.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
"/static/fonts/roboto/Roboto-Medium.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
"/static/fonts/roboto/Roboto-Regular.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
"/static/fonts/roboto/Roboto-Bold.woff2":
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
},
}),
].filter(Boolean),
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,
};

View File

@@ -1,43 +0,0 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports.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",
},
};
module.exports.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(__dirname, "../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(__dirname, "../src/util/empty.js")
),
];
module.exports.optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
terserOptions: {
ecma: latestBuild ? undefined : 5,
},
}),
],
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,3 @@
self.addEventListener("fetch", function(event) {
event.respondWith(fetch(event.request));
});

View 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

View File

@@ -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

View File

@@ -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();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,22 @@ import { Lovelace } from "../../../src/panels/lovelace/types";
import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [
() => import("./arsaboo").then((mod) => mod.demoArsaboo),
() => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
() => import("./kernehed").then((mod) => mod.demoKernehed),
() => import("./jimpower").then((mod) => mod.demoJimpower),
() =>
import(/* webpackChunkName: "arsaboo" */ "./arsaboo").then(
(mod) => mod.demoArsaboo
),
() =>
import(/* webpackChunkName: "teachingbirds" */ "./teachingbirds").then(
(mod) => mod.demoTeachingbirds
),
() =>
import(/* webpackChunkName: "kernehed" */ "./kernehed").then(
(mod) => mod.demoKernehed
),
() =>
import(/* webpackChunkName: "jimpower" */ "./jimpower").then(
(mod) => mod.demoJimpower
),
];
export let selectedDemoConfigIndex: number = 0;
@@ -25,7 +37,7 @@ export const setDemoConfig = async (
selectedDemoConfigIndex = index;
selectedDemoConfig = confProm;
hass.addEntities(config.entities(), true);
lovelace.saveConfig(config.lovelace());
hass.addEntities(config.entities(hass.localize), true);
lovelace.saveConfig(config.lovelace(hass.localize));
hass.mockTheme(config.theme());
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { LovelaceConfig } from "../../../../src/data/lovelace";
import "../../custom-cards/card-modder";
import { DemoConfig } from "../types";
export const demoLovelaceJimpower: () => LovelaceConfig = () => ({
export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
name: "Kingia Castle",
resources: [
// {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { DemoConfig } from "../types";
export const demoLovelaceKernehed: () => LovelaceConfig = () => ({
export const demoLovelaceKernehed: DemoConfig["lovelace"] = () => ({
name: "Hem",
resources: [
// {

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,7 @@
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { DemoConfig } from "../types";
export const demoLovelaceTeachingbirds: () => LovelaceConfig = () => ({
export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
title: "Home",
resources: [
// {
// url: "/local/lovelace/mini-media-player.js?v=0.6",
// type: "module",
// },
// {
// url: "/local/lovelace/slider-entity-row.js?v=d6da75",
// type: "js",
// },
// {
// url: "/local/lovelace/monster-card.js?v=0.2.3",
// type: "js",
// },
// {
// url: "/local/lovelace/tracker-card.js?v=0.1.5",
// type: "js",
// },
// {
// url: "/local/lovelace/home-setter.js?v=0.0.1",
// type: "js",
// },
],
views: [
{
cards: [
@@ -1201,484 +1179,5 @@ export const demoLovelaceTeachingbirds: () => LovelaceConfig = () => ({
title: "Info",
icon: "mdi:lan",
},
// {
// cards: [
// {
// cards: [
// {
// entities: [
// "switch.dafang_night_mode",
// "light.isa_ceiling_light",
// "switch.dafang_h264_rtsp_server",
// ],
// camera_image: "camera.upstairs",
// type: "picture-glance",
// title: "Upstairs",
// },
// {
// entities: [
// {
// tap_action: {
// action: "call-service",
// service: "script.dafang_down",
// },
// entity: "script.dafang_down",
// },
// {
// tap_action: {
// action: "toggle",
// },
// entity: "script.dafang_up",
// },
// {
// tap_action: {
// action: "toggle",
// },
// entity: "script.dafang_left",
// },
// {
// tap_action: {
// action: "toggle",
// },
// entity: "script.dafang_right",
// },
// {
// entity: "script.dafang_calibrate",
// hold_action: {
// action: "toggle",
// },
// tap_action: {
// action: "none",
// },
// icon: "mdi:reload",
// },
// ],
// show_name: false,
// type: "glance",
// show_state: false,
// },
// {
// type: "picture-entity",
// entity: "camera.motion_dafang",
// },
// ],
// type: "vertical-stack",
// },
// {
// cards: [
// {
// entities: [
// "light.living_room_ceiling_light_level",
// "light.living_room_spotlights_level",
// ],
// camera_image: "camera.back_door",
// type: "picture-glance",
// title: "Back door",
// },
// {
// entities: [
// {
// entity: "script.sannce_down",
// tap_action: {
// action: "toggle",
// },
// icon: "mdi:chevron-down",
// },
// {
// entity: "script.sannce_up",
// tap_action: {
// action: "toggle",
// },
// icon: "mdi:chevron-up",
// },
// {
// entity: "script.sannce_left",
// tap_action: {
// action: "toggle",
// },
// icon: "mdi:chevron-left",
// },
// {
// entity: "script.sannce_right",
// tap_action: {
// action: "toggle",
// },
// icon: "mdi:chevron-right",
// },
// {
// entity: "script.sannce_calibrate",
// hold_action: {
// action: "toggle",
// },
// tap_action: {
// action: "none",
// },
// icon: "mdi:reload",
// },
// ],
// show_name: false,
// type: "glance",
// show_state: false,
// },
// {
// type: "picture-entity",
// entity: "camera.motion_sannce",
// },
// ],
// type: "vertical-stack",
// },
// {
// cards: [
// {
// entities: ["sensor.ring_front_door_battery"],
// camera_image: "camera.front_door",
// title: "Latest Motion Front Door",
// show_state: false,
// type: "picture-glance",
// entity: "camera.front_door",
// },
// ],
// type: "vertical-stack",
// },
// ],
// path: "cameras",
// title: "Cameras",
// icon: "mdi:cctv",
// },
// {
// cards: [
// {
// cards: [
// {
// entities: [
// {
// name: "Road temp",
// entity: "sensor.trafikverket_alta_road_temperature",
// },
// {
// entity: "sensor.homesolna_travel_time",
// name: "Solna",
// icon: "mdi:car",
// },
// {
// entity: "sensor.homec9_travel_time",
// name: "C9",
// icon: "mdi:car",
// },
// ],
// type: "glance",
// },
// {
// cards: [
// {
// entity: "camera.tpl_skvaltan",
// type: "picture-entity",
// show_state: false,
// },
// {
// entity: "camera.tpl_nacka",
// type: "picture-entity",
// show_state: false,
// },
// ],
// type: "horizontal-stack",
// },
// {
// cards: [
// {
// entity: "camera.vikdalen",
// type: "picture-entity",
// show_state: false,
// },
// {
// entity: "camera.tpl_kvarnholmen",
// type: "picture-entity",
// show_state: false,
// },
// ],
// type: "horizontal-stack",
// },
// {
// cards: [
// {
// entity: "camera.marinstaden",
// type: "picture-entity",
// show_state: false,
// },
// {
// entity: "camera.svindersvik",
// type: "picture-entity",
// show_state: false,
// },
// ],
// type: "horizontal-stack",
// },
// {
// cards: [
// {
// entity: "camera.sicklatunneln",
// type: "picture-entity",
// show_state: false,
// },
// {
// entity: "camera.tpl_grondal",
// type: "picture-entity",
// show_state: false,
// },
// ],
// type: "horizontal-stack",
// },
// ],
// type: "vertical-stack",
// },
// ],
// path: "traffic",
// title: "Traffic info",
// icon: "mdi:car",
// },
// {
// cards: [
// {
// filter: {
// include: [
// {
// entity_id: "input_boolean.ad_*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Appdaemon Apps",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.cats*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Cats",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.house*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "House",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.lights*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Lights",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.presence*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Presence",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.security*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Security",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.system*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "System",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.temperature*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Temperature",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "automation.tts*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "TTS",
// },
// show_empty: false,
// },
// {
// filter: {
// include: [
// {
// entity_id: "alert.*",
// },
// ],
// },
// type: "custom:monster-card",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Alerts",
// },
// show_empty: false,
// },
// ],
// path: "automations",
// title: "Automations",
// icon: "mdi:flash-auto",
// },
{
cards: [
{
cards: [
{
entities: [
{
url: "https://www.home-assistant.io/",
type: "weblink",
name: "Home Assistant",
icon: "mdi:home-assistant",
},
{
url: "https://rc--home-assistant-docs.netlify.com/",
type: "weblink",
name: "Home Assistant Beta",
icon: "mdi:home-assistant",
},
{
url:
"https://github.com/home-assistant/home-assistant-polymer/releases",
type: "weblink",
name: "Lovelace release notes",
icon: "mdi:home-heart",
},
{
url: "https://s3.amazonaws.com/hassio-version/stable.json",
type: "weblink",
name: "Hassio build",
icon: "mdi:home-assistant",
},
{
url:
"https://github.com/dresden-elektronik/deconz-rest-plugin/releases",
type: "weblink",
name: "Deconz release notes",
icon: "mdi:home-assistant",
},
],
show_header_toggle: false,
type: "entities",
title: "Links",
},
],
type: "vertical-stack",
},
// {
// cards: [
// {
// type: "custom:home-setter",
// pages: [
// {
// path: "lovelace/home",
// name: "Default",
// },
// {
// path: "lovelace/dashboard",
// name: "Dashboard",
// },
// ],
// },
// {
// title: null,
// type: "custom:tracker-card",
// trackers: [
// "sensor.custom_card_tracker",
// "sensor.custom_component_tracker",
// ],
// },
// ],
// type: "vertical-stack",
// },
],
path: "settings",
title: "Settings",
icon: "mdi:cogs",
},
],
});

View File

@@ -1,12 +1,13 @@
import { LovelaceConfig } from "../../../src/data/lovelace";
import { Entity } from "../../../src/fake_data/entity";
import { LocalizeFunc } from "../../../src/common/translations/localize";
export interface DemoConfig {
index?: number;
name: string;
authorName: string;
authorUrl: string;
lovelace: () => LovelaceConfig;
entities: () => Entity[];
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => Entity[];
theme: () => { [key: string]: string } | null;
}

View File

@@ -6,12 +6,9 @@ import {
PropertyDeclarations,
} from "lit-element";
import { until } from "lit-html/directives/until";
import "@polymer/paper-icon-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";
@@ -24,8 +21,9 @@ import {
export class HADemoCard extends LitElement implements LovelaceCard {
public lovelace?: Lovelace;
public hass?: MockHomeAssistant;
public hass!: MockHomeAssistant;
private _switching?: boolean;
private _hidden = localStorage.hide_demo_card;
static get properties(): PropertyDeclarations {
return {
@@ -36,7 +34,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
}
public getCardSize() {
return 2;
return this._hidden ? 0 : 2;
}
public setConfig(
@@ -46,14 +44,13 @@ export class HADemoCard extends LitElement implements LovelaceCard {
) {}
protected render() {
if (this._hidden) {
return;
}
return html`
<ha-card>
<div class="picker">
<ha-paper-icon-button-prev
@click=${this._prevConfig}
.disabled=${this._switching}
></ha-paper-icon-button-prev>
<div>
<div class="label">
${this._switching
? html`
<paper-spinner-lite active></paper-spinner-lite>
@@ -63,9 +60,12 @@ export class HADemoCard extends LitElement implements LovelaceCard {
(conf) => html`
${conf.name}
<small>
by
<a target="_blank" href="${conf.authorUrl}">
${conf.authorName}
${this.hass.localize(
"ui.panel.page-demo.cards.demo.demo_by",
"name",
conf.authorName
)}
</a>
</small>
`
@@ -73,30 +73,29 @@ export class HADemoCard extends LitElement implements LovelaceCard {
""
)}
</div>
<ha-paper-icon-button-next
@click=${this._nextConfig}
.disabled=${this._switching}
></ha-paper-icon-button-next>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</mwc-button>
</div>
<div class="content">
Welcome home! You've reached the Home Assistant demo where we showcase
the best UIs created by our community.
<div class="content small-hidden">
${this.hass.localize("ui.panel.page-demo.cards.demo.introduction")}
</div>
<div class="actions">
<div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank">
<mwc-button>Learn more about Home Assistant</mwc-button>
<mwc-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</mwc-button>
</a>
</div>
</ha-card>
`;
}
private _prevConfig() {
this._updateConfig(
selectedDemoConfigIndex > 0
? selectedDemoConfigIndex - 1
: demoConfigs.length - 1
);
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this._hidden) {
this.style.display = "none";
}
}
private _nextConfig() {
@@ -110,7 +109,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
private async _updateConfig(index: number) {
this._switching = true;
try {
await setDemoConfig(this.hass!, this.lovelace!, index);
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err) {
alert("Failed to switch config :-(");
} finally {
@@ -125,6 +124,10 @@ export class HADemoCard extends LitElement implements LovelaceCard {
color: var(--primary-color);
}
.actions a {
text-decoration: none;
}
.content {
padding: 16px;
}
@@ -136,17 +139,27 @@ export class HADemoCard extends LitElement implements LovelaceCard {
height: 60px;
}
.picker div {
text-align: center;
.picker mwc-button {
margin-right: 8px;
}
.picker small {
.label {
padding-left: 16px;
}
.label small {
display: block;
}
.actions {
padding-left: 8px;
}
@media only screen and (max-width: 500px) {
.small-hidden {
display: none;
}
}
`,
];
}

View File

@@ -1,4 +1,4 @@
import { HomeAssistantAppEl } from "../../src/layouts/app/home-assistant";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import {
provideHass,
MockHomeAssistant,
@@ -18,7 +18,7 @@ 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
@@ -26,8 +26,14 @@ class HaDemo extends HomeAssistantAppEl {
this._updateHass(hassUpdate),
};
const hass = provideHass(this, initial);
mockLovelace(hass);
const hass = (this.hass = provideHass(this, initial));
const localizePromise =
// @ts-ignore
this._loadFragmentTranslations(hass.language, "page-demo").then(
() => this.hass!.localize
);
mockLovelace(hass, localizePromise);
mockAuth(hass);
mockTranslations(hass);
mockHistory(hass);
@@ -37,12 +43,16 @@ class HaDemo extends HomeAssistantAppEl {
mockEvents(hass);
mockMediaPlayer(hass);
mockFrontend(hass);
selectedDemoConfig.then((conf) => {
hass.addEntities(conf.entities());
if (conf.theme) {
hass.mockTheme(conf.theme());
// Once config is loaded AND localize, set entities and apply theme.
Promise.all([selectedDemoConfig, localizePromise]).then(
([conf, localize]) => {
hass.addEntities(conf.entities(localize));
if (conf.theme) {
hass.mockTheme(conf.theme());
}
}
});
);
// Taken from polymer/pwa-helpers. BSD-3 licensed
document.body.addEventListener(

View File

@@ -7,13 +7,13 @@
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Regular.ttf"
href="/static/fonts/roboto/Roboto-Regular.woff2"
as="font"
crossorigin
/>
<link
rel="preload"
href="/static/fonts/roboto/Roboto-Medium.ttf"
href="/static/fonts/roboto/Roboto-Medium.woff2"
as="font"
crossorigin
/>
@@ -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;
@@ -90,7 +87,7 @@
#ha-init-skeleton::before {
display: block;
content: "";
height: 112px;
height: 64px;
background-color: #03a9f4;
}
</style>
@@ -98,6 +95,21 @@
<body>
<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) {

View File

@@ -4,12 +4,16 @@ import "../custom-cards/ha-demo-card";
import { HADemoCard } from "../custom-cards/ha-demo-card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { selectedDemoConfig } from "../configs/demo-configs";
import { LocalizeFunc } from "../../../src/common/translations/localize";
export const mockLovelace = (hass: MockHomeAssistant) => {
selectedDemoConfig.then((config) => hass.addEntities(config.entities()));
export const mockLovelace = (
hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", () =>
selectedDemoConfig.then((config) => config.lovelace())
Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
)
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());

View File

@@ -1,90 +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 webpackBase = require("../config/webpack.js");
const { createDemoConfig } = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production";
// 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 chunkFilename =
isProd && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = "/";
const latestBuild = false;
module.exports = {
mode: isProd ? "production" : "development",
devtool: isProd ? "cheap-source-map" : "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: webpackBase.optimization(latestBuild),
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/",
},
]),
...webpackBase.plugins,
isProd &&
new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js",
importWorkboxFrom: "local",
include: [],
}),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};
module.exports = createDemoConfig({
isProdBuild,
isStatsBuild,
latestBuild,
});

View File

@@ -14,14 +14,14 @@ const ENTITIES = [
target_temp_high: 75,
target_temp_low: 70,
fan_mode: "Auto Low",
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
operation_mode: "auto",
operation_list: ["heat", "cool", "auto", "off"],
hold_mode: "home",
fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
hvac_modes: ["heat", "cool", "auto", "off"],
swing_mode: "Auto",
swing_list: ["Auto", "1", "2", "3", "Off"],
swing_modes: ["Auto", "1", "2", "3", "Off"],
friendly_name: "Ecobee",
supported_features: 1014,
supported_features: 59,
preset_mode: "eco",
preset_modes: ["away", "eco"],
}),
getEntity("climate", "nest", "heat", {
current_temperature: 17,
@@ -29,14 +29,12 @@ const ENTITIES = [
max_temp: 25,
temperature: 19,
fan_mode: "Auto Low",
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
operation_mode: "heat",
operation_list: ["heat", "cool", "auto", "off"],
hold_mode: "home",
fan_modes: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
hvac_modes: ["heat", "cool", "auto", "off"],
swing_mode: "Auto",
swing_list: ["Auto", "1", "2", "3", "Off"],
swing_modes: ["Auto", "1", "2", "3", "Off"],
friendly_name: "Nest",
supported_features: 1014,
supported_features: 43,
}),
];

View File

@@ -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;
}
@@ -56,7 +56,7 @@ class HaGallery extends PolymerElement {
color: var(--primary-color);
}
a paper-item {
a {
color: var(--primary-text-color);
text-decoration: none;
}
@@ -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,17 +133,27 @@ class HaGallery extends PolymerElement {
</paper-item>
</a>
</template>
</paper-card>
</ha-card>
</div>
</template>
</div>
</app-header-layout>
<notification-manager id='notifications'></notification-manager>
<notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager>
`;
}
static get properties() {
return {
_fakeHass: {
type: Object,
// Just enough for computeRTL
value: {
language: "en",
translationMetadata: {
translations: {},
},
},
},
_demo: {
type: String,
value: document.location.hash.substr(1),

View File

@@ -1,7 +1,7 @@
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const webpackBase = require("../config/webpack.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";
@@ -44,8 +44,8 @@ module.exports = {
to: "static/images/leaflet/",
},
{
from: "../node_modules/@polymer/font-roboto-local/fonts",
to: "static/fonts",
from: "../node_modules/roboto-fontface/fonts/roboto/*.woff2",
to: "static/fonts/roboto/",
},
{
from: "../node_modules/leaflet/dist/images",

View File

@@ -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"),
};

View File

@@ -1,315 +0,0 @@
const del = require("del");
const path = require("path");
const gulp = require("gulp");
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;
}
/**
* 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);
/**
* 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, ["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, ["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(hashFilename())
.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: "<%= 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, ["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;

View File

@@ -1,3 +1,3 @@
var requireDir = require('require-dir');
var requireDir = require("require-dir");
requireDir('./gulp/tasks/');
requireDir("./build-scripts/gulp/");

View File

@@ -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));
}

View File

@@ -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);

View 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);

View File

@@ -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);

View 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);

View File

@@ -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);

View 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;
}
}

View File

@@ -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() {

View File

@@ -1,6 +1,7 @@
import "@polymer/iron-icon/iron-icon";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -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.",
},
};
@@ -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,12 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin-right: 4px;
--iron-icon-height: 45px;
}
.protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a, ha-markdown a {
color: var(--primary-color);
}
</style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
@@ -161,12 +183,18 @@ 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]]">
<mwc-button on-click="openChangelog">Changelog</mwc-button>
@@ -175,6 +203,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</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 +253,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">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-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"
@@ -298,7 +323,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 +335,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 +360,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]]"
@@ -371,7 +424,7 @@ 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)]]"
@@ -381,10 +434,20 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
><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 +511,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 +553,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 +571,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 +602,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);

View File

@@ -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>
@@ -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;

View File

@@ -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,35 +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></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]]">
@@ -93,50 +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,
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) {
@@ -145,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;
},
@@ -163,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);
}
}

View File

@@ -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);

View 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;
}
}

View 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);
}

View 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;
}
}

View File

@@ -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);

View 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;
}
}

View File

@@ -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,10 +41,11 @@ class HassioHassUpdate extends PolymerElement {
>Update</ha-call-api-button
>
<a
href="https://github.com/home-assistant/home-assistant/releases"
href="[[computeReleaseNotesUrl(hassInfo.version)]]"
target="_blank"
><mwc-button>Release notes</mwc-button></a
>
<mwc-button>Release notes</mwc-button>
</a>
</div>
</paper-card>
</div>
@@ -97,6 +85,12 @@ class HassioHassUpdate extends PolymerElement {
computeUpdateAvailable(hassInfo) {
return hassInfo.version !== hassInfo.last_version;
}
computeReleaseNotesUrl(version) {
return `https://${
version.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`;
}
}
customElements.define("hassio-hass-update", HassioHassUpdate);

View File

@@ -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);

View 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,
});
};

View File

@@ -2,20 +2,65 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
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>
@@ -132,23 +173,17 @@ class HassioSnapshot extends PolymerElement {
>
</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);

View 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,
});
};

View File

@@ -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);

View File

@@ -1,41 +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]]" route="[[route]]"></hassio-main>
</template>
`;
}
static get properties() {
return {
hass: Object,
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-toggle-menu", (ev) =>
this.hassioPanel.fire("hass-toggle-menu", ev.detail)
);
}
_locationChanged() {
this.hassioPanel.navigate(window.location.pathname);
}
}
customElements.define("hassio-app", HassioApp);

View File

@@ -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);

View File

@@ -1,120 +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></hass-loading-screen>
</template>
<template is="dom-if" if="[[loaded]]">
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
<hassio-pages-with-tabs
hass="[[hass]]"
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]]"
route="[[route]]"
></hassio-addon-view>
</template>
</template>
`;
}
static get properties() {
return {
hass: Object,
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);

188
hassio/src/hassio-main.ts Normal file
View File

@@ -0,0 +1,188 @@
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;
@property() public narrow!: boolean;
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(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(/* webpackChunkName: "hassio-ingress-view" */ "./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,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
route,
});
} else {
el.hass = this.hass;
el.narrow = this.narrow;
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;
}
}

View File

@@ -1,161 +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></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,
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);

View File

@@ -0,0 +1,136 @@
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 narrow!: boolean;
@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
.hass=${this.hass}
.narrow=${this.narrow}
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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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>`;

View File

@@ -1,311 +0,0 @@
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 { 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">
<mwc-button
disabled="[[creatingSnapshot]]"
on-click="_createSnapshot"
>Create</mwc-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);

View 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;
}
}

View File

@@ -4,12 +4,14 @@ 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;
@@ -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,
});

View File

@@ -4,12 +4,12 @@ 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;

View File

@@ -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,7 +8,7 @@ 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);

View File

@@ -1,7 +1,10 @@
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 webpackBase = require("../config/webpack.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";
@@ -30,11 +33,22 @@ module.exports = {
},
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: {

View File

@@ -17,15 +17,15 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@material/mwc-button": "^0.3.6",
"@material/mwc-ripple": "^0.3.6",
"@mdi/svg": "^3.0.39",
"@polymer/app-layout": "^3.0.1",
"@material/mwc-base": "^0.6.0",
"@material/mwc-button": "^0.6.0",
"@material/mwc-ripple": "^0.6.0",
"@mdi/svg": "3.7.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",
"@polymer/font-roboto": "^3.0.2",
"@polymer/font-roboto-local": "^3.0.2",
"@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
@@ -34,18 +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-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",
@@ -56,118 +57,124 @@
"@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",
"@polymer/polymer": "3.1.0",
"@vaadin/vaadin-combo-box": "^4.2.8",
"@vaadin/vaadin-date-picker": "^3.3.3",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"chart.js": "~2.7.2",
"chartjs-chart-timeline": "^0.2.1",
"codemirror": "^5.43.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.0",
"hls.js": "^0.12.3",
"home-assistant-js-websocket": "^3.3.0",
"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.2",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.12.0",
"leaflet": "^1.3.4",
"lit-element": "^2.0.0",
"lit-html": "^1.0.0",
"marked": "^0.6.0",
"mdn-polyfills": "^5.12.0",
"memoize-one": "^5.0.0",
"moment": "^2.22.2",
"preact": "^8.3.1",
"js-yaml": "^3.13.0",
"leaflet": "^1.4.0",
"lit-element": "^2.2.0",
"lit-html": "^1.1.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",
"roboto-fontface": "^0.10.0",
"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-class-properties": "^7.3.0",
"@babel/plugin-proposal-decorators": "^7.3.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/hls.js": "^0.12.2",
"@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",
"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-import-resolver-webpack": "^0.10.1",
"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",
"require-dir": "^1.2.0",
"sinon": "^7.3.1",
"terser-webpack-plugin": "^1.2.3",
"ts-mocha": "^2.0.0",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"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"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
"resolutions": {
"@polymer/polymer": "3.1.0",
"@webcomponents/webcomponentsjs": "^2.2.6",
"@webcomponents/shadycss": "^1.9.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0"
"@webcomponents/webcomponentsjs": "^2.2.10",
"@vaadin/vaadin-lumo-styles": "^1.4.2",
"@polymer/polymer": "3.1.0"
},
"main": "src/home-assistant.js",
"husky": {

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 824 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Some files were not shown because too many files have changed in this diff Show More