mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
commit
9ad121f9e6
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -11,3 +11,4 @@
|
||||
*.mp3 binary
|
||||
|
||||
demo/public/api/camera_proxy_stream/* binary
|
||||
demo/public/api/media_player_proxy/* binary
|
||||
|
23
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
23
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@ -3,6 +3,7 @@ name: Report a bug with the UI, Frontend or Lovelace
|
||||
about: Report an issue related to the Home Assistant frontend.
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
@ -10,6 +11,7 @@ labels: bug
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have updated to the latest available Home Assistant version.
|
||||
@ -17,21 +19,22 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
- [ ] I have tried a different browser to see if it is related to my browser.
|
||||
|
||||
## The problem
|
||||
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us about the current behavior.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
|
||||
<!--
|
||||
Describe what you expected to happen or it should look/behave.
|
||||
If possible provide a screenshot with a description.
|
||||
-->
|
||||
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
<!--
|
||||
Provide steps for us, that helps reproducing your issue.
|
||||
For example:
|
||||
@ -43,8 +46,8 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
6. Set the HVAC action to cool
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us reproducing
|
||||
and finding the issue quicker. Version information is found in the
|
||||
@ -54,13 +57,13 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
your issue in a different browser and be sure to include your findings.
|
||||
-->
|
||||
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- UI Type (States or Lovelace):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
|
||||
## Problem-relevant configuration
|
||||
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
@ -72,6 +75,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
```
|
||||
|
||||
## Javascript errors shown in your browser console/inspector
|
||||
|
||||
<!--
|
||||
If you come across any javascript or other error logs, e.g., in your browser
|
||||
console/inspector please provide them.
|
||||
@ -82,4 +86,3 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||
url: https://github.com/home-assistant/home-assistant/issues
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||
- name: Report incorrect or missing information on our website
|
||||
url: https://github.com/home-assistant/home-assistant.io/issues
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Home Assistant Polymer [](https://travis-ci.org/home-assistant/home-assistant-polymer)
|
||||
# Home Assistant Frontend
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
@ -19,12 +19,15 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
## Frontend development
|
||||
|
||||
### Classic environment
|
||||
|
||||
A complete guide can be found at the following [link](https://www.home-assistant.io/developers/frontend/). It describes a short guide for the build of project.
|
||||
|
||||
### Docker environment
|
||||
|
||||
It is possible to compile the project and/or run commands in the development environment having only the [Docker](https://www.docker.com) pre-installed in the system. On the root of project you can do:
|
||||
* `sh ./script/docker_run.sh build` Build all the project with one command
|
||||
* `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the *classic environment*) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||
|
||||
- `sh ./script/docker_run.sh build` Build all the project with one command
|
||||
- `sh ./script/docker_run.sh bash` Open an interactive shell (the same environment generated by the _classic environment_) where you can run commands. This bash work on your project directory and any change on your file is automatically present within your build bash.
|
||||
|
||||
**Note**: if you have installed `npm` in addition to the `docker`, you can use the commands `npm run docker_build` and `npm run bash` to get a full build or bash as explained above
|
||||
|
||||
|
@ -24,7 +24,7 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
gulp.series("create-test-translation", "build-translations")
|
||||
"build-translations"
|
||||
),
|
||||
"copy-static",
|
||||
"webpack-watch-app"
|
||||
|
@ -65,6 +65,12 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
gulp.task("copy-translations", (done) => {
|
||||
const staticDir = paths.static;
|
||||
copyTranslations(staticDir);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
|
@ -2,6 +2,7 @@ const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const paths = require("../paths");
|
||||
const { mapFiles } = require("../util");
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(
|
||||
__dirname,
|
||||
@ -57,20 +58,6 @@ function generateIconset(iconsetName, iconNames) {
|
||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||
}
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all icons used by the project.
|
||||
function findIcons(searchPath, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||
|
@ -1,14 +1,18 @@
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const source = require("vinyl-source-stream");
|
||||
const vinylBuffer = require("vinyl-buffer");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const hash = require("gulp-hash");
|
||||
const hashFilename = require("gulp-hash-filename");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
const inDir = "translations";
|
||||
const workDir = "build-translations";
|
||||
@ -39,8 +43,6 @@ const TRANSLATION_FRAGMENTS = [
|
||||
"developer-tools",
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
@ -116,11 +118,9 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let taskName = "clean-translations";
|
||||
gulp.task(taskName, function() {
|
||||
return del([`${outDir}/**/*.json`]);
|
||||
gulp.task("clean-translations", function() {
|
||||
return del([workDir]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
@ -129,29 +129,23 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
taskName = "create-test-metadata";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("ensure-translations-build-dir", function writeTestMetaData(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("create-test-metadata", function(cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
test: {
|
||||
nativeName: "Test",
|
||||
},
|
||||
}),
|
||||
cb
|
||||
);
|
||||
});
|
||||
|
||||
taskName = "create-test-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("create-test-metadata", function() {
|
||||
"create-test-translation",
|
||||
gulp.series("create-test-metadata", function createTestTranslation() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return recursiveEmpty(data);
|
||||
@ -161,7 +155,6 @@ gulp.task(
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
@ -172,235 +165,215 @@ tasks.push(taskName);
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
taskName = "build-master-translation";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("clean-translations", function() {
|
||||
return gulp
|
||||
.src("src/translations/en.json")
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-master-translation", function() {
|
||||
return gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
|
||||
taskName = "build-merged-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
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
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
gulp.task("build-merged-translations", 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
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [workDir + "/translationMaster.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
var taskName;
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
gulp.task(taskName, function() {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment));
|
||||
});
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-merged-translations", function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task(taskName, function() {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir));
|
||||
});
|
||||
|
||||
splitTasks.push(taskName);
|
||||
|
||||
taskName = "build-flattened-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series(...splitTasks, function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(hashFilename())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
gulp.task("build-flattened-translations", function() {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
taskName = "build-translation-fingerprints";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-flattened-translations", function() {
|
||||
return gulp
|
||||
.src(outDir + "/**/*.json")
|
||||
.pipe(
|
||||
rename({
|
||||
extname: "",
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
hash({
|
||||
algorithm: "md5",
|
||||
hashLength: 32,
|
||||
template: "<%= name %>.json",
|
||||
})
|
||||
)
|
||||
.pipe(hash.manifest("translationFingerprints.json"))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
// After generating fingerprints of our translation files, consolidate
|
||||
// all translation fragment fingerprints under the translation name key
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const [path, _md5] = key.rsplit("-", 1);
|
||||
// let translation = key;
|
||||
let translation = path;
|
||||
const parts = translation.split("/");
|
||||
if (parts.length === 2) {
|
||||
translation = parts[1];
|
||||
}
|
||||
if (!(translation in newData)) {
|
||||
newData[translation] = {
|
||||
fingerprints: {},
|
||||
};
|
||||
}
|
||||
newData[translation].fingerprints[path] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
const fingerprints = {};
|
||||
|
||||
taskName = "build-translations";
|
||||
gulp.task(
|
||||
taskName,
|
||||
gulp.series("build-translation-fingerprints", function() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
"src/translations/translationMetadata.json",
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
})
|
||||
);
|
||||
tasks.push(taskName);
|
||||
"build-translation-fingerprints",
|
||||
function fingerprintTranslationFiles() {
|
||||
// Fingerprint full file of each language
|
||||
const files = fs.readdirSync(fullDir);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fingerprints[files[i].split(".")[0]] = {
|
||||
// In dev we create fake hashes
|
||||
hash: env.isProdBuild
|
||||
? crypto
|
||||
.createHash("md5")
|
||||
.update(fs.readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||
.digest("hex")
|
||||
: "dev",
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = tasks;
|
||||
mapFiles(outDir, ".json", (filename) => {
|
||||
const parsed = path.parse(filename);
|
||||
|
||||
// nl.json -> nl-<hash>.json
|
||||
if (!(parsed.name in fingerprints)) {
|
||||
throw new Error(`Unable to find hash for ${filename}`);
|
||||
}
|
||||
|
||||
fs.renameSync(
|
||||
filename,
|
||||
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
|
||||
parsed.ext
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
const stream = source("translationFingerprints.json");
|
||||
stream.write(JSON.stringify(fingerprints));
|
||||
process.nextTick(() => stream.end());
|
||||
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||
}
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
"clean-translations",
|
||||
"ensure-translations-build-dir",
|
||||
env.isProdBuild ? (done) => done() : "create-test-translation",
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
gulp.parallel(...splitTasks),
|
||||
"build-flattened-translations",
|
||||
"build-translation-fingerprints",
|
||||
function writeMetadata() {
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
path.join(paths.translations_src, "translationMetadata.json"),
|
||||
workDir + "/testMetadata.json",
|
||||
workDir + "/translationFingerprints.json",
|
||||
],
|
||||
{ allowEmpty: true }
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (data[key].nativeName) {
|
||||
newData[key] = data[key];
|
||||
} else {
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
);
|
||||
}
|
||||
if (data[key]) newData[key] = value;
|
||||
});
|
||||
return newData;
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ const gulp = require("gulp");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const log = require("fancy-log");
|
||||
const path = require("path");
|
||||
const paths = require("../paths");
|
||||
const {
|
||||
createAppConfig,
|
||||
@ -58,9 +59,13 @@ const handler = (done) => (err, stats) => {
|
||||
gulp.task("webpack-watch-app", () => {
|
||||
// we are not calling done, so this command will run forever
|
||||
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
|
||||
{},
|
||||
{ ignored: /build-translations/ },
|
||||
handler()
|
||||
);
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations")
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
|
@ -29,4 +29,6 @@ module.exports = {
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_publicPath: "/api/hassio/app/",
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
};
|
||||
|
16
build-scripts/util.js
Normal file
16
build-scripts/util.js
Normal file
@ -0,0 +1,16 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
};
|
@ -148,11 +148,17 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
// 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) => {
|
||||
const englishFilename = `en-${translationMetadata.translations.en.hash}.json`;
|
||||
|
||||
// core
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFilename}`
|
||||
] = `build-translations/output/${englishFilename}`;
|
||||
|
||||
Object.keys(translationMetadata.fragments).forEach((fragment) => {
|
||||
workBoxTranslationsTemplatedURLs[
|
||||
`/static/translations/${englishFP[key]}`
|
||||
] = `build-translations/output/${key}.json`;
|
||||
`/static/translations/${fragment}/${englishFilename}`
|
||||
] = `build-translations/output/${fragment}/${englishFilename}`;
|
||||
});
|
||||
|
||||
config.plugins.push(
|
||||
|
@ -26,10 +26,12 @@ import { CastManager } from "../../../../src/cast/cast_manager";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-layout";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@ -133,7 +135,9 @@ class HcCast extends LitElement {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const llColl = getLovelaceCollection(this.connection);
|
||||
const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
|
||||
? getLovelaceCollection(this.connection)
|
||||
: getLegacyLovelaceCollection(this.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
llColl.refresh().then(
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
LovelaceConfig,
|
||||
getLovelaceCollection,
|
||||
fetchResources,
|
||||
LegacyLovelaceConfig,
|
||||
getLegacyLovelaceCollection,
|
||||
} from "../../../../src/data/lovelace";
|
||||
import "./hc-launch-screen";
|
||||
import { castContext } from "../cast_context";
|
||||
@ -23,6 +25,7 @@ import { CAST_NS } from "../../../../src/cast/const";
|
||||
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
|
||||
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
|
||||
let resourcesLoaded = false;
|
||||
|
||||
@ -168,19 +171,14 @@ export class HcMain extends HassElement {
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
return;
|
||||
}
|
||||
if (!resourcesLoaded) {
|
||||
resourcesLoaded = true;
|
||||
loadLovelaceResources(
|
||||
await fetchResources(this.hass!.connection),
|
||||
this.hass!.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
this._urlPath = msg.urlPath;
|
||||
if (this._unsubLovelace) {
|
||||
this._unsubLovelace();
|
||||
}
|
||||
const llColl = getLovelaceCollection(this.hass!.connection, msg.urlPath);
|
||||
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
|
||||
: getLegacyLovelaceCollection(this.hass!.connection);
|
||||
// We first do a single refresh because we need to check if there is LL
|
||||
// configuration.
|
||||
try {
|
||||
@ -199,6 +197,15 @@ export class HcMain extends HassElement {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!resourcesLoaded) {
|
||||
resourcesLoaded = true;
|
||||
const resources = atLeastVersion(this.hass.connection.haVersion, 0, 107)
|
||||
? await fetchResources(this.hass!.connection)
|
||||
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
|
||||
if (resources) {
|
||||
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
|
||||
}
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
|
BIN
demo/public/api/media_player_proxy/media_player.family_room_2
Normal file
BIN
demo/public/api/media_player_proxy/media_player.family_room_2
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
@ -18,6 +18,7 @@ import {
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@ -39,7 +40,6 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const repo = this.repo;
|
||||
const addons = this._getAddons(this.addons, this.filter);
|
||||
const ha105pluss = this._computeHA105plus;
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
@ -57,7 +57,9 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
</h1>
|
||||
<p class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
|
||||
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
|
||||
${repo.url}
|
||||
</a>
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
@ -90,7 +92,11 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${ha105pluss && addon.icon
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
@ -115,11 +121,6 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
}
|
||||
|
||||
private get _computeHA105plus(): boolean {
|
||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
hassioStyle,
|
||||
|
@ -36,6 +36,7 @@ import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
const PERMIS_DESC = {
|
||||
rating: {
|
||||
@ -185,14 +186,19 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
Visit
|
||||
<a href="${this.addon.url}" target="_blank">
|
||||
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
|
||||
${this.addon.name} page</a
|
||||
>
|
||||
for details.
|
||||
</div>
|
||||
${this.addon.logo
|
||||
? html`
|
||||
<a href="${this.addon.url}" target="_blank" class="logo">
|
||||
<a
|
||||
href="${this.addon.url}"
|
||||
target="_blank"
|
||||
class="logo"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
||||
</a>
|
||||
`
|
||||
@ -428,6 +434,7 @@ class HassioAddonInfo extends LitElement {
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
rel="noopener"
|
||||
>
|
||||
<mwc-button>
|
||||
Open web UI
|
||||
@ -653,7 +660,10 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private get _computeCannotIngressSidebar(): boolean {
|
||||
return !this.addon.ingress || !this._computeHA92plus;
|
||||
return (
|
||||
!this.addon.ingress ||
|
||||
!atLeastVersion(this.hass.connection.haVersion, 0, 92)
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeUsesProtectedOptions(): boolean {
|
||||
@ -662,11 +672,6 @@ class HassioAddonInfo extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeHA92plus(): boolean {
|
||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
|
||||
}
|
||||
|
||||
private async _startOnBootToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
|
@ -15,6 +15,7 @@ import { navigate } from "../../../src/common/navigate";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import "../components/hassio-card-content";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
|
||||
@customElement("hassio-addons")
|
||||
class HassioAddons extends LitElement {
|
||||
@ -22,9 +23,6 @@ class HassioAddons extends LitElement {
|
||||
@property() public addons?: HassioAddonInfo[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const [major, minor] = this.hass.config.version.split(".", 2);
|
||||
const ha105pluss =
|
||||
Number(major) > 0 || (major === "0" && Number(minor) >= 105);
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Add-ons</h1>
|
||||
@ -68,7 +66,11 @@ class HassioAddons extends LitElement {
|
||||
: addon.installed && addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${ha105pluss && addon.icon
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.connection.haVersion,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></hassio-card-content>
|
||||
|
@ -123,7 +123,7 @@ export class HassioUpdate extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="${releaseNotesUrl}" target="_blank">
|
||||
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
|
||||
<mwc-button>Release notes</mwc-button>
|
||||
</a>
|
||||
<ha-call-api-button
|
||||
|
13
package.json
13
package.json
@ -19,8 +19,6 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@material/chips": "^5.0.0",
|
||||
"@material/data-table": "^5.0.0",
|
||||
"@material/mwc-base": "^0.13.0",
|
||||
"@material/mwc-button": "^0.13.0",
|
||||
"@material/mwc-checkbox": "^0.13.0",
|
||||
"@material/mwc-dialog": "^0.13.0",
|
||||
@ -70,6 +68,7 @@
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@thomasloven/round-slider": "0.3.7",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||
"@vaadin/vaadin-date-picker": "^4.0.7",
|
||||
"@webcomponents/shadycss": "^1.9.0",
|
||||
@ -85,7 +84,7 @@
|
||||
"fuse.js": "^3.4.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "4.4.1",
|
||||
"home-assistant-js-websocket": "4.5.0",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.4.0",
|
||||
@ -97,10 +96,12 @@
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"moment": "^2.24.0",
|
||||
"node-vibrant": "^3.1.5",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.18.4",
|
||||
"react-big-calendar": "^0.20.4",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"resize-observer": "^1.0.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"superstruct": "^0.6.1",
|
||||
"tslib": "^1.10.0",
|
||||
@ -145,13 +146,11 @@
|
||||
"fs-extra": "^7.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-hash": "^4.2.2",
|
||||
"gulp-hash-filename": "^2.0.1",
|
||||
"gulp-insert": "^0.5.0",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
@ -174,6 +173,8 @@
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"typescript": "^3.7.2",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"web-component-tester": "^6.9.2",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
|
@ -11,17 +11,13 @@
|
||||
"src/panels/dev-template/ha-panel-dev-template.js",
|
||||
"src/panels/history/ha-panel-history.js",
|
||||
"src/panels/iframe/ha-panel-iframe.js",
|
||||
"src/panels/kiosk/ha-panel-kiosk.js",
|
||||
"src/panels/logbook/ha-panel-logbook.js",
|
||||
"src/panels/map/ha-panel-map.js",
|
||||
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||
"hassio/src/entrypoint.js"
|
||||
],
|
||||
"sources": [
|
||||
"src/**/*",
|
||||
"!src/translations/*"
|
||||
],
|
||||
"sources": ["src/**/*", "!src/translations/*"],
|
||||
"lint": {
|
||||
"rules": ["polymer-3"],
|
||||
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200228.0",
|
||||
version="20200306.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -1,45 +0,0 @@
|
||||
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 {
|
||||
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,127 +0,0 @@
|
||||
import "@polymer/paper-styles/element-styles/paper-material-styles";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
import { fetchThumbnailUrlWithCache } from "../data/camera";
|
||||
|
||||
const UPDATE_INTERVAL = 10000; // ms
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
min-height: 48px;
|
||||
line-height: 0;
|
||||
}
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.caption {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||
<img
|
||||
src="[[cameraFeedSrc]]"
|
||||
class="camera-feed"
|
||||
alt="[[_computeStateName(stateObj)]]"
|
||||
on-load="_imageLoaded"
|
||||
on-error="_imageError"
|
||||
/>
|
||||
</template>
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<template is="dom-if" if="[[!imageLoaded]]">
|
||||
([[localize('ui.card.camera.not_available')]])
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: "updateCameraFeedSrc",
|
||||
},
|
||||
cameraFeedSrc: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
imageLoaded: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener("click", () => this.cardTapped());
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
_imageLoaded() {
|
||||
this.imageLoaded = true;
|
||||
}
|
||||
|
||||
_imageError() {
|
||||
this.imageLoaded = false;
|
||||
}
|
||||
|
||||
cardTapped() {
|
||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
||||
}
|
||||
|
||||
async updateCameraFeedSrc() {
|
||||
this.cameraFeedSrc = await fetchThumbnailUrlWithCache(
|
||||
this.hass,
|
||||
this.stateObj.entity_id
|
||||
);
|
||||
}
|
||||
|
||||
_computeStateName(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-camera-card", HaCameraCard);
|
@ -1,81 +0,0 @@
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "./ha-camera-card";
|
||||
import "./ha-entities-card";
|
||||
import "./ha-history_graph-card";
|
||||
import "./ha-media_player-card";
|
||||
import "./ha-persistent_notification-card";
|
||||
import "./ha-plant-card";
|
||||
import "./ha-weather-card";
|
||||
|
||||
import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
|
||||
|
||||
class HaCardChooser extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
cardData: {
|
||||
type: Object,
|
||||
observer: "cardDataChanged",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_updateCard(newData) {
|
||||
dynamicContentUpdater(
|
||||
this,
|
||||
"HA-" + newData.cardType.toUpperCase() + "-CARD",
|
||||
newData
|
||||
);
|
||||
}
|
||||
|
||||
createObserver() {
|
||||
this._updatesAllowed = false;
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
if (!entries.length) return;
|
||||
if (entries[0].isIntersecting) {
|
||||
this.style.height = "";
|
||||
if (this._detachedChild) {
|
||||
this.appendChild(this._detachedChild);
|
||||
this._detachedChild = null;
|
||||
}
|
||||
this._updateCard(this.cardData); // Don't use 'newData' as it might have changed.
|
||||
this._updatesAllowed = true;
|
||||
} else {
|
||||
// Set the card to be 48px high. Otherwise if the card is kept as 0px height then all
|
||||
// following cards would trigger the observer at once.
|
||||
const offsetHeight = this.offsetHeight;
|
||||
this.style.height = `${offsetHeight || 48}px`;
|
||||
if (this.lastChild) {
|
||||
this._detachedChild = this.lastChild;
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this._updatesAllowed = false;
|
||||
}
|
||||
});
|
||||
this.observer.observe(this);
|
||||
}
|
||||
|
||||
cardDataChanged(newData) {
|
||||
if (!newData) return;
|
||||
// ha-entities-card is exempt from observer as it doesn't load heavy resources.
|
||||
// and usually doesn't load external resources (except for entity_picture).
|
||||
const eligibleToObserver =
|
||||
window.IntersectionObserver && newData.cardType !== "entities";
|
||||
if (!eligibleToObserver) {
|
||||
if (this.observer) {
|
||||
this.observer.unobserve(this);
|
||||
this.observer = null;
|
||||
}
|
||||
this.style.height = "";
|
||||
this._updateCard(newData);
|
||||
return;
|
||||
}
|
||||
if (!this.observer) {
|
||||
this.createObserver();
|
||||
}
|
||||
if (this._updatesAllowed) {
|
||||
this._updateCard(newData);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("ha-card-chooser", HaCardChooser);
|
@ -1,182 +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 "../components/entity/ha-entity-toggle";
|
||||
import "../components/ha-card";
|
||||
import "../state-summary/state-card-content";
|
||||
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stateMoreInfoType } from "../common/entity/state_more_info_type";
|
||||
import { canToggleState } from "../common/entity/can_toggle_state";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.states {
|
||||
margin: -4px 0;
|
||||
}
|
||||
.state {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
||||
compensating this with reduced padding */
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
.header .name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
ha-entity-toggle {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.more-info {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card>
|
||||
<template is="dom-if" if="[[title]]">
|
||||
<div
|
||||
class$="[[computeTitleClass(groupEntity)]]"
|
||||
on-click="entityTapped"
|
||||
>
|
||||
<div class="flex name">[[title]]</div>
|
||||
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[groupEntity]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="states">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[states]]"
|
||||
on-dom-change="addTapEvents"
|
||||
>
|
||||
<div class$="[[computeStateClass(item)]]">
|
||||
<state-card-content
|
||||
hass="[[hass]]"
|
||||
class="state-card"
|
||||
state-obj="[[item]]"
|
||||
></state-card-content>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
states: Array,
|
||||
groupEntity: Object,
|
||||
title: {
|
||||
type: String,
|
||||
computed: "computeTitle(states, groupEntity, localize)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// We need to save a single bound function reference to ensure the event listener
|
||||
// can identify it properly.
|
||||
this.entityTapped = this.entityTapped.bind(this);
|
||||
}
|
||||
|
||||
computeTitle(states, groupEntity, localize) {
|
||||
if (groupEntity) {
|
||||
return computeStateName(groupEntity).trim();
|
||||
}
|
||||
const domain = computeStateDomain(states[0]);
|
||||
return (
|
||||
(localize && localize(`domain.${domain}`)) || domain.replace(/_/g, " ")
|
||||
);
|
||||
}
|
||||
|
||||
computeTitleClass(groupEntity) {
|
||||
let classes = "header horizontal layout center ";
|
||||
if (groupEntity) {
|
||||
classes += "more-info";
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
computeStateClass(stateObj) {
|
||||
return stateMoreInfoType(stateObj) !== "hidden"
|
||||
? "state more-info"
|
||||
: "state";
|
||||
}
|
||||
|
||||
addTapEvents() {
|
||||
const cards = this.root.querySelectorAll(".state");
|
||||
cards.forEach((card) => {
|
||||
if (card.classList.contains("more-info")) {
|
||||
card.addEventListener("click", this.entityTapped);
|
||||
} else {
|
||||
card.removeEventListener("click", this.entityTapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
entityTapped(ev) {
|
||||
const item = this.root
|
||||
.querySelector("dom-repeat")
|
||||
.itemForElement(ev.target);
|
||||
let entityId;
|
||||
if (!item && !this.groupEntity) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
if (item) {
|
||||
entityId = item.entity_id;
|
||||
} else {
|
||||
entityId = this.groupEntity.entity_id;
|
||||
}
|
||||
this.fire("hass-more-info", { entityId: entityId });
|
||||
}
|
||||
|
||||
showGroupToggle(groupEntity, states) {
|
||||
if (
|
||||
!groupEntity ||
|
||||
!states ||
|
||||
groupEntity.attributes.control === "hidden" ||
|
||||
(groupEntity.state !== "on" && groupEntity.state !== "off")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show if we can toggle 2+ entities in group
|
||||
let canToggleCount = 0;
|
||||
for (let i = 0; i < states.length; i++) {
|
||||
if (!canToggleState(this.hass, states[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
canToggleCount++;
|
||||
|
||||
if (canToggleCount > 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return canToggleCount > 1;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-entities-card", HaEntitiesCard);
|
@ -1,409 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-progress/paper-progress";
|
||||
import "@polymer/paper-styles/element-styles/paper-material-styles";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import HassMediaPlayerEntity from "../util/hass-media-player-model";
|
||||
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
|
||||
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style
|
||||
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
|
||||
>
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
.banner:before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
/* removed .25% from 16:9 ratio to fix YT black bars */
|
||||
padding-top: 56%;
|
||||
transition: padding-top 0.8s;
|
||||
}
|
||||
|
||||
.banner.no-cover {
|
||||
background-position: center center;
|
||||
background-image: url(/static/images/card_media_player_bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.banner.content-type-music:before {
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.banner.content-type-game:before {
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.banner.no-cover:before {
|
||||
padding-top: 88px;
|
||||
}
|
||||
|
||||
.banner > .cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
transition: opacity 0.8s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.banner.is-off > .cover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.banner > .caption {
|
||||
@apply --paper-font-caption;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
|
||||
padding: 8px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.banner.is-off > .caption {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.banner > .caption .title {
|
||||
@apply --paper-font-common-nowrap;
|
||||
font-size: 1.2em;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: var(--paper-progress-height, 4px);
|
||||
margin-top: calc(-1 * var(--paper-progress-height, 4px));
|
||||
--paper-progress-active-color: var(--accent-color);
|
||||
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
@apply --paper-font-body1;
|
||||
padding: 8px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
|
||||
.controls paper-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
paper-icon-button[disabled] {
|
||||
opacity: var(--dark-disabled-opacity);
|
||||
}
|
||||
|
||||
paper-icon-button.primary {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
paper-icon-button.primary[disabled] {
|
||||
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
|
||||
}
|
||||
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
class$="[[computeBannerClasses(playerObj, _coverShowing, _coverLoadError)]]"
|
||||
>
|
||||
<div class="cover" id="cover"></div>
|
||||
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
|
||||
[[playerObj.secondaryTitle]]<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-progress
|
||||
max="[[stateObj.attributes.media_duration]]"
|
||||
value="[[playbackPosition]]"
|
||||
hidden$="[[computeHideProgress(playerObj)]]"
|
||||
class="progress"
|
||||
></paper-progress>
|
||||
|
||||
<div class="controls layout horizontal justified">
|
||||
<paper-icon-button
|
||||
aria-label="Turn off"
|
||||
icon="hass:power"
|
||||
on-click="handleTogglePower"
|
||||
invisible$="[[computeHidePowerButton(playerObj)]]"
|
||||
class="self-center secondary"
|
||||
></paper-icon-button>
|
||||
|
||||
<div class="playback-controls">
|
||||
<paper-icon-button
|
||||
aria-label="Previous track"
|
||||
icon="hass:skip-previous"
|
||||
invisible$="[[!playerObj.supportsPreviousTrack]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handlePrevious"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
aria-label="Play or Pause"
|
||||
class="primary"
|
||||
icon="[[computePlaybackControlIcon(playerObj)]]"
|
||||
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handlePlaybackControl"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
aria-label="Next track"
|
||||
icon="hass:skip-next"
|
||||
invisible$="[[!playerObj.supportsNextTrack]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handleNext"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
|
||||
<paper-icon-button
|
||||
aria-label="More information."
|
||||
icon="hass:dots-vertical"
|
||||
on-click="handleOpenMoreInfo"
|
||||
class="self-center secondary"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
playerObj: {
|
||||
type: Object,
|
||||
computed: "computePlayerObj(hass, stateObj)",
|
||||
observer: "playerObjChanged",
|
||||
},
|
||||
playbackControlIcon: {
|
||||
type: String,
|
||||
computed: "computePlaybackControlIcon(playerObj)",
|
||||
},
|
||||
playbackPosition: Number,
|
||||
_coverShowing: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
_coverLoadError: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async playerObjChanged(playerObj, oldPlayerObj) {
|
||||
if (playerObj.isPlaying && playerObj.showProgress) {
|
||||
if (!this._positionTracking) {
|
||||
this._positionTracking = setInterval(
|
||||
() => this.updatePlaybackPosition(),
|
||||
1000
|
||||
);
|
||||
}
|
||||
} else if (this._positionTracking) {
|
||||
clearInterval(this._positionTracking);
|
||||
this._positionTracking = null;
|
||||
}
|
||||
if (playerObj.showProgress) {
|
||||
this.updatePlaybackPosition();
|
||||
}
|
||||
|
||||
const picture = playerObj.stateObj.attributes.entity_picture;
|
||||
const oldPicture =
|
||||
oldPlayerObj && oldPlayerObj.stateObj.attributes.entity_picture;
|
||||
|
||||
if (picture !== oldPicture && !picture) {
|
||||
this.$.cover.style.backgroundImage = "";
|
||||
return;
|
||||
}
|
||||
if (picture === oldPicture) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have a new picture url
|
||||
// If entity picture is non-relative, we use that url directly.
|
||||
if (picture.substr(0, 1) !== "/") {
|
||||
this._coverShowing = true;
|
||||
this._coverLoadError = false;
|
||||
this.$.cover.style.backgroundImage = `url(${picture})`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
content_type: contentType,
|
||||
content,
|
||||
} = await fetchMediaPlayerThumbnailWithCache(
|
||||
this.hass,
|
||||
playerObj.stateObj.entity_id
|
||||
);
|
||||
this._coverShowing = true;
|
||||
this._coverLoadError = false;
|
||||
this.$.cover.style.backgroundImage = `url(data:${contentType};base64,${content})`;
|
||||
} catch (err) {
|
||||
this._coverShowing = false;
|
||||
this._coverLoadError = true;
|
||||
this.$.cover.style.backgroundImage = "";
|
||||
}
|
||||
}
|
||||
|
||||
updatePlaybackPosition() {
|
||||
this.playbackPosition = this.playerObj.currentProgress;
|
||||
}
|
||||
|
||||
computeBannerClasses(playerObj, coverShowing, coverLoadError) {
|
||||
var cls = "banner";
|
||||
|
||||
if (!playerObj) {
|
||||
return cls;
|
||||
}
|
||||
|
||||
if (playerObj.isOff || playerObj.isIdle) {
|
||||
cls += " is-off no-cover";
|
||||
} else if (
|
||||
!playerObj.stateObj.attributes.entity_picture ||
|
||||
coverLoadError ||
|
||||
!coverShowing
|
||||
) {
|
||||
cls += " no-cover";
|
||||
} else if (playerObj.stateObj.attributes.media_content_type === "music") {
|
||||
cls += " content-type-music";
|
||||
} else if (playerObj.stateObj.attributes.media_content_type === "game") {
|
||||
cls += " content-type-game";
|
||||
}
|
||||
return cls;
|
||||
}
|
||||
|
||||
computeHideProgress(playerObj) {
|
||||
return !playerObj.showProgress;
|
||||
}
|
||||
|
||||
computeHidePowerButton(playerObj) {
|
||||
return playerObj.isOff
|
||||
? !playerObj.supportsTurnOn
|
||||
: !playerObj.supportsTurnOff;
|
||||
}
|
||||
|
||||
computePlayerObj(hass, stateObj) {
|
||||
return new HassMediaPlayerEntity(hass, stateObj);
|
||||
}
|
||||
|
||||
computePrimaryText(localize, playerObj) {
|
||||
return (
|
||||
playerObj.primaryTitle ||
|
||||
localize(`state.media_player.${playerObj.stateObj.state}`) ||
|
||||
localize(`state.default.${playerObj.stateObj.state}`) ||
|
||||
playerObj.stateObj.state
|
||||
);
|
||||
}
|
||||
|
||||
computePlaybackControlIcon(playerObj) {
|
||||
if (playerObj.isPlaying) {
|
||||
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
|
||||
}
|
||||
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
|
||||
if (
|
||||
playerObj.hasMediaControl &&
|
||||
playerObj.supportsPause &&
|
||||
!playerObj.isPaused
|
||||
) {
|
||||
return "hass:play-pause";
|
||||
}
|
||||
return playerObj.supportsPlay ? "hass:play" : null;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
_computeStateName(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
handleNext(ev) {
|
||||
ev.stopPropagation();
|
||||
this.playerObj.nextTrack();
|
||||
}
|
||||
|
||||
handleOpenMoreInfo(ev) {
|
||||
ev.stopPropagation();
|
||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
||||
}
|
||||
|
||||
handlePlaybackControl(ev) {
|
||||
ev.stopPropagation();
|
||||
this.playerObj.mediaPlayPause();
|
||||
}
|
||||
|
||||
handlePrevious(ev) {
|
||||
ev.stopPropagation();
|
||||
this.playerObj.previousTrack();
|
||||
}
|
||||
|
||||
handleTogglePower(ev) {
|
||||
ev.stopPropagation();
|
||||
this.playerObj.togglePower();
|
||||
}
|
||||
}
|
||||
customElements.define("ha-media_player-card", HaMediaPlayerCard);
|
@ -1,76 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-markdown";
|
||||
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
}
|
||||
ha-markdown {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
ha-markdown p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
mwc-button {
|
||||
margin: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card header="[[computeTitle(stateObj)]]">
|
||||
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
|
||||
<mwc-button on-click="dismissTap"
|
||||
>[[localize('ui.card.persistent_notification.dismiss')]]</mwc-button
|
||||
>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
};
|
||||
}
|
||||
|
||||
computeTitle(stateObj) {
|
||||
return stateObj.attributes.title || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
dismissTap(ev) {
|
||||
ev.preventDefault();
|
||||
this.hass.callService("persistent_notification", "dismiss", {
|
||||
notification_id: computeObjectId(this.stateObj.entity_id),
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
"ha-persistent_notification-card",
|
||||
HaPersistentNotificationCard
|
||||
);
|
@ -1,165 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-icon";
|
||||
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
|
||||
class HaPlantCard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.has-plant-image .banner {
|
||||
padding-top: 30%;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
line-height: 40px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.has-plant-image .header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px 24px 32px;
|
||||
}
|
||||
.has-plant-image .content {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
ha-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.attributes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.attributes div {
|
||||
text-align: center;
|
||||
}
|
||||
.problem {
|
||||
color: var(--google-red-500);
|
||||
font-weight: bold;
|
||||
}
|
||||
.uom {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card
|
||||
class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
|
||||
>
|
||||
<div
|
||||
class="banner"
|
||||
style="background-image:url([[stateObj.attributes.entity_picture]])"
|
||||
>
|
||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[computeAttributes(stateObj.attributes)]]"
|
||||
>
|
||||
<div class="attributes" on-click="attributeClicked">
|
||||
<div>
|
||||
<ha-icon
|
||||
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
|
||||
></ha-icon>
|
||||
</div>
|
||||
<div
|
||||
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
|
||||
>
|
||||
[[computeValue(stateObj.attributes, item)]]
|
||||
</div>
|
||||
<div class="uom">
|
||||
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
|
||||
item)]]
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
config: Object,
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.sensors = {
|
||||
moisture: "hass:water",
|
||||
temperature: "hass:thermometer",
|
||||
brightness: "hass:white-balance-sunny",
|
||||
conductivity: "hass:emoticon-poop",
|
||||
battery: "hass:battery",
|
||||
};
|
||||
}
|
||||
|
||||
computeTitle(stateObj) {
|
||||
return (this.config && this.config.name) || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
computeAttributes(data) {
|
||||
return Object.keys(this.sensors).filter((key) => key in data);
|
||||
}
|
||||
|
||||
computeIcon(attr, batLvl) {
|
||||
const icon = this.sensors[attr];
|
||||
if (attr === "battery") {
|
||||
if (batLvl <= 5) {
|
||||
return `${icon}-alert`;
|
||||
}
|
||||
if (batLvl < 95) {
|
||||
return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`;
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
computeValue(attributes, attr) {
|
||||
return attributes[attr];
|
||||
}
|
||||
|
||||
computeUom(dict, attr) {
|
||||
return dict[attr] || "";
|
||||
}
|
||||
|
||||
computeAttributeClass(problem, attr) {
|
||||
return problem.indexOf(attr) === -1 ? "" : "problem";
|
||||
}
|
||||
|
||||
computeImageClass(entityPicture) {
|
||||
return entityPicture ? "has-plant-image" : "";
|
||||
}
|
||||
|
||||
attributeClicked(ev) {
|
||||
this.fire("hass-more-info", {
|
||||
entityId: this.stateObj.attributes.sensors[ev.model.item],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-plant-card", HaPlantCard);
|
@ -1,383 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-icon";
|
||||
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
);
|
||||
font-size: var(--paper-font-headline_-_font-size);
|
||||
font-weight: var(--paper-font-headline_-_font-weight);
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
text-rendering: var(
|
||||
--paper-font-common-expensive-kerning_-_text-rendering
|
||||
);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
padding: 24px 16px 16px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 16px;
|
||||
font-size: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .name {
|
||||
margin-left: 0px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.now {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
:host([rtl]) .main {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.main ha-icon {
|
||||
--iron-icon-height: 72px;
|
||||
--iron-icon-width: 72px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
:host([rtl]) .main ha-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.main .temp {
|
||||
font-size: 52px;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([rtl]) .main .temp {
|
||||
direction: ltr;
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.main .temp span {
|
||||
font-size: 24px;
|
||||
line-height: 1em;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.measurand {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:host([rtl]) .measurand {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.forecast {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.forecast div {
|
||||
flex: 0 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forecast .icon {
|
||||
margin: 4px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:host([rtl]) .forecast .temp {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.attributes,
|
||||
.templow,
|
||||
.precipitation {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .precipitation {
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
[[computeState(stateObj.state, localize)]]
|
||||
<div class="name">[[computeName(stateObj)]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="now">
|
||||
<div class="main">
|
||||
<template is="dom-if" if="[[showWeatherIcon(stateObj.state)]]">
|
||||
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
|
||||
</template>
|
||||
<div class="temp">
|
||||
[[stateObj.attributes.temperature]]<span
|
||||
>[[getUnit('temperature')]]</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attributes">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.pressure)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.air_pressure')]]:
|
||||
<span class="measurand">
|
||||
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.humidity)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.humidity')]]:
|
||||
<span class="measurand"
|
||||
>[[stateObj.attributes.humidity]] %</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.wind_speed)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.wind_speed')]]:
|
||||
<span class="measurand">
|
||||
[[getWindSpeed(stateObj.attributes.wind_speed)]]
|
||||
</span>
|
||||
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template is="dom-if" if="[[forecast]]">
|
||||
<div class="forecast">
|
||||
<template is="dom-repeat" items="[[forecast]]">
|
||||
<div>
|
||||
<div class="weekday">
|
||||
[[computeDate(item.datetime)]]<br />
|
||||
<template is="dom-if" if="[[!_showValue(item.templow)]]">
|
||||
[[computeTime(item.datetime)]]
|
||||
</template>
|
||||
</div>
|
||||
<template is="dom-if" if="[[_showValue(item.condition)]]">
|
||||
<div class="icon">
|
||||
<ha-icon
|
||||
icon="[[getWeatherIcon(item.condition)]]"
|
||||
></ha-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(item.temperature)]]">
|
||||
<div class="temp">
|
||||
[[item.temperature]] [[getUnit('temperature')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(item.templow)]]">
|
||||
<div class="templow">
|
||||
[[item.templow]] [[getUnit('temperature')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(item.precipitation)]]">
|
||||
<div class="precipitation">
|
||||
[[item.precipitation]] [[getUnit('precipitation')]]
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
config: Object,
|
||||
stateObj: Object,
|
||||
forecast: {
|
||||
type: Array,
|
||||
computed: "computeForecast(stateObj.attributes.forecast)",
|
||||
},
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.cardinalDirections = [
|
||||
"N",
|
||||
"NNE",
|
||||
"NE",
|
||||
"ENE",
|
||||
"E",
|
||||
"ESE",
|
||||
"SE",
|
||||
"SSE",
|
||||
"S",
|
||||
"SSW",
|
||||
"SW",
|
||||
"WSW",
|
||||
"W",
|
||||
"WNW",
|
||||
"NW",
|
||||
"NNW",
|
||||
"N",
|
||||
];
|
||||
this.weatherIcons = {
|
||||
"clear-night": "hass:weather-night",
|
||||
cloudy: "hass:weather-cloudy",
|
||||
exceptional: "hass:alert-circle-outline",
|
||||
fog: "hass:weather-fog",
|
||||
hail: "hass:weather-hail",
|
||||
lightning: "hass:weather-lightning",
|
||||
"lightning-rainy": "hass:weather-lightning-rainy",
|
||||
partlycloudy: "hass:weather-partly-cloudy",
|
||||
pouring: "hass:weather-pouring",
|
||||
rainy: "hass:weather-rainy",
|
||||
snowy: "hass:weather-snowy",
|
||||
"snowy-rainy": "hass:weather-snowy-rainy",
|
||||
sunny: "hass:weather-sunny",
|
||||
windy: "hass:weather-windy",
|
||||
"windy-variant": "hass:weather-windy-variant",
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.addEventListener("click", this._onClick);
|
||||
super.ready();
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
|
||||
}
|
||||
|
||||
computeForecast(forecast) {
|
||||
return forecast && forecast.slice(0, 5);
|
||||
}
|
||||
|
||||
getUnit(measure) {
|
||||
const lengthUnit = this.hass.config.unit_system.length || "";
|
||||
switch (measure) {
|
||||
case "air_pressure":
|
||||
return lengthUnit === "km" ? "hPa" : "inHg";
|
||||
case "length":
|
||||
return lengthUnit;
|
||||
case "precipitation":
|
||||
return lengthUnit === "km" ? "mm" : "in";
|
||||
default:
|
||||
return this.hass.config.unit_system[measure] || "";
|
||||
}
|
||||
}
|
||||
|
||||
computeState(state, localize) {
|
||||
return localize(`state.weather.${state}`) || state;
|
||||
}
|
||||
|
||||
computeName(stateObj) {
|
||||
return (this.config && this.config.name) || computeStateName(stateObj);
|
||||
}
|
||||
|
||||
showWeatherIcon(condition) {
|
||||
return condition in this.weatherIcons;
|
||||
}
|
||||
|
||||
getWeatherIcon(condition) {
|
||||
return this.weatherIcons[condition];
|
||||
}
|
||||
|
||||
windBearingToText(degree) {
|
||||
const degreenum = parseInt(degree);
|
||||
if (isFinite(degreenum)) {
|
||||
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
|
||||
}
|
||||
return degree;
|
||||
}
|
||||
|
||||
getWindSpeed(speed) {
|
||||
return `${speed} ${this.getUnit("length")}/h`;
|
||||
}
|
||||
|
||||
getWindBearing(bearing, localize) {
|
||||
if (bearing != null) {
|
||||
const cardinalDirection = this.windBearingToText(bearing);
|
||||
return `(${localize(
|
||||
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
|
||||
) || cardinalDirection})`;
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
|
||||
_showValue(item) {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
}
|
||||
|
||||
computeDate(data) {
|
||||
const date = new Date(data);
|
||||
return date.toLocaleDateString(this.hass.language, { weekday: "short" });
|
||||
}
|
||||
|
||||
computeTime(data) {
|
||||
const date = new Date(data);
|
||||
return date.toLocaleTimeString(this.hass.language, { hour: "numeric" });
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-weather-card", HaWeatherCard);
|
11
src/common/config/version.ts
Normal file
11
src/common/config/version.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const atLeastVersion = (
|
||||
version: string,
|
||||
major: number,
|
||||
minor: number
|
||||
): boolean => {
|
||||
const [haMajor, haMinor] = version.split(".", 2);
|
||||
return (
|
||||
Number(haMajor) > major ||
|
||||
(Number(haMajor) === major && Number(haMinor) >= minor)
|
||||
);
|
||||
};
|
107
src/common/util/deep-equal.ts
Normal file
107
src/common/util/deep-equal.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a && b && typeof a === "object" && typeof b === "object") {
|
||||
if (a.constructor !== b.constructor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i: number | [any, any];
|
||||
let length: number;
|
||||
if (Array.isArray(a)) {
|
||||
length = a.length;
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Map && b instanceof Map) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Set && b instanceof Set) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||||
// @ts-ignore
|
||||
length = a.length;
|
||||
// @ts-ignore
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.constructor === RegExp) {
|
||||
return a.source === b.source && a.flags === b.flags;
|
||||
}
|
||||
if (a.valueOf !== Object.prototype.valueOf) {
|
||||
return a.valueOf() === b.valueOf();
|
||||
}
|
||||
if (a.toString !== Object.prototype.toString) {
|
||||
return a.toString() === b.toString();
|
||||
}
|
||||
|
||||
let keys: string[];
|
||||
keys = Object.keys(a);
|
||||
length = keys.length;
|
||||
if (length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// true if both NaN, false otherwise
|
||||
return a !== a && b !== b;
|
||||
};
|
@ -1,27 +1,21 @@
|
||||
import { repeat } from "lit-html/directives/repeat";
|
||||
import deepClone from "deep-clone-simple";
|
||||
|
||||
import {
|
||||
MDCDataTableAdapter,
|
||||
MDCDataTableFoundation,
|
||||
} from "@material/data-table";
|
||||
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
import { scroll } from "lit-virtualizer";
|
||||
|
||||
import {
|
||||
html,
|
||||
query,
|
||||
queryAll,
|
||||
CSSResult,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
LitElement,
|
||||
} from "lit-element";
|
||||
|
||||
import { BaseElement } from "@material/mwc-base/base-element";
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
// @ts-ignore
|
||||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
@ -35,6 +29,8 @@ import { HaCheckbox } from "../ha-checkbox";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@ -50,8 +46,7 @@ export interface RowClickedEvent {
|
||||
}
|
||||
|
||||
export interface SelectionChangedEvent {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface SortingChangedEvent {
|
||||
@ -76,6 +71,8 @@ export interface DataTableColumnData extends DataTableSortColumnData {
|
||||
title: string;
|
||||
type?: "numeric" | "icon";
|
||||
template?: <T>(data: any, row: T) => TemplateResult | string;
|
||||
width?: string;
|
||||
grows?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableRowData {
|
||||
@ -84,26 +81,23 @@ export interface DataTableRowData {
|
||||
}
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends BaseElement {
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
@property({ type: Boolean, attribute: "auto-height" })
|
||||
public autoHeight = 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[];
|
||||
@property({ type: Boolean }) private _filterable = false;
|
||||
@property({ type: Boolean }) private _headerChecked = false;
|
||||
@property({ type: Boolean }) private _headerIndeterminate = false;
|
||||
@property({ type: Array }) private _checkedRows: string[] = [];
|
||||
@property({ type: String }) private _filter = "";
|
||||
@property({ type: String }) private _sortColumn?: string;
|
||||
@property({ type: String }) private _sortDirection: SortingDirection = null;
|
||||
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
@query(".scroller") private _scroller!: HTMLDivElement;
|
||||
@query(".mdc-data-table__table") private _table!: HTMLDivElement;
|
||||
private _checkableRowsCount?: number;
|
||||
private _checkedRows: string[] = [];
|
||||
private _sortColumns: {
|
||||
[key: string]: DataTableSortColumnData;
|
||||
} = {};
|
||||
@ -114,18 +108,17 @@ export class HaDataTable extends BaseElement {
|
||||
(value: string) => {
|
||||
this._filter = value;
|
||||
},
|
||||
200,
|
||||
100,
|
||||
false
|
||||
);
|
||||
|
||||
public clearSelection(): void {
|
||||
this._headerChecked = false;
|
||||
this._headerIndeterminate = false;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
this._checkedRows = [];
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
protected firstUpdated(properties: PropertyValues) {
|
||||
super.firstUpdated(properties);
|
||||
this._worker = sortFilterWorker();
|
||||
}
|
||||
|
||||
@ -159,6 +152,12 @@ export class HaDataTable extends BaseElement {
|
||||
this._debounceSearch(this.filter);
|
||||
}
|
||||
|
||||
if (properties.has("data")) {
|
||||
this._checkableRowsCount = this.data.filter(
|
||||
(row) => row.selectable !== false
|
||||
).length;
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
@ -173,7 +172,7 @@ export class HaDataTable extends BaseElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcScrollHeight}>
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
@ -184,168 +183,151 @@ export class HaDataTable extends BaseElement {
|
||||
`
|
||||
: ""}
|
||||
</slot>
|
||||
<div class="scroller">
|
||||
<table class="mdc-data-table__table">
|
||||
<thead>
|
||||
<tr class="mdc-data-table__header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<th
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxChange}
|
||||
.indeterminate=${this._headerIndeterminate}
|
||||
.checked=${this._headerChecked}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</th>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"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),
|
||||
};
|
||||
return html`
|
||||
<th
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
@click=${this._handleHeaderClick}
|
||||
data-column-id="${key}"
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${sorted && this._sortDirection === "desc"
|
||||
? "hass:arrow-down"
|
||||
: "hass:arrow-up"}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
</th>
|
||||
`;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="mdc-data-table__content">
|
||||
${repeat(
|
||||
this._filteredData!,
|
||||
(row: DataTableRowData) => row[this.id],
|
||||
(row: DataTableRowData) => html`
|
||||
<tr
|
||||
data-row-id="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row"
|
||||
.selectable=${row.selectable !== false}
|
||||
<div
|
||||
class="mdc-data-table__table ${classMap({
|
||||
"auto-height": this.autoHeight,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
height: this.autoHeight
|
||||
? `${this._filteredData.length * 53 + 57}px`
|
||||
: `calc(100% - ${this._header?.clientHeight}px)`,
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
${this.selectable
|
||||
? html`
|
||||
<td
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxChange}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</td>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<td
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"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], row)
|
||||
: row[key]}
|
||||
</td>
|
||||
`;
|
||||
})}
|
||||
</tr>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length ===
|
||||
this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"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),
|
||||
grows: Boolean(column.grows),
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: String(
|
||||
column.width
|
||||
),
|
||||
})
|
||||
: ""}
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${sorted && this._sortDirection === "desc"
|
||||
? "hass:arrow-down"
|
||||
: "hass:arrow-up"}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="mdc-data-table__content scroller">
|
||||
${scroll({
|
||||
items: this._filteredData,
|
||||
renderItem: (row: DataTableRowData) => html`
|
||||
<div
|
||||
.rowId="${row[this.id]}"
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row ${classMap({
|
||||
"mdc-data-table__row--selected": this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
),
|
||||
})}"
|
||||
aria-selected=${ifDefined(
|
||||
this._checkedRows.includes(String(row[this.id]))
|
||||
? true
|
||||
: undefined
|
||||
)}
|
||||
.selectable=${row.selectable !== false}
|
||||
>
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
return html`
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--numeric": Boolean(
|
||||
column.type && column.type === "numeric"
|
||||
),
|
||||
"mdc-data-table__cell--icon": Boolean(
|
||||
column.type && column.type === "icon"
|
||||
),
|
||||
grows: Boolean(column.grows),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: String(
|
||||
column.width
|
||||
),
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row[key], row)
|
||||
: row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected createAdapter(): MDCDataTableAdapter {
|
||||
return {
|
||||
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
|
||||
if (!(this.rowElements[rowIndex] as any).selectable) {
|
||||
return;
|
||||
}
|
||||
this.rowElements[rowIndex].classList.add(cssClasses);
|
||||
},
|
||||
getRowCount: () => this.rowElements.length,
|
||||
getRowElements: () => this.rowElements,
|
||||
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
|
||||
getRowIndexByChildElement: (el: Element) =>
|
||||
Array.prototype.indexOf.call(this.rowElements, el.closest("tr")),
|
||||
getSelectedRowCount: () => this._checkedRows.length,
|
||||
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
|
||||
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
|
||||
isHeaderRowCheckboxChecked: () => this._headerChecked,
|
||||
isRowsSelectable: () => this.selectable,
|
||||
notifyRowSelectionChanged: () => undefined,
|
||||
notifySelectedAll: () => undefined,
|
||||
notifyUnselectedAll: () => undefined,
|
||||
registerHeaderRowCheckbox: () => undefined,
|
||||
registerRowCheckboxes: () => undefined,
|
||||
removeClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
|
||||
this.rowElements[rowIndex].classList.remove(cssClasses);
|
||||
},
|
||||
setAttributeAtRowIndex: (
|
||||
rowIndex: number,
|
||||
attr: string,
|
||||
value: string
|
||||
) => {
|
||||
this.rowElements[rowIndex].setAttribute(attr, value);
|
||||
},
|
||||
setHeaderRowCheckboxChecked: (checked: boolean) => {
|
||||
this._headerChecked = checked;
|
||||
},
|
||||
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
|
||||
this._headerIndeterminate = indeterminate;
|
||||
},
|
||||
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
|
||||
if (!(this.rowElements[rowIndex] as any).selectable) {
|
||||
return;
|
||||
}
|
||||
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async _filterData() {
|
||||
const startTime = new Date().getTime();
|
||||
this.curRequest++;
|
||||
@ -373,14 +355,10 @@ export class HaDataTable extends BaseElement {
|
||||
this._filteredData = data;
|
||||
}
|
||||
|
||||
private _getRowIdAtIndex(rowIndex: number): string {
|
||||
return this.rowElements[rowIndex].getAttribute("data-row-id")!;
|
||||
}
|
||||
|
||||
private _handleHeaderClick(ev: Event) {
|
||||
const columnId = (ev.target as HTMLElement)
|
||||
.closest("th")!
|
||||
.getAttribute("data-column-id")!;
|
||||
const columnId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__header-cell"
|
||||
) as any).columnId;
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
@ -400,19 +378,32 @@ export class HaDataTable extends BaseElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxChange(ev: Event) {
|
||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
this._headerChecked = checkbox.checked;
|
||||
this._headerIndeterminate = checkbox.indeterminate;
|
||||
this.mdcFoundation.handleHeaderRowCheckboxChange();
|
||||
if (checkbox.checked) {
|
||||
this._checkedRows = this._filteredData
|
||||
.filter((data) => data.selectable !== false)
|
||||
.map((data) => data[this.id]);
|
||||
this._checkedRowsChanged();
|
||||
} else {
|
||||
this._checkedRows = [];
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRowCheckboxChange(ev: Event) {
|
||||
private _handleRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
const rowId = checkbox.closest("tr")!.getAttribute("data-row-id");
|
||||
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
|
||||
|
||||
this._setRowChecked(rowId!, checkbox.checked);
|
||||
this.mdcFoundation.handleRowCheckboxChange(ev);
|
||||
if (checkbox.checked) {
|
||||
if (this._checkedRows.includes(rowId)) {
|
||||
return;
|
||||
}
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
} else {
|
||||
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
|
||||
}
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
@ -420,26 +411,15 @@ export class HaDataTable extends BaseElement {
|
||||
if (target.tagName === "HA-CHECKBOX") {
|
||||
return;
|
||||
}
|
||||
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
|
||||
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
}
|
||||
|
||||
private _setRowChecked(rowId: string, checked: boolean) {
|
||||
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);
|
||||
}
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
this._filteredData = [...this._filteredData];
|
||||
fireEvent(this, "selection-changed", {
|
||||
id: rowId,
|
||||
selected: checked,
|
||||
value: this._checkedRows,
|
||||
});
|
||||
}
|
||||
|
||||
@ -447,15 +427,20 @@ export class HaDataTable extends BaseElement {
|
||||
this._debounceSearch(ev.detail.value);
|
||||
}
|
||||
|
||||
private async _calcScrollHeight() {
|
||||
private async _calcTableHeight() {
|
||||
if (this.autoHeight) {
|
||||
return;
|
||||
}
|
||||
await this.updateComplete;
|
||||
this._scroller.style.maxHeight = `calc(100% - ${this._header.clientHeight}px)`;
|
||||
this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
/* default mdc styles, colors changed, without checkbox styles */
|
||||
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
.mdc-data-table__content {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@ -477,7 +462,7 @@ export class HaDataTable extends BaseElement {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mdc-data-table__row--selected {
|
||||
@ -485,12 +470,13 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row {
|
||||
border-top-color: rgba(var(--rgb-primary-text-color), 0.12);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.mdc-data-table__row {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
@ -507,16 +493,24 @@ export class HaDataTable extends BaseElement {
|
||||
|
||||
.mdc-data-table__header-row {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mdc-data-table__row {
|
||||
height: 52px;
|
||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell,
|
||||
.mdc-data-table__header-cell {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--checkbox,
|
||||
@ -538,10 +532,10 @@ export class HaDataTable extends BaseElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
@ -568,12 +562,20 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__cell--icon {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon,
|
||||
.mdc-data-table__cell--icon {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon {
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
|
||||
text-align: center;
|
||||
}
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon:first-child ha-icon {
|
||||
margin-left: 8px;
|
||||
@ -604,6 +606,10 @@ export class HaDataTable extends BaseElement {
|
||||
.mdc-data-table__header-cell--numeric {
|
||||
text-align: right;
|
||||
}
|
||||
.mdc-data-table__header-cell--numeric.sortable:hover,
|
||||
.mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
|
||||
text-align: left;
|
||||
}
|
||||
[dir="rtl"] .mdc-data-table__header-cell--numeric,
|
||||
.mdc-data-table__header-cell--numeric[dir="rtl"] {
|
||||
/* @noflip */
|
||||
@ -634,27 +640,21 @@ export class HaDataTable extends BaseElement {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: left 0.2s ease 0s;
|
||||
transition: left 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-icon {
|
||||
top: 15px;
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted ha-icon {
|
||||
left: -20px;
|
||||
}
|
||||
.mdc-data-table__header-cell:not(.not-sorted) span,
|
||||
.mdc-data-table__header-cell.not-sorted:hover span {
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
|
||||
.mdc-data-table__header-cell.sortable.not-sorted:hover span {
|
||||
left: 24px;
|
||||
}
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted)
|
||||
span,
|
||||
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover
|
||||
span {
|
||||
left: 12px;
|
||||
}
|
||||
.mdc-data-table__header-cell:not(.not-sorted) ha-icon,
|
||||
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
|
||||
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
|
||||
left: 12px;
|
||||
}
|
||||
.table-header {
|
||||
@ -664,12 +664,25 @@ export class HaDataTable extends BaseElement {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.scroller {
|
||||
overflow: auto;
|
||||
}
|
||||
slot[name="header"] {
|
||||
display: block;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.scroller {
|
||||
display: flex;
|
||||
position: relative;
|
||||
contain: strict;
|
||||
height: calc(100% - 57px);
|
||||
}
|
||||
.mdc-data-table__table:not(.auto-height) .scroller {
|
||||
overflow: auto;
|
||||
}
|
||||
.grows {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -1,374 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import { timeOut } from "@polymer/polymer/lib/utils/async";
|
||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../cards/ha-badges-card";
|
||||
import "../cards/ha-card-chooser";
|
||||
import "./ha-demo-badge";
|
||||
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { splitByGroups } from "../common/entity/split_by_groups";
|
||||
import { getGroupEntities } from "../common/entity/get_group_entities";
|
||||
|
||||
// mapping domain to size of the card.
|
||||
const DOMAINS_WITH_CARD = {
|
||||
camera: 4,
|
||||
history_graph: 4,
|
||||
media_player: 3,
|
||||
persistent_notification: 0,
|
||||
plant: 3,
|
||||
weather: 4,
|
||||
};
|
||||
|
||||
// 4 types:
|
||||
// badges: 0 .. 10
|
||||
// before groups < 0
|
||||
// groups: X
|
||||
// rest: 100
|
||||
|
||||
const PRIORITY = {
|
||||
// before groups < 0
|
||||
configurator: -20,
|
||||
persistent_notification: -15,
|
||||
|
||||
// badges have priority >= 0
|
||||
updater: 0,
|
||||
sun: 1,
|
||||
person: 2,
|
||||
device_tracker: 3,
|
||||
alarm_control_panel: 4,
|
||||
timer: 5,
|
||||
sensor: 6,
|
||||
binary_sensor: 7,
|
||||
mailbox: 8,
|
||||
};
|
||||
|
||||
const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100);
|
||||
|
||||
const sortPriority = (domainA, domainB) => domainA.priority - domainB.priority;
|
||||
|
||||
const entitySortBy = (entityA, entityB) => {
|
||||
const nameA = (
|
||||
entityA.attributes.friendly_name || entityA.entity_id
|
||||
).toLowerCase();
|
||||
const nameB = (
|
||||
entityB.attributes.friendly_name || entityB.entity_id
|
||||
).toLowerCase();
|
||||
|
||||
if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const iterateDomainSorted = (collection, func) => {
|
||||
Object.keys(collection)
|
||||
.map((key) => collection[key])
|
||||
.sort(sortPriority)
|
||||
.forEach((domain) => {
|
||||
domain.states.sort(entitySortBy);
|
||||
func(domain);
|
||||
});
|
||||
};
|
||||
|
||||
class HaCards extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-factors"></style>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding: 4px 4px 0;
|
||||
transform: translateZ(0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badges {
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.column {
|
||||
max-width: 500px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
ha-card-chooser {
|
||||
display: block;
|
||||
margin: 4px 4px 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
:host {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
ha-card-chooser {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.column {
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="main">
|
||||
<template is="dom-if" if="[[cards.badges.length]]">
|
||||
<div class="badges">
|
||||
<template is="dom-if" if="[[cards.demo]]">
|
||||
<ha-demo-badge></ha-demo-badge>
|
||||
</template>
|
||||
|
||||
<ha-badges-card
|
||||
states="[[cards.badges]]"
|
||||
hass="[[hass]]"
|
||||
></ha-badges-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="horizontal layout center-justified">
|
||||
<template is="dom-repeat" items="[[cards.columns]]" as="column">
|
||||
<div class="column flex-1">
|
||||
<template is="dom-repeat" items="[[column]]" as="card">
|
||||
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
columns: {
|
||||
type: Number,
|
||||
value: 2,
|
||||
},
|
||||
|
||||
states: Object,
|
||||
|
||||
viewVisible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
orderedGroupEntities: Array,
|
||||
|
||||
cards: Object,
|
||||
};
|
||||
}
|
||||
|
||||
static get observers() {
|
||||
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
|
||||
}
|
||||
|
||||
updateCards(columns, states, viewVisible, orderedGroupEntities) {
|
||||
if (!viewVisible) {
|
||||
if (this.$.main.parentNode) {
|
||||
this.$.main._parentNode = this.$.main.parentNode;
|
||||
this.$.main.parentNode.removeChild(this.$.main);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.$.main.parentNode && this.$.main._parentNode) {
|
||||
this.$.main._parentNode.appendChild(this.$.main);
|
||||
}
|
||||
this._debouncer = Debouncer.debounce(
|
||||
this._debouncer,
|
||||
timeOut.after(10),
|
||||
() => {
|
||||
// Things might have changed since it got scheduled.
|
||||
if (this.viewVisible) {
|
||||
this.cards = this.computeCards(columns, states, orderedGroupEntities);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
emptyCards() {
|
||||
return {
|
||||
demo: false,
|
||||
badges: [],
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
|
||||
computeCards(columns, states, orderedGroupEntities) {
|
||||
const hass = this.hass;
|
||||
|
||||
const cards = this.emptyCards();
|
||||
|
||||
const entityCount = [];
|
||||
for (let i = 0; i < columns; i++) {
|
||||
cards.columns.push([]);
|
||||
entityCount.push(0);
|
||||
}
|
||||
|
||||
// Find column with < 5 entities, else column with lowest count
|
||||
function getIndex(size) {
|
||||
let minIndex = 0;
|
||||
for (let i = 0; i < entityCount.length; i++) {
|
||||
if (entityCount[i] < 5) {
|
||||
minIndex = i;
|
||||
break;
|
||||
}
|
||||
if (entityCount[i] < entityCount[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
entityCount[minIndex] += size;
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
function addEntitiesCard(name, entities, groupEntity) {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
const owncard = [];
|
||||
const other = [];
|
||||
|
||||
let size = 0;
|
||||
|
||||
entities.forEach((entity) => {
|
||||
const domain = computeStateDomain(entity);
|
||||
|
||||
if (
|
||||
domain in DOMAINS_WITH_CARD &&
|
||||
!entity.attributes.custom_ui_state_card
|
||||
) {
|
||||
owncard.push(entity);
|
||||
size += DOMAINS_WITH_CARD[domain];
|
||||
} else {
|
||||
other.push(entity);
|
||||
size++;
|
||||
}
|
||||
});
|
||||
|
||||
// Add 1 to the size if we're rendering entities card
|
||||
size += other.length > 0;
|
||||
|
||||
const curIndex = getIndex(size);
|
||||
|
||||
if (other.length > 0) {
|
||||
cards.columns[curIndex].push({
|
||||
hass: hass,
|
||||
cardType: "entities",
|
||||
states: other,
|
||||
groupEntity: groupEntity || false,
|
||||
});
|
||||
}
|
||||
|
||||
owncard.forEach((entity) => {
|
||||
cards.columns[curIndex].push({
|
||||
hass: hass,
|
||||
cardType: computeStateDomain(entity),
|
||||
stateObj: entity,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const splitted = splitByGroups(states);
|
||||
if (orderedGroupEntities) {
|
||||
splitted.groups.sort(
|
||||
(gr1, gr2) =>
|
||||
orderedGroupEntities[gr1.entity_id] -
|
||||
orderedGroupEntities[gr2.entity_id]
|
||||
);
|
||||
} else {
|
||||
splitted.groups.sort(
|
||||
(gr1, gr2) => gr1.attributes.order - gr2.attributes.order
|
||||
);
|
||||
}
|
||||
|
||||
const badgesColl = {};
|
||||
const beforeGroupColl = {};
|
||||
const afterGroupedColl = {};
|
||||
|
||||
Object.keys(splitted.ungrouped).forEach((key) => {
|
||||
const state = splitted.ungrouped[key];
|
||||
const domain = computeStateDomain(state);
|
||||
|
||||
if (domain === "a") {
|
||||
cards.demo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const priority = getPriority(domain);
|
||||
let coll;
|
||||
|
||||
if (priority < 0) {
|
||||
coll = beforeGroupColl;
|
||||
} else if (priority < 10) {
|
||||
coll = badgesColl;
|
||||
} else {
|
||||
coll = afterGroupedColl;
|
||||
}
|
||||
|
||||
if (!(domain in coll)) {
|
||||
coll[domain] = {
|
||||
domain: domain,
|
||||
priority: priority,
|
||||
states: [],
|
||||
};
|
||||
}
|
||||
|
||||
coll[domain].states.push(state);
|
||||
});
|
||||
|
||||
if (orderedGroupEntities) {
|
||||
Object.keys(badgesColl)
|
||||
.map((key) => badgesColl[key])
|
||||
.forEach((domain) => {
|
||||
cards.badges.push.apply(cards.badges, domain.states);
|
||||
});
|
||||
|
||||
cards.badges.sort(
|
||||
(e1, e2) =>
|
||||
orderedGroupEntities[e1.entity_id] -
|
||||
orderedGroupEntities[e2.entity_id]
|
||||
);
|
||||
} else {
|
||||
iterateDomainSorted(badgesColl, (domain) => {
|
||||
cards.badges.push.apply(cards.badges, domain.states);
|
||||
});
|
||||
}
|
||||
|
||||
iterateDomainSorted(beforeGroupColl, (domain) => {
|
||||
addEntitiesCard(domain.domain, domain.states);
|
||||
});
|
||||
|
||||
splitted.groups.forEach((groupState) => {
|
||||
const entities = getGroupEntities(states, groupState);
|
||||
addEntitiesCard(
|
||||
groupState.entity_id,
|
||||
Object.keys(entities).map((key) => entities[key]),
|
||||
groupState
|
||||
);
|
||||
});
|
||||
|
||||
iterateDomainSorted(afterGroupedColl, (domain) => {
|
||||
addEntitiesCard(domain.domain, domain.states);
|
||||
});
|
||||
|
||||
// Remove empty columns
|
||||
cards.columns = cards.columns.filter((val) => val.length > 0);
|
||||
|
||||
return cards;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-cards", HaCards);
|
@ -8,7 +8,7 @@ import {
|
||||
customElement,
|
||||
unsafeCSS,
|
||||
} from "lit-element";
|
||||
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
// @ts-ignore
|
||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@ -33,22 +33,27 @@ export class HaChips extends LitElement {
|
||||
${this.items.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<button
|
||||
class="mdc-chip"
|
||||
.index=${idx}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
<span class="mdc-chip__text">${item}</span>
|
||||
</button>
|
||||
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
|
||||
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
|
||||
<span role="gridcell">
|
||||
<span
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="mdc-chip__primary-action"
|
||||
>
|
||||
<span class="mdc-chip__text">${item}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(ev) {
|
||||
private _handleClick(ev): void {
|
||||
fireEvent(this, "chip-clicked", {
|
||||
index: ev.target.closest("button").index,
|
||||
index: ev.currentTarget.index,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { html, customElement } from "lit-element";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive.js";
|
||||
import { ripple } from "@material/mwc-ripple/ripple-directive";
|
||||
|
||||
import "@material/mwc-fab";
|
||||
import { Constructor } from "../types";
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
query,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import {
|
||||
HaFormElement,
|
||||
@ -19,13 +21,14 @@ import "@polymer/paper-input/paper-input";
|
||||
// tslint:disable-next-line
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { PaperSliderElement } from "@polymer/paper-slider/paper-slider";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@property() public schema!: HaFormIntegerSchema;
|
||||
@property() public data!: HaFormIntegerData;
|
||||
@property() public label!: string;
|
||||
@property() public suffix!: string;
|
||||
@property() public data?: HaFormIntegerData;
|
||||
@property() public label?: string;
|
||||
@property() public suffix?: string;
|
||||
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
@ -39,20 +42,31 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
? html`
|
||||
<div>
|
||||
${this.label}
|
||||
<ha-paper-slider
|
||||
pin=""
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-paper-slider>
|
||||
<div class="flex">
|
||||
${this.schema.optional && this.schema.default === undefined
|
||||
? html`
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this.data !== undefined}
|
||||
></ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
<ha-paper-slider
|
||||
pin=""
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
.disabled=${this.data === undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-paper-slider>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<paper-input
|
||||
type="number"
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
.value=${this._value}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
@ -61,7 +75,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.data || 0;
|
||||
return this.data || this.schema.default || 0;
|
||||
}
|
||||
|
||||
private _handleCheckboxChange(ev: Event) {
|
||||
const checked = (ev.target as HaCheckbox).checked;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: checked ? this._value : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
@ -75,6 +96,14 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -95,7 +95,7 @@ export interface HaFormTimeData {
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema;
|
||||
data: HaFormDataContainer | HaFormData;
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ class HaMarkdown extends UpdatingElement {
|
||||
node.host !== document.location.host
|
||||
) {
|
||||
node.target = "_blank";
|
||||
node.rel = "noreferrer";
|
||||
|
||||
// protect referrer on external links and deny window.opener access for security reasons
|
||||
// (see https://mathiasbynens.github.io/rel-noopener/)
|
||||
|
@ -18,6 +18,11 @@ class HaPaperSlider extends PaperSliderClass {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||
background-color: var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||
border: 2px solid var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner::before {
|
||||
top: unset;
|
||||
margin-left: unset;
|
||||
|
@ -32,6 +32,7 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { compare } from "../common/string/compare";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
|
||||
@ -51,6 +52,9 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
const aLovelace = a.component_name === "lovelace";
|
||||
const bLovelace = b.component_name === "lovelace";
|
||||
|
||||
if (aLovelace && bLovelace) {
|
||||
return compare(a.title!, b.title!);
|
||||
}
|
||||
if (aLovelace && !bLovelace) {
|
||||
return -1;
|
||||
}
|
||||
@ -71,14 +75,9 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
|
||||
return 1;
|
||||
}
|
||||
// both not built in, sort by title
|
||||
if (a.title! < b.title!) {
|
||||
return -1;
|
||||
}
|
||||
if (a.title! > b.title!) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return compare(a.title!, b.title!);
|
||||
};
|
||||
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const panels = hass.panels;
|
||||
@ -90,7 +89,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
if (!panel.title) {
|
||||
if (!panel.title || panel.url_path === DEFAULT_PAGE) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
@ -114,8 +113,7 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public alwaysExpand = false;
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
@property() public _defaultPage?: string =
|
||||
localStorage.defaultPage || DEFAULT_PANEL;
|
||||
|
||||
@property() private _externalConfig?: ExternalConfig;
|
||||
@property() private _notifications?: PersistentNotification[];
|
||||
// property used only in css
|
||||
@ -144,6 +142,9 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPanel =
|
||||
this.hass.panels[DEFAULT_PAGE] || this.hass.panels[DEFAULT_PANEL];
|
||||
|
||||
return html`
|
||||
<div class="menu">
|
||||
${!this.narrow
|
||||
@ -168,9 +169,9 @@ class HaSidebar extends LitElement {
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanel(
|
||||
this._defaultPage,
|
||||
"hass:apps",
|
||||
hass.localize("panel.states")
|
||||
defaultPanel.url_path,
|
||||
defaultPanel.icon || "hass:view-dashboard",
|
||||
defaultPanel.title || hass.localize("panel.states")
|
||||
)}
|
||||
${beforeSpacer.map((panel) =>
|
||||
this._renderPanel(
|
||||
|
@ -41,15 +41,6 @@ export const fetchThumbnailUrl = async (
|
||||
return hass.hassUrl(path.path);
|
||||
};
|
||||
|
||||
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.warn("This method has been deprecated.");
|
||||
return hass.callWS<CameraThumbnail>({
|
||||
type: "camera_thumbnail",
|
||||
entity_id: entityId,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchStreamUrl = async (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
|
@ -66,7 +66,15 @@ export const fetchDeviceTriggerCapabilities = (
|
||||
trigger,
|
||||
});
|
||||
|
||||
const whitelist = ["above", "below", "code", "for"];
|
||||
const whitelist = [
|
||||
"above",
|
||||
"below",
|
||||
"brightness",
|
||||
"code",
|
||||
"for",
|
||||
"position",
|
||||
"set_brightness",
|
||||
];
|
||||
|
||||
export const deviceAutomationsEqual = (
|
||||
a: DeviceAutomation,
|
||||
|
@ -6,12 +6,20 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HASSDomEvent } from "../common/dom/fire_event";
|
||||
|
||||
export interface LovelacePanelConfig {
|
||||
mode: "yaml" | "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface LegacyLovelaceConfig extends LovelaceConfig {
|
||||
resources?: LovelaceResource[];
|
||||
}
|
||||
|
||||
export interface LovelaceResource {
|
||||
id: string;
|
||||
type: "css" | "js" | "module" | "html";
|
||||
@ -31,7 +39,9 @@ interface LovelaceGenericDashboard {
|
||||
id: string;
|
||||
url_path: string;
|
||||
require_admin: boolean;
|
||||
sidebar?: { icon: string; title: string };
|
||||
show_in_sidebar: boolean;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
|
||||
@ -45,7 +55,9 @@ export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
|
||||
|
||||
export interface LovelaceDashboardMutableParams {
|
||||
require_admin: boolean;
|
||||
sidebar: { icon: string; title: string } | null;
|
||||
show_in_sidebar: boolean;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardCreateParams
|
||||
@ -270,6 +282,34 @@ export const getLovelaceCollection = (
|
||||
)
|
||||
);
|
||||
|
||||
// Legacy functions to support cast for Home Assistion < 0.107
|
||||
const fetchLegacyConfig = (
|
||||
conn: Connection,
|
||||
force: boolean
|
||||
): Promise<LovelaceConfig> =>
|
||||
conn.sendMessagePromise({
|
||||
type: "lovelace/config",
|
||||
force,
|
||||
});
|
||||
|
||||
const subscribeLegacyLovelaceUpdates = (
|
||||
conn: Connection,
|
||||
onChange: () => void
|
||||
) => conn.subscribeEvents(onChange, "lovelace_updated");
|
||||
|
||||
export const getLegacyLovelaceCollection = (conn: Connection) =>
|
||||
getCollection(
|
||||
conn,
|
||||
"_lovelace",
|
||||
(conn2) => fetchLegacyConfig(conn2, false),
|
||||
(_conn, store) =>
|
||||
subscribeLegacyLovelaceUpdates(conn, () =>
|
||||
fetchLegacyConfig(conn, false).then((config) =>
|
||||
store.setState(config, true)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export interface WindowWithLovelaceProm extends Window {
|
||||
llConfProm?: Promise<LovelaceConfig>;
|
||||
llResProm?: Promise<LovelaceResource[]>;
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export const SUPPORT_PAUSE = 1;
|
||||
export const SUPPORT_SEEK = 2;
|
||||
@ -17,30 +15,47 @@ export const SUPPORT_STOP = 4096;
|
||||
export const SUPPORTS_PLAY = 16384;
|
||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
export const OFF_STATES = ["off", "idle"];
|
||||
export const CONTRAST_RATIO = 3.5;
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
content_type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const fetchMediaPlayerThumbnailWithCache = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) =>
|
||||
timeCachePromiseFunc(
|
||||
"_media_playerTmb",
|
||||
9000,
|
||||
fetchMediaPlayerThumbnail,
|
||||
hass,
|
||||
entityId
|
||||
);
|
||||
|
||||
export const fetchMediaPlayerThumbnail = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => {
|
||||
return hass.callWS<MediaPlayerThumbnail>({
|
||||
type: "media_player_thumbnail",
|
||||
entity_id: entityId,
|
||||
});
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
progress +=
|
||||
(Date.now() -
|
||||
new Date(stateObj.attributes.media_position_updated_at).getTime()) /
|
||||
1000.0;
|
||||
return progress;
|
||||
};
|
||||
|
||||
export const computeMediaDescription = (stateObj: HassEntity): string => {
|
||||
let secondaryTitle: string;
|
||||
|
||||
switch (stateObj.attributes.media_content_type) {
|
||||
case "music":
|
||||
secondaryTitle = stateObj.attributes.media_artist;
|
||||
break;
|
||||
case "playlist":
|
||||
secondaryTitle = stateObj.attributes.media_playlist;
|
||||
break;
|
||||
case "tvshow":
|
||||
secondaryTitle = stateObj.attributes.media_series_title;
|
||||
if (stateObj.attributes.media_season) {
|
||||
secondaryTitle += " S" + stateObj.attributes.media_season;
|
||||
|
||||
if (stateObj.attributes.media_episode) {
|
||||
secondaryTitle += "E" + stateObj.attributes.media_episode;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
secondaryTitle = stateObj.attributes.app_name
|
||||
? stateObj.attributes.app_name
|
||||
: "";
|
||||
}
|
||||
|
||||
return secondaryTitle;
|
||||
};
|
||||
|
@ -2,9 +2,9 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface LoggedError {
|
||||
name: string;
|
||||
message: string;
|
||||
message: [string];
|
||||
level: string;
|
||||
source: string;
|
||||
source: [string, number];
|
||||
// unix timestamp in seconds
|
||||
timestamp: number;
|
||||
exception: string;
|
||||
|
@ -38,7 +38,7 @@ class StepFlowExternal extends LitElement {
|
||||
<div class="content">
|
||||
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
|
||||
<div class="open-button">
|
||||
<a href=${this.step.url} target="_blank">
|
||||
<a href=${this.step.url} target="_blank" rel="noreferrer">
|
||||
<mwc-button raised>
|
||||
${localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
|
@ -97,6 +97,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
)}<a
|
||||
href="https://www.home-assistant.io/integrations/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.home_assistant_website"
|
||||
)}</a
|
||||
|
@ -113,6 +113,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
class="button"
|
||||
href="${this._agentInfo.onboarding.url}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
><mwc-button unelevated>Yes!</mwc-button></a
|
||||
>
|
||||
<mwc-button outlined>No</mwc-button>
|
||||
@ -185,6 +186,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
href=${this._agentInfo.attribution.url}
|
||||
class="attribution"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this._agentInfo.attribution.name}</a
|
||||
>
|
||||
`
|
||||
|
@ -15,13 +15,6 @@ export const demoPanels: Panels = {
|
||||
config: null,
|
||||
url_path: "dev-state",
|
||||
},
|
||||
states: {
|
||||
component_name: "states",
|
||||
icon: null,
|
||||
title: null,
|
||||
config: null,
|
||||
url_path: "states",
|
||||
},
|
||||
"dev-event": {
|
||||
component_name: "dev-event",
|
||||
icon: null,
|
||||
@ -43,13 +36,6 @@ export const demoPanels: Panels = {
|
||||
config: null,
|
||||
url_path: "profile",
|
||||
},
|
||||
kiosk: {
|
||||
component_name: "kiosk",
|
||||
icon: null,
|
||||
title: null,
|
||||
config: null,
|
||||
url_path: "kiosk",
|
||||
},
|
||||
"dev-info": {
|
||||
component_name: "dev-info",
|
||||
icon: null,
|
||||
|
@ -135,6 +135,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
return css`
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
|
@ -23,7 +23,7 @@ import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/ap
|
||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
|
||||
const NON_SWIPABLE_PANELS = ["kiosk", "map"];
|
||||
const NON_SWIPABLE_PANELS = ["map"];
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
|
@ -8,8 +8,9 @@ import {
|
||||
RouteOptions,
|
||||
} from "./hass-router-page";
|
||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
|
||||
const CACHE_URL_PATHS = ["lovelace", "states", "developer-tools"];
|
||||
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
|
||||
const COMPONENTS = {
|
||||
calendar: () =>
|
||||
import(
|
||||
@ -31,10 +32,6 @@ const COMPONENTS = {
|
||||
import(
|
||||
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
|
||||
),
|
||||
states: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
|
||||
),
|
||||
history: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
|
||||
@ -43,10 +40,6 @@ const COMPONENTS = {
|
||||
import(
|
||||
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
|
||||
),
|
||||
kiosk: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
|
||||
),
|
||||
logbook: () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"
|
||||
@ -88,7 +81,7 @@ const getRoutes = (panels: Panels): RouterOptions => {
|
||||
|
||||
@customElement("partial-panel-resolver")
|
||||
class PartialPanelResolver extends HassRouterPage {
|
||||
@property() public hass?: HomeAssistant;
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public narrow?: boolean;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@ -100,11 +93,8 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
|
||||
if (
|
||||
this.hass!.panels &&
|
||||
(!oldHass || oldHass.panels !== this.hass!.panels)
|
||||
) {
|
||||
this._updateRoutes();
|
||||
if (this.hass.panels && (!oldHass || oldHass.panels !== this.hass.panels)) {
|
||||
this._updateRoutes(oldHass?.panels);
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +107,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
const hass = this.hass!;
|
||||
const hass = this.hass;
|
||||
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer panels
|
||||
@ -135,11 +125,20 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateRoutes() {
|
||||
this.routerOptions = getRoutes(this.hass!.panels);
|
||||
await this.rebuild();
|
||||
await this.pageRendered;
|
||||
removeInitSkeleton();
|
||||
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
|
||||
this.routerOptions = getRoutes(this.hass.panels);
|
||||
|
||||
if (
|
||||
!oldPanels ||
|
||||
!deepEqual(
|
||||
oldPanels[this._currentPage],
|
||||
this.hass.panels[this._currentPage]
|
||||
)
|
||||
) {
|
||||
await this.rebuild();
|
||||
await this.pageRendered;
|
||||
removeInitSkeleton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { LitElement, customElement, property, html } from "lit-element";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
@customElement("ha-automation-action-device_id")
|
||||
export class HaDeviceAction extends LitElement {
|
||||
@ -27,14 +28,20 @@ export class HaDeviceAction extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) =>
|
||||
capabilities && capabilities.extra_fields
|
||||
? capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: action[item.name] };
|
||||
})
|
||||
: undefined
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const deviceId = this._deviceId || this.action.device_id;
|
||||
const extraFieldsData =
|
||||
this._capabilities && this._capabilities.extra_fields
|
||||
? this._capabilities.extra_fields.map((item) => {
|
||||
return { [item.name]: this.action[item.name] };
|
||||
})
|
||||
: undefined;
|
||||
const extraFieldsData = this._extraFieldsData(
|
||||
this._capabilities,
|
||||
this.action
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
@ -82,10 +89,8 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
|
||||
private async _getCapabilities() {
|
||||
const action = this.action;
|
||||
|
||||
this._capabilities = action.domain
|
||||
? await fetchDeviceActionCapabilities(this.hass, action)
|
||||
this._capabilities = this.action.domain
|
||||
? await fetchDeviceActionCapabilities(this.hass, this.action)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -156,6 +156,7 @@ export class HaAutomationEditor extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/automation/trigger/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.learn_more"
|
||||
@ -184,6 +185,7 @@ export class HaAutomationEditor extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scripts/conditions/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.learn_more"
|
||||
@ -212,6 +214,7 @@ export class HaAutomationEditor extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/automation/action/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.learn_more"
|
||||
|
@ -63,6 +63,7 @@ class HaAutomationPicker extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/automation/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.learn_more"
|
||||
|
@ -123,6 +123,7 @@ class DialogThingtalk extends LitElement {
|
||||
<a
|
||||
href="https://almond.stanford.edu/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="attribution"
|
||||
>Powered by Almond</a
|
||||
>
|
||||
|
@ -95,11 +95,15 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<a href="https://account.nabucasa.com" target="_blank"
|
||||
><mwc-button
|
||||
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button
|
||||
></a
|
||||
<a
|
||||
href="https://account.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-button
|
||||
>[[localize('ui.panel.config.cloud.account.manage_account')]]</mwc-button
|
||||
>
|
||||
</a>
|
||||
<mwc-button style="float: right" on-click="handleLogout"
|
||||
>[[localize('ui.panel.config.cloud.account.sign_out')]]</mwc-button
|
||||
>
|
||||
@ -117,8 +121,12 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
</p>
|
||||
<p>
|
||||
[[localize('ui.panel.config.cloud.account.integrations_introduction2')]]
|
||||
<a href="https://www.nabucasa.com" target="_blank"
|
||||
>[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]]</a
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
[[localize('ui.panel.config.cloud.account.integrations_link_all_features')]] </a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -49,6 +49,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
<a
|
||||
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
|
||||
@ -59,6 +60,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/amazon_alexa/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.config_documentation"
|
||||
|
@ -55,6 +55,7 @@ export class CloudGooglePref extends LitElement {
|
||||
<a
|
||||
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.google.enable_ha_skill"
|
||||
@ -65,6 +66,7 @@ export class CloudGooglePref extends LitElement {
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/google_assistant/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.google.config_documentation"
|
||||
|
@ -77,12 +77,21 @@ export class CloudRemotePref extends LitElement {
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.remote.instance_will_be_available"
|
||||
)}
|
||||
<a href="https://${remote_domain}" target="_blank" class="break-word">
|
||||
<a
|
||||
href="https://${remote_domain}"
|
||||
target="_blank"
|
||||
class="break-word"
|
||||
rel="noreferrer"
|
||||
>
|
||||
https://${remote_domain}</a
|
||||
>.
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="https://www.nabucasa.com/config/remote/" target="_blank">
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/remote/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<mwc-button
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.remote.link_learn_how_it_works"
|
||||
|
@ -46,7 +46,11 @@ export class CloudWebhooks extends LitElement {
|
||||
${this._renderBody()}
|
||||
|
||||
<div class="footer">
|
||||
<a href="https://www.nabucasa.com/config/webhooks" target="_blank">
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/webhooks"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.webhooks.link_learn_more"
|
||||
)}
|
||||
|
@ -78,7 +78,7 @@ export class DialogManageCloudhook extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="paper-dialog-buttons">
|
||||
<a href="${docsUrl}" target="_blank">
|
||||
<a href="${docsUrl}" target="_blank" rel="noreferrer">
|
||||
<mwc-button
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_cloudhook.view_documentation"
|
||||
|
@ -84,18 +84,26 @@ class CloudLogin extends LocalizeMixin(
|
||||
</p>
|
||||
<p>
|
||||
[[localize('ui.panel.config.cloud.login.introduction2')]]
|
||||
<a href="https://www.nabucasa.com" target="_blank"
|
||||
>Nabu Casa, Inc</a
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Nabu Casa, Inc
|
||||
</a>
|
||||
[[localize('ui.panel.config.cloud.login.introduction2a')]]
|
||||
</p>
|
||||
<p>
|
||||
[[localize('ui.panel.config.cloud.login.introduction3')]]
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://www.nabucasa.com" target="_blank"
|
||||
>[[localize('ui.panel.config.cloud.login.learn_more_link')]]</a
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
[[localize('ui.panel.config.cloud.login.learn_more_link')]]
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -74,8 +74,8 @@ class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<p>
|
||||
[[localize('ui.panel.config.cloud.register.information4')]]
|
||||
</p><ul>
|
||||
<li><a href="https://home-assistant.io/tos/" target="_blank">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li>
|
||||
<li><a href="https://home-assistant.io/privacy/" target="_blank">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
|
||||
<li><a href="https://home-assistant.io/tos/" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_terms_conditions')]]</a></li>
|
||||
<li><a href="https://home-assistant.io/privacy/" target="_blank" rel="noreferrer">[[localize('ui.panel.config.cloud.register.link_privacy_policy')]]</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -31,6 +31,7 @@ class HaFormCustomize extends LocalizeMixin(PolymerElement) {
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/configuration/customizing-devices/#customization-using-the-ui"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.config.customize.warning.include_link')]]</a
|
||||
>.<br />
|
||||
[[localize('ui.panel.config.customize.warning.not_applied')]]
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-config-devices-dashboard")
|
||||
export class HaConfigDeviceDashboard extends LitElement {
|
||||
@ -127,6 +128,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: DataTableRowData) => {
|
||||
const battery = device.battery_entity
|
||||
? this.hass.states[device.battery_entity]
|
||||
@ -155,6 +157,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
},
|
||||
manufacturer: {
|
||||
@ -163,6 +166,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
model: {
|
||||
title: this.hass.localize(
|
||||
@ -170,6 +174,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
area: {
|
||||
title: this.hass.localize(
|
||||
@ -177,6 +182,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
integration: {
|
||||
title: this.hass.localize(
|
||||
@ -184,6 +190,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
battery_entity: {
|
||||
title: this.hass.localize(
|
||||
@ -191,6 +198,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "60px",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
@ -247,8 +255,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
return batteryEntity ? batteryEntity.entity_id : undefined;
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: CustomEvent) {
|
||||
const deviceId = (ev.detail as RowClickedEvent).id;
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const deviceId = ev.detail.id;
|
||||
navigate(this, `/config/devices/device/${deviceId}`);
|
||||
}
|
||||
}
|
||||
|
@ -134,6 +134,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: DataTableRowData) => {
|
||||
const battery = device.battery_entity
|
||||
? this.hass.states[device.battery_entity]
|
||||
@ -163,6 +164,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
manufacturer: {
|
||||
title: this.hass.localize(
|
||||
@ -170,6 +172,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
model: {
|
||||
title: this.hass.localize(
|
||||
@ -177,6 +180,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
area: {
|
||||
title: this.hass.localize(
|
||||
@ -184,6 +188,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
integration: {
|
||||
title: this.hass.localize(
|
||||
@ -191,6 +196,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
battery_entity: {
|
||||
title: this.hass.localize(
|
||||
@ -198,6 +204,7 @@ export class HaDevicesDataTable extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
width: "60px",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
|
@ -13,7 +13,10 @@ import { isComponentLoaded } from "../../../../../common/config/is_component_loa
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { HaPaperDialog } from "../../../../../components/dialog/ha-paper-dialog";
|
||||
import { ExtEntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import {
|
||||
ExtEntityRegistryEntry,
|
||||
removeEntityRegistryEntry,
|
||||
} from "../../../../../data/entity_registry";
|
||||
import {
|
||||
deleteInputBoolean,
|
||||
fetchInputBoolean,
|
||||
@ -109,23 +112,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
if (this._item === undefined) {
|
||||
return html``;
|
||||
}
|
||||
if (!this._componentLoaded) {
|
||||
return html`
|
||||
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
|
||||
The ${this.entry.platform} component is not loaded, please add it your
|
||||
configuration. Either by adding 'default_config:' or
|
||||
'${this.entry.platform}:'.
|
||||
</paper-dialog-scrollable>
|
||||
`;
|
||||
}
|
||||
if (this._item === null) {
|
||||
return html`
|
||||
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
|
||||
This entity can not be edited from the UI. Only entities setup from
|
||||
the UI are editable.
|
||||
</paper-dialog-scrollable>
|
||||
`;
|
||||
}
|
||||
const stateObj = this.hass.states[this.entry.entity_id];
|
||||
return html`
|
||||
<paper-dialog-scrollable .dialogElement=${this.dialogElement}>
|
||||
${this._error
|
||||
@ -134,13 +121,23 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<div @value-changed=${this._valueChanged}>
|
||||
${dynamicElement(`ha-${this.entry.platform}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
entry: this.entry,
|
||||
})}
|
||||
</div>
|
||||
${!this._componentLoaded
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
"platform",
|
||||
this.entry.platform
|
||||
)
|
||||
: this._item === null
|
||||
? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable")
|
||||
: html`
|
||||
<div @value-changed=${this._valueChanged}>
|
||||
${dynamicElement(`ha-${this.entry.platform}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
entry: this.entry,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
<ha-registry-basic-editor
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
@ -151,13 +148,14 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._confirmDeleteItem}
|
||||
.disabled=${this._submitting}
|
||||
.disabled=${this._submitting ||
|
||||
(!this._item && !stateObj?.attributes.restored)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._updateItem}
|
||||
.disabled=${this._submitting || !this._item.name}
|
||||
.disabled=${this._submitting || (this._item && !this._item.name)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
</mwc-button>
|
||||
@ -178,16 +176,15 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
}
|
||||
|
||||
private async _updateItem(): Promise<void> {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
try {
|
||||
await HELPERS[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this._item.id,
|
||||
this._item
|
||||
);
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this._item.id,
|
||||
this._item
|
||||
);
|
||||
}
|
||||
await this._registryEditor?.updateEntry();
|
||||
fireEvent(this, "close-dialog");
|
||||
} catch (err) {
|
||||
@ -198,9 +195,6 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
}
|
||||
|
||||
private async _confirmDeleteItem(): Promise<void> {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@ -214,7 +208,15 @@ export class EntityRegistrySettingsHelper extends LitElement {
|
||||
this._submitting = true;
|
||||
|
||||
try {
|
||||
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS[this.entry.platform].delete(this.hass!, this._item.id);
|
||||
} else {
|
||||
const stateObj = this.hass.states[this.entry.entity_id];
|
||||
if (!stateObj?.attributes.restored) {
|
||||
return;
|
||||
}
|
||||
await removeEntityRegistryEntry(this.hass!, this.entry.entity_id);
|
||||
}
|
||||
fireEvent(this, "close-dialog");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
|
@ -3,7 +3,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { UnsubscribeFunc, HassEntities } from "home-assistant-js-websocket";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@ -49,6 +49,7 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
// tslint:disable-next-line: no-duplicate-imports
|
||||
import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface StateEntity extends EntityRegistryEntry {
|
||||
readonly?: boolean;
|
||||
@ -58,6 +59,7 @@ export interface StateEntity extends EntityRegistryEntry {
|
||||
export interface EntityRow extends StateEntity {
|
||||
icon: string;
|
||||
unavailable: boolean;
|
||||
restored: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@ -68,6 +70,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
@property() public narrow!: boolean;
|
||||
@property() public route!: Route;
|
||||
@property() private _entities?: EntityRegistryEntry[];
|
||||
@property() private _stateEntities: StateEntity[] = [];
|
||||
@property() private _showDisabled = false;
|
||||
@property() private _showUnavailable = true;
|
||||
@property() private _showReadOnly = true;
|
||||
@ -94,6 +97,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
};
|
||||
|
||||
@ -104,6 +108,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "55px",
|
||||
template: (_status, entity: any) =>
|
||||
entity.unavailable || entity.disabled_by || entity.readonly
|
||||
? html`
|
||||
@ -115,14 +120,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
style=${styleMap({
|
||||
color: entity.unavailable ? "var(--google-red-500)" : "",
|
||||
})}
|
||||
.icon=${entity.unavailable
|
||||
.icon=${entity.restored
|
||||
? "hass:restore-alert"
|
||||
: entity.unavailable
|
||||
? "hass:alert-circle"
|
||||
: entity.disabled_by
|
||||
? "hass:cancel"
|
||||
: "hass:pencil-off"}
|
||||
></ha-icon>
|
||||
<paper-tooltip position="left">
|
||||
${entity.unavailable
|
||||
${entity.restored
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.restored"
|
||||
)
|
||||
: entity.unavailable
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.unavailable"
|
||||
)
|
||||
@ -158,6 +169,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
};
|
||||
columns.platform = {
|
||||
title: this.hass.localize(
|
||||
@ -165,6 +177,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (platform) =>
|
||||
this.hass.localize(`component.${platform}.config.title`) || platform,
|
||||
};
|
||||
@ -177,40 +190,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
private _filteredEntities = memoize(
|
||||
(
|
||||
entities: EntityRegistryEntry[],
|
||||
states: HassEntities,
|
||||
stateEntities: StateEntity[],
|
||||
showDisabled: boolean,
|
||||
showUnavailable: boolean,
|
||||
showReadOnly: boolean
|
||||
): EntityRow[] => {
|
||||
const stateEntities: StateEntity[] = [];
|
||||
if (showReadOnly) {
|
||||
const regEntityIds = new Set(
|
||||
entities.map((entity) => entity.entity_id)
|
||||
);
|
||||
for (const entityId of Object.keys(states)) {
|
||||
if (regEntityIds.has(entityId)) {
|
||||
continue;
|
||||
}
|
||||
stateEntities.push({
|
||||
name: computeStateName(states[entityId]),
|
||||
entity_id: entityId,
|
||||
platform: computeDomain(entityId),
|
||||
disabled_by: null,
|
||||
readonly: true,
|
||||
selectable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!showDisabled) {
|
||||
entities = entities.filter((entity) => !Boolean(entity.disabled_by));
|
||||
}
|
||||
|
||||
const result: EntityRow[] = [];
|
||||
|
||||
for (const entry of entities.concat(stateEntities)) {
|
||||
const state = states[entry.entity_id];
|
||||
const unavailable = state?.state === "unavailable";
|
||||
for (const entry of showReadOnly
|
||||
? entities.concat(stateEntities)
|
||||
: entities) {
|
||||
const entity = this.hass.states[entry.entity_id];
|
||||
const unavailable = entity?.state === "unavailable";
|
||||
const restored = entity?.attributes.restored;
|
||||
|
||||
if (!showUnavailable && unavailable) {
|
||||
continue;
|
||||
@ -218,14 +214,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
result.push({
|
||||
...entry,
|
||||
icon: state
|
||||
? stateIcon(state)
|
||||
icon: entity
|
||||
? stateIcon(entity)
|
||||
: domainIcon(computeDomain(entry.entity_id)),
|
||||
name:
|
||||
computeEntityRegistryName(this.hass!, entry) ||
|
||||
this.hass.localize("state.default.unavailable"),
|
||||
unavailable,
|
||||
status: unavailable
|
||||
restored,
|
||||
status: restored
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.restored"
|
||||
)
|
||||
: unavailable
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.unavailable"
|
||||
)
|
||||
@ -389,7 +390,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._filteredEntities(
|
||||
this._entities,
|
||||
this.hass.states,
|
||||
this._stateEntities,
|
||||
this._showDisabled,
|
||||
this._showUnavailable,
|
||||
this._showReadOnly
|
||||
@ -418,6 +419,43 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
loadEntityEditorDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps): void {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass");
|
||||
let changed = false;
|
||||
if (!this.hass || !this._entities) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("hass") || changedProps.has("_entities")) {
|
||||
const stateEntities: StateEntity[] = [];
|
||||
const regEntityIds = new Set(
|
||||
this._entities.map((entity) => entity.entity_id)
|
||||
);
|
||||
for (const entityId of Object.keys(this.hass.states)) {
|
||||
if (regEntityIds.has(entityId)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!oldHass ||
|
||||
this.hass.states[entityId] !== oldHass.states[entityId]
|
||||
) {
|
||||
changed = true;
|
||||
}
|
||||
stateEntities.push({
|
||||
name: computeStateName(this.hass.states[entityId]),
|
||||
entity_id: entityId,
|
||||
platform: computeDomain(entityId),
|
||||
disabled_by: null,
|
||||
readonly: true,
|
||||
selectable: false,
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
this._stateEntities = stateEntities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _showDisabledChanged() {
|
||||
this._showDisabled = !this._showDisabled;
|
||||
}
|
||||
@ -434,16 +472,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(ev: CustomEvent): void {
|
||||
const changedSelection = ev.detail as SelectionChangedEvent;
|
||||
const entity = changedSelection.id;
|
||||
if (changedSelection.selected) {
|
||||
this._selectedEntities = [...this._selectedEntities, entity];
|
||||
} else {
|
||||
this._selectedEntities = this._selectedEntities.filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
}
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedEntities = ev.detail.value;
|
||||
}
|
||||
|
||||
private _enableSelected() {
|
||||
@ -499,13 +531,26 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_title",
|
||||
`ui.panel.config.entities.picker.remove_selected.confirm_${
|
||||
removeableEntities.length !== this._selectedEntities.length
|
||||
? "partly_"
|
||||
: ""
|
||||
}title`,
|
||||
"number",
|
||||
removeableEntities.length
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_text"
|
||||
),
|
||||
text:
|
||||
removeableEntities.length === this._selectedEntities.length
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_text"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
|
||||
"removable",
|
||||
removeableEntities.length,
|
||||
"selected",
|
||||
this._selectedEntities.length
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
confirm: () => {
|
||||
|
@ -39,8 +39,8 @@ export class HaConfigHelpers extends LitElement {
|
||||
@property() private _stateItems: HassEntity[] = [];
|
||||
|
||||
private _columns = memoize(
|
||||
(_language): DataTableColumnContainer => {
|
||||
return {
|
||||
(narrow, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
@ -54,28 +54,45 @@ export class HaConfigHelpers extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) =>
|
||||
html`
|
||||
${name}
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${item.entity_id}
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
type: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.type"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
|
||||
type}
|
||||
${narrow
|
||||
? html`
|
||||
<div class="secondary">
|
||||
${item.entity_id}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.entity_id = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.entity_id"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
};
|
||||
}
|
||||
columns.type = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.type"
|
||||
),
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
filterable: true,
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
|
||||
type}
|
||||
`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
@ -106,7 +123,7 @@ export class HaConfigHelpers extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automation}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._stateItems)}
|
||||
@row-click=${this._openEditDialog}
|
||||
>
|
||||
|
@ -194,12 +194,14 @@ class HaConfigEntryPage extends LitElement {
|
||||
return css`
|
||||
.content {
|
||||
padding: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
ha-devices-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() private _params?: LovelaceDashboardDetailsDialogParams;
|
||||
@property() private _urlPath!: LovelaceDashboard["url_path"];
|
||||
@property() private _showSidebar!: boolean;
|
||||
@property() private _sidebarIcon!: string;
|
||||
@property() private _sidebarTitle!: string;
|
||||
@property() private _showInSidebar!: boolean;
|
||||
@property() private _icon!: string;
|
||||
@property() private _title!: string;
|
||||
@property() private _requireAdmin!: LovelaceDashboard["require_admin"];
|
||||
|
||||
@property() private _error?: string;
|
||||
@ -38,17 +38,16 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._urlPath = this._params.urlPath || "";
|
||||
if (this._params.dashboard) {
|
||||
this._urlPath = this._params.dashboard.url_path || "";
|
||||
this._showSidebar = !!this._params.dashboard.sidebar;
|
||||
this._sidebarIcon = this._params.dashboard.sidebar?.icon || "";
|
||||
this._sidebarTitle = this._params.dashboard.sidebar?.title || "";
|
||||
this._showInSidebar = !!this._params.dashboard.show_in_sidebar;
|
||||
this._icon = this._params.dashboard.icon || "";
|
||||
this._title = this._params.dashboard.title || "";
|
||||
this._requireAdmin = this._params.dashboard.require_admin || false;
|
||||
} else {
|
||||
this._urlPath = "";
|
||||
this._showSidebar = true;
|
||||
this._sidebarIcon = "";
|
||||
this._sidebarTitle = "";
|
||||
this._showInSidebar = true;
|
||||
this._icon = "";
|
||||
this._title = "";
|
||||
this._requireAdmin = false;
|
||||
}
|
||||
await this.updateComplete;
|
||||
@ -59,6 +58,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
const urlInvalid = !/^[a-zA-Z0-9_-]+$/.test(this._urlPath);
|
||||
const titleInvalid = !this._urlPath.trim();
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@ -67,97 +67,130 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.dashboard
|
||||
? this._sidebarTitle ||
|
||||
this.hass!.localize(
|
||||
this._params.urlPath
|
||||
? this._title ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.edit_dashboard"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-switch
|
||||
.checked=${this._showSidebar}
|
||||
@change=${this._showSidebarChanged}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
|
||||
)}</ha-switch
|
||||
>
|
||||
${this._showSidebar
|
||||
? html`
|
||||
<ha-icon-input
|
||||
.value=${this._sidebarIcon}
|
||||
@value-changed=${this._sidebarIconChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.icon"
|
||||
)}
|
||||
></ha-icon-input>
|
||||
${this._params.dashboard && !this._params.dashboard.id
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
: this._params.urlPath === "lovelace"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_default"
|
||||
)
|
||||
: html`
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
.value=${this._sidebarTitle}
|
||||
@value-changed=${this._sidebarTitleChanged}
|
||||
.label=${this.hass!.localize(
|
||||
.value=${this._title}
|
||||
@value-changed=${this._titleChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.title"
|
||||
)}
|
||||
@blur=${this._fillUrlPath}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
${!this._params.dashboard
|
||||
? html`
|
||||
<paper-input
|
||||
.value=${this._urlPath}
|
||||
@value-changed=${this._urlChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url"
|
||||
.invalid=${titleInvalid}
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.title_required"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
|
||||
)}
|
||||
.invalid=${urlInvalid}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
<ha-switch
|
||||
.checked=${this._requireAdmin}
|
||||
@change=${this._requireAdminChanged}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.require_admin"
|
||||
)}</ha-switch
|
||||
>
|
||||
</div>
|
||||
<ha-icon-input
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.icon"
|
||||
)}
|
||||
></ha-icon-input>
|
||||
${!this._params.dashboard
|
||||
? html`
|
||||
<paper-input
|
||||
.value=${this._urlPath}
|
||||
@value-changed=${this._urlChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url"
|
||||
)}
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
|
||||
)}
|
||||
.invalid=${urlInvalid}
|
||||
></paper-input>
|
||||
`
|
||||
: ""}
|
||||
<ha-switch
|
||||
.checked=${this._showInSidebar}
|
||||
@change=${this._showSidebarChanged}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
|
||||
)}
|
||||
</ha-switch>
|
||||
<ha-switch
|
||||
.checked=${this._requireAdmin}
|
||||
@change=${this._requireAdminChanged}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.require_admin"
|
||||
)}
|
||||
</ha-switch>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
${this._params.dashboard
|
||||
${this._params.urlPath
|
||||
? html`
|
||||
${this._params.dashboard?.id
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deleteDashboard}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.delete"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click="${this._deleteDashboard}"
|
||||
.disabled=${this._submitting}
|
||||
@click=${this._toggleDefault}
|
||||
.disabled=${this._params.urlPath === "lovelace" &&
|
||||
(!localStorage.defaultPage ||
|
||||
localStorage.defaultPage === "lovelace")}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.delete"
|
||||
)}
|
||||
${this._params.urlPath === localStorage.defaultPage ||
|
||||
(this._params.urlPath === "lovelace" &&
|
||||
!localStorage.defaultPage)
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.remove_default"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html``}
|
||||
: ""}
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateDashboard}"
|
||||
.disabled=${urlInvalid || this._submitting}
|
||||
.disabled=${urlInvalid || titleInvalid || this._submitting}
|
||||
>
|
||||
${this._params.dashboard
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.update"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
${this._params.urlPath
|
||||
? this._params.dashboard?.id
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.update"
|
||||
)
|
||||
: this.hass.localize("ui.common.close")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.create"
|
||||
)}
|
||||
</mwc-button>
|
||||
@ -170,43 +203,59 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
this._urlPath = ev.detail.value;
|
||||
}
|
||||
|
||||
private _sidebarIconChanged(ev: PolymerChangedEvent<string>) {
|
||||
private _iconChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._sidebarIcon = ev.detail.value;
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
private _sidebarTitleChanged(ev: PolymerChangedEvent<string>) {
|
||||
private _titleChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._sidebarTitle = ev.detail.value;
|
||||
this._title = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fillUrlPath() {
|
||||
if (this._urlPath) {
|
||||
return;
|
||||
}
|
||||
const parts = this._sidebarTitle.split(" ");
|
||||
const parts = this._title.split(" ");
|
||||
|
||||
if (parts.length) {
|
||||
this._urlPath = parts[0].toLowerCase();
|
||||
this._urlPath = parts.join("_").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private _showSidebarChanged(ev: Event) {
|
||||
this._showSidebar = (ev.target as HaSwitch).checked;
|
||||
this._showInSidebar = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private _requireAdminChanged(ev: Event) {
|
||||
this._requireAdmin = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
private _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
if (urlPath === localStorage.defaultPage) {
|
||||
delete localStorage.defaultPage;
|
||||
} else {
|
||||
localStorage.defaultPage = urlPath;
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
||||
this._close();
|
||||
}
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: Partial<LovelaceDashboardMutableParams> = {
|
||||
require_admin: this._requireAdmin,
|
||||
sidebar: this._showSidebar
|
||||
? { icon: this._sidebarIcon, title: this._sidebarTitle }
|
||||
: null,
|
||||
show_in_sidebar: this._showInSidebar,
|
||||
icon: this._icon || undefined,
|
||||
title: this._title,
|
||||
};
|
||||
if (this._params!.dashboard) {
|
||||
await this._params!.updateDashboard(values);
|
||||
@ -217,7 +266,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
values as LovelaceDashboardCreateParams
|
||||
);
|
||||
}
|
||||
this._params = undefined;
|
||||
this._close();
|
||||
} catch (err) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
} finally {
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
css,
|
||||
} from "lit-element";
|
||||
import memoize from "memoize-one";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@ -24,13 +25,11 @@ import {
|
||||
updateDashboard,
|
||||
deleteDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelacePanelConfig,
|
||||
} from "../../../../data/lovelace";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { compare } from "../../../../common/string/compare";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
|
||||
@ -43,7 +42,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
@property() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
private _columns = memoize(
|
||||
(_language, dashboards): DataTableColumnContainer => {
|
||||
(narrow: boolean, _language, dashboards): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
@ -62,89 +61,147 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (title, dashboard: any) => {
|
||||
const titleTemplate = html`
|
||||
${title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-icon
|
||||
style="padding-left: 10px;"
|
||||
icon="hass:check-circle-outline"
|
||||
></ha-icon>
|
||||
<paper-tooltip>
|
||||
This is the default dashdoard.
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
return narrow
|
||||
? html`
|
||||
${titleTemplate}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
)}${dashboard.filename
|
||||
? html`
|
||||
- ${dashboard.filename}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: titleTemplate;
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
};
|
||||
|
||||
if (!narrow) {
|
||||
columns.mode = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
template: (mode) =>
|
||||
html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${mode}`
|
||||
) || mode}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
if (dashboards.some((dashboard) => dashboard.mode === "yaml")) {
|
||||
columns.filename = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const columns2: DataTableColumnContainer = {
|
||||
require_admin: {
|
||||
if (dashboards.some((dashboard) => dashboard.filename)) {
|
||||
columns.filename = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||
),
|
||||
width: "15%",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
};
|
||||
}
|
||||
columns.require_admin = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
||||
),
|
||||
sortable: true,
|
||||
type: "icon",
|
||||
width: "100px",
|
||||
template: (requireAdmin: boolean) =>
|
||||
requireAdmin
|
||||
? html`
|
||||
<ha-icon icon="hass:check-circle-outline"></ha-icon>
|
||||
<ha-icon icon="hass:check"></ha-icon>
|
||||
`
|
||||
: html`
|
||||
-
|
||||
`,
|
||||
},
|
||||
sidebar: {
|
||||
};
|
||||
columns.show_in_sidebar = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
||||
),
|
||||
type: "icon",
|
||||
width: "100px",
|
||||
template: (sidebar) =>
|
||||
sidebar
|
||||
? html`
|
||||
<ha-icon icon="hass:check-circle-outline"></ha-icon>
|
||||
<ha-icon icon="hass:check"></ha-icon>
|
||||
`
|
||||
: html`
|
||||
-
|
||||
`,
|
||||
},
|
||||
url_path: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
filterable: true,
|
||||
template: (urlPath) =>
|
||||
html`
|
||||
<mwc-button .urlPath=${urlPath} @click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
columns.url_path = {
|
||||
title: "",
|
||||
filterable: true,
|
||||
width: "75px",
|
||||
template: (urlPath) =>
|
||||
narrow
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hass:open-in-new"
|
||||
.urlPath=${urlPath}
|
||||
@click=${this._navigate}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button .urlPath=${urlPath} @click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
};
|
||||
return { ...columns, ...columns2 };
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
return dashboards.map((dashboard) => {
|
||||
return {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
icon: dashboard.sidebar?.icon,
|
||||
title: dashboard.sidebar?.title || dashboard.url_path,
|
||||
};
|
||||
});
|
||||
const defaultMode = (this.hass.panels?.lovelace
|
||||
?.config as LovelacePanelConfig).mode;
|
||||
const isDefault =
|
||||
!localStorage.defaultPage || localStorage.defaultPage === "lovelace";
|
||||
return [
|
||||
{
|
||||
icon: "hass:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
default: isDefault,
|
||||
sidebar: isDefault,
|
||||
require_admin: false,
|
||||
url_path: "lovelace",
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
},
|
||||
...dashboards.map((dashboard) => {
|
||||
return {
|
||||
...dashboard,
|
||||
default: localStorage.defaultPage === dashboard.url_path,
|
||||
};
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -161,9 +218,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${lovelaceTabs}
|
||||
.columns=${this._columns(this.hass.language, this._dashboards)}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this._dashboards
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
@row-click=${this._editDashboard}
|
||||
id="url_path"
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-fab
|
||||
@ -194,28 +256,22 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
}
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const id = (ev.detail as RowClickedEvent).id;
|
||||
const dashboard = id
|
||||
? this._dashboards.find((res) => res.id === id)
|
||||
: undefined;
|
||||
if (!dashboard) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._openDialog(dashboard);
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||
this._openDialog(dashboard, urlPath);
|
||||
}
|
||||
|
||||
private _addDashboard() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private async _openDialog(dashboard?: LovelaceDashboard): Promise<void> {
|
||||
private async _openDialog(
|
||||
dashboard?: LovelaceDashboard,
|
||||
urlPath?: string
|
||||
): Promise<void> {
|
||||
showDashboardDetailDialog(this, {
|
||||
dashboard,
|
||||
urlPath,
|
||||
createDashboard: async (values: LovelaceDashboardCreateParams) => {
|
||||
const created = await createDashboard(this.hass!, values);
|
||||
this._dashboards = this._dashboards!.concat(
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
|
||||
export interface LovelaceDashboardDetailsDialogParams {
|
||||
dashboard?: LovelaceDashboard;
|
||||
urlPath?: string;
|
||||
createDashboard: (values: LovelaceDashboardCreateParams) => Promise<unknown>;
|
||||
updateDashboard: (
|
||||
updates: Partial<LovelaceDashboardMutableParams>
|
||||
|
@ -57,6 +57,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
type: {
|
||||
title: this.hass.localize(
|
||||
@ -64,6 +65,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
template: (type) =>
|
||||
html`
|
||||
${this.hass.localize(
|
||||
|
@ -128,6 +128,7 @@ class DialogPersonDetail extends LitElement {
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/#presence-detection"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.link_presence_detection_integrations"
|
||||
)}</a
|
||||
|
@ -54,6 +54,7 @@ class HaSceneDashboard extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scene/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.learn_more")}
|
||||
</a>
|
||||
|
@ -122,6 +122,7 @@ export class HaScriptEditor extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scripts/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.link_available_actions"
|
||||
|
@ -54,6 +54,7 @@ class HaScriptPicker extends LitElement {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/scripts/editor/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.picker.learn_more"
|
||||
|
@ -25,6 +25,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import "@material/mwc-button";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("zha-add-group-page")
|
||||
export class ZHAAddGroupPage extends LitElement {
|
||||
@ -82,7 +83,6 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleAddSelectionChanged}
|
||||
class="table"
|
||||
>
|
||||
</zha-devices-data-table>
|
||||
|
||||
@ -114,21 +114,10 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
this.devices = await fetchGroupableDevices(this.hass!);
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(ev: CustomEvent): void {
|
||||
const changedSelection = ev.detail as SelectionChangedEvent;
|
||||
const entity = changedSelection.id;
|
||||
if (
|
||||
changedSelection.selected &&
|
||||
!this._selectedDevicesToAdd.includes(entity)
|
||||
) {
|
||||
this._selectedDevicesToAdd.push(entity);
|
||||
} else {
|
||||
const index = this._selectedDevicesToAdd.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
this._selectedDevicesToAdd.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _createGroup(): Promise<void> {
|
||||
@ -168,11 +157,6 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.table {
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
ha-config-section *:last-child {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
title: "Name",
|
||||
sortable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
@ -56,6 +57,7 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
title: "Name",
|
||||
sortable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
id: {
|
||||
title: "ID",
|
||||
@ -65,10 +67,12 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
`;
|
||||
},
|
||||
sortable: true,
|
||||
width: "15%",
|
||||
},
|
||||
endpoint_id: {
|
||||
title: "Endpoint ID",
|
||||
sortable: true,
|
||||
width: "15%",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -80,6 +84,7 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
.data=${this._clusters(this.clusters)}
|
||||
.id=${"cluster_id"}
|
||||
selectable
|
||||
auto-height
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
@ -71,16 +72,19 @@ class ZHAConfigDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
nwk: {
|
||||
title: "Nwk",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
ieee: {
|
||||
title: "IEEE",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -139,6 +143,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
.data=${this._memoizeDevices(this._devices)}
|
||||
@row-click=${this._handleDeviceClicked}
|
||||
.id=${"ieee"}
|
||||
auto-height
|
||||
></ha-data-table>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
@ -53,6 +53,7 @@ export class ZHADevicesDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name) => html`
|
||||
<div @click=${this._handleClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
@ -66,6 +67,7 @@ export class ZHADevicesDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name) => html`
|
||||
<div @click=${this._handleClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
@ -76,11 +78,13 @@ export class ZHADevicesDataTable extends LitElement {
|
||||
title: "Manufacturer",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
},
|
||||
model: {
|
||||
title: "Model",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -91,14 +95,15 @@ export class ZHADevicesDataTable extends LitElement {
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._devices(this.devices)}
|
||||
.selectable=${this.selectable}
|
||||
auto-height
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleClicked(ev: CustomEvent) {
|
||||
const ieee = (ev.target as HTMLElement)
|
||||
.closest("tr")!
|
||||
.getAttribute("data-row-id")!;
|
||||
const ieee = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__row"
|
||||
) as any).rowId;
|
||||
showZHADeviceInfoDialog(this, { ieee });
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import { HomeAssistant } from "../../../types";
|
||||
import { ItemSelectedEvent } from "./types";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("zha-group-binding-control")
|
||||
export class ZHAGroupBindingControl extends LitElement {
|
||||
@ -200,21 +201,11 @@ export class ZHAGroupBindingControl extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClusterSelectionChanged(event: CustomEvent): void {
|
||||
const changedSelection = event.detail as SelectionChangedEvent;
|
||||
const clusterId = changedSelection.id;
|
||||
if (
|
||||
changedSelection.selected &&
|
||||
!this._selectedClusters.includes(clusterId)
|
||||
) {
|
||||
this._selectedClusters.push(clusterId);
|
||||
} else {
|
||||
const index = this._selectedClusters.indexOf(clusterId);
|
||||
if (index !== -1) {
|
||||
this._selectedClusters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this._selectedClusters = [...this._selectedClusters];
|
||||
private _handleClusterSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedClusters = ev.detail.value;
|
||||
|
||||
this._clustersToBind = [];
|
||||
for (const clusterIndex of this._selectedClusters) {
|
||||
const selectedCluster = this._clusters.find((cluster) => {
|
||||
|
@ -31,6 +31,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import "@material/mwc-button";
|
||||
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("zha-group-page")
|
||||
export class ZHAGroupPage extends LitElement {
|
||||
@ -145,7 +146,6 @@ export class ZHAGroupPage extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleRemoveSelectionChanged}
|
||||
class="table"
|
||||
>
|
||||
</zha-devices-data-table>
|
||||
|
||||
@ -180,7 +180,6 @@ export class ZHAGroupPage extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleAddSelectionChanged}
|
||||
class="table"
|
||||
>
|
||||
</zha-devices-data-table>
|
||||
|
||||
@ -223,38 +222,16 @@ export class ZHAGroupPage extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(ev: CustomEvent): void {
|
||||
const changedSelection = ev.detail as SelectionChangedEvent;
|
||||
const entity = changedSelection.id;
|
||||
if (
|
||||
changedSelection.selected &&
|
||||
!this._selectedDevicesToAdd.includes(entity)
|
||||
) {
|
||||
this._selectedDevicesToAdd.push(entity);
|
||||
} else {
|
||||
const index = this._selectedDevicesToAdd.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
this._selectedDevicesToAdd.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleRemoveSelectionChanged(ev: CustomEvent): void {
|
||||
const changedSelection = ev.detail as SelectionChangedEvent;
|
||||
const entity = changedSelection.id;
|
||||
if (
|
||||
changedSelection.selected &&
|
||||
!this._selectedDevicesToRemove.includes(entity)
|
||||
) {
|
||||
this._selectedDevicesToRemove.push(entity);
|
||||
} else {
|
||||
const index = this._selectedDevicesToRemove.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
this._selectedDevicesToRemove.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this._selectedDevicesToRemove = [...this._selectedDevicesToRemove];
|
||||
private _handleRemoveSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToRemove = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _addMembersToGroup(): Promise<void> {
|
||||
@ -309,11 +286,6 @@ export class ZHAGroupPage extends LitElement {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.table {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
mwc-button paper-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
@ -19,6 +19,7 @@ import "@polymer/paper-spinner/paper-spinner";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("zha-groups-dashboard")
|
||||
export class ZHAGroupsDashboard extends LitElement {
|
||||
@ -102,21 +103,12 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
|
||||
}
|
||||
|
||||
private _handleRemoveSelectionChanged(ev: CustomEvent): void {
|
||||
const changedSelection = ev.detail as SelectionChangedEvent;
|
||||
const groupId = Number(changedSelection.id);
|
||||
if (
|
||||
changedSelection.selected &&
|
||||
!this._selectedGroupsToRemove.includes(groupId)
|
||||
) {
|
||||
this._selectedGroupsToRemove.push(groupId);
|
||||
} else {
|
||||
const index = this._selectedGroupsToRemove.indexOf(groupId);
|
||||
if (index !== -1) {
|
||||
this._selectedGroupsToRemove.splice(index, 1);
|
||||
}
|
||||
}
|
||||
this._selectedGroupsToRemove = [...this._selectedGroupsToRemove];
|
||||
private _handleRemoveSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedGroupsToRemove = ev.detail.value.map((value) =>
|
||||
Number(value)
|
||||
);
|
||||
}
|
||||
|
||||
private async _removeGroup(): Promise<void> {
|
||||
|
@ -52,6 +52,7 @@ export class ZHAGroupsDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name) => html`
|
||||
<div @click=${this._handleRowClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
@ -65,6 +66,7 @@ export class ZHAGroupsDataTable extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name) => html`
|
||||
<div @click=${this._handleRowClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
@ -73,6 +75,8 @@ export class ZHAGroupsDataTable extends LitElement {
|
||||
},
|
||||
group_id: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
width: "15%",
|
||||
template: (groupId: number) => {
|
||||
return html`
|
||||
${formatAsPaddedHex(groupId)}
|
||||
@ -82,6 +86,8 @@ export class ZHAGroupsDataTable extends LitElement {
|
||||
},
|
||||
members: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
width: "15%",
|
||||
template: (members: ZHADevice[]) => {
|
||||
return html`
|
||||
${members.length}
|
||||
@ -98,14 +104,15 @@ export class ZHAGroupsDataTable extends LitElement {
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._groups(this.groups)}
|
||||
.selectable=${this.selectable}
|
||||
auto-height
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: CustomEvent) {
|
||||
const groupId = (ev.target as HTMLElement)
|
||||
.closest("tr")!
|
||||
.getAttribute("data-row-id")!;
|
||||
const groupId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__row"
|
||||
) as any).rowId;
|
||||
navigate(this, `/config/zha/group/${groupId}`);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ export class ZwaveNetwork extends LitElement {
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/z-wave/control-panel/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.zwave.learn_more")}
|
||||
</a>
|
||||
|
@ -61,9 +61,11 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/configuration/events/"
|
||||
target="_blank"
|
||||
>[[localize( 'ui.panel.developer-tools.tabs.events.documentation'
|
||||
)]]</a
|
||||
rel="noreferrer"
|
||||
>
|
||||
[[localize( 'ui.panel.developer-tools.tabs.events.documentation'
|
||||
)]]
|
||||
</a>
|
||||
</p>
|
||||
<div class="ha-form">
|
||||
<paper-input
|
||||
|
@ -15,7 +15,6 @@ import "./integrations-card";
|
||||
|
||||
const JS_TYPE = __BUILD__;
|
||||
const JS_VERSION = __VERSION__;
|
||||
const OPT_IN_PANEL = "states";
|
||||
|
||||
class HaPanelDevInfo extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@ -25,32 +24,10 @@ class HaPanelDevInfo extends LitElement {
|
||||
const customUiList: Array<{ name: string; url: string; version: string }> =
|
||||
(window as any).CUSTOM_UI_LIST || [];
|
||||
|
||||
const nonDefaultLink =
|
||||
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
|
||||
? "/lovelace"
|
||||
: "/states";
|
||||
|
||||
const nonDefaultLinkText =
|
||||
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
|
||||
? this.hass.localize("ui.panel.developer-tools.tabs.info.lovelace_ui")
|
||||
: `${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.states_ui"
|
||||
)} (DEPRECATED)`;
|
||||
|
||||
const defaultPageText = `${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.default_ui",
|
||||
"action",
|
||||
localStorage.defaultPage === OPT_IN_PANEL
|
||||
? this.hass.localize("ui.panel.developer-tools.tabs.info.remove")
|
||||
: this.hass.localize("ui.panel.developer-tools.tabs.info.set"),
|
||||
"name",
|
||||
`${OPT_IN_PANEL} (DEPRECATED)`
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div class="about">
|
||||
<p class="version">
|
||||
<a href="https://www.home-assistant.io" target="_blank"
|
||||
<a href="https://www.home-assistant.io" target="_blank" rel="noreferrer"
|
||||
><img
|
||||
src="/static/icons/favicon-192x192.png"
|
||||
height="192"
|
||||
@ -59,7 +36,7 @@ class HaPanelDevInfo extends LitElement {
|
||||
)}"
|
||||
/></a>
|
||||
<br />
|
||||
<h2>Home Assistant ${hass.config.version}</h2>
|
||||
<h2>Home Assistant ${hass.connection.haVersion}</h2>
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
@ -71,7 +48,7 @@ class HaPanelDevInfo extends LitElement {
|
||||
<p class="develop">
|
||||
<a
|
||||
href="https://www.home-assistant.io/developers/credits/"
|
||||
target="_blank"
|
||||
target="_blank" rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.developed_by"
|
||||
@ -85,7 +62,7 @@ class HaPanelDevInfo extends LitElement {
|
||||
${this.hass.localize("ui.panel.developer-tools.tabs.info.source")}
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant"
|
||||
target="_blank"
|
||||
target="_blank" rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.server"
|
||||
)}</a
|
||||
@ -93,7 +70,7 @@ class HaPanelDevInfo extends LitElement {
|
||||
—
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant-polymer"
|
||||
target="_blank"
|
||||
target="_blank" rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.frontend"
|
||||
)}</a
|
||||
@ -103,14 +80,14 @@ class HaPanelDevInfo extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.info.built_using"
|
||||
)}
|
||||
<a href="https://www.python.org">Python 3</a>,
|
||||
<a href="https://www.polymer-project.org" target="_blank">Polymer</a>,
|
||||
<a href="https://www.python.org" target="_blank" rel="noreferrer">Python 3</a>,
|
||||
<a href="https://www.polymer-project.org" target="_blank" rel="noreferrer">Polymer</a>,
|
||||
${this.hass.localize("ui.panel.developer-tools.tabs.info.icons_by")}
|
||||
<a href="https://www.google.com/design/icons/" target="_blank"
|
||||
<a href="https://www.google.com/design/icons/" target="_blank" rel="noreferrer"
|
||||
>Google</a
|
||||
>
|
||||
and
|
||||
<a href="https://MaterialDesignIcons.com" target="_blank"
|
||||
<a href="https://MaterialDesignIcons.com" target="_blank" rel="noreferrer"
|
||||
>MaterialDesignIcons.com</a
|
||||
>.
|
||||
</p>
|
||||
@ -142,11 +119,6 @@ class HaPanelDevInfo extends LitElement {
|
||||
: ""
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
|
||||
<a href="#" @click="${this._toggleDefaultPage}">${defaultPageText}</a
|
||||
><br />
|
||||
</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<system-health-card .hass=${this.hass}></system-health-card>
|
||||
@ -167,15 +139,6 @@ class HaPanelDevInfo extends LitElement {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
protected _toggleDefaultPage(): void {
|
||||
if (localStorage.defaultPage === OPT_IN_PANEL) {
|
||||
delete localStorage.defaultPage;
|
||||
} else {
|
||||
localStorage.defaultPage = OPT_IN_PANEL;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -32,12 +32,20 @@ class IntegrationsCard extends LitElement {
|
||||
<tr>
|
||||
<td>${domain}</td>
|
||||
<td>
|
||||
<a href=${integrationDocsUrl(domain)} target="_blank">
|
||||
<a
|
||||
href=${integrationDocsUrl(domain)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href=${integrationIssuesUrl(domain)} target="_blank">
|
||||
<a
|
||||
href=${integrationIssuesUrl(domain)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Issues
|
||||
</a>
|
||||
</td>
|
||||
|
@ -54,15 +54,22 @@ class DialogSystemLogDetail extends LitElement {
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<p>
|
||||
Logger: ${item.name}
|
||||
Logger: ${item.name}<br />
|
||||
Source: ${item.source.join(":")}
|
||||
${integration
|
||||
? html`
|
||||
<br />
|
||||
Integration: ${domainToName(this.hass.localize, integration)}
|
||||
(<a href=${integrationDocsUrl(integration)} target="_blank"
|
||||
(<a
|
||||
href=${integrationDocsUrl(integration)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>documentation</a
|
||||
>,
|
||||
<a href=${integrationIssuesUrl(integration)} target="_blank"
|
||||
<a
|
||||
href=${integrationIssuesUrl(integration)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>issues</a
|
||||
>)
|
||||
`
|
||||
@ -81,11 +88,18 @@ class DialogSystemLogDetail extends LitElement {
|
||||
Last logged:
|
||||
${formatSystemLogTime(item.timestamp, this.hass!.language)}
|
||||
</p>
|
||||
${item.message
|
||||
${item.message.length > 1
|
||||
? html`
|
||||
<pre>${item.message}</pre>
|
||||
<ul>
|
||||
${item.message.map(
|
||||
(msg) =>
|
||||
html`
|
||||
<li>${msg}</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: html``}
|
||||
: item.message[0]}
|
||||
${item.exception
|
||||
? html`
|
||||
<pre>${item.exception}</pre>
|
||||
|
@ -62,7 +62,7 @@ export class SystemLogCard extends LitElement {
|
||||
<paper-item @click=${this._openLog} .logItem=${item}>
|
||||
<paper-item-body two-line>
|
||||
<div class="row">
|
||||
${item.message}
|
||||
${item.message[0]}
|
||||
</div>
|
||||
<div secondary>
|
||||
${formatSystemLogTime(
|
||||
@ -75,7 +75,7 @@ export class SystemLogCard extends LitElement {
|
||||
this.hass!.localize,
|
||||
integrations[idx]!
|
||||
)
|
||||
: item.source}
|
||||
: item.source[0]}
|
||||
(${item.level})
|
||||
${item.count > 1
|
||||
? html`
|
||||
|
@ -68,6 +68,7 @@ class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
|
||||
<a
|
||||
href="http://jinja.pocoo.org/docs/dev/templates/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.developer-tools.tabs.templates.jinja_documentation')]]</a
|
||||
>
|
||||
</li>
|
||||
@ -75,6 +76,7 @@ class HaPanelDevTemplate extends LocalizeMixin(PolymerElement) {
|
||||
<a
|
||||
href="https://home-assistant.io/docs/configuration/templating/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>[[localize('ui.panel.developer-tools.tabs.templates.template_extensions')]]</a
|
||||
>
|
||||
</li>
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../states/ha-panel-states";
|
||||
|
||||
class HaPanelKiosk extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<ha-panel-states
|
||||
id="kiosk-states"
|
||||
hass="[[hass]]"
|
||||
show-menu
|
||||
route="[[route]]"
|
||||
panel-visible
|
||||
></ha-panel-states>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
route: Object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-panel-kiosk", HaPanelKiosk);
|
@ -24,6 +24,8 @@ import {
|
||||
import { AlarmPanelCardConfig } from "./types";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
|
||||
const ICONS = {
|
||||
armed_away: "hass:shield-lock",
|
||||
@ -46,8 +48,27 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
return document.createElement("hui-alarm-panel-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig() {
|
||||
return { states: ["arm_home", "arm_away"], entity: "" };
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
) {
|
||||
const includeDomains = ["alarm_control_panel"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
states: ["arm_home", "arm_away"],
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
}
|
||||
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -30,9 +30,10 @@ import { ButtonCardConfig } from "./types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import { ActionHandlerEvent, LovelaceConfig } from "../../../data/lovelace";
|
||||
import { computeActiveState } from "../../../common/entity/compute_active_state";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-button-card")
|
||||
export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
@ -43,13 +44,28 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
return document.createElement("hui-button-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
): object {
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill
|
||||
);
|
||||
|
||||
return {
|
||||
tap_action: { action: "toggle" },
|
||||
hold_action: { action: "more-info" },
|
||||
show_icon: true,
|
||||
show_name: true,
|
||||
state_color: true,
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,26 @@ import { customElement } from "lit-element";
|
||||
|
||||
import { HuiConditionalBase } from "../components/hui-conditional-base";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { ConditionalCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-conditional-card")
|
||||
class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import(
|
||||
/* webpackChunkName: "hui-conditional-card-editor" */ "../editor/config-elements/hui-conditional-card-editor"
|
||||
);
|
||||
return document.createElement("hui-conditional-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return {
|
||||
conditions: [],
|
||||
card: {},
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: ConditionalCardConfig): void {
|
||||
this.validateConfig(config);
|
||||
|
||||
|
@ -27,6 +27,8 @@ import { createHeaderFooterElement } from "../create-element/create-header-foote
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { LovelaceConfig } from "../../../data/lovelace";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
@ -37,8 +39,22 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
return document.createElement("hui-entities-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): object {
|
||||
return { entities: [] };
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
lovelaceConfig: LovelaceConfig,
|
||||
entities?: string[],
|
||||
entitiesFill?: string[]
|
||||
) {
|
||||
const maxEntities = 3;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
lovelaceConfig,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFill
|
||||
);
|
||||
|
||||
return { entities: foundEntities };
|
||||
}
|
||||
|
||||
@property() private _config?: EntitiesCardConfig;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user