Merge pull request #3171 from home-assistant/dev

20190507.0
This commit is contained in:
Paulus Schoutsen 2019-05-07 22:48:16 -07:00 committed by GitHub
commit 484b1c8444
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
172 changed files with 2179 additions and 1253 deletions

51
build-scripts/gulp/app.js Normal file
View File

@ -0,0 +1,51 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean",
gulp.parallel(
"gen-service-worker-dev",
"gen-icons",
"gen-pages-dev",
"gen-index-html-dev",
"build-translations"
),
"copy-static",
"webpack-watch-app"
)
);
gulp.task(
"build-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel("gen-icons", "build-translations"),
"copy-static",
gulp.parallel(
"webpack-prod-app",
// Do not compress static files in CI, it's SLOW.
...(process.env.CI === "true" ? [] : ["compress-static"])
),
gulp.parallel(
"gen-pages-prod",
"gen-index-html-prod",
"gen-service-worker-prod"
)
)
);

View File

@ -3,3 +3,4 @@ const gulp = require("gulp");
const config = require("../paths");
gulp.task("clean", () => del([config.root, config.build_dir]));
gulp.task("clean-demo", () => del([config.demo_root, config.build_dir]));

View File

@ -0,0 +1,36 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop-demo",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-demo",
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
"copy-static-demo",
"webpack-dev-server-demo"
)
);
gulp.task(
"build-demo",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-demo",
gulp.parallel("gen-icons", "gen-icons-demo", "build-translations"),
"copy-static-demo",
"webpack-prod-demo"
)
);

View File

@ -1,29 +0,0 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"develop",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean",
gulp.parallel(
"copy-static",
"gen-service-worker-dev",
"gen-icons",
"gen-pages-dev",
"gen-index-html-dev",
gulp.series("build-translations", "copy-translations")
),
"webpack-watch"
)
);

View File

@ -5,17 +5,21 @@ const path = require("path");
const fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const config = require("../paths");
const paths = require("../paths");
const npmPath = (...parts) =>
path.resolve(config.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(config.polymer_dir, ...parts);
const staticPath = (...parts) => path.resolve(config.root, "static", ...parts);
path.resolve(paths.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));
function copyTranslations() {
const genStaticPath = (staticDir) => (...parts) =>
path.resolve(staticDir, ...parts);
function copyTranslations(staticDir) {
const staticPath = genStaticPath(staticDir);
// Translation output
fs.copySync(
polyPath("build-translations/output"),
@ -23,9 +27,8 @@ function copyTranslations() {
);
}
function copyStatic() {
// Basic static files
fs.copySync(polyPath("public"), config.root);
function copyPolyfills(staticDir) {
const staticPath = genStaticPath(staticDir);
// Web Component polyfills and adapters
copyFileDir(
@ -40,31 +43,16 @@ function copyStatic() {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// Local fonts
fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts"));
// External dependency assets
copyFileDir(
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
staticPath("panels/calendar/")
);
copyFileDir(
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
);
}
gulp.task("copy-static", (done) => {
copyStatic();
done();
});
function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir);
// Local fonts
fs.copySync(npmPath("@polymer/font-roboto-local/fonts"), staticPath("fonts"));
}
gulp.task("compress-static", () => {
function compressStatic(staticDir) {
const staticPath = genStaticPath(staticDir);
const fonts = gulp
.src(staticPath("fonts/**/*.ttf"))
.pipe(zopfli())
@ -79,9 +67,44 @@ gulp.task("compress-static", () => {
.pipe(gulp.dest(staticPath("translations")));
return merge(fonts, polyfills, translations);
});
}
gulp.task("copy-translations", (done) => {
copyTranslations();
gulp.task("copy-static", (done) => {
const staticDir = paths.static;
const staticPath = genStaticPath(paths.static);
// Basic static files
fs.copySync(polyPath("public"), paths.root);
copyPolyfills(staticDir);
copyFonts(staticDir);
copyTranslations(staticDir);
// Panel assets
copyFileDir(
npmPath("react-big-calendar/lib/css/react-big-calendar.css"),
staticPath("panels/calendar/")
);
copyFileDir(
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")
);
done();
});
gulp.task("compress-static", () => compressStatic(paths.static));
gulp.task("copy-static-demo", (done) => {
// Copy app static files
fs.copySync(polyPath("public"), paths.demo_root);
// Copy demo static files
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_root);
copyPolyfills(paths.demo_static);
copyFonts(paths.demo_static);
copyTranslations(paths.demo_static);
done();
});

View File

@ -1,6 +1,7 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs");
const paths = require("../paths");
const ICON_PACKAGE_PATH = path.resolve(
__dirname,
@ -118,6 +119,15 @@ gulp.task("gen-icons-hass", (done) => {
});
gulp.task("gen-icons", gulp.series("gen-icons-hass", "gen-icons-mdi"));
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
fs.writeFileSync(
path.resolve(paths.demo_dir, "hademo-icons.html"),
generateIconset("hademo", iconNames)
);
done();
});
module.exports = {
findIcons,
generateIconset,

View File

@ -1,31 +0,0 @@
// Run HA develop mode
const gulp = require("gulp");
require("./clean.js");
require("./translations.js");
require("./gen-icons.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
gulp.task(
"build-release",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean",
gulp.parallel(
"copy-static",
"gen-icons",
gulp.series("build-translations", "copy-translations")
),
gulp.parallel("webpack-prod", "compress-static"),
gulp.parallel(
"gen-pages-prod",
"gen-index-html-prod",
"gen-service-worker-prod"
)
)
);

View File

@ -1,7 +1,11 @@
// Tasks to run webpack.
const gulp = require("gulp");
const path = require("path");
const webpack = require("webpack");
const { createAppConfig } = require("../webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
const paths = require("../paths");
const { createAppConfig, createDemoConfig } = require("../webpack");
const handler = (done) => (err, stats) => {
if (err) {
@ -12,7 +16,7 @@ const handler = (done) => (err, stats) => {
return;
}
console.log(`Build done @ ${new Date().toLocaleTimeString()}`);
log(`Build done @ ${new Date().toLocaleTimeString()}`);
if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString("minimal"));
@ -23,7 +27,7 @@ const handler = (done) => (err, stats) => {
}
};
gulp.task("webpack-watch", () => {
gulp.task("webpack-watch-app", () => {
const compiler = webpack([
createAppConfig({
isProdBuild: false,
@ -41,7 +45,7 @@ gulp.task("webpack-watch", () => {
});
gulp.task(
"webpack-prod",
"webpack-prod-app",
() =>
new Promise((resolve) =>
webpack(
@ -61,3 +65,52 @@ gulp.task(
)
)
);
gulp.task("webpack-dev-server-demo", () => {
const compiler = webpack([
createDemoConfig({
isProdBuild: false,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: false,
latestBuild: true,
isStatsBuild: false,
}),
]);
new WebpackDevServer(compiler, {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.demo_dir, "dist"),
}).listen(8080, "localhost", function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8080");
});
});
gulp.task(
"webpack-prod-demo",
() =>
new Promise((resolve) =>
webpack(
[
createDemoConfig({
isProdBuild: true,
latestBuild: false,
isStatsBuild: false,
}),
createDemoConfig({
isProdBuild: true,
latestBuild: true,
isStatsBuild: false,
}),
],
handler(resolve)
)
)
);

View File

@ -2,9 +2,16 @@ var path = require("path");
module.exports = {
polymer_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
root: path.resolve(__dirname, "../hass_frontend"),
static: path.resolve(__dirname, "../hass_frontend/static"),
output: path.resolve(__dirname, "../hass_frontend/frontend_latest"),
output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"),
demo_dir: path.resolve(__dirname, "../demo"),
demo_root: path.resolve(__dirname, "../demo/dist"),
demo_static: path.resolve(__dirname, "../demo/dist/static"),
demo_output: path.resolve(__dirname, "../demo/dist/frontend_latest"),
demo_output_es5: path.resolve(__dirname, "../demo/frontend_es5"),
};

View File

@ -17,6 +17,12 @@ if (!version) {
}
version = version[0];
const genMode = (isProdBuild) => (isProdBuild ? "production" : "development");
const genDevTool = (isProdBuild) =>
isProdBuild ? "cheap-source-map" : "inline-cheap-module-source-map";
const genChunkFilename = (isProdBuild, isStatsBuild) =>
isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"],
alias: {
@ -29,6 +35,20 @@ const resolve = {
},
};
const cssLoader = {
test: /\.css$/,
use: "raw-loader",
};
const htmlLoader = {
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
};
const plugins = [
// Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
@ -75,8 +95,6 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
] = `build-translations/output/${key}.json`;
});
const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/";
const entry = {
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
@ -88,28 +106,11 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
};
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild
? "cheap-source-map "
: "inline-cheap-module-source-map",
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry,
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
},
optimization: optimization(latestBuild),
plugins: [
@ -165,20 +166,56 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`;
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: latestBuild ? paths.output : paths.output_es5,
publicPath,
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
},
resolve,
};
};
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
return {
mode: genMode(isProdBuild),
devtool: genDevTool(isProdBuild),
entry: {
main: "./demo/src/entrypoint.ts",
compatibility: "./src/entrypoints/compatibility.ts",
},
module: {
rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
},
optimization: optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
}),
...plugins,
].filter(Boolean),
resolve,
output: {
filename: "[name].js",
chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
path: path.resolve(
paths.demo_root,
latestBuild ? "frontend_latest" : "frontend_es5"
),
publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
},
};
};
module.exports = {
resolve,
plugins,
optimization,
createAppConfig,
createDemoConfig,
};

View File

@ -74,9 +74,6 @@
content="https://www.home-assistant.io/images/default-social.png"
/>
<title>Home Assistant Demo</title>
<script src="./custom-elements-es5-adapter.js"></script>
<script src="./compatibility.js"></script>
<script src="./main.js" async></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
@ -98,6 +95,47 @@
<body>
<div id="ha-init-skeleton"></div>
<ha-demo></ha-demo>
<script>
function _ls(src) {
var doc = document.documentElement;
var script = doc.insertBefore(
document.createElement("script"),
doc.lastChild
);
script.type = "text/javascript";
script.src = src;
}
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: "shadow",
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
var webComponentsSupported =
"customElements" in window &&
"content" in document.createElement("template");
if (!webComponentsSupported) {
_ls("/static/polyfills/webcomponents-bundle.js");
}
var isS101 = /\s+Version\/10\.1(?:\.\d+)?\s+Safari\//.test(
navigator.userAgent
);
</script>
<script type="module" src="./frontend_latest/main.js"></script>
<script nomodule>
(function() {
// // Safari 10.1 supports type=module but ignores nomodule, so we add this check.
if (!isS101) {
_ls("./static/polyfills/custom-elements-es5-adapter.js");
_ls("./frontend_es5/compatibility.js");
_ls("./frontend_es5/main.js");
}
})();
</script>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function(d, t) {

View File

View File

@ -4,16 +4,6 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(dirname "$0")/../.."
OUTPUT_DIR=dist
rm -rf $OUTPUT_DIR
mkdir $OUTPUT_DIR
node script/gen-icons.js
cd ..
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
cd demo
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
./node_modules/.bin/gulp build-demo

View File

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

View File

@ -1,4 +1,4 @@
import { HomeAssistantAppEl } from "../../src/layouts/app/home-assistant";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import {
provideHass,
MockHomeAssistant,
@ -18,7 +18,7 @@ import { HomeAssistant } from "../../src/types";
import { mockFrontend } from "./stubs/frontend";
class HaDemo extends HomeAssistantAppEl {
protected async _handleConnProm() {
protected async _initialize() {
const initial: Partial<MockHomeAssistant> = {
panelUrl: (this as any).panelUrl,
// Override updateHass so that the correct hass lifecycle methods are called
@ -26,7 +26,7 @@ class HaDemo extends HomeAssistantAppEl {
this._updateHass(hassUpdate),
};
const hass = provideHass(this, initial);
const hass = (this.hass = provideHass(this, initial));
mockLovelace(hass);
mockAuth(hass);
mockTranslations(hass);

View File

@ -1,90 +1,13 @@
const path = require("path");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = require("../build-scripts/webpack.js");
const { createDemoConfig } = require("../build-scripts/webpack.js");
const isProd = process.env.NODE_ENV === "production";
// This file exists because we haven't migrated the stats script yet
const isProdBuild = process.env.NODE_ENV === "production";
const isStatsBuild = process.env.STATS === "1";
const chunkFilename =
isProd && !isStatsBuild ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = "/";
const latestBuild = false;
module.exports = {
mode: isProd ? "production" : "development",
devtool: isProd ? "cheap-source-map" : "inline-source-map",
entry: {
main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.ts",
},
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
optimization: webpackBase.optimization(latestBuild),
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProd ? "production" : "development"
),
}),
new CopyWebpackPlugin([
"public",
"../node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/@polymer/font-roboto-local/fonts",
to: "static/fonts",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
...webpackBase.plugins,
isProd &&
new WorkboxPlugin.GenerateSW({
swDest: "service_worker.js",
importWorkboxFrom: "local",
include: [],
}),
].filter(Boolean),
resolve: webpackBase.resolve,
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};
module.exports = createDemoConfig({
isProdBuild,
isStatsBuild,
latestBuild,
});

View File

@ -9,7 +9,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonAudio extends EventsMixin(PolymerElement) {
static get template() {

View File

@ -10,7 +10,7 @@ import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";

View File

@ -5,7 +5,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
static get template() {

View File

@ -4,7 +4,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import EventsMixin from "../../../src/mixins/events-mixin";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";

View File

@ -4,7 +4,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import EventsMixin from "../../../src/mixins/events-mixin";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() {

View File

@ -75,7 +75,7 @@
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^3.4.0",
"home-assistant-js-websocket": "^4.1.1",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.13.0",

View File

@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp build-release
./node_modules/.bin/gulp build-app

View File

@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp develop
./node_modules/.bin/gulp develop-app

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20190502.0",
version="20190507.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -3,7 +3,7 @@ import "@polymer/paper-item/paper-item-body";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
import "../components/ha-icon-next";

View File

@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../common/entity/compute_state_name";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { fetchThumbnailUrlWithCache } from "../data/camera";

View File

@ -10,7 +10,7 @@ import computeStateDomain from "../common/entity/compute_state_domain";
import computeStateName from "../common/entity/compute_state_name";
import stateMoreInfoType from "../common/entity/state_more_info_type";
import canToggleState from "../common/entity/can_toggle_state";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {

View File

@ -6,7 +6,7 @@ import "../components/state-history-charts";
import "../data/ha-state-history-data";
import computeStateName from "../common/entity/compute_state_name";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -9,7 +9,7 @@ import HassMediaPlayerEntity from "../util/hass-media-player-model";
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
import computeStateName from "../common/entity/compute_state_name";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
/*

View File

@ -5,7 +5,7 @@ import "../components/ha-card";
import "../components/ha-icon";
import computeStateName from "../common/entity/compute_state_name";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
class HaPlantCard extends EventsMixin(PolymerElement) {
static get template() {

View File

@ -6,7 +6,7 @@ import computeStateName from "../common/entity/compute_state_name";
import "../components/ha-card";
import "../components/ha-icon";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl";

View File

@ -1,4 +1,4 @@
export default (a, b) => {
export const compare = (a: string, b: string) => {
if (a < b) {
return -1;
}
@ -8,3 +8,6 @@ export default (a, b) => {
return 0;
};
export const caseInsensitiveCompare = (a: string, b: string) =>
compare(a.toLowerCase(), b.toLowerCase());

View File

@ -4,8 +4,14 @@
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
export default function debounce(func, wait, immediate) {
// tslint:disable-next-line: ban-types
export const debounce = <T extends Function>(
func: T,
wait,
immediate = false
): T => {
let timeout;
// @ts-ignore
return function(...args) {
// tslint:disable:no-this-assignment
// @ts-ignore
@ -23,4 +29,4 @@ export default function debounce(func, wait, immediate) {
func.apply(context, args);
}
};
}
};

View File

@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button";
import EventsMixin from "../../mixins/events-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -14,7 +14,7 @@ import {
} from "lit-element";
import { HomeAssistant } from "../../types";
import { HassEntity } from "home-assistant-js-websocket";
import { forwardHaptic } from "../../util/haptics";
import { forwardHaptic } from "../../data/haptics";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined && !STATES_OFF.includes(stateObj.state);
@ -90,7 +90,7 @@ class HaEntityToggle extends LitElement {
if (!this.hass || !this.stateObj) {
return;
}
forwardHaptic(this, "light");
forwardHaptic("light");
const stateDomain = computeStateDomain(this.stateObj);
let serviceDomain;
let service;

View File

@ -27,9 +27,11 @@ class HaCard extends LitElement {
color: var(--primary-text-color);
display: block;
transition: all 0.3s ease-out;
position: relative;
}
.header:not(:empty),
.header::slotted(*) {
.card-header,
:host ::slotted(.card-header) {
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
@ -38,12 +40,31 @@ class HaCard extends LitElement {
padding: 24px 16px 16px;
display: block;
}
:host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0px;
margin-top: -8px;
}
:host ::slotted(.card-content) {
padding: 16px;
}
:host ::slotted(.card-actions) {
border-top: 1px solid #e8e8e8;
padding: 5px 16px;
}
`;
}
protected render(): TemplateResult {
return html`
<slot class="header" name="header">${this.header}</slot>
${this.header
? html`
<div class="card-header">${this.header}</div>
`
: html``}
<slot></slot>
`;
}

View File

@ -3,7 +3,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
/**
* Color-picker custom element

View File

@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
class HaComboBox extends EventsMixin(PolymerElement) {
static get template() {

View File

@ -31,7 +31,9 @@ export class HaDateInput extends LitElement {
paper-input {
width: 30px;
text-align: center;
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
--paper-input-container-input_-_-moz-appearance: textfield;
--paper-input-container-shared-input-style_-_appearance: textfield;
--paper-input-container-input-webkit-spinner_-_-webkit-appearance: none;
--paper-input-container-input-webkit-spinner_-_margin: 0;
--paper-input-container-input-webkit-spinner_-_display: none;

View File

@ -8,7 +8,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-paper-slider";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -1,5 +1,5 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
let loaded = null;

View File

@ -4,7 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getAppKey } from "../data/notify_html5";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
export const pushSupported =
"serviceWorker" in navigator &&

View File

@ -2,7 +2,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
import isComponentLoaded from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";

View File

@ -3,7 +3,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin";
import { EventsMixin } from "../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -48,6 +48,7 @@ export class PaperTimeInput extends PolymerElement {
margin: 0;
display: none;
}
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
}
paper-dropdown-menu {

View File

@ -16,7 +16,7 @@ import {
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { User, fetchUsers } from "../../data/user";
import compare from "../../common/string/compare";
import { compare } from "../../common/string/compare";
class HaEntityPicker extends LitElement {
public hass?: HomeAssistant;

View File

@ -1,4 +1,7 @@
import { createCollection } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { compare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
export interface AreaRegistryEntry {
area_id: string;
@ -9,9 +12,6 @@ export interface AreaRegistryEntryMutableParams {
name: string;
}
export const fetchAreaRegistry = (hass: HomeAssistant) =>
hass.callWS<AreaRegistryEntry[]>({ type: "config/area_registry/list" });
export const createAreaRegistryEntry = (
hass: HomeAssistant,
values: AreaRegistryEntryMutableParams
@ -37,3 +37,33 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
type: "config/area_registry/delete",
area_id: areaId,
});
const fetchAreaRegistry = (conn) =>
conn
.sendMessagePromise({
type: "config/area_registry/list",
})
.then((areas) => areas.sort((ent1, ent2) => compare(ent1.name, ent2.name)));
const subscribeAreaRegistryUpdates = (conn, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchAreaRegistry(conn).then((areas) => store.setState(areas, true)),
500,
true
),
"area_registry_updated"
);
export const subscribeAreaRegistry = (
hass: HomeAssistant,
onChange: (areas: AreaRegistryEntry[]) => void
) =>
createCollection<AreaRegistryEntry[]>(
"_areaRegistry",
fetchAreaRegistry,
subscribeAreaRegistryUpdates,
hass.connection,
onChange
);

View File

@ -14,6 +14,8 @@ export interface SignedPath {
path: string;
}
export const hassUrl = `${location.protocol}//${location.host}`;
export const getSignedPath = (
hass: HomeAssistant,
path: string

View File

@ -1,4 +1,17 @@
import { HomeAssistant } from "../types";
import { createCollection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
import { LocalizeFunc } from "../common/translations/localize";
export interface ConfigEntry {
entry_id: string;
domain: string;
title: string;
source: string;
state: string;
connection_class: string;
supports_options: boolean;
}
export interface FieldSchema {
name: string;
@ -9,7 +22,10 @@ export interface FieldSchema {
export interface ConfigFlowProgress {
flow_id: string;
handler: string;
context: { [key: string]: any };
context: {
title_placeholders: { [key: string]: string };
[key: string]: any;
};
}
export interface ConfigFlowStepForm {
@ -74,3 +90,53 @@ export const getConfigFlowsInProgress = (hass: HomeAssistant) =>
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
const fetchConfigFlowInProgress = (conn) =>
conn.sendMessagePromise({
type: "config/entity_registry/list",
});
const subscribeConfigFlowInProgressUpdates = (conn, store) =>
debounce(
conn.subscribeEvents(
() =>
fetchConfigFlowInProgress(conn).then((flows) =>
store.setState(flows, true)
),
500,
true
),
"config_entry_discovered"
);
export const subscribeConfigFlowInProgress = (
hass: HomeAssistant,
onChange: (flows: ConfigFlowProgress[]) => void
) =>
createCollection<ConfigFlowProgress[]>(
"_configFlowProgress",
fetchConfigFlowInProgress,
subscribeConfigFlowInProgressUpdates,
hass.connection,
onChange
);
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
export const localizeConfigFlowTitle = (
localize: LocalizeFunc,
flow: ConfigFlowProgress
) => {
const placeholders = flow.context.title_placeholders || {};
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
return localize(`component.${flow.handler}.config.title`);
}
const args: string[] = [];
placeholderKeys.forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(`component.${flow.handler}.config.flow_title`, ...args);
};

View File

@ -0,0 +1,22 @@
/**
* Broadcast connection status updates
*/
import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
export type ConnectionStatus = "connected" | "auth-invalid" | "disconnected";
declare global {
// for fire event
interface HASSDomEvents {
"connection-status": ConnectionStatus;
}
interface GlobalEventHandlersEventMap {
"connection-status": HASSDomEvent<ConnectionStatus>;
}
}
export const broadcastConnectionStatus = (status: ConnectionStatus) => {
fireEvent(window, "connection-status", status);
};

View File

@ -1,4 +1,6 @@
import { HomeAssistant } from "../types";
import { createCollection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
export interface DeviceRegistryEntry {
id: string;
@ -18,9 +20,6 @@ export interface DeviceRegistryEntryMutableParams {
name_by_user?: string;
}
export const fetchDeviceRegistry = (hass: HomeAssistant) =>
hass.callWS<DeviceRegistryEntry[]>({ type: "config/device_registry/list" });
export const updateDeviceRegistryEntry = (
hass: HomeAssistant,
deviceId: string,
@ -31,3 +30,33 @@ export const updateDeviceRegistryEntry = (
device_id: deviceId,
...updates,
});
const fetchDeviceRegistry = (conn) =>
conn.sendMessagePromise({
type: "config/device_registry/list",
});
const subscribeDeviceRegistryUpdates = (conn, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchDeviceRegistry(conn).then((devices) =>
store.setState(devices, true)
),
500,
true
),
"device_registry_updated"
);
export const subscribeDeviceRegistry = (
hass: HomeAssistant,
onChange: (devices: DeviceRegistryEntry[]) => void
) =>
createCollection<DeviceRegistryEntry[]>(
"_dr",
fetchDeviceRegistry,
subscribeDeviceRegistryUpdates,
hass.connection,
onChange
);

View File

@ -1,5 +1,7 @@
import { createCollection } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import computeStateName from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
export interface EntityRegistryEntry {
entity_id: string;
@ -26,11 +28,6 @@ export const computeEntityRegistryName = (
return state ? computeStateName(state) : null;
};
export const fetchEntityRegistry = (
hass: HomeAssistant
): Promise<EntityRegistryEntry[]> =>
hass.callWS<EntityRegistryEntry[]>({ type: "config/entity_registry/list" });
export const updateEntityRegistryEntry = (
hass: HomeAssistant,
entityId: string,
@ -50,3 +47,33 @@ export const removeEntityRegistryEntry = (
type: "config/entity_registry/remove",
entity_id: entityId,
});
const fetchEntityRegistry = (conn) =>
conn.sendMessagePromise({
type: "config/entity_registry/list",
});
const subscribeEntityRegistryUpdates = (conn, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchEntityRegistry(conn).then((entities) =>
store.setState(entities, true)
),
500,
true
),
"entity_registry_updated"
);
export const subscribeEntityRegistry = (
hass: HomeAssistant,
onChange: (entities: EntityRegistryEntry[]) => void
) =>
createCollection<EntityRegistryEntry[]>(
"_entityRegistry",
fetchEntityRegistry,
subscribeEntityRegistryUpdates,
hass.connection,
onChange
);

View File

@ -1,5 +1,5 @@
/**
* Utility function that enables haptic feedback
* Broadcast haptic feedback requests
*/
import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
@ -27,6 +27,6 @@ declare global {
}
}
export const forwardHaptic = (el: HTMLElement, hapticType: HapticType) => {
fireEvent(el, "haptic", hapticType);
export const forwardHaptic = (hapticType: HapticType) => {
fireEvent(window, "haptic", hapticType);
};

View File

@ -1,14 +1,26 @@
import { handleFetchPromise } from "../util/hass-call-api";
import { HomeAssistant } from "../types";
export interface OnboardingStep {
step: string;
done: boolean;
export interface OnboardingUserStepResponse {
auth_code: string;
}
interface UserStepResponse {
export interface OnboardingIntegrationStepResponse {
auth_code: string;
}
export interface OnboardingResponses {
user: OnboardingUserStepResponse;
integration: OnboardingIntegrationStepResponse;
}
export type ValidOnboardingStep = keyof OnboardingResponses;
export interface OnboardingStep {
step: ValidOnboardingStep;
done: boolean;
}
export const fetchOnboardingOverview = () =>
fetch("/api/onboarding", { credentials: "same-origin" });
@ -17,11 +29,22 @@ export const onboardUserStep = (params: {
name: string;
username: string;
password: string;
language: string;
}) =>
handleFetchPromise<UserStepResponse>(
handleFetchPromise<OnboardingUserStepResponse>(
fetch("/api/onboarding/users", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify(params),
})
);
export const onboardIntegrationStep = (
hass: HomeAssistant,
params: { client_id: string }
) =>
hass.callApi<OnboardingIntegrationStepResponse>(
"POST",
"onboarding/integration",
params
);

View File

@ -12,6 +12,7 @@ import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import "../../components/ha-form";
import "../../components/ha-markdown";
@ -37,10 +38,14 @@ import "./step-flow-abort";
import "./step-flow-create-entry";
import {
DeviceRegistryEntry,
fetchDeviceRegistry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { HomeAssistant } from "../../types";
import { caseInsensitiveCompare } from "../../common/string/compare";
let instance = 0;
@ -57,30 +62,19 @@ declare global {
@customElement("dialog-config-flow")
class ConfigFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property()
private _params?: HaConfigFlowParams;
@property()
private _loading = true;
@property() private _params?: HaConfigFlowParams;
@property() private _loading = true;
private _instance = instance;
@property()
private _step:
@property() private _step:
| ConfigFlowStep
| undefined
// Null means we need to pick a config flow
| null;
@property()
private _devices?: DeviceRegistryEntry[];
@property()
private _areas?: AreaRegistryEntry[];
@property()
private _handlers?: string[];
@property() private _devices?: DeviceRegistryEntry[];
@property() private _areas?: AreaRegistryEntry[];
@property() private _handlers?: string[];
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
public async showDialog(params: HaConfigFlowParams): Promise<void> {
this._params = params;
@ -95,7 +89,13 @@ class ConfigFlowDialog extends LitElement {
this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog());
try {
this._handlers = await getConfigFlowHandlers(this.hass);
this._handlers = (await getConfigFlowHandlers(this.hass)).sort(
(handlerA, handlerB) =>
caseInsensitiveCompare(
this.hass.localize(`component.${handlerA}.config.title`),
this.hass.localize(`component.${handlerB}.config.title`)
)
);
} finally {
this._loading = false;
}
@ -196,6 +196,10 @@ class ConfigFlowDialog extends LitElement {
this._fetchDevices(this._step.result);
this._fetchAreas();
}
if (changedProps.has("_devices") && this._dialog) {
this._scheduleCenterDialog();
}
}
private _scheduleCenterDialog() {
@ -207,16 +211,17 @@ class ConfigFlowDialog extends LitElement {
}
private async _fetchDevices(configEntryId) {
// Wait 5 seconds to give integrations time to find devices
await new Promise((resolve) => setTimeout(resolve, 5000));
const devices = await fetchDeviceRegistry(this.hass);
this._unsubDevices = subscribeDeviceRegistry(this.hass, (devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
});
}
private async _fetchAreas() {
this._areas = await fetchAreaRegistry(this.hass);
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
this._areas = areas;
});
}
private async _processStep(
@ -261,6 +266,14 @@ class ConfigFlowDialog extends LitElement {
this._step = undefined;
this._params = undefined;
this._devices = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {

View File

@ -169,6 +169,18 @@ class StepFlowCreateEntry extends LitElement {
.buttons > *:last-child {
margin-left: auto;
}
paper-dropdown-menu-light {
cursor: pointer;
}
paper-item {
cursor: pointer;
white-space: nowrap;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.device {
width: auto;
}
}
`,
];
}

View File

@ -4,7 +4,7 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoAlarmControlPanel extends LocalizeMixin(

View File

@ -15,7 +15,7 @@ import attributeClassNames from "../../../common/entity/attribute_class_names";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTLDirection } from "../../../common/util/compute_rtl";

View File

@ -10,7 +10,7 @@ import "../../../components/ha-attributes";
import "../../../components/ha-paper-dropdown-menu";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*

View File

@ -10,7 +10,7 @@ import "../../../components/ha-labeled-slider";
import "../../../components/ha-paper-dropdown-menu";
import featureClassNames from "../../../common/entity/feature_class_names";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
const FEATURE_CLASS_NAMES = {
@ -182,12 +182,17 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
dynamic-align=""
label="[[localize('ui.card.light.effect')]]"
>
<paper-listbox slot="dropdown-content" selected="{{effectIndex}}">
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.effect]]"
on-selected-changed="effectChanged"
attr-for-selected="item-name"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.effect_list]]"
>
<paper-item>[[item]]</paper-item>
<paper-item item-name$="[[item]]">[[item]]</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
@ -212,12 +217,6 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
observer: "stateObjChanged",
},
effectIndex: {
type: Number,
value: -1,
observer: "effectChanged",
},
brightnessSliderValue: {
type: Number,
value: 0,
@ -264,13 +263,6 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
s: newVal.attributes.hs_color[1] / 100,
};
}
if (newVal.attributes.effect_list) {
props.effectIndex = newVal.attributes.effect_list.indexOf(
newVal.attributes.effect
);
} else {
props.effectIndex = -1;
}
}
this.setProperties(props);
@ -293,17 +285,15 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
return classes.join(" ");
}
effectChanged(effectIndex) {
var effectInput;
// Selected Option will transition to '' before transitioning to new value
if (effectIndex === "" || effectIndex === -1) return;
effectChanged(ev) {
var oldVal = this.stateObj.attributes.effect;
var newVal = ev.detail.value;
effectInput = this.stateObj.attributes.effect_list[effectIndex];
if (effectInput === this.stateObj.attributes.effect) return;
if (!newVal || oldVal === newVal) return;
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
effect: effectInput,
effect: newVal,
});
}

View File

@ -12,7 +12,7 @@ import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
import attributeClassNames from "../../../common/entity/attribute_class_names";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTLDirection } from "../../../common/util/compute_rtl";

View File

@ -109,12 +109,17 @@ class MoreInfoVacuum extends PolymerElement {
dynamic-align=""
label="Fan speed"
>
<paper-listbox slot="dropdown-content" selected="{{fanSpeedIndex}}">
<paper-listbox
slot="dropdown-content"
selected="[[stateObj.attributes.fan_speed]]"
on-selected-changed="fanSpeedChanged"
attr-for-selected="item-name"
>
<template
is="dom-repeat"
items="[[stateObj.attributes.fan_speed_list]]"
>
<paper-item>[[item]]</paper-item>
<paper-item item-name$="[[item]]">[[item]]</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
@ -150,12 +155,6 @@ class MoreInfoVacuum extends PolymerElement {
stateObj: {
type: Object,
},
fanSpeedIndex: {
type: Number,
value: -1,
observer: "fanSpeedChanged",
},
};
}
@ -206,17 +205,15 @@ class MoreInfoVacuum extends PolymerElement {
);
}
fanSpeedChanged(fanSpeedIndex) {
var fanSpeedInput;
// Selected Option will transition to '' before transitioning to new value
if (fanSpeedIndex === "" || fanSpeedIndex === -1) return;
fanSpeedChanged(ev) {
var oldVal = this.stateObj.attributes.fan_speed;
var newVal = ev.detail.value;
fanSpeedInput = this.stateObj.attributes.fan_speed_list[fanSpeedIndex];
if (fanSpeedInput === this.stateObj.attributes.fan_speed) return;
if (!newVal || oldVal === newVal) return;
this.hass.callService("vacuum", "set_fan_speed", {
entity_id: this.stateObj.entity_id,
fan_speed: fanSpeedInput,
fan_speed: newVal,
});
}

View File

@ -14,7 +14,7 @@ import "../../../components/ha-paper-dropdown-menu";
import featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
/*

View File

@ -15,7 +15,7 @@ import computeStateName from "../../common/entity/compute_state_name";
import computeStateDomain from "../../common/entity/compute_state_domain";
import isComponentLoaded from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
import EventsMixin from "../../mixins/events-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"];

View File

@ -5,7 +5,7 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../mixins/events-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import computeStateName from "../../common/entity/compute_state_name";

View File

@ -11,7 +11,7 @@ import "../resources/roboto";
// properly into iron-meta, which is used to transfer iconsets to iron-icon.
import "../components/ha-iconset-svg";
import "../layouts/app/home-assistant";
import "../layouts/home-assistant";
setPassiveTouchGestures(true);
/* LastPass createElement workaround. See #428 */

View File

@ -14,6 +14,7 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
import { HomeAssistant } from "../types";
import { hassUrl } from "../data/auth";
declare global {
interface Window {
@ -21,7 +22,6 @@ declare global {
}
}
const hassUrl = `${location.protocol}//${location.host}`;
const isExternal = location.search.includes("external_auth=1");
const authProm = isExternal

View File

@ -15,7 +15,7 @@ function initRouting() {
// Get api from network.
workbox.routing.registerRoute(
new RegExp(`${location.host}/api/.*`),
new RegExp(`${location.host}/(api|auth)/.*`),
new workbox.strategies.NetworkOnly()
);

View File

@ -1,7 +1,7 @@
import { ExternalMessaging } from "./external_messaging";
export const externalForwardConnectionEvents = (bus: ExternalMessaging) => {
document.addEventListener("connection-status", (ev) =>
window.addEventListener("connection-status", (ev) =>
bus.fireMessage({
type: "connection-status",
payload: { event: ev.detail },
@ -10,6 +10,6 @@ export const externalForwardConnectionEvents = (bus: ExternalMessaging) => {
};
export const externalForwardHaptics = (bus: ExternalMessaging) =>
document.addEventListener("haptic", (ev) =>
window.addEventListener("haptic", (ev) =>
bus.fireMessage({ type: "haptic", payload: { hapticType: ev.detail } })
);

View File

@ -44,7 +44,7 @@
Home Assistant
</div>
<ha-onboarding>Initializing</ha-onboarding>
<ha-onboarding></ha-onboarding>
</div>
<%= renderTemplate('_js_base') %>

View File

@ -1,161 +0,0 @@
import {
ERR_INVALID_AUTH,
subscribeEntities,
subscribeConfig,
subscribeServices,
callService,
} from "home-assistant-js-websocket";
import { translationMetadata } from "../../resources/translations-metadata";
import LocalizeMixin from "../../mixins/localize-mixin";
import EventsMixin from "../../mixins/events-mixin";
import { getState } from "../../util/ha-pref-storage";
import { getLocalLanguage } from "../../util/hass-translation";
import { fetchWithAuth } from "../../util/fetch-with-auth";
import hassCallApi from "../../util/hass-call-api";
import { subscribePanels } from "../../data/ws-panels";
import { forwardHaptic } from "../../util/haptics";
import { fireEvent } from "../../common/dom/fire_event";
export default (superClass) =>
class extends EventsMixin(LocalizeMixin(superClass)) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._handleConnProm();
}
async _handleConnProm() {
let auth;
let conn;
try {
const result = await window.hassConnection;
auth = result.auth;
conn = result.conn;
} catch (err) {
this._error = true;
return;
}
this.hass = Object.assign(
{
auth,
connection: conn,
connected: true,
states: null,
config: null,
themes: null,
panels: null,
services: null,
user: null,
panelUrl: this._panelUrl,
language: getLocalLanguage(),
// If resources are already loaded, don't discard them
resources: (this.hass && this.hass.resources) || null,
localize: () => "",
translationMetadata: translationMetadata,
dockedSidebar: false,
moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => {
if (__DEV__) {
// eslint-disable-next-line
console.log("Calling service", domain, service, serviceData);
}
try {
await callService(conn, domain, service, serviceData);
} catch (err) {
if (__DEV__) {
// eslint-disable-next-line
console.error(
"Error calling service",
domain,
service,
serviceData,
err
);
}
forwardHaptic(this, "error");
const message =
this.hass.localize(
"ui.notification_toast.service_call_failed",
"service",
`${domain}/${service}`
) + ` ${err.message}`;
this.fire("hass-notification", { message });
throw err;
}
},
callApi: async (method, path, parameters) =>
hassCallApi(auth, method, path, parameters),
fetchWithAuth: (path, init) =>
fetchWithAuth(auth, `${auth.data.hassUrl}${path}`, init),
// For messages that do not get a response
sendWS: (msg) => {
if (__DEV__) {
// eslint-disable-next-line
console.log("Sending", msg);
}
conn.sendMessage(msg);
},
// For messages that expect a response
callWS: (msg) => {
if (__DEV__) {
/* eslint-disable no-console */
console.log("Sending", msg);
}
const resp = conn.sendMessagePromise(msg);
if (__DEV__) {
resp.then(
(result) => console.log("Received", result),
(err) => console.error("Error", err)
);
}
return resp;
},
},
getState()
);
this.hassConnected();
}
hassConnected() {
super.hassConnected();
const conn = this.hass.connection;
fireEvent(document, "connection-status", "connected");
conn.addEventListener("ready", () => this.hassReconnected());
conn.addEventListener("disconnected", () => this.hassDisconnected());
// If we reconnect after losing connection and auth is no longer valid.
conn.addEventListener("reconnect-error", (_conn, err) => {
if (err === ERR_INVALID_AUTH) {
fireEvent(document, "connection-status", "auth-invalid");
location.reload();
}
});
subscribeEntities(conn, (states) => this._updateHass({ states }));
subscribeConfig(conn, (config) => this._updateHass({ config }));
subscribeServices(conn, (services) => this._updateHass({ services }));
subscribePanels(conn, (panels) => this._updateHass({ panels }));
}
hassReconnected() {
super.hassReconnected();
this._updateHass({ connected: true });
fireEvent(document, "connection-status", "connected");
}
hassDisconnected() {
super.hassDisconnected();
this._updateHass({ connected: false });
fireEvent(document, "connection-status", "disconnected");
}
};

View File

@ -1,45 +1,20 @@
import "@polymer/app-route/app-location";
import { html, LitElement, PropertyValues, css, property } from "lit-element";
import "../home-assistant-main";
import "../ha-init-page";
import "../../resources/ha-style";
import { registerServiceWorker } from "../../util/register-service-worker";
import { DEFAULT_PANEL } from "../../common/const";
import "./home-assistant-main";
import "./ha-init-page";
import "../resources/ha-style";
import { registerServiceWorker } from "../util/register-service-worker";
import { DEFAULT_PANEL } from "../common/const";
import HassBaseMixin from "./hass-base-mixin";
import AuthMixin from "./auth-mixin";
import TranslationsMixin from "./translations-mixin";
import ThemesMixin from "./themes-mixin";
import MoreInfoMixin from "./more-info-mixin";
import SidebarMixin from "./sidebar-mixin";
import { dialogManagerMixin } from "./dialog-manager-mixin";
import ConnectionMixin from "./connection-mixin";
import NotificationMixin from "./notification-mixin";
import DisconnectToastMixin from "./disconnect-toast-mixin";
import { urlSyncMixin } from "./url-sync-mixin";
import { Route, HomeAssistant } from "../../types";
import { navigate } from "../../common/navigate";
import { Route, HomeAssistant } from "../types";
import { navigate } from "../common/navigate";
import { HassElement } from "../state/hass-element";
(LitElement.prototype as any).html = html;
(LitElement.prototype as any).css = css;
const ext = <T>(baseClass: T, mixins): T =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
AuthMixin,
ThemesMixin,
TranslationsMixin,
MoreInfoMixin,
SidebarMixin,
DisconnectToastMixin,
ConnectionMixin,
NotificationMixin,
dialogManagerMixin,
urlSyncMixin,
]) {
export class HomeAssistantAppEl extends HassElement {
@property() private _route?: Route;
@property() private _error?: boolean;
@property() private _panelUrl?: string;
@ -69,6 +44,7 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initialize();
setTimeout(registerServiceWorker, 1000);
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
@ -86,6 +62,16 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
}
}
protected async _initialize() {
try {
const { auth, conn } = await window.hassConnection;
this.initializeHass(auth, conn);
} catch (err) {
this._error = true;
return;
}
}
private _routeChanged(ev) {
const route = ev.detail.value as Route;
// If it's the first route that we process,

View File

@ -1,7 +1,7 @@
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
import { PaperDialogBehavior } from "@polymer/paper-dialog-behavior/paper-dialog-behavior";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import EventsMixin from "./events-mixin";
import { EventsMixin } from "./events-mixin";
/**
* @polymerMixin
* @appliesMixin EventsMixin

View File

@ -33,7 +33,7 @@ import { fireEvent } from "../common/dom/fire_event";
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/* @polymerMixin */
export default dedupingMixin(
export const EventsMixin = dedupingMixin(
(superClass) =>
class extends superClass {
/**

View File

@ -31,10 +31,10 @@ export const localizeLiteBaseMixin = (superClass) =>
return;
}
this._updateResources();
this._downloadResources();
}
private async _updateResources() {
private async _downloadResources() {
const { language, data } = await getTranslation(
this.translationFragment,
this.language

View File

@ -1,146 +1,95 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import {
LitElement,
CSSResult,
css,
html,
PropertyValues,
property,
customElement,
TemplateResult,
property,
} from "lit-element";
import { genClientId } from "home-assistant-js-websocket";
import {
getAuth,
createConnection,
genClientId,
Auth,
} from "home-assistant-js-websocket";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { OnboardingStep, onboardUserStep } from "../data/onboarding";
import { PolymerChangedEvent } from "../polymer-types";
import {
OnboardingStep,
ValidOnboardingStep,
OnboardingResponses,
fetchOnboardingOverview,
} from "../data/onboarding";
import { registerServiceWorker } from "../util/register-service-worker";
import { HASSDomEvent } from "../common/dom/fire_event";
import "./onboarding-create-user";
import "./onboarding-loading";
import { hassUrl } from "../data/auth";
import { HassElement } from "../state/hass-element";
@customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
public translationFragment = "page-onboarding";
interface OnboardingEvent<T extends ValidOnboardingStep> {
type: T;
result: OnboardingResponses[T];
}
@property() private _name = "";
@property() private _username = "";
@property() private _password = "";
@property() private _passwordConfirm = "";
@property() private _loading = false;
@property() private _errorMsg?: string = undefined;
protected render(): TemplateResult | void {
return html`
<p>
${this.localize("ui.panel.page-onboarding.intro")}
</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
declare global {
interface HASSDomEvents {
"onboarding-step": OnboardingEvent<ValidOnboardingStep>;
}
interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>;
}
}
<form>
<paper-input
autofocus
name="name"
label="${this.localize("ui.panel.page-onboarding.user.data.name")}"
.value=${this._name}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
@blur=${this._maybePopulateUsername}
></paper-input>
@customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
public translationFragment = "page-onboarding";
<paper-input
name="username"
label="${this.localize("ui.panel.page-onboarding.user.data.username")}"
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
@property() private _loading = false;
@property() private _steps?: OnboardingStep[];
<paper-input
name="password"
label="${this.localize("ui.panel.page-onboarding.user.data.password")}"
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
protected render(): TemplateResult | void {
const step = this._curStep()!;
<paper-input
name="passwordConfirm"
label="${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}"
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}"
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
if (this._loading || !step) {
return html`
<onboarding-loading></onboarding-loading>
`;
} else if (step.step === "user") {
return html`
<onboarding-create-user
.localize=${this.localize}
.language=${this.language}
></onboarding-create-user>
`;
} else if (step.step === "integration") {
return html`
<onboarding-integrations
.hass=${this.hass}
.onboardingLocalize=${this.localize}
></onboarding-integrations>
`;
}
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
this._fetchOnboardingSteps();
import("./onboarding-integrations");
registerServiceWorker(false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
}
private _curStep() {
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
}
private async _fetchOnboardingSteps() {
try {
const response = await window.stepsPromise;
const response = await (window.stepsPromise || fetchOnboardingOverview());
if (response.status === 404) {
// We don't load the component when onboarding is done
document.location.href = "/";
document.location.assign("/");
return;
}
@ -148,83 +97,73 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
if (steps.every((step) => step.done)) {
// Onboarding is done!
document.location.href = "/";
document.location.assign("/");
return;
}
if (steps[0].done) {
// First step is already done, so we need to get auth somewhere else.
const auth = await getAuth({
hassUrl,
});
await this._connectHass(auth);
}
this._steps = steps;
} catch (err) {
alert("Something went wrong loading loading onboarding, try refreshing");
}
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
}
private _maybePopulateUsername(): void {
if (this._username) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private async _submitForm(): Promise<void> {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
private async _handleStepDone(
ev: HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>
) {
const stepResult = ev.detail;
this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : step
);
if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"];
this._loading = true;
this._errorMsg = "";
try {
const clientId = genClientId();
const { auth_code } = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
const auth = await getAuth({
hassUrl,
authCode: result.auth_code,
});
await this._connectHass(auth);
} catch (err) {
alert("Ah snap, something went wrong!");
location.reload();
} finally {
this._loading = false;
}
} else if (stepResult.type === "integration") {
const result = stepResult.result as OnboardingResponses["integration"];
this._loading = true;
// Revoke current auth token.
await this.hass!.auth.revoke();
const state = btoa(
JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`,
clientId,
clientId: genClientId(),
})
);
document.location.href = `/?auth_callback=1&code=${encodeURIComponent(
auth_code
)}&state=${state}`;
} catch (err) {
// tslint:disable-next-line
console.error(err);
this._loading = false;
this._errorMsg = err.message;
document.location.assign(
`/?auth_callback=1&code=${encodeURIComponent(
result.auth_code
)}&state=${state}`
);
}
}
static get styles(): CSSResult {
return css`
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
`;
private async _connectHass(auth: Auth) {
const conn = await createConnection({ auth });
this.initializeHass(auth, conn);
// Load config strings for integrations
(this as any)._loadFragmentTranslations(this.hass!.language, "config");
}
}

View File

@ -0,0 +1,86 @@
import {
LitElement,
TemplateResult,
html,
customElement,
property,
CSSResult,
css,
} from "lit-element";
import "../components/ha-icon";
@customElement("integration-badge")
class IntegrationBadge extends LitElement {
@property() public icon!: string;
@property() public title!: string;
@property() public badgeIcon?: string;
@property({ type: Boolean, reflect: true }) public clickable = false;
protected render(): TemplateResult | void {
return html`
<div class="icon">
<iron-icon .icon=${this.icon}></iron-icon>
${this.badgeIcon
? html`
<ha-icon class="badge" .icon=${this.badgeIcon}></ha-icon>
`
: ""}
</div>
<div class="title">${this.title}</div>
`;
}
static get styles(): CSSResult {
return css`
:host {
display: inline-flex;
flex-direction: column;
text-align: center;
color: var(--primary-text-color);
}
:host([clickable]) {
color: var(--primary-text-color);
}
.icon {
position: relative;
margin: 0 auto 8px;
height: 40px;
width: 40px;
border-radius: 50%;
border: 1px solid var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
}
:host([clickable]) .icon {
border-color: var(--primary-color);
border-width: 2px;
}
.badge {
position: absolute;
color: var(--primary-color);
bottom: -5px;
right: -5px;
background-color: white;
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
}
.title {
min-height: 2.3em;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"integration-badge": IntegrationBadge;
}
}

View File

@ -0,0 +1,210 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import {
LitElement,
CSSResult,
css,
html,
PropertyValues,
property,
customElement,
TemplateResult,
} from "lit-element";
import { genClientId } from "home-assistant-js-websocket";
import { onboardUserStep } from "../data/onboarding";
import { PolymerChangedEvent } from "../polymer-types";
import { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
@customElement("onboarding-create-user")
class OnboardingCreateUser extends LitElement {
@property() public localize!: LocalizeFunc;
@property() public language!: string;
@property() private _name = "";
@property() private _username = "";
@property() private _password = "";
@property() private _passwordConfirm = "";
@property() private _loading = false;
@property() private _errorMsg?: string = undefined;
protected render(): TemplateResult | void {
return html`
<p>
${this.localize("ui.panel.page-onboarding.intro")}
</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
}
<form>
<paper-input
name="name"
label="${this.localize("ui.panel.page-onboarding.user.data.name")}"
.value=${this._name}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
name="username"
label="${this.localize("ui.panel.page-onboarding.user.data.username")}"
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="password"
label="${this.localize("ui.panel.page-onboarding.user.data.password")}"
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="passwordConfirm"
label="${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}"
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}"
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
setTimeout(
() => this.shadowRoot!.querySelector("paper-input")!.focus(),
100
);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
}
private _maybePopulateUsername(): void {
if (this._username) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private async _submitForm(): Promise<void> {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
this._loading = true;
this._errorMsg = "";
try {
const clientId = genClientId();
const result = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
language: this.language,
});
fireEvent(this, "onboarding-step", {
type: "user",
result,
});
} catch (err) {
// tslint:disable-next-line
console.error(err);
this._loading = false;
this._errorMsg = err.body.message;
}
}
static get styles(): CSSResult {
return css`
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-create-user": OnboardingCreateUser;
}
}

View File

@ -0,0 +1,196 @@
import {
LitElement,
TemplateResult,
html,
customElement,
PropertyValues,
property,
CSSResult,
css,
} from "lit-element";
import "@material/mwc-button/mwc-button";
import {
loadConfigFlowDialog,
showConfigFlowDialog,
} from "../dialogs/config-flow/show-dialog-config-flow";
import { HomeAssistant } from "../types";
import {
getConfigFlowsInProgress,
getConfigEntries,
ConfigEntry,
ConfigFlowProgress,
localizeConfigFlowTitle,
} from "../data/config_entries";
import { compare } from "../common/string/compare";
import "./integration-badge";
import { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import { fireEvent } from "../common/dom/fire_event";
import { onboardIntegrationStep } from "../data/onboarding";
import { genClientId } from "home-assistant-js-websocket";
@customElement("onboarding-integrations")
class OnboardingIntegrations extends LitElement {
@property() public hass!: HomeAssistant;
@property() public onboardingLocalize!: LocalizeFunc;
@property() private _entries?: ConfigEntry[];
@property() private _discovered?: ConfigFlowProgress[];
private _unsubEvents?: () => void;
public connectedCallback() {
super.connectedCallback();
this.hass.connection
.subscribeEvents(
debounce(() => this._loadData(), 500),
"config_entry_discovered"
)
.then((unsub) => {
this._unsubEvents = unsub;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubEvents) {
this._unsubEvents();
}
}
protected render(): TemplateResult | void {
if (!this._entries || !this._discovered) {
return html``;
}
// Render discovered and existing entries together sorted by localized title.
const entries: Array<[string, TemplateResult]> = this._entries.map(
(entry) => {
const title = this.hass.localize(
`component.${entry.domain}.config.title`
);
return [
title,
html`
<integration-badge
.title=${title}
icon="hass:check"
></integration-badge>
`,
];
}
);
const discovered: Array<[string, TemplateResult]> = this._discovered.map(
(flow) => {
const title = localizeConfigFlowTitle(this.hass.localize, flow);
return [
title,
html`
<button .flowId=${flow.flow_id} @click=${this._continueFlow}>
<integration-badge
clickable
.title=${title}
icon="hass:plus"
></integration-badge>
</button>
`,
];
}
);
const content = [...entries, ...discovered]
.sort((a, b) => compare(a[0], b[0]))
.map((item) => item[1]);
return html`
<p>
${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")}
</p>
<div class="badges">
${content}
<button @click=${this._createFlow}>
<integration-badge
clickable
title=${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations"
)}
icon="hass:dots-horizontal"
></integration-badge>
</button>
</div>
<div class="footer">
<mwc-button @click=${this._finish}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish"
)}
</mwc-button>
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
loadConfigFlowDialog();
this._loadData();
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
}
private _createFlow() {
showConfigFlowDialog(this, {
dialogClosedCallback: () => this._loadData(),
});
}
private _continueFlow(ev) {
showConfigFlowDialog(this, {
continueFlowId: ev.currentTarget.flowId,
dialogClosedCallback: () => this._loadData(),
});
}
private async _loadData() {
const [discovered, entries] = await Promise.all([
getConfigFlowsInProgress(this.hass!),
getConfigEntries(this.hass!),
]);
this._discovered = discovered;
this._entries = entries;
}
private async _finish() {
const result = await onboardIntegrationStep(this.hass, {
client_id: genClientId(),
});
fireEvent(this, "onboarding-step", {
type: "integration",
result,
});
}
static get styles(): CSSResult {
return css`
.badges {
margin-top: 24px;
}
.badges > * {
width: 24%;
min-width: 90px;
margin-bottom: 24px;
}
button {
display: inline-block;
cursor: pointer;
padding: 0;
border: 0;
background: 0;
font: inherit;
}
.footer {
text-align: right;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-integrations": OnboardingIntegrations;
}
}

View File

@ -0,0 +1,70 @@
import {
LitElement,
TemplateResult,
html,
customElement,
CSSResult,
css,
} from "lit-element";
@customElement("onboarding-loading")
class OnboardingLoading extends LitElement {
protected render(): TemplateResult | void {
return html`
<div class="loader"></div>
`;
}
static get styles(): CSSResult {
return css`
/* MIT License (MIT). Copyright (c) 2014 Luke Haas */
.loader,
.loader:after {
border-radius: 50%;
width: 40px;
height: 40px;
}
.loader {
margin: 60px auto;
font-size: 4px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(3, 169, 244, 0.2);
border-right: 1.1em solid rgba(3, 169, 244, 0.2);
border-bottom: 1.1em solid rgba(3, 169, 244, 0.2);
border-left: 1.1em solid rgb(3, 168, 244);
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.4s infinite linear;
animation: load8 1.4s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-loading": OnboardingLoading;
}
}

View File

@ -7,7 +7,7 @@ import React from "react";
/* eslint-enable */
import BigCalendar from "react-big-calendar";
import moment from "moment";
import EventsMixin from "../../mixins/events-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
import "../../resources/ha-style";

View File

@ -4,24 +4,23 @@ import {
html,
css,
CSSResult,
PropertyDeclarations,
property,
} from "lit-element";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-fab/paper-fab";
import { HomeAssistant } from "../../../types";
import {
AreaRegistryEntry,
fetchAreaRegistry,
updateAreaRegistryEntry,
deleteAreaRegistryEntry,
createAreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import "../../../components/ha-card";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import compare from "../../../common/string/compare";
import "../ha-config-section";
import {
showAreaRegistryDetailDialog,
@ -29,22 +28,23 @@ import {
} from "./show-dialog-area-registry-detail";
import { classMap } from "lit-html/directives/class-map";
import { computeRTL } from "../../../common/util/compute_rtl";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
class HaConfigAreaRegistry extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _items?: AreaRegistryEntry[];
@property() public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() private _areas?: AreaRegistryEntry[];
private _unsubAreas?: UnsubscribeFunc;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_items: {},
};
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubAreas) {
this._unsubAreas();
}
}
protected render(): TemplateResult | void {
if (!this.hass || this._items === undefined) {
if (!this.hass || this._areas === undefined) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
@ -72,8 +72,8 @@ class HaConfigAreaRegistry extends LitElement {
)}
</a>
</span>
<paper-card>
${this._items.map((entry) => {
<ha-card>
${this._areas.map((entry) => {
return html`
<paper-item @click=${this._openEditEntry} .entry=${entry}>
<paper-item-body>
@ -82,7 +82,7 @@ class HaConfigAreaRegistry extends LitElement {
</paper-item>
`;
})}
${this._items.length === 0
${this._areas.length === 0
? html`
<div class="empty">
${this.hass.localize(
@ -96,7 +96,7 @@ class HaConfigAreaRegistry extends LitElement {
</div>
`
: html``}
</paper-card>
</ha-card>
</ha-config-section>
</hass-subpage>
@ -116,14 +116,16 @@ class HaConfigAreaRegistry extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
loadAreaRegistryDetailDialog();
}
private async _fetchData() {
this._items = (await fetchAreaRegistry(this.hass!)).sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
protected updated(changedProps) {
super.updated(changedProps);
if (!this._unsubAreas) {
this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
this._areas = areas;
});
}
}
private _createArea() {
@ -137,22 +139,10 @@ class HaConfigAreaRegistry extends LitElement {
private _openDialog(entry?: AreaRegistryEntry) {
showAreaRegistryDetailDialog(this, {
entry,
createEntry: async (values) => {
const created = await createAreaRegistryEntry(this.hass!, values);
this._items = this._items!.concat(created).sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
},
updateEntry: async (values) => {
const updated = await updateAreaRegistryEntry(
this.hass!,
entry!.area_id,
values
);
this._items = this._items!.map((ent) =>
ent === entry ? updated : ent
);
},
createEntry: async (values) =>
createAreaRegistryEntry(this.hass!, values),
updateEntry: async (values) =>
updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
removeEntry: async () => {
if (
!confirm(`Are you sure you want to delete this area?
@ -164,7 +154,6 @@ All devices in this area will become unassigned.`)
try {
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
this._items = this._items!.filter((ent) => ent !== entry);
return true;
} catch (err) {
return false;
@ -178,10 +167,10 @@ All devices in this area will become unassigned.`)
a {
color: var(--primary-color);
}
paper-card {
display: block;
ha-card {
max-width: 600px;
margin: 16px auto;
overflow: hidden;
}
.empty {
text-align: center;

View File

@ -226,6 +226,9 @@ class HaAutomationEditor extends LitElement {
return [
haStyle,
css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
@ -234,15 +237,12 @@ class HaAutomationEditor extends LitElement {
.content {
padding-bottom: 20px;
}
paper-card {
display: block;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers paper-card,
.script paper-card {
.triggers ha-card,
.script ha-card {
margin-top: 16px;
}
.add-card mwc-button {

View File

@ -1,6 +1,5 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-fab/paper-fab";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-item/paper-item-body";
@ -8,6 +7,7 @@ import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../layouts/ha-app-layout";
@ -32,6 +32,10 @@ class HaAutomationPicker extends LocalizeMixin(NavigateMixin(PolymerElement)) {
display: block;
}
ha-card {
overflow: hidden;
}
paper-item {
cursor: pointer;
}
@ -82,13 +86,16 @@ class HaAutomationPicker extends LocalizeMixin(NavigateMixin(PolymerElement)) {
<div slot="introduction">
[[localize('ui.panel.config.automation.picker.introduction')]]
<p>
<a href="https://home-assistant.io/docs/automation/editor/">
<a
href="https://home-assistant.io/docs/automation/editor/"
target="_blank"
>
[[localize('ui.panel.config.automation.picker.learn_more')]]
</a>
</p>
</div>
<paper-card
<ha-card
heading="[[localize('ui.panel.config.automation.picker.pick_automation')]]"
>
<template is="dom-if" if="[[!automations.length]]">
@ -107,7 +114,7 @@ class HaAutomationPicker extends LocalizeMixin(NavigateMixin(PolymerElement)) {
<ha-icon-next></ha-icon-next>
</paper-item>
</template>
</paper-card>
</ha-card>
</ha-config-section>
<paper-fab

View File

@ -7,11 +7,12 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-toggle-button/paper-toggle-button";
// tslint:disable-next-line
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import "./cloud-exposed-entities";
@ -36,7 +37,7 @@ export class CloudAlexaPref extends LitElement {
const enabled = this.cloudStatus!.prefs.alexa_enabled;
return html`
<paper-card heading="Alexa">
<ha-card header="Alexa">
<paper-toggle-button
.checked="${enabled}"
@change="${this._toggleChanged}"
@ -73,7 +74,7 @@ export class CloudAlexaPref extends LitElement {
`
: ""}
</div>
</paper-card>
</ha-card>
`;
}
@ -92,10 +93,11 @@ export class CloudAlexaPref extends LitElement {
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
ha-card > paper-toggle-button {
margin: -4px 0;
position: absolute;
right: 8px;
top: 16px;
top: 32px;
}
`;
}

View File

@ -7,12 +7,13 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-toggle-button/paper-toggle-button";
// tslint:disable-next-line
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
import "../../../components/buttons/ha-call-api-button";
import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import "./cloud-exposed-entities";
@ -41,7 +42,7 @@ export class CloudGooglePref extends LitElement {
} = this.cloudStatus.prefs;
return html`
<paper-card heading="Google Assistant">
<ha-card header="Google Assistant">
<paper-toggle-button
id="google_enabled"
.checked="${google_enabled}"
@ -105,7 +106,7 @@ export class CloudGooglePref extends LitElement {
>Sync devices</ha-call-api-button
>
</div>
</paper-card>
</ha-card>
`;
}
@ -137,10 +138,11 @@ export class CloudGooglePref extends LitElement {
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
ha-card > paper-toggle-button {
margin: -4px 0;
position: absolute;
right: 8px;
top: 16px;
top: 32px;
}
ha-call-api-button {
color: var(--primary-color);

View File

@ -8,12 +8,13 @@ import {
css,
} from "lit-element";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-toggle-button/paper-toggle-button";
import "@polymer/paper-item/paper-item-body";
// tslint:disable-next-line
import { PaperToggleButtonElement } from "@polymer/paper-toggle-button/paper-toggle-button";
import "../../../components/ha-card";
import { fireEvent } from "../../../common/dom/fire_event";
import { HomeAssistant } from "../../../types";
import {
@ -48,16 +49,16 @@ export class CloudRemotePref extends LitElement {
if (!remote_certificate) {
return html`
<paper-card heading="Remote Control">
<ha-card header="Remote Control">
<div class="preparing">
Remote access is being prepared. We will notify you when it's ready.
</div>
</paper-card>
</ha-card>
`;
}
return html`
<paper-card heading="Remote Control">
<ha-card header="Remote Control">
<paper-toggle-button
.checked="${remote_connected}"
@change="${this._toggleChanged}"
@ -83,7 +84,7 @@ export class CloudRemotePref extends LitElement {
`
: ""}
</div>
</paper-card>
</ha-card>
`;
}
@ -111,19 +112,17 @@ export class CloudRemotePref extends LitElement {
static get styles(): CSSResult {
return css`
paper-card {
display: block;
}
.preparing {
padding: 0 16px 16px;
}
a {
color: var(--primary-color);
}
paper-card > paper-toggle-button {
ha-card > paper-toggle-button {
margin: -4px 0;
position: absolute;
right: 8px;
top: 16px;
top: 32px;
}
.card-actions {
display: flex;

View File

@ -51,20 +51,18 @@ export class CloudWebhooks extends LitElement {
return html`
${this.renderStyle()}
<ha-card header="Webhooks">
<div class="body">
<div class="card-content">
Anything that is configured to be triggered by a webhook can be given
a publicly accessible URL to allow you to send data back to Home
Assistant from anywhere, without exposing your instance to the
internet.
</div>
${this._renderBody()}
internet. ${this._renderBody()}
<div class="footer">
<a href="https://www.nabucasa.com/config/webhooks" target="_blank">
Learn more about creating webhook-powered automations.
</a>
</div>
</div>
</ha-card>
`;
}
@ -194,15 +192,12 @@ export class CloudWebhooks extends LitElement {
private renderStyle() {
return html`
<style>
.body {
padding: 0 16px 8px;
}
.body-text {
padding: 0 16px;
padding: 8px 0;
}
.webhook {
display: flex;
padding: 4px 16px;
padding: 4px 0;
}
.progress {
margin-right: 16px;
@ -211,7 +206,7 @@ export class CloudWebhooks extends LitElement {
justify-content: center;
}
.footer {
padding: 16px;
padding-top: 16px;
}
.body-text a,
.footer a {

View File

@ -1,10 +1,10 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-call-api-button";
import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
@ -13,7 +13,7 @@ import "../ha-config-section";
import "./cloud-webhooks";
import formatDateTime from "../../../common/datetime/format_date_time";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event";
import { fetchCloudSubscriptionInfo } from "../../../data/cloud";
@ -41,9 +41,6 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding-bottom: 24px;
direction: ltr;
}
paper-card {
display: block;
}
.account-row {
display: flex;
padding: 0 16px;
@ -82,7 +79,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</p>
</div>
<paper-card heading="Nabu Casa Account">
<ha-card header="Nabu Casa Account">
<div class="account-row">
<paper-item-body two-line="">
[[cloudStatus.email]]
@ -105,7 +102,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
>Sign out</mwc-button
>
</div>
</paper-card>
</ha-card>
</ha-config-section>
<ha-config-section is-wide="[[isWide]]">

View File

@ -1,12 +1,12 @@
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-progress-button";
import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
@ -20,8 +20,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
direction: ltr;
}
paper-card {
display: block;
ha-card {
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
@ -47,9 +46,8 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
</style>
<hass-subpage header="Forgot Password">
<div class="content">
<paper-card>
<ha-card header="Forgot your password">
<div class="card-content">
<h1>Forgot your password?</h1>
<p>
Enter your email address and we will send you a link to reset
your password.
@ -72,7 +70,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
>Send reset email</ha-progress-button
>
</div>
</paper-card>
</ha-card>
</div>
</hass-subpage>
`;

View File

@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item-body";
@ -8,12 +7,13 @@ import "@polymer/paper-ripple/paper-ripple";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-progress-button";
import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import "../ha-config-section";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
import NavigateMixin from "../../../mixins/navigate-mixin";
import "../../../components/ha-icon-next";
/*
@ -34,14 +34,14 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
[slot="introduction"] a {
color: var(--primary-color);
}
paper-card {
display: block;
}
paper-item {
cursor: pointer;
}
paper-card:last-child {
margin-top: 24px;
ha-card {
overflow: hidden;
}
ha-card .card-header {
margin-bottom: -8px;
}
h1 {
@apply --paper-font-headline;
@ -97,7 +97,7 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
</p>
</div>
<paper-card hidden$="[[!flashMessage]]">
<ha-card hidden$="[[!flashMessage]]">
<div class="card-content flash-msg">
[[flashMessage]]
<paper-icon-button icon="hass:close" on-click="_dismissFlash"
@ -105,11 +105,10 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
>
<paper-ripple id="flashRipple" noink=""></paper-ripple>
</div>
</paper-card>
</ha-card>
<paper-card>
<ha-card header="Sign in">
<div class="card-content">
<h1>Sign In</h1>
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
<paper-input
label="Email"
@ -142,9 +141,9 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
forgot password?
</button>
</div>
</paper-card>
</ha-card>
<paper-card>
<ha-card>
<paper-item on-click="_handleRegister">
<paper-item-body two-line="">
Start your free 1 month trial
@ -152,7 +151,7 @@ class HaConfigCloudLogin extends NavigateMixin(EventsMixin(PolymerElement)) {
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</paper-card>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>

View File

@ -1,13 +1,13 @@
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-progress-button";
import "../../../layouts/hass-subpage";
import "../../../resources/ha-style";
import "../ha-config-section";
import EventsMixin from "../../../mixins/events-mixin";
import { EventsMixin } from "../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
@ -29,15 +29,9 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
a {
color: var(--primary-color);
}
paper-card {
display: block;
}
paper-item {
cursor: pointer;
}
paper-card:last-child {
margin-top: 24px;
}
h1 {
@apply --paper-font-headline;
margin: 0;
@ -84,10 +78,9 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
</p>
</div>
<paper-card>
<ha-card header="Create Account">
<div class="card-content">
<div class="header">
<h1>Create Account</h1>
<div class="error" hidden$="[[!_error]]">[[_error]]</div>
</div>
<paper-input autofocus="" id="email" label="Email address" type="email" value="{{email}}" on-keydown="_keyDown" error-message="Invalid email"></paper-input>
@ -97,7 +90,7 @@ class HaConfigCloudRegister extends EventsMixin(PolymerElement) {
<ha-progress-button on-click="_handleRegister" progress="[[_requestInProgress]]">Start trial</ha-progress-button>
<button class="link" hidden="[[_requestInProgress]]" on-click="_handleResendVerifyEmail">Resend confirmation email</button>
</div>
</paper-card>
</ha-card>
</ha-config-section>
</div>
</hass-subpage>

View File

@ -1,9 +1,9 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-call-service-button";
import "../../../resources/ha-style";
@ -57,8 +57,8 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
>[[localize('ui.panel.config.core.section.core.introduction')]]</span
>
<paper-card
heading="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
<ha-card
header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.validation.introduction')]]
@ -91,10 +91,10 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
<div id="configLog" class="validate-log">[[validateLog]]</div>
</template>
</div>
</paper-card>
</ha-card>
<paper-card
heading="[[localize('ui.panel.config.core.section.core.reloading.heading')]]"
<ha-card
header="[[localize('ui.panel.config.core.section.core.reloading.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.reloading.introduction')]]
@ -128,10 +128,10 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
>[[localize('ui.panel.config.core.section.core.reloading.script')]]
</ha-call-service-button>
</div>
</paper-card>
</ha-card>
<paper-card
heading="[[localize('ui.panel.config.core.section.core.server_management.heading')]]"
<ha-card
header="[[localize('ui.panel.config.core.section.core.server_management.heading')]]"
>
<div class="card-content">
[[localize('ui.panel.config.core.section.core.server_management.introduction')]]
@ -152,7 +152,7 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
>[[localize('ui.panel.config.core.section.core.server_management.stop')]]
</ha-call-service-button>
</div>
</paper-card>
</ha-card>
</ha-config-section>
`;
}

View File

@ -4,7 +4,7 @@ import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../../../mixins/events-mixin";
import { EventsMixin } from "../../../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin

View File

@ -2,12 +2,12 @@ import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/ha-menu-button";
import "../../../components/ha-icon-next";
@ -25,12 +25,12 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex ha-style">
ha-card {
overflow: hidden;
}
.content {
padding-bottom: 32px;
}
paper-card {
display: block;
}
a {
text-decoration: none;
color: var(--primary-text-color);
@ -51,7 +51,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<span slot="introduction">[[localize('ui.panel.config.introduction')]]</span>
<template is="dom-if" if="[[computeIsLoaded(hass, 'cloud')]]">
<paper-card>
<ha-card>
<a href='/config/cloud' tabindex="-1">
<paper-item>
<paper-item-body two-line="">
@ -69,11 +69,11 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</paper-card>
</ha-card>
</a>
</template>
<paper-card>
<ha-card>
<a href='/config/integrations/dashboard' tabindex="-1">
<paper-item>
<paper-item-body two-line>
@ -97,7 +97,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
</paper-card>
</ha-card>
<ha-config-navigation hass="[[hass]]"></ha-config-navigation>
</ha-config-section>

View File

@ -1,5 +1,4 @@
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@ -10,6 +9,7 @@ import LocalizeMixin from "../../../mixins/localize-mixin";
import isComponentLoaded from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
const CORE_PAGES = ["core", "customize", "entity_registry", "area_registry"];
@ -21,14 +21,14 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex">
paper-card {
display: block;
ha-card {
overflow: hidden;
}
paper-item {
cursor: pointer;
}
</style>
<paper-card>
<ha-card>
<template is="dom-repeat" items="[[pages]]">
<template is="dom-if" if="[[_computeLoaded(hass, item)]]">
<paper-item on-click="_navigate">
@ -40,7 +40,7 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
</paper-item>
</template>
</template>
</paper-card>
</ha-card>
`;
}

View File

@ -4,24 +4,23 @@ import {
html,
css,
CSSResult,
PropertyDeclarations,
property,
} from "lit-element";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-card/paper-card";
import { HomeAssistant } from "../../../types";
import {
EntityRegistryEntry,
fetchEntityRegistry,
computeEntityRegistryName,
updateEntityRegistryEntry,
removeEntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-loading-screen";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import compare from "../../../common/string/compare";
import domainIcon from "../../../common/entity/domain_icon";
import stateIcon from "../../../common/entity/state_icon";
import computeDomain from "../../../common/entity/compute_domain";
@ -30,22 +29,24 @@ import {
showEntityRegistryDetailDialog,
loadEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { compare } from "../../../common/string/compare";
class HaConfigEntityRegistry extends LitElement {
public hass?: HomeAssistant;
public isWide?: boolean;
private _items?: EntityRegistryEntry[];
@property() public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() private _entities?: EntityRegistryEntry[];
private _unsubEntities?: UnsubscribeFunc;
static get properties(): PropertyDeclarations {
return {
hass: {},
isWide: {},
_items: {},
};
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubEntities) {
this._unsubEntities();
}
}
protected render(): TemplateResult | void {
if (!this.hass || this._items === undefined) {
if (!this.hass || this._entities === undefined) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
@ -77,8 +78,8 @@ class HaConfigEntityRegistry extends LitElement {
)}
</a>
</span>
<paper-card>
${this._items.map((entry) => {
<ha-card>
${this._entities.map((entry) => {
const state = this.hass!.states[entry.entity_id];
return html`
<paper-icon-item @click=${this._openEditEntry} .entry=${entry}>
@ -103,7 +104,7 @@ class HaConfigEntityRegistry extends LitElement {
</paper-icon-item>
`;
})}
</paper-card>
</ha-card>
</ha-config-section>
</hass-subpage>
`;
@ -111,14 +112,18 @@ class HaConfigEntityRegistry extends LitElement {
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
this._fetchData();
loadEntityRegistryDetailDialog();
}
private async _fetchData(): Promise<void> {
this._items = (await fetchEntityRegistry(this.hass!)).sort((ent1, ent2) =>
protected updated(changedProps) {
super.updated(changedProps);
if (!this._unsubEntities) {
this._unsubEntities = subscribeEntityRegistry(this.hass, (entities) => {
this._entities = entities.sort((ent1, ent2) =>
compare(ent1.entity_id, ent2.entity_id)
);
});
}
}
private _openEditEntry(ev: MouseEvent): void {
@ -131,7 +136,7 @@ class HaConfigEntityRegistry extends LitElement {
entry.entity_id,
updates
);
this._items = this._items!.map((ent) =>
this._entities = this._entities!.map((ent) =>
ent === entry ? updated : ent
);
},
@ -148,7 +153,7 @@ Deleting an entry will not remove the entity from Home Assistant. To do this, yo
try {
await removeEntityRegistryEntry(this.hass!, entry.entity_id);
this._items = this._items!.filter((ent) => ent !== entry);
this._entities = this._entities!.filter((ent) => ent !== entry);
return true;
} catch (err) {
return false;
@ -162,9 +167,9 @@ Deleting an entry will not remove the entity from Home Assistant. To do this, yo
a {
color: var(--primary-color);
}
paper-card {
display: block;
ha-card {
direction: ltr;
overflow: hidden;
}
paper-icon-item {
cursor: pointer;

View File

@ -1,11 +1,11 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-spinner/paper-spinner";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-card";
import computeStateName from "../../common/entity/compute_state_name";
@ -13,8 +13,7 @@ class HaEntityConfig extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
paper-card {
display: block;
ha-card {
direction: ltr;
}
@ -38,7 +37,7 @@ class HaEntityConfig extends PolymerElement {
@apply --layout-justified;
}
</style>
<paper-card>
<ha-card>
<div class="card-content">
<div class="device-picker">
<paper-dropdown-menu
@ -89,7 +88,7 @@ class HaEntityConfig extends PolymerElement {
>
</template>
</div>
</paper-card>
</ha-card>
`;
}

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