mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-02 14:31:57 +00:00
Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1eabeb29f | |||
| 5ff8fe68ba | |||
| a2a039ebc5 | |||
| 1064aed1b0 | |||
| 7025592e8e | |||
| 4966354b62 | |||
| 68d6faf4af | |||
| e3346483b9 | |||
| e8fb79e5ce | |||
| d612162ab1 | |||
| 86f8ef3a70 | |||
| 0e43435362 | |||
| aaefe0b09f | |||
| bc731a9dc3 | |||
| da25701dca | |||
| 21ae483dc9 | |||
| 38b6e9ca10 | |||
| d31245866c | |||
| 4e08d8f3b3 | |||
| 1e717ab33e | |||
| 995fb4974e | |||
| ffb76132f8 | |||
| acba3af54b | |||
| 40ac456937 | |||
| 5c32413bf7 | |||
| 22792c70c5 | |||
| a8ed87298a | |||
| b15270dfe2 | |||
| 58ad949bc8 | |||
| adce40de56 | |||
| 0f487ae4bf | |||
| 2848e3a63b | |||
| 5a172a64c5 | |||
| 433aa16ea6 | |||
| 50cb8cf3cc | |||
| 4e5406b27b | |||
| 80eb80619a | |||
| bf71b3a869 | |||
| ff270c4b7d | |||
| 5415068917 | |||
| 357a67c00d | |||
| cbe4269320 | |||
| fbd5185ce2 | |||
| a33cf97e2c | |||
| 7e7da26543 | |||
| 79058e893b | |||
| 2eb548bb74 | |||
| 08baf8a757 | |||
| f02fa6a94b | |||
| 2ed6d0e73c | |||
| 35d9b2ac3c | |||
| 18d09c6f04 | |||
| 70b81de49d | |||
| f0808c1f54 | |||
| e779f0747e | |||
| bdd18775c3 | |||
| 711d51c022 | |||
| 1b0d8bba29 | |||
| 2988cc512f | |||
| a2f8e5f3e7 | |||
| 680bf06a4b | |||
| ff0b1881e2 | |||
| de653e1f7b | |||
| bb41170765 | |||
| 0ed2bc93aa | |||
| 04770f8ee2 | |||
| 15a2790b9f | |||
| 83880791b1 | |||
| 4dca3289f6 | |||
| 083a3ebfc4 | |||
| 6117c4e989 | |||
| 609763e658 | |||
| 2c57ab60f1 | |||
| dd17a153d2 | |||
| c2d551bb7c | |||
| e0b1921108 | |||
| fcf39ceb96 | |||
| 3cc979a077 | |||
| 9972973774 | |||
| 20ae32bc26 | |||
| a29892023b | |||
| b283fec482 | |||
| e0116a8236 | |||
| d1990a4bac | |||
| cbba1849e2 | |||
| 43393d1647 | |||
| b47ee1051c | |||
| 393adacc9e | |||
| 073428849e | |||
| e6ac0258e3 | |||
| d7e7798a55 | |||
| 2557414b11 | |||
| f7065fbce9 | |||
| 016564eee9 | |||
| ff3087c39c | |||
| 239438ee5d | |||
| 5458cda31f | |||
| 36f49e66fd | |||
| 2bafd38ea8 | |||
| 73b3262491 | |||
| 808cde033f | |||
| fa8f6b7b91 | |||
| 94c120cdb1 | |||
| 7b2be54f8f | |||
| 4b56db5255 | |||
| 93165c9111 | |||
| caa604d5ca | |||
| e7e9e2cf85 | |||
| daa04e9973 | |||
| 5355269f5d | |||
| 2665a75250 | |||
| 8a39d18323 | |||
| b8a026397b | |||
| bd5fe302eb | |||
| de0f1b2b65 | |||
| defaa2b276 | |||
| 60efe00a1f | |||
| fe93b993db | |||
| f6afc92d3c | |||
| e4c635c855 | |||
| a3e59e168f | |||
| e56355b406 | |||
| 8ef15c50b4 | |||
| 81588469b8 | |||
| 70a920af3c | |||
| 1329e60c89 | |||
| 9b7c095080 | |||
| 654ff99cd1 | |||
| 0511bc360e | |||
| ea9e8cc392 | |||
| 8433678371 | |||
| 757bc00854 | |||
| 2551393821 | |||
| 0acd41b7f0 | |||
| 85ca73db84 | |||
| 444cbd00d9 | |||
| 15b500886c | |||
| 3aac834e72 | |||
| 6edf23b91f | |||
| e445251b02 | |||
| 693151b590 | |||
| 1249c0eea9 | |||
| 3133118870 | |||
| de5c1a0545 | |||
| c61e2fb459 | |||
| 64a2a19da3 | |||
| 74fe1f820c | |||
| 69929f5dc3 | |||
| fcd793fc9e | |||
| 8a3b1d76a1 | |||
| 9f520d7628 | |||
| 258cfddc3f | |||
| 3697500402 | |||
| b4942ad27e | |||
| 1e217e8d2f | |||
| 0056237d85 | |||
| 920ee741f3 | |||
| 6ecc60423f | |||
| 09e7638c89 | |||
| b82b4a639e | |||
| d08aa51c16 | |||
| 385ffe6d8f | |||
| 564e6d4073 | |||
| a4bd816eb5 | |||
| 13c18a9bb7 | |||
| 562d7a7cf4 | |||
| 89f33a1730 | |||
| ff7309f5c4 | |||
| 1c614c855f | |||
| 6a3238951d | |||
| 0dab5828fb | |||
| d0b9c09f8f | |||
| 55f4629256 | |||
| 004565217e | |||
| c07b39ebde | |||
| 8b17b6ed1c | |||
| 1d16bdbe54 | |||
| 9e2a0c77d5 | |||
| 4f41508110 | |||
| eaedb2e5ae | |||
| 75ad1f51a9 | |||
| 142175c6ab | |||
| f1980d6bcf | |||
| 5a7b5200fe | |||
| d284d53b93 | |||
| bc01df42d8 | |||
| 901752bec3 | |||
| e3ef3cfae1 | |||
| ab476d2f1b | |||
| 5ca82fd39c | |||
| da35c263d2 | |||
| 2a617a9639 | |||
| c730aab28f | |||
| 274c2016c0 | |||
| 9b3891f778 | |||
| b705de956e | |||
| e37201f84f | |||
| f53eea81c4 | |||
| 0fa8db1682 | |||
| 46f5224e70 | |||
| 12be2a9775 | |||
| 6196bbdc5e | |||
| b41f4777d4 | |||
| f2812bc706 | |||
| 04500bc237 | |||
| 2a6b877cf1 | |||
| c3896a4613 | |||
| c6fb896fe4 | |||
| 669fbb7e77 | |||
| 971865e4f9 | |||
| 9078e41855 | |||
| 466c48a7d0 | |||
| 31a047ce9e | |||
| bd24ffa5d0 | |||
| 99f4bd7398 | |||
| 417177b097 | |||
| c407cab501 | |||
| 044cf22f47 | |||
| 75aa940d44 | |||
| 7be8080726 | |||
| 13fbc813cd | |||
| 44d1458229 | |||
| f06f3ee2e5 | |||
| a889a02e15 | |||
| 6bf3d6a689 | |||
| 1d7dcca495 | |||
| ad8f049570 | |||
| 73c56a68b6 | |||
| b34b52f305 | |||
| 39d052273d | |||
| e435b9153b | |||
| 0792278927 | |||
| 06d59b3cde | |||
| 1e7497ad33 | |||
| 49d0f2359b | |||
| bb73039205 | |||
| d4d6b7e2ce | |||
| 7b5201599d | |||
| 11c08e9a69 | |||
| 731bb176f7 | |||
| b0fce93de8 | |||
| fdbe89e87e | |||
| a8d0a2293f | |||
| 8ac278bc59 | |||
| 70d6c6b902 | |||
| 0621218e16 | |||
| 2424376fba | |||
| 3973374f3f | |||
| c25a38b82f | |||
| 3c0ba1d7eb | |||
| be678b02c5 | |||
| 0078b48e3c | |||
| 540f1d9bce | |||
| 5e3cb812ec | |||
| 6d10a5dd4c | |||
| 96d14b7ab7 | |||
| b96b026905 | |||
| c25f2d3941 | |||
| 785453aa79 | |||
| 4dbf5327bd | |||
| 603240c467 | |||
| bbc3e7d93f | |||
| fbee4937a0 | |||
| 0a77728652 | |||
| e3ed0cf436 | |||
| d05dc2e4dc | |||
| c437cd3865 | |||
| 442171169b | |||
| cc12dbb6ee | |||
| 60b3a960ae | |||
| 5a957c3c9e | |||
| be4d431dc3 | |||
| 0005c75091 | |||
| 880b382a16 | |||
| d012512a79 | |||
| e2ac842690 | |||
| 67d8d48855 | |||
| 00f2d36cb5 | |||
| 035057b185 | |||
| 982966c8d9 | |||
| f5e3a9ad40 | |||
| 141c3f1ea4 | |||
| 4ea483e3de | |||
| 8eca956cd1 | |||
| c9242a5075 | |||
| df29a5becb | |||
| fb589337f8 | |||
| ea5ee6189d | |||
| a39e47cced | |||
| 49d69f65ad | |||
| 424d677bcb | |||
| 59e4cdc62a | |||
| 9d3dfad98c | |||
| 555b746f4b | |||
| ce6a97d065 | |||
| 88567df36d | |||
| f55cbd9e9a | |||
| 7d00cc1eff | |||
| 29301ddee7 | |||
| 978b773968 | |||
| 4f30cae6aa | |||
| 5f29b66a8d | |||
| b94da1bd19 | |||
| f9b0a0fc13 | |||
| 300ffdae04 | |||
| 476525e0d4 | |||
| edecf9d58f | |||
| 38bf2e116b | |||
| 0719c4d1ae | |||
| 12840231be | |||
| 4728c12225 | |||
| 90526ac563 | |||
| 6f7ea03e35 | |||
| 78900e05ad | |||
| 495f4aa19c | |||
| 88c480759f | |||
| ab75365636 | |||
| 0266617c71 | |||
| aef45c5043 | |||
| deeb0146c7 | |||
| 5dea674f20 | |||
| 646fe34d09 | |||
| c67907aa58 | |||
| e78f4c5ace | |||
| e891fdc3eb | |||
| 95a258c2a5 | |||
| f1fabd09a6 | |||
| 0d77bdaf32 | |||
| 320be2e5d9 | |||
| 6a098ad0b5 | |||
| e895e91a11 | |||
| fc3f7ca4b2 | |||
| 9ad7f0dbac | |||
| 12d8a04c15 | |||
| 0f7a3887a7 | |||
| ef51f29e28 | |||
| b61bbee35a | |||
| 64dd8c463d | |||
| d2a95e9f06 | |||
| 0cb0525516 | |||
| dcaf4fdfe2 | |||
| 0c13757910 | |||
| 0cdcd74c9d | |||
| db3968399f | |||
| 7494a49238 | |||
| 55d2a3c8b1 | |||
| be4e45c22c | |||
| efb28d337a | |||
| edd77e1f32 | |||
| 848dd7e071 | |||
| ba79633758 | |||
| 860973bdbd | |||
| d4d897e79e | |||
| 4850f3d588 | |||
| 8bc53c235f | |||
| c74793b1d5 | |||
| 56bac8a8c1 | |||
| 184575fd54 | |||
| e148559d3e | |||
| 95b76dbb85 |
@@ -1,11 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Do not delete any text from this template!
|
||||
-->
|
||||
|
||||
**Checklist:**
|
||||
|
||||
- [ ] I updated to the latest version available
|
||||
- [ ] I cleared the cache of my browser
|
||||
|
||||
**Home Assistant release with the issue:**
|
||||
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
- Or use this command: hass --version
|
||||
@@ -14,22 +27,50 @@
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
**UI (States or Lovelace UI?):**
|
||||
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
-->
|
||||
|
||||
**Browser and Operating System:**
|
||||
|
||||
<!--
|
||||
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
|
||||
-->
|
||||
|
||||
**Description of problem:**
|
||||
|
||||
<!--
|
||||
Explain what the issue is, and how things should look/behave. If possible provide a screenshot with a description.
|
||||
Explain what the issue is, and what is the current behaviour. If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
**Expected behaviour:**
|
||||
|
||||
<!--
|
||||
Explain how things should look/behave. If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
**Relevant config:**
|
||||
|
||||
<!--
|
||||
Give the config of both the integration that is used, the Lovelace config, scene, automation or otherwise relevant configuration.
|
||||
-->
|
||||
|
||||
**Steps to reproduce this problem:**
|
||||
|
||||
<!--
|
||||
Sum up all steps that are necessary to reproduce this bug.
|
||||
For example:
|
||||
1. Add a climate integration
|
||||
2. Navigate to Lovelace
|
||||
3. Click more info of the climate entity
|
||||
4. Set the hvac action to heat
|
||||
5. Set the temperature higher than the current temperature
|
||||
6. Set the hvac action to cool
|
||||
-->
|
||||
|
||||
**Javascript errors shown in the web inspector (if applicable):**
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ""
|
||||
labels: feature request
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 1
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2020-01-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
|
||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||
issues:
|
||||
daysUntilLock: 30
|
||||
+2
-10
@@ -8,19 +8,11 @@ install: yarn install
|
||||
script:
|
||||
- npm run build
|
||||
- hassio/script/build_hassio
|
||||
# Because else eslint fails because hassio has cleaned that build
|
||||
- ./node_modules/.bin/gulp gen-icons-app
|
||||
- npm run test
|
||||
# - xvfb-run wct --module-resolution=node --npm
|
||||
# - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then wct --module-resolution=node --npm --plugin sauce; fi'
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- 'docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21'
|
||||
deploy:
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
'on':
|
||||
branch: master
|
||||
dist: trusty
|
||||
addons:
|
||||
sauce_connect: true
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
[](https://home-assistant.io/demo/)
|
||||
[](https://demo.home-assistant.io/)
|
||||
|
||||
- [View demo of the Polymer frontend](https://home-assistant.io/demo/)
|
||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||
- [More information about Home Assistant](https://home-assistant.io)
|
||||
- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html)
|
||||
|
||||
@@ -31,3 +31,5 @@ It is possible to compile the project and/or run commands in the development env
|
||||
## License
|
||||
|
||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
|
||||
|
||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
paths:
|
||||
include:
|
||||
- translations/en.json
|
||||
pr: none
|
||||
schedules:
|
||||
- cron: "30 0 * * *"
|
||||
displayName: "translation update"
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
always: true
|
||||
variables:
|
||||
- group: translation
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: 'home-assistant/ci-azure'
|
||||
endpoint: 'home-assistant'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Upload'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 12.x'
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||
|
||||
./script/translations_upload_base
|
||||
displayName: 'Upload Translation'
|
||||
|
||||
- job: 'Download'
|
||||
dependsOn:
|
||||
- 'Upload'
|
||||
condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual'))
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: 'Use Node 12.x'
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
- template: templates/azp-step-git-init.yaml@azure
|
||||
- script: |
|
||||
export LOKALISE_TOKEN="$(lokaliseToken)"
|
||||
export AZURE_BRANCH="$(Build.SourceBranchName)"
|
||||
|
||||
npm install
|
||||
./script/translations_download
|
||||
displayName: 'Download Translation'
|
||||
- script: |
|
||||
git checkout dev
|
||||
git add translation
|
||||
git commit -am "[ci skip] Translation update"
|
||||
git push
|
||||
displayName: 'Update translation'
|
||||
+14
-1
@@ -3,7 +3,7 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
throw Error("latestBuild not defined for babel loader config");
|
||||
}
|
||||
return {
|
||||
test: /\.m?js$/,
|
||||
test: /\.m?js$|\.tsx?$/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
@@ -12,6 +12,12 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
require("@babel/preset-env").default,
|
||||
{ modules: false },
|
||||
],
|
||||
[
|
||||
require("@babel/preset-typescript").default,
|
||||
{
|
||||
jsxPragma: "h",
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
plugins: [
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
@@ -21,6 +27,13 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
],
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/syntax-dynamic-import",
|
||||
[
|
||||
"@babel/transform-react-jsx",
|
||||
{
|
||||
pragma: "h",
|
||||
},
|
||||
],
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
isProdBuild: process.env.NODE_ENV === "production",
|
||||
isStatsBuild: process.env.STATS === "1",
|
||||
isTravis: process.env.TRAVIS === "true",
|
||||
isNetlify: process.env.NETLIFY === "true",
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
// Run HA develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
const envVars = require("../env");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./compress.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
@@ -18,7 +21,7 @@ gulp.task(
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-service-worker-dev",
|
||||
"gen-icons",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
@@ -35,13 +38,11 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel("gen-icons", "build-translations"),
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "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"])
|
||||
),
|
||||
"webpack-prod-app",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis ? [] : ["compress-app"]),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Run cast develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
@@ -16,7 +15,12 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel("gen-icons", "gen-index-cast-dev", "build-translations"),
|
||||
gulp.parallel(
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-index-cast-dev",
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static-cast",
|
||||
"webpack-dev-server-cast"
|
||||
)
|
||||
@@ -29,7 +33,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel("gen-icons", "build-translations"),
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"copy-static-cast",
|
||||
"webpack-prod-cast",
|
||||
"gen-index-cast-prod"
|
||||
|
||||
@@ -9,15 +9,31 @@ gulp.task(
|
||||
return del([config.root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-demo",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.demo_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-cast",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.cast_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-hassio",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.hassio_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", function cleanOutputAndBuildDir() {
|
||||
return del([config.gallery_root, config.build_dir]);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Tasks to compress
|
||||
|
||||
const gulp = require("gulp");
|
||||
const zopfli = require("gulp-zopfli-green");
|
||||
const merge = require("merge-stream");
|
||||
const path = require("path");
|
||||
const paths = require("../paths");
|
||||
|
||||
gulp.task("compress-app", function compressApp() {
|
||||
const jsLatest = gulp
|
||||
.src(path.resolve(paths.output, "**/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(paths.output));
|
||||
|
||||
const jsEs5 = gulp
|
||||
.src(path.resolve(paths.output_es5, "**/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(paths.output_es5));
|
||||
|
||||
const polyfills = gulp
|
||||
.src(path.resolve(paths.static, "polyfills/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(path.resolve(paths.static, "polyfills")));
|
||||
|
||||
const translations = gulp
|
||||
.src(path.resolve(paths.static, "translations/*.json"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(path.resolve(paths.static, "translations")));
|
||||
|
||||
return merge(jsLatest, jsEs5, polyfills, translations);
|
||||
});
|
||||
|
||||
gulp.task("compress-hassio", function compressApp() {
|
||||
return gulp
|
||||
.src(path.resolve(paths.hassio_root, "**/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(paths.hassio_root));
|
||||
});
|
||||
@@ -17,7 +17,8 @@ gulp.task(
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel(
|
||||
"gen-icons",
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-icons-demo",
|
||||
"gen-index-demo-dev",
|
||||
"build-translations"
|
||||
@@ -34,7 +35,12 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
|
||||
gulp.parallel(
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-icons-demo",
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static-demo",
|
||||
"webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
|
||||
@@ -14,7 +14,7 @@ function hasHtml(data) {
|
||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||
} else if (hasHtml(data[key])) {
|
||||
errors.push(`HTML found in ${file.path} at key ${recKey}.${key}`);
|
||||
@@ -23,7 +23,7 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
}
|
||||
|
||||
function checkHtml() {
|
||||
let errors = [];
|
||||
const errors = [];
|
||||
|
||||
return mapStream(function(file, cb) {
|
||||
const content = file.contents;
|
||||
|
||||
@@ -11,12 +11,6 @@ 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 castTemplatePath = (tpl) =>
|
||||
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`);
|
||||
|
||||
const readFile = (pth) => fs.readFileSync(pth).toString();
|
||||
|
||||
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
@@ -25,10 +19,19 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
|
||||
};
|
||||
|
||||
const renderDemoTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, demoTemplatePath);
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(config.demo_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
|
||||
const renderCastTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, castTemplatePath);
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(config.cast_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
|
||||
const renderGalleryTemplate = (pth, data = {}) =>
|
||||
renderTemplate(pth, data, (tpl) =>
|
||||
path.resolve(config.gallery_dir, "src/html/", `${tpl}.html.template`)
|
||||
);
|
||||
|
||||
const minifyHtml = (content) =>
|
||||
minify(content, {
|
||||
@@ -209,8 +212,33 @@ gulp.task("gen-index-demo-prod", (done) => {
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5DemoJS: es5Manifest["main.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
const minified = minifyHtml(content);
|
||||
|
||||
fs.outputFileSync(path.resolve(config.demo_root, "index.html"), minified);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-gallery-dev", (done) => {
|
||||
// In dev mode we don't mangle names, so we hardcode urls. That way we can
|
||||
// run webpack as last in watch mode, which blocks output.
|
||||
const content = renderGalleryTemplate("index", {
|
||||
latestGalleryJS: "./entrypoint.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), content);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-index-gallery-prod", (done) => {
|
||||
const latestManifest = require(path.resolve(
|
||||
config.gallery_output,
|
||||
"manifest.json"
|
||||
));
|
||||
const content = renderGalleryTemplate("index", {
|
||||
latestGalleryJS: latestManifest["entrypoint.js"],
|
||||
});
|
||||
const minified = minifyHtml(content);
|
||||
|
||||
fs.outputFileSync(path.resolve(config.gallery_root, "index.html"), minified);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Run demo develop mode
|
||||
const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
require("./entry-html.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-gallery",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-app", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"gen-index-gallery-dev",
|
||||
"webpack-dev-server-gallery"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-gallery",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"webpack-prod-gallery",
|
||||
"gen-index-gallery-prod"
|
||||
)
|
||||
);
|
||||
@@ -4,8 +4,6 @@ const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const cpx = require("cpx");
|
||||
const fs = require("fs-extra");
|
||||
const zopfli = require("gulp-zopfli-green");
|
||||
const merge = require("merge-stream");
|
||||
const paths = require("../paths");
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
@@ -67,20 +65,6 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
function compressStatic(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
const polyfills = gulp
|
||||
.src(staticPath("polyfills/*.js"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(staticPath("polyfills")));
|
||||
const translations = gulp
|
||||
.src(staticPath("translations/*.json"))
|
||||
.pipe(zopfli())
|
||||
.pipe(gulp.dest(staticPath("translations")));
|
||||
|
||||
return merge(polyfills, translations);
|
||||
}
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
@@ -100,11 +84,12 @@ gulp.task("copy-static", (done) => {
|
||||
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);
|
||||
fs.copySync(
|
||||
polyPath("public/static"),
|
||||
path.resolve(paths.demo_root, "static")
|
||||
);
|
||||
// Copy demo static files
|
||||
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
|
||||
|
||||
@@ -126,3 +111,15 @@ gulp.task("copy-static-cast", (done) => {
|
||||
copyTranslations(paths.cast_static);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("copy-static-gallery", (done) => {
|
||||
// Copy app static files
|
||||
fs.copySync(polyPath("public/static"), paths.gallery_static);
|
||||
// Copy gallery static files
|
||||
fs.copySync(path.resolve(paths.gallery_dir, "public"), paths.gallery_root);
|
||||
|
||||
copyMapPanel(paths.gallery_static);
|
||||
copyFonts(paths.gallery_static);
|
||||
copyTranslations(paths.gallery_static);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -57,18 +57,6 @@ function generateIconset(iconsetName, iconNames) {
|
||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||
}
|
||||
|
||||
// Generate the full MDI iconset
|
||||
function genMDIIcons() {
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
||||
);
|
||||
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
||||
}
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
@@ -101,24 +89,27 @@ function findIcons(searchPath, iconsetName) {
|
||||
return icons;
|
||||
}
|
||||
|
||||
function genHassIcons() {
|
||||
gulp.task("gen-icons-mdi", (done) => {
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
||||
);
|
||||
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-icons-app", (done) => {
|
||||
const iconNames = findIcons("./src", "hass");
|
||||
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
||||
}
|
||||
|
||||
gulp.task("gen-icons-mdi", (done) => {
|
||||
genMDIIcons();
|
||||
done();
|
||||
});
|
||||
gulp.task("gen-icons-hass", (done) => {
|
||||
genHassIcons();
|
||||
done();
|
||||
});
|
||||
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
|
||||
|
||||
gulp.task("gen-icons-demo", (done) => {
|
||||
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
||||
@@ -129,8 +120,21 @@ gulp.task("gen-icons-demo", (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
};
|
||||
gulp.task("gen-icons-hassio", (done) => {
|
||||
const iconNames = findIcons(
|
||||
path.resolve(paths.hassio_dir, "./src"),
|
||||
"hassio"
|
||||
);
|
||||
// Find hassio icons inside HA main repo.
|
||||
for (const item of findIcons(
|
||||
path.resolve(paths.polymer_dir, "./src"),
|
||||
"hassio"
|
||||
)) {
|
||||
iconNames.add(item);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.hassio_dir, "hassio-icons.html"),
|
||||
generateIconset("hassio", iconNames)
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const gulp = require("gulp");
|
||||
|
||||
const envVars = require("../env");
|
||||
|
||||
require("./clean.js");
|
||||
require("./gen-icons.js");
|
||||
require("./webpack.js");
|
||||
require("./compress.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||
"webpack-watch-hassio"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||
"webpack-prod-hassio",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis ? [] : ["compress-hassio"])
|
||||
)
|
||||
);
|
||||
@@ -45,11 +45,10 @@ function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
if (typeof data[key] === "object") {
|
||||
output = Object.assign(
|
||||
{},
|
||||
output,
|
||||
recursiveFlatten(prefix + key + ".", data[key])
|
||||
);
|
||||
output = {
|
||||
...output,
|
||||
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||
};
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
}
|
||||
@@ -99,18 +98,16 @@ function recursiveEmpty(data) {
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokalise_transform(data, original) {
|
||||
function lokaliseTransform(data, original, file) {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokalise_transform(value, original);
|
||||
output[key] = lokaliseTransform(value, original, file);
|
||||
} 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`
|
||||
);
|
||||
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
@@ -183,7 +180,7 @@ gulp.task(
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokalise_transform(data, data);
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
@@ -198,6 +195,11 @@ gulp.task(
|
||||
gulp.series("build-master-translation", function() {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
foreach(function(stream, file) {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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");
|
||||
@@ -9,8 +8,33 @@ const {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
} = require("../webpack");
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
createConfigFunc({ ...params, latestBuild: true }),
|
||||
createConfigFunc({ ...params, latestBuild: false }),
|
||||
];
|
||||
|
||||
const runDevServer = ({
|
||||
compiler,
|
||||
contentBase,
|
||||
port,
|
||||
listenHost = "localhost",
|
||||
}) =>
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase,
|
||||
}).listen(port, listenHost, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", `http://localhost:${port}`);
|
||||
});
|
||||
|
||||
const handler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
console.log(err.stack || err);
|
||||
@@ -32,20 +56,11 @@ const handler = (done) => (err, stats) => {
|
||||
};
|
||||
|
||||
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
|
||||
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
|
||||
{},
|
||||
handler()
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
@@ -53,47 +68,17 @@ gulp.task(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
bothBuilds(createAppConfig, { isProdBuild: true }),
|
||||
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(8090, "localhost", function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", "http://localhost:8090");
|
||||
runDevServer({
|
||||
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
|
||||
contentBase: paths.demo_root,
|
||||
port: 8090,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,51 +87,22 @@ gulp.task(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: false,
|
||||
}),
|
||||
],
|
||||
bothBuilds(createDemoConfig, {
|
||||
isProdBuild: true,
|
||||
}),
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-cast", () => {
|
||||
const compiler = webpack([
|
||||
createCastConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
}),
|
||||
createCastConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase: path.resolve(paths.cast_dir, "dist"),
|
||||
}).listen(
|
||||
8080,
|
||||
runDevServer({
|
||||
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
|
||||
contentBase: paths.cast_root,
|
||||
port: 8080,
|
||||
// Accessible from the network, because that's how Cast hits it.
|
||||
"0.0.0.0",
|
||||
function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", "http://localhost:8080");
|
||||
}
|
||||
);
|
||||
listenHost: "0.0.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
@@ -154,16 +110,59 @@ gulp.task(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
[
|
||||
createCastConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
}),
|
||||
createCastConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
}),
|
||||
],
|
||||
bothBuilds(createCastConfig, {
|
||||
isProdBuild: true,
|
||||
}),
|
||||
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-watch-hassio", () => {
|
||||
// we are not calling done, so this command will run forever
|
||||
webpack(
|
||||
createHassioConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: false,
|
||||
})
|
||||
).watch({}, handler());
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-hassio",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
createHassioConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: false,
|
||||
}),
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-gallery", () => {
|
||||
runDevServer({
|
||||
compiler: webpack(
|
||||
createGalleryConfig({ latestBuild: true, isProdBuild: false })
|
||||
),
|
||||
contentBase: paths.gallery_root,
|
||||
port: 8100,
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"webpack-prod-gallery",
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
webpack(
|
||||
createGalleryConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
}),
|
||||
|
||||
handler(resolve)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -20,4 +20,13 @@ module.exports = {
|
||||
cast_static: path.resolve(__dirname, "../cast/dist/static"),
|
||||
cast_output: path.resolve(__dirname, "../cast/dist/frontend_latest"),
|
||||
cast_output_es5: path.resolve(__dirname, "../cast/dist/frontend_es5"),
|
||||
|
||||
gallery_dir: path.resolve(__dirname, "../gallery"),
|
||||
gallery_root: path.resolve(__dirname, "../gallery/dist"),
|
||||
gallery_output: path.resolve(__dirname, "../gallery/dist/frontend_latest"),
|
||||
gallery_static: path.resolve(__dirname, "../gallery/dist/static"),
|
||||
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_publicPath: "/api/hassio/app/",
|
||||
};
|
||||
|
||||
+197
-241
@@ -3,8 +3,6 @@ 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");
|
||||
@@ -17,288 +15,246 @@ if (!version) {
|
||||
}
|
||||
version = version[0];
|
||||
|
||||
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
|
||||
const genDevTool = (isProdBuild) =>
|
||||
isProdBuild ? "source-map" : "inline-cheap-module-source-map";
|
||||
const genFilename = (isProdBuild, dontHash = new Set()) => ({ chunk }) => {
|
||||
if (!isProdBuild || dontHash.has(chunk.name)) {
|
||||
return `${chunk.name}.js`;
|
||||
}
|
||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||
};
|
||||
const genChunkFilename = (isProdBuild, isStatsBuild) =>
|
||||
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
|
||||
const resolve = {
|
||||
extensions: [".ts", ".js", ".json", ".tsx"],
|
||||
alias: {
|
||||
react: "preact-compat",
|
||||
"react-dom": "preact-compat",
|
||||
// Not necessary unless you consume a module using `createClass`
|
||||
"create-react-class": "preact-compat/lib/create-react-class",
|
||||
// Not necessary unless you consume a module requiring `react-dom-factories`
|
||||
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
||||
},
|
||||
};
|
||||
|
||||
const tsLoader = (latestBuild) => ({
|
||||
test: /\.ts|tsx$/,
|
||||
exclude: [path.resolve(paths.polymer_dir, "node_modules")],
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: latestBuild
|
||||
? { noEmit: false }
|
||||
: { target: "es5", noEmit: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const cssLoader = {
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
};
|
||||
const htmlLoader = {
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
// Ignore moment.js locales
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// Color.js is bloated, it contains all color definitions for all material color sets.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@polymer\/paper-styles\/color\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// Ignore roboto pointing at CDN. We use local font-roboto-local.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@polymer\/font-roboto\/roboto\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// Ignore mwc icons pointing at CDN.
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@material\/mwc-icon\/mwc-icon-font\.js$/,
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
];
|
||||
|
||||
const optimization = (latestBuild) => ({
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
terserOptions: {
|
||||
safari10: true,
|
||||
ecma: latestBuild ? undefined : 5,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
const isCI = process.env.CI === "true";
|
||||
|
||||
// Create an object mapping browser urls to their paths during build
|
||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||
const workBoxTranslationsTemplatedURLs = {};
|
||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||
Object.keys(englishFP).forEach((key) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
});
|
||||
|
||||
const entry = {
|
||||
app: "./src/entrypoints/app.ts",
|
||||
authorize: "./src/entrypoints/authorize.ts",
|
||||
onboarding: "./src/entrypoints/onboarding.ts",
|
||||
core: "./src/entrypoints/core.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
||||
};
|
||||
|
||||
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
|
||||
if (!latestBuild) {
|
||||
rules.push(babelLoaderConfig({ latestBuild }));
|
||||
}
|
||||
|
||||
const createWebpackConfig = ({
|
||||
entry,
|
||||
outputRoot,
|
||||
defineOverlay,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
}) => {
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
|
||||
entry,
|
||||
module: {
|
||||
rules,
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
terserOptions: {
|
||||
safari10: true,
|
||||
ecma: latestBuild ? undefined : 5,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__DEMO__: false,
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__DEMO__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
...defineOverlay,
|
||||
}),
|
||||
...plugins,
|
||||
isProdBuild &&
|
||||
!isCI &&
|
||||
!isStatsBuild &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
algorithm(input, compressionOptions, callback) {
|
||||
return zopfli.gzip(input, compressionOptions, callback);
|
||||
},
|
||||
}),
|
||||
latestBuild &&
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: "./src/entrypoints/service-worker-hass.js",
|
||||
swDest: "service_worker.js",
|
||||
importWorkboxFrom: "local",
|
||||
include: [/\.js$/],
|
||||
templatedURLs: {
|
||||
...workBoxTranslationsTemplatedURLs,
|
||||
"/static/icons/favicon-192x192.png":
|
||||
"public/icons/favicon-192x192.png",
|
||||
"/static/fonts/roboto/Roboto-Light.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
||||
"/static/fonts/roboto/Roboto-Medium.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
||||
"/static/fonts/roboto/Roboto-Regular.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
||||
"/static/fonts/roboto/Roboto-Bold.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
||||
},
|
||||
}),
|
||||
// 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")
|
||||
),
|
||||
].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: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: latestBuild ? paths.output : paths.output_es5,
|
||||
filename: ({ chunk }) => {
|
||||
const dontHash = new Set();
|
||||
|
||||
if (!isProdBuild || dontHash.has(chunk.name)) {
|
||||
return `${chunk.name}.js`;
|
||||
}
|
||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||
},
|
||||
chunkFilename:
|
||||
isProdBuild && !isStatsBuild
|
||||
? "chunk.[chunkhash].js"
|
||||
: "[name].chunk.js",
|
||||
path: path.resolve(
|
||||
outputRoot,
|
||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||
),
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
},
|
||||
resolve,
|
||||
};
|
||||
};
|
||||
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
const config = createWebpackConfig({
|
||||
entry: {
|
||||
app: "./src/entrypoints/app.ts",
|
||||
authorize: "./src/entrypoints/authorize.ts",
|
||||
onboarding: "./src/entrypoints/onboarding.ts",
|
||||
core: "./src/entrypoints/core.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
||||
},
|
||||
outputRoot: paths.root,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
});
|
||||
|
||||
if (latestBuild) {
|
||||
// Create an object mapping browser urls to their paths during build
|
||||
const translationMetadata = require("../build-translations/translationMetadata.json");
|
||||
const workBoxTranslationsTemplatedURLs = {};
|
||||
const englishFP = translationMetadata.translations.en.fingerprints;
|
||||
Object.keys(englishFP).forEach((key) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
});
|
||||
|
||||
config.plugins.push(
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: "./src/entrypoints/service-worker-hass.js",
|
||||
swDest: "service_worker.js",
|
||||
importWorkboxFrom: "local",
|
||||
include: [/\.js$/],
|
||||
templatedURLs: {
|
||||
...workBoxTranslationsTemplatedURLs,
|
||||
"/static/icons/favicon-192x192.png":
|
||||
"public/icons/favicon-192x192.png",
|
||||
"/static/fonts/roboto/Roboto-Light.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2",
|
||||
"/static/fonts/roboto/Roboto-Medium.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2",
|
||||
"/static/fonts/roboto/Roboto-Regular.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2",
|
||||
"/static/fonts/roboto/Roboto-Bold.woff2":
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
|
||||
if (!latestBuild) {
|
||||
rules.push(babelLoaderConfig({ latestBuild }));
|
||||
}
|
||||
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
return createWebpackConfig({
|
||||
entry: {
|
||||
main: "./demo/src/entrypoint.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
},
|
||||
module: {
|
||||
rules,
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
||||
__DEMO__: true,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
...plugins,
|
||||
].filter(Boolean),
|
||||
resolve,
|
||||
output: {
|
||||
filename: genFilename(isProdBuild),
|
||||
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
|
||||
path: path.resolve(
|
||||
paths.demo_root,
|
||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
||||
compatibility: path.resolve(
|
||||
paths.polymer_dir,
|
||||
"src/entrypoints/compatibility.ts"
|
||||
),
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
},
|
||||
};
|
||||
outputRoot: paths.demo_root,
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
||||
__DEMO__: true,
|
||||
},
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
});
|
||||
};
|
||||
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) => {
|
||||
const isStatsBuild = false;
|
||||
const entry = {
|
||||
launcher: "./cast/src/launcher/entrypoint.ts",
|
||||
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
|
||||
};
|
||||
|
||||
if (latestBuild) {
|
||||
entry.receiver = "./cast/src/receiver/entrypoint.ts";
|
||||
entry.receiver = path.resolve(paths.cast_dir, "src/receiver/entrypoint.ts");
|
||||
}
|
||||
|
||||
const rules = [tsLoader(latestBuild), cssLoader, htmlLoader];
|
||||
if (!latestBuild) {
|
||||
rules.push(babelLoaderConfig({ latestBuild }));
|
||||
}
|
||||
|
||||
return {
|
||||
mode: genMode(isProdBuild),
|
||||
devtool: genDevTool(isProdBuild),
|
||||
return createWebpackConfig({
|
||||
entry,
|
||||
module: {
|
||||
rules,
|
||||
outputRoot: paths.cast_root,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
});
|
||||
};
|
||||
|
||||
const createHassioConfig = ({ isProdBuild, latestBuild }) => {
|
||||
if (latestBuild) {
|
||||
throw new Error("Hass.io does not support latest build!");
|
||||
}
|
||||
const config = createWebpackConfig({
|
||||
entry: {
|
||||
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
|
||||
},
|
||||
optimization: optimization(latestBuild),
|
||||
plugins: [
|
||||
new ManifestPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__DEMO__: false,
|
||||
__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.cast_root,
|
||||
latestBuild ? "frontend_latest" : "frontend_es5"
|
||||
),
|
||||
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
|
||||
// For workerize loader
|
||||
globalObject: "self",
|
||||
outputRoot: "",
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
});
|
||||
|
||||
config.output.path = paths.hassio_root;
|
||||
config.output.publicPath = paths.hassio_publicPath;
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) => {
|
||||
if (!latestBuild) {
|
||||
throw new Error("Gallery only supports latest build!");
|
||||
}
|
||||
const config = createWebpackConfig({
|
||||
entry: {
|
||||
entrypoint: path.resolve(paths.gallery_dir, "src/entrypoint.js"),
|
||||
},
|
||||
};
|
||||
outputRoot: paths.gallery_root,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolve,
|
||||
plugins,
|
||||
optimization,
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
|
||||
Content-Security-Policy: form-action https:
|
||||
Feature-Policy: vibrate 'none'; geolocation 'none'; midi 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; payment 'none'
|
||||
Referrer-Policy: no-referrer-when-downgrade
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
|
||||
/images/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
|
||||
/manifest.json
|
||||
Cache-Control: public, max-age: 3600, s-maxage=3600
|
||||
|
||||
/frontend_es5/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
|
||||
/frontend_latest/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
@@ -39,6 +39,7 @@ class HcLovelace extends LitElement {
|
||||
mode: "storage",
|
||||
language: "en",
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
};
|
||||
return this.lovelaceConfig.views[index].panel
|
||||
|
||||
@@ -175,9 +175,9 @@ export class HcMain extends HassElement {
|
||||
} catch (err) {
|
||||
// Generate a Lovelace config.
|
||||
this._unsubLovelace = () => undefined;
|
||||
const {
|
||||
generateLovelaceConfigFromHass,
|
||||
} = await import("../../../../src/panels/lovelace/common/generate-lovelace-config");
|
||||
const { generateLovelaceConfigFromHass } = await import(
|
||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const { createCastConfig } = require("../build-scripts/webpack.js");
|
||||
const { isProdBuild } = require("../build-scripts/env.js");
|
||||
|
||||
// File just used for stats builds
|
||||
|
||||
const latestBuild = true;
|
||||
|
||||
module.exports = createCastConfig({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Cache-Control: public, max-age: 0, s-maxage=3600, must-revalidate
|
||||
Content-Security-Policy: form-action https:
|
||||
Referrer-Policy: no-referrer-when-downgrade
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 1; mode=block
|
||||
|
||||
/api/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
|
||||
/frontend_es5/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
|
||||
/frontend_latest/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
@@ -7,22 +7,26 @@
|
||||
{
|
||||
"src": "/static/icons/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/favicon-1024x1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
],
|
||||
"lang": "en-US",
|
||||
|
||||
@@ -217,6 +217,18 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||
icon: "hademo:currency-usd",
|
||||
},
|
||||
},
|
||||
"sensor.study_temp": {
|
||||
entity_id: "sensor.study_temp",
|
||||
state: "20.9",
|
||||
attributes: {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
friendly_name: localize(
|
||||
"ui.panel.page-demo.config.arsaboo.names.temperature_study"
|
||||
),
|
||||
icon: "hademo:thermometer",
|
||||
},
|
||||
},
|
||||
"cover.garagedoor": {
|
||||
entity_id: "cover.garagedoor",
|
||||
state: "closed",
|
||||
|
||||
@@ -446,6 +446,11 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
|
||||
"script.tv_off",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "sensor",
|
||||
entity: "sensor.study_temp",
|
||||
graph: "line",
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
title: "Doorbell",
|
||||
|
||||
@@ -23,27 +23,24 @@ export const demoThemeJimpower = () => ({
|
||||
"paper-listbox-background-color": "#2E333A",
|
||||
"table-row-background-color": "#353840",
|
||||
"paper-grey-50": "var(--primary-text-color)",
|
||||
"paper-toggle-button-checked-button-color": "var(--accent-color)",
|
||||
"switch-checked-color": "var(--accent-color)",
|
||||
"paper-dialog-background-color": "#434954",
|
||||
"secondary-text-color": "#5294E2",
|
||||
"google-red-500": "#E45E65",
|
||||
"divider-color": "rgba(0, 0, 0, .12)",
|
||||
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
|
||||
"google-green-500": "#39E949",
|
||||
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "var(--primary-color)",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
|
||||
"paper-card-background-color": "#434954",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
"dark-primary-color": "var(--accent-color)",
|
||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||
"paper-slider-pin-color": "var(--accent-color)",
|
||||
"paper-item-icon-active-color": "#F9C536",
|
||||
"accent-color": "#E45E65",
|
||||
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
|
||||
"table-row-alternative-background-color": "#3E424B",
|
||||
});
|
||||
|
||||
@@ -24,27 +24,24 @@ export const demoThemeKernehed = () => ({
|
||||
"paper-listbox-background-color": "#141414",
|
||||
"table-row-background-color": "#292929",
|
||||
"paper-grey-50": "var(--primary-text-color)",
|
||||
"paper-toggle-button-checked-button-color": "var(--accent-color)",
|
||||
"switch-checked-color": "var(--accent-color)",
|
||||
"paper-dialog-background-color": "#292929",
|
||||
"secondary-text-color": "#b58e31",
|
||||
"google-red-500": "#b58e31",
|
||||
"divider-color": "rgba(0, 0, 0, .12)",
|
||||
"paper-toggle-button-unchecked-ink-color": "var(--disabled-text-color)",
|
||||
"google-green-500": "#2980b9",
|
||||
"paper-toggle-button-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"switch-unchecked-button-color": "var(--disabled-text-color)",
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "#777777",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"paper-toggle-button-checked-ink-color": "var(--accent-color)",
|
||||
"paper-card-background-color": "#292929",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"paper-toggle-button-unchecked-bar-color": "var(--disabled-text-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
"dark-primary-color": "var(--accent-color)",
|
||||
"paper-slider-secondary-color": "var(--secondary-background-color)",
|
||||
"paper-slider-pin-color": "var(--accent-color)",
|
||||
"paper-item-icon-active-color": "#b58e31",
|
||||
"accent-color": "#2980b9",
|
||||
"paper-toggle-button-checked-bar-color": "var(--accent-color)",
|
||||
"table-row-alternative-background-color": "#292929",
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@ export const demoThemeTeachingbirds = () => ({
|
||||
"paper-slider-knob-color": "var(--primary-color)",
|
||||
"paper-listbox-color": "#FFFFFF",
|
||||
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
|
||||
"paper-toggle-button-checked-ink-color": "var(--dark-primary-color)",
|
||||
"paper-toggle-button-unchecked-bar-color": "var(--primary-text-color)",
|
||||
"switch-unchecked-track-color": "var(--primary-text-color)",
|
||||
"paper-card-background-color": "#4e4e4e",
|
||||
"label-badge-text-color": "var(--text-primary-color)",
|
||||
"primary-background-color": "#303030",
|
||||
@@ -22,7 +21,7 @@ export const demoThemeTeachingbirds = () => ({
|
||||
"secondary-background-color": "#2b2b2b",
|
||||
"paper-slider-knob-start-color": "var(--primary-color)",
|
||||
"paper-item-icon-active-color": "#d8bf50",
|
||||
"paper-toggle-button-checked-button-color": "var(--primary-color)",
|
||||
"switch-checked-color": "var(--primary-color)",
|
||||
"secondary-text-color": "#389638",
|
||||
"disabled-text-color": "#545454",
|
||||
"paper-item-icon_-_color": "var(--primary-text-color)",
|
||||
|
||||
@@ -53,7 +53,7 @@ class CardModder extends LitElement {
|
||||
for (var k in this._config.style) {
|
||||
if (window.cardTools.hasTemplate(this._config.style[k]))
|
||||
this.templated.push(k);
|
||||
this.card.style.setProperty(k, '');
|
||||
this.card.style.setProperty(k, "");
|
||||
target.style.setProperty(
|
||||
k,
|
||||
window.cardTools.parseTemplate(this._config.style[k])
|
||||
|
||||
@@ -161,8 +161,8 @@ if (!window.cardTools) {
|
||||
};
|
||||
|
||||
cardTools.longpress = (element) => {
|
||||
customElements.whenDefined("long-press").then(() => {
|
||||
const longpress = document.body.querySelector("long-press");
|
||||
customElements.whenDefined("action-handler").then(() => {
|
||||
const longpress = document.body.querySelector("action-handler");
|
||||
longpress.bind(element);
|
||||
});
|
||||
return element;
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyDeclarations,
|
||||
} from "lit-element";
|
||||
import { LitElement, html, CSSResult, css, property } from "lit-element";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
@@ -20,19 +14,11 @@ import {
|
||||
} from "../configs/demo-configs";
|
||||
|
||||
export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
public lovelace?: Lovelace;
|
||||
public hass!: MockHomeAssistant;
|
||||
private _switching?: boolean;
|
||||
@property() public lovelace?: Lovelace;
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
@property() private _switching?: boolean;
|
||||
private _hidden = localStorage.hide_demo_card;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
lovelace: {},
|
||||
hass: {},
|
||||
_switching: {},
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize() {
|
||||
return this._hidden ? 0 : 2;
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ import "./resources/hademo-icons";
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() => {
|
||||
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
|
||||
import(
|
||||
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
+66
-61
@@ -65,74 +65,79 @@ const generateHistory = (state, deltas) => {
|
||||
const incrementalUnits = ["clients", "queries", "ads"];
|
||||
|
||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockAPI(new RegExp("history/period/.+"), (
|
||||
hass,
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
// @ts-ignore
|
||||
parameters
|
||||
) => {
|
||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||
const entities = params.filter_entity_id.split(",");
|
||||
mockHass.mockAPI(
|
||||
new RegExp("history/period/.+"),
|
||||
(
|
||||
hass,
|
||||
// @ts-ignore
|
||||
method,
|
||||
path,
|
||||
// @ts-ignore
|
||||
parameters
|
||||
) => {
|
||||
const params = parseQuery<HistoryQueryParams>(path.split("?")[1]);
|
||||
const entities = params.filter_entity_id.split(",");
|
||||
|
||||
const results: HassEntity[][] = [];
|
||||
const results: HassEntity[][] = [];
|
||||
|
||||
for (const entityId of entities) {
|
||||
const state = hass.states[entityId];
|
||||
for (const entityId of entities) {
|
||||
const state = hass.states[entityId];
|
||||
|
||||
if (!state) {
|
||||
continue;
|
||||
}
|
||||
if (!state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!state.attributes.unit_of_measurement) {
|
||||
results.push(generateHistory(state, [state.state]));
|
||||
continue;
|
||||
}
|
||||
if (!state.attributes.unit_of_measurement) {
|
||||
results.push(generateHistory(state, [state.state]));
|
||||
continue;
|
||||
}
|
||||
|
||||
const numberState = Number(state.state);
|
||||
const numberState = Number(state.state);
|
||||
|
||||
if (isNaN(numberState)) {
|
||||
// tslint:disable-next-line
|
||||
console.log(
|
||||
"Ignoring state with unparsable state but with a unit",
|
||||
entityId,
|
||||
state
|
||||
if (isNaN(numberState)) {
|
||||
// tslint:disable-next-line
|
||||
console.log(
|
||||
"Ignoring state with unparsable state but with a unit",
|
||||
entityId,
|
||||
state
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const statesToGenerate = 15;
|
||||
let genFunc;
|
||||
|
||||
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
||||
let initial = Math.floor(
|
||||
numberState * 0.4 + numberState * Math.random() * 0.2
|
||||
);
|
||||
const diff = Math.max(
|
||||
1,
|
||||
Math.floor((numberState - initial) / statesToGenerate)
|
||||
);
|
||||
genFunc = () => {
|
||||
initial += diff;
|
||||
return Math.min(numberState, initial);
|
||||
};
|
||||
} else {
|
||||
const diff = Math.floor(
|
||||
numberState * (numberState > 80 ? 0.05 : 0.5)
|
||||
);
|
||||
genFunc = () =>
|
||||
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
||||
}
|
||||
|
||||
results.push(
|
||||
generateHistory(
|
||||
{
|
||||
entity_id: state.entity_id,
|
||||
attributes: state.attributes,
|
||||
},
|
||||
Array.from({ length: statesToGenerate }, genFunc)
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const statesToGenerate = 15;
|
||||
let genFunc;
|
||||
|
||||
if (incrementalUnits.includes(state.attributes.unit_of_measurement)) {
|
||||
let initial = Math.floor(
|
||||
numberState * 0.4 + numberState * Math.random() * 0.2
|
||||
);
|
||||
const diff = Math.max(
|
||||
1,
|
||||
Math.floor((numberState - initial) / statesToGenerate)
|
||||
);
|
||||
genFunc = () => {
|
||||
initial += diff;
|
||||
return Math.min(numberState, initial);
|
||||
};
|
||||
} else {
|
||||
const diff = Math.floor(numberState * (numberState > 80 ? 0.05 : 0.5));
|
||||
genFunc = () =>
|
||||
numberState - diff + Math.floor(Math.random() * 2 * diff);
|
||||
}
|
||||
|
||||
results.push(
|
||||
generateHistory(
|
||||
{
|
||||
entity_id: state.entity_id,
|
||||
attributes: state.attributes,
|
||||
},
|
||||
Array.from({ length: statesToGenerate }, genFunc)
|
||||
)
|
||||
);
|
||||
return results;
|
||||
}
|
||||
return results;
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,9 +12,10 @@ export const mockLovelace = (
|
||||
localizePromise: Promise<LocalizeFunc>
|
||||
) => {
|
||||
hass.mockWS("lovelace/config", () =>
|
||||
Promise.all([selectedDemoConfig, localizePromise]).then(
|
||||
([config, localize]) => config.lovelace(localize)
|
||||
)
|
||||
Promise.all([
|
||||
selectedDemoConfig,
|
||||
localizePromise,
|
||||
]).then(([config, localize]) => config.lovelace(localize))
|
||||
);
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const { createDemoConfig } = require("../build-scripts/webpack.js");
|
||||
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
|
||||
|
||||
// This file exists because we haven't migrated the stats script yet
|
||||
// File just used for stats builds
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const isStatsBuild = process.env.STATS === "1";
|
||||
const latestBuild = false;
|
||||
const latestBuild = true;
|
||||
|
||||
module.exports = createDemoConfig({
|
||||
isProdBuild,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#2157BC">
|
||||
<title>HAGallery</title>
|
||||
<script src='./main.js' async></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -4,14 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUTPUT_DIR=dist
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
||||
cd ..
|
||||
./node_modules/.bin/gulp build-translations gen-icons
|
||||
cd gallery
|
||||
|
||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
|
||||
./node_modules/.bin/gulp build-gallery
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
cd ..
|
||||
./node_modules/.bin/gulp build-translations gen-icons
|
||||
cd gallery
|
||||
|
||||
../node_modules/.bin/webpack-dev-server
|
||||
./node_modules/.bin/gulp develop-gallery
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import JsYaml from "js-yaml";
|
||||
import { safeLoad } from "js-yaml";
|
||||
|
||||
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
|
||||
|
||||
@@ -62,7 +62,7 @@ class DemoCard extends PolymerElement {
|
||||
card.removeChild(card.lastChild);
|
||||
}
|
||||
|
||||
const el = createCardElement(JsYaml.safeLoad(config.config)[0]);
|
||||
const el = createCardElement(safeLoad(config.config)[0]);
|
||||
el.hass = this.hass;
|
||||
card.appendChild(el);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ class DemoCards extends PolymerElement {
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
<ha-switch checked="{{_showConfig}}">Show config</ha-switch>
|
||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||
Show config
|
||||
</ha-switch>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class="cards">
|
||||
@@ -51,6 +53,10 @@ class DemoCards extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-cards", DemoCards);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { html, LitElement, TemplateResult } from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
|
||||
import "../../../src/components/ha-card";
|
||||
import { longPress } from "../../../src/panels/lovelace/common/directives/long-press-directive";
|
||||
import { actionHandler } from "../../../src/panels/lovelace/common/directives/action-handler-directive";
|
||||
import { ActionHandlerEvent } from "../../../src/data/lovelace";
|
||||
|
||||
export class DemoUtilLongPress extends LitElement {
|
||||
protected render(): TemplateResult | void {
|
||||
@@ -12,9 +13,8 @@ export class DemoUtilLongPress extends LitElement {
|
||||
() => html`
|
||||
<ha-card>
|
||||
<mwc-button
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
.longPress="${longPress()}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({})}
|
||||
>
|
||||
(long) press me!
|
||||
</mwc-button>
|
||||
@@ -28,12 +28,8 @@ export class DemoUtilLongPress extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTap(ev: Event) {
|
||||
this._addValue(ev, "tap");
|
||||
}
|
||||
|
||||
private _handleHold(ev: Event) {
|
||||
this._addValue(ev, "hold");
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
this._addValue(ev, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _addValue(ev: Event, value: string) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#2157BC" />
|
||||
<title>HAGallery</title>
|
||||
|
||||
<script type="module" src="<%= latestGalleryJS %>"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
+59
-75
@@ -1,6 +1,6 @@
|
||||
const path = require("path");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const webpackBase = require("../build-scripts/webpack.js");
|
||||
const { createGalleryConfig } = require("../build-scripts/webpack.js");
|
||||
const { babelLoaderConfig } = require("../build-scripts/babel.js");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
@@ -9,80 +9,64 @@ const buildPath = path.resolve(__dirname, "dist");
|
||||
const publicPath = isProd ? "./" : "http://localhost:8080/";
|
||||
const latestBuild = true;
|
||||
|
||||
const rules = [
|
||||
{
|
||||
exclude: [path.resolve(__dirname, "../node_modules")],
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: latestBuild
|
||||
? { noEmit: false }
|
||||
: {
|
||||
target: "es5",
|
||||
noEmit: false,
|
||||
},
|
||||
module.exports = createGalleryConfig({
|
||||
latestBuild: true,
|
||||
});
|
||||
|
||||
const bla = () => {
|
||||
const oldExports = {
|
||||
mode: isProd ? "production" : "development",
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProd ? "none" : "inline-source-map",
|
||||
entry: "./src/entrypoint.js",
|
||||
module: {
|
||||
rules: [
|
||||
babelLoaderConfig({ latestBuild }),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!latestBuild) {
|
||||
rules.push(babelLoaderConfig({ latestBuild }));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: isProd ? "production" : "development",
|
||||
// Disabled in prod while we make Home Assistant able to serve the right files.
|
||||
// Was source-map
|
||||
devtool: isProd ? "none" : "inline-source-map",
|
||||
entry: "./src/entrypoint.js",
|
||||
module: {
|
||||
rules,
|
||||
},
|
||||
optimization: webpackBase.optimization(latestBuild),
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
"public",
|
||||
{ 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/roboto-fontface/fonts/roboto/*.woff2",
|
||||
to: "static/fonts/roboto/",
|
||||
},
|
||||
{
|
||||
from: "../node_modules/leaflet/dist/images",
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
]),
|
||||
].filter(Boolean),
|
||||
resolve: webpackBase.resolve,
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
path: buildPath,
|
||||
publicPath,
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./public",
|
||||
},
|
||||
optimization: webpackBase.optimization(latestBuild),
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
"public",
|
||||
{ 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/roboto-fontface/fonts/roboto/*.woff2",
|
||||
to: "static/fonts/roboto/",
|
||||
},
|
||||
{
|
||||
from: "../node_modules/leaflet/dist/images",
|
||||
to: "static/images/leaflet/",
|
||||
},
|
||||
]),
|
||||
].filter(Boolean),
|
||||
resolve: webpackBase.resolve,
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename: chunkFilename,
|
||||
path: buildPath,
|
||||
publicPath,
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./public",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUTPUT_DIR=build
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
||||
node script/gen-icons.js
|
||||
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
|
||||
./node_modules/.bin/gulp build-hassio
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUTPUT_DIR=build
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
mkdir $OUTPUT_DIR
|
||||
node script/gen-icons.js
|
||||
../node_modules/.bin/webpack --watch --progress
|
||||
./node_modules/.bin/gulp develop-hassio
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
} = require("../../build-scripts/gulp/gen-icons.js");
|
||||
|
||||
function genHassioIcons() {
|
||||
const iconNames = findIcons("./src", "hassio");
|
||||
|
||||
for (const item of findIcons("../src", "hassio")) {
|
||||
iconNames.add(item);
|
||||
}
|
||||
|
||||
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
|
||||
}
|
||||
|
||||
genMDIIcons();
|
||||
genHassioIcons();
|
||||
@@ -44,9 +44,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
selected="{{selectedInput}}"
|
||||
>
|
||||
<template is="dom-repeat" items="[[inputDevices]]">
|
||||
<paper-item device\$="[[item.device]]"
|
||||
>[[item.name]]</paper-item
|
||||
>
|
||||
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
@@ -57,9 +55,7 @@ class HassioAddonAudio extends EventsMixin(PolymerElement) {
|
||||
selected="{{selectedOutput}}"
|
||||
>
|
||||
<template is="dom-repeat" items="[[outputDevices]]">
|
||||
<paper-item device\$="[[item.device]]"
|
||||
>[[item.name]]</paper-item
|
||||
>
|
||||
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
|
||||
</template>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
|
||||
@@ -373,19 +373,21 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<iron-icon icon="hassio:information"></iron-icon>
|
||||
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
|
||||
</span>
|
||||
<template is="dom-if" if="[[_computeUsesProtectedOptions(addon)]]">
|
||||
<div class="state">
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<iron-icon icon="hassio:information"></iron-icon>
|
||||
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<ha-switch
|
||||
on-change="protectionToggled"
|
||||
checked="[[addon.protected]]"
|
||||
></ha-switch>
|
||||
</div>
|
||||
<ha-switch
|
||||
on-change="protectionToggled"
|
||||
checked="[[addon.protected]]"
|
||||
></ha-switch>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -569,7 +571,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
openChangelog() {
|
||||
this.hass
|
||||
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
|
||||
.then((resp) => resp, () => "Error getting changelog")
|
||||
.then(
|
||||
(resp) => resp,
|
||||
() => "Error getting changelog"
|
||||
)
|
||||
.then((content) => {
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: "Changelog",
|
||||
@@ -607,6 +612,10 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
return !addon.ingress || !this._computeHA92plus(hass);
|
||||
}
|
||||
|
||||
_computeUsesProtectedOptions(addon) {
|
||||
return addon.docker_api || addon.full_access || addon.host_pid;
|
||||
}
|
||||
|
||||
_computeHA92plus(hass) {
|
||||
const [major, minor] = hass.config.version.split(".", 2);
|
||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
@@ -33,12 +34,15 @@ export class HassioUpdate extends LitElement {
|
||||
@property() public error?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (
|
||||
this.hassInfo.version === this.hassInfo.last_version &&
|
||||
this.supervisorInfo.version === this.supervisorInfo.last_version &&
|
||||
(!this.hassOsInfo ||
|
||||
this.hassOsInfo.version === this.hassOsInfo.version_latest)
|
||||
) {
|
||||
const updatesAvailable: number = [
|
||||
this.hassInfo,
|
||||
this.supervisorInfo,
|
||||
this.hassOsInfo,
|
||||
].filter((value) => {
|
||||
return !!value && value.version !== value.last_version;
|
||||
}).length;
|
||||
|
||||
if (!updatesAvailable) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -50,6 +54,11 @@ export class HassioUpdate extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
${updatesAvailable > 1
|
||||
? "Updates Available 🎉"
|
||||
: "Update Available 🎉"}
|
||||
</div>
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant",
|
||||
this.hassInfo.version,
|
||||
@@ -57,16 +66,15 @@ export class HassioUpdate extends LitElement {
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.hassInfo.last_version.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`
|
||||
}.home-assistant.io/latest-release-notes/`,
|
||||
"hassio:home-assistant"
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Hass.io Supervisor",
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.last_version,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${
|
||||
this.supervisorInfo.last_version
|
||||
}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.last_version}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
@@ -74,9 +82,7 @@ export class HassioUpdate extends LitElement {
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
"hassio/hassos/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${
|
||||
this.hassOsInfo.version_latest
|
||||
}`
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
@@ -89,18 +95,31 @@ export class HassioUpdate extends LitElement {
|
||||
curVersion: string,
|
||||
lastVersion: string,
|
||||
apiPath: string,
|
||||
releaseNotesUrl: string
|
||||
releaseNotesUrl: string,
|
||||
icon?: string
|
||||
): TemplateResult {
|
||||
if (lastVersion === curVersion) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<paper-card heading="${name} update available! 🎉">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
${name} ${lastVersion} is available and you are currently running
|
||||
${name} ${curVersion}.
|
||||
${icon
|
||||
? html`
|
||||
<div class="icon">
|
||||
<iron-icon .icon="${icon}" />
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="update-heading">${name} ${lastVersion}</div>
|
||||
<div class="warning">
|
||||
You are currently running version ${curVersion}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="${releaseNotesUrl}" target="_blank">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
<ha-call-api-button
|
||||
.hass=${this.hass}
|
||||
.path=${apiPath}
|
||||
@@ -108,9 +127,6 @@ export class HassioUpdate extends LitElement {
|
||||
>
|
||||
Update
|
||||
</ha-call-api-button>
|
||||
<a href="${releaseNotesUrl}" target="_blank">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
@@ -140,6 +156,23 @@ export class HassioUpdate extends LitElement {
|
||||
display: inline-block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.icon {
|
||||
--iron-icon-height: 48px;
|
||||
--iron-icon-width: 48px;
|
||||
float: right;
|
||||
margin: 0 0 2px 10px;
|
||||
}
|
||||
.update-heading {
|
||||
font-size: var(--paper-font-subhead_-_font-size);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.warning {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
padding: 16px;
|
||||
|
||||
@@ -12,7 +12,9 @@ export const showHassioMarkdownDialog = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"),
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
Regular → Executable
+42
-24
@@ -3,6 +3,7 @@ import "@material/mwc-button";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
@@ -94,13 +95,23 @@ class HassioSnapshotDialog extends PolymerElement {
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.download {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.warning,
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.buttons li {
|
||||
list-style-type: none;
|
||||
}
|
||||
.buttons .icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.no-margin-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<ha-paper-dialog
|
||||
id="dialog"
|
||||
@@ -132,7 +143,7 @@ class HassioSnapshotDialog extends PolymerElement {
|
||||
</template>
|
||||
<template is="dom-if" if="[[_addons.length]]">
|
||||
<div>Add-ons:</div>
|
||||
<paper-dialog-scrollable>
|
||||
<paper-dialog-scrollable class="no-margin-top">
|
||||
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]] <span class="details">([[item.version]])</span>
|
||||
@@ -151,28 +162,35 @@ class HassioSnapshotDialog extends PolymerElement {
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">Error: [[error]]</p>
|
||||
</template>
|
||||
<div class="buttons">
|
||||
<paper-icon-button
|
||||
icon="hassio:delete"
|
||||
on-click="_deleteClicked"
|
||||
class="warning"
|
||||
title="Delete snapshot"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
on-click="_downloadClicked"
|
||||
icon="hassio:download"
|
||||
class="download"
|
||||
title="Download snapshot"
|
||||
></paper-icon-button>
|
||||
<mwc-button on-click="_partialRestoreClicked"
|
||||
>Restore selected</mwc-button
|
||||
>
|
||||
<div>Actions:</div>
|
||||
<ul class="buttons">
|
||||
<li>
|
||||
<mwc-button on-click="_downloadClicked">
|
||||
<iron-icon icon="hassio:download" class="icon"></iron-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>
|
||||
</li>
|
||||
<li>
|
||||
<mwc-button on-click="_partialRestoreClicked">
|
||||
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||
Restore Selected
|
||||
</mwc-button>
|
||||
</li>
|
||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
||||
<mwc-button on-click="_fullRestoreClicked"
|
||||
>Wipe & restore</mwc-button
|
||||
>
|
||||
<li>
|
||||
<mwc-button on-click="_fullRestoreClicked">
|
||||
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||
Wipe & restore
|
||||
</mwc-button>
|
||||
</li>
|
||||
</template>
|
||||
</div>
|
||||
<li>
|
||||
<mwc-button on-click="_deleteClicked">
|
||||
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>
|
||||
</li>
|
||||
</ul>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ export const showHassioSnapshotDialog = (
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot",
|
||||
dialogImport: () =>
|
||||
import(/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"),
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import {
|
||||
HassRouterPage,
|
||||
@@ -27,6 +27,7 @@ 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";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
|
||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||
@@ -56,12 +57,16 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
addon: {
|
||||
tag: "hassio-addon-view",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"),
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"),
|
||||
import(
|
||||
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -161,14 +166,20 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
}),
|
||||
]);
|
||||
if (!addon.ingress_url) {
|
||||
throw new Error("Add-on does not support Ingress");
|
||||
alert("Add-on does not support Ingress");
|
||||
return;
|
||||
}
|
||||
if (addon.state !== "started") {
|
||||
alert("Add-on is not running. Please start it first");
|
||||
navigate(this, `/hassio/addon/${addon.slug}`, true);
|
||||
return;
|
||||
}
|
||||
location.assign(addon.ingress_url);
|
||||
// await a promise that doesn't resolve, so we show the loading screen
|
||||
// while we load the next page.
|
||||
await new Promise(() => undefined);
|
||||
} catch (err) {
|
||||
alert(`Unable to open ingress connection `);
|
||||
alert("Unable to open ingress connection");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +1,11 @@
|
||||
const webpack = require("webpack");
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
const zopfli = require("@gfx/zopfli");
|
||||
const { createHassioConfig } = require("../build-scripts/webpack.js");
|
||||
const { isProdBuild } = require("../build-scripts/env.js");
|
||||
|
||||
const config = require("./config.js");
|
||||
const webpackBase = require("../build-scripts/webpack.js");
|
||||
const { babelLoaderConfig } = require("../build-scripts/babel.js");
|
||||
// File just used for stats builds
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const isCI = process.env.CI === "true";
|
||||
const chunkFilename = isProdBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
|
||||
const latestBuild = false;
|
||||
|
||||
const rules = [
|
||||
{
|
||||
exclude: [config.nodeDir],
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: latestBuild
|
||||
? { noEmit: false }
|
||||
: {
|
||||
target: "es5",
|
||||
noEmit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!latestBuild) {
|
||||
rules.push(babelLoaderConfig({ latestBuild }));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
devtool: isProdBuild ? "source-map" : "inline-source-map",
|
||||
entry: {
|
||||
entrypoint: "./src/entrypoint.js",
|
||||
},
|
||||
module: {
|
||||
rules,
|
||||
},
|
||||
optimization: webpackBase.optimization(latestBuild),
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: JSON.stringify(!isProdBuild),
|
||||
__DEMO__: false,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
),
|
||||
}),
|
||||
isProdBuild &&
|
||||
!isCI &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
algorithm(input, compressionOptions, callback) {
|
||||
return zopfli.gzip(input, compressionOptions, callback);
|
||||
},
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
chunkFilename,
|
||||
path: config.buildDir,
|
||||
publicPath: `${config.publicPath}/`,
|
||||
},
|
||||
};
|
||||
module.exports = createHassioConfig({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
});
|
||||
|
||||
+30
-28
@@ -8,7 +8,7 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'src/**/*.tsx' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
|
||||
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
|
||||
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
||||
"test": "npm run lint && npm run mocha",
|
||||
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
|
||||
@@ -19,13 +19,14 @@
|
||||
"dependencies": {
|
||||
"@material/chips": "^3.2.0",
|
||||
"@material/data-table": "^3.2.0",
|
||||
"@material/mwc-base": "^0.8.0",
|
||||
"@material/mwc-button": "^0.8.0",
|
||||
"@material/mwc-checkbox": "^0.8.0",
|
||||
"@material/mwc-fab": "^0.8.0",
|
||||
"@material/mwc-ripple": "^0.8.0",
|
||||
"@material/mwc-switch": "^0.8.0",
|
||||
"@mdi/svg": "4.4.95",
|
||||
"@material/mwc-base": "^0.10.0",
|
||||
"@material/mwc-button": "^0.10.0",
|
||||
"@material/mwc-checkbox": "^0.10.0",
|
||||
"@material/mwc-dialog": "^0.10.0",
|
||||
"@material/mwc-fab": "^0.10.0",
|
||||
"@material/mwc-ripple": "^0.10.0",
|
||||
"@material/mwc-switch": "^0.10.0",
|
||||
"@mdi/svg": "4.7.95",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
@@ -67,14 +68,14 @@
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@thomasloven/round-slider": "^0.2.2",
|
||||
"@vaadin/vaadin-combo-box": "^4.2.8",
|
||||
"@vaadin/vaadin-date-picker": "^3.3.3",
|
||||
"@thomasloven/round-slider": "0.3.7",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.6",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.3",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.7",
|
||||
"chart.js": "~2.8.0",
|
||||
"chartjs-chart-timeline": "^0.3.0",
|
||||
"codemirror": "^5.45.0",
|
||||
"codemirror": "^5.49.0",
|
||||
"cpx": "^1.5.0",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
@@ -84,11 +85,11 @@
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "^4.4.0",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.4.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.4.0",
|
||||
"lit-element": "^2.2.1",
|
||||
"lit-html": "^1.1.0",
|
||||
"lit-virtualizer": "^0.4.2",
|
||||
"marked": "^0.6.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
@@ -98,7 +99,6 @@
|
||||
"react-big-calendar": "^0.20.4",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"round-slider": "^1.3.3",
|
||||
"superstruct": "^0.6.1",
|
||||
"tslib": "^1.10.0",
|
||||
"unfetch": "^4.1.0",
|
||||
@@ -106,24 +106,28 @@
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-external-helpers": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.4.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.4.0",
|
||||
"@gfx/zopfli": "^1.0.11",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-external-helpers": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/chromecast-caf-sender": "^1.0.1",
|
||||
"@types/codemirror": "^0.0.78",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/leaflet": "^1.4.3",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"babel-loader": "^8.0.5",
|
||||
"chai": "^4.2.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^5.0.2",
|
||||
"del": "^4.0.0",
|
||||
"eslint": "^6.3.0",
|
||||
@@ -153,19 +157,18 @@
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^6.0.2",
|
||||
"parse5": "^5.1.0",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier": "^1.19.1",
|
||||
"raw-loader": "^2.0.0",
|
||||
"reify": "^0.18.1",
|
||||
"require-dir": "^1.2.0",
|
||||
"sinon": "^7.3.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^6.1.1",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"tslint": "^5.14.0",
|
||||
"tslint": "^5.20.1",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.6.3",
|
||||
"typescript": "^3.7.2",
|
||||
"web-component-tester": "^6.9.2",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
@@ -178,7 +181,6 @@
|
||||
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
|
||||
"resolutions": {
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@vaadin/vaadin-lumo-styles": "^1.4.2",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"lit-html": "^1.1.2"
|
||||
},
|
||||
|
||||
@@ -29,11 +29,15 @@ mkdir -p ${LOCAL_DIR}
|
||||
docker run \
|
||||
-v ${LOCAL_DIR}:/opt/dest/locale \
|
||||
--rm \
|
||||
lokalise/lokalise-cli@sha256:b8329d20280263cad04f65b843e54b9e8e6909a348a678eac959550b5ef5c75f lokalise \
|
||||
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
export ${PROJECT_ID} \
|
||||
--export_empty skip \
|
||||
--type json \
|
||||
--unzip_to /opt/dest
|
||||
--project-id ${PROJECT_ID} \
|
||||
file download \
|
||||
--export-empty-as skip \
|
||||
--format json \
|
||||
--json-unescaped-slashes=true \
|
||||
--replace-breaks=false \
|
||||
--original-filenames=false \
|
||||
--unzip-to /opt/dest
|
||||
|
||||
./node_modules/.bin/gulp check-downloaded-translations
|
||||
@@ -26,17 +26,18 @@ LANG_ISO=en
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "${CURRENT_BRANCH-}" != "master" ] && [ "${TRAVIS_BRANCH-}" != "master" ] ; then
|
||||
echo "Please only run the translations upload script from a clean checkout of master."
|
||||
if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ] ; then
|
||||
echo "Please only run the translations upload script from a clean checkout of dev."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker run \
|
||||
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
|
||||
lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 lokalise \
|
||||
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
import ${PROJECT_ID} \
|
||||
--project-id ${PROJECT_ID} \
|
||||
file upload \
|
||||
--file /opt/src/${LOCAL_FILE} \
|
||||
--lang_iso ${LANG_ISO} \
|
||||
--convert_placeholders 0 \
|
||||
--replace 1
|
||||
--lang-iso ${LANG_ISO} \
|
||||
--convert-placeholders=false \
|
||||
--replace-modified=true
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Safe bash settings
|
||||
# -e Exit on command fail
|
||||
# -u Exit on unset variable
|
||||
# -o pipefail Exit if piped command has error code
|
||||
set -eu -o pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
script/translations_upload_base
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20191002.1",
|
||||
version="20200108.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
||||
+10
-13
@@ -7,7 +7,7 @@ import {
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
import "../components/ha-form";
|
||||
import "../components/ha-form/ha-form";
|
||||
import "../components/ha-markdown";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
@@ -98,9 +98,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
.content=${this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.abort.${step.reason}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
|
||||
)}
|
||||
></ha-markdown>
|
||||
`;
|
||||
@@ -119,6 +117,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
.error=${step.errors}
|
||||
.computeLabel=${this._computeLabelCallback(step)}
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
default:
|
||||
@@ -223,10 +222,12 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private _stepDataChanged(ev: CustomEvent) {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _computeStepDescription(step: DataEntryFlowStepForm) {
|
||||
const resourceKey = `ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.step.${step.step_id}.description`;
|
||||
const resourceKey = `ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`;
|
||||
const args: string[] = [];
|
||||
const placeholders = step.description_placeholders || {};
|
||||
Object.keys(placeholders).forEach((key) => {
|
||||
@@ -240,9 +241,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
|
||||
step.step_id
|
||||
}.data.${schema.name}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,9 +249,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.error.${error}`
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,18 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
CSSResult,
|
||||
css,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import "./ha-auth-flow";
|
||||
import { AuthProvider, fetchAuthProviders } from "../data/auth";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
|
||||
import(/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider");
|
||||
import(
|
||||
/* webpackChunkName: "pick-auth-provider" */ "../auth/ha-pick-auth-provider"
|
||||
);
|
||||
|
||||
interface QueryParams {
|
||||
client_id?: string;
|
||||
@@ -20,11 +22,11 @@ interface QueryParams {
|
||||
}
|
||||
|
||||
class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
public clientId?: string;
|
||||
public redirectUri?: string;
|
||||
public oauth2State?: string;
|
||||
private _authProvider?: AuthProvider;
|
||||
private _authProviders?: AuthProvider[];
|
||||
@property() public clientId?: string;
|
||||
@property() public redirectUri?: string;
|
||||
@property() public oauth2State?: string;
|
||||
@property() private _authProvider?: AuthProvider;
|
||||
@property() private _authProviders?: AuthProvider[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -48,16 +50,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_authProvider: {},
|
||||
_authProviders: {},
|
||||
clientId: {},
|
||||
redirectUri: {},
|
||||
oauth2State: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._authProviders) {
|
||||
return html`
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/entity/ha-state-label-badge";
|
||||
|
||||
class HaBadgesCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-state-label-badge {
|
||||
display: inline-block;
|
||||
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-repeat" items="[[states]]">
|
||||
<ha-state-label-badge
|
||||
hass="[[hass]]"
|
||||
state="[[item]]"
|
||||
></ha-state-label-badge>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
states: Array,
|
||||
};
|
||||
}
|
||||
}
|
||||
customElements.define("ha-badges-card", HaBadgesCard);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TemplateResult, html } from "lit-html";
|
||||
import { customElement, LitElement, property } from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import "../components/entity/ha-state-label-badge";
|
||||
|
||||
import { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-badges-card")
|
||||
class HaBadgesCard extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public states?: HassEntity[];
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.states) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.states.map(
|
||||
(state) => html`
|
||||
<ha-state-label-badge
|
||||
.hass=${this.hass}
|
||||
.state=${state}
|
||||
@click=${this._handleClick}
|
||||
></ha-state-label-badge>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev: Event) {
|
||||
const entityId = ((ev.target as any).state as HassEntity).entity_id;
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-badges-card": HaBadgesCard;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a component is loaded. */
|
||||
export default function isComponentLoaded(
|
||||
export const isComponentLoaded = (
|
||||
hass: HomeAssistant,
|
||||
component: string
|
||||
): boolean {
|
||||
return hass && hass.config.components.indexOf(component) !== -1;
|
||||
}
|
||||
): boolean => hass && hass.config.components.indexOf(component) !== -1;
|
||||
|
||||
@@ -35,6 +35,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"camera",
|
||||
"climate",
|
||||
"configurator",
|
||||
"counter",
|
||||
"cover",
|
||||
"fan",
|
||||
"group",
|
||||
|
||||
@@ -10,11 +10,11 @@ function toLocaleDateStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleDateStringSupportsOptions()
|
||||
export default toLocaleDateStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
|
||||
|
||||
@@ -10,7 +10,7 @@ function toLocaleStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleStringSupportsOptions()
|
||||
export default toLocaleStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
year: "numeric",
|
||||
@@ -19,4 +19,4 @@ export default (toLocaleStringSupportsOptions()
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");
|
||||
|
||||
@@ -10,10 +10,10 @@ function toLocaleTimeStringSupportsOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default (toLocaleTimeStringSupportsOptions()
|
||||
export default toLocaleTimeStringSupportsOptions()
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
|
||||
|
||||
@@ -21,12 +21,12 @@ const hexToRgb = (hex: string): string | null => {
|
||||
* localTheme: selected theme.
|
||||
* updateMeta: boolean if we should update the theme-color meta element.
|
||||
*/
|
||||
export default function applyThemesOnElement(
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
themes,
|
||||
localTheme,
|
||||
updateMeta = false
|
||||
) {
|
||||
) => {
|
||||
if (!element._themes) {
|
||||
element._themes = {};
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export default function applyThemesOnElement(
|
||||
element.updateStyles(styles);
|
||||
} else if (window.ShadyCSS) {
|
||||
// implement updateStyles() method of Polymer elements
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ (element), styles);
|
||||
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
|
||||
}
|
||||
|
||||
if (!updateMeta) {
|
||||
@@ -76,4 +76,4 @@ export default function applyThemesOnElement(
|
||||
styles["--primary-color"] || meta.getAttribute("default-content");
|
||||
meta.setAttribute("content", themeColor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { directive, Part, NodePart } from "lit-html";
|
||||
|
||||
export const dynamicElement = directive(
|
||||
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => {
|
||||
if (!(part instanceof NodePart)) {
|
||||
throw new Error(
|
||||
"dynamicContentDirective can only be used in content bindings"
|
||||
);
|
||||
}
|
||||
|
||||
let element = part.value as HTMLElement | undefined;
|
||||
|
||||
if (
|
||||
element !== undefined &&
|
||||
tag.toUpperCase() === (element as HTMLElement).tagName
|
||||
) {
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
element![key] = value;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
element = document.createElement(tag);
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
element![key] = value;
|
||||
});
|
||||
}
|
||||
part.setValue(element);
|
||||
}
|
||||
);
|
||||
@@ -11,7 +11,9 @@ export const setupLeafletMap = async (
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
// tslint:disable-next-line
|
||||
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType;
|
||||
const Leaflet = (await import(
|
||||
/* webpackChunkName: "leaflet" */ "leaflet"
|
||||
)) as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* tslint:disable */
|
||||
// @ts-ignore
|
||||
export const SpeechRecognition =
|
||||
// @ts-ignore
|
||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
// @ts-ignore
|
||||
export const SpeechGrammarList =
|
||||
// @ts-ignore
|
||||
window.SpeechGrammarList || window.webkitSpeechGrammarList;
|
||||
// @ts-ignore
|
||||
export const SpeechRecognitionEvent =
|
||||
// @ts-ignore
|
||||
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
|
||||
/* tslint:enable */
|
||||
@@ -14,6 +14,7 @@ const fixedIcons = {
|
||||
climate: "hass:thermostat",
|
||||
configurator: "hass:settings",
|
||||
conversation: "hass:text-to-speech",
|
||||
counter: "hass:counter",
|
||||
device_tracker: "hass:account",
|
||||
fan: "hass:fan",
|
||||
google_assistant: "hass:google-assistant",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// interface OnChangeComponent {
|
||||
// props: {
|
||||
// index: number;
|
||||
// onChange(index: number, data: object);
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function onChangeEvent(this: OnChangeComponent, prop, ev) {
|
||||
export function onChangeEvent(this: any, prop, ev) {
|
||||
const origData = this.props[prop];
|
||||
|
||||
if (ev.target.value === origData[ev.target.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { ...origData };
|
||||
|
||||
if (ev.target.value) {
|
||||
data[ev.target.name] = ev.target.value;
|
||||
} else {
|
||||
delete data[ev.target.name];
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.index, data);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { render } from "preact";
|
||||
|
||||
export default function unmount(mountEl) {
|
||||
render(
|
||||
// @ts-ignore
|
||||
() => null,
|
||||
mountEl
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,11 @@ import "@material/mwc-button";
|
||||
|
||||
@customElement("search-input")
|
||||
class SearchInput extends LitElement {
|
||||
@property() private filter?: string;
|
||||
@property() public filter?: string;
|
||||
|
||||
public focus() {
|
||||
this.shadowRoot!.querySelector("paper-input")!.focus();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const applyPatch = (data, path, value) => {
|
||||
if (path.length === 1) {
|
||||
data[path[0]] = value;
|
||||
} else {
|
||||
if (!data[path[0]]) {
|
||||
data[path[0]] = {};
|
||||
}
|
||||
return applyPatch(data[path[0]], path.slice(1), value);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPath = (data, path) => {
|
||||
if (path.length === 1) {
|
||||
return data[path[0]];
|
||||
} else {
|
||||
if (data[path[0]] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return getPath(data[path[0]], path.slice(1));
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./ha-progress-button";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
@@ -14,6 +15,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
id="progress"
|
||||
progress="[[progress]]"
|
||||
on-click="buttonTapped"
|
||||
tabindex="0"
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
@@ -49,10 +51,7 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
};
|
||||
}
|
||||
|
||||
buttonTapped() {
|
||||
if (this.confirmation && !window.confirm(this.confirmation)) {
|
||||
return;
|
||||
}
|
||||
callService() {
|
||||
this.progress = true;
|
||||
var el = this;
|
||||
var eventData = {
|
||||
@@ -79,6 +78,17 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
el.fire("hass-service-called", eventData);
|
||||
});
|
||||
}
|
||||
|
||||
buttonTapped() {
|
||||
if (this.confirmation) {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.confirmation,
|
||||
confirm: () => this.callService(),
|
||||
});
|
||||
} else {
|
||||
this.callService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-call-service-button", HaCallServiceButton);
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
MDCDataTableFoundation,
|
||||
} from "@material/data-table";
|
||||
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import {
|
||||
BaseElement,
|
||||
html,
|
||||
query,
|
||||
queryAll,
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
classMap,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "@material/mwc-base/base-element";
|
||||
} from "lit-element";
|
||||
|
||||
import { BaseElement } from "@material/mwc-base/base-element";
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
// @ts-ignore
|
||||
@@ -59,38 +61,38 @@ export interface SortingChangedEvent {
|
||||
|
||||
export type SortingDirection = "desc" | "asc" | null;
|
||||
|
||||
export interface DataTabelColumnContainer {
|
||||
[key: string]: DataTabelColumnData;
|
||||
export interface DataTableColumnContainer {
|
||||
[key: string]: DataTableColumnData;
|
||||
}
|
||||
|
||||
export interface DataTabelSortColumnData {
|
||||
export interface DataTableSortColumnData {
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
filterKey?: string;
|
||||
direction?: SortingDirection;
|
||||
}
|
||||
|
||||
export interface DataTabelColumnData extends DataTabelSortColumnData {
|
||||
export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric";
|
||||
template?: (data: any) => TemplateResult;
|
||||
type?: "numeric" | "icon";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
}
|
||||
|
||||
export interface DataTabelRowData {
|
||||
export interface DataTableRowData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends BaseElement {
|
||||
@property({ type: Object }) public columns: DataTabelColumnContainer = {};
|
||||
@property({ type: Array }) public data: DataTabelRowData[] = [];
|
||||
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
@property({ type: String }) public id = "id";
|
||||
@property({ type: String }) public filter = "";
|
||||
protected mdcFoundation!: MDCDataTableFoundation;
|
||||
protected readonly mdcFoundationClass = MDCDataTableFoundation;
|
||||
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
|
||||
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
|
||||
@query("#header-checkbox") private _headerCheckbox!: HaCheckbox;
|
||||
@property({ type: Boolean }) private _filterable = false;
|
||||
@property({ type: Boolean }) private _headerChecked = false;
|
||||
@property({ type: Boolean }) private _headerIndeterminate = false;
|
||||
@@ -98,21 +100,27 @@ export class HaDataTable extends BaseElement {
|
||||
@property({ type: String }) private _filter = "";
|
||||
@property({ type: String }) private _sortColumn?: string;
|
||||
@property({ type: String }) private _sortDirection: SortingDirection = null;
|
||||
@property({ type: Array }) private _filteredData: DataTabelRowData[] = [];
|
||||
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
|
||||
private _sortColumns: {
|
||||
[key: string]: DataTabelSortColumnData;
|
||||
[key: string]: DataTableSortColumnData;
|
||||
} = {};
|
||||
private curRequest = 0;
|
||||
private _worker: any | undefined;
|
||||
|
||||
private _debounceSearch = debounce(
|
||||
(ev) => {
|
||||
this._filter = ev.detail.value;
|
||||
(value: string) => {
|
||||
this._filter = value;
|
||||
},
|
||||
200,
|
||||
false
|
||||
);
|
||||
|
||||
public clearSelection(): void {
|
||||
this._headerChecked = false;
|
||||
this._headerIndeterminate = false;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this._worker = sortFilterWorker();
|
||||
@@ -134,8 +142,8 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
}
|
||||
|
||||
const clonedColumns: DataTabelColumnContainer = deepClone(this.columns);
|
||||
Object.values(clonedColumns).forEach((column: DataTabelColumnData) => {
|
||||
const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
|
||||
Object.values(clonedColumns).forEach((column: DataTableColumnData) => {
|
||||
delete column.title;
|
||||
delete column.type;
|
||||
delete column.template;
|
||||
@@ -144,6 +152,10 @@ export class HaDataTable extends BaseElement {
|
||||
this._sortColumns = clonedColumns;
|
||||
}
|
||||
|
||||
if (properties.has("filter")) {
|
||||
this._debounceSearch(this.filter);
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
@@ -157,14 +169,18 @@ export class HaDataTable extends BaseElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this._filterable
|
||||
? html`
|
||||
<search-input
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
`
|
||||
: ""}
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header">
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</slot>
|
||||
<table class="mdc-data-table__table">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
@@ -176,7 +192,6 @@ export class HaDataTable extends BaseElement {
|
||||
scope="col"
|
||||
>
|
||||
<ha-checkbox
|
||||
id="header-checkbox"
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxChange}
|
||||
.indeterminate=${this._headerIndeterminate}
|
||||
@@ -190,9 +205,12 @@ export class HaDataTable extends BaseElement {
|
||||
const [key, column] = columnEntry;
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
"mdc-data-table__header-cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__header-cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
),
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
};
|
||||
@@ -222,8 +240,8 @@ export class HaDataTable extends BaseElement {
|
||||
<tbody class="mdc-data-table__content">
|
||||
${repeat(
|
||||
this._filteredData!,
|
||||
(row: DataTabelRowData) => row[this.id],
|
||||
(row: DataTabelRowData) => html`
|
||||
(row: DataTableRowData) => row[this.id],
|
||||
(row: DataTableRowData) => html`
|
||||
<tr
|
||||
data-row-id="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
@@ -237,7 +255,9 @@ export class HaDataTable extends BaseElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxChange}
|
||||
.checked=${this._checkedRows.includes(row[this.id])}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</td>
|
||||
@@ -251,10 +271,13 @@ export class HaDataTable extends BaseElement {
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
),
|
||||
})}"
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key])
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</td>
|
||||
`;
|
||||
@@ -364,9 +387,10 @@ export class HaDataTable extends BaseElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxChange() {
|
||||
this._headerChecked = this._headerCheckbox.checked;
|
||||
this._headerIndeterminate = this._headerCheckbox.indeterminate;
|
||||
private _handleHeaderRowCheckboxChange(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
this._headerChecked = checkbox.checked;
|
||||
this._headerIndeterminate = checkbox.indeterminate;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
}
|
||||
|
||||
@@ -379,20 +403,26 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
const rowId = (ev.target as HTMLElement)
|
||||
.closest("tr")!
|
||||
.getAttribute("data-row-id")!;
|
||||
const target = ev.target as HTMLElement;
|
||||
if (target.tagName === "HA-CHECKBOX") {
|
||||
return;
|
||||
}
|
||||
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
}
|
||||
|
||||
private _setRowChecked(rowId: string, checked: boolean) {
|
||||
if (checked && !this._checkedRows.includes(rowId)) {
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else if (!checked) {
|
||||
const index = this._checkedRows.indexOf(rowId);
|
||||
if (index !== -1) {
|
||||
this._checkedRows.splice(index, 1);
|
||||
if (checked) {
|
||||
if (this._checkedRows.includes(rowId)) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else {
|
||||
const index = this._checkedRows.indexOf(rowId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows.splice(index, 1);
|
||||
}
|
||||
fireEvent(this, "selection-changed", {
|
||||
id: rowId,
|
||||
@@ -401,7 +431,7 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
this._debounceSearch(ev);
|
||||
this._debounceSearch(ev.detail.value);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -421,7 +451,7 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
.mdc-data-table {
|
||||
background-color: var(--card-background-color);
|
||||
background-color: var(--data-table-background-color);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
@@ -516,6 +546,11 @@ export class HaDataTable extends BaseElement {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -543,6 +578,10 @@ export class HaDataTable extends BaseElement {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* custom from here */
|
||||
|
||||
.mdc-data-table {
|
||||
@@ -554,7 +593,7 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__header-cell.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric)
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon)
|
||||
span {
|
||||
position: relative;
|
||||
left: -24px;
|
||||
@@ -565,13 +604,16 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__header-cell.not-sorted ha-icon {
|
||||
left: -36px;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__cell--numeric):hover
|
||||
.mdc-data-table__header-cell.not-sorted:not(.mdc-data-table__header-cell--numeric):not(.mdc-data-table__header-cell--icon):hover
|
||||
span {
|
||||
left: 0px;
|
||||
}
|
||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
||||
left: 0px;
|
||||
}
|
||||
.table-header {
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
DataTabelColumnContainer,
|
||||
DataTabelColumnData,
|
||||
DataTabelRowData,
|
||||
DataTableColumnContainer,
|
||||
DataTableColumnData,
|
||||
DataTableRowData,
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
@@ -9,8 +9,8 @@ import memoizeOne from "memoize-one";
|
||||
|
||||
export const filterSortData = memoizeOne(
|
||||
async (
|
||||
data: DataTabelRowData[],
|
||||
columns: DataTabelColumnContainer,
|
||||
data: DataTableRowData[],
|
||||
columns: DataTableColumnContainer,
|
||||
filter: string,
|
||||
direction: SortingDirection,
|
||||
sortColumn?: string
|
||||
@@ -27,8 +27,8 @@ export const filterSortData = memoizeOne(
|
||||
|
||||
const _memFilterData = memoizeOne(
|
||||
async (
|
||||
data: DataTabelRowData[],
|
||||
columns: DataTabelColumnContainer,
|
||||
data: DataTableRowData[],
|
||||
columns: DataTableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
if (!filter) {
|
||||
@@ -40,8 +40,8 @@ const _memFilterData = memoizeOne(
|
||||
|
||||
const _memSortData = memoizeOne(
|
||||
(
|
||||
data: DataTabelRowData[],
|
||||
columns: DataTabelColumnContainer,
|
||||
data: DataTableRowData[],
|
||||
columns: DataTableColumnContainer,
|
||||
direction: SortingDirection,
|
||||
sortColumn: string
|
||||
) => {
|
||||
@@ -50,8 +50,8 @@ const _memSortData = memoizeOne(
|
||||
);
|
||||
|
||||
export const filterData = (
|
||||
data: DataTabelRowData[],
|
||||
columns: DataTabelColumnContainer,
|
||||
data: DataTableRowData[],
|
||||
columns: DataTableColumnContainer,
|
||||
filter: string
|
||||
) =>
|
||||
data.filter((row) => {
|
||||
@@ -71,8 +71,8 @@ export const filterData = (
|
||||
});
|
||||
|
||||
export const sortData = (
|
||||
data: DataTabelRowData[],
|
||||
column: DataTabelColumnData,
|
||||
data: DataTableRowData[],
|
||||
column: DataTableColumnData,
|
||||
direction: SortingDirection,
|
||||
sortColumn: string
|
||||
) =>
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import "./ha-devices-picker";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
}
|
||||
|
||||
interface AreaDevices {
|
||||
id?: string;
|
||||
name: string;
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: AreaDevices }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
width: 100%;
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
float: right;
|
||||
}
|
||||
.devices {
|
||||
display: none;
|
||||
}
|
||||
.devices.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.devices.length]] devices</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector(
|
||||
"[secondary]"
|
||||
)!.textContent = `${model.item.devices.length.toString()} devices`;
|
||||
};
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public label?: string;
|
||||
@property() public value?: string;
|
||||
@property() public area?: string;
|
||||
@property() public devices?: string[];
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no devices with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
/**
|
||||
* Show only deviced with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
@property() private _areaPicker = true;
|
||||
@property() private _devices?: DeviceRegistryEntry[];
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
@property() private _entities?: EntityRegistryEntry[];
|
||||
private _selectedDevices: string[] = [];
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): AreaDevices[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._filteredDevices = inputDevices;
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const devicesByArea: DevicesByArea = {};
|
||||
|
||||
for (const device of inputDevices) {
|
||||
const areaId = device.area_id;
|
||||
if (areaId) {
|
||||
if (!(areaId in devicesByArea)) {
|
||||
devicesByArea[areaId] = {
|
||||
id: areaId,
|
||||
name: areaLookup[areaId].name,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
devicesByArea[areaId].devices.push(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Object.keys(devicesByArea)
|
||||
.sort((a, b) =>
|
||||
compare(devicesByArea[a].name || "", devicesByArea[b].name || "")
|
||||
)
|
||||
.map((key) => devicesByArea[key]);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("area") && this.area) {
|
||||
this._areaPicker = true;
|
||||
this.value = this.area;
|
||||
} else if (changedProps.has("devices") && this.devices) {
|
||||
this._areaPicker = false;
|
||||
const filteredDeviceIds = this._filteredDevices.map(
|
||||
(device) => device.id
|
||||
);
|
||||
const selectedDevices = this.devices.filter((device) =>
|
||||
filteredDeviceIds.includes(device)
|
||||
);
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return;
|
||||
}
|
||||
const areas = this._getDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
if (!this._areaPicker || areas.length === 0) {
|
||||
return html`
|
||||
<ha-devices-picker
|
||||
@value-changed=${this._devicesPicked}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${this._selectedDevices}
|
||||
.pickDeviceLabel=${`Add ${this.label} device`}
|
||||
.pickedDeviceLabel=${`${this.label} device`}
|
||||
></ha-devices-picker>
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose an area</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaPicked}
|
||||
>
|
||||
<paper-input
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.device-picker.device")
|
||||
: `${this.label} in area`}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose individual devices</mwc-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue([]);
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _switchPicker() {
|
||||
this._areaPicker = !this._areaPicker;
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: PolymerChangedEvent<string>) {
|
||||
const value = ev.detail.value;
|
||||
let selectedDevices = [];
|
||||
const target = ev.target as any;
|
||||
if (target.selectedItem) {
|
||||
selectedDevices = target.selectedItem.devices;
|
||||
}
|
||||
|
||||
if (value !== this._value || this._selectedDevices !== selectedDevices) {
|
||||
this._setValue(selectedDevices, value);
|
||||
}
|
||||
}
|
||||
|
||||
private _devicesPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const selectedDevices = ev.detail.value;
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
|
||||
private _setValue(selectedDevices: string[], value = "") {
|
||||
this.value = value;
|
||||
this._selectedDevices = selectedDevices;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: selectedDevices });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-devices-picker": HaAreaDevicesPicker;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this.value = automation;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "change");
|
||||
fireEvent(this, "value-changed", { value: automation });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
@@ -21,83 +21,295 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
computeDeviceName,
|
||||
} from "../../data/device_registry";
|
||||
import { compare } from "../../common/string/compare";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
area: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
<style>
|
||||
paper-item {
|
||||
margin: -10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.area]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector(".name")!.textContent = model.item.name!;
|
||||
root.querySelector("[secondary]")!.textContent = model.item.area!;
|
||||
};
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass?: HomeAssistant;
|
||||
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public label?: string;
|
||||
@property() public value?: string;
|
||||
@property() public devices?: DeviceRegistryEntry[];
|
||||
@property() public areas?: AreaRegistryEntry[];
|
||||
@property() public entities?: EntityRegistryEntry[];
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no devices with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
/**
|
||||
* Show only deviced with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
|
||||
private _sortedDevices = memoizeOne((devices?: DeviceRegistryEntry[]) => {
|
||||
if (!devices || devices.length === 1) {
|
||||
return devices || [];
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): Device[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const outputDevices = inputDevices.map((device) => {
|
||||
return {
|
||||
id: device.id,
|
||||
name: computeDeviceName(
|
||||
device,
|
||||
this.hass,
|
||||
deviceEntityLookup[device.id]
|
||||
),
|
||||
area: device.area_id ? areaLookup[device.area_id].name : "No area",
|
||||
};
|
||||
});
|
||||
if (outputDevices.length === 1) {
|
||||
return outputDevices;
|
||||
}
|
||||
return outputDevices.sort((a, b) => compare(a.name || "", b.name || ""));
|
||||
}
|
||||
const sorted = [...devices];
|
||||
sorted.sort((a, b) => compare(a.name || "", b.name || ""));
|
||||
return sorted;
|
||||
});
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass!.connection!, (devices) => {
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this.devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this.areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this.entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.devices || !this.areas || !this.entities) {
|
||||
return;
|
||||
}
|
||||
const devices = this._getDevices(
|
||||
this.devices,
|
||||
this.areas,
|
||||
this.entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
return html`
|
||||
<paper-dropdown-menu-light .label=${this.label}>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._value}
|
||||
attr-for-selected="data-device-id"
|
||||
@iron-select=${this._deviceChanged}
|
||||
<vaadin-combo-box-light
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${devices}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._deviceChanged}
|
||||
>
|
||||
<paper-input
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.device-picker.device")
|
||||
: this.label}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<paper-item data-device-id="">
|
||||
No device
|
||||
</paper-item>
|
||||
${this._sortedDevices(this.devices).map(
|
||||
(device) => html`
|
||||
<paper-item data-device-id=${device.id}>
|
||||
${device.name_by_user || device.name}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu-light>
|
||||
${this.value
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${devices.length > 0
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.device-picker.show_devices"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</paper-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue("");
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _deviceChanged(ev) {
|
||||
const newValue = ev.detail.item.dataset.deviceId;
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _deviceChanged(ev: PolymerChangedEvent<string>) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this.value = newValue;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-dropdown-menu-light {
|
||||
width: 100%;
|
||||
paper-input > paper-icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
paper-listbox {
|
||||
min-width: 200px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-icon-button/paper-icon-button-light";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import "./ha-device-picker";
|
||||
|
||||
@customElement("ha-devices-picker")
|
||||
class HaDevicesPicker extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public value?: string[];
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
@property({ attribute: "picked-device-label" })
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
public pickedDeviceLabel?: string;
|
||||
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDevices = this._currentDevices;
|
||||
return html`
|
||||
${currentDevices.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
<ha-device-picker
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedDeviceLabel}
|
||||
@value-changed=${this._deviceChanged}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.label=${this.pickDeviceLabel}
|
||||
@value-changed=${this._addDevice}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentDevices() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateDevices(devices) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: devices,
|
||||
});
|
||||
|
||||
this.value = devices;
|
||||
}
|
||||
|
||||
private _deviceChanged(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const curValue = (event.currentTarget as any).curValue;
|
||||
const newValue = event.detail.value;
|
||||
if (newValue === curValue || newValue !== "") {
|
||||
return;
|
||||
}
|
||||
if (newValue === "") {
|
||||
this._updateDevices(
|
||||
this._currentDevices.filter((dev) => dev !== curValue)
|
||||
);
|
||||
} else {
|
||||
this._updateDevices(
|
||||
this._currentDevices.map((dev) => (dev === curValue ? newValue : dev))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _addDevice(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
const currentDevices = this._currentDevices;
|
||||
if (currentDevices.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateDevices([...currentDevices, toAdd]);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-devices-picker": HaDevicesPicker;
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,9 @@ class HaChartBase extends mixinBehaviors(
|
||||
}
|
||||
|
||||
if (scriptsLoaded === null) {
|
||||
scriptsLoaded = import(/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js");
|
||||
scriptsLoaded = import(
|
||||
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
|
||||
);
|
||||
}
|
||||
scriptsLoaded.then((ChartModule) => {
|
||||
this.ChartClass = ChartModule.default;
|
||||
|
||||
@@ -22,7 +22,20 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
class HaEntitiesPickerLight extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public value?: string[];
|
||||
@property({ attribute: "domain-filter" }) public domainFilter?: string;
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
@property({ attribute: "picked-entity-label" })
|
||||
public pickedEntityLabel?: string;
|
||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
||||
@@ -31,6 +44,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
${currentEntities.map(
|
||||
@@ -40,7 +54,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
allow-custom-entity
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.domainFilter=${this.domainFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
@@ -52,7 +67,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.domainFilter=${this.domainFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.label=${this.pickEntityLabel}
|
||||
@value-changed=${this._addEntity}
|
||||
|
||||
@@ -2,7 +2,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import "./state-badge";
|
||||
@@ -21,6 +21,7 @@ import { HomeAssistant } from "../../types";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
@@ -60,7 +61,27 @@ class HaEntityPicker extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public label?: string;
|
||||
@property() public value?: string;
|
||||
@property({ attribute: "domain-filter" }) public domainFilter?: string;
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
/**
|
||||
* Show only entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
@property({ type: Boolean }) private _opened?: boolean;
|
||||
@property() private _hass?: HomeAssistant;
|
||||
@@ -68,8 +89,10 @@ class HaEntityPicker extends LitElement {
|
||||
private _getStates = memoizeOne(
|
||||
(
|
||||
hass: this["hass"],
|
||||
domainFilter: this["domainFilter"],
|
||||
entityFilter: this["entityFilter"]
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
entityFilter: this["entityFilter"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
) => {
|
||||
let states: HassEntity[] = [];
|
||||
|
||||
@@ -78,14 +101,30 @@ class HaEntityPicker extends LitElement {
|
||||
}
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
if (domainFilter) {
|
||||
if (includeDomains) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
entityIds = entityIds.filter(
|
||||
(eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
|
||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
states = entityIds.sort().map((key) => hass!.states[key]);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value ||
|
||||
(stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class))
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
@@ -93,6 +132,7 @@ class HaEntityPicker extends LitElement {
|
||||
stateObj.entity_id === this.value || entityFilter!(stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
@@ -108,8 +148,10 @@ class HaEntityPicker extends LitElement {
|
||||
protected render(): TemplateResult | void {
|
||||
const states = this._getStates(
|
||||
this._hass,
|
||||
this.domainFilter,
|
||||
this.entityFilter
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -139,10 +181,13 @@ class HaEntityPicker extends LitElement {
|
||||
${this.value
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label="Clear"
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
@@ -152,7 +197,9 @@ class HaEntityPicker extends LitElement {
|
||||
${states.length > 0
|
||||
? html`
|
||||
<paper-icon-button
|
||||
aria-label="Show entities"
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.show_entities"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
@@ -166,6 +213,11 @@ class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._setValue("");
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
@@ -177,14 +229,18 @@ class HaEntityPicker extends LitElement {
|
||||
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue !== this._value) {
|
||||
this.value = ev.detail.value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > paper-icon-button {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
@@ -90,16 +89,6 @@ export class HaStateLabelBadge extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (this.state) {
|
||||
fireEvent(this, "hass-more-info", { entityId: this.state.entity_id });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
|
||||
class HaAttributes extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layout vertical">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[computeDisplayAttributes(stateObj, filtersArray)]]"
|
||||
as="attribute"
|
||||
>
|
||||
<div class="data-entry layout justified horizontal">
|
||||
<div class="key">[[formatAttribute(attribute)]]</div>
|
||||
<div class="value">
|
||||
[[formatAttributeValue(stateObj, attribute)]]
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="attribution" hidden$="[[!computeAttribution(stateObj)]]">
|
||||
[[computeAttribution(stateObj)]]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
extraFilters: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
filtersArray: {
|
||||
type: Array,
|
||||
computed: "computeFiltersArray(extraFilters)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeFiltersArray(extraFilters) {
|
||||
return Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
|
||||
extraFilters ? extraFilters.split(",") : []
|
||||
);
|
||||
}
|
||||
|
||||
computeDisplayAttributes(stateObj, filtersArray) {
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(stateObj.attributes).filter(function(key) {
|
||||
return filtersArray.indexOf(key) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
formatAttribute(attribute) {
|
||||
return attribute.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
formatAttributeValue(stateObj, attribute) {
|
||||
var value = stateObj.attributes[attribute];
|
||||
if (value === null) return "-";
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
|
||||
}
|
||||
|
||||
computeAttribution(stateObj) {
|
||||
return stateObj.attributes.attribution;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-attributes", HaAttributes);
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
property,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import hassAttributeUtil from "../util/hass-attributes-util";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@property() public stateObj?: HassEntity;
|
||||
@property() public extraFilters?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
${this.computeDisplayAttributes(
|
||||
Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
)
|
||||
).map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">${attribute.replace(/_/g, " ")}</div>
|
||||
<div class="value">
|
||||
${this.formatAttributeValue(attribute)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this.stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private computeDisplayAttributes(filtersArray: string[]): string[] {
|
||||
if (!this.stateObj) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(this.stateObj.attributes).filter((key) => {
|
||||
return filtersArray.indexOf(key) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
private formatAttributeValue(attribute: string): string {
|
||||
if (!this.stateObj) {
|
||||
return "-";
|
||||
}
|
||||
const value = this.stateObj.attributes[attribute];
|
||||
if (value === null) {
|
||||
return "-";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-attributes": HaAttributes;
|
||||
}
|
||||
}
|
||||
@@ -122,8 +122,9 @@ class HaCameraStream extends LitElement {
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
// tslint:disable-next-line
|
||||
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
const Hls = ((await import(
|
||||
/* webpackChunkName: "hls.js" */ "hls.js"
|
||||
)) as any).default as HLSModule;
|
||||
let hlsSupported = Hls.isSupported();
|
||||
const videoEl = this._videoEl;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Constructor, customElement, CSSResult, css } from "lit-element";
|
||||
import { customElement, CSSResult, css } from "lit-element";
|
||||
import "@material/mwc-checkbox";
|
||||
// tslint:disable-next-line
|
||||
import { Checkbox } from "@material/mwc-checkbox";
|
||||
import { style } from "@material/mwc-checkbox/mwc-checkbox-css";
|
||||
import { Constructor } from "../types";
|
||||
// tslint:disable-next-line
|
||||
const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;
|
||||
|
||||
|
||||
@@ -72,9 +72,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
computeCurrentStatus(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
if (stateObj.attributes.current_temperature != null) {
|
||||
return `${stateObj.attributes.current_temperature} ${
|
||||
hass.config.unit_system.temperature
|
||||
}`;
|
||||
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.current_humidity != null) {
|
||||
return `${stateObj.attributes.current_humidity} %`;
|
||||
@@ -89,22 +87,16 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
stateObj.attributes.target_temp_low != null &&
|
||||
stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_temp_low}-${
|
||||
stateObj.attributes.target_temp_high
|
||||
} ${hass.config.unit_system.temperature}`;
|
||||
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${stateObj.attributes.temperature} ${
|
||||
hass.config.unit_system.temperature
|
||||
}`;
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
stateObj.attributes.target_humidity_low != null &&
|
||||
stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_humidity_low}-${
|
||||
stateObj.attributes.target_humidity_high
|
||||
}%`;
|
||||
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
if (stateObj.attributes.humidity != null) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
@@ -121,9 +113,7 @@ class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
const stateString = localize(`state.climate.${stateObj.state}`);
|
||||
return stateObj.attributes.hvac_action
|
||||
? `${localize(
|
||||
`state_attributes.climate.hvac_action.${
|
||||
stateObj.attributes.hvac_action
|
||||
}`
|
||||
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { loadCodeMirror } from "../resources/codemirror.ondemand";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
UpdatingElement,
|
||||
property,
|
||||
customElement,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { Editor } from "codemirror";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"editor-save": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-code-editor")
|
||||
export class HaCodeEditor extends UpdatingElement {
|
||||
public codemirror?: Editor;
|
||||
@property() public mode?: string;
|
||||
@property() public autofocus = false;
|
||||
@property() public rtl = false;
|
||||
@property() public error = false;
|
||||
@property() private _value = "";
|
||||
|
||||
public set value(value: string) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.codemirror ? this.codemirror.getValue() : this._value;
|
||||
}
|
||||
|
||||
public get hasComments(): boolean {
|
||||
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
this.codemirror.refresh();
|
||||
if (this.autofocus !== false) {
|
||||
this.codemirror.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues): void {
|
||||
super.update(changedProps);
|
||||
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("mode")) {
|
||||
this.codemirror.setOption("mode", this.mode);
|
||||
}
|
||||
if (changedProps.has("autofocus")) {
|
||||
this.codemirror.setOption("autofocus", this.autofocus !== false);
|
||||
}
|
||||
if (changedProps.has("_value") && this._value !== this.value) {
|
||||
this.codemirror.setValue(this._value);
|
||||
}
|
||||
if (changedProps.has("rtl")) {
|
||||
this.codemirror.setOption("gutters", this._calcGutters());
|
||||
this._setScrollBarDirection();
|
||||
}
|
||||
if (changedProps.has("error")) {
|
||||
this.classList.toggle("error-state", this.error);
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._load();
|
||||
}
|
||||
|
||||
private async _load(): Promise<void> {
|
||||
const loaded = await loadCodeMirror();
|
||||
|
||||
const codeMirror = loaded.codeMirror;
|
||||
|
||||
const shadowRoot = this.attachShadow({ mode: "open" });
|
||||
|
||||
shadowRoot!.innerHTML = `
|
||||
<style>
|
||||
${loaded.codeMirrorCss}
|
||||
.CodeMirror {
|
||||
height: var(--code-mirror-height, auto);
|
||||
direction: var(--code-mirror-direction, ltr);
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
max-height: var(--code-mirror-max-height, --code-mirror-height);
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
|
||||
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
|
||||
transition: 0.2s ease border-right;
|
||||
}
|
||||
:host(.error-state) .CodeMirror-gutters {
|
||||
border-color: var(--error-state-color, red);
|
||||
}
|
||||
.CodeMirror-focused .CodeMirror-gutters {
|
||||
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: var(--paper-dialog-color, var(--primary-text-color));
|
||||
}
|
||||
.rtl .CodeMirror-vscrollbar {
|
||||
right: auto;
|
||||
left: 0px;
|
||||
}
|
||||
.rtl-gutter {
|
||||
width: 20px;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
this.codemirror = codeMirror(shadowRoot, {
|
||||
value: this._value,
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: this.mode,
|
||||
autofocus: this.autofocus !== false,
|
||||
viewportMargin: Infinity,
|
||||
extraKeys: {
|
||||
Tab: "indentMore",
|
||||
"Shift-Tab": "indentLess",
|
||||
},
|
||||
gutters: this._calcGutters(),
|
||||
});
|
||||
this._setScrollBarDirection();
|
||||
this.codemirror!.on("changes", () => this._onChange());
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
const newValue = this.value;
|
||||
if (newValue === this._value) {
|
||||
return;
|
||||
}
|
||||
this._value = newValue;
|
||||
fireEvent(this, "value-changed", { value: this._value });
|
||||
}
|
||||
|
||||
private _calcGutters(): string[] {
|
||||
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
|
||||
}
|
||||
|
||||
private _setScrollBarDirection(): void {
|
||||
if (this.codemirror) {
|
||||
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor": HaCodeEditor;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user