Compare commits
150 Commits
20200414.0
...
add-import
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f995d8d525 | ||
![]() |
e83ede245d | ||
![]() |
05ac275780 | ||
![]() |
966c0bc06c | ||
![]() |
abf136dd63 | ||
![]() |
b6309cfd16 | ||
![]() |
b69d5e0fa3 | ||
![]() |
5084cde6b9 | ||
![]() |
ca1cc7ed0d | ||
![]() |
808a31db2b | ||
![]() |
44ad75aead | ||
![]() |
0961c9d05e | ||
![]() |
56754b4d43 | ||
![]() |
661779ad4e | ||
![]() |
cf37ebb652 | ||
![]() |
82741e490b | ||
![]() |
5fed28808e | ||
![]() |
321c0cfc84 | ||
![]() |
eba7cedaa6 | ||
![]() |
71492d0467 | ||
![]() |
9630a58ea7 | ||
![]() |
89f6f16ba2 | ||
![]() |
2d646da97f | ||
![]() |
39b5460598 | ||
![]() |
636429ccfa | ||
![]() |
e5abb95f5c | ||
![]() |
c631554eb0 | ||
![]() |
db07eeb916 | ||
![]() |
3216a46e76 | ||
![]() |
df002d7a67 | ||
![]() |
e8a0632108 | ||
![]() |
0a92c28bac | ||
![]() |
d419547463 | ||
![]() |
da392912b3 | ||
![]() |
9ebee02727 | ||
![]() |
0bdcfcc42f | ||
![]() |
43623a30bc | ||
![]() |
99e73054a9 | ||
![]() |
108233f3b8 | ||
![]() |
8f2a7c95b3 | ||
![]() |
7fdd525dac | ||
![]() |
7ed24137eb | ||
![]() |
07cd30eaca | ||
![]() |
df8cf66e02 | ||
![]() |
f067bcd877 | ||
![]() |
e11ef55dd8 | ||
![]() |
fa8bd30e83 | ||
![]() |
04a2ff7506 | ||
![]() |
bc68e20041 | ||
![]() |
786da25b9f | ||
![]() |
c3832d56c3 | ||
![]() |
79d1a2f458 | ||
![]() |
f16a674a39 | ||
![]() |
6e38a80efc | ||
![]() |
6a3a1297ad | ||
![]() |
5ca63a8052 | ||
![]() |
8b04df093c | ||
![]() |
d2a5494335 | ||
![]() |
de545e90e2 | ||
![]() |
57ab7e829b | ||
![]() |
6847830575 | ||
![]() |
2084ecc4c6 | ||
![]() |
b0c27e587e | ||
![]() |
1687d90d02 | ||
![]() |
dfd9bf3c64 | ||
![]() |
5a25b9c2f1 | ||
![]() |
bf68101754 | ||
![]() |
487bd8d3fc | ||
![]() |
3ba9c931b9 | ||
![]() |
8484f7595a | ||
![]() |
ee889d59d4 | ||
![]() |
ae10330844 | ||
![]() |
f3e88f6f2e | ||
![]() |
2abfd0392d | ||
![]() |
462c1f94d6 | ||
![]() |
f4710891d0 | ||
![]() |
1fdb6b8034 | ||
![]() |
8029c3d672 | ||
![]() |
61fdee14c6 | ||
![]() |
117e4e468f | ||
![]() |
869ec113f4 | ||
![]() |
00e7b93011 | ||
![]() |
03e581870a | ||
![]() |
b04fe141ac | ||
![]() |
b0168fbb85 | ||
![]() |
75ba343b5e | ||
![]() |
88217473f7 | ||
![]() |
01e5dfc9b3 | ||
![]() |
58869677bd | ||
![]() |
032b01aec1 | ||
![]() |
a47c3fa854 | ||
![]() |
7c6ba1a782 | ||
![]() |
0d08f5413b | ||
![]() |
949cc17a9e | ||
![]() |
607c1a1ef0 | ||
![]() |
db2dab8227 | ||
![]() |
eb4ba4fc78 | ||
![]() |
bf888b1547 | ||
![]() |
c468aab9b2 | ||
![]() |
a0dae802f2 | ||
![]() |
14330fbd93 | ||
![]() |
bf55be7f7f | ||
![]() |
e10987a705 | ||
![]() |
355f40d740 | ||
![]() |
2503fabe1d | ||
![]() |
32c7c0b4f0 | ||
![]() |
301a964a65 | ||
![]() |
49f2fd2af7 | ||
![]() |
4f518cac2c | ||
![]() |
8420f73919 | ||
![]() |
1ef2d3c7f2 | ||
![]() |
fc20fd32f1 | ||
![]() |
13a8bf6993 | ||
![]() |
b2f424a6f8 | ||
![]() |
a9d927551c | ||
![]() |
fdf7b516a0 | ||
![]() |
9a00078169 | ||
![]() |
1dfb632fc4 | ||
![]() |
1c1f9a6a89 | ||
![]() |
2b76b3887e | ||
![]() |
a49c84032b | ||
![]() |
c72bb5b22b | ||
![]() |
24a844dddc | ||
![]() |
ae903973a7 | ||
![]() |
82442bb5ec | ||
![]() |
8786302190 | ||
![]() |
d27a17cf8e | ||
![]() |
b4b90ca59d | ||
![]() |
ecd967a68d | ||
![]() |
8812340768 | ||
![]() |
659da7bf80 | ||
![]() |
91eabf3c38 | ||
![]() |
e355c81e41 | ||
![]() |
d45a674652 | ||
![]() |
f91b46e88c | ||
![]() |
f57754212c | ||
![]() |
97c454aa0d | ||
![]() |
b516f10a35 | ||
![]() |
0ccc788148 | ||
![]() |
5c941e0afb | ||
![]() |
492e4d2df4 | ||
![]() |
66f33ad497 | ||
![]() |
ff81536463 | ||
![]() |
ba0cba1a2b | ||
![]() |
fc7771ec13 | ||
![]() |
4c3069e5b7 | ||
![]() |
ed54a185e4 | ||
![]() |
4f81085d0f | ||
![]() |
8383caf6a6 | ||
![]() |
1b9f224569 |
@@ -11,17 +11,12 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
},
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "h",
|
||||
"version": "15.0"
|
||||
},
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "./webpack.config.js"
|
||||
@@ -74,7 +69,7 @@
|
||||
"import/extensions": [
|
||||
2,
|
||||
"ignorePackages",
|
||||
{ "ts": "never", "js": "never" }
|
||||
{ "ts": "ignorePackages", "js": "ignorePackages" }
|
||||
],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": 0,
|
||||
@@ -88,13 +83,6 @@
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0
|
||||
},
|
||||
"plugins": [
|
||||
"disable",
|
||||
"import",
|
||||
"react",
|
||||
"lit",
|
||||
"prettier",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
||||
"processor": "disable/disable"
|
||||
}
|
||||
|
12
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -62,6 +62,18 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
- Browser and browser version:
|
||||
- Operating system:
|
||||
|
||||
## State of relevant entities
|
||||
|
||||
<!--
|
||||
If your issue is about how an entity is shown in the UI, please add the state
|
||||
and attributes for all situations with a screenshot of the UI.
|
||||
You can find this information at `/developer-tools/state`
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Problem-relevant configuration
|
||||
|
||||
<!--
|
||||
|
6
.github/workflows/ci.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
- name: Build icons
|
||||
run: ./node_modules/.bin/gulp gen-icons-hassio gen-icons-mdi gen-icons-app
|
||||
run: ./node_modules/.bin/gulp gen-icons-json
|
||||
- name: Build translations
|
||||
run: ./node_modules/.bin/gulp build-translations
|
||||
- name: Run eslint
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-app
|
||||
env:
|
||||
TRAVIS: "true"
|
||||
IS_TEST: "true"
|
||||
supervisor:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
@@ -122,4 +122,4 @@ jobs:
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-hassio
|
||||
env:
|
||||
TRAVIS: "true"
|
||||
IS_TEST: "true"
|
||||
|
1
.gitignore
vendored
@@ -5,7 +5,6 @@ npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
.reify-cache
|
||||
demo/hademo-icons.html
|
||||
|
||||
# Python stuff
|
||||
*.py[cod]
|
||||
|
@@ -2,36 +2,9 @@ build
|
||||
build-translations/*
|
||||
translations/*
|
||||
node_modules/*
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
hass_frontend/*
|
||||
.reify-cache
|
||||
demo/hademo-icons.html
|
||||
|
||||
# Python stuff
|
||||
*.py[cod]
|
||||
*.egg
|
||||
*.egg-info
|
||||
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
lib
|
||||
bin
|
||||
dist
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Cast dev settings
|
||||
src/cast/dev_const.ts
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
yarn-error.log
|
||||
|
||||
#asdf
|
||||
.tool-versions
|
||||
|
@@ -1,50 +1,40 @@
|
||||
const options = ({ latestBuild }) => ({
|
||||
presets: [
|
||||
!latestBuild && [require("@babel/preset-env").default, { modules: false }],
|
||||
require("@babel/preset-typescript").default,
|
||||
].filter(Boolean),
|
||||
plugins: [
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
[
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
{ loose: true, useBuiltIns: true },
|
||||
],
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/syntax-dynamic-import",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-class-properties").default,
|
||||
{ loose: true },
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
module.exports.options = options;
|
||||
module.exports.babelLoaderConfig = ({ latestBuild }) => {
|
||||
if (latestBuild === undefined) {
|
||||
throw Error("latestBuild not defined for babel loader config");
|
||||
}
|
||||
return {
|
||||
test: /\.m?js$|\.tsx?$/,
|
||||
exclude: [require.resolve("@mdi/js/mdi.js"), require.resolve("hls.js")],
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: [
|
||||
!latestBuild && [
|
||||
require("@babel/preset-env").default,
|
||||
{ modules: false },
|
||||
],
|
||||
[
|
||||
require("@babel/preset-typescript").default,
|
||||
{
|
||||
jsxPragma: "h",
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
plugins: [
|
||||
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
|
||||
[
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
{ loose: true, useBuiltIns: true },
|
||||
],
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/syntax-dynamic-import",
|
||||
[
|
||||
"@babel/transform-react-jsx",
|
||||
{
|
||||
pragma: "h",
|
||||
},
|
||||
],
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
require("@babel/plugin-proposal-decorators").default,
|
||||
{ decoratorsBeforeExport: true },
|
||||
],
|
||||
[
|
||||
require("@babel/plugin-proposal-class-properties").default,
|
||||
{ loose: true },
|
||||
],
|
||||
],
|
||||
},
|
||||
options: options({ latestBuild }),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -1,3 +1,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths.js");
|
||||
|
||||
module.exports = {
|
||||
isProdBuild() {
|
||||
return process.env.NODE_ENV === "production";
|
||||
@@ -5,10 +9,19 @@ module.exports = {
|
||||
isStatsBuild() {
|
||||
return process.env.STATS === "1";
|
||||
},
|
||||
isTravis() {
|
||||
return process.env.TRAVIS === "true";
|
||||
isTest() {
|
||||
return process.env.IS_TEST === "true";
|
||||
},
|
||||
isNetlify() {
|
||||
return process.env.NETLIFY === "true";
|
||||
},
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||
.match(/\d{8}\.\d+/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
return version[0];
|
||||
},
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ const envVars = require("../env");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gen-icons-json.js");
|
||||
require("./gather-static.js");
|
||||
require("./compress.js");
|
||||
require("./webpack.js");
|
||||
@@ -21,7 +21,7 @@ gulp.task(
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-service-worker-dev",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi"),
|
||||
"gen-icons-json",
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
"build-translations"
|
||||
@@ -38,11 +38,11 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static",
|
||||
"webpack-prod-app",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis() ? [] : ["compress-app"]),
|
||||
(envVars.isTest() ? [] : ["compress-app"]),
|
||||
gulp.parallel(
|
||||
"gen-pages-prod",
|
||||
"gen-index-app-prod",
|
||||
|
@@ -2,7 +2,6 @@ const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
@@ -15,12 +14,8 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel(
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-index-cast-dev",
|
||||
"build-translations"
|
||||
),
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static-cast",
|
||||
"webpack-dev-server-cast"
|
||||
)
|
||||
@@ -33,7 +28,8 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-cast",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static-cast",
|
||||
"webpack-prod-cast",
|
||||
"gen-index-cast-prod"
|
||||
|
@@ -3,7 +3,7 @@ const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gen-icons-json.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
@@ -16,13 +16,8 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel(
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-icons-demo",
|
||||
"gen-index-demo-dev",
|
||||
"build-translations"
|
||||
),
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"),
|
||||
"copy-static-demo",
|
||||
"webpack-dev-server-demo"
|
||||
)
|
||||
@@ -35,12 +30,9 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-demo",
|
||||
gulp.parallel(
|
||||
"gen-icons-app",
|
||||
"gen-icons-mdi",
|
||||
"gen-icons-demo",
|
||||
"build-translations"
|
||||
),
|
||||
// Cast needs to be backwards compatible and older HA has no translations
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static-demo",
|
||||
"webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
|
@@ -2,7 +2,7 @@ const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const mapStream = require("map-stream");
|
||||
|
||||
const inDir = "translations";
|
||||
const inDir = "translations/frontend";
|
||||
const downloadDir = inDir + "/downloads";
|
||||
|
||||
const tasks = [];
|
||||
@@ -12,7 +12,7 @@ function hasHtml(data) {
|
||||
}
|
||||
|
||||
function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
Object.keys(data).forEach(function(key) {
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (typeof data[key] === "object") {
|
||||
const nextRecKey = recKey ? `${recKey}.${key}` : key;
|
||||
recursiveCheckHasHtml(file, data[key], errors, nextRecKey);
|
||||
@@ -25,7 +25,7 @@ function recursiveCheckHasHtml(file, data, errors, recKey) {
|
||||
function checkHtml() {
|
||||
const errors = [];
|
||||
|
||||
return mapStream(function(file, cb) {
|
||||
return mapStream(function (file, cb) {
|
||||
const content = file.contents;
|
||||
let error;
|
||||
if (content) {
|
||||
@@ -42,19 +42,19 @@ function checkHtml() {
|
||||
}
|
||||
|
||||
let taskName = "clean-downloaded-translations";
|
||||
gulp.task(taskName, function() {
|
||||
gulp.task(taskName, function () {
|
||||
return del([`${downloadDir}/**`]);
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "check-translations-html";
|
||||
gulp.task(taskName, function() {
|
||||
gulp.task(taskName, function () {
|
||||
return gulp.src(`${downloadDir}/*.json`).pipe(checkHtml());
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
||||
taskName = "move-downloaded-translations";
|
||||
gulp.task(taskName, function() {
|
||||
gulp.task(taskName, function () {
|
||||
return gulp.src(`${downloadDir}/*.json`).pipe(gulp.dest(inDir));
|
||||
});
|
||||
tasks.push(taskName);
|
||||
|
@@ -47,11 +47,9 @@ gulp.task("gen-pages-dev", (done) => {
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: `/frontend_latest/${page}.js`,
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5PageJS: `/frontend_es5/${page}.js`,
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
});
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, `${page}.html`), content);
|
||||
@@ -66,11 +64,9 @@ gulp.task("gen-pages-prod", (done) => {
|
||||
for (const page of PAGES) {
|
||||
const content = renderTemplate(page, {
|
||||
latestPageJS: latestManifest[`${page}.js`],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5PageJS: es5Manifest[`${page}.js`],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
|
||||
fs.outputFileSync(
|
||||
@@ -88,13 +84,11 @@ gulp.task("gen-index-app-dev", (done) => {
|
||||
latestAppJS: "/frontend_latest/app.js",
|
||||
latestCoreJS: "/frontend_latest/core.js",
|
||||
latestCustomPanelJS: "/frontend_latest/custom-panel.js",
|
||||
latestHassIconsJS: "/frontend_latest/hass-icons.js",
|
||||
|
||||
es5Compatibility: "/frontend_es5/compatibility.js",
|
||||
es5AppJS: "/frontend_es5/app.js",
|
||||
es5CoreJS: "/frontend_es5/core.js",
|
||||
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
|
||||
es5HassIconsJS: "/frontend_es5/hass-icons.js",
|
||||
}).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
fs.outputFileSync(path.resolve(config.root, "index.html"), content);
|
||||
@@ -108,13 +102,11 @@ gulp.task("gen-index-app-prod", (done) => {
|
||||
latestAppJS: latestManifest["app.js"],
|
||||
latestCoreJS: latestManifest["core.js"],
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
latestHassIconsJS: latestManifest["hass-icons.js"],
|
||||
|
||||
es5Compatibility: es5Manifest["compatibility.js"],
|
||||
es5AppJS: es5Manifest["app.js"],
|
||||
es5CoreJS: es5Manifest["core.js"],
|
||||
es5CustomPanelJS: es5Manifest["custom-panel.js"],
|
||||
es5HassIconsJS: es5Manifest["hass-icons.js"],
|
||||
});
|
||||
const minified = minifyHtml(content).replace(/#THEMEC/g, "{{ theme_color }}");
|
||||
|
||||
|
@@ -3,7 +3,7 @@ const gulp = require("gulp");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gen-icons-json.js");
|
||||
require("./gather-static.js");
|
||||
require("./webpack.js");
|
||||
require("./service-worker.js");
|
||||
@@ -16,7 +16,8 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"gen-index-gallery-dev",
|
||||
"webpack-dev-server-gallery"
|
||||
@@ -30,7 +31,8 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-gallery",
|
||||
gulp.parallel("gen-icons-app", "gen-icons-mdi", "build-translations"),
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
"copy-static-gallery",
|
||||
"webpack-prod-gallery",
|
||||
"gen-index-gallery-prod"
|
||||
|
@@ -26,6 +26,13 @@ function copyTranslations(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
function copyMdiIcons(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// MDI icons output
|
||||
fs.copySync(polyPath("build/mdi"), staticPath("mdi"));
|
||||
}
|
||||
|
||||
function copyPolyfills(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
@@ -73,19 +80,15 @@ gulp.task("copy-translations", (done) => {
|
||||
|
||||
gulp.task("copy-static", (done) => {
|
||||
const staticDir = paths.static;
|
||||
const staticPath = genStaticPath(paths.static);
|
||||
// Basic static files
|
||||
fs.copySync(polyPath("public"), paths.root);
|
||||
|
||||
copyPolyfills(staticDir);
|
||||
copyFonts(staticDir);
|
||||
copyTranslations(staticDir);
|
||||
copyMdiIcons(staticDir);
|
||||
|
||||
// Panel assets
|
||||
copyFileDir(
|
||||
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
|
||||
staticPath("panels/calendar/")
|
||||
);
|
||||
copyMapPanel(staticDir);
|
||||
done();
|
||||
});
|
||||
@@ -103,6 +106,7 @@ gulp.task("copy-static-demo", (done) => {
|
||||
copyMapPanel(paths.demo_static);
|
||||
copyFonts(paths.demo_static);
|
||||
copyTranslations(paths.demo_static);
|
||||
copyMdiIcons(paths.demo_static);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -115,6 +119,7 @@ gulp.task("copy-static-cast", (done) => {
|
||||
copyMapPanel(paths.cast_static);
|
||||
copyFonts(paths.cast_static);
|
||||
copyTranslations(paths.cast_static);
|
||||
copyMdiIcons(paths.cast_static);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -127,5 +132,6 @@ gulp.task("copy-static-gallery", (done) => {
|
||||
copyMapPanel(paths.gallery_static);
|
||||
copyFonts(paths.gallery_static);
|
||||
copyTranslations(paths.gallery_static);
|
||||
copyMdiIcons(paths.gallery_static);
|
||||
done();
|
||||
});
|
||||
|
112
build-scripts/gulp/gen-icons-json.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const gulp = require("gulp");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const hash = require("object-hash");
|
||||
|
||||
const ICON_PACKAGE_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../../node_modules/@mdi/svg/"
|
||||
);
|
||||
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
|
||||
const PACKAGE_PATH = path.resolve(ICON_PACKAGE_PATH, "package.json");
|
||||
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
|
||||
const OUTPUT_DIR = path.resolve(__dirname, "../../build/mdi");
|
||||
|
||||
const encoding = "utf8";
|
||||
|
||||
const getMeta = () => {
|
||||
const file = fs.readFileSync(META_PATH, { encoding });
|
||||
const meta = JSON.parse(file);
|
||||
return meta.map((icon) => {
|
||||
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
|
||||
encoding,
|
||||
});
|
||||
return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name };
|
||||
});
|
||||
};
|
||||
|
||||
const splitBySize = (meta) => {
|
||||
const chunks = [];
|
||||
const CHUNK_SIZE = 50000;
|
||||
|
||||
let curSize = 0;
|
||||
let startKey;
|
||||
let icons = [];
|
||||
|
||||
Object.values(meta).forEach((icon) => {
|
||||
if (startKey === undefined) {
|
||||
startKey = icon.name;
|
||||
}
|
||||
curSize += icon.path.length;
|
||||
icons.push(icon);
|
||||
if (curSize > CHUNK_SIZE) {
|
||||
chunks.push({
|
||||
startKey,
|
||||
endKey: icon.name,
|
||||
icons,
|
||||
});
|
||||
curSize = 0;
|
||||
startKey = undefined;
|
||||
icons = [];
|
||||
}
|
||||
});
|
||||
|
||||
chunks.push({
|
||||
startKey,
|
||||
icons,
|
||||
});
|
||||
|
||||
return chunks;
|
||||
};
|
||||
|
||||
const findDifferentiator = (curString, prevString) => {
|
||||
for (let i = 0; i < curString.length; i++) {
|
||||
if (curString[i] !== prevString[i]) {
|
||||
return curString.substring(0, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error("Cannot find differentiator", curString, prevString);
|
||||
};
|
||||
|
||||
gulp.task("gen-icons-json", (done) => {
|
||||
const meta = getMeta();
|
||||
const split = splitBySize(meta);
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
const parts = [];
|
||||
|
||||
let lastEnd;
|
||||
split.forEach((chunk) => {
|
||||
let startKey;
|
||||
if (lastEnd === undefined) {
|
||||
chunk.startKey = undefined;
|
||||
startKey = undefined;
|
||||
} else {
|
||||
startKey = findDifferentiator(chunk.startKey, lastEnd);
|
||||
}
|
||||
lastEnd = chunk.endKey;
|
||||
|
||||
const output = {};
|
||||
chunk.icons.forEach((icon) => {
|
||||
output[icon.name] = icon.path;
|
||||
});
|
||||
const filename = hash(output);
|
||||
parts.push({ start: startKey, file: filename });
|
||||
fs.writeFileSync(
|
||||
path.resolve(OUTPUT_DIR, `${filename}.json`),
|
||||
JSON.stringify(output)
|
||||
);
|
||||
});
|
||||
|
||||
const file = fs.readFileSync(PACKAGE_PATH, { encoding });
|
||||
const package = JSON.parse(file);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(OUTPUT_DIR, "iconMetadata.json"),
|
||||
JSON.stringify({ version: package.version, parts })
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
@@ -1,127 +0,0 @@
|
||||
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,
|
||||
"../../node_modules/@mdi/svg/"
|
||||
);
|
||||
const META_PATH = path.resolve(ICON_PACKAGE_PATH, "meta.json");
|
||||
const ICON_PATH = path.resolve(ICON_PACKAGE_PATH, "svg");
|
||||
const OUTPUT_DIR = path.resolve(__dirname, "../../build");
|
||||
const MDI_OUTPUT_PATH = path.resolve(OUTPUT_DIR, "mdi.html");
|
||||
const HASS_OUTPUT_PATH = path.resolve(OUTPUT_DIR, "hass-icons.html");
|
||||
|
||||
const BUILT_IN_PANEL_ICONS = [
|
||||
"calendar", // Calendar
|
||||
"settings", // Config
|
||||
"home-assistant", // Hass.io
|
||||
"poll-box", // History panel
|
||||
"format-list-bulleted-type", // Logbook
|
||||
"mailbox", // Mailbox
|
||||
"tooltip-account", // Map
|
||||
"cart", // Shopping List
|
||||
"hammer", // developer-tools
|
||||
];
|
||||
|
||||
// Given an icon name, load the SVG file
|
||||
function loadIcon(name) {
|
||||
const iconPath = path.resolve(ICON_PATH, `${name}.svg`);
|
||||
try {
|
||||
return fs.readFileSync(iconPath, "utf-8");
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Given an SVG file, convert it to an iron-iconset-svg definition
|
||||
function transformXMLtoPolymer(name, xml) {
|
||||
const start = xml.indexOf("><path") + 1;
|
||||
const end = xml.length - start - 6;
|
||||
const pth = xml.substr(start, end);
|
||||
return `<g id="${name}">${pth}</g>`;
|
||||
}
|
||||
|
||||
// Given an iconset name and icon names, generate a polymer iconset
|
||||
function generateIconset(iconsetName, iconNames) {
|
||||
const iconDefs = Array.from(iconNames)
|
||||
.map((name) => {
|
||||
const iconDef = loadIcon(name);
|
||||
if (!iconDef) {
|
||||
throw new Error(`Unknown icon referenced: ${name}`);
|
||||
}
|
||||
return transformXMLtoPolymer(name, iconDef);
|
||||
})
|
||||
.join("");
|
||||
return `<ha-iconset-svg name="${iconsetName}" size="24"><svg><defs>${iconDefs}</defs></svg></ha-iconset-svg>`;
|
||||
}
|
||||
|
||||
// Find all icons used by the project.
|
||||
function findIcons(searchPath, iconsetName) {
|
||||
const iconRegex = new RegExp(`${iconsetName}:[\\w-]+`, "g");
|
||||
const icons = new Set();
|
||||
function processFile(filename) {
|
||||
const content = fs.readFileSync(filename);
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
while ((match = iconRegex.exec(content))) {
|
||||
// strip off "hass:" and add to set
|
||||
icons.add(match[0].substr(iconsetName.length + 1));
|
||||
}
|
||||
}
|
||||
mapFiles(searchPath, ".js", processFile);
|
||||
mapFiles(searchPath, ".ts", processFile);
|
||||
return icons;
|
||||
}
|
||||
|
||||
gulp.task("gen-icons-mdi", (done) => {
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(path.resolve(ICON_PACKAGE_PATH, META_PATH), "UTF-8")
|
||||
);
|
||||
const iconNames = meta.map((iconInfo) => iconInfo.name);
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(MDI_OUTPUT_PATH, generateIconset("mdi", iconNames));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-icons-app", (done) => {
|
||||
const iconNames = findIcons("./src", "hass");
|
||||
BUILT_IN_PANEL_ICONS.forEach((name) => iconNames.add(name));
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
}
|
||||
fs.writeFileSync(HASS_OUTPUT_PATH, generateIconset("hass", iconNames));
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-icons-demo", (done) => {
|
||||
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.demo_dir, "hademo-icons.html"),
|
||||
generateIconset("hademo", iconNames)
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-icons-hassio", (done) => {
|
||||
const iconNames = findIcons(
|
||||
path.resolve(paths.hassio_dir, "./src"),
|
||||
"hassio"
|
||||
);
|
||||
// Find hassio icons inside HA main repo.
|
||||
for (const item of findIcons(
|
||||
path.resolve(paths.polymer_dir, "./src"),
|
||||
"hassio"
|
||||
)) {
|
||||
iconNames.add(item);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.resolve(paths.hassio_dir, "hassio-icons.html"),
|
||||
generateIconset("hassio", iconNames)
|
||||
);
|
||||
done();
|
||||
});
|
@@ -3,7 +3,7 @@ const gulp = require("gulp");
|
||||
const envVars = require("../env");
|
||||
|
||||
require("./clean.js");
|
||||
require("./gen-icons.js");
|
||||
require("./gen-icons-json.js");
|
||||
require("./webpack.js");
|
||||
require("./compress.js");
|
||||
|
||||
@@ -14,7 +14,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||
"gen-icons-json",
|
||||
"webpack-watch-hassio"
|
||||
)
|
||||
);
|
||||
@@ -26,9 +26,9 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
gulp.parallel("gen-icons-hassio", "gen-icons-mdi"),
|
||||
"gen-icons-json",
|
||||
"webpack-prod-hassio",
|
||||
...// Don't compress running tests
|
||||
(envVars.isTravis() ? [] : ["compress-hassio"])
|
||||
(envVars.isTest() ? [] : ["compress-hassio"])
|
||||
)
|
||||
);
|
||||
|
@@ -14,13 +14,20 @@ const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
const inDir = "translations";
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
const workDir = "build-translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
let mergeBackend = false;
|
||||
|
||||
String.prototype.rsplit = function(sep, maxsplit) {
|
||||
gulp.task("translations-enable-merge-backend", (done) => {
|
||||
mergeBackend = true;
|
||||
done();
|
||||
});
|
||||
|
||||
String.prototype.rsplit = function (sep, maxsplit) {
|
||||
var split = this.split(sep);
|
||||
return maxsplit
|
||||
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
|
||||
@@ -45,7 +52,7 @@ const TRANSLATION_FRAGMENTS = [
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (typeof data[key] === "object") {
|
||||
output = {
|
||||
...output,
|
||||
@@ -107,7 +114,12 @@ function lokaliseTransform(data, original, file) {
|
||||
output[key] = lokaliseTransform(value, original, file);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (match, key) => {
|
||||
const replace = key.split("::").reduce((tr, k) => tr[k], original);
|
||||
const replace = key.split("::").reduce((tr, k) => {
|
||||
if (!tr) {
|
||||
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
||||
}
|
||||
return tr[k];
|
||||
}, original);
|
||||
if (typeof replace !== "string") {
|
||||
throw Error(`Invalid key placeholder ${key} in ${file.path}`);
|
||||
}
|
||||
@@ -118,7 +130,7 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
gulp.task("clean-translations", function() {
|
||||
gulp.task("clean-translations", function () {
|
||||
return del([workDir]);
|
||||
});
|
||||
|
||||
@@ -129,7 +141,7 @@ gulp.task("ensure-translations-build-dir", (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("create-test-metadata", function(cb) {
|
||||
gulp.task("create-test-metadata", function (cb) {
|
||||
fs.writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({
|
||||
@@ -147,7 +159,7 @@ gulp.task(
|
||||
return gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
transform(function (data, file) {
|
||||
return recursiveEmpty(data);
|
||||
})
|
||||
)
|
||||
@@ -165,28 +177,40 @@ gulp.task(
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
gulp.task("build-master-translation", function() {
|
||||
gulp.task("build-master-translation", function () {
|
||||
const src = [path.join(paths.translations_src, "en.json")];
|
||||
|
||||
if (mergeBackend) {
|
||||
src.push(path.join(inBackendDir, "en.json"));
|
||||
}
|
||||
|
||||
return gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.src(src)
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
transform(function (data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(rename("translationMaster.json"))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: "translationMaster.json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir));
|
||||
});
|
||||
|
||||
gulp.task("build-merged-translations", function() {
|
||||
gulp.task("build-merged-translations", function () {
|
||||
return gulp
|
||||
.src([inDir + "/*.json", workDir + "/test.json"], { allowEmpty: true })
|
||||
.src([inFrontendDir + "/*.json", workDir + "/test.json"], {
|
||||
allowEmpty: true,
|
||||
})
|
||||
.pipe(
|
||||
transform(function(data, file) {
|
||||
transform(function (data, file) {
|
||||
return lokaliseTransform(data, data, file);
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
foreach(function(stream, file) {
|
||||
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
|
||||
@@ -202,7 +226,10 @@ gulp.task("build-merged-translations", function() {
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inDir + "/" + lang + ".json");
|
||||
src.push(inFrontendDir + "/" + lang + ".json");
|
||||
if (mergeBackend) {
|
||||
src.push(inBackendDir + "/" + lang + ".json");
|
||||
}
|
||||
}
|
||||
}
|
||||
return gulp
|
||||
@@ -223,7 +250,7 @@ var taskName;
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(taskName, function() {
|
||||
gulp.task(taskName, function () {
|
||||
// Return only the translations for this fragment.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
@@ -242,12 +269,12 @@ TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(taskName, function() {
|
||||
gulp.task(taskName, function () {
|
||||
// Remove the fragment translations from the core translation.
|
||||
return gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
transform((data, file) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
@@ -259,7 +286,7 @@ gulp.task(taskName, function() {
|
||||
|
||||
splitTasks.push(taskName);
|
||||
|
||||
gulp.task("build-flattened-translations", function() {
|
||||
gulp.task("build-flattened-translations", function () {
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
return gulp
|
||||
.src(
|
||||
@@ -269,7 +296,7 @@ gulp.task("build-flattened-translations", function() {
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
transform(function (data) {
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
return flatten(data);
|
||||
})
|
||||
@@ -351,7 +378,7 @@ gulp.task(
|
||||
)
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform(function(data) {
|
||||
transform(function (data) {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
|
@@ -28,7 +28,7 @@ const runDevServer = ({
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase,
|
||||
}).listen(port, listenHost, function(err) {
|
||||
}).listen(port, listenHost, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
@@ -1,20 +1,12 @@
|
||||
const webpack = require("webpack");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const paths = require("./paths.js");
|
||||
const env = require("./env.js");
|
||||
const { babelLoaderConfig } = require("./babel.js");
|
||||
|
||||
let version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
|
||||
.match(/\d{8}\.\d+/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
version = version[0];
|
||||
|
||||
const createWebpackConfig = ({
|
||||
entry,
|
||||
outputRoot,
|
||||
@@ -38,17 +30,11 @@ const createWebpackConfig = ({
|
||||
test: /\.css$/,
|
||||
use: "raw-loader",
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
exportAsEs6Default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
esprima: "esprima",
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
@@ -68,8 +54,9 @@ const createWebpackConfig = ({
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(version),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__DEMO__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development"
|
||||
@@ -96,14 +83,6 @@ const createWebpackConfig = ({
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
react: "preact-compat",
|
||||
"react-dom": "preact-compat",
|
||||
// Not necessary unless you consume a module using `createClass`
|
||||
"create-react-class": "preact-compat/lib/create-react-class",
|
||||
// Not necessary unless you consume a module requiring `react-dom-factories`
|
||||
"react-dom-factories": "preact-compat/lib/react-dom-factories",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
filename: ({ chunk }) => {
|
||||
@@ -136,7 +115,6 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
core: "./src/entrypoints/core.ts",
|
||||
compatibility: "./src/entrypoints/compatibility.ts",
|
||||
"custom-panel": "./src/entrypoints/custom-panel.ts",
|
||||
"hass-icons": "./src/entrypoints/hass-icons.ts",
|
||||
},
|
||||
outputRoot: paths.root,
|
||||
isProdBuild,
|
||||
@@ -198,7 +176,7 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
|
||||
},
|
||||
outputRoot: paths.demo_root,
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`DEMO-${version}`),
|
||||
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
||||
__DEMO__: true,
|
||||
},
|
||||
isProdBuild,
|
||||
@@ -221,6 +199,9 @@ const createCastConfig = ({ isProdBuild, latestBuild }) => {
|
||||
outputRoot: paths.cast_root,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
defineOverlay: {
|
||||
__BACKWARDS_COMPAT__: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import "../../../src/resources/ha-style";
|
||||
import "../../../src/resources/hass-icons";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-connect";
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "@polymer/iron-icon";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||
@@ -32,6 +31,7 @@ import {
|
||||
import "../../../../src/layouts/loading-screen";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import "./hc-layout";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@@ -82,7 +82,7 @@ class HcCast extends LitElement {
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<mwc-button raised @click=${this._handleLaunch}>
|
||||
<iron-icon icon="hass:cast"></iron-icon>
|
||||
<ha-icon icon="hass:cast"></ha-icon>
|
||||
Start Casting
|
||||
</mwc-button>
|
||||
</p>
|
||||
@@ -120,7 +120,7 @@ class HcCast extends LitElement {
|
||||
${this.castManager.status
|
||||
? html`
|
||||
<mwc-button @click=${this._handleLaunch}>
|
||||
<iron-icon icon="hass:cast-connected"></iron-icon>
|
||||
<ha-icon icon="hass:cast-connected"></ha-icon>
|
||||
Manage
|
||||
</mwc-button>
|
||||
`
|
||||
@@ -242,7 +242,7 @@ class HcCast extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
mwc-button iron-icon {
|
||||
mwc-button ha-icon {
|
||||
margin-right: 8px;
|
||||
height: 18px;
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
Auth,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
loadTokens,
|
||||
saveTokens,
|
||||
} from "../../../../src/common/auth/token_storage";
|
||||
import "../../../../src/components/ha-icon";
|
||||
import "../../../../src/layouts/loading-screen";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
import "./hc-layout";
|
||||
@@ -136,11 +136,11 @@ export class HcConnect extends LitElement {
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._handleDemo}>
|
||||
Show Demo
|
||||
<iron-icon
|
||||
<ha-icon
|
||||
.icon=${this.castManager.castState === "CONNECTED"
|
||||
? "hass:cast-connected"
|
||||
: "hass:cast"}
|
||||
></iron-icon>
|
||||
></ha-icon>
|
||||
</mwc-button>
|
||||
<div class="spacer"></div>
|
||||
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
|
||||
@@ -316,7 +316,7 @@ export class HcConnect extends LitElement {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
mwc-button iron-icon {
|
||||
mwc-button ha-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
@@ -90,15 +90,17 @@ export class HcMain extends HassElement {
|
||||
super.firstUpdated(changedProps);
|
||||
import("../second-load");
|
||||
window.addEventListener("location-changed", () => {
|
||||
if (location.pathname.startsWith("/lovelace/")) {
|
||||
this._lovelacePath = location.pathname.substr(10);
|
||||
const panelPath = `/${this._urlPath || "lovelace"}/`;
|
||||
if (location.pathname.startsWith(panelPath)) {
|
||||
this._lovelacePath = location.pathname.substr(panelPath.length);
|
||||
this._sendStatus();
|
||||
}
|
||||
});
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const panelPath = `/${this._urlPath || "lovelace"}/`;
|
||||
const href = isNavigationClick(ev);
|
||||
if (href && href.startsWith("/lovelace/")) {
|
||||
this._lovelacePath = href.substr(10);
|
||||
if (href && href.startsWith(panelPath)) {
|
||||
this._lovelacePath = href.substr(panelPath.length);
|
||||
this._sendStatus();
|
||||
}
|
||||
});
|
||||
@@ -170,10 +172,10 @@ export class HcMain extends HassElement {
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
return;
|
||||
}
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
}
|
||||
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
}
|
||||
this._urlPath = msg.urlPath;
|
||||
if (this._unsubLovelace) {
|
||||
this._unsubLovelace();
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import "../../../src/resources/hass-icons";
|
||||
import "../../../src/resources/roboto";
|
||||
import "./layout/hc-lovelace";
|
||||
|
@@ -33,7 +33,7 @@ export const demoThemeJimpower = () => ({
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "var(--primary-color)",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"paper-card-background-color": "#434954",
|
||||
"card-background-color": "#434954",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
|
@@ -34,7 +34,7 @@ export const demoThemeKernehed = () => ({
|
||||
"label-badge-border-color": "green",
|
||||
"paper-listbox-color": "#777777",
|
||||
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
|
||||
"paper-card-background-color": "#292929",
|
||||
"card-background-color": "#292929",
|
||||
"label-badge-text-color": "var(--primary-text-color)",
|
||||
"paper-slider-knob-start-color": "var(--accent-color)",
|
||||
"switch-unchecked-track-color": "var(--disabled-text-color)",
|
||||
|
@@ -63,8 +63,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
elements: [
|
||||
{
|
||||
style: {
|
||||
"--iron-icon-width": "100px",
|
||||
"--iron-icon-height": "100px",
|
||||
"--mdc-icon-size": "100px",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
|
@@ -13,7 +13,7 @@ export const demoThemeTeachingbirds = () => ({
|
||||
"paper-listbox-color": "#FFFFFF",
|
||||
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
|
||||
"switch-unchecked-track-color": "var(--primary-text-color)",
|
||||
"paper-card-background-color": "#4e4e4e",
|
||||
"card-background-color": "#4e4e4e",
|
||||
"label-badge-text-color": "var(--text-primary-color)",
|
||||
"primary-background-color": "#303030",
|
||||
"sidebar-icon-color": "var(--paper-item-icon-color)",
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import "@polymer/paper-styles/typography";
|
||||
import "@polymer/polymer/lib/elements/dom-if";
|
||||
import "@polymer/polymer/lib/elements/dom-repeat";
|
||||
import "../../src/components/ha-iconset-svg";
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/hass-icons";
|
||||
import "../../src/resources/roboto";
|
||||
import "./ha-demo";
|
||||
import "./resources/hademo-icons";
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() => {
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import iconSetContent from "../../hademo-icons.html";
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
documentContainer.innerHTML = iconSetContent;
|
||||
document.head.appendChild(documentContainer.content);
|
@@ -17,6 +17,7 @@ export const mockLovelace = (
|
||||
);
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-view").then(() => {
|
||||
|
@@ -1,474 +1,10 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTranslations = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_translations", () => ({
|
||||
resources: {
|
||||
"component.lifx.config.abort.no_devices_found":
|
||||
"No LIFX devices found on the network.",
|
||||
"component.lifx.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of LIFX is possible.",
|
||||
"component.lifx.config.step.confirm.description":
|
||||
"Do you want to set up LIFX?",
|
||||
"component.lifx.config.step.confirm.title": "LIFX",
|
||||
"component.lifx.config.title": "LIFX",
|
||||
"component.hangouts.config.abort.already_configured":
|
||||
"Google Hangouts is already configured",
|
||||
"component.hangouts.config.abort.unknown": "Unknown error occurred.",
|
||||
"component.hangouts.config.error.invalid_2fa":
|
||||
"Invalid 2 Factor Authentication, please try again.",
|
||||
"component.hangouts.config.error.invalid_2fa_method":
|
||||
"Invalid 2FA Method (Verify on Phone).",
|
||||
"component.hangouts.config.error.invalid_login":
|
||||
"Invalid Login, please try again.",
|
||||
"component.hangouts.config.step.2fa.data.2fa": "2FA Pin",
|
||||
"component.hangouts.config.step.2fa.title": "2-Factor-Authentication",
|
||||
"component.hangouts.config.step.user.data.email": "E-Mail Address",
|
||||
"component.hangouts.config.step.user.data.password": "Password",
|
||||
"component.hangouts.config.step.user.title": "Google Hangouts Login",
|
||||
"component.hangouts.config.title": "Google Hangouts",
|
||||
"component.rainmachine.config.error.identifier_exists":
|
||||
"Account already registered",
|
||||
"component.rainmachine.config.error.invalid_credentials":
|
||||
"Invalid credentials",
|
||||
"component.rainmachine.config.step.user.data.ip_address":
|
||||
"Hostname or IP Address",
|
||||
"component.rainmachine.config.step.user.data.password": "Password",
|
||||
"component.rainmachine.config.step.user.data.port": "Port",
|
||||
"component.rainmachine.config.step.user.title":
|
||||
"Fill in your information",
|
||||
"component.rainmachine.config.title": "RainMachine",
|
||||
"component.homematicip_cloud.config.abort.already_configured":
|
||||
"Access point is already configured",
|
||||
"component.homematicip_cloud.config.abort.connection_aborted":
|
||||
"Could not connect to HMIP server",
|
||||
"component.homematicip_cloud.config.abort.unknown":
|
||||
"Unknown error occurred.",
|
||||
"component.homematicip_cloud.config.error.invalid_pin":
|
||||
"Invalid PIN, please try again.",
|
||||
"component.homematicip_cloud.config.error.press_the_button":
|
||||
"Please press the blue button.",
|
||||
"component.homematicip_cloud.config.error.register_failed":
|
||||
"Failed to register, please try again.",
|
||||
"component.homematicip_cloud.config.error.timeout_button":
|
||||
"Blue button press timeout, please try again.",
|
||||
"component.homematicip_cloud.config.step.init.data.hapid":
|
||||
"Access point ID (SGTIN)",
|
||||
"component.homematicip_cloud.config.step.init.data.name":
|
||||
"Name (optional, used as name prefix for all devices)",
|
||||
"component.homematicip_cloud.config.step.init.data.pin":
|
||||
"Pin Code (optional)",
|
||||
"component.homematicip_cloud.config.step.init.title":
|
||||
"Pick HomematicIP Access point",
|
||||
"component.homematicip_cloud.config.step.link.description":
|
||||
"Press the blue button on the access point and the submit button to register HomematicIP with Home Assistant.\n\n",
|
||||
"component.homematicip_cloud.config.step.link.title": "Link Access point",
|
||||
"component.homematicip_cloud.config.title": "HomematicIP Cloud",
|
||||
"component.daikin.config.abort.already_configured":
|
||||
"Device is already configured",
|
||||
"component.daikin.config.abort.device_fail":
|
||||
"Unexpected error creating device.",
|
||||
"component.daikin.config.abort.device_timeout":
|
||||
"Timeout connecting to the device.",
|
||||
"component.daikin.config.step.user.data.host": "Host",
|
||||
"component.daikin.config.step.user.description":
|
||||
"Enter IP address of your Daikin AC.",
|
||||
"component.daikin.config.step.user.title": "Configure Daikin AC",
|
||||
"component.daikin.config.title": "Daikin AC",
|
||||
"component.unifi.config.abort.already_configured":
|
||||
"Controller site is already configured",
|
||||
"component.unifi.config.abort.user_privilege":
|
||||
"User needs to be administrator",
|
||||
"component.unifi.config.error.faulty_credentials": "Bad user credentials",
|
||||
"component.unifi.config.error.service_unavailable":
|
||||
"No service available",
|
||||
"component.unifi.config.step.user.data.host": "Host",
|
||||
"component.unifi.config.step.user.data.password": "Password",
|
||||
"component.unifi.config.step.user.data.port": "Port",
|
||||
"component.unifi.config.step.user.data.site": "Site ID",
|
||||
"component.unifi.config.step.user.data.username": "User name",
|
||||
"component.unifi.config.step.user.data.verify_ssl":
|
||||
"Controller using proper certificate",
|
||||
"component.unifi.config.step.user.title": "Set up UniFi Controller",
|
||||
"component.unifi.config.title": "UniFi Controller",
|
||||
"component.nest.config.abort.already_setup":
|
||||
"You can only configure a single Nest account.",
|
||||
"component.nest.config.abort.authorize_url_fail":
|
||||
"Unknown error generating an authorize url.",
|
||||
"component.nest.config.abort.authorize_url_timeout":
|
||||
"Timeout generating authorize url.",
|
||||
"component.nest.config.abort.no_flows":
|
||||
"You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/nest/).",
|
||||
"component.nest.config.error.internal_error":
|
||||
"Internal error validating code",
|
||||
"component.nest.config.error.invalid_code": "Invalid code",
|
||||
"component.nest.config.error.timeout": "Timeout validating code",
|
||||
"component.nest.config.error.unknown": "Unknown error validating code",
|
||||
"component.nest.config.step.init.data.flow_impl": "Provider",
|
||||
"component.nest.config.step.init.description":
|
||||
"Pick via which authentication provider you want to authenticate with Nest.",
|
||||
"component.nest.config.step.init.title": "Authentication Provider",
|
||||
"component.nest.config.step.link.data.code": "Pin code",
|
||||
"component.nest.config.step.link.description":
|
||||
"To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
|
||||
"component.nest.config.step.link.title": "Link Nest Account",
|
||||
"component.nest.config.title": "Nest",
|
||||
"component.mailgun.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
|
||||
"component.mailgun.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.mailgun.config.create_entry.default":
|
||||
"To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.",
|
||||
"component.mailgun.config.step.user.description":
|
||||
"Are you sure you want to set up Mailgun?",
|
||||
"component.mailgun.config.step.user.title": "Set up the Mailgun Webhook",
|
||||
"component.mailgun.config.title": "Mailgun",
|
||||
"component.tellduslive.config.abort.already_setup":
|
||||
"TelldusLive is already configured",
|
||||
"component.tellduslive.config.abort.authorize_url_fail":
|
||||
"Unknown error generating an authorize url.",
|
||||
"component.tellduslive.config.abort.authorize_url_timeout":
|
||||
"Timeout generating authorize url.",
|
||||
"component.tellduslive.config.abort.unknown": "Unknown error occurred",
|
||||
"component.tellduslive.config.error.auth_error":
|
||||
"Authentication error, please try again",
|
||||
"component.tellduslive.config.step.auth.description":
|
||||
"To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})",
|
||||
"component.tellduslive.config.step.auth.title":
|
||||
"Authenticate against TelldusLive",
|
||||
"component.tellduslive.config.step.user.data.host": "Host",
|
||||
"component.tellduslive.config.step.user.title": "Pick endpoint.",
|
||||
"component.tellduslive.config.title": "Telldus Live",
|
||||
"component.esphome.config.abort.already_configured":
|
||||
"ESP is already configured",
|
||||
"component.esphome.config.error.connection_error":
|
||||
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
|
||||
"component.esphome.config.error.invalid_password": "Invalid password!",
|
||||
"component.esphome.config.error.resolve_error":
|
||||
"Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips",
|
||||
"component.esphome.config.step.authenticate.data.password": "Password",
|
||||
"component.esphome.config.step.authenticate.description":
|
||||
"Please enter the password you set in your configuration.",
|
||||
"component.esphome.config.step.authenticate.title": "Enter Password",
|
||||
"component.esphome.config.step.user.data.host": "Host",
|
||||
"component.esphome.config.step.user.data.port": "Port",
|
||||
"component.esphome.config.step.user.description":
|
||||
"Please enter connection settings of your [ESPHome](https://esphomelib.com/) node.",
|
||||
"component.esphome.config.step.user.title": "ESPHome",
|
||||
"component.esphome.config.title": "ESPHome",
|
||||
"component.luftdaten.config.error.communication_error":
|
||||
"Unable to communicate with the Luftdaten API",
|
||||
"component.luftdaten.config.error.invalid_sensor":
|
||||
"Sensor not available or invalid",
|
||||
"component.luftdaten.config.error.sensor_exists":
|
||||
"Sensor already registered",
|
||||
"component.luftdaten.config.step.user.data.show_on_map": "Show on map",
|
||||
"component.luftdaten.config.step.user.data.station_id":
|
||||
"Luftdaten Sensor ID",
|
||||
"component.luftdaten.config.step.user.title": "Define Luftdaten",
|
||||
"component.luftdaten.config.title": "Luftdaten",
|
||||
"component.upnp.config.abort.already_configured":
|
||||
"UPnP/IGD is already configured",
|
||||
"component.upnp.config.abort.incomplete_device":
|
||||
"Ignoring incomplete UPnP device",
|
||||
"component.upnp.config.abort.no_devices_discovered":
|
||||
"No UPnP/IGDs discovered",
|
||||
"component.upnp.config.abort.no_devices_found":
|
||||
"No UPnP/IGD devices found on the network.",
|
||||
"component.upnp.config.abort.no_sensors_or_port_mapping":
|
||||
"Enable at least sensors or port mapping",
|
||||
"component.upnp.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of UPnP/IGD is necessary.",
|
||||
"component.upnp.config.step.confirm.description":
|
||||
"Do you want to set up UPnP/IGD?",
|
||||
"component.upnp.config.step.confirm.title": "UPnP/IGD",
|
||||
"component.upnp.config.step.init.title": "UPnP/IGD",
|
||||
"component.upnp.config.step.user.data.enable_port_mapping":
|
||||
"Enable port mapping for Home Assistant",
|
||||
"component.upnp.config.step.user.data.enable_sensors":
|
||||
"Add traffic sensors",
|
||||
"component.upnp.config.step.user.data.igd": "UPnP/IGD",
|
||||
"component.upnp.config.step.user.title":
|
||||
"Configuration options for the UPnP/IGD",
|
||||
"component.upnp.config.title": "UPnP/IGD",
|
||||
"component.point.config.abort.already_setup":
|
||||
"You can only configure a Point account.",
|
||||
"component.point.config.abort.authorize_url_fail":
|
||||
"Unknown error generating an authorize url.",
|
||||
"component.point.config.abort.authorize_url_timeout":
|
||||
"Timeout generating authorize url.",
|
||||
"component.point.config.abort.external_setup":
|
||||
"Point successfully configured from another flow.",
|
||||
"component.point.config.abort.no_flows":
|
||||
"You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/).",
|
||||
"component.point.config.create_entry.default":
|
||||
"Successfully authenticated with Minut for your Point device(s)",
|
||||
"component.point.config.error.follow_link":
|
||||
"Please follow the link and authenticate before pressing Submit",
|
||||
"component.point.config.error.no_token": "Not authenticated with Minut",
|
||||
"component.point.config.step.auth.description":
|
||||
"Please follow the link below and <b>Accept</b> access to your Minut account, then come back and press <b>Submit</b> below.\n\n[Link]({authorization_url})",
|
||||
"component.point.config.step.auth.title": "Authenticate Point",
|
||||
"component.point.config.step.user.data.flow_impl": "Provider",
|
||||
"component.point.config.step.user.description":
|
||||
"Pick via which authentication provider you want to authenticate with Point.",
|
||||
"component.point.config.step.user.title": "Authentication Provider",
|
||||
"component.point.config.title": "Minut Point",
|
||||
"component.auth.mfa_setup.notify.abort.no_available_service":
|
||||
"No notification services available.",
|
||||
"component.auth.mfa_setup.notify.error.invalid_code":
|
||||
"Invalid code, please try again.",
|
||||
"component.auth.mfa_setup.notify.step.init.description":
|
||||
"Please select one of the notification services:",
|
||||
"component.auth.mfa_setup.notify.step.init.title":
|
||||
"Set up one-time password delivered by notify component",
|
||||
"component.auth.mfa_setup.notify.step.setup.description":
|
||||
"A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
|
||||
"component.auth.mfa_setup.notify.step.setup.title": "Verify setup",
|
||||
"component.auth.mfa_setup.notify.title": "Notify One-Time Password",
|
||||
"component.auth.mfa_setup.totp.error.invalid_code":
|
||||
"Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate.",
|
||||
"component.auth.mfa_setup.totp.step.init.description":
|
||||
"To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
|
||||
"component.auth.mfa_setup.totp.step.init.title":
|
||||
"Set up two-factor authentication using TOTP",
|
||||
"component.auth.mfa_setup.totp.title": "TOTP",
|
||||
"component.emulated_roku.config.abort.name_exists": "Name already exists",
|
||||
"component.emulated_roku.config.step.user.data.advertise_ip":
|
||||
"Advertise IP",
|
||||
"component.emulated_roku.config.step.user.data.advertise_port":
|
||||
"Advertise port",
|
||||
"component.emulated_roku.config.step.user.data.host_ip": "Host IP",
|
||||
"component.emulated_roku.config.step.user.data.listen_port":
|
||||
"Listen port",
|
||||
"component.emulated_roku.config.step.user.data.name": "Name",
|
||||
"component.emulated_roku.config.step.user.data.upnp_bind_multicast":
|
||||
"Bind multicast (True/False)",
|
||||
"component.emulated_roku.config.step.user.title":
|
||||
"Define server configuration",
|
||||
"component.emulated_roku.config.title": "EmulatedRoku",
|
||||
"component.owntracks.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.owntracks.config.create_entry.default":
|
||||
"\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `<Your name>`\n - Device ID: `<Your device name>`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `<Your name>`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information.",
|
||||
"component.owntracks.config.step.user.description":
|
||||
"Are you sure you want to set up OwnTracks?",
|
||||
"component.owntracks.config.step.user.title": "Set up OwnTracks",
|
||||
"component.owntracks.config.title": "OwnTracks",
|
||||
"component.zone.config.error.name_exists": "Name already exists",
|
||||
"component.zone.config.step.init.data.icon": "Icon",
|
||||
"component.zone.config.step.init.data.latitude": "Latitude",
|
||||
"component.zone.config.step.init.data.longitude": "Longitude",
|
||||
"component.zone.config.step.init.data.name": "Name",
|
||||
"component.zone.config.step.init.data.passive": "Passive",
|
||||
"component.zone.config.step.init.data.radius": "Radius",
|
||||
"component.zone.config.step.init.title": "Define zone parameters",
|
||||
"component.zone.config.title": "Zone",
|
||||
"component.hue.config.abort.all_configured":
|
||||
"All Philips Hue bridges are already configured",
|
||||
"component.hue.config.abort.already_configured":
|
||||
"Bridge is already configured",
|
||||
"component.hue.config.abort.cannot_connect":
|
||||
"Unable to connect to the bridge",
|
||||
"component.hue.config.abort.discover_timeout":
|
||||
"Unable to discover Hue bridges",
|
||||
"component.hue.config.abort.no_bridges":
|
||||
"No Philips Hue bridges discovered",
|
||||
"component.hue.config.abort.unknown": "Unknown error occurred",
|
||||
"component.hue.config.error.linking": "Unknown linking error occurred.",
|
||||
"component.hue.config.error.register_failed":
|
||||
"Failed to register, please try again",
|
||||
"component.hue.config.step.init.data.host": "Host",
|
||||
"component.hue.config.step.init.title": "Pick Hue bridge",
|
||||
"component.hue.config.step.link.description":
|
||||
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n",
|
||||
"component.hue.config.step.link.title": "Link Hub",
|
||||
"component.hue.config.title": "Philips Hue",
|
||||
"component.tradfri.config.abort.already_configured":
|
||||
"Bridge is already configured",
|
||||
"component.tradfri.config.error.cannot_connect":
|
||||
"Unable to connect to the gateway.",
|
||||
"component.tradfri.config.error.invalid_key":
|
||||
"Failed to register with provided key. If this keeps happening, try restarting the gateway.",
|
||||
"component.tradfri.config.error.timeout": "Timeout validating the code.",
|
||||
"component.tradfri.config.step.auth.data.host": "Host",
|
||||
"component.tradfri.config.step.auth.data.security_code": "Security Code",
|
||||
"component.tradfri.config.step.auth.description":
|
||||
"You can find the security code on the back of your gateway.",
|
||||
"component.tradfri.config.step.auth.title": "Enter security code",
|
||||
"component.tradfri.config.title": "IKEA TRÅDFRI",
|
||||
"component.mqtt.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of MQTT is allowed.",
|
||||
"component.mqtt.config.error.cannot_connect":
|
||||
"Unable to connect to the broker.",
|
||||
"component.mqtt.config.step.broker.data.broker": "Broker",
|
||||
"component.mqtt.config.step.broker.data.discovery": "Enable discovery",
|
||||
"component.mqtt.config.step.broker.data.password": "Password",
|
||||
"component.mqtt.config.step.broker.data.port": "Port",
|
||||
"component.mqtt.config.step.broker.data.username": "Username",
|
||||
"component.mqtt.config.step.broker.description":
|
||||
"Please enter the connection information of your MQTT broker.",
|
||||
"component.mqtt.config.step.broker.title": "MQTT",
|
||||
"component.mqtt.config.step.hassio_confirm.data.discovery":
|
||||
"Enable discovery",
|
||||
"component.mqtt.config.step.hassio_confirm.description":
|
||||
"Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?",
|
||||
"component.mqtt.config.step.hassio_confirm.title":
|
||||
"MQTT Broker via Hass.io add-on",
|
||||
"component.mqtt.config.title": "MQTT",
|
||||
"component.geofency.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
|
||||
"component.geofency.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.geofency.config.create_entry.default":
|
||||
"To send events to Home Assistant, you will need to setup the webhook feature in Geofency.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
|
||||
"component.geofency.config.step.user.description":
|
||||
"Are you sure you want to set up the Geofency Webhook?",
|
||||
"component.geofency.config.step.user.title":
|
||||
"Set up the Geofency Webhook",
|
||||
"component.geofency.config.title": "Geofency Webhook",
|
||||
"component.simplisafe.config.error.identifier_exists":
|
||||
"Account already registered",
|
||||
"component.simplisafe.config.error.invalid_credentials":
|
||||
"Invalid credentials",
|
||||
"component.simplisafe.config.step.user.data.code":
|
||||
"Code (for Home Assistant)",
|
||||
"component.simplisafe.config.step.user.data.password": "Password",
|
||||
"component.simplisafe.config.step.user.data.username": "Email Address",
|
||||
"component.simplisafe.config.step.user.title": "Fill in your information",
|
||||
"component.simplisafe.config.title": "SimpliSafe",
|
||||
"component.dialogflow.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages.",
|
||||
"component.dialogflow.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.dialogflow.config.create_entry.default":
|
||||
"To send events to Home Assistant, you will need to setup [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details.",
|
||||
"component.dialogflow.config.step.user.description":
|
||||
"Are you sure you want to set up Dialogflow?",
|
||||
"component.dialogflow.config.step.user.title":
|
||||
"Set up the Dialogflow Webhook",
|
||||
"component.dialogflow.config.title": "Dialogflow",
|
||||
"component.deconz.config.abort.already_configured":
|
||||
"Bridge is already configured",
|
||||
"component.deconz.config.abort.no_bridges":
|
||||
"No deCONZ bridges discovered",
|
||||
"component.deconz.config.abort.one_instance_only":
|
||||
"Component only supports one deCONZ instance",
|
||||
"component.deconz.config.error.no_key": "Couldn't get an API key",
|
||||
"component.deconz.config.step.init.data.host": "Host",
|
||||
"component.deconz.config.step.init.data.port": "Port",
|
||||
"component.deconz.config.step.init.title": "Define deCONZ gateway",
|
||||
"component.deconz.config.step.link.description":
|
||||
'Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press "Unlock Gateway" button',
|
||||
"component.deconz.config.step.link.title": "Link with deCONZ",
|
||||
"component.deconz.config.step.options.data.allow_clip_sensor":
|
||||
"Allow importing virtual sensors",
|
||||
"component.deconz.config.step.options.data.allow_deconz_groups":
|
||||
"Allow importing deCONZ groups",
|
||||
"component.deconz.config.step.options.title":
|
||||
"Extra configuration options for deCONZ",
|
||||
"component.deconz.config.title": "deCONZ Zigbee gateway",
|
||||
"component.openuv.config.error.identifier_exists":
|
||||
"Coordinates already registered",
|
||||
"component.openuv.config.error.invalid_api_key": "Invalid API key",
|
||||
"component.openuv.config.step.user.data.api_key": "OpenUV API Key",
|
||||
"component.openuv.config.step.user.data.elevation": "Elevation",
|
||||
"component.openuv.config.step.user.data.latitude": "Latitude",
|
||||
"component.openuv.config.step.user.data.longitude": "Longitude",
|
||||
"component.openuv.config.step.user.title": "Fill in your information",
|
||||
"component.openuv.config.title": "OpenUV",
|
||||
"component.locative.config.title": "Locative Webhook",
|
||||
"component.locative.config.step.user.title":
|
||||
"Set up the Locative Webhook",
|
||||
"component.locative.config.step.user.description":
|
||||
"Are you sure you want to set up the Locative Webhook?",
|
||||
"component.locative.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.locative.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency.",
|
||||
"component.locative.config.create_entry.default":
|
||||
"To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
|
||||
"component.ios.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of Home Assistant iOS is necessary.",
|
||||
"component.ios.config.step.confirm.description":
|
||||
"Do you want to set up the Home Assistant iOS component?",
|
||||
"component.ios.config.step.confirm.title": "Home Assistant iOS",
|
||||
"component.ios.config.title": "Home Assistant iOS",
|
||||
"component.smhi.config.error.name_exists": "Name already exists",
|
||||
"component.smhi.config.error.wrong_location": "Location Sweden only",
|
||||
"component.smhi.config.step.user.data.latitude": "Latitude",
|
||||
"component.smhi.config.step.user.data.longitude": "Longitude",
|
||||
"component.smhi.config.step.user.data.name": "Name",
|
||||
"component.smhi.config.step.user.title": "Location in Sweden",
|
||||
"component.smhi.config.title": "Swedish weather service (SMHI)",
|
||||
"component.sonos.config.abort.no_devices_found":
|
||||
"No Sonos devices found on the network.",
|
||||
"component.sonos.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of Sonos is necessary.",
|
||||
"component.sonos.config.step.confirm.description":
|
||||
"Do you want to set up Sonos?",
|
||||
"component.sonos.config.step.confirm.title": "Sonos",
|
||||
"component.sonos.config.title": "Sonos",
|
||||
"component.ifttt.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.",
|
||||
"component.ifttt.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.ifttt.config.create_entry.default":
|
||||
'To send events to Home Assistant, you will need to use the "Make a web request" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.',
|
||||
"component.ifttt.config.step.user.description":
|
||||
"Are you sure you want to set up IFTTT?",
|
||||
"component.ifttt.config.step.user.title":
|
||||
"Set up the IFTTT Webhook Applet",
|
||||
"component.ifttt.config.title": "IFTTT",
|
||||
"component.twilio.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages.",
|
||||
"component.twilio.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.twilio.config.create_entry.default":
|
||||
"To send events to Home Assistant, you will need to setup [Webhooks with Twilio]({twilio_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data.",
|
||||
"component.twilio.config.step.user.description":
|
||||
"Are you sure you want to set up Twilio?",
|
||||
"component.twilio.config.step.user.title": "Set up the Twilio Webhook",
|
||||
"component.twilio.config.title": "Twilio",
|
||||
"component.zha.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of ZHA is allowed.",
|
||||
"component.zha.config.error.cannot_connect":
|
||||
"Unable to connect to ZHA device.",
|
||||
"component.zha.config.step.user.data.radio_type": "Radio Type",
|
||||
"component.zha.config.step.user.data.usb_path": "USB Device Path",
|
||||
"component.zha.config.step.user.title": "ZHA",
|
||||
"component.zha.config.title": "ZHA",
|
||||
"component.gpslogger.config.title": "GPSLogger Webhook",
|
||||
"component.gpslogger.config.step.user.title":
|
||||
"Set up the GPSLogger Webhook",
|
||||
"component.gpslogger.config.step.user.description":
|
||||
"Are you sure you want to set up the GPSLogger Webhook?",
|
||||
"component.gpslogger.config.abort.one_instance_allowed":
|
||||
"Only a single instance is necessary.",
|
||||
"component.gpslogger.config.abort.not_internet_accessible":
|
||||
"Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger.",
|
||||
"component.gpslogger.config.create_entry.default":
|
||||
"To send events to Home Assistant, you will need to setup the webhook feature in GPSLogger.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details.",
|
||||
"component.zwave.config.abort.already_configured":
|
||||
"Z-Wave is already configured",
|
||||
"component.zwave.config.abort.one_instance_only":
|
||||
"Component only supports one Z-Wave instance",
|
||||
"component.zwave.config.error.option_error":
|
||||
"Z-Wave validation failed. Is the path to the USB stick correct?",
|
||||
"component.zwave.config.step.user.data.network_key":
|
||||
"Network Key (leave blank to auto-generate)",
|
||||
"component.zwave.config.step.user.data.usb_path": "USB Path",
|
||||
"component.zwave.config.step.user.description":
|
||||
"See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables",
|
||||
"component.zwave.config.step.user.title": "Set up Z-Wave",
|
||||
"component.zwave.config.title": "Z-Wave",
|
||||
"component.cast.config.abort.no_devices_found":
|
||||
"No Google Cast devices found on the network.",
|
||||
"component.cast.config.abort.single_instance_allowed":
|
||||
"Only a single configuration of Google Cast is necessary.",
|
||||
"component.cast.config.step.confirm.description":
|
||||
"Do you want to set up Google Cast?",
|
||||
"component.cast.config.step.confirm.title": "Google Cast",
|
||||
"component.cast.config.title": "Google Cast",
|
||||
},
|
||||
}));
|
||||
hass.mockWS(
|
||||
"frontend/get_translations",
|
||||
(/* msg: {language: string, category: string} */) => {
|
||||
return { resources: {} };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@@ -50,10 +50,9 @@ const CONFIGS = [
|
||||
top: 12%
|
||||
left: 6%
|
||||
transform: rotate(-60deg) scaleX(-1)
|
||||
--iron-icon-height: 30px
|
||||
--iron-icon-width: 30px
|
||||
--iron-icon-stroke-color: black
|
||||
--iron-icon-fill-color: rgba(50, 50, 50, .75)
|
||||
--mdc-icon-size: 30px
|
||||
--mdc-icon-stroke-color: black
|
||||
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
|
||||
- type: image
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
@@ -99,10 +98,9 @@ const CONFIGS = [
|
||||
top: 12%
|
||||
left: 6%
|
||||
transform: rotate(-60deg) scaleX(-1)
|
||||
--iron-icon-height: 30px
|
||||
--iron-icon-width: 30px
|
||||
--iron-icon-stroke-color: black
|
||||
--iron-icon-fill-color: rgba(50, 50, 50, .75)
|
||||
--mdc-icon-size: 30px
|
||||
--mdc-icon-stroke-color: black
|
||||
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
|
||||
- type: image
|
||||
entity: light.bed_light
|
||||
tap_action:
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import "@polymer/paper-styles/typography";
|
||||
import "@polymer/polymer/lib/elements/dom-if";
|
||||
import "@polymer/polymer/lib/elements/dom-repeat";
|
||||
import "../../src/components/ha-iconset-svg";
|
||||
import "../../src/resources/ha-style";
|
||||
import "../../src/resources/hass-icons";
|
||||
import "../../src/resources/roboto";
|
||||
import "./ha-gallery";
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-icon";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
@@ -28,7 +28,7 @@ class HaGallery extends PolymerElement {
|
||||
app-header-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
paper-icon-button.invisible {
|
||||
ha-icon-button.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -67,11 +67,11 @@ class HaGallery extends PolymerElement {
|
||||
<app-header-layout>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
<ha-icon-button
|
||||
icon="hass:arrow-left"
|
||||
on-click="_backTapped"
|
||||
class$='[[_computeHeaderButtonClass(_demo)]]'
|
||||
></paper-icon-button>
|
||||
></ha-icon-button>
|
||||
<div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
@@ -98,7 +98,7 @@ class HaGallery extends PolymerElement {
|
||||
<a href='#[[item]]'>
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
@@ -114,7 +114,7 @@ class HaGallery extends PolymerElement {
|
||||
<a href='#[[item]]'>
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
@@ -130,7 +130,7 @@ class HaGallery extends PolymerElement {
|
||||
<a href='#[[item]]'>
|
||||
<paper-item>
|
||||
<paper-item-body>{{ item }}</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
|
1
hassio/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
hassio-icons.html
|
||||
|
@@ -41,13 +41,19 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repo = this.repo;
|
||||
const addons = this._getAddons(this.addons, this.filter);
|
||||
let _addons = this.addons;
|
||||
if (!this.hass.userData?.showAdvanced) {
|
||||
_addons = _addons.filter((addon) => {
|
||||
return !addon.advanced;
|
||||
});
|
||||
}
|
||||
const addons = this._getAddons(_addons, this.filter);
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
No results found in "${repo.name}"
|
||||
No results found in "${repo.name}."
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -57,66 +63,55 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
<h1>
|
||||
${repo.name}
|
||||
</h1>
|
||||
<p class="description">
|
||||
Maintained by ${repo.maintainer}<br />
|
||||
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
|
||||
${repo.url}
|
||||
</a>
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
${addon.advanced && !this.hass.userData?.showAdvanced
|
||||
? ""
|
||||
: html`
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed &&
|
||||
addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle"}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed"
|
||||
: addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system"}
|
||||
.iconClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`}
|
||||
<paper-card
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed && addon.installed !== addon.version
|
||||
? "hassio:arrow-up-bold-circle"
|
||||
: "hassio:puzzle"}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed"
|
||||
: addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system"}
|
||||
.iconClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
@@ -12,11 +12,16 @@ import {
|
||||
HassioAddonRepository,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/layouts/loading-screen";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-search-input";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import "../../../src/common/search/search-input";
|
||||
import "./hassio-addon-repository";
|
||||
import "./hassio-repositories-editor";
|
||||
|
||||
import { supervisorTabs } from "../hassio-panel";
|
||||
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
@@ -35,11 +40,15 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
};
|
||||
|
||||
class HassioAddonStore extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() private _addons?: HassioAddonInfo[];
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() private _repos?: HassioAddonRepository[];
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) private _addons?: HassioAddonInfo[];
|
||||
|
||||
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
|
||||
|
||||
@property() private _filter?: string;
|
||||
|
||||
@@ -52,42 +61,86 @@ class HassioAddonStore extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addons || !this._repos) {
|
||||
return html` <loading-screen></loading-screen> `;
|
||||
}
|
||||
const repos: TemplateResult[] = [];
|
||||
|
||||
for (const repo of this._repos) {
|
||||
const addons = this._addons!.filter(
|
||||
(addon) => addon.repository === repo.slug
|
||||
);
|
||||
if (this._repos) {
|
||||
for (const repo of this._repos) {
|
||||
const addons = this._addons!.filter(
|
||||
(addon) => addon.repository === repo.slug
|
||||
);
|
||||
|
||||
if (addons.length === 0) {
|
||||
continue;
|
||||
if (addons.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
repos.push(html`
|
||||
<hassio-addon-repository
|
||||
.hass=${this.hass}
|
||||
.repo=${repo}
|
||||
.addons=${addons}
|
||||
.filter=${this._filter!}
|
||||
></hassio-addon-repository>
|
||||
`);
|
||||
}
|
||||
|
||||
repos.push(html`
|
||||
<hassio-addon-repository
|
||||
.hass=${this.hass}
|
||||
.repo=${repo}
|
||||
.addons=${addons}
|
||||
.filter=${this._filter}
|
||||
></hassio-addon-repository>
|
||||
`);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hassio-repositories-editor
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.repos=${this._repos}
|
||||
></hassio-repositories-editor>
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
hassio
|
||||
main-page
|
||||
.tabs=${supervisorTabs}
|
||||
>
|
||||
<span slot="header">Add-on store</span>
|
||||
<paper-menu-button
|
||||
close-on-activate
|
||||
no-animations
|
||||
horizontal-align="right"
|
||||
horizontal-offset="-5"
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button
|
||||
icon="hassio:dots-vertical"
|
||||
slot="dropdown-trigger"
|
||||
alt="menu"
|
||||
></ha-icon-button>
|
||||
<paper-listbox slot="dropdown-content" role="listbox">
|
||||
<paper-item @tap=${this._manageRepositories}>
|
||||
Repositories
|
||||
</paper-item>
|
||||
<paper-item @tap=${this.refreshData}>
|
||||
Reload
|
||||
</paper-item>
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
${repos.length === 0
|
||||
? html`<loading-screen></loading-screen>`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
no-label-float
|
||||
no-underline
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
</div>
|
||||
|
||||
<hassio-search-input
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></hassio-search-input>
|
||||
|
||||
${repos}
|
||||
${repos}
|
||||
`}
|
||||
${!this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="advanced">
|
||||
Missing add-ons? Enable advanced mode on
|
||||
<a href="/profile" target="_top">
|
||||
your profile page
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -103,6 +156,13 @@ class HassioAddonStore extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _manageRepositories() {
|
||||
showRepositoriesDialog(this, {
|
||||
repos: this._repos!,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _loadData() {
|
||||
try {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
@@ -123,6 +183,25 @@ class HassioAddonStore extends LitElement {
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.search {
|
||||
padding: 0 16px;
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.search search-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.advanced {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.advanced a {
|
||||
margin-left: 0.5em;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,152 +0,0 @@
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { repeat } from "lit-html/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-repositories-editor")
|
||||
class HassioRepositoriesEditor extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public repos!: HassioAddonRepository[];
|
||||
|
||||
@property() private _repoUrl = "";
|
||||
|
||||
private _sortedRepos = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repos = this._sortedRepos(this.repos);
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
Repositories
|
||||
</h1>
|
||||
<p class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</p>
|
||||
<div class="card-group">
|
||||
${// Use repeat so that the fade-out from call-service-api-button
|
||||
// stays with the correct repo after we add/delete one.
|
||||
repeat(
|
||||
repos,
|
||||
(repo) => repo.slug,
|
||||
(repo) => html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${repo.name}
|
||||
.description=${repo.url}
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeRemoveRepoData(repos, repo.url)}
|
||||
class="warning"
|
||||
>
|
||||
Remove
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
.value=${this._repoUrl}
|
||||
@value-changed=${this._urlChanged}
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
path="hassio/supervisor/options"
|
||||
.hass=${this.hass}
|
||||
.data=${this.computeAddRepoData(repos, this._repoUrl)}
|
||||
>
|
||||
Add
|
||||
</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("repos")) {
|
||||
this._repoUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
private _urlChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._repoUrl = ev.detail.value;
|
||||
}
|
||||
|
||||
private computeRemoveRepoData(repoList, url) {
|
||||
const list = repoList
|
||||
.filter((repo) => repo.url !== url)
|
||||
.map((repo) => repo.source);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
private computeAddRepoData(repoList, url) {
|
||||
const list = repoList ? repoList.map((repo) => repo.source) : [];
|
||||
list.push(url);
|
||||
return { addons_repositories: list };
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
hassioStyle,
|
||||
css`
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-repositories-editor": HassioRepositoriesEditor;
|
||||
}
|
||||
}
|
@@ -18,20 +18,21 @@ import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioHardwareAudio,
|
||||
HassioHardwareAudioDevice,
|
||||
} from "../../../src/data/hassio/hardware";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
} from "../../../../src/data/hassio/hardware";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addon-audio")
|
||||
class HassioAddonAudio extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@@ -56,7 +57,7 @@ class HassioAddonAudio extends LitElement {
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="device"
|
||||
.selected=${this._selectedInput}
|
||||
.selected=${this._selectedInput!}
|
||||
>
|
||||
${this._inputDevices &&
|
||||
this._inputDevices.map((item) => {
|
||||
@@ -75,7 +76,7 @@ class HassioAddonAudio extends LitElement {
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="device"
|
||||
.selected=${this._selectedOutput}
|
||||
.selected=${this._selectedOutput!}
|
||||
>
|
||||
${this._outputDevices &&
|
||||
this._outputDevices.map((item) => {
|
||||
@@ -183,6 +184,9 @@ class HassioAddonAudio extends LitElement {
|
||||
} catch {
|
||||
this._error = "Failed to set addon audio device";
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
hassio/src/addon-view/config/hassio-addon-config-tab.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
|
||||
import "./hassio-addon-audio";
|
||||
import "./hassio-addon-config";
|
||||
import "./hassio-addon-network";
|
||||
|
||||
@customElement("hassio-addon-config-tab")
|
||||
class HassioAddonConfigDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-addon-config
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-config>
|
||||
${this.addon.network
|
||||
? html`
|
||||
<hassio-addon-network
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-network>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.audio
|
||||
? html`
|
||||
<hassio-addon-audio
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-audio>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-config-tab": HassioAddonConfigDashboard;
|
||||
}
|
||||
}
|
@@ -12,24 +12,26 @@ import {
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
|
||||
@customElement("hassio-addon-config")
|
||||
class HassioAddonConfig extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@@ -43,7 +45,8 @@ class HassioAddonConfig extends LitElement {
|
||||
const valid = editor ? editor.isValid : true;
|
||||
|
||||
return html`
|
||||
<paper-card heading="Config">
|
||||
<h1>${this.addon.name}</h1>
|
||||
<paper-card heading="Configuration">
|
||||
<div class="card-content">
|
||||
<ha-yaml-editor
|
||||
@value-changed=${this._configChanged}
|
||||
@@ -113,6 +116,7 @@ class HassioAddonConfig extends LitElement {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to reset all your options?",
|
||||
confirmText: "reset options",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -164,6 +168,9 @@ class HassioAddonConfig extends LitElement {
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,15 +10,17 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
setHassioAddonOption,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
interface NetworkItem {
|
||||
description: string;
|
||||
@@ -32,9 +34,9 @@ interface NetworkItemInput extends PaperInputElement {
|
||||
|
||||
@customElement("hassio-addon-network")
|
||||
class HassioAddonNetwork extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@@ -70,7 +72,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
<paper-input
|
||||
@value-changed=${this._configChanged}
|
||||
placeholder="disabled"
|
||||
.value=${item.host}
|
||||
.value=${String(item.host)}
|
||||
.container=${item.container}
|
||||
no-label-float
|
||||
></paper-input>
|
||||
@@ -165,6 +167,9 @@ class HassioAddonNetwork extends LitElement {
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveTapped(): Promise<void> {
|
||||
@@ -191,6 +196,9 @@ class HassioAddonNetwork extends LitElement {
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
if (!this._error && this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,92 @@
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
fetchHassioAddonDocumentation,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/layouts/loading-screen";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
|
||||
@customElement("hassio-addon-documentation-tab")
|
||||
class HassioAddonDocumentationDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@property() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<paper-card>
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<ha-markdown .content=${this._content}></ha-markdown>`
|
||||
: html`<loading-screen></loading-screen>`}
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
this._content = await fetchHassioAddonDocumentation(
|
||||
this.hass,
|
||||
this.addon!.slug
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon documentation, ${
|
||||
err.body?.message || err
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
|
||||
}
|
||||
}
|
185
hassio/src/addon-view/hassio-addon-dashboard.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./config/hassio-addon-audio";
|
||||
import "./config/hassio-addon-config";
|
||||
import "./info/hassio-addon-info";
|
||||
import "./log/hassio-addon-logs";
|
||||
import "./config/hassio-addon-network";
|
||||
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
|
||||
import "./hassio-addon-router";
|
||||
|
||||
@customElement("hassio-addon-dashboard")
|
||||
class HassioAddonDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
private _computeTail = memoizeOne((route: Route) => {
|
||||
const dividerPos = route.path.indexOf("/", 1);
|
||||
return dividerPos === -1
|
||||
? {
|
||||
prefix: route.prefix + route.path,
|
||||
path: "",
|
||||
}
|
||||
: {
|
||||
prefix: route.prefix + route.path.substr(0, dividerPos),
|
||||
path: route.path.substr(dividerPos),
|
||||
};
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
|
||||
const addonTabs: PageNavigation[] = [
|
||||
{
|
||||
name: "Info",
|
||||
path: `/hassio/addon/${this.addon.slug}/info`,
|
||||
icon: "hassio:information-variant",
|
||||
},
|
||||
];
|
||||
|
||||
if (this.addon.documentation) {
|
||||
addonTabs.push({
|
||||
name: "Documentation",
|
||||
path: `/hassio/addon/${this.addon.slug}/documentation`,
|
||||
icon: "hassio:file-document",
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addon.version) {
|
||||
addonTabs.push(
|
||||
{
|
||||
name: "Configuration",
|
||||
path: `/hassio/addon/${this.addon.slug}/config`,
|
||||
icon: "hassio:cogs",
|
||||
},
|
||||
{
|
||||
name: "Log",
|
||||
path: `/hassio/addon/${this.addon.slug}/logs`,
|
||||
icon: "hassio:math-log",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const route = this._computeTail(this.route);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
|
||||
.route=${route}
|
||||
hassio
|
||||
.tabs=${addonTabs}
|
||||
>
|
||||
<span slot="header">${this.addon.name}</span>
|
||||
<hassio-addon-router
|
||||
.route=${route}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-router>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
hassio-addon-logs {
|
||||
max-width: calc(100% - 8px);
|
||||
min-width: 600px;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config,
|
||||
hassio-addon-logs {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
await this._routeDataChanged(this.route);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private async _apiCalled(ev): Promise<void> {
|
||||
const path: string = ev.detail.path;
|
||||
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "uninstall") {
|
||||
history.back();
|
||||
} else {
|
||||
await this._routeDataChanged(this.route);
|
||||
}
|
||||
}
|
||||
|
||||
private async _routeDataChanged(routeData: Route): Promise<void> {
|
||||
const addon = routeData.path.split("/")[1];
|
||||
try {
|
||||
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||
this.addon = addoninfo;
|
||||
} catch {
|
||||
this.addon = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-dashboard": HassioAddonDashboard;
|
||||
}
|
||||
}
|
53
hassio/src/addon-view/hassio-addon-router.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../src/layouts/hass-router-page";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||
import "./info/hassio-addon-info-tab";
|
||||
import "./config/hassio-addon-config-tab";
|
||||
import "./log/hassio-addon-log-tab";
|
||||
import "./documentation/hassio-addon-documentation-tab";
|
||||
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
|
||||
@customElement("hassio-addon-router")
|
||||
class HassioAddonRouter extends HassRouterPage {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "info",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
info: {
|
||||
tag: "hassio-addon-info-tab",
|
||||
},
|
||||
documentation: {
|
||||
tag: "hassio-addon-documentation-tab",
|
||||
},
|
||||
config: {
|
||||
tag: "hassio-addon-config-tab",
|
||||
},
|
||||
logs: {
|
||||
tag: "hassio-addon-log-tab",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.addon = this.addon;
|
||||
el.narrow = this.narrow;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-router": HassioAddonRouter;
|
||||
}
|
||||
}
|
@@ -1,157 +0,0 @@
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./hassio-addon-audio";
|
||||
import "./hassio-addon-config";
|
||||
import "./hassio-addon-info";
|
||||
import "./hassio-addon-logs";
|
||||
import "./hassio-addon-network";
|
||||
|
||||
@customElement("hassio-addon-view")
|
||||
class HassioAddonView extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() public addon?: HassioAddonDetails;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
return html`
|
||||
<hass-subpage header="Hass.io: add-on details" hassio>
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-info>
|
||||
|
||||
${this.addon && this.addon.version
|
||||
? html`
|
||||
<hassio-addon-config
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-config>
|
||||
|
||||
${this.addon.audio
|
||||
? html`
|
||||
<hassio-addon-audio
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-audio>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.network
|
||||
? html`
|
||||
<hassio-addon-network
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-network>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<hassio-addon-logs
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-logs>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
hassio-addon-logs {
|
||||
max-width: calc(100% - 8px);
|
||||
min-width: 600px;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config,
|
||||
hassio-addon-logs {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
await this._routeDataChanged(this.route);
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private async _apiCalled(ev): Promise<void> {
|
||||
const path: string = ev.detail.path;
|
||||
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "uninstall") {
|
||||
history.back();
|
||||
} else {
|
||||
await this._routeDataChanged(this.route);
|
||||
}
|
||||
}
|
||||
|
||||
private async _routeDataChanged(routeData: Route): Promise<void> {
|
||||
const addon = routeData.path.substr(1);
|
||||
try {
|
||||
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||
this.addon = addoninfo;
|
||||
} catch {
|
||||
this.addon = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-view": HassioAddonView;
|
||||
}
|
||||
}
|
62
hassio/src/addon-view/info/hassio-addon-info-tab.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
|
||||
import "./hassio-addon-info";
|
||||
|
||||
@customElement("hassio-addon-info-tab")
|
||||
class HassioAddonInfoDashboard extends LitElement {
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-info>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-info-tab": HassioAddonInfoDashboard;
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
@@ -12,14 +11,15 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-label-badge";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-switch";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../../src/common/navigate";
|
||||
import "../../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-label-badge";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import "../../../../src/components/ha-icon";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
HassioAddonDetails,
|
||||
@@ -29,18 +29,29 @@ import {
|
||||
setHassioAddonOption,
|
||||
setHassioAddonSecurity,
|
||||
uninstallHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-card-content";
|
||||
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
const STAGE_ICON = {
|
||||
stable: "mdi:check-circle",
|
||||
experimental: "mdi:flask",
|
||||
deprecated: "mdi:exclamation-thick",
|
||||
};
|
||||
|
||||
const PERMIS_DESC = {
|
||||
stage: {
|
||||
title: "Add-on Stage",
|
||||
description: `Add-ons can have one of three stages:\n\n<ha-icon icon='${STAGE_ICON.stable}'></ha-icon>**Stable**: These are add-ons ready to be used in production.\n<ha-icon icon='${STAGE_ICON.experimental}'></ha-icon>**Experimental**: These may contain bugs, and may be unfinished.\n<ha-icon icon='${STAGE_ICON.deprecated}'></ha-icon>**Deprecated**: These add-ons will no longer receive any updates.`,
|
||||
},
|
||||
rating: {
|
||||
title: "Add-on Security Rating",
|
||||
description:
|
||||
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
|
||||
"Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
|
||||
},
|
||||
host_network: {
|
||||
title: "Host Network",
|
||||
@@ -58,19 +69,19 @@ const PERMIS_DESC = {
|
||||
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||
},
|
||||
hassio_api: {
|
||||
title: "Hass.io API Access",
|
||||
title: "Supervisor API Access",
|
||||
description:
|
||||
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
|
||||
"The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
|
||||
},
|
||||
docker_api: {
|
||||
title: "Full Docker Access",
|
||||
description:
|
||||
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||
},
|
||||
host_pid: {
|
||||
title: "Host Processes Namespace",
|
||||
description:
|
||||
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
|
||||
},
|
||||
apparmor: {
|
||||
title: "AppArmor",
|
||||
@@ -91,9 +102,11 @@ const PERMIS_DESC = {
|
||||
|
||||
@customElement("hassio-addon-info")
|
||||
class HassioAddonInfo extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@@ -158,51 +171,71 @@ class HassioAddonInfo extends LitElement {
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<div class="addon-header">
|
||||
${this.addon.name}
|
||||
${!this.narrow ? this.addon.name : ""}
|
||||
<div class="addon-version light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
${this.addon.version}
|
||||
${this._computeIsRunning
|
||||
? html`
|
||||
<iron-icon
|
||||
<ha-icon
|
||||
title="Add-on is running"
|
||||
class="running"
|
||||
icon="hassio:circle"
|
||||
></iron-icon>
|
||||
></ha-icon>
|
||||
`
|
||||
: html`
|
||||
<iron-icon
|
||||
<ha-icon
|
||||
title="Add-on is stopped"
|
||||
class="stopped"
|
||||
icon="hassio:circle"
|
||||
></iron-icon>
|
||||
></ha-icon>
|
||||
`}
|
||||
`
|
||||
: html` ${this.addon.version_latest} `}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
Current version: ${this.addon.version}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link">changelog</span>)
|
||||
</div>
|
||||
`
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}
|
||||
>Changelog</span
|
||||
>`}
|
||||
</div>
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
Visit
|
||||
<a href="${this.addon.url}" target="_blank" rel="noreferrer">
|
||||
<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"
|
||||
<img
|
||||
class="logo"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
|
||||
</a>
|
||||
src="/api/hassio/addons/${this.addon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
: ""}
|
||||
<div class="security">
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: this.addon.stage === "stable",
|
||||
yellow: this.addon.stage === "experimental",
|
||||
red: this.addon.stage === "deprecated",
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
.icon=${STAGE_ICON[this.addon.stage]}
|
||||
label="stage"
|
||||
description=""
|
||||
></ha-label-badge>
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
@@ -327,14 +360,18 @@ class HassioAddonInfo extends LitElement {
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this.addon.auto_update || this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<ha-switch
|
||||
@change=${this._autoUpdateToggled}
|
||||
.checked=${this.addon.auto_update}
|
||||
haptic
|
||||
></ha-switch>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.ingress
|
||||
? html`
|
||||
<div class="state">
|
||||
@@ -362,7 +399,7 @@ class HassioAddonInfo extends LitElement {
|
||||
<div>
|
||||
Protection mode
|
||||
<span>
|
||||
<iron-icon icon="hassio:information"></iron-icon>
|
||||
<ha-icon icon="hassio:information"></ha-icon>
|
||||
<paper-tooltip>
|
||||
Grant the add-on elevated system access.
|
||||
</paper-tooltip>
|
||||
@@ -383,29 +420,8 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="card-actions">
|
||||
${this.addon.version
|
||||
? html`
|
||||
<mwc-button class="warning" @click=${this._uninstallClicked}>
|
||||
Uninstall
|
||||
</mwc-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/rebuild"
|
||||
>
|
||||
Rebuild
|
||||
</ha-call-api-button>
|
||||
`
|
||||
: ""}
|
||||
${this._computeIsRunning
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/restart"
|
||||
>
|
||||
Restart
|
||||
</ha-call-api-button>
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
@@ -413,6 +429,13 @@ class HassioAddonInfo extends LitElement {
|
||||
>
|
||||
Stop
|
||||
</ha-call-api-button>
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/restart"
|
||||
>
|
||||
Restart
|
||||
</ha-call-api-button>
|
||||
`
|
||||
: html`
|
||||
<ha-call-api-button
|
||||
@@ -425,7 +448,7 @@ class HassioAddonInfo extends LitElement {
|
||||
${this._computeShowWebUI
|
||||
? html`
|
||||
<a
|
||||
.href=${this._pathWebui}
|
||||
href=${this._pathWebui!}
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
@@ -444,6 +467,23 @@ class HassioAddonInfo extends LitElement {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<mwc-button
|
||||
class=" right warning"
|
||||
@click=${this._uninstallClicked}
|
||||
>
|
||||
Uninstall
|
||||
</mwc-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning right"
|
||||
.hass=${this.hass}
|
||||
.path="hassio/addons/${this.addon.slug}/rebuild"
|
||||
>
|
||||
Rebuild
|
||||
</ha-call-api-button>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
${!this.addon.available
|
||||
@@ -506,6 +546,7 @@ class HassioAddonInfo extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: 8px;
|
||||
font-size: 24px;
|
||||
color: var(--paper-card-header-color, --primary-text-color);
|
||||
}
|
||||
@@ -521,7 +562,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.logo img {
|
||||
img.logo {
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
display: block;
|
||||
@@ -534,7 +575,7 @@ class HassioAddonInfo extends LitElement {
|
||||
width: 180px;
|
||||
display: inline-block;
|
||||
}
|
||||
.state iron-icon {
|
||||
.state ha-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -542,10 +583,10 @@ class HassioAddonInfo extends LitElement {
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
iron-icon.running {
|
||||
ha-icon.running {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.stopped {
|
||||
ha-icon.stopped {
|
||||
color: var(--google-red-300);
|
||||
}
|
||||
ha-call-api-button {
|
||||
@@ -590,7 +631,15 @@ class HassioAddonInfo extends LitElement {
|
||||
.security ha-label-badge {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
--iron-icon-height: 45px;
|
||||
--ha-label-badge-padding: 8px 0 0 0;
|
||||
}
|
||||
.changelog {
|
||||
display: contents;
|
||||
}
|
||||
.changelog-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -776,9 +825,17 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _uninstallClicked(): Promise<void> {
|
||||
if (!confirm("Are you sure you want to uninstall this add-on?")) {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.addon.name,
|
||||
text: "Are you sure you want to uninstall this add-on?",
|
||||
confirmText: "uninstall add-on",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
try {
|
||||
await uninstallHassioAddon(this.hass, this.addon.slug);
|
59
hassio/src/addon-view/log/hassio-addon-log-tab.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
|
||||
import "./hassio-addon-logs";
|
||||
|
||||
@customElement("hassio-addon-log-tab")
|
||||
class HassioAddonLogDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <paper-spinner-lite active></paper-spinner-lite> `;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-addon-logs
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
></hassio-addon-logs>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-log-tab": HassioAddonLogDashboard;
|
||||
}
|
||||
}
|
@@ -7,27 +7,26 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import {
|
||||
fetchHassioAddonLogs,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addon-logs")
|
||||
class HassioAddonLogs extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@query("#content") private _logContent!: any;
|
||||
@property() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
@@ -36,9 +35,16 @@ class HassioAddonLogs extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-card heading="Log">
|
||||
<h1>${this.addon.name}</h1>
|
||||
<paper-card>
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
<div class="card-content" id="content"></div>
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html
|
||||
.content=${this._content}
|
||||
></hassio-ansi-to-html>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
||||
</div>
|
||||
@@ -50,17 +56,11 @@ class HassioAddonLogs extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
ANSI_HTML_STYLE,
|
||||
css`
|
||||
:host,
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
@@ -72,11 +72,7 @@ class HassioAddonLogs extends LitElement {
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
const content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||
while (this._logContent.lastChild) {
|
||||
this._logContent.removeChild(this._logContent.lastChild as Node);
|
||||
}
|
||||
this._logContent.appendChild(parseTextToColoredPre(content));
|
||||
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
|
||||
}
|
@@ -1,223 +0,0 @@
|
||||
import { css } from "lit-element";
|
||||
|
||||
interface State {
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
underline: boolean;
|
||||
strikethrough: boolean;
|
||||
foregroundColor: null | string;
|
||||
backgroundColor: null | string;
|
||||
}
|
||||
|
||||
export const ANSI_HTML_STYLE = css`
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.underline.strikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
.fg-red {
|
||||
color: rgb(222, 56, 43);
|
||||
}
|
||||
.fg-green {
|
||||
color: rgb(57, 181, 74);
|
||||
}
|
||||
.fg-yellow {
|
||||
color: rgb(255, 199, 6);
|
||||
}
|
||||
.fg-blue {
|
||||
color: rgb(0, 111, 184);
|
||||
}
|
||||
.fg-magenta {
|
||||
color: rgb(118, 38, 113);
|
||||
}
|
||||
.fg-cyan {
|
||||
color: rgb(44, 181, 233);
|
||||
}
|
||||
.fg-white {
|
||||
color: rgb(204, 204, 204);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
.bg-red {
|
||||
background-color: rgb(222, 56, 43);
|
||||
}
|
||||
.bg-green {
|
||||
background-color: rgb(57, 181, 74);
|
||||
}
|
||||
.bg-yellow {
|
||||
background-color: rgb(255, 199, 6);
|
||||
}
|
||||
.bg-blue {
|
||||
background-color: rgb(0, 111, 184);
|
||||
}
|
||||
.bg-magenta {
|
||||
background-color: rgb(118, 38, 113);
|
||||
}
|
||||
.bg-cyan {
|
||||
background-color: rgb(44, 181, 233);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: rgb(204, 204, 204);
|
||||
}
|
||||
`;
|
||||
|
||||
export function parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
let i = 0;
|
||||
|
||||
const state: State = {
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
foregroundColor: null,
|
||||
backgroundColor: null,
|
||||
};
|
||||
|
||||
const addSpan = (content) => {
|
||||
const span = document.createElement("span");
|
||||
if (state.bold) {
|
||||
span.classList.add("bold");
|
||||
}
|
||||
if (state.italic) {
|
||||
span.classList.add("italic");
|
||||
}
|
||||
if (state.underline) {
|
||||
span.classList.add("underline");
|
||||
}
|
||||
if (state.strikethrough) {
|
||||
span.classList.add("strikethrough");
|
||||
}
|
||||
if (state.foregroundColor !== null) {
|
||||
span.classList.add(`fg-${state.foregroundColor}`);
|
||||
}
|
||||
if (state.backgroundColor !== null) {
|
||||
span.classList.add(`bg-${state.backgroundColor}`);
|
||||
}
|
||||
span.appendChild(document.createTextNode(content));
|
||||
pre.appendChild(span);
|
||||
};
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
const j = match!.index;
|
||||
addSpan(text.substring(i, j));
|
||||
i = j + match[0].length;
|
||||
|
||||
if (match[1] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match[1].split(";").forEach((colorCode: string) => {
|
||||
switch (parseInt(colorCode, 10)) {
|
||||
case 0:
|
||||
// reset
|
||||
state.bold = false;
|
||||
state.italic = false;
|
||||
state.underline = false;
|
||||
state.strikethrough = false;
|
||||
state.foregroundColor = null;
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
case 1:
|
||||
state.bold = true;
|
||||
break;
|
||||
case 3:
|
||||
state.italic = true;
|
||||
break;
|
||||
case 4:
|
||||
state.underline = true;
|
||||
break;
|
||||
case 9:
|
||||
state.strikethrough = true;
|
||||
break;
|
||||
case 22:
|
||||
state.bold = false;
|
||||
break;
|
||||
case 23:
|
||||
state.italic = false;
|
||||
break;
|
||||
case 24:
|
||||
state.underline = false;
|
||||
break;
|
||||
case 29:
|
||||
state.strikethrough = false;
|
||||
break;
|
||||
case 30:
|
||||
// foreground black
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 31:
|
||||
state.foregroundColor = "red";
|
||||
break;
|
||||
case 32:
|
||||
state.foregroundColor = "green";
|
||||
break;
|
||||
case 33:
|
||||
state.foregroundColor = "yellow";
|
||||
break;
|
||||
case 34:
|
||||
state.foregroundColor = "blue";
|
||||
break;
|
||||
case 35:
|
||||
state.foregroundColor = "magenta";
|
||||
break;
|
||||
case 36:
|
||||
state.foregroundColor = "cyan";
|
||||
break;
|
||||
case 37:
|
||||
state.foregroundColor = "white";
|
||||
break;
|
||||
case 39:
|
||||
// foreground reset
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 40:
|
||||
state.backgroundColor = "black";
|
||||
break;
|
||||
case 41:
|
||||
state.backgroundColor = "red";
|
||||
break;
|
||||
case 42:
|
||||
state.backgroundColor = "green";
|
||||
break;
|
||||
case 43:
|
||||
state.backgroundColor = "yellow";
|
||||
break;
|
||||
case 44:
|
||||
state.backgroundColor = "blue";
|
||||
break;
|
||||
case 45:
|
||||
state.backgroundColor = "magenta";
|
||||
break;
|
||||
case 46:
|
||||
state.backgroundColor = "cyan";
|
||||
break;
|
||||
case 47:
|
||||
state.backgroundColor = "white";
|
||||
break;
|
||||
case 49:
|
||||
// background reset
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
addSpan(text.substring(i));
|
||||
|
||||
return pre;
|
||||
}
|
253
hassio/src/components/hassio-ansi-to-html.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
interface State {
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
underline: boolean;
|
||||
strikethrough: boolean;
|
||||
foregroundColor: null | string;
|
||||
backgroundColor: null | string;
|
||||
}
|
||||
|
||||
@customElement("hassio-ansi-to-html")
|
||||
class HassioAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
return html`${this._parseTextToColoredPre(this.content)}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.underline.strikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
.fg-red {
|
||||
color: rgb(222, 56, 43);
|
||||
}
|
||||
.fg-green {
|
||||
color: rgb(57, 181, 74);
|
||||
}
|
||||
.fg-yellow {
|
||||
color: rgb(255, 199, 6);
|
||||
}
|
||||
.fg-blue {
|
||||
color: rgb(0, 111, 184);
|
||||
}
|
||||
.fg-magenta {
|
||||
color: rgb(118, 38, 113);
|
||||
}
|
||||
.fg-cyan {
|
||||
color: rgb(44, 181, 233);
|
||||
}
|
||||
.fg-white {
|
||||
color: rgb(204, 204, 204);
|
||||
}
|
||||
.bg-black {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
.bg-red {
|
||||
background-color: rgb(222, 56, 43);
|
||||
}
|
||||
.bg-green {
|
||||
background-color: rgb(57, 181, 74);
|
||||
}
|
||||
.bg-yellow {
|
||||
background-color: rgb(255, 199, 6);
|
||||
}
|
||||
.bg-blue {
|
||||
background-color: rgb(0, 111, 184);
|
||||
}
|
||||
.bg-magenta {
|
||||
background-color: rgb(118, 38, 113);
|
||||
}
|
||||
.bg-cyan {
|
||||
background-color: rgb(44, 181, 233);
|
||||
}
|
||||
.bg-white {
|
||||
background-color: rgb(204, 204, 204);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
let i = 0;
|
||||
|
||||
const state: State = {
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
foregroundColor: null,
|
||||
backgroundColor: null,
|
||||
};
|
||||
|
||||
const addSpan = (content) => {
|
||||
const span = document.createElement("span");
|
||||
if (state.bold) {
|
||||
span.classList.add("bold");
|
||||
}
|
||||
if (state.italic) {
|
||||
span.classList.add("italic");
|
||||
}
|
||||
if (state.underline) {
|
||||
span.classList.add("underline");
|
||||
}
|
||||
if (state.strikethrough) {
|
||||
span.classList.add("strikethrough");
|
||||
}
|
||||
if (state.foregroundColor !== null) {
|
||||
span.classList.add(`fg-${state.foregroundColor}`);
|
||||
}
|
||||
if (state.backgroundColor !== null) {
|
||||
span.classList.add(`bg-${state.backgroundColor}`);
|
||||
}
|
||||
span.appendChild(document.createTextNode(content));
|
||||
pre.appendChild(span);
|
||||
};
|
||||
|
||||
/* eslint-disable no-cond-assign */
|
||||
let match;
|
||||
// eslint-disable-next-line
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
const j = match!.index;
|
||||
addSpan(text.substring(i, j));
|
||||
i = j + match[0].length;
|
||||
|
||||
if (match[1] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match[1].split(";").forEach((colorCode: string) => {
|
||||
switch (parseInt(colorCode, 10)) {
|
||||
case 0:
|
||||
// reset
|
||||
state.bold = false;
|
||||
state.italic = false;
|
||||
state.underline = false;
|
||||
state.strikethrough = false;
|
||||
state.foregroundColor = null;
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
case 1:
|
||||
state.bold = true;
|
||||
break;
|
||||
case 3:
|
||||
state.italic = true;
|
||||
break;
|
||||
case 4:
|
||||
state.underline = true;
|
||||
break;
|
||||
case 9:
|
||||
state.strikethrough = true;
|
||||
break;
|
||||
case 22:
|
||||
state.bold = false;
|
||||
break;
|
||||
case 23:
|
||||
state.italic = false;
|
||||
break;
|
||||
case 24:
|
||||
state.underline = false;
|
||||
break;
|
||||
case 29:
|
||||
state.strikethrough = false;
|
||||
break;
|
||||
case 30:
|
||||
// foreground black
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 31:
|
||||
state.foregroundColor = "red";
|
||||
break;
|
||||
case 32:
|
||||
state.foregroundColor = "green";
|
||||
break;
|
||||
case 33:
|
||||
state.foregroundColor = "yellow";
|
||||
break;
|
||||
case 34:
|
||||
state.foregroundColor = "blue";
|
||||
break;
|
||||
case 35:
|
||||
state.foregroundColor = "magenta";
|
||||
break;
|
||||
case 36:
|
||||
state.foregroundColor = "cyan";
|
||||
break;
|
||||
case 37:
|
||||
state.foregroundColor = "white";
|
||||
break;
|
||||
case 39:
|
||||
// foreground reset
|
||||
state.foregroundColor = null;
|
||||
break;
|
||||
case 40:
|
||||
state.backgroundColor = "black";
|
||||
break;
|
||||
case 41:
|
||||
state.backgroundColor = "red";
|
||||
break;
|
||||
case 42:
|
||||
state.backgroundColor = "green";
|
||||
break;
|
||||
case 43:
|
||||
state.backgroundColor = "yellow";
|
||||
break;
|
||||
case 44:
|
||||
state.backgroundColor = "blue";
|
||||
break;
|
||||
case 45:
|
||||
state.backgroundColor = "magenta";
|
||||
break;
|
||||
case 46:
|
||||
state.backgroundColor = "cyan";
|
||||
break;
|
||||
case 47:
|
||||
state.backgroundColor = "white";
|
||||
break;
|
||||
case 49:
|
||||
// background reset
|
||||
state.backgroundColor = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
addSpan(text.substring(i));
|
||||
|
||||
return pre;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ansi-to-html": HassioAnsiToHtml;
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../src/components/ha-relative-time";
|
||||
import "../../../src/components/ha-icon";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("hassio-card-content")
|
||||
@@ -48,11 +48,11 @@ class HassioCardContent extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<iron-icon
|
||||
<ha-icon
|
||||
class=${this.iconClass}
|
||||
.icon=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></iron-icon>
|
||||
></ha-icon>
|
||||
`}
|
||||
<div>
|
||||
<div class="title">
|
||||
@@ -78,25 +78,25 @@ class HassioCardContent extends LitElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
iron-icon {
|
||||
ha-icon {
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
ha-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
ha-icon.running,
|
||||
ha-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
ha-icon.hassupdate,
|
||||
ha-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
iron-icon.not_available {
|
||||
ha-icon.not_available {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.title {
|
||||
|
@@ -1,82 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("hassio-search-input")
|
||||
class HassioSearchInput extends LitElement {
|
||||
@property() private filter?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="search-container">
|
||||
<paper-input
|
||||
label="Search"
|
||||
.value=${this.filter}
|
||||
@value-changed=${this._filterInputChanged}
|
||||
>
|
||||
<iron-icon
|
||||
icon="hassio:magnify"
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
></iron-icon>
|
||||
${this.filter &&
|
||||
html`
|
||||
<paper-icon-button
|
||||
slot="suffix"
|
||||
class="suffix"
|
||||
@click=${this._clearSearch}
|
||||
icon="hassio:close"
|
||||
alt="Clear"
|
||||
title="Clear"
|
||||
></paper-icon-button>
|
||||
`}
|
||||
</paper-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 16px;
|
||||
}
|
||||
.search-container {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.prefix {
|
||||
margin: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-search-input": HassioSearchInput;
|
||||
}
|
||||
}
|
@@ -96,7 +96,7 @@ class HassioAddons extends LitElement {
|
||||
}
|
||||
|
||||
private _addonTapped(ev: any): void {
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
|
||||
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||
}
|
||||
|
||||
private _openStore(): void {
|
||||
|
@@ -13,34 +13,51 @@ import {
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import "./hassio-addons";
|
||||
import "./hassio-update";
|
||||
|
||||
import { supervisorTabs } from "../hassio-panel";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-update>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.addons=${this.supervisorInfo.addons}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
hassio
|
||||
main-page
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
>
|
||||
<span slot="header">Dashboard</span>
|
||||
<div class="content">
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-update>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.addons=${this.supervisorInfo.addons}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import {
|
||||
css,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import "../../../src/components/ha-icon";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
@@ -112,7 +112,7 @@ export class HassioUpdate extends LitElement {
|
||||
${icon
|
||||
? html`
|
||||
<div class="icon">
|
||||
<iron-icon .icon=${icon}></iron-icon>
|
||||
<ha-icon .icon=${icon}></ha-icon>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -158,15 +158,16 @@ export class HassioUpdate extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
.icon {
|
||||
--iron-icon-height: 48px;
|
||||
--iron-icon-width: 48px;
|
||||
--mdc-icon-size: 48px;
|
||||
float: right;
|
||||
margin: 0 0 2px 10px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.update-heading {
|
||||
font-size: var(--paper-font-subhead_-_font-size);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--secondary-text-color);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -36,10 +36,10 @@ class HassioMarkdownDialog extends LitElement {
|
||||
return html`
|
||||
<ha-paper-dialog id="dialog" with-backdrop="">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
<ha-icon-button
|
||||
icon="hassio:close"
|
||||
dialog-dismiss=""
|
||||
></paper-icon-button>
|
||||
></ha-icon-button>
|
||||
<div main-title="">${this.title}</div>
|
||||
</app-toolbar>
|
||||
<paper-dialog-scrollable>
|
||||
|
232
hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import {
|
||||
HassioAddonRepository,
|
||||
fetchHassioAddonsInfo,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
|
||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
private _dialogParams?: HassioRepositoryDialogParams;
|
||||
|
||||
@query("#repository_input") private _optionInput?: PaperInputElement;
|
||||
|
||||
@property() private _opened = false;
|
||||
|
||||
@property() private _prosessing = false;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
public async showDialog(_dialogParams: any): Promise<void> {
|
||||
this._dialogParams = _dialogParams;
|
||||
this._repos = _dialogParams.repos;
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
}
|
||||
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repositories = this._filteredRepositories(this._repos);
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closing=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
heading="Manage add-on repositories"
|
||||
>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="form">
|
||||
${repositories.length
|
||||
? repositories.map((repo) => {
|
||||
return html`
|
||||
<paper-item class="option">
|
||||
<paper-item-body three-line>
|
||||
<div>${repo.name}</div>
|
||||
<div secondary>${repo.maintainer}</div>
|
||||
<div secondary>${repo.url}</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-button
|
||||
.slug=${repo.slug}
|
||||
title="Remove"
|
||||
@click=${this._removeRepository}
|
||||
icon="hassio:delete"
|
||||
></ha-icon-button>
|
||||
</paper-item>
|
||||
`;
|
||||
})
|
||||
: html`
|
||||
<paper-item>
|
||||
No repositories
|
||||
</paper-item>
|
||||
`}
|
||||
<div class="layout horizontal bottom">
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="repository_input"
|
||||
label="Add repository"
|
||||
@keydown=${this._handleKeyAdd}
|
||||
></paper-input>
|
||||
<mwc-button @click=${this._addRepository}>
|
||||
${this._prosessing
|
||||
? html`<paper-spinner active></paper-spinner>`
|
||||
: "Add"}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
|
||||
Close
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.option {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() =>
|
||||
(this.shadowRoot?.querySelector(
|
||||
"[dialogInitialFocus]"
|
||||
) as HTMLElement)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyAdd(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
this._addRepository();
|
||||
}
|
||||
|
||||
private async _addRepository() {
|
||||
const input = this._optionInput;
|
||||
if (!input || !input.value) {
|
||||
return;
|
||||
}
|
||||
this._prosessing = true;
|
||||
const repositories = this._filteredRepositories(this._repos);
|
||||
const newRepositories = repositories.map((repo) => {
|
||||
return repo.source;
|
||||
});
|
||||
newRepositories.push(input.value);
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._repos = addonsInfo.repositories;
|
||||
|
||||
await this._dialogParams!.loadData();
|
||||
|
||||
input.value = "";
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
}
|
||||
this._prosessing = false;
|
||||
}
|
||||
|
||||
private async _removeRepository(ev: Event) {
|
||||
const slug = (ev.target as any).slug;
|
||||
const repositories = this._filteredRepositories(this._repos);
|
||||
const repository = repositories.find((repo) => {
|
||||
return repo.slug === slug;
|
||||
});
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
const newRepositories = repositories
|
||||
.map((repo) => {
|
||||
return repo.source;
|
||||
})
|
||||
.filter((repo) => {
|
||||
return repo !== repository.source;
|
||||
});
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._repos = addonsInfo.repositories;
|
||||
|
||||
await this._dialogParams!.loadData();
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-repositories": HassioRepositoriesDialog;
|
||||
}
|
||||
}
|
22
hassio/src/dialogs/repositories/show-dialog-repositories.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "./dialog-hassio-repositories";
|
||||
import { HassioAddonRepository } from "../../../../src/data/hassio/addon";
|
||||
|
||||
export interface HassioRepositoryDialogParams {
|
||||
repos: HassioAddonRepository[];
|
||||
loadData: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showRepositoriesDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioRepositoryDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-repositories",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,10 +1,10 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import { PaperDialogElement } from "@polymer/paper-dialog";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-icon";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
@@ -119,10 +119,10 @@ class HassioSnapshotDialog extends LitElement {
|
||||
.on-iron-overlay-closed=${this._dialogClosed}
|
||||
>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
<ha-icon-button
|
||||
icon="hassio:close"
|
||||
dialog-dismiss=""
|
||||
></paper-icon-button>
|
||||
></ha-icon-button>
|
||||
<div main-title="">${this._computeName}</div>
|
||||
</app-toolbar>
|
||||
<div class="details">
|
||||
@@ -200,13 +200,13 @@ class HassioSnapshotDialog extends LitElement {
|
||||
<ul class="buttons">
|
||||
<li>
|
||||
<mwc-button @click=${this._downloadClicked}>
|
||||
<iron-icon icon="hassio:download" class="icon"></iron-icon>
|
||||
<ha-icon icon="hassio:download" class="icon"></ha-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>
|
||||
</li>
|
||||
<li>
|
||||
<mwc-button @click=${this._partialRestoreClicked}>
|
||||
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||
<ha-icon icon="hassio:history" class="icon"> </ha-icon>
|
||||
Restore Selected
|
||||
</mwc-button>
|
||||
</li>
|
||||
@@ -214,7 +214,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
? html`
|
||||
<li>
|
||||
<mwc-button @click=${this._fullRestoreClicked}>
|
||||
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
|
||||
<ha-icon icon="hassio:history" class="icon"> </ha-icon>
|
||||
Wipe & restore
|
||||
</mwc-button>
|
||||
</li>
|
||||
@@ -222,7 +222,7 @@ class HassioSnapshotDialog extends LitElement {
|
||||
: ""}
|
||||
<li>
|
||||
<mwc-button @click=${this._deleteClicked}>
|
||||
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
|
||||
<ha-icon icon="hassio:delete" class="icon warning"> </ha-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>
|
||||
</li>
|
||||
|
33
hassio/src/dialogs/suggestAddonRestart.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { LitElement } from "lit-element";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
restartHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
export const suggestAddonRestart = async (
|
||||
element: LitElement,
|
||||
hass: HomeAssistant,
|
||||
addon: HassioAddonDetails
|
||||
): Promise<void> => {
|
||||
const confirmed = await showConfirmationDialog(element, {
|
||||
title: addon.name,
|
||||
text: "Do you want to restart the add-on with your changes?",
|
||||
confirmText: "restart add-on",
|
||||
dismissText: "no",
|
||||
});
|
||||
if (confirmed) {
|
||||
try {
|
||||
await restartHassioAddon(hass, addon.slug);
|
||||
} catch (err) {
|
||||
showAlertDialog(element, {
|
||||
title: "Failed to restart",
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@@ -2,8 +2,6 @@ window.loadES5Adapter().then(() => {
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
|
||||
// eslint-disable-next-line
|
||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "@polymer/paper-icon-button";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import { customElement, property, PropertyValues } from "lit-element";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
@@ -32,13 +32,13 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-pages-with-tabs";
|
||||
import "./hassio-panel";
|
||||
|
||||
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
|
||||
// The register callback of the IronA11yKeysBehavior inside ha-icon-button
|
||||
// is not called, causing _keyBindings to be uninitiliazed for ha-icon-button,
|
||||
// causing an exception when added to DOM. When transpiled to ES5, this will
|
||||
// break the build.
|
||||
customElements.get("paper-icon-button").prototype._keyBindings = {};
|
||||
customElements.get("ha-icon-button").prototype._keyBindings = {};
|
||||
|
||||
@customElement("hassio-main")
|
||||
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
@@ -55,17 +55,17 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-pages-with-tabs",
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
snapshots: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
addon: {
|
||||
tag: "hassio-addon-view",
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hassio-addon-view" */ "./addon-view/hassio-addon-view"
|
||||
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
|
||||
),
|
||||
},
|
||||
ingress: {
|
||||
@@ -94,6 +94,20 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme || this.hass.themes.default_theme
|
||||
);
|
||||
|
||||
this.style.setProperty(
|
||||
"--app-header-background-color",
|
||||
"var(--sidebar-background-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-text-color",
|
||||
"var(--sidebar-text-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--app-header-border-bottom",
|
||||
"1px solid var(--divider-color)"
|
||||
);
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
@@ -132,8 +146,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const route =
|
||||
el.nodeName === "HASSIO-PAGES-WITH-TABS" ? this.route : this.routeTail;
|
||||
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
|
||||
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
@@ -205,7 +218,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
await awaitAlert(
|
||||
{
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
title: "Hass.io",
|
||||
title: "Supervisor",
|
||||
},
|
||||
() => history.back()
|
||||
);
|
||||
@@ -231,7 +244,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
title: addon.name,
|
||||
},
|
||||
() => navigate(this, `/hassio/addon/${addon.slug}`, true)
|
||||
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
|
||||
);
|
||||
|
||||
return;
|
||||
|
@@ -1,141 +0,0 @@
|
||||
import "@polymer/app-layout/app-header-layout/app-header-layout";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import scrollToTarget from "../../src/common/dom/scroll-to-target";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import "../../src/components/ha-menu-button";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import "../../src/resources/ha-style";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-tabs-router";
|
||||
|
||||
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
|
||||
|
||||
@customElement("hassio-pages-with-tabs")
|
||||
class HassioPagesWithTabs extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfo;
|
||||
|
||||
@property() public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const page = this._page;
|
||||
return html`
|
||||
<app-header-layout has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
hassio
|
||||
></ha-menu-button>
|
||||
<div main-title>Supervisor</div>
|
||||
${HAS_REFRESH_BUTTON.includes(page)
|
||||
? html`
|
||||
<paper-icon-button
|
||||
icon="hassio:refresh"
|
||||
@click=${this.refreshClicked}
|
||||
></paper-icon-button>
|
||||
`
|
||||
: undefined}
|
||||
</app-toolbar>
|
||||
<paper-tabs
|
||||
scrollable
|
||||
attr-for-selected="page-name"
|
||||
.selected=${page}
|
||||
@iron-activate=${this.handlePageSelected}
|
||||
>
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<hassio-tabs-router
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-tabs-router>
|
||||
</app-header-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
private handlePageSelected(ev) {
|
||||
const newPage = ev.detail.item.getAttribute("page-name");
|
||||
if (newPage !== this._page) {
|
||||
navigate(this, `/hassio/${newPage}`);
|
||||
}
|
||||
|
||||
scrollToTarget(
|
||||
this,
|
||||
// @ts-ignore
|
||||
this.shadowRoot!.querySelector("app-header-layout").header.scrollTarget
|
||||
);
|
||||
}
|
||||
|
||||
private refreshClicked() {
|
||||
if (this._page === "snapshots") {
|
||||
// @ts-ignore
|
||||
this.shadowRoot.querySelector("hassio-snapshots").refreshData();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.shadowRoot.querySelector("hassio-addon-store").refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private get _page() {
|
||||
return this.route.path.substr(1);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-pages-with-tabs": HassioPagesWithTabs;
|
||||
}
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../src/layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./addon-store/hassio-addon-store";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./dashboard/hassio-dashboard";
|
||||
@@ -17,29 +16,33 @@ import "./dashboard/hassio-dashboard";
|
||||
import "./snapshots/hassio-snapshots";
|
||||
import "./system/hassio-system";
|
||||
|
||||
@customElement("hassio-tabs-router")
|
||||
class HassioTabsRouter extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@customElement("hassio-panel-router")
|
||||
class HassioPanelRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public supervisorInfo: HassioSupervisorInfo;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property() public hostInfo: HassioHostInfo;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public hassInfo: HassioHomeAssistantInfo;
|
||||
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
|
||||
|
||||
@property() public hassOsInfo!: HassioHassOSInfo;
|
||||
@property({ attribute: false }) public hostInfo: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-dashboard",
|
||||
},
|
||||
snapshots: {
|
||||
tag: "hassio-snapshots",
|
||||
},
|
||||
store: {
|
||||
tag: "hassio-addon-store",
|
||||
},
|
||||
snapshots: {
|
||||
tag: "hassio-snapshots",
|
||||
},
|
||||
system: {
|
||||
tag: "hassio-system",
|
||||
},
|
||||
@@ -47,27 +50,18 @@ class HassioTabsRouter extends HassRouterPage {
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
if ("setProperties" in el) {
|
||||
// As long as we have Polymer pages
|
||||
(el as PolymerElement).setProperties({
|
||||
hass: this.hass,
|
||||
supervisorInfo: this.supervisorInfo,
|
||||
hostInfo: this.hostInfo,
|
||||
hassInfo: this.hassInfo,
|
||||
hassOsInfo: this.hassOsInfo,
|
||||
});
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
el.supervisorInfo = this.supervisorInfo;
|
||||
el.hostInfo = this.hostInfo;
|
||||
el.hassInfo = this.hassInfo;
|
||||
el.hassOsInfo = this.hassOsInfo;
|
||||
}
|
||||
el.hass = this.hass;
|
||||
el.route = this.route;
|
||||
el.narrow = this.narrow;
|
||||
el.supervisorInfo = this.supervisorInfo;
|
||||
el.hostInfo = this.hostInfo;
|
||||
el.hassInfo = this.hassInfo;
|
||||
el.hassOsInfo = this.hassOsInfo;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-tabs-router": HassioTabsRouter;
|
||||
"hassio-panel-router": HassioPanelRouter;
|
||||
}
|
||||
}
|
77
hassio/src/hassio-panel.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
|
||||
import {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import "../../src/resources/ha-style";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-panel-router";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
|
||||
export const supervisorTabs: PageNavigation[] = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
icon: "hassio:view-dashboard",
|
||||
},
|
||||
{
|
||||
name: "Add-on store",
|
||||
path: `/hassio/store`,
|
||||
icon: "hassio:store",
|
||||
},
|
||||
{
|
||||
name: "Snapshots",
|
||||
path: `/hassio/snapshots`,
|
||||
icon: "hassio:backup-restore",
|
||||
},
|
||||
{
|
||||
name: "System",
|
||||
path: `/hassio/system`,
|
||||
icon: "hassio:cogs",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("hassio-panel")
|
||||
class HassioPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassInfo=${this.hassInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-panel": HassioPanel;
|
||||
}
|
||||
}
|
@@ -86,7 +86,7 @@ class HassioIngressView extends LitElement {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
paper-icon-button {
|
||||
ha-icon-button {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
`;
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import "../../../src/components/ha-iconset-svg";
|
||||
import iconSetContent from "../../hassio-icons.html";
|
||||
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
documentContainer.innerHTML = iconSetContent;
|
||||
document.head.appendChild(documentContainer.content);
|
@@ -4,8 +4,12 @@ export const hassioStyle = css`
|
||||
.content {
|
||||
margin: 8px;
|
||||
}
|
||||
h1 {
|
||||
h1,
|
||||
.description,
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
|
@@ -30,11 +30,14 @@ import {
|
||||
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
||||
import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import "../components/hassio-card-content";
|
||||
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
import { supervisorTabs } from "../hassio-panel";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
name: string;
|
||||
@@ -43,9 +46,13 @@ interface CheckboxItem {
|
||||
|
||||
@customElement("hassio-snapshots")
|
||||
class HassioSnapshots extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@property() private _snapshotName = "";
|
||||
|
||||
@@ -81,136 +88,153 @@ class HassioSnapshots extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
Create snapshot
|
||||
</h1>
|
||||
<p class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your Home
|
||||
Assistant instance.
|
||||
</p>
|
||||
<div class="card-group">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
autofocus
|
||||
label="Name"
|
||||
name="snapshotName"
|
||||
.value=${this._snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
Type:
|
||||
<paper-radio-group
|
||||
name="snapshotType"
|
||||
.selected=${this._snapshotType}
|
||||
@selected-changed=${this._handleRadioValueChanged}
|
||||
>
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
${this._snapshotType === "full"
|
||||
? undefined
|
||||
: html`
|
||||
Folders:
|
||||
${this._folderList.map(
|
||||
(folder, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${folder.checked}
|
||||
@checked-changed=${this._folderChecked}
|
||||
>
|
||||
${folder.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
Add-ons:
|
||||
${this._addonList.map(
|
||||
(addon, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked="{{item.checked}}"
|
||||
@checked-changed=${this._addonChecked}
|
||||
>
|
||||
${addon.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
`}
|
||||
Security:
|
||||
<paper-checkbox
|
||||
name="snapshotHasPassword"
|
||||
.checked=${this._snapshotHasPassword}
|
||||
@checked-changed=${this._handleCheckboxValueChanged}
|
||||
>
|
||||
Password protection
|
||||
</paper-checkbox>
|
||||
${this._snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
label="Password"
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this._snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
`
|
||||
: undefined}
|
||||
${this._error !== ""
|
||||
? html` <p class="error">${this._error}</p> `
|
||||
: undefined}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
Create
|
||||
</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
hassio
|
||||
main-page
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
>
|
||||
<span slot="header">Snapshots</span>
|
||||
|
||||
<h1>Available snapshots</h1>
|
||||
<div class="card-group">
|
||||
${this._snapshots === undefined
|
||||
? undefined
|
||||
: this._snapshots.length === 0
|
||||
? html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
You don't have any snapshots yet.
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
: this._snapshots.map(
|
||||
(snapshot) => html`
|
||||
<paper-card
|
||||
class="pointer"
|
||||
.snapshot=${snapshot}
|
||||
@click=${this._snapshotClicked}
|
||||
>
|
||||
<ha-icon-button
|
||||
icon="hassio:reload"
|
||||
slot="toolbar-icon"
|
||||
aria-label="Reload snapshots"
|
||||
@click=${this.refreshData}
|
||||
></ha-icon-button>
|
||||
|
||||
<div class="content">
|
||||
<h1>
|
||||
Create snapshot
|
||||
</h1>
|
||||
<p class="description">
|
||||
Snapshots allow you to easily backup and restore all data of your
|
||||
Home Assistant instance.
|
||||
</p>
|
||||
<div class="card-group">
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input
|
||||
autofocus
|
||||
label="Name"
|
||||
name="snapshotName"
|
||||
.value=${this._snapshotName}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
Type:
|
||||
<paper-radio-group
|
||||
name="snapshotType"
|
||||
.selected=${this._snapshotType}
|
||||
@selected-changed=${this._handleRadioValueChanged}
|
||||
>
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
${this._snapshotType === "full"
|
||||
? undefined
|
||||
: html`
|
||||
Folders:
|
||||
${this._folderList.map(
|
||||
(folder, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${folder.checked}
|
||||
@checked-changed=${this._folderChecked}
|
||||
>
|
||||
${folder.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
Add-ons:
|
||||
${this._addonList.map(
|
||||
(addon, idx) => html`
|
||||
<paper-checkbox
|
||||
.idx=${idx}
|
||||
.checked=${addon.checked}
|
||||
@checked-changed=${this._addonChecked}
|
||||
>
|
||||
${addon.name}
|
||||
</paper-checkbox>
|
||||
`
|
||||
)}
|
||||
`}
|
||||
Security:
|
||||
<paper-checkbox
|
||||
name="snapshotHasPassword"
|
||||
.checked=${this._snapshotHasPassword}
|
||||
@checked-changed=${this._handleCheckboxValueChanged}
|
||||
>
|
||||
Password protection
|
||||
</paper-checkbox>
|
||||
${this._snapshotHasPassword
|
||||
? html`
|
||||
<paper-input
|
||||
label="Password"
|
||||
type="password"
|
||||
name="snapshotPassword"
|
||||
.value=${this._snapshotPassword}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
></paper-input>
|
||||
`
|
||||
: undefined}
|
||||
${this._error !== ""
|
||||
? html` <p class="error">${this._error}</p> `
|
||||
: undefined}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button
|
||||
.disabled=${this._creatingSnapshot}
|
||||
@click=${this._createSnapshot}
|
||||
>
|
||||
Create
|
||||
</mwc-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<h1>Available snapshots</h1>
|
||||
<div class="card-group">
|
||||
${this._snapshots === undefined
|
||||
? undefined
|
||||
: this._snapshots.length === 0
|
||||
? html`
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${snapshot.name || snapshot.slug}
|
||||
.description=${this._computeDetails(snapshot)}
|
||||
.datetime=${snapshot.date}
|
||||
.icon=${snapshot.type === "full"
|
||||
? "hassio:package-variant-closed"
|
||||
: "hassio:package-variant"}
|
||||
.
|
||||
.icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
You don't have any snapshots yet.
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
: this._snapshots.map(
|
||||
(snapshot) => html`
|
||||
<paper-card
|
||||
class="pointer"
|
||||
.snapshot=${snapshot}
|
||||
@click=${this._snapshotClicked}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${snapshot.name || snapshot.slug}
|
||||
.description=${this._computeDetails(snapshot)}
|
||||
.datetime=${snapshot.date}
|
||||
.icon=${snapshot.type === "full"
|
||||
? "hassio:package-variant-closed"
|
||||
: "hassio:package-variant"}
|
||||
.icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -12,13 +12,23 @@ import {
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
fetchHassioHostInfo,
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo as HassioHostInfoType,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
updateOS,
|
||||
changeHostOptions,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
class HassioHostInfo extends LitElement {
|
||||
@@ -76,21 +86,15 @@ class HassioHostInfo extends LitElement {
|
||||
<div class="card-actions">
|
||||
${this.hostInfo.features.includes("reboot")
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/host/reboot"
|
||||
>Reboot</ha-call-api-button
|
||||
<mwc-button class="warning" @click=${this._rebootHost}
|
||||
>Reboot</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.features.includes("shutdown")
|
||||
? html`
|
||||
<ha-call-api-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
path="hassio/host/shutdown"
|
||||
>Shutdown</ha-call-api-button
|
||||
<mwc-button class="warning" @click=${this._shutdownHost}
|
||||
>Shutdown</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
@@ -106,11 +110,7 @@ class HassioHostInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest
|
||||
? html`
|
||||
<ha-call-api-button .hass=${this.hass} path="hassio/os/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
`
|
||||
? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> `
|
||||
: ""}
|
||||
</div>
|
||||
</paper-card>
|
||||
@@ -189,6 +189,72 @@ class HassioHostInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _rebootHost(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Reboot",
|
||||
text: "Are you sure you want to reboot the host?",
|
||||
confirmText: "reboot host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to reboot",
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _shutdownHost(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Shutdown",
|
||||
text: "Are you sure you want to shutdown the host?",
|
||||
confirmText: "shutdown host",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to shutdown",
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateOS(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "Update",
|
||||
text: "Are you sure you want to update the OS?",
|
||||
confirmText: "update os",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOS(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to update",
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _objectToMarkdown(obj, indent = ""): string {
|
||||
let data = "";
|
||||
Object.keys(obj).forEach((key) => {
|
||||
@@ -210,11 +276,25 @@ class HassioHostInfo extends LitElement {
|
||||
return data;
|
||||
}
|
||||
|
||||
private _changeHostnameClicked(): void {
|
||||
const curHostname = this.hostInfo.hostname;
|
||||
const hostname = prompt("Please enter a new hostname:", curHostname);
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.hostInfo.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: "Change hostname",
|
||||
inputLabel: "Please enter a new hostname:",
|
||||
inputType: "string",
|
||||
defaultValue: curHostname,
|
||||
});
|
||||
|
||||
if (hostname && hostname !== curHostname) {
|
||||
this.hass.callApi("POST", "hassio/host/options", { hostname });
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
this.hostInfo = await fetchHassioHostInfo(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Setting hostname failed",
|
||||
text: err.body.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@@ -142,17 +143,30 @@ class HassioSupervisorInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _joinBeta() {
|
||||
if (
|
||||
!confirm(`WARNING:
|
||||
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: "WARNING",
|
||||
text: html` Beta releases are for testers and early adopters and can
|
||||
contain unstable code changes.
|
||||
<br />
|
||||
<b>
|
||||
Make sure you have backups of your data before you activate this
|
||||
feature.
|
||||
</b>
|
||||
<br /><br />
|
||||
This includes beta releases for:
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
<br />
|
||||
Do you want to join the beta channel?`,
|
||||
confirmText: "join beta",
|
||||
dismissText: "no",
|
||||
});
|
||||
|
||||
This includes beta releases for:
|
||||
- Home Assistant (Release Candidates)
|
||||
- Hass.io supervisor
|
||||
- Host system`)
|
||||
) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data: SupervisorOptions = { channel: "beta" };
|
||||
await setSupervisorOption(this.hass, data);
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import {
|
||||
css,
|
||||
@@ -7,22 +10,58 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fetchSupervisorLogs } from "../../../src/data/hassio/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
|
||||
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
|
||||
import "../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "../../../src/layouts/loading-screen";
|
||||
|
||||
interface LogProvider {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "core",
|
||||
name: "Core",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("hassio-supervisor-log")
|
||||
class HassioSupervisorLog extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() private _error?: string;
|
||||
|
||||
@query("#content") private _logContent!: HTMLDivElement;
|
||||
@property() private _selectedLogProvider = "supervisor";
|
||||
|
||||
@property() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
@@ -33,7 +72,36 @@ class HassioSupervisorLog extends LitElement {
|
||||
return html`
|
||||
<paper-card>
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
<div class="card-content" id="content"></div>
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<paper-dropdown-menu
|
||||
label="Log provider"
|
||||
@iron-select=${this._setLogProvider}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="provider"
|
||||
.selected=${this._selectedLogProvider}
|
||||
>
|
||||
${logProviders.map((provider) => {
|
||||
return html`
|
||||
<paper-item provider=${provider.key}
|
||||
>${provider.name}</paper-item
|
||||
>
|
||||
`;
|
||||
})}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html
|
||||
.content=${this._content}
|
||||
></hassio-ansi-to-html>`
|
||||
: html`<loading-screen></loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
|
||||
</div>
|
||||
@@ -45,7 +113,6 @@ class HassioSupervisorLog extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
ANSI_HTML_STYLE,
|
||||
css`
|
||||
paper-card {
|
||||
width: 100%;
|
||||
@@ -53,22 +120,36 @@ class HassioSupervisorLog extends LitElement {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
paper-dropdown-menu {
|
||||
padding: 0 2%;
|
||||
width: 96%;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-content {
|
||||
padding-top: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
this._content = undefined;
|
||||
|
||||
try {
|
||||
const content = await fetchSupervisorLogs(this.hass);
|
||||
while (this._logContent.lastChild) {
|
||||
this._logContent.removeChild(this._logContent.lastChild as Node);
|
||||
}
|
||||
this._logContent.appendChild(parseTextToColoredPre(content));
|
||||
this._content = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err) {
|
||||
this._error = `Failed to get supervisor logs, ${
|
||||
err.body?.message || err
|
||||
|
@@ -14,15 +14,22 @@ import {
|
||||
} from "../../../src/data/hassio/host";
|
||||
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./hassio-host-info";
|
||||
import "./hassio-supervisor-info";
|
||||
import "./hassio-supervisor-log";
|
||||
|
||||
import { supervisorTabs } from "../hassio-panel";
|
||||
|
||||
@customElement("hassio-system")
|
||||
class HassioSystem extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfo;
|
||||
|
||||
@@ -32,22 +39,32 @@ class HassioSystem extends LitElement {
|
||||
|
||||
public render(): TemplateResult | void {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Information</h1>
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
hassio
|
||||
main-page
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
>
|
||||
<span slot="header">System</span>
|
||||
<div class="content">
|
||||
<h1>Information</h1>
|
||||
<div class="card-group">
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisorInfo=${this.supervisorInfo}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<h1>System log</h1>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
<h1>System log</h1>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
72
package.json
@@ -8,12 +8,12 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint '{**/src,src}/**/*.{js,ts,html}' --ignore-path .gitignore",
|
||||
"format:eslint": "eslint '{**/src,src}/**/*.{js,ts,html}' --fix --ignore-path .gitignore",
|
||||
"lint:prettier": "prettier '{**/src,src}/**/*.{js,ts,json,css,md}' --check",
|
||||
"format:prettier": "prettier '{**/src,src}/**/*.{js,ts,json,css,md}' --write",
|
||||
"lint:eslint": "eslint '**/src/**/*.{js,ts,html}' --ignore-path .gitignore",
|
||||
"format:eslint": "eslint '**/src/**/*.{js,ts,html}' --fix --ignore-path .gitignore",
|
||||
"lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check",
|
||||
"format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write",
|
||||
"lint:types": "tsc",
|
||||
"lint:lit": "lit-analyzer '{**/src,src}/**/*.ts'",
|
||||
"lint:lit": "lit-analyzer '**/src/**/*.ts'",
|
||||
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
|
||||
"format": "npm run format:eslint && npm run format:prettier",
|
||||
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
|
||||
@@ -24,23 +24,28 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@material/chips": "^5.0.0",
|
||||
"@material/mwc-button": "^0.13.0",
|
||||
"@material/mwc-checkbox": "^0.13.0",
|
||||
"@material/mwc-dialog": "^0.13.0",
|
||||
"@material/mwc-fab": "^0.13.0",
|
||||
"@material/mwc-ripple": "^0.13.0",
|
||||
"@material/mwc-switch": "^0.13.0",
|
||||
"@fullcalendar/core": "5.0.0-beta.2",
|
||||
"@fullcalendar/daygrid": "5.0.0-beta.2",
|
||||
"@material/chips": "^6.0.0-canary.35a32aaea.0",
|
||||
"@material/mwc-button": "0.14.1",
|
||||
"@material/mwc-checkbox": "0.14.1",
|
||||
"@material/mwc-dialog": "0.14.1",
|
||||
"@material/mwc-fab": "0.14.1",
|
||||
"@material/mwc-formfield": "0.14.1",
|
||||
"@material/mwc-icon-button": "0.14.1",
|
||||
"@material/mwc-list": "0.14.1",
|
||||
"@material/mwc-menu": "0.14.1",
|
||||
"@material/mwc-ripple": "0.14.1",
|
||||
"@material/mwc-switch": "0.14.1",
|
||||
"@mdi/js": "4.9.95",
|
||||
"@mdi/svg": "4.9.95",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-localize-behavior": "^3.0.1",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
"@polymer/font-roboto": "^3.0.2",
|
||||
"@polymer/iron-autogrow-textarea": "^3.0.1",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
||||
"@polymer/iron-image": "^3.0.1",
|
||||
"@polymer/iron-input": "^3.0.1",
|
||||
"@polymer/iron-label": "^3.0.1",
|
||||
@@ -56,7 +61,6 @@
|
||||
"@polymer/paper-dialog-scrollable": "^3.0.1",
|
||||
"@polymer/paper-drawer-panel": "^3.0.1",
|
||||
"@polymer/paper-dropdown-menu": "^3.0.1",
|
||||
"@polymer/paper-icon-button": "^3.0.2",
|
||||
"@polymer/paper-input": "^3.0.1",
|
||||
"@polymer/paper-item": "^3.0.1",
|
||||
"@polymer/paper-listbox": "^3.0.1",
|
||||
@@ -74,7 +78,6 @@
|
||||
"@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",
|
||||
@@ -86,12 +89,13 @@
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^3.0.2",
|
||||
"fecha": "^4.2.0",
|
||||
"fuse.js": "^3.4.4",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "5.0.0",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"intl-messageformat": "^8.3.9",
|
||||
"js-yaml": "^3.13.1",
|
||||
"leaflet": "^1.4.0",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
@@ -103,16 +107,13 @@
|
||||
"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",
|
||||
"unfetch": "^4.1.0",
|
||||
"web-animations-js": "^2.3.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"xss": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -124,7 +125,6 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.9.4",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/preset-typescript": "^7.9.0",
|
||||
"@types/chai": "^4.1.7",
|
||||
@@ -136,6 +136,7 @@
|
||||
"@types/leaflet-draw": "^1.0.1",
|
||||
"@types/memoize-one": "4.1.0",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/resize-observer-browser": "^0.1.3",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
||||
"@typescript-eslint/parser": "^2.28.0",
|
||||
@@ -151,7 +152,6 @@
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-lit": "^1.2.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-wc": "^1.2.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
@@ -162,7 +162,6 @@
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.5",
|
||||
@@ -171,7 +170,7 @@
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^6.0.2",
|
||||
"npm": "^6.14.4",
|
||||
"object-hash": "^2.0.3",
|
||||
"parse5": "^5.1.0",
|
||||
"prettier": "^2.0.4",
|
||||
"raw-loader": "^2.0.0",
|
||||
@@ -198,14 +197,19 @@
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"lit-html": "^1.1.2",
|
||||
"@material/button": "^5.0.0",
|
||||
"@material/checkbox": "^5.0.0",
|
||||
"@material/dialog": "^5.0.0",
|
||||
"@material/fab": "^5.0.0",
|
||||
"@material/switch": "^5.0.0",
|
||||
"@material/ripple": "^5.0.0",
|
||||
"@material/dom": "^5.0.0",
|
||||
"@material/touch-target": "^5.0.0"
|
||||
"@material/animation": "6.0.0",
|
||||
"@material/base": "6.0.0",
|
||||
"@material/checkbox": "6.0.0",
|
||||
"@material/density": "6.0.0",
|
||||
"@material/dom": "6.0.0",
|
||||
"@material/elevation": "6.0.0",
|
||||
"@material/feature-targeting": "6.0.0",
|
||||
"@material/ripple": "6.0.0",
|
||||
"@material/rtl": "6.0.0",
|
||||
"@material/shape": "6.0.0",
|
||||
"@material/theme": "6.0.0",
|
||||
"@material/touch-target": "6.0.0",
|
||||
"@material/typography": "6.0.0"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 715 B After Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 910 B After Width: | Height: | Size: 888 B |
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 807 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 639 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 774 B |
@@ -6,6 +6,6 @@ set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json
|
||||
STATS=1 NODE_ENV=production ./node_modules/.bin/webpack --profile --json > compilation-stats.json
|
||||
npx webpack-bundle-analyzer compilation-stats.json hass_frontend/frontend_latest
|
||||
rm compilation-stats.json
|
||||
|
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200414.0",
|
||||
version="20200505.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -103,6 +103,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
${this.localize("ui.panel.page-authorize.abort_intro")}:
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`
|
||||
)}
|
||||
@@ -113,6 +114,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
${this._computeStepDescription(step)
|
||||
? html`
|
||||
<ha-markdown
|
||||
breaks
|
||||
.content=${this._computeStepDescription(step)}
|
||||
></ha-markdown>
|
||||
`
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-item/paper-item.js";
|
||||
import "@polymer/paper-item/paper-item-body.js";
|
||||
import { html, LitElement, property } from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "../components/ha-icon-next";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import "@polymer/paper-card";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag.js";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import "../components/state-history-charts";
|
||||
import "../data/ha-state-history-data";
|
||||
|
@@ -87,3 +87,66 @@ export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
/** HA Color Pallete. */
|
||||
export const HA_COLOR_PALETTE = [
|
||||
"ff0029",
|
||||
"66a61e",
|
||||
"377eb8",
|
||||
"984ea3",
|
||||
"00d2d5",
|
||||
"ff7f00",
|
||||
"af8d00",
|
||||
"7f80cd",
|
||||
"b3e900",
|
||||
"c42e60",
|
||||
"a65628",
|
||||
"f781bf",
|
||||
"8dd3c7",
|
||||
"bebada",
|
||||
"fb8072",
|
||||
"80b1d3",
|
||||
"fdb462",
|
||||
"fccde5",
|
||||
"bc80bd",
|
||||
"ffed6f",
|
||||
"c4eaff",
|
||||
"cf8c00",
|
||||
"1b9e77",
|
||||
"d95f02",
|
||||
"e7298a",
|
||||
"e6ab02",
|
||||
"a6761d",
|
||||
"0097ff",
|
||||
"00d067",
|
||||
"f43600",
|
||||
"4ba93b",
|
||||
"5779bb",
|
||||
"927acc",
|
||||
"97ee3f",
|
||||
"bf3947",
|
||||
"9f5b00",
|
||||
"f48758",
|
||||
"8caed6",
|
||||
"f2b94f",
|
||||
"eff26e",
|
||||
"e43872",
|
||||
"d9b100",
|
||||
"9d7a00",
|
||||
"698cff",
|
||||
"d9d9d9",
|
||||
"00d27e",
|
||||
"d06800",
|
||||
"009f82",
|
||||
"c49200",
|
||||
"cbe8ff",
|
||||
"fecddf",
|
||||
"c27eb6",
|
||||
"8cd2ce",
|
||||
"c4b8d9",
|
||||
"f883b0",
|
||||
"a49100",
|
||||
"f48800",
|
||||
"27d0df",
|
||||
"a04a9b",
|
||||
];
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import fecha from "fecha";
|
||||
import { format } from "fecha";
|
||||
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatDate = toLocaleDateStringSupportsOptions
|
||||
@@ -8,4 +8,4 @@ export const formatDate = toLocaleDateStringSupportsOptions
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "longDate");
|
||||
: (dateObj: Date) => format(dateObj, "longDate");
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import fecha from "fecha";
|
||||
import { format } from "fecha";
|
||||
import { toLocaleStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatDateTime = toLocaleStringSupportsOptions
|
||||
@@ -10,11 +10,7 @@ export const formatDateTime = toLocaleStringSupportsOptions
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) =>
|
||||
fecha.format(
|
||||
dateObj,
|
||||
`${fecha.masks.longDate}, ${fecha.masks.shortTime}`
|
||||
);
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
|
||||
|
||||
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
@@ -26,8 +22,4 @@ export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) =>
|
||||
fecha.format(
|
||||
dateObj,
|
||||
`${fecha.masks.longDate}, ${fecha.masks.mediumTime}`
|
||||
);
|
||||
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm:ss");
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import fecha from "fecha";
|
||||
import { format } from "fecha";
|
||||
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
|
||||
|
||||
export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
@@ -7,7 +7,7 @@ export const formatTime = toLocaleTimeStringSupportsOptions
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
|
||||
: (dateObj: Date) => format(dateObj, "shortTime");
|
||||
|
||||
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
? (dateObj: Date, locales: string) =>
|
||||
@@ -16,4 +16,4 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumTime");
|
||||
: (dateObj: Date) => format(dateObj, "mediumTime");
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Map } from "leaflet";
|
||||
import type { Map } from "leaflet";
|
||||
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
export type LeafletModuleType = typeof import("leaflet");
|
||||
@@ -13,9 +13,9 @@ export const setupLeafletMap = async (
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
const Leaflet = (await import(
|
||||
const Leaflet = ((await import(
|
||||
/* webpackChunkName: "leaflet" */ "leaflet"
|
||||
)) as LeafletModuleType;
|
||||
)) as any).default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
if (draw) {
|
||||
|