Compare commits

..

495 Commits

Author SHA1 Message Date
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
Paulus Schoutsen
c55291dd18 Merge pull request #2945 from home-assistant/dev
20190315.1
2019-03-15 23:18:28 -07:00
Paulus Schoutsen
27b61776e8 Bumped version to 20190315.1 2019-03-15 23:17:13 -07:00
Paulus Schoutsen
baa13a1b6c Update translations 2019-03-15 23:16:00 -07:00
Paulus Schoutsen
23ca1b972d Fix router (#2943)
* Fix router

* Fix demo

* Extract update routes

* Lint
2019-03-15 23:15:16 -07:00
Paulus Schoutsen
2d75e797c7 Hide service toast in the demo (#2942)
* Hide service toast in the demo

* Update provide_hass.ts
2019-03-15 23:10:50 -07:00
Ian Richardson
117ea32586 Fix markdown style (#2944) 2019-03-15 23:10:34 -07:00
Paulus Schoutsen
68909c80ff Hide entity config option for user group (#2941) 2019-03-15 22:43:27 -07:00
Paulus Schoutsen
7aa296e774 move cert info to a dialog (#2940) 2019-03-15 22:43:19 -07:00
Paulus Schoutsen
18df636573 Merge pull request #2935 from home-assistant/dev
20190315.0
2019-03-15 10:42:05 -07:00
Paulus Schoutsen
e1540e45f9 Bumped version to 20190315.0 2019-03-15 10:40:54 -07:00
Paulus Schoutsen
d0b0284200 Update translations 2019-03-15 10:40:50 -07:00
Paulus Schoutsen
915c441a94 Cleanup config flow (#2932)
* Break up config flow dialog

* Allow picking devices when config flow finishes

* Lint

* Tweaks
2019-03-15 10:40:18 -07:00
Ian Richardson
2aec877310 Set entity-button defaults (#2897)
* Set entity-button defaults

tap_action: toggle
hold_action: more-info

* Address review comment, but unsure about lint

* Address review comments
2019-03-15 09:24:10 -07:00
Paulus Schoutsen
1e291e80b7 Fix translation (#2934) 2019-03-15 09:23:21 -07:00
Paulus Schoutsen
7d92eede1f Update translations 2019-03-14 16:28:04 -07:00
Paulus Schoutsen
9fc8c0764c Fix demo stub 2019-03-14 15:38:33 -07:00
Paulus Schoutsen
4ff2d941c3 Lint 2019-03-14 15:35:02 -07:00
Paulus Schoutsen
2349e2f251 Config panel routing (#2928)
* Config panel routing

* Abstract routing

* Convert partial-panel-resolver

* decorator

* Remove showMenu

* Tweaks to make it faster

* Rename update method

* Less aggressive loading
2019-03-14 15:16:46 -07:00
Paulus Schoutsen
8785b03fd8 Menu button simplify (#2930)
* Simplify the hass-menu button

* Purge showMenu boolean
2019-03-14 13:54:46 -07:00
Paulus Schoutsen
92e6c5adfd Bail out early when no stream component (#2927) 2019-03-13 13:32:59 -07:00
Paulus Schoutsen
6015eff8a2 Merge pull request #2926 from home-assistant/dev
20190313.0
2019-03-13 12:47:50 -07:00
Paulus Schoutsen
1451a78dd3 Bumped version to 20190313.0 2019-03-13 12:46:32 -07:00
Paulus Schoutsen
094f558556 Update translations 2019-03-13 12:46:27 -07:00
Paulus Schoutsen
cd466df42c Show message when cert not ready 2019-03-13 12:42:42 -07:00
Jason Hu
a626961ae5 Add Afrikaans and Basque languages support (#2922)
* Add Basque language

* Add Afrikaans language

* Update translations
2019-03-12 19:44:26 -07:00
Paulus Schoutsen
bbc32278d8 Cache thumbnails (#2924) 2019-03-12 19:43:04 -07:00
Paulus Schoutsen
e2ed1a9fd9 Fix bugs (#2923) 2019-03-12 19:41:02 -07:00
Paulus Schoutsen
cd94442455 Merge pull request #2917 from home-assistant/dev
20190312.0
2019-03-12 11:28:16 -07:00
Paulus Schoutsen
c9eea4acc1 Do not allow disabling managed webhooks (#2921) 2019-03-12 11:15:24 -07:00
Paulus Schoutsen
e55ca54509 Lint 2019-03-12 11:14:07 -07:00
Paulus Schoutsen
4118497978 Lint 2019-03-12 11:06:23 -07:00
Paulus Schoutsen
882dc38b12 Support native HLS support (#2920) 2019-03-12 11:03:20 -07:00
Paulus Schoutsen
bcade77075 Update cloud card (#2919) 2019-03-12 10:53:13 -07:00
Paulus Schoutsen
8c13e524b9 Bumped version to 20190312.0 2019-03-12 07:49:41 -07:00
Paulus Schoutsen
ffa47ccf34 Update translations 2019-03-12 07:49:12 -07:00
Paulus Schoutsen
1e22d13588 Cloud remote (#2916)
* Add cloud management

* Update text
2019-03-12 07:43:55 -07:00
Paulus Schoutsen
19804a713d Stream HLS (#2913)
* Stream HLS

* Lint
2019-03-11 22:40:41 -07:00
Paulus Schoutsen
eeaaecd5b7 Show err entity registry (#2914)
* Show error in entity registry

* Fix area translations and error message

* Fix person domain filter
2019-03-11 22:33:17 -07:00
Paulus Schoutsen
9a00c65e3b Fix fetch translation (#2909) 2019-03-11 16:26:59 -07:00
Paulus Schoutsen
c026c65d53 Fix correct dialog-save element registration 2019-03-11 14:43:15 -07:00
Ian Richardson
e9c245015c Add automation to list of domains that can use header toggle (#2900) 2019-03-11 12:12:17 -07:00
David F. Mulcahey
cdde6f6f4c device name (#2901) 2019-03-11 12:12:03 -07:00
Ian Richardson
262537c287 🔧 properly override entity picture with icon (#2902) 2019-03-11 12:10:24 -07:00
Ian Richardson
ec04c80413 add show_icon to glance-card (#2903)
*  add `show_icon` to glance-card

* lint/error
2019-03-11 12:09:32 -07:00
Paulus Schoutsen
86548052e5 Allow changing group (#2908)
* Allow changing group

* Styling + rename

* Fix type
2019-03-11 12:08:09 -07:00
Paulus Schoutsen
1890dd8683 Bumped version to 20190309.0 2019-03-09 21:25:17 -08:00
Paulus Schoutsen
2908eb693a Update translations 2019-03-09 21:25:12 -08:00
Ian Richardson
7fe4084073 🔧 Fix hui-theme-select-editor definition typo (#2899) 2019-03-09 18:23:26 -06:00
Paulus Schoutsen
ee948302ed Convert onboarding to Lit (#2894)
* Convert onboarding to Lit

* Apply suggestions from code review

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

* Add confirm password field
2019-03-08 13:51:37 -08:00
Jason Hu
f809bf0550 Save user language setting to backend (#2784)
* Save user language setting to backend

* Remove hass.selectedLanguage

* Lint

* Address code review comment

* Refactoring translation

* Code review

* Add back selectedLanguage and local app storage

* Move getTranslations to data/frontend.ts

* Fix mock hass

* Rewrite translations-mixin

* revert no need changes

* Final tweak
2019-03-08 02:49:58 -08:00
Malte Franken
ed9dff99d3 Geolocation source configurable in map editor (#2755)
* wip

* added input list editor

* input label configurable, variables renamed

* new input field working as expected now

* fix lint issues

* fix lint issues

* fix lint issues and code clean-up

* fix lint issues

* uses property decorator now

* change the way css is included

* moved heading from input list editor to map card editor

* moved styling of input list editor to map card editor

* stopped propagating event

* return new value in event instead of changing the input value

* added button to clear value; consolidate value when leaving input field

* fix lint issues

* fix lint issues

* using customElement decorator

* fix lint issues
2019-03-07 14:06:40 -08:00
Ian Richardson
f5d0162aec Convert hui-plant-status-card to TypeScript/LitElement (#2891)
* Convert plant-status card to TS/Lit

* Cleanup
2019-03-07 13:59:21 -08:00
Paulus Schoutsen
32682a2be0 Bumped version to 20190305.1 2019-03-07 10:53:56 -08:00
Paulus Schoutsen
836844a312 Fix checking cloudhooks exist (#2893) 2019-03-07 10:53:52 -08:00
Paulus Schoutsen
8b82fa940e Fix checking cloudhooks exist (#2893) 2019-03-07 10:53:24 -08:00
David F. Mulcahey
d4be171df9 Direct device binding for ZHA config panel (#2856)
* device binding

* review comments

* Update zha-binding.ts
2019-03-07 10:53:06 -08:00
Ian Richardson
57be7ac873 Cleanup cards (#2870)
* Cleanup cards

* Update hui-iframe-card.ts

* address review comments

* Apply paper-styles

https://github.com/PolymerElements/paper-styles/blob/v3.0.1/classes/typography.js

I didn't find `paper-font-common-nowrap` in anything after v1.0.0 but did end up applying what I found here: 923ede74eb/third_party/polymer/v1_0/components/paper-styles/typography.html

* Added comments on paper-style usage
2019-03-07 10:52:39 -08:00
Ian Richardson
a9cecb55ac Update ISSUE_TEMPLATE.md (#2892) 2019-03-07 10:51:57 -08:00
Paulus Schoutsen
1c6bf8b94a Upgrade home-assistant-js-websocket to 3.3.0 (#2887) 2019-03-06 09:52:28 -08:00
Paulus Schoutsen
1c6235546a Swap out babel-minify for terser (#2885) 2019-03-05 13:10:35 -08:00
Paulus Schoutsen
daaaef96b0 Limit service worker (#2886) 2019-03-05 12:42:42 -08:00
Ian Richardson
aa3b6343ed Cleanup Editors and some common elements (#2882)
This is what I do while watching TV 😄
2019-03-05 11:36:17 -08:00
Paulus Schoutsen
3e5d372bbe Merge pull request #2884 from home-assistant/dev
20190305.0
2019-03-05 11:30:44 -08:00
Paulus Schoutsen
3bab8686c8 Bumped version to 20190305.0 2019-03-05 11:30:03 -08:00
Paulus Schoutsen
8c0af2c140 Update translations 2019-03-05 11:29:58 -08:00
Paulus Schoutsen
f008f8f41a Fix map entities (#2883)
* Fix map entities

* Fix switching tabs
2019-03-05 11:28:31 -08:00
Paulus Schoutsen
587a7c3b66 Merge pull request #2880 from home-assistant/dev
20190303.0
2019-03-03 21:55:12 -08:00
Paulus Schoutsen
b25fff9852 Bumped version to 20190303.0 2019-03-03 21:54:40 -08:00
Paulus Schoutsen
45e5f7d0ff Update translations 2019-03-03 21:52:59 -08:00
yosilevy
19c44f7c7b Conditional element support for use in picture-elements (#2865)
* Conditional element support for use in picture-elements

* Refactored

* Refactor to HTMLElement, separated files

* New file

* Added disconnected callback, handled multiple config calls.

* Added update method

* Refactored and simplified - elements contained internally

* Removed excess if

* Refactored also picture elements
2019-03-03 11:27:34 -08:00
Paulus Schoutsen
241d7345d0 Do not fire command if we know component not loaded (#2875) 2019-03-03 10:26:10 -08:00
Jason Hu
34c6356a47 Display service call error message (#2874) 2019-03-01 23:09:57 -08:00
Paulus Schoutsen
9383d80354 Better fix for ha-entity-toggle (#2873) 2019-03-01 11:17:48 -08:00
yosilevy
178e4de452 Fixed history graph tooltip so dates are readable (#2872) 2019-03-01 10:59:17 -08:00
Paulus Schoutsen
787abc4611 Merge pull request #2869 from home-assistant/dev
20190228.0
2019-02-28 17:42:16 -08:00
Paulus Schoutsen
c2948638d6 Bumped version to 20190228.0 2019-02-28 17:41:38 -08:00
Paulus Schoutsen
1db93a4f7b Fix ha-entity-toggle restoring old state (#2868) 2019-02-28 17:41:20 -08:00
Paulus Schoutsen
8f4d24b6da Update translatins 2019-02-28 17:11:59 -08:00
Thomas Lovén
03d4a648f5 Trim overflowing cards (#2864)
* Trim overflowing cards

* Fix picture-elements instead
2019-02-28 14:30:43 -08:00
Paulus Schoutsen
8dba463dd4 Fix define 2019-02-28 12:02:32 -08:00
Paulus Schoutsen
7c21a07a66 fix import 2019-02-28 11:48:42 -08:00
Paulus Schoutsen
82189ab3c6 Fix system health check (#2866)
* Fix system health check

* Update src/panels/dev-info/system-health-card.ts

Co-Authored-By: balloob <paulus@home-assistant.io>
2019-02-28 10:24:19 -08:00
Ian Richardson
c0896d173d cleanup rows (#2863) 2019-02-28 09:48:18 -08:00
Paulus Schoutsen
5032b6e63b Merge pull request #2862 from home-assistant/dev
20190227.0
2019-02-27 16:11:40 -08:00
Paulus Schoutsen
9bf06ca0af Bumped version to 20190227.0 2019-02-27 16:10:34 -08:00
Paulus Schoutsen
4fa1c3e883 Update translations 2019-02-27 16:10:34 -08:00
Paulus Schoutsen
339be43eea Check system health loaded (#2861) 2019-02-27 16:03:50 -08:00
Jason Hu
00c08a09db Fix login issue on FireFox (#2860) 2019-02-27 14:24:26 -08:00
Thomas Lovén
bed257a4eb Add ha-card-box-shadow css variable for themeing (#2855) 2019-02-27 11:30:03 -08:00
Thomas Lovén
b73a2e838a Stop brightness display of light-card from blocking clicks (#2850) 2019-02-26 11:07:06 -08:00
starkillerOG
6580d4ce92 Color picker: dynamic segmentation (#2806)
* Color picker: dynamic segmentation

* Color Picker: dynamic segmentation

* Color Picker: dynamic segments

* == --> ===

* spaces

* use setProperties()

* fix dynamic segements

* Change size position and collor of segmentation button

* add spaces

* Add ;
2019-02-25 20:38:46 -08:00
Victor Vostrikov
8c23674683 Add labels to Person (#2844)
* Added label for Person

* Added icon for Person

* Changed comment for label

* Added translation for person

* Updated translation for person
2019-02-25 12:01:18 -08:00
Paulus Schoutsen
3e28b6f2e2 Convert ha-menu-button to TS (#2825)
* Convert ha-menu-button to TS

* Address comments

* Fix icon searcher
2019-02-25 11:53:46 -08:00
Paulus Schoutsen
6d2e480ed5 Convert ha-url-sync to TS (#2824)
* Convert ha-url-sync

* Change url-sync to be a mixin
2019-02-25 11:11:33 -08:00
Paulus Schoutsen
90a1f7e51c Convert map card to Lit/TS (#2826)
* Convert map card to Lit/TS

* Address comments
2019-02-25 11:10:22 -08:00
yosilevy
63e6506510 Cleanup (#2837) 2019-02-25 11:10:08 -08:00
Paulus Schoutsen
220fec6dc9 Initial commit (#2833) 2019-02-23 20:43:18 -08:00
Paulus Schoutsen
534b18ee30 Convert config flow to Lit/TS (#2814)
* Convert config flow to Lit/TS

* Apply suggestions from code review

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

* Add missing import

* Apply suggestions from code review

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

* Address comments
2019-02-23 20:35:11 -08:00
Jason Hu
e406a50b50 Relaod lovelace config if language changed (#2805) 2019-02-23 13:39:14 -08:00
yosilevy
b764e87a00 Force LTR on error popup in dev-info panel (#2834) 2019-02-23 13:30:58 -08:00
yosilevy
b7f62c5822 Fix after conversion to Lit (#2835) 2019-02-23 13:30:32 -08:00
yosilevy
dede819a12 Removed toast (entities turn on/off and OK service calls) (#2822)
* Removed toast when entities turn on/off and services are SUCCESSFULLY called. Still will show on service error.

* Removed unused import

* Removed translations
2019-02-23 11:28:31 -06:00
Yosi Levy
083f0d16ef Initial commit 2019-02-23 17:18:57 +02:00
Paulus Schoutsen
c7500a504d Remove duplicate file 2019-02-22 22:31:39 -08:00
yosilevy
3348405518 Link updates following markdown removal (#2813) 2019-02-22 20:40:21 -08:00
yosilevy
70b2ff3365 Fixed arrow in sub-page to use new arrows and fixed new button icon issues (#2807)
* Fixed arrow in sub-page to use new arrows and fixed new button icon placement problem

* Changed icon to text + added localization
2019-02-22 20:39:39 -08:00
Ian Richardson
a259a12eab 🧹 cleanup elements (#2820)
* 🧹 cleanup elements

* lint
2019-02-22 20:37:27 -08:00
Paulus Schoutsen
979025539e Show person as badges (#2823) 2019-02-22 20:30:12 -06:00
Ian Richardson
6da311078a Convert input-number to Lit/TS (#2792)
* Convert input-number to Lit/TS

Should I worry about width for the state display with slider?

* address review comments

* clientWidth not currently working
* unsure about the typing of _InputElement

* remove unused import

* get clientwidth

* added comment
2019-02-22 12:08:18 -08:00
Paulus Schoutsen
7d1991ac78 Update translations 2019-02-21 16:50:15 -08:00
Paulus Schoutsen
2c2199fb84 Merge pull request #2810 from home-assistant/dev
20190220.0
2019-02-20 07:46:42 -08:00
Paulus Schoutsen
e12da05d4e Bumped version to 20190220.0 2019-02-20 07:45:28 -08:00
Paulus Schoutsen
25d10cf092 Update translations 2019-02-20 07:45:12 -08:00
Paulus Schoutsen
1cdaebd92f Add an event subscribe card (#2804) 2019-02-19 19:33:55 -08:00
Jason Hu
f5d3f1c042 Update windy.com widget used in arsaboo demo (#2803) 2019-02-19 14:51:58 -08:00
Paulus Schoutsen
4073238103 RTL arrow components (#2750)
* New arrow and chevron next+prev components

* New component files

* Refactor

* Updated super + commets

* Fix ha-style
2019-02-19 13:54:02 -08:00
Diogo Gomes
513eaea4f4 Added suppression info (#2790)
* added suppression info

* added to type
2019-02-19 13:53:50 -08:00
Yosi Levy
b4ac3ddfbd Merge branch 'RTL-arrow-components' of https://github.com/yosilevy/home-assistant-polymer into RTL-arrow-components 2019-02-19 20:18:40 +02:00
Yosi Levy
0a269c9e26 Fix ha-style 2019-02-19 20:17:41 +02:00
Paulus Schoutsen
1f2371641e Merge pull request #2802 from home-assistant/dev
20190219.0
2019-02-19 10:14:00 -08:00
yosilevy
8b582f3fcf Merge branch 'dev' into RTL-arrow-components 2019-02-19 20:07:36 +02:00
yosilevy
1afb8f109e Localization updates (#2767)
* Label localization

* Added various missing localization labels + paper-fab RTL location fix (was totally gone behind app-drawer)

* Removed ha-markdown from all translations. Refactored links to separate anchors.
2019-02-19 10:04:33 -08:00
Yosi Levy
5824e0b706 Merge branch 'dev' of https://github.com/home-assistant/home-assistant-polymer into RTL-arrow-components 2019-02-19 20:01:30 +02:00
Paulus Schoutsen
97deed9299 Bumped version to 20190219.0 2019-02-19 09:22:15 -08:00
Paulus Schoutsen
9efcca002d Update translations 2019-02-19 09:22:12 -08:00
Thomas Lovén
7904483272 Register closeEditor as property of hui-editor (#2797)
* Register closeEditor as property of hui-editor

* Belt and suspenders

* Update hui-editor.ts
2019-02-19 09:20:21 -08:00
yosilevy
8a9594d918 Fix missing focus on editor when dialog loaded (#2799) 2019-02-19 09:12:05 -08:00
Timmo
90c09e967a 🔨 Make edit card only close with cancel or save (#2798) 2019-02-19 09:10:58 -08:00
Timmo
5ba1cc5075 🔥 Fixes entity-button icon color (#2796) 2019-02-19 09:09:44 -08:00
Paulus Schoutsen
12064a086b Fix attribute-prop mapping (#2794) 2019-02-19 09:09:07 -08:00
Ian Richardson
32d0e8bf1d less broken thermostat-card (#2793)
* Update hui-thermostat-card.ts

* made light and thermo more consistent
2019-02-18 22:16:18 -08:00
shbatm
79a5947587 Add missing localization for Fan Mode in Climate More Info (#2716)
* Add missing localization for Fan Mode in Climate More Info

* Separate Climate Fan Mode from Operation Mode

* Separated out climate.fan_mode into state_attributess

Separated out climate.fan_mode into state_attributess

* Fix bad merge and update localizeFanMode funtion.
2019-02-18 21:47:15 -08:00
Paulus Schoutsen
c8cda3c817 Add person icon (#2789) 2019-02-18 18:41:56 -08:00
Paulus Schoutsen
197cf0f8cc Merge pull request #2787 from home-assistant/dev
20190218.0
2019-02-18 13:16:19 -08:00
Paulus Schoutsen
392af26503 Bumped version to 20190218.0 2019-02-18 13:14:20 -08:00
Paulus Schoutsen
41343c9774 Update translations 2019-02-18 13:14:14 -08:00
Ian Richardson
4afce7600b Convert timer-row to TS/Lit (#2743)
* Convert timer-row to TS/Lit

* added translations

* cleanup

* address review comments and fix interval

* lint

* address review comments

* address review comments

* address review comments
2019-02-18 13:12:40 -08:00
Paulus Schoutsen
e4b4a94a5f Convert notification-button to Lit/TS and add badge (#2732)
* Convert notification-button to Lit/TS and add badge

* review comments

* address review comments

* css is dumb

* Update package.json

* address review comments

* lint

* address review comments
2019-02-17 22:49:24 -08:00
Ian Richardson
3db79607b7 Cleanup remaining entity not-found warnings (#2779) 2019-02-17 22:45:49 -08:00
Ian Richardson
2ada32be02 Cleanup mwc-button css (#2780)
``font-weight: 500;`
`color: var(--primary-color);`
are not necessary to specify
2019-02-17 22:44:01 -08:00
Ian Richardson
a4ec8719f9 warning when light unavilable (#2771)
* error-card when light unavilable

* single warning element for all

* address review comments

* address review comments
2019-02-17 20:43:46 -08:00
Ian Richardson
5e6b28d965 address review comments 2019-02-17 22:17:45 -06:00
Ian Richardson
7d8f790708 fix for thermostat reporting null target temp (#2730)
* fix for thermostat reporting null target temp

* address review comments
2019-02-17 20:00:21 -08:00
Jason Hu
b6b224be77 Fix user initial in sidebar (#2777) 2019-02-17 10:48:03 -08:00
Paulus Schoutsen
3b008b6359 Revoke old camera image after new one has loaded (#2772) 2019-02-17 09:06:50 -08:00
Jason Hu
da80bfa3c7 Change recommend VSCode TSLint plugin to offical supported one (#2775) 2019-02-16 23:35:10 -08:00
Yosi Levy
fcd06a9000 Updated super + commets 2019-02-16 22:28:46 +02:00
Paulus Schoutsen
762908207f Merge pull request #2769 from home-assistant/dev
20190216.0
2019-02-16 11:59:34 -08:00
Paulus Schoutsen
b40b5b95f1 Bumped version to 20190216.0 2019-02-16 11:59:01 -08:00
Paulus Schoutsen
fe176f2752 Update translations 2019-02-16 11:58:54 -08:00
Paulus Schoutsen
c7796e9557 Allow picking users (#2768)
* Allow picking users

* Update ha-user-badge.ts
2019-02-16 11:58:07 -08:00
Ian Richardson
679457e36a lint 2019-02-16 13:57:39 -06:00
Leonardo Merza
2d3d4db4dd centered loading for system health and logs (#2759) 2019-02-16 11:54:57 -08:00
MatthewFlamm
f127bbc64d standardize more-info-weather and add hourly/daily (#2766) 2019-02-16 11:54:14 -08:00
Ian Richardson
bdaf96b114 address review comments 2019-02-16 13:31:41 -06:00
Yosi Levy
ad55bae212 Refactor 2019-02-15 21:07:42 +02:00
Paulus Schoutsen
ea8958adae Merge pull request #2758 from home-assistant/dev
20190215.0
2019-02-15 09:57:28 -08:00
Paulus Schoutsen
e7b664a2ff Bumped version to 20190215.0 2019-02-15 09:47:46 -08:00
Paulus Schoutsen
541d1a5380 Update translations 2019-02-15 09:47:30 -08:00
Paulus Schoutsen
ac179f5b45 Unused entities to respect tap/hold action (#2754) 2019-02-15 09:46:32 -08:00
Paulus Schoutsen
34f36c6179 Update package.json 2019-02-15 08:49:48 -08:00
Ian Richardson
56c1920cc1 css is dumb 2019-02-14 23:18:21 -06:00
Ian Richardson
456880c7cf address review comments 2019-02-14 23:18:20 -06:00
Ian Richardson
08222dfbec review comments 2019-02-14 23:18:19 -06:00
Ian Richardson
e8d84e8ba5 Convert notification-button to Lit/TS and add badge 2019-02-14 23:18:18 -06:00
Paulus Schoutsen
c570ce9720 Update translations 2019-02-14 19:09:21 -08:00
Paulus Schoutsen
f8b66a78fa Fix alarm panel look (#2753) 2019-02-14 15:53:42 -08:00
Paulus Schoutsen
e2fc98526b Fix header padding for history graph card (#2748) 2019-02-14 12:34:53 -08:00
Paulus Schoutsen
856a393531 Fix tight space (#2749) 2019-02-14 12:34:45 -08:00
David F. Mulcahey
8bf2a2f8db display values as hex so that they match the zigbee spec docs (#2751) 2019-02-14 12:34:32 -08:00
Bram Kragten
3f6bbffcd6 Fix styling of raw config save button (#2752) 2019-02-14 12:22:23 -08:00
Paulus Schoutsen
4d5087bd8d Update translations 2019-02-14 12:11:17 -08:00
Paulus Schoutsen
f9663143a6 Merge pull request #2747 from home-assistant/dev
20190213.0
2019-02-13 15:13:04 -08:00
Paulus Schoutsen
c4aac72e68 Bumped version to 20190213.0 2019-02-13 15:04:51 -08:00
Paulus Schoutsen
f4048bf4ba Update translations 2019-02-13 15:04:46 -08:00
Bram Kragten
b384d17fd3 YAML editor fixes (#2737)
* YAML editor fixes

* Fix auto height in dialog

* remove height from paper-dialog-buttons

* wait for next paint instead of fixed time

* resize again after codemirror is updated

* afternextrender merge
2019-02-13 15:03:52 -08:00
Bram Kragten
2ae30ac024 Position delete/move menu better for mobile (#2738) 2019-02-13 12:32:32 -08:00
Yosi Levy
9b9b2f0710 New component files 2019-02-13 19:58:28 +00:00
Yosi Levy
5d58dfab3e New arrow and chevron next+prev components 2019-02-13 19:56:12 +00:00
Ian Richardson
38ba6058be Proper fix to alarm-control-panel-card (#2741) 2019-02-13 11:22:39 -08:00
kethoth
0f779dd7f8 Apply text color to system information text (#2739)
Apply primary-text-color to content so the system information text can be themed.  Currently text remains black, which is unreadable on dark themes.
2019-02-13 11:22:19 -08:00
Thomas Lovén
3ca842187a Convert ha-card to LitElement and TypeScript (#2701)
* Convert ha-card to LitElement and TypeScript

* CSS away the header instead

* Travis fixes

* Remove duplicate styles
2019-02-13 11:16:01 -08:00
Thomas Lovén
e36dada843 Tweak some file permissions that were odd (#2745) 2019-02-13 07:32:52 -08:00
Paulus Schoutsen
1b8c567fd7 Use mwc-button instead of paper-button (#2744)
* Convert from paper-button to mwc-button

* Fixes

* Bye paper-button

* Fixes

* Final fixes

* Fix rebase conversion
2019-02-12 23:08:29 -08:00
Paulus Schoutsen
e1c2cf770a Convert lit ts layout (#2742)
* Convert layout to TS/Lit

* Further cleanup

* Apply suggestions from code review

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

* Simplify error screen
2019-02-12 22:41:36 -08:00
Paulus Schoutsen
ab6cd578e8 Add lit-plugin to recommendations (#2735) 2019-02-12 13:24:00 -08:00
Paulus Schoutsen
4058a0c8d0 Merge pull request #2736 from home-assistant/dev
20190212.0
2019-02-12 12:04:48 -08:00
Paulus Schoutsen
421e5bb169 Bumped version to 20190212.0 2019-02-12 11:56:37 -08:00
Paulus Schoutsen
6faea73c9f Update translations 2019-02-12 11:56:29 -08:00
David F. Mulcahey
3a644621fe Make ZHA config panel device oriented (#2722)
* change to be zha device centric instead of entity centric

* clusters by device

* device centric API

* lit ts and cleanup

* type

* review comments and fix remove

* fix set attribute
2019-02-12 11:55:42 -08:00
Paulus Schoutsen
abbfea0b6a Person: Pick device tracker (#2726)
* Allow picking devices to track

* Tweak translation

* Update translation
2019-02-12 11:52:30 -08:00
Ian Richardson
2f2cdad16b static styles, decorators and width fix (#2727)
Not sure how the width got messed up, perhaps changes to ha-card?
2019-02-12 11:52:02 -08:00
Ian Richardson
aae3c26a64 render remote entity-row as a toggle (#2731) 2019-02-12 09:43:59 -08:00
Ian Richardson
0f680bcfd6 Fix typo in alarm editor (#2729)
* Fix typo in alarm editor

* tacking on decorators, styles and card size fix
2019-02-12 09:40:39 -08:00
Paulus Schoutsen
f71612d6cf Update translations 2019-02-11 23:40:15 -08:00
aquarium
5cc23ab084 Update Store Auth card to use primary-text-color (#2725) 2019-02-11 14:45:05 -08:00
yosilevy
9d6c0773c5 Rtl configuration fixes (#2720)
* RTL fixes to config pages

* Disconnect toast RTL

* RTL user editor - force LTR (only the user box - not the action box)

* Many RTL fixes in configuration. Keeping scrollbar in RTL so it's not obstructed. Added missing localization.

* Update disconnect-toast-mixin.ts
2019-02-11 14:40:09 -08:00
yosilevy
44dca3b86d Make yaml editor scrollabel in RTL mode (#2706)
* Make yaml editor scrollabel in RTL mode

* Refactor

* Refactor scopped CSS

* Refactor

* Fixes

* Refactor
2019-02-11 14:18:17 -08:00
Paulus Schoutsen
310b81de04 Convert HUI-IMAGE to TypeScript/Lit (#2713)
* Fix gallery demos

* Convert HUI-IMAGE to TypeScript/Lit

* Clean up
2019-02-11 14:14:29 -08:00
Paulus Schoutsen
f23258eb8c Convert state badge to TypeScript (#2712) 2019-02-09 11:55:46 -08:00
Paulus Schoutsen
039bc587cc Add decorators (#2711)
* Add decorators

* Lint
2019-02-09 10:47:39 -08:00
Paulus Schoutsen
46e1139946 Add MVP person editor (#2703)
* Add MVP person editor

* Better highlight the config.yaml people

* Add note
2019-02-09 10:41:45 -08:00
Bram Kragten
8938ad8f8d Add ctrl/cmd +s support back to editor (#2694)
* Add ctrl/cmd +s support back to editor

* Update hui-yaml-editor.ts
2019-02-07 19:33:53 -08:00
yosilevy
102cb06d28 RTL support for arrows in scrolable tabs (#2696)
* RTL support for arrows in scrolable tabs

* Refactor
2019-02-07 19:32:32 -08:00
Paulus Schoutsen
17d0ae003a Lint 2019-02-07 17:16:39 -08:00
Thomas Lovén
7a16961387 Add background and border-radius to themeable options for ha-card (#2700)
* Add background and border-radius to themeable options for ha-card

* Change variable names
2019-02-07 13:30:02 -08:00
Robert Schindler
d3bdbce0d0 Added strings for command line auth provider (#2561)
* Added strings for command line auth provider

Regards home-assistant/home-assistant#19985

* Reuse existing translation keys for new command_line auth provider
2019-02-06 16:38:31 -08:00
Paulus Schoutsen
504e4987b7 Fix event action in automation editor (#2686)
* Fix event action in automation editor

* Fix webpack resolve

* Update ha-automation-editor.js
2019-02-06 11:13:00 -08:00
Paulus Schoutsen
f00de454d1 Convert automation editor to Lit (#2687)
* Convert automation editor to Lit

* Apply suggestions from code review

Co-Authored-By: balloob <paulus@home-assistant.io>
2019-02-06 11:01:15 -08:00
Paulus Schoutsen
ce35416284 Migrate more-info-content to UpdatingElement (#2693) 2019-02-06 11:00:17 -08:00
Paulus Schoutsen
7773589e2c Update Lit (#2692) 2019-02-06 10:59:47 -08:00
Paulus Schoutsen
5d900f9ced Split unused entities by domain (#2671) 2019-02-06 10:57:53 -08:00
Paulus Schoutsen
7a344c865f Fix gauge card gallery demo (#2688) 2019-02-05 22:13:27 -08:00
yosilevy
bd0bc2047d hui editor arrows RTL support (#2673)
* hui editor arrows RTL support

* Refactor

* Refactor

* Refactor
2019-02-05 13:05:19 -08:00
Paulus Schoutsen
2482d78a06 Optimize demo (#2681) 2019-02-05 07:28:23 -08:00
yosilevy
18fc0d0342 Added forgotten label localization (#2672) 2019-02-03 21:24:29 -08:00
Paulus Schoutsen
024ce5c379 Merge pull request #2670 from home-assistant/dev
20190203.0
2019-02-03 11:29:09 -08:00
Paulus Schoutsen
f74fe5718e Bumped version to 20190203.0 2019-02-03 11:27:23 -08:00
Paulus Schoutsen
ef395d4c9f Update translations 2019-02-03 11:27:12 -08:00
Paulus Schoutsen
5d42e4f68d Fix more info on light/thermostat pushing content (#2669) 2019-02-03 11:19:14 -08:00
Paulus Schoutsen
acce6f0b2f Fix sidebar issues (#2667) 2019-02-03 10:58:57 -08:00
Paulus Schoutsen
dadb5f92ee Fix menu button on dev-info page (#2663) 2019-02-02 22:07:02 -08:00
Paulus Schoutsen
69aff1e204 Don't include some UI elements from base bundle (#2665) 2019-02-02 22:06:53 -08:00
yosilevy
810fd802b5 Lovelace editor - flex spacing (#2666)
* Converted lovelace editor to flex spacing

* Removed margin-top leftover
2019-02-02 22:06:36 -08:00
Paulus Schoutsen
cf1b9e5067 Merge pull request #2661 from home-assistant/dev
20190202.0
2019-02-02 14:00:28 -08:00
Paulus Schoutsen
83aaf4699c Bumped version to 20190202.0 2019-02-02 13:49:31 -08:00
Paulus Schoutsen
72aa98fe5c Update translations 2019-02-02 13:49:25 -08:00
yosilevy
86b353e627 History+logbook positioning update + RTL fixes + label refactor (#2659) 2019-02-02 13:36:33 -08:00
Paulus Schoutsen
79183bb6ea Cleanups (#2658)
* Import voice dialog only when needed

* Import ha-sidebar when we have first painted the page.

* Add css on LitElement for custom cards

* Import polyfill on first update

* Cleanup of imports

* TS conversion more info mixin

* Migrate auth mixin to TS

* Lint
2019-02-02 13:23:48 -08:00
Paulus Schoutsen
4921686bdf Hash translation files (#2652)
* Hash translation files

* Fix rebuild while develop runs
2019-02-02 09:42:22 -08:00
yosilevy
a5bdf096dc Marked Arabic as RTL + added a bunch of lovelace menu entries to label files (#2650)
* Marked Arabic as RTL + added a bunch of lovelace menu entries to label files

* Refactor keys - added menus
2019-02-01 21:33:26 -08:00
Paulus Schoutsen
bfee69e7ff Merge pull request #2649 from home-assistant/dev
20190201.0
2019-02-01 12:26:33 -08:00
Paulus Schoutsen
db53d37493 Bumped version to 20190201.0 2019-02-01 12:25:39 -08:00
Paulus Schoutsen
c294124b8d Update translations 2019-02-01 12:25:27 -08:00
Paulus Schoutsen
2afc8607c6 Fix RTL issues (#2648)
* Convert home-assistant-main to Lit/TS

* different approach

* LRT RTL

* Lint

* RTL fix for generic entity row

* Remove fetching from selectedLanguage

* RTL the RTL languages in the picker

* Fix drawer adjust to RTL
2019-02-01 12:22:11 -08:00
yosilevy
e2ff51f425 RTL fixes for raw config editor and card editor -> card preview (#2532)
* RTL fixes for raw config editor and card editor -> card preview

* Fixed missing line break

* Fixed weird vscode line break problems

* Refactored to LitElement + addressed updating RTL only when needed

* Refactor

* Fixed forgotten import

* Reverted to HTMLElement + added RTL check in set hass

* Added conditional update
2019-02-01 09:53:08 -08:00
Paulus Schoutsen
25a579f7ed Fixes (#2643)
* Sort areas alphabetically in device card

* Fix background color of registry editors when using themes

* Fix area/entity reg dialog being disabled after deletion

* Better fix card background

* Warn user when system health component not loaded
2019-02-01 09:40:57 -08:00
yosilevy
ecd33fd93c RTL toast fix (#2646)
* RTL toast fix

* Removed reflectToAttribute
2019-02-01 09:40:45 -08:00
684 changed files with 39615 additions and 55220 deletions

View File

@@ -13,6 +13,10 @@
**Last working Home Assistant release (if known):** **Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:** **Browser and Operating System:**
<!-- <!--

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
8.11.1 12.1

View File

@@ -1,8 +1,9 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"eg2.tslint", "ms-vscode.vscode-typescript-tslint-plugin",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bierner.lit-html" "bierner.lit-html",
] "runem.lit-plugin"
]
} }

View File

@@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
throw Error("latestBuild not defined for babel loader config"); throw Error("latestBuild not defined for babel loader config");
} }
return { return {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.tsx?$/,
use: { use: {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
@@ -12,7 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
require("@babel/preset-env").default, require("@babel/preset-env").default,
{ modules: false }, { modules: false },
], ],
require("@babel/preset-typescript").default, [
require("@babel/preset-typescript").default,
{
jsxPragma: "h",
},
],
].filter(Boolean), ].filter(Boolean),
plugins: [ plugins: [
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
@@ -28,6 +33,14 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
pragma: "h", pragma: "h",
}, },
], ],
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },
],
[
require("@babel/plugin-proposal-class-properties").default,
{ loose: true },
],
], ],
}, },
}, },

51
build-scripts/gulp/app.js Normal file
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,6 @@
const del = require("del");
const gulp = require("gulp");
const config = require("../paths");
gulp.task("clean", () => del([config.root, config.build_dir]));
gulp.task("clean-demo", () => del([config.demo_root, config.build_dir]));

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",
});
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,110 @@
// Gulp task to gather all static files.
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const paths = require("../paths");
const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
const genStaticPath = (staticDir) => (...parts) =>
path.resolve(staticDir, ...parts);
function copyTranslations(staticDir) {
const staticPath = genStaticPath(staticDir);
// Translation output
fs.copySync(
polyPath("build-translations/output"),
staticPath("translations")
);
}
function copyPolyfills(staticDir) {
const staticPath = genStaticPath(staticDir);
// Web Component polyfills and adapters
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"),
staticPath("polyfills/")
);
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js"),
staticPath("polyfills/")
);
copyFileDir(
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
}
function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir);
// Local fonts
fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts"));
}
function compressStatic(staticDir) {
const staticPath = genStaticPath(staticDir);
const fonts = gulp
.src(staticPath("fonts/**/*.ttf"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("fonts")));
const polyfills = gulp
.src(staticPath("polyfills/*.js"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("polyfills")));
const translations = gulp
.src(staticPath("translations/*.json"))
.pipe(zopfli())
.pipe(gulp.dest(staticPath("translations")));
return merge(fonts, polyfills, translations);
}
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
// Basic static files
fs.copySync(polyPath("public"), paths.root);
copyPolyfills(staticDir);
copyFonts(staticDir);
copyTranslations(staticDir);
// Panel assets
copyFileDir(
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
staticPath("panels/calendar/")
);
copyFileDir(
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
);
done();
});
gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => {
// Copy app static files
fs.copySync(polyPath("public"), paths.demo_root);
// Copy demo static files
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
copyPolyfills(paths.demo_static);
copyFonts(paths.demo_static);
copyTranslations(paths.demo_static);
done();
});

View File

@@ -1,7 +1,7 @@
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const config = require("../config"); const paths = require("../paths");
const ICON_PACKAGE_PATH = path.resolve( const ICON_PACKAGE_PATH = path.resolve(
__dirname, __dirname,
@@ -38,13 +38,13 @@ function loadIcon(name) {
function transformXMLtoPolymer(name, xml) { function transformXMLtoPolymer(name, xml) {
const start = xml.indexOf("><path") + 1; const start = xml.indexOf("><path") + 1;
const end = xml.length - start - 6; const end = xml.length - start - 6;
const path = xml.substr(start, end); const pth = xml.substr(start, end);
return `<g id="${name}">${path}</g>`; return `<g id="${name}">${pth}</g>`;
} }
// Given an iconset name and icon names, generate a polymer iconset // Given an iconset name and icon names, generate a polymer iconset
function generateIconset(name, iconNames) { function generateIconset(iconsetName, iconNames) {
const iconDefs = iconNames const iconDefs = Array.from(iconNames)
.map((name) => { .map((name) => {
const iconDef = loadIcon(name); const iconDef = loadIcon(name);
if (!iconDef) { if (!iconDef) {
@@ -53,7 +53,7 @@ function generateIconset(name, iconNames) {
return transformXMLtoPolymer(name, iconDef); return transformXMLtoPolymer(name, iconDef);
}) })
.join(""); .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 // Generate the full MDI iconset
@@ -62,7 +62,9 @@ function genMDIIcons() {
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8") fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
); );
const iconNames = meta.map((iconInfo) => iconInfo.name); 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)); fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
} }
@@ -81,7 +83,7 @@ function mapFiles(startPath, filter, mapFunc) {
} }
// Find all icons used by the project. // Find all icons used by the project.
function findIcons(path, iconsetName) { function findIcons(searchPath, iconsetName) {
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g"); const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
const icons = new Set(); const icons = new Set();
function processFile(filename) { function processFile(filename) {
@@ -93,20 +95,38 @@ function findIcons(path, iconsetName) {
icons.add(match[0].substr(iconsetName.length + 1)); icons.add(match[0].substr(iconsetName.length + 1));
} }
} }
mapFiles(path, ".js", processFile); mapFiles(searchPath, ".js", processFile);
mapFiles(path, ".ts", processFile); mapFiles(searchPath, ".ts", processFile);
return Array.from(icons); return icons;
} }
function genHassIcons() { function genHassIcons() {
const iconNames = findIcons("./src", "hass").concat(BUILT_IN_PANEL_ICONS); const iconNames = findIcons("./src", "hass");
fs.existsSync(OUTPUT_DIR) || fs.mkdirSync(OUTPUT_DIR); 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)); fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
} }
gulp.task("gen-icons-mdi", () => genMDIIcons()); gulp.task("gen-icons-mdi", (done) => {
gulp.task("gen-icons-hass", () => genHassIcons()); genMDIIcons();
gulp.task("gen-icons", ["gen-icons-hass", "gen-icons-mdi"], () => {}); 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 = { module.exports = {
findIcons, findIcons,

View File

@@ -0,0 +1,29 @@
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const config = require("../paths.js");
const swPath = path.resolve(config.root, "service_worker.js");
const writeSW = (content) => fs.outputFileSync(swPath, content.trim() + "\n");
gulp.task("gen-service-worker-dev", (done) => {
writeSW(
`
console.debug('Service worker disabled in development');
`
);
done();
});
gulp.task("gen-service-worker-prod", (done) => {
fs.copySync(
path.resolve(config.output, "service_worker.js"),
path.resolve(config.root, "service_worker.js")
);
done();
});

View File

@@ -0,0 +1,392 @@
const del = require("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const foreach = require("gulp-foreach");
const hash = require("gulp-hash");
const hashFilename = require("gulp-hash-filename");
const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const inDir = "translations";
const workDir = "build-translations";
const fullDir = workDir + "/full";
const coreDir = workDir + "/core";
const outDir = workDir + "/output";
String.prototype.rsplit = function(sep, maxsplit) {
var split = this.split(sep);
return maxsplit
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
: split;
};
// Panel translations which should be split from the core translations. These
// should mirror the fragment definitions in polymer.json, so that we load
// additional resources at equivalent points.
const TRANSLATION_FRAGMENTS = [
"config",
"history",
"logbook",
"mailbox",
"profile",
"shopping-list",
"page-authorize",
"page-onboarding",
];
const tasks = [];
function recursiveFlatten(prefix, data) {
let output = {};
Object.keys(data).forEach(function(key) {
if (typeof data[key] === "object") {
output = Object.assign(
{},
output,
recursiveFlatten(prefix + key + ".", data[key])
);
} else {
output[prefix + key] = data[key];
}
});
return output;
}
function flatten(data) {
return recursiveFlatten("", data);
}
function emptyFilter(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof data[key] === "object") {
newData[key] = emptyFilter(data[key]);
} else {
newData[key] = data[key];
}
}
});
return newData;
}
function recursiveEmpty(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof data[key] === "object") {
newData[key] = recursiveEmpty(data[key]);
} else {
newData[key] = "TRANSLATED";
}
}
});
return newData;
}
/**
* Replace Lokalise key placeholders with their actual values.
*
* We duplicate the behavior of Lokalise here so that placeholders can
* be included in src/translations/en.json, but still be usable while
* developing locally.
*
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
*/
const re_key_reference = /\[%key:([^%]+)%\]/;
function lokalise_transform(data, original) {
const output = {};
Object.entries(data).forEach(([key, value]) => {
if (value instanceof Object) {
output[key] = lokalise_transform(value, original);
} else {
output[key] = value.replace(re_key_reference, (match, key) => {
const replace = key.split("::").reduce((tr, k) => tr[k], original);
if (typeof replace !== "string") {
throw Error(
`Invalid key placeholder ${key} in src/translations/en.json`
);
}
return replace;
});
}
});
return output;
}
let taskName = "clean-translations";
gulp.task(taskName, function() {
return del([`${outDir}/**/*.json`]);
});
tasks.push(taskName);
taskName = "create-test-metadata";
gulp.task(taskName, function(cb) {
fs.writeFile(
workDir + "/testMetadata.json",
JSON.stringify({
test: {
nativeName: "Test",
},
}),
cb
);
});
tasks.push(taskName);
taskName = "create-test-translation";
gulp.task(
taskName,
gulp.series("create-test-metadata", function() {
return gulp
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return recursiveEmpty(data);
})
)
.pipe(rename("test.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
/**
* This task will build a master translation file, to be used as the base for
* all languages. This starts with src/translations/en.json, and replaces all
* Lokalise key placeholders with their target values. Under normal circumstances,
* this will be the same as translations/en.json However, we build it here to
* facilitate both making changes in development mode, and to ensure that the
* project is buildable immediately after merging new translation keys, since
* the Lokalise update to translations/en.json will not happen immediately.
*/
taskName = "build-master-translation";
gulp.task(
taskName,
gulp.series("clean-translations", function() {
return gulp
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return lokalise_transform(data, data);
})
)
.pipe(rename("translationMaster.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-merged-translations";
gulp.task(
taskName,
gulp.series("build-master-translation", function() {
return gulp
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
.pipe(
foreach(function(stream, file) {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-");
const src = [workDir + "/translationMaster.json"];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
if (lang === "test") {
src.push(workDir + "/test.json");
} else {
src.push(inDir + "/" + lang + ".json");
}
}
return gulp
.src(src, { allowEmpty: true })
.pipe(transform((data) => emptyFilter(data)))
.pipe(
merge({
fileName: tr + ".json",
})
)
.pipe(gulp.dest(fullDir));
})
);
})
);
tasks.push(taskName);
const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = "build-translation-fragment-" + fragment;
gulp.task(
taskName,
gulp.series("build-merged-translations", function() {
// Return only the translations for this fragment.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
},
},
}))
)
.pipe(gulp.dest(workDir + "/" + fragment));
})
);
tasks.push(taskName);
splitTasks.push(taskName);
});
taskName = "build-translation-core";
gulp.task(
taskName,
gulp.series("build-merged-translations", function() {
// Remove the fragment translations from the core translation.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
return data;
})
)
.pipe(gulp.dest(coreDir));
})
);
tasks.push(taskName);
splitTasks.push(taskName);
taskName = "build-flattened-translations";
gulp.task(
taskName,
gulp.series(...splitTasks, function() {
// Flatten the split versions of our translations, and move them into outDir
return gulp
.src(
TRANSLATION_FRAGMENTS.map(
(fragment) => workDir + "/" + fragment + "/*.json"
).concat(coreDir + "/*.json"),
{ base: workDir }
)
.pipe(
transform(function(data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(minify())
.pipe(hashFilename())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
filePath.dirname = "";
}
})
)
.pipe(gulp.dest(outDir));
})
);
tasks.push(taskName);
taskName = "build-translation-fingerprints";
gulp.task(
taskName,
gulp.series("build-flattened-translations", function() {
return gulp
.src(outDir + "/**/*.json")
.pipe(
rename({
extname: "",
})
)
.pipe(
hash({
algorithm: "md5",
hashLength: 32,
template: "<%= name %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))
.pipe(
transform(function(data) {
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const [path, _md5] = key.rsplit("-", 1);
// let translation = key;
let translation = path;
const parts = translation.split("/");
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[path] = value;
});
return newData;
})
)
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
taskName = "build-translations";
gulp.task(
taskName,
gulp.series("build-translation-fingerprints", function() {
return gulp
.src(
[
"src/translations/translationMetadata.json",
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function(data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
})
);
tasks.push(taskName);
module.exports = tasks;

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

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

@@ -0,0 +1,222 @@
const webpack = require("webpack");
const fs = require("fs");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const { babelLoaderConfig } = require("./babel.js");
let version = fs
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
.match(/\d{8}\.\d+/);
if (!version) {
throw Error("Version not found");
}
version = version[0];
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
const genDevTool = (isProdBuild) =>
isProdBuild ? "cheap-source-map" : "inline-cheap-module-source-map";
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
};
const genChunkFilename = (isProdBuild, isStatsBuild) =>
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
};
const cssLoader = {
test: /\.css$/,
use: "raw-loader",
};
const htmlLoader = {
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
};
const plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Color.js is bloated, it contains all color definitions for all material color sets.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/paper-styles\/color\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore roboto pointing at CDN. We use local font-roboto-local.
new webpack.NormalModuleReplacementPlugin(
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
];
const optimization = (latestBuild) => ({
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
terserOptions: {
safari10: true,
ecma: latestBuild ? undefined : 5,
},
}),
],
});
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
const isCI = process.env.CI === "true";
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata["translations"]["en"]["fingerprints"];
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
] = `build-translations/output/${key}.json`;
});
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",
core: "./src/entrypoints/core.ts",
compatibility: "./src/entrypoints/compatibility.ts",
"custom-panel": "./src/entrypoints/custom-panel.ts",
"hass-icons": "./src/entrypoints/hass-icons.ts",
};
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry,
module: {
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(version),
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}),
latestBuild &&
new WorkboxPlugin.InjectManifest({
swSrc: "./src/entrypoints/service-worker-hass.js",
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [/\.js$/],
templatedURLs: {
...workBoxTranslationsTemplatedURLs,
"/static/icons/favicon-192x192.png":
"public/icons/favicon-192x192.png",
"/static/fonts/roboto/Roboto-Light.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf",
"/static/fonts/roboto/Roboto-Medium.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf",
"/static/fonts/roboto/Roboto-Regular.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf",
"/static/fonts/roboto/Roboto-Bold.ttf":
"node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf",
},
}),
].filter(Boolean),
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: latestBuild ? paths.output : paths.output_es5,
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
},
resolve,
};
};
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry: {
main: "./demo/src/entrypoint.ts",
compatibility: "./src/entrypoints/compatibility.ts",
},
module: {
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
},
optimization: optimization(latestBuild),
plugins: [
new ManifestPlugin(),
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: genFilename(isProdBuild),
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.demo_root,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
},
};
};
module.exports = {
resolve,
plugins,
optimization,
createAppConfig,
createDemoConfig,
};

View File

@@ -1,29 +0,0 @@
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
module.exports.minimizer = [
// Took options from Polymer build tool
// https://github.com/Polymer/tools/blob/master/packages/build/src/js-transform.ts
new BabelMinifyPlugin(
{
// Disable the minify-constant-folding plugin because it has a bug relating
// to invalid substitution of constant values into export specifiers:
// https://github.com/babel/minify/issues/820
evaluate: false,
// TODO(aomarks) Find out why we disabled this plugin.
simplifyComparisons: false,
// Prevent removal of things that babel thinks are unreachable, but sometimes
// gets wrong: https://github.com/Polymer/tools/issues/724
deadcode: false,
// Disable the simplify plugin because it can eat some statements preceeding
// loops. https://github.com/babel/minify/issues/824
simplify: false,
// This is breaking ES6 output. https://github.com/Polymer/tools/issues/261
mangle: false,
},
{}
),
];

View File

View File

@@ -4,16 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
OUTPUT_DIR=dist ./node_modules/.bin/gulp build-demo
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

View File

@@ -4,12 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/../.."
node script/gen-icons.js ./node_modules/.bin/gulp develop-demo
cd ..
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
cd demo
../node_modules/.bin/webpack-dev-server

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

11
demo/script/size_stats Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
# Analyze stats
# Stop on errors
set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json dist
rm compilation-stats.json

File diff suppressed because it is too large Load Diff

View File

@@ -528,7 +528,7 @@ export const demoLovelaceArsaboo: () => LovelaceConfig = () => ({
{ {
type: "iframe", type: "iframe",
aspect_ratio: "90%", aspect_ratio: "90%",
url: "https://embed.windy.com/embed2.html?rain,32.487,-84.023,5", url: "https://embed.windy.com/embed2.html?lat=32.487&lon=-84.023&zoom=5&level=surface&overlay=rain&menu=&message=&marker=&calendar=&pressure=&type=map&location=coordinates&detail=&detailLat=32.487&detailLon=--84.023&metricWind=default&metricTemp=default&radarRange=-1",
}, },
{ {
type: "entities", type: "entities",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,11 @@ import {
} from "lit-element"; } from "lit-element";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import "@polymer/paper-icon-button"; import "@polymer/paper-icon-button";
import "@polymer/paper-button"; import "@material/mwc-button";
import "@polymer/paper-spinner/paper-spinner-lite"; import "@polymer/paper-spinner/paper-spinner-lite";
import "../../../src/components/ha-card"; 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 { LovelaceCard, Lovelace } from "../../../src/panels/lovelace/types";
import { LovelaceCardConfig } from "../../../src/data/lovelace"; import { LovelaceCardConfig } from "../../../src/data/lovelace";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -47,12 +49,10 @@ export class HADemoCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card> <ha-card>
<div class="picker"> <div class="picker">
<paper-icon-button <ha-paper-icon-button-prev
@click=${this._prevConfig} @click=${this._prevConfig}
icon="hass:chevron-right"
style="transform: rotate(180deg)"
.disabled=${this._switching} .disabled=${this._switching}
></paper-icon-button> ></ha-paper-icon-button-prev>
<div> <div>
${this._switching ${this._switching
? html` ? html`
@@ -73,11 +73,10 @@ export class HADemoCard extends LitElement implements LovelaceCard {
"" ""
)} )}
</div> </div>
<paper-icon-button <ha-paper-icon-button-next
@click=${this._nextConfig} @click=${this._nextConfig}
icon="hass:chevron-right"
.disabled=${this._switching} .disabled=${this._switching}
></paper-icon-button> ></ha-paper-icon-button-next>
</div> </div>
<div class="content"> <div class="content">
Welcome home! You've reached the Home Assistant demo where we showcase Welcome home! You've reached the Home Assistant demo where we showcase
@@ -85,7 +84,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="actions"> <div class="actions">
<a href="https://www.home-assistant.io" target="_blank"> <a href="https://www.home-assistant.io" target="_blank">
<paper-button>Learn more about Home Assistant</paper-button> <mwc-button>Learn more about Home Assistant</mwc-button>
</a> </a>
</div> </div>
</ha-card> </ha-card>
@@ -146,12 +145,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
} }
.actions { .actions {
padding-left: 5px; padding-left: 8px;
}
.actions paper-button {
color: var(--primary-color);
font-weight: 500;
} }
`, `,
]; ];

View File

@@ -1,4 +1,4 @@
import { HomeAssistantAppEl } from "../../src/layouts/app/home-assistant"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import { import {
provideHass, provideHass,
MockHomeAssistant, MockHomeAssistant,
@@ -15,9 +15,10 @@ import { mockTemplate } from "./stubs/template";
import { mockEvents } from "./stubs/events"; import { mockEvents } from "./stubs/events";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { mockFrontend } from "./stubs/frontend";
class HaDemo extends HomeAssistantAppEl { class HaDemo extends HomeAssistantAppEl {
protected async _handleConnProm() { protected async _initialize() {
const initial: Partial<MockHomeAssistant> = { const initial: Partial<MockHomeAssistant> = {
panelUrl: (this as any).panelUrl, panelUrl: (this as any).panelUrl,
// Override updateHass so that the correct hass lifecycle methods are called // Override updateHass so that the correct hass lifecycle methods are called
@@ -25,7 +26,7 @@ class HaDemo extends HomeAssistantAppEl {
this._updateHass(hassUpdate), this._updateHass(hassUpdate),
}; };
const hass = provideHass(this, initial); const hass = (this.hass = provideHass(this, initial));
mockLovelace(hass); mockLovelace(hass);
mockAuth(hass); mockAuth(hass);
mockTranslations(hass); mockTranslations(hass);
@@ -35,6 +36,7 @@ class HaDemo extends HomeAssistantAppEl {
mockTemplate(hass); mockTemplate(hass);
mockEvents(hass); mockEvents(hass);
mockMediaPlayer(hass); mockMediaPlayer(hass);
mockFrontend(hass);
selectedDemoConfig.then((conf) => { selectedDemoConfig.then((conf) => {
hass.addEntities(conf.entities()); hass.addEntities(conf.entities());
if (conf.theme) { if (conf.theme) {

View File

@@ -74,9 +74,6 @@
content="https://www.home-assistant.io/images/default-social.png" content="https://www.home-assistant.io/images/default-social.png"
/> />
<title>Home Assistant Demo</title> <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> <style>
body { body {
font-family: Roboto, Noto, sans-serif; font-family: Roboto, Noto, sans-serif;
@@ -96,7 +93,23 @@
</style> </style>
</head> </head>
<body> <body>
<ha-demo><div id="ha-init-skeleton"></div></ha-demo> <div id="ha-init-skeleton"></div>
<ha-demo></ha-demo>
<%= renderTemplate('_js_base') %>
<script type="module" src="<%= latestDemoJS %>"></script>
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
_ls("/static/polyfills/custom-elements-es5-adapter.js");
_ls("<%= es5Compatibility %>");
_ls("<%= es5DemoJS %>");
}
})();
</script>
<script> <script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]]; var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function(d, t) { (function(d, t) {

View File

@@ -0,0 +1,7 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
};

View File

@@ -3,7 +3,6 @@ import "../custom-cards/ha-demo-card";
// tslint:disable-next-line // tslint:disable-next-line
import { HADemoCard } from "../custom-cards/ha-demo-card"; import { HADemoCard } from "../custom-cards/ha-demo-card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { HUIView } from "../../../src/panels/lovelace/hui-view";
import { selectedDemoConfig } from "../configs/demo-configs"; import { selectedDemoConfig } from "../configs/demo-configs";
export const mockLovelace = (hass: MockHomeAssistant) => { export const mockLovelace = (hass: MockHomeAssistant) => {
@@ -16,13 +15,17 @@ export const mockLovelace = (hass: MockHomeAssistant) => {
hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/config/save", () => Promise.resolve());
}; };
// Patch HUI-VIEW to make the lovelace object available to the demo card customElements.whenDefined("hui-view").then(() => {
const oldCreateCard = HUIView.prototype.createCardElement; // tslint:disable-next-line
const HUIView = customElements.get("hui-view");
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView.prototype.createCardElement;
HUIView.prototype.createCardElement = function(config) { HUIView.prototype.createCardElement = function(config) {
const el = oldCreateCard.call(this, config); const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") { if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace; (el as HADemoCard).lovelace = this.lovelace;
} }
return el; return el;
}; };
});

View File

@@ -1,100 +1,13 @@
const path = require("path"); const { createDemoConfig } = require("../build-scripts/webpack.js");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const { minimizer } = require("../config/babel.js");
const isProd = process.env.NODE_ENV === "production"; // This file exists because we haven't migrated the stats script yet
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = "/";
const isProdBuild = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1";
const latestBuild = false; const latestBuild = false;
module.exports = { module.exports = createDemoConfig({
mode: isProd ? "production" : "development", isProdBuild,
// Disabled in prod while we make Home Assistant able to serve the right files. isStatsBuild,
// Was source-map latestBuild,
devtool: isProd ? "none" : "inline-source-map", });
entry: {
main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.js",
},
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: {
minimizer,
},
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProd ? "production" : "development"
),
}),
new CopyWebpackPlugin([
"public",
"../node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/@polymer/font-roboto-local/fonts",
to: "static/fonts",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
isProd &&
new WorkboxPlugin.GenerateSW({
swDest: "service_worker_es5.js",
importWorkboxFrom: "local",
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};

View File

@@ -2,6 +2,19 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; import "../components/demo-cards";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
const ENTITIES = [
getEntity("sensor", "brightness", "12", {}),
getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%",
}),
getEntity("sensor", "outside_temperature", "15.6", {
unit_of_measurement: "°C",
}),
];
const CONFIGS = [ const CONFIGS = [
{ {
@@ -66,7 +79,7 @@ const CONFIGS = [
class DemoGaugeEntity extends PolymerElement { class DemoGaugeEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards configs="[[_configs]]"></demo-cards> <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`; `;
} }
@@ -78,6 +91,12 @@ class DemoGaugeEntity extends PolymerElement {
}, },
}; };
} }
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
} }
customElements.define("demo-hui-gauge-card", DemoGaugeEntity); customElements.define("demo-hui-gauge-card", DemoGaugeEntity);

View File

@@ -2,6 +2,17 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; import "../components/demo-cards";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
const ENTITIES = [
getEntity("light", "kitchen_lights", "on", {
friendly_name: "Kitchen Lights",
}),
getEntity("light", "bed_light", "off", {
friendly_name: "Bed Light",
}),
];
const CONFIGS = [ const CONFIGS = [
{ {
@@ -10,6 +21,8 @@ const CONFIGS = [
- type: picture-entity - type: picture-entity
image: /images/kitchen.png image: /images/kitchen.png
entity: light.kitchen_lights entity: light.kitchen_lights
tap_action:
action: toggle
`, `,
}, },
{ {
@@ -18,6 +31,8 @@ const CONFIGS = [
- type: picture-entity - type: picture-entity
image: /images/bed.png image: /images/bed.png
entity: light.bed_light entity: light.bed_light
tap_action:
action: toggle
`, `,
}, },
{ {
@@ -68,7 +83,7 @@ const CONFIGS = [
class DemoPicEntity extends PolymerElement { class DemoPicEntity extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards configs="[[_configs]]"></demo-cards> <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`; `;
} }
@@ -80,6 +95,12 @@ class DemoPicEntity extends PolymerElement {
}, },
}; };
} }
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
} }
customElements.define("demo-hui-picture-entity-card", DemoPicEntity); customElements.define("demo-hui-picture-entity-card", DemoPicEntity);

View File

@@ -2,6 +2,25 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/demo-cards"; import "../components/demo-cards";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
const ENTITIES = [
getEntity("switch", "decorative_lights", "on", {
friendly_name: "Decorative Lights",
}),
getEntity("light", "ceiling_lights", "on", {
friendly_name: "Ceiling Lights",
}),
getEntity("binary_sensor", "movement_backyard", "on", {
friendly_name: "Movement Backyard",
device_class: "moving",
}),
getEntity("binary_sensor", "basement_floor_wet", "off", {
friendly_name: "Basement Floor Wet",
device_class: "moisture",
}),
];
const CONFIGS = [ const CONFIGS = [
{ {
@@ -105,7 +124,7 @@ const CONFIGS = [
class DemoPicGlance extends PolymerElement { class DemoPicGlance extends PolymerElement {
static get template() { static get template() {
return html` return html`
<demo-cards configs="[[_configs]]"></demo-cards> <demo-cards id="demos" configs="[[_configs]]"></demo-cards>
`; `;
} }
@@ -117,6 +136,12 @@ class DemoPicGlance extends PolymerElement {
}, },
}; };
} }
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
hass.addEntities(ENTITIES);
}
} }
customElements.define("demo-hui-picture-glance-card", DemoPicGlance); customElements.define("demo-hui-picture-glance-card", DemoPicGlance);

View File

@@ -1,5 +1,5 @@
import { html, LitElement, TemplateResult } from "lit-element"; import { html, LitElement, TemplateResult } from "lit-element";
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive"; import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
@@ -11,13 +11,13 @@ export class DemoUtilLongPress extends LitElement {
${[1, 2, 3].map( ${[1, 2, 3].map(
() => html` () => html`
<ha-card> <ha-card>
<paper-button <mwc-button
@ha-click="${this._handleTap}" @ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}" @ha-hold="${this._handleHold}"
.longPress="${longPress()}" .longPress="${longPress()}"
> >
(long) press me! (long) press me!
</paper-button> </mwc-button>
<textarea></textarea> <textarea></textarea>
@@ -60,11 +60,6 @@ export class DemoUtilLongPress extends LitElement {
margin-bottom: 16px; margin-bottom: 16px;
} }
paper-button {
font-weight: bold;
color: var(--primary-color);
}
textarea { textarea {
height: 50px; height: 50px;
} }

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-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-icon-button/paper-icon-button"; 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/managers/notification-manager"; import "../../src/managers/notification-manager";
import "../../src/components/ha-card";
const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im); const DEMOS = require.context("./demos", true, /^(.*\.(ts$))[^.]*$/im);
@@ -38,13 +38,13 @@ class HaGallery extends PolymerElement {
align-items: start; align-items: start;
} }
.pickers paper-card { .pickers ha-card {
width: 400px; width: 400px;
display: block; display: block;
margin: 16px 8px; margin: 16px 8px;
} }
.pickers paper-card:last-child { .pickers ha-card:last-child {
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -79,7 +79,7 @@ class HaGallery extends PolymerElement {
<div id='demo'></div> <div id='demo'></div>
<template is='dom-if' if='[[!_demo]]'> <template is='dom-if' if='[[!_demo]]'>
<div class='pickers'> <div class='pickers'>
<paper-card heading="Lovelace card demos"> <ha-card header="Lovelace card demos">
<div class='card-content intro'> <div class='card-content intro'>
<p> <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. 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> </paper-item>
</a> </a>
</template> </template>
</paper-card> </ha-card>
<paper-card heading="More Info demos"> <ha-card header="More Info demos">
<div class='card-content intro'> <div class='card-content intro'>
<p> <p>
More info screens show up when an entity is clicked. More info screens show up when an entity is clicked.
@@ -117,9 +117,9 @@ class HaGallery extends PolymerElement {
</paper-item> </paper-item>
</a> </a>
</template> </template>
</paper-card> </ha-card>
<paper-card heading="Util demos"> <ha-card header="Util demos">
<div class='card-content intro'> <div class='card-content intro'>
<p> <p>
Test pages for our utility functions. Test pages for our utility functions.
@@ -133,7 +133,7 @@ class HaGallery extends PolymerElement {
</paper-item> </paper-item>
</a> </a>
</template> </template>
</paper-card> </ha-card>
</div> </div>
</template> </template>
</div> </div>

View File

@@ -1,12 +1,13 @@
const path = require("path"); const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../build-scripts/babel.js");
const { minimizer } = require("../config/babel.js"); const webpackBase = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js"; const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist"); const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/"; const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = true;
module.exports = { module.exports = {
mode: isProd ? "production" : "development", mode: isProd ? "production" : "development",
@@ -16,7 +17,7 @@ module.exports = {
entry: "./src/entrypoint.js", entry: "./src/entrypoint.js",
module: { module: {
rules: [ rules: [
babelLoaderConfig({ latestBuild: true }), babelLoaderConfig({ latestBuild }),
{ {
test: /\.css$/, test: /\.css$/,
use: "raw-loader", use: "raw-loader",
@@ -32,9 +33,7 @@ module.exports = {
}, },
], ],
}, },
optimization: { optimization: webpackBase.optimization(latestBuild),
minimizer,
},
plugins: [ plugins: [
new CopyWebpackPlugin([ new CopyWebpackPlugin([
"public", "public",
@@ -53,19 +52,8 @@ module.exports = {
to: "static/images/leaflet/", to: "static/images/leaflet/",
}, },
]), ]),
isProd &&
new UglifyJsPlugin({
extractComments: true,
sourceMap: true,
uglifyOptions: {
// Disabling because it broke output
mangle: false,
},
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: webpackBase.resolve,
extensions: [".ts", ".js", ".json"],
},
output: { output: {
filename: "[name].js", filename: "[name].js",
chunkFilename: chunkFilename, chunkFilename: chunkFilename,

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,299 +0,0 @@
const path = require("path");
const gulp = require("gulp");
const foreach = require("gulp-foreach");
const hash = require("gulp-hash");
const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const isDemo = process.env.DEMO === "1";
const inDir = "translations";
const workDir = "build-translations";
const fullDir = workDir + "/full";
const coreDir = workDir + "/core";
const outDir = workDir + "/output";
// Panel translations which should be split from the core translations. These
// should mirror the fragment definitions in polymer.json, so that we load
// additional resources at equivalent points.
const TRANSLATION_FRAGMENTS = [
"config",
"history",
"logbook",
"mailbox",
"profile",
"shopping-list",
"page-authorize",
"page-onboarding",
];
const tasks = [];
function recursiveFlatten(prefix, data) {
let output = {};
Object.keys(data).forEach(function(key) {
if (typeof data[key] === "object") {
output = Object.assign(
{},
output,
recursiveFlatten(prefix + key + ".", data[key])
);
} else {
output[prefix + key] = data[key];
}
});
return output;
}
function flatten(data) {
return recursiveFlatten("", data);
}
function emptyFilter(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof data[key] === "object") {
newData[key] = emptyFilter(data[key]);
} else {
newData[key] = data[key];
}
}
});
return newData;
}
/**
* Replace Lokalise key placeholders with their actual values.
*
* We duplicate the behavior of Lokalise here so that placeholders can
* be included in src/translations/en.json, but still be usable while
* developing locally.
*
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
*/
const re_key_reference = /\[%key:([^%]+)%\]/;
function lokalise_transform(data, original) {
const output = {};
Object.entries(data).forEach(([key, value]) => {
if (value instanceof Object) {
output[key] = lokalise_transform(value, original);
} else {
output[key] = value.replace(re_key_reference, (match, key) => {
const replace = key.split("::").reduce((tr, k) => tr[k], original);
if (typeof replace !== "string") {
throw Error(
`Invalid key placeholder ${key} in src/translations/en.json`
);
}
return replace;
});
}
});
return output;
}
/**
* This task will build a master translation file, to be used as the base for
* all languages. This starts with src/translations/en.json, and replaces all
* Lokalise key placeholders with their target values. Under normal circumstances,
* this will be the same as translations/en.json However, we build it here to
* facilitate both making changes in development mode, and to ensure that the
* project is buildable immediately after merging new translation keys, since
* the Lokalise update to translations/en.json will not happen immediately.
*/
let taskName = "build-master-translation";
gulp.task(taskName, function() {
return gulp
.src("src/translations/en.json")
.pipe(
transform(function(data, file) {
return lokalise_transform(data, data);
})
)
.pipe(rename("translationMaster.json"))
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);
taskName = "build-merged-translations";
gulp.task(taskName, ["build-master-translation"], function() {
return gulp.src(inDir + "/*.json").pipe(
foreach(function(stream, file) {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-");
const src = [workDir + "/translationMaster.json"];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
src.push(inDir + "/" + lang + ".json");
}
return gulp
.src(src)
.pipe(transform((data) => emptyFilter(data)))
.pipe(
merge({
fileName: tr + ".json",
})
)
.pipe(gulp.dest(fullDir));
})
);
});
tasks.push(taskName);
const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = "build-translation-fragment-" + fragment;
gulp.task(taskName, ["build-merged-translations"], function() {
// Return only the translations for this fragment.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
},
},
}))
)
.pipe(gulp.dest(workDir + "/" + fragment));
});
tasks.push(taskName);
splitTasks.push(taskName);
});
taskName = "build-translation-core";
gulp.task(taskName, ["build-merged-translations"], function() {
// Remove the fragment translations from the core translation.
return gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
return data;
})
)
.pipe(gulp.dest(coreDir));
});
tasks.push(taskName);
splitTasks.push(taskName);
taskName = "build-flattened-translations";
gulp.task(taskName, splitTasks, function() {
// Flatten the split versions of our translations, and move them into outDir
return gulp
.src(
TRANSLATION_FRAGMENTS.map(
(fragment) => workDir + "/" + fragment + "/*.json"
).concat(coreDir + "/*.json"),
{ base: workDir }
)
.pipe(
transform(function(data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(minify())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
filePath.dirname = "";
}
})
)
.pipe(gulp.dest(outDir));
});
tasks.push(taskName);
taskName = "build-translation-fingerprints";
gulp.task(taskName, ["build-flattened-translations"], function() {
return gulp
.src(outDir + "/**/*.json")
.pipe(
rename({
extname: "",
})
)
.pipe(
hash({
algorithm: "md5",
hashLength: 32,
template: isDemo ? "<%= name %>.json" : "<%= name %>-<%= hash %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))
.pipe(
transform(function(data) {
// After generating fingerprints of our translation files, consolidate
// all translation fragment fingerprints under the translation name key
const newData = {};
Object.entries(data).forEach(([key, value]) => {
const parts = key.split("/");
let translation = key;
if (parts.length === 2) {
translation = parts[1];
}
if (!(translation in newData)) {
newData[translation] = {
fingerprints: {},
};
}
newData[translation].fingerprints[key] = value;
});
return newData;
})
)
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);
taskName = "build-translations";
gulp.task(taskName, ["build-translation-fingerprints"], function() {
return gulp
.src([
"src/translations/translationMetadata.json",
workDir + "/translationFingerprints.json",
])
.pipe(merge({}))
.pipe(
transform(function(data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
tasks.push(taskName);
module.exports = tasks;

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, findIcons,
generateIconset, generateIconset,
genMDIIcons, genMDIIcons,
} = require("../../gulp/tasks/gen-icons.js"); } = require("../../build-scripts/gulp/gen-icons.js");
const MENU_BUTTON_ICON = "menu";
function genHassioIcons() { 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)); 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

@@ -1,6 +1,6 @@
import "web-animations-js/web-animations-next-lite.min"; import "web-animations-js/web-animations-next-lite.min";
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@@ -9,7 +9,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonAudio extends EventsMixin(PolymerElement) { class HassioAddonAudio extends EventsMixin(PolymerElement) {
static get template() { static get template() {
@@ -65,7 +65,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
</paper-dropdown-menu> </paper-dropdown-menu>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<paper-button on-click="_saveSettings">Save</paper-button> <mwc-button on-click="_saveSettings">Save</mwc-button>
</div> </div>
</paper-card> </paper-card>
`; `;

View File

@@ -1,5 +1,5 @@
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea"; import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -50,8 +50,8 @@ class HassioAddonConfig extends PolymerElement {
data="[[resetData]]" data="[[resetData]]"
>Reset to defaults</ha-call-api-button >Reset to defaults</ha-call-api-button
> >
<paper-button on-click="saveTapped" disabled="[[!configParsed]]" <mwc-button on-click="saveTapped" disabled="[[!configParsed]]"
>Save</paper-button >Save</mwc-button
> >
</div> </div>
</paper-card> </paper-card>

View File

@@ -1,6 +1,7 @@
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-toggle-button/paper-toggle-button"; import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -9,55 +10,62 @@ import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style"; 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"; import "../components/hassio-card-content";
const PERMIS_DESC = { const PERMIS_DESC = {
rating: { rating: {
title: "Addon Security Rating", title: "Add-on Security Rating",
description: 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: { host_network: {
title: "Host Network", title: "Host Network",
description: 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: { homeassistant_api: {
title: "Home Assistant API Access", title: "Home Assistant API Access",
description: 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: { full_access: {
title: "Full Hardware Access", title: "Full Hardware Access",
description: 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: { hassio_api: {
title: "Hass.io API Access", title: "Hass.io API Access",
description: 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: { docker_api: {
title: "Full Docker Access", title: "Full Docker Access",
description: 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: { host_pid: {
title: "Host Processes Namespace", title: "Host Processes Namespace",
description: 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: { apparmor: {
title: "AppArmor", title: "AppArmor",
description: 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: { auth_api: {
title: "Home Assistant Authentication", title: "Home Assistant Authentication",
description: description:
"An addon can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.", "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
}, },
}; };
@@ -77,7 +85,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
color: white; color: white;
--paper-card-header-color: white; --paper-card-header-color: white;
} }
paper-card.warning paper-button { paper-card.warning mwc-button {
color: white !important; color: white !important;
} }
.warning { .warning {
@@ -102,10 +110,18 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin: 16px 0; margin: 16px 0;
display: block; display: block;
} }
.state {
display: flex;
margin: 8px 0;
}
.state div { .state div {
width: 150px; width: 180px;
display: inline-block; display: inline-block;
} }
.state iron-icon {
width: 16px;
color: var(--secondary-text-color);
}
paper-toggle-button { paper-toggle-button {
display: inline; display: inline;
} }
@@ -149,6 +165,9 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
margin-right: 4px; margin-right: 4px;
--iron-icon-height: 45px; --iron-icon-height: 45px;
} }
.protection-enable mwc-button {
--mdc-theme-primary: white;
}
</style> </style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]"> <template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
@@ -161,20 +180,38 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
icon="hassio:arrow-up-bold-circle" icon="hassio:arrow-up-bold-circle"
icon-class="update" icon-class="update"
></hassio-card-content> ></hassio-card-content>
<template is="dom-if" if="[[!addon.available]]">
<p>This update is no longer compatible with your system.</p>
</template>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button <ha-call-api-button
hass="[[hass]]" hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/update" 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]]"> <template is="dom-if" if="[[addon.changelog]]">
<paper-button on-click="openChangelog">Changelog</paper-button> <mwc-button on-click="openChangelog">Changelog</mwc-button>
</template> </template>
</div> </div>
</paper-card> </paper-card>
</template> </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> <paper-card>
<div class="card-content"> <div class="card-content">
<div class="addon-header"> <div class="addon-header">
@@ -213,22 +250,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
<img src="/api/hassio/addons/[[addonSlug]]/logo" /> <img src="/api/hassio/addons/[[addonSlug]]/logo" />
</a> </a>
</template> </template>
<template is="dom-if" if="[[!addon.protected]]">
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this addon is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this addon.
</div>
<div class="card-actions">
<paper-button on-click="protectionToggled">Enable Protection mode</paper-button>
</div>
</div>
</paper-card>
</template>
<div class="security"> <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 <ha-label-badge
class$="[[computeSecurityClassName(addon.rating)]]" class$="[[computeSecurityClassName(addon.rating)]]"
on-click="showMoreInfo" on-click="showMoreInfo"
@@ -250,7 +272,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
<ha-label-badge <ha-label-badge
on-click="showMoreInfo" on-click="showMoreInfo"
id="full_access" id="full_access"
icon="hassio:chip" icon="hassio:chip"
label="hardware" label="hardware"
description="" description=""
></ha-label-badge> ></ha-label-badge>
@@ -298,7 +320,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
id="apparmor" id="apparmor"
icon="hassio:shield" icon="hassio:shield"
label="apparmor" label="apparmor"
description="[[addon.apparmor]]" description=""
></ha-label-badge> ></ha-label-badge>
</template> </template>
<template is="dom-if" if="[[addon.auth_api]]"> <template is="dom-if" if="[[addon.auth_api]]">
@@ -310,6 +332,15 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
description="" description=""
></ha-label-badge> ></ha-label-badge>
</template> </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> </div>
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<div class="state"> <div class="state">
@@ -326,8 +357,27 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
checked="[[addon.auto_update]]" checked="[[addon.auto_update]]"
></paper-toggle-button> ></paper-toggle-button>
</div> </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 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 <paper-toggle-button
on-change="protectionToggled" on-change="protectionToggled"
checked="[[addon.protected]]" checked="[[addon.protected]]"
@@ -337,8 +387,8 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</div> </div>
<div class="card-actions"> <div class="card-actions">
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<paper-button class="warning" on-click="_unistallClicked" <mwc-button class="warning" on-click="_unistallClicked"
>Uninstall</paper-button >Uninstall</mwc-button
> >
<template is="dom-if" if="[[addon.build]]"> <template is="dom-if" if="[[addon.build]]">
<ha-call-api-button <ha-call-api-button
@@ -371,20 +421,30 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</template> </template>
<template <template
is="dom-if" is="dom-if"
if="[[computeShowWebUI(addon.webui, isRunning)]]" if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
> >
<a <a
href="[[pathWebui(addon.webui)]]" href="[[pathWebui(addon.webui)]]"
tabindex="-1" tabindex="-1"
target="_blank" target="_blank"
class="right" class="right"
><paper-button>Open web UI</paper-button></a ><mwc-button>Open web UI</mwc-button></a
> >
</template> </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>
<template is="dom-if" if="[[!addon.version]]"> <template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]"> <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> </template>
<ha-call-api-button <ha-call-api-button
disabled="[[!addon.available]]" disabled="[[!addon.available]]"
@@ -448,8 +508,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
return webui && webui.replace("[HOST]", document.location.hostname); return webui && webui.replace("[HOST]", document.location.hostname);
} }
computeShowWebUI(webui, isRunning) { computeShowWebUI(ingress, webui, isRunning) {
return webui && isRunning; return !ingress && webui && isRunning;
}
openIngress() {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
computeShowIngressUI(ingress, isRunning) {
return ingress && isRunning;
} }
computeStartOnBoot(state) { computeStartOnBoot(state) {
@@ -482,9 +550,14 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
this.set("addon.protected", !this.addon.protected); 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) { showMoreInfo(e) {
const id = e.target.getAttribute("id"); const id = e.target.getAttribute("id");
this.fire("hassio-markdown-dialog", { showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title, title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description, content: PERMIS_DESC[id].description,
}); });
@@ -495,7 +568,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`) .callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then((resp) => resp, () => "Error getting changelog") .then((resp) => resp, () => "Error getting changelog")
.then((content) => { .then((content) => {
this.fire("hassio-markdown-dialog", { showHassioMarkdownDialog(this, {
title: "Changelog", title: "Changelog",
content: content, content: content,
}); });
@@ -526,5 +599,14 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
this.fire("hass-api-called", eventData); 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); customElements.define("hassio-addon-info", HassioAddonInfo);

View File

@@ -1,4 +1,4 @@
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -24,7 +24,7 @@ class HassioAddonLogs extends PolymerElement {
<paper-card heading="Log"> <paper-card heading="Log">
<div class="card-content" id="content"></div> <div class="card-content" id="content"></div>
<div class="card-actions"> <div class="card-actions">
<paper-button on-click="refresh">Refresh</paper-button> <mwc-button on-click="refresh">Refresh</mwc-button>
</div> </div>
</paper-card> </paper-card>
`; `;

View File

@@ -5,7 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonNetwork extends EventsMixin(PolymerElement) { class HassioAddonNetwork extends EventsMixin(PolymerElement) {
static get template() { static get template() {
@@ -37,16 +37,19 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
<tr> <tr>
<th>Container</th> <th>Container</th>
<th>Host</th> <th>Host</th>
<th>Description</th>
</tr> </tr>
<template is="dom-repeat" items="[[config]]"> <template is="dom-repeat" items="[[config]]">
<tr> <tr>
<td>[[item.container]]</td> <td>[[item.container]]</td>
<td> <td>
<paper-input <paper-input
placeholder="disabled"
value="{{item.host}}" value="{{item.host}}"
no-label-float="" no-label-float=""
></paper-input> ></paper-input>
</td> </td>
<td>[[item.description]]</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
@@ -60,7 +63,7 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
data="[[resetData]]" data="[[resetData]]"
>Reset to defaults</ha-call-api-button >Reset to defaults</ha-call-api-button
> >
<paper-button on-click="saveTapped">Save</paper-button> <mwc-button on-click="saveTapped">Save</mwc-button>
</div> </div>
</paper-card> </paper-card>
`; `;
@@ -89,9 +92,11 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
if (!addon) return; if (!addon) return;
const network = addon.network || {}; const network = addon.network || {};
const description = addon.network_description || {};
const items = Object.keys(network).map((key) => ({ const items = Object.keys(network).map((key) => ({
container: key, container: key,
host: network[key], host: network[key],
description: description[key],
})); }));
this.config = items.sort(function(el1, el2) { this.config = items.sort(function(el1, el2) {
return el1.host - el2.host; 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-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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-audio";
import "./hassio-addon-config"; import "./hassio-addon-config";
import "./hassio-addon-info"; import "./hassio-addon-info";
@@ -18,7 +14,7 @@ import "./hassio-addon-network";
class HassioAddonView extends PolymerElement { class HassioAddonView extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style>
:host { :host {
color: var(--primary-text-color); color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color); --paper-card-header-color: var(--primary-text-color);
@@ -51,39 +47,19 @@ class HassioAddonView extends PolymerElement {
} }
} }
</style> </style>
<app-route <hass-subpage header="Hass.io: add-on details" hassio>
route="[[route]]"
pattern="/addon/:slug"
data="{{routeData}}"
active="{{routeMatches}}"
></app-route>
<app-header-layout has-scrolling-region="">
<app-header fixed="" slot="header">
<app-toolbar>
<ha-menu-button
hassio
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></ha-menu-button>
<paper-icon-button
icon="hassio:arrow-left"
on-click="backTapped"
></paper-icon-button>
<div main-title="">Hass.io: add-on details</div>
</app-toolbar>
</app-header>
<div class="content"> <div class="content">
<hassio-addon-info <hassio-addon-info
hass="[[hass]]" hass="[[hass]]"
addon="[[addon]]" addon="[[addon]]"
addon-slug="[[routeData.slug]]" addon-slug="[[addonSlug]]"
></hassio-addon-info> ></hassio-addon-info>
<template is="dom-if" if="[[addon.version]]"> <template is="dom-if" if="[[addon.version]]">
<hassio-addon-config <hassio-addon-config
hass="[[hass]]" hass="[[hass]]"
addon="[[addon]]" addon="[[addon]]"
addon-slug="[[routeData.slug]]" addon-slug="[[addonSlug]]"
></hassio-addon-config> ></hassio-addon-config>
<template is="dom-if" if="[[addon.audio]]"> <template is="dom-if" if="[[addon.audio]]">
@@ -97,52 +73,38 @@ class HassioAddonView extends PolymerElement {
<hassio-addon-network <hassio-addon-network
hass="[[hass]]" hass="[[hass]]"
addon="[[addon]]" addon="[[addon]]"
addon-slug="[[routeData.slug]]" addon-slug="[[addonSlug]]"
></hassio-addon-network> ></hassio-addon-network>
</template> </template>
<hassio-addon-logs <hassio-addon-logs
hass="[[hass]]" hass="[[hass]]"
addon-slug="[[routeData.slug]]" addon-slug="[[addonSlug]]"
></hassio-addon-logs> ></hassio-addon-logs>
</template> </template>
</div> </div>
</app-header-layout> </hass-subpage>
<hassio-markdown-dialog
title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
showMenu: Boolean, route: {
narrow: Boolean,
route: Object,
routeData: {
type: Object, type: Object,
observer: "routeDataChanged", observer: "routeDataChanged",
}, },
routeMatches: Boolean, addonSlug: {
addon: Object,
markdownTitle: String,
markdownContent: {
type: String, type: String,
value: "", computed: "_computeSlug(route)",
}, },
addon: Object,
}; };
} }
ready() { ready() {
super.ready(); super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this.addEventListener("hassio-markdown-dialog", (ev) =>
this.openMarkdown(ev)
);
} }
apiCalled(ev) { apiCalled(ev) {
@@ -151,15 +113,15 @@ class HassioAddonView extends PolymerElement {
if (!path) return; if (!path) return;
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") { if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
this.backTapped(); history.back();
} else { } else {
this.routeDataChanged(this.routeData); this.routeDataChanged(this.route);
} }
} }
routeDataChanged(routeData) { routeDataChanged(routeData) {
if (!this.routeMatches || !routeData || !routeData.slug) return; const addon = routeData.path.substr(1);
this.hass.callApi("get", `hassio/addons/${routeData.slug}/info`).then( this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
(info) => { (info) => {
this.addon = info.data; this.addon = info.data;
}, },
@@ -169,16 +131,8 @@ class HassioAddonView extends PolymerElement {
); );
} }
backTapped() { _computeSlug(route) {
history.back(); return route.path.substr(1);
}
openMarkdown(ev) {
this.setProperties({
markdownTitle: ev.detail.title,
markdownContent: ev.detail.content,
});
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
} }
} }

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

@@ -1,4 +1,4 @@
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -26,26 +26,13 @@ class HassioHassUpdate extends PolymerElement {
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]"> <template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
<div class="content"> <div class="content">
<div class="card-group"> <div class="card-group">
<div class="title">Update available! 🎉</div> <paper-card heading="Update available! 🎉">
<paper-card>
<div class="card-content"> <div class="card-content">
<hassio-card-content Home Assistant [[hassInfo.last_version]] is available and you
hass="[[hass]]" are currently running Home Assistant [[hassInfo.version]].
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>
<template is="dom-if" if="[[error]]"> <template is="dom-if" if="[[error]]">
<div class="error">Error: [[error]]</div> <div class="error">Error: [[error]]</div>
</template> </template>
<p>
<a
href="https://www.home-assistant.io/latest-release-notes/"
target="_blank"
>Read the release notes</a
>
</p>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button <ha-call-api-button
@@ -54,9 +41,9 @@ class HassioHassUpdate extends PolymerElement {
>Update</ha-call-api-button >Update</ha-call-api-button
> >
<a <a
href="https://github.com/home-assistant/home-assistant/releases" href="https://www.home-assistant.io/latest-release-notes/"
target="_blank" target="_blank"
><paper-button>Release notes</paper-button></a ><mwc-button>Release notes</mwc-button></a
> >
</div> </div>
</paper-card> </paper-card>

View File

@@ -1,18 +1,21 @@
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
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";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends PolymerElement { class HassioMarkdownDialog extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style include="ha-style-dialog"> <style include="ha-style-dialog">
paper-dialog { ha-paper-dialog {
min-width: 350px; min-width: 350px;
font-size: 14px; font-size: 14px;
border-radius: 2px; border-radius: 2px;
@@ -31,10 +34,10 @@ class HassioMarkdownDialog extends PolymerElement {
margin: 4px; margin: 4px;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog { ha-paper-dialog {
max-height: 100%; max-height: 100%;
} }
paper-dialog::before { ha-paper-dialog::before {
content: ""; content: "";
position: fixed; position: fixed;
z-index: -1; z-index: -1;
@@ -50,7 +53,7 @@ class HassioMarkdownDialog extends PolymerElement {
} }
} }
</style> </style>
<paper-dialog id="dialog" with-backdrop=""> <ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar> <app-toolbar>
<paper-icon-button <paper-icon-button
icon="hassio:close" icon="hassio:close"
@@ -61,7 +64,7 @@ class HassioMarkdownDialog extends PolymerElement {
<paper-dialog-scrollable> <paper-dialog-scrollable>
<ha-markdown content="[[content]]"></ha-markdown> <ha-markdown content="[[content]]"></ha-markdown>
</paper-dialog-scrollable> </paper-dialog-scrollable>
</paper-dialog> </ha-paper-dialog>
`; `;
} }
@@ -72,8 +75,14 @@ class HassioMarkdownDialog extends PolymerElement {
}; };
} }
openDialog() { public showDialog(params) {
this.$.dialog.open(); 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

@@ -1,21 +1,66 @@
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; 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-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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() { static get template() {
return html` return html`
<style include="ha-style-dialog"> <style include="ha-style-dialog">
paper-dialog { ha-paper-dialog {
min-width: 350px; min-width: 350px;
font-size: 14px; font-size: 14px;
border-radius: 2px; border-radius: 2px;
@@ -29,7 +74,7 @@ class HassioSnapshot extends PolymerElement {
app-toolbar [main-title] { app-toolbar [main-title] {
margin-left: 16px; margin-left: 16px;
} }
paper-dialog-scrollable { ha-paper-dialog-scrollable {
margin: 0; margin: 0;
} }
paper-checkbox { paper-checkbox {
@@ -37,7 +82,7 @@ class HassioSnapshot extends PolymerElement {
margin: 4px; margin: 4px;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
paper-dialog { ha-paper-dialog {
max-height: 100%; max-height: 100%;
height: 100%; height: 100%;
} }
@@ -57,7 +102,7 @@ class HassioSnapshot extends PolymerElement {
color: var(--google-red-500); color: var(--google-red-500);
} }
</style> </style>
<paper-dialog <ha-paper-dialog
id="dialog" id="dialog"
with-backdrop="" with-backdrop=""
on-iron-overlay-closed="_dialogClosed" on-iron-overlay-closed="_dialogClosed"
@@ -77,22 +122,18 @@ class HassioSnapshot extends PolymerElement {
<paper-checkbox checked="{{restoreHass}}"> <paper-checkbox checked="{{restoreHass}}">
Home Assistant [[snapshot.homeassistant]] Home Assistant [[snapshot.homeassistant]]
</paper-checkbox> </paper-checkbox>
<template is="dom-if" if="[[snapshot.addons.length]]"> <template is="dom-if" if="[[_folders.length]]">
<div>Folders:</div> <div>Folders:</div>
<template is="dom-repeat" items="[[snapshot.folders]]"> <template is="dom-repeat" items="[[_folders]]">
<paper-checkbox checked="{{item.checked}}"> <paper-checkbox checked="{{item.checked}}">
[[item.name]] [[item.name]]
</paper-checkbox> </paper-checkbox>
</template> </template>
</template> </template>
<template is="dom-if" if="[[snapshot.addons.length]]"> <template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div> <div>Add-ons:</div>
<paper-dialog-scrollable> <paper-dialog-scrollable>
<template <template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
is="dom-repeat"
items="[[snapshot.addons]]"
sort="_sortAddons"
>
<paper-checkbox checked="{{item.checked}}"> <paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span> [[item.name]] <span class="details">([[item.version]])</span>
</paper-checkbox> </paper-checkbox>
@@ -123,32 +164,26 @@ class HassioSnapshot extends PolymerElement {
class="download" class="download"
title="Download snapshot" title="Download snapshot"
></paper-icon-button> ></paper-icon-button>
<paper-button on-click="_partialRestoreClicked" <mwc-button on-click="_partialRestoreClicked"
>Restore selected</paper-button >Restore selected</mwc-button
> >
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]"> <template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<paper-button on-click="_fullRestoreClicked" <mwc-button on-click="_fullRestoreClicked"
>Wipe &amp; restore</paper-button >Wipe &amp; restore</mwc-button
> >
</template> </template>
</div> </div>
</paper-dialog> </ha-paper-dialog>
`; `;
} }
static get properties() { static get properties() {
return { return {
hass: Object, hass: Object,
snapshotSlug: { dialogParams: Object,
type: String,
notify: true,
observer: "_snapshotSlugChanged",
},
snapshotDeleted: {
type: Boolean,
notify: true,
},
snapshot: Object, snapshot: Object,
_folders: Object,
_addons: Object,
restoreHass: { restoreHass: {
type: Boolean, type: Boolean,
value: true, value: true,
@@ -158,140 +193,136 @@ class HassioSnapshot extends PolymerElement {
}; };
} }
_snapshotSlugChanged(snapshotSlug) { public async showDialog(params: HassioSnapshotDialogParams) {
if (!snapshotSlug || snapshotSlug === "update") return; // @ts-ignore
this.hass.callApi("get", `hassio/snapshots/${snapshotSlug}/info`).then( const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
(info) => { this.setProperties({
info.data.folders = this._computeFolders(info.data.folders); dialogParams: params,
info.data.addons = this._computeAddons(info.data.addons); snapshot,
this.snapshot = info.data; _folders: _computeFolders(snapshot.folders),
this.$.dialog.open(); _addons: _computeAddons(snapshot.addons),
}, });
() => { (this.$.dialog as PaperDialogElement).open();
this.snapshot = null;
}
);
} }
_computeFolders(folders) { protected _isFullSnapshot(type) {
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) {
return type === "full"; return type === "full";
} }
_partialRestoreClicked() { protected _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) { if (!confirm("Are you sure you want to restore this snapshot?")) {
return; return;
} }
const addons = this.snapshot.addons // @ts-ignore
const addons = this._addons
.filter((addon) => addon.checked) .filter((addon) => addon.checked)
.map((addon) => addon.slug); .map((addon) => addon.slug);
const folders = this.snapshot.folders // @ts-ignore
const folders = this._folders
.filter((folder) => folder.checked) .filter((folder) => folder.checked)
.map((folder) => folder.slug); .map((folder) => folder.slug);
const data = { const data = {
// @ts-ignore
homeassistant: this.restoreHass, homeassistant: this.restoreHass,
addons: addons, addons,
folders: folders, 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 this.hass
.callApi( .callApi(
"post", "POST",
`hassio/snapshots/${this.snapshotSlug}/restore/partial`, // @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
data data
) )
.then( .then(
() => { () => {
alert("Snapshot restored!"); alert("Snapshot restored!");
this.$.dialog.close(); (this.$.dialog as PaperDialogElement).close();
}, },
(error) => { (error) => {
// @ts-ignore
this.error = error.body.message; this.error = error.body.message;
} }
); );
} }
_fullRestoreClicked() { protected _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) { if (!confirm("Are you sure you want to restore this snapshot?")) {
return; return;
} }
// @ts-ignore
const data = this.snapshot.protected const data = this.snapshot.protected
? { password: this.snapshotPassword } ? {
: null; password:
// @ts-ignore
this.snapshotPassword,
}
: undefined;
// @ts-ignore
this.hass this.hass
.callApi( .callApi(
"post", "POST",
`hassio/snapshots/${this.snapshotSlug}/restore/full`, // @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
data data
) )
.then( .then(
() => { () => {
alert("Snapshot restored!"); alert("Snapshot restored!");
this.$.dialog.close(); (this.$.dialog as PaperDialogElement).close();
}, },
(error) => { (error) => {
// @ts-ignore
this.error = error.body.message; this.error = error.body.message;
} }
); );
} }
_deleteClicked() { protected _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) { if (!confirm("Are you sure you want to delete this snapshot?")) {
return; return;
} }
// @ts-ignore
this.hass this.hass
.callApi("post", `hassio/snapshots/${this.snapshotSlug}/remove`) // @ts-ignore
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
.then( .then(
() => { () => {
this.$.dialog.close(); (this.$.dialog as PaperDialogElement).close();
this.snapshotDeleted = true; // @ts-ignore
this.dialogParams!.onDelete();
}, },
(error) => { (error) => {
// @ts-ignore
this.error = error.body.message; this.error = error.body.message;
} }
); );
} }
async _downloadClicked() { protected async _downloadClicked() {
let signedPath; let signedPath;
try { try {
signedPath = await getSignedPath( signedPath = await getSignedPath(
// @ts-ignore
this.hass, this.hass,
`/api/hassio/snapshots/${this.snapshotSlug}/download` // @ts-ignore
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
); );
} catch (err) { } catch (err) {
alert(`Error: ${err.message}`); alert(`Error: ${err.message}`);
return; return;
} }
// @ts-ignore
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_"); const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("A"); const a = document.createElement("a");
a.href = signedPath.path; a.href = signedPath.path;
a.download = `Hass_io_${name}.tar`; a.download = `Hass_io_${name}.tar`;
this.$.dialog.appendChild(a); this.$.dialog.appendChild(a);
@@ -299,23 +330,23 @@ class HassioSnapshot extends PolymerElement {
this.$.dialog.removeChild(a); this.$.dialog.removeChild(a);
} }
_computeName(snapshot) { protected _computeName(snapshot) {
return snapshot.name || snapshot.slug; return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
} }
_computeType(type) { protected _computeType(type) {
return type === "full" ? "Full snapshot" : "Partial snapshot"; return type === "full" ? "Full snapshot" : "Partial snapshot";
} }
_computeSize(size) { protected _computeSize(size) {
return Math.ceil(size * 10) / 10 + " MB"; return Math.ceil(size * 10) / 10 + " MB";
} }
_sortAddons(a, b) { protected _sortAddons(a, b) {
return a.name < b.name ? -1 : 1; return a.name < b.name ? -1 : 1;
} }
_formatDatetime(datetime) { protected _formatDatetime(datetime) {
return new Date(datetime).toLocaleDateString(navigator.language, { return new Date(datetime).toLocaleDateString(navigator.language, {
weekday: "long", weekday: "long",
year: "numeric", year: "numeric",
@@ -326,8 +357,18 @@ class HassioSnapshot extends PolymerElement {
}); });
} }
_dialogClosed() { protected _dialogClosed() {
this.snapshotSlug = null; 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(() => { window.loadES5Adapter().then(() => {
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js"); import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
import(/* webpackChunkName: "hassio-main" */ "./hassio-main.js"); 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,51 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-main";
import "./resources/hassio-icons";
class HassioApp extends PolymerElement {
static get template() {
return html`
<template is="dom-if" if="[[hass]]">
<hassio-main
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
route="[[route]]"
></hassio-main>
</template>
`;
}
static get properties() {
return {
hass: Object,
narrow: Boolean,
showMenu: Boolean,
route: Object,
hassioPanel: {
type: Object,
value: window.parent.hassioPanel,
},
};
}
ready() {
super.ready();
window.setProperties = this.setProperties.bind(this);
this.addEventListener("location-changed", () => this._locationChanged());
this.addEventListener("hass-open-menu", () => this._menuEvent(true));
this.addEventListener("hass-close-menu", () => this._menuEvent(false));
}
_menuEvent(shouldOpen) {
this.hassioPanel.fire(shouldOpen ? "hass-open-menu" : "hass-close-menu");
}
_locationChanged() {
this.hassioPanel.navigate(window.location.pathname);
}
}
customElements.define("hassio-app", HassioApp);

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,129 +0,0 @@
import "@polymer/app-route/app-route";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/layouts/hass-loading-screen";
import "./addon-view/hassio-addon-view";
import "./hassio-data";
import "./hassio-pages-with-tabs";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import EventsMixin from "../../src/mixins/events-mixin";
import NavigateMixin from "../../src/mixins/navigate-mixin";
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<app-route
route="[[route]]"
pattern="/:page"
data="{{routeData}}"
></app-route>
<hassio-data
id="data"
hass="[[hass]]"
supervisor="{{supervisorInfo}}"
homeassistant="{{hassInfo}}"
host="{{hostInfo}}"
></hassio-data>
<template is="dom-if" if="[[!loaded]]">
<hass-loading-screen
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></hass-loading-screen>
</template>
<template is="dom-if" if="[[loaded]]">
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
<hassio-pages-with-tabs
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
page="[[routeData.page]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
host-info="[[hostInfo]]"
></hassio-pages-with-tabs>
</template>
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
<hassio-addon-view
hass="[[hass]]"
narrow="[[narrow]]"
show-menu="[[showMenu]]"
route="[[route]]"
></hassio-addon-view>
</template>
</template>
`;
}
static get properties() {
return {
hass: Object,
narrow: Boolean,
showMenu: Boolean,
route: {
type: Object,
// Fake route object
value: {
prefix: "/hassio",
path: "/dashboard",
__queryParams: {},
},
observer: "routeChanged",
},
routeData: Object,
supervisorInfo: Object,
hostInfo: Object,
hassInfo: Object,
loaded: {
type: Boolean,
computed: "computeIsLoaded(supervisorInfo, hostInfo, hassInfo)",
},
};
}
ready() {
super.ready();
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
connectedCallback() {
super.connectedCallback();
this.routeChanged(this.route);
}
apiCalled(ev) {
if (ev.detail.success) {
let tries = 1;
const tryUpdate = () => {
this.$.data.refresh().catch(function() {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
computeIsLoaded(supervisorInfo, hostInfo, hassInfo) {
return supervisorInfo !== null && hostInfo !== null && hassInfo !== null;
}
routeChanged(route) {
if (route.path === "" && route.prefix === "/hassio") {
this.navigate("/hassio/dashboard", true);
}
this.fire("iron-resize");
}
equalsAddon(page) {
return page && page === "addon";
}
}
customElements.define("hassio-main", HassioMain);

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

@@ -0,0 +1,183 @@
import { customElement, PropertyValues, property } from "lit-element";
import { PolymerElement } from "@polymer/polymer";
import "@polymer/paper-icon-button";
import "../../src/resources/ha-style";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { HomeAssistant } from "../../src/types";
import {
fetchHassioSupervisorInfo,
fetchHassioHostInfo,
fetchHassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
fetchHassioAddonInfo,
createHassioSession,
HassioPanelInfo,
} from "../../src/data/hassio";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-pages-with-tabs";
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
// causing an exception when added to DOM. When transpiled to ES5, this will
// break the build.
customElements.get("paper-icon-button").prototype._keyBindings = {};
@customElement("hassio-main")
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-pages-with-tabs",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-view",
load: () => import("./addon-view/hassio-addon-view"),
},
ingress: {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
},
};
@property() private _supervisorInfo: HassioSupervisorInfo;
@property() private _hostInfo: HassioHostInfo;
@property() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
// So here we are going to translate toggle events into the appropriate
// open and close events. These events are a no-op in newer versions of
// Home Assistant.
this.addEventListener("hass-toggle-menu", () => {
fireEvent(
(window.parent as any).customPanel,
// @ts-ignore
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
);
});
// Paulus - March 19, 2019
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
window.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
})
);
makeDialogManager(this, document.body);
}
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route =
el.nodeName === "HASSIO-PAGES-WITH-TABS" ? this.route : this.routeTail;
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
supervisorInfo: this._supervisorInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
route,
});
} else {
el.hass = this.hass;
el.supervisorInfo = this._supervisorInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.route = route;
}
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
await this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
}
private async _redirectIngress(addonSlug: string) {
try {
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress_url) {
throw new Error("Add-on does not support Ingress");
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
} catch (err) {
alert(`Unable to open ingress connection `);
}
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-main": HassioMain;
}
}

View File

@@ -1,167 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/components/ha-menu-button";
import "../../src/resources/ha-style";
import "./addon-store/hassio-addon-store";
import "./dashboard/hassio-dashboard";
import "./hassio-markdown-dialog";
import "./snapshots/hassio-snapshot";
import "./snapshots/hassio-snapshots";
import "./system/hassio-system";
import scrollToTarget from "../../src/common/dom/scroll-to-target";
import NavigateMixin from "../../src/mixins/navigate-mixin";
class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-positioning ha-style">
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
</style>
<app-header-layout id="layout" has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
hassio
narrow="[[narrow]]"
show-menu="[[showMenu]]"
></ha-menu-button>
<div main-title>Hass.io</div>
<template is="dom-if" if="[[showRefreshButton(page)]]">
<paper-icon-button
icon="hassio:refresh"
on-click="refreshClicked"
></paper-icon-button>
</template>
</app-toolbar>
<paper-tabs
scrollable=""
selected="[[page]]"
attr-for-selected="page-name"
on-iron-activate="handlePageSelected"
>
<paper-tab page-name="dashboard">Dashboard</paper-tab>
<paper-tab page-name="snapshots">Snapshots</paper-tab>
<paper-tab page-name="store">Add-on store</paper-tab>
<paper-tab page-name="system">System</paper-tab>
</paper-tabs>
</app-header>
<template is="dom-if" if='[[equals(page, "dashboard")]]'>
<hassio-dashboard
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
hass-info="[[hassInfo]]"
></hassio-dashboard>
</template>
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
<hassio-snapshots
hass="[[hass]]"
installed-addons="[[supervisorInfo.addons]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshots>
</template>
<template is="dom-if" if='[[equals(page, "store")]]'>
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
</template>
<template is="dom-if" if='[[equals(page, "system")]]'>
<hassio-system
hass="[[hass]]"
supervisor-info="[[supervisorInfo]]"
host-info="[[hostInfo]]"
></hassio-system>
</template>
</app-header-layout>
<hassio-markdown-dialog
title="[[markdownTitle]]"
content="[[markdownContent]]"
></hassio-markdown-dialog>
<template is="dom-if" if='[[equals(page, "snapshots")]]'>
<hassio-snapshot
hass="[[hass]]"
snapshot-slug="{{snapshotSlug}}"
snapshot-deleted="{{snapshotDeleted}}"
></hassio-snapshot>
</template>
`;
}
static get properties() {
return {
hass: Object,
showMenu: Boolean,
narrow: Boolean,
page: String,
supervisorInfo: Object,
hostInfo: Object,
hassInfo: Object,
snapshotSlug: String,
snapshotDeleted: Boolean,
markdownTitle: String,
markdownContent: {
type: String,
value: "",
},
};
}
ready() {
super.ready();
this.addEventListener("hassio-markdown-dialog", (ev) =>
this.openMarkdown(ev)
);
}
handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this.page) {
this.navigate(`/hassio/${newPage}`);
}
scrollToTarget(this, this.$.layout.header.scrollTarget);
}
equals(a, b) {
return a === b;
}
showRefreshButton(page) {
return page === "store" || page === "snapshots";
}
refreshClicked() {
if (this.page === "snapshots") {
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
} else {
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
}
}
openMarkdown(ev) {
this.setProperties({
markdownTitle: ev.detail.title,
markdownContent: ev.detail.content,
});
this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog();
}
}
customElements.define("hassio-pages-with-tabs", HassioPagesWithTabs);

View File

@@ -0,0 +1,131 @@
import {
LitElement,
TemplateResult,
html,
CSSResultArray,
css,
customElement,
property,
} from "lit-element";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "../../src/components/ha-menu-button";
import "../../src/resources/ha-style";
import "./hassio-tabs-router";
import scrollToTarget from "../../src/common/dom/scroll-to-target";
import { haStyle } from "../../src/resources/styles";
import { HomeAssistant, Route } from "../../src/types";
import { navigate } from "../../src/common/navigate";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio";
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@customElement("hassio-pages-with-tabs")
class HassioPagesWithTabs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
protected render(): TemplateResult | void {
const page = this._page;
return html`
<app-header-layout has-scrolling-region>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button hassio></ha-menu-button>
<div main-title>Hass.io</div>
${HAS_REFRESH_BUTTON.includes(page)
? html`
<paper-icon-button
icon="hassio:refresh"
@click=${this.refreshClicked}
></paper-icon-button>
`
: undefined}
</app-toolbar>
<paper-tabs
scrollable
attr-for-selected="page-name"
.selected=${page}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="dashboard">Dashboard</paper-tab>
<paper-tab page-name="snapshots">Snapshots</paper-tab>
<paper-tab page-name="store">Add-on store</paper-tab>
<paper-tab page-name="system">System</paper-tab>
</paper-tabs>
</app-header>
<hassio-tabs-router
.route=${this.route}
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
></hassio-tabs-router>
</app-header-layout>
`;
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._page) {
navigate(this, `/hassio/${newPage}`);
}
scrollToTarget(
this,
// @ts-ignore
this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget
);
}
private refreshClicked() {
if (this._page === "snapshots") {
// @ts-ignore
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
} else {
// @ts-ignore
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
}
}
private get _page() {
return this.route.path.substr(1);
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-pages-with-tabs": HassioPagesWithTabs;
}
}

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"); const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;"); 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"> documentContainer.innerHTML = `<dom-module id="hassio-style">
<template> <template>
<style> <style>
.card-group { ${hassioStyle.toString()}
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;
}
</style> </style>
</template> </template>
</dom-module>`; </dom-module>`;

View File

@@ -1,311 +0,0 @@
import "@polymer/paper-button/paper-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hassio-card-content";
import "../resources/hassio-style";
import EventsMixin from "../../../src/mixins/events-mixin";
class HassioSnapshots extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style hassio-style">
paper-radio-group {
display: block;
}
paper-radio-button {
padding: 0 0 2px 2px;
}
paper-radio-button,
paper-checkbox,
paper-input[type="password"] {
display: block;
margin: 4px 0 4px 48px;
}
.pointer {
cursor: pointer;
}
</style>
<div class="content">
<div class="card-group">
<div class="title">
Create snapshot
<div class="description">
Snapshots allow you to easily backup and restore all data of your
Hass.io instance.
</div>
</div>
<paper-card>
<div class="card-content">
<paper-input
autofocus=""
label="Name"
value="{{snapshotName}}"
></paper-input>
Type:
<paper-radio-group selected="{{snapshotType}}">
<paper-radio-button name="full">
Full snapshot
</paper-radio-button>
<paper-radio-button name="partial">
Partial snapshot
</paper-radio-button>
</paper-radio-group>
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
Folders:
<template is="dom-repeat" items="[[folderList]]">
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
Add-ons:
<template
is="dom-repeat"
items="[[addonList]]"
sort="_sortAddons"
>
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
</template>
Security:
<paper-checkbox checked="{{snapshotHasPassword}}"
>Password protection</paper-checkbox
>
<template is="dom-if" if="[[snapshotHasPassword]]">
<paper-input
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template>
<template is="dom-if" if="[[error]]">
<p class="error">[[error]]</p>
</template>
</div>
<div class="card-actions">
<paper-button
disabled="[[creatingSnapshot]]"
on-click="_createSnapshot"
>Create</paper-button
>
</div>
</paper-card>
</div>
<div class="card-group">
<div class="title">Available snapshots</div>
<template is="dom-if" if="[[!snapshots.length]]">
<paper-card>
<div class="card-content">You don't have any snapshots yet.</div>
</paper-card>
</template>
<template
is="dom-repeat"
items="[[snapshots]]"
as="snapshot"
sort="_sortSnapshots"
>
<paper-card class="pointer" on-click="_snapshotClicked">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[_computeName(snapshot)]]"
description="[[_computeDetails(snapshot)]]"
datetime="[[snapshot.date]]"
icon="[[_computeIcon(snapshot.type)]]"
icon-class="snapshot"
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
snapshotName: {
type: String,
value: "",
},
snapshotPassword: {
type: String,
value: "",
},
snapshotHasPassword: Boolean,
snapshotType: {
type: String,
value: "full",
},
snapshots: {
type: Array,
value: [],
},
installedAddons: {
type: Array,
observer: "_installedAddonsChanged",
},
addonList: Array,
folderList: {
type: Array,
value: [
{
slug: "homeassistant",
name: "Home Assistant configuration",
checked: true,
},
{ slug: "ssl", name: "SSL", checked: true },
{ slug: "share", name: "Share", checked: true },
{ slug: "addons/local", name: "Local add-ons", checked: true },
],
},
snapshotSlug: {
type: String,
notify: true,
},
snapshotDeleted: {
type: Boolean,
notify: true,
observer: "_snapshotDeletedChanged",
},
creatingSnapshot: Boolean,
dialogOpened: Boolean,
error: String,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this._updateSnapshots();
}
_apiCalled(ev) {
if (ev.detail.success) {
this._updateSnapshots();
}
}
_updateSnapshots() {
this.hass.callApi("get", "hassio/snapshots").then(
(result) => {
this.snapshots = result.data.snapshots;
},
(error) => {
this.error = error.message;
}
);
}
_createSnapshot() {
this.error = "";
if (this.snapshotHasPassword && !this.snapshotPassword.length) {
this.error = "Please enter a password.";
return;
}
this.creatingSnapshot = true;
let name = this.snapshotName;
if (!name.length) {
name = new Date().toLocaleDateString(navigator.language, {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
});
}
let data;
let path;
if (this.snapshotType === "full") {
data = { name: name };
path = "hassio/snapshots/new/full";
} else {
const addons = this.addonList
.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this.folderList
.filter((folder) => folder.checked)
.map((folder) => folder.slug);
data = { name: name, folders: folders, addons: addons };
path = "hassio/snapshots/new/partial";
}
if (this.snapshotHasPassword) {
data.password = this.snapshotPassword;
}
this.hass.callApi("post", path, data).then(
() => {
this.creatingSnapshot = false;
this.fire("hass-api-called", { success: true });
},
(error) => {
this.creatingSnapshot = false;
this.error = error.message;
}
);
}
_installedAddonsChanged(addons) {
this.addonList = addons.map((addon) => ({
slug: addon.slug,
name: addon.name,
checked: true,
}));
}
_sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
_sortSnapshots(a, b) {
return a.date < b.date ? 1 : -1;
}
_computeName(snapshot) {
return snapshot.name || snapshot.slug;
}
_computeDetails(snapshot) {
const type =
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
return snapshot.protected ? `${type}, password protected` : type;
}
_computeIcon(type) {
return type === "full"
? "hassio:package-variant-closed"
: "hassio:package-variant";
}
_snapshotClicked(ev) {
this.snapshotSlug = ev.model.snapshot.slug;
}
_fullSelected(type) {
return type === "full";
}
_snapshotDeletedChanged(snapshotDeleted) {
if (snapshotDeleted) {
this._updateSnapshots();
this.snapshotDeleted = false;
}
}
refreshData() {
this.hass.callApi("post", "hassio/snapshots/reload").then(() => {
this._updateSnapshots();
});
}
}
customElements.define("hassio-snapshots", HassioSnapshots);

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

@@ -1,15 +1,17 @@
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; 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) { class HassioHostInfo extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style>
paper-card { paper-card {
display: inline-block; display: inline-block;
width: 400px; width: 400px;
@@ -39,7 +41,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
color: var(--google-red-500); color: var(--google-red-500);
margin-top: 16px; margin-top: 16px;
} }
paper-button.info { mwc-button.info {
max-width: calc(50% - 12px); max-width: calc(50% - 12px);
} }
table.info { table.info {
@@ -67,13 +69,13 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
</template> </template>
</tbody> </tbody>
</table> </table>
<paper-button raised on-click="_showHardware" class="info"> <mwc-button raised on-click="_showHardware" class="info">
Hardware Hardware
</paper-button> </mwc-button>
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]"> <template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
<paper-button raised on-click="_changeHostnameClicked" class="info"> <mwc-button raised on-click="_changeHostnameClicked" class="info">
Change hostname Change hostname
</paper-button> </mwc-button>
</template> </template>
<template is="dom-if" if="[[errors]]"> <template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div> <div class="errors">Error: [[errors]]</div>
@@ -173,7 +175,7 @@ class HassioHostInfo extends EventsMixin(PolymerElement) {
() => "Error getting hardware info" () => "Error getting hardware info"
) )
.then((content) => { .then((content) => {
this.fire("hassio-markdown-dialog", { showHassioMarkdownDialog(this, {
title: "Hardware", title: "Hardware",
content: content, content: content,
}); });

View File

@@ -1,15 +1,15 @@
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; 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) { class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style>
paper-card { paper-card {
display: inline-block; display: inline-block;
width: 400px; width: 400px;
@@ -80,11 +80,11 @@ class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
> >
</template> </template>
<template is="dom-if" if='[[_equals(data.channel, "stable")]]'> <template is="dom-if" if='[[_equals(data.channel, "stable")]]'>
<paper-button <mwc-button
on-click="_joinBeta" on-click="_joinBeta"
class="warning" class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host" title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</paper-button >Join beta channel</mwc-button
> >
</template> </template>
</div> </div>

View File

@@ -1,4 +1,4 @@
import "@polymer/paper-button/paper-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -24,7 +24,7 @@ class HassioSupervisorLog extends PolymerElement {
<paper-card> <paper-card>
<div class="card-content" id="content"></div> <div class="card-content" id="content"></div>
<div class="card-actions"> <div class="card-actions">
<paper-button on-click="refresh">Refresh</paper-button> <mwc-button on-click="refresh">Refresh</mwc-button>
</div> </div>
</paper-card> </paper-card>
`; `;

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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -9,9 +8,10 @@ import "./hassio-supervisor-log";
class HassioSystem extends PolymerElement { class HassioSystem extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style>
.content { .content {
margin: 4px; margin: 4px;
color: var(--primary-text-color);
} }
.title { .title {
margin-top: 24px; margin-top: 24px;

View File

@@ -1,11 +1,15 @@
const webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin"); const CompressionPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const config = require("./config.js"); const config = require("./config.js");
const { babelLoaderConfig } = require("../config/babel.js"); const { babelLoaderConfig } = require("../build-scripts/babel.js");
const { minimizer } = require("../config/babel.js"); const webpackBase = require("../build-scripts/webpack.js");
const isProdBuild = process.env.NODE_ENV === "production"; const isProdBuild = process.env.NODE_ENV === "production";
const isCI = process.env.CI === "true"; const isCI = process.env.CI === "true";
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js"; const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const latestBuild = false;
module.exports = { module.exports = {
mode: isProdBuild ? "production" : "development", mode: isProdBuild ? "production" : "development",
@@ -15,7 +19,7 @@ module.exports = {
}, },
module: { module: {
rules: [ rules: [
babelLoaderConfig({ latestBuild: false }), babelLoaderConfig({ latestBuild }),
{ {
test: /\.(html)$/, test: /\.(html)$/,
use: { use: {
@@ -27,15 +31,24 @@ module.exports = {
}, },
], ],
}, },
optimization: { optimization: webpackBase.optimization(latestBuild),
minimizer,
},
plugins: [ 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 && isProdBuild &&
!isCI && !isCI &&
new CompressionPlugin({ new CompressionPlugin({
cache: true, cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/], exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
},
}), }),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {

View File

@@ -17,8 +17,10 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@mdi/svg": "^3.0.39", "@material/mwc-button": "^0.5.0",
"@polymer/app-layout": "^3.0.1", "@material/mwc-ripple": "^0.5.0",
"@mdi/svg": "3.5.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2", "@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2", "@polymer/app-storage": "^3.0.2",
@@ -32,19 +34,19 @@
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1", "@polymer/iron-label": "^3.0.1",
"@polymer/iron-media-query": "^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-pages": "^3.0.1",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/neon-animation": "^3.0.1", "@polymer/neon-animation": "^3.0.1",
"@polymer/paper-button": "^3.0.1",
"@polymer/paper-card": "^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": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1", "@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-drawer-panel": "^3.0.1", "@polymer/paper-drawer-panel": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.0.1", "@polymer/paper-dropdown-menu": "^3.0.1",
"@polymer/paper-fab": "^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-input": "^3.0.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
@@ -55,110 +57,120 @@
"@polymer/paper-ripple": "^3.0.1", "@polymer/paper-ripple": "^3.0.1",
"@polymer/paper-scroll-header-panel": "^3.0.1", "@polymer/paper-scroll-header-panel": "^3.0.1",
"@polymer/paper-slider": "^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-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.0.1", "@polymer/paper-tabs": "^3.0.1",
"@polymer/paper-toast": "^3.0.1", "@polymer/paper-toast": "^3.0.1",
"@polymer/paper-toggle-button": "^3.0.1", "@polymer/paper-toggle-button": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "^3.0.5", "@polymer/polymer": "^3.2.0",
"@vaadin/vaadin-combo-box": "^4.2.0", "@vaadin/vaadin-combo-box": "^4.2.8",
"@vaadin/vaadin-date-picker": "^3.3.1", "@vaadin/vaadin-date-picker": "^3.3.3",
"@webcomponents/shadycss": "^1.6.0", "@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.0", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.7.2", "chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.2.1", "chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.43.0", "codemirror": "^5.45.0",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
"eslint-import-resolver-webpack": "^0.10.1", "fecha": "^3.0.2",
"fecha": "^3.0.0", "fuse.js": "^3.4.4",
"home-assistant-js-websocket": "^3.2.4", "google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.2.1",
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-yaml": "^3.12.0", "js-yaml": "^3.13.0",
"leaflet": "^1.3.4", "leaflet": "^1.4.0",
"lit-element": "2.0.0-rc.5", "lit-element": "^2.1.0",
"lit-html": "1.0.0-rc.2", "lit-html": "^1.0.0",
"marked": "^0.6.0", "marked": "^0.6.1",
"mdn-polyfills": "^5.12.0", "mdn-polyfills": "^5.16.0",
"moment": "^2.22.2", "memoize-one": "^5.0.2",
"preact": "^8.3.1", "moment": "^2.24.0",
"preact": "^8.4.2",
"preact-compat": "^3.18.4", "preact-compat": "^3.18.4",
"react-big-calendar": "^0.19.2", "react-big-calendar": "^0.20.4",
"regenerator-runtime": "^0.12.1", "regenerator-runtime": "^0.13.2",
"round-slider": "^1.3.2", "round-slider": "^1.3.3",
"superstruct": "^0.6.0", "superstruct": "^0.6.1",
"unfetch": "^4.0.1", "unfetch": "^4.1.0",
"web-animations-js": "^2.3.1", "web-animations-js": "^2.3.1",
"xss": "^1.0.3" "xss": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.2", "@babel/core": "^7.4.0",
"@babel/plugin-external-helpers": "^7.0.0", "@babel/plugin-external-helpers": "^7.2.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-transform-react-jsx": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.4.0",
"@babel/preset-env": "^7.1.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-typescript": "^7.1.0", "@babel/plugin-transform-react-jsx": "^7.3.0",
"@gfx/zopfli": "^1.0.9", "@babel/preset-env": "^7.4.2",
"@babel/preset-typescript": "^7.3.3",
"@gfx/zopfli": "^1.0.11",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71", "@types/hls.js": "^0.12.3",
"@types/mocha": "^5.2.5", "@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"babel-eslint": "^10", "babel-eslint": "^10",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.5",
"babel-minify-webpack-plugin": "^0.3.1",
"chai": "^4.2.0", "chai": "^4.2.0",
"compression-webpack-plugin": "^2.0.0", "compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.5.2", "copy-webpack-plugin": "^5.0.2",
"del": "^3.0.0", "del": "^4.0.0",
"eslint": "^5.6.0", "eslint": "^5.15.3",
"eslint-config-airbnb-base": "^13.1.0", "eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^4.0.0", "eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "^2.14.0", "eslint-import-resolver-webpack": "^0.11.0",
"eslint-plugin-prettier": "^3.0.0", "eslint-plugin-import": "^2.16.0",
"eslint-plugin-react": "^7.11.1", "eslint-plugin-prettier": "^3.0.1",
"gulp": "^3.9.1", "eslint-plugin-react": "^7.12.4",
"fs-extra": "^7.0.1",
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0", "gulp-foreach": "^0.1.0",
"gulp-hash": "^4.2.2", "gulp-hash": "^4.2.2",
"gulp-hash-filename": "^2.0.1",
"gulp-insert": "^0.5.0", "gulp-insert": "^0.5.0",
"gulp-json-transform": "^0.4.5", "gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0", "gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
"gulp-rename": "^1.4.0", "gulp-rename": "^1.4.0",
"gulp-zopfli-green": "^3.0.1",
"html-loader": "^0.5.5", "html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^1.1.0", "husky": "^1.3.1",
"lint-staged": "^8.0.2", "lint-staged": "^8.1.5",
"lodash.template": "^4.4.0",
"merge-stream": "^1.0.1", "merge-stream": "^1.0.1",
"mocha": "^5.2.0", "mocha": "^6.0.2",
"parse5": "^5.1.0", "parse5": "^5.1.0",
"polymer-cli": "^1.8.0", "polymer-cli": "^1.9.7",
"prettier": "^1.14.3", "prettier": "^1.16.4",
"raw-loader": "^0.5.1", "raw-loader": "^2.0.0",
"reify": "^0.18.1", "reify": "^0.18.1",
"require-dir": "^1.0.0", "require-dir": "^1.2.0",
"sinon": "^7.1.1", "sinon": "^7.3.1",
"ts-mocha": "^2.0.0", "terser-webpack-plugin": "^1.2.3",
"tslint": "^5.11.0", "ts-mocha": "^6.0.0",
"tslint-config-prettier": "^1.15.0", "tslint": "^5.14.0",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0", "tslint-eslint-rules": "^5.4.0",
"tslint-plugin-prettier": "^2.0.1", "tslint-plugin-prettier": "^2.0.1",
"typescript": "^3.1.4", "typescript": "^3.4.1",
"uglifyjs-webpack-plugin": "^2.1.1", "uglifyjs-webpack-plugin": "^2.1.2",
"wct-browser-legacy": "^1.0.1", "wct-browser-legacy": "^1.0.2",
"web-component-tester": "^6.8.0", "web-component-tester": "^6.9.2",
"webpack": "^4.19.1", "webpack": "^4.29.6",
"webpack-cli": "^3.1.0", "webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.1.8", "webpack-dev-server": "^3.2.1",
"workbox-webpack-plugin": "^3.5.0" "webpack-manifest-plugin": "^2.0.4",
"workbox-webpack-plugin": "^4.1.1"
}, },
"resolutions": { "resolutions": {
"@polymer/polymer": "3.1.0", "@webcomponents/webcomponentsjs": "^2.2.7",
"@webcomponents/webcomponentsjs": "2.2.1", "@vaadin/vaadin-lumo-styles": "^1.4.2"
"@webcomponents/shadycss": "^1.6.0",
"@vaadin/vaadin-overlay": "3.2.2",
"@vaadin/vaadin-lumo-styles": "1.3.0"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@@ -1,30 +1,7 @@
"""Frontend for Home Assistant.""" """Frontend for Home Assistant."""
import os from pathlib import Path
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
}
def where(): def where():
"""Return path to the frontend.""" """Return path to the frontend."""
return os.path.dirname(__file__) return Path(__file__).parent
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

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

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