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"); const config = require("../paths");
gulp.task("clean", () => del([config.root, config.build_dir])); 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 fs = require("fs-extra");
const zopfli = require("gulp-zopfli-green"); const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream"); const merge = require("merge-stream");
const config = require("../paths"); const paths = require("../paths");
const npmPath = (...parts) => const npmPath = (...parts) =>
path.resolve(config.polymer_dir, "node_modules", ...parts); path.resolve(paths.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(config.polymer_dir, ...parts); const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
const staticPath = (...parts) => path.resolve(config.root, "static", ...parts);
const copyFileDir = (fromFile, toDir) => const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile))); 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 // Translation output
fs.copySync( fs.copySync(
polyPath("build-translations/output"), polyPath("build-translations/output"),
@ -23,9 +27,8 @@ function copyTranslations() {
); );
} }
function copyStatic() { function copyPolyfills(staticDir) {
// Basic static files const staticPath = genStaticPath(staticDir);
fs.copySync(polyPath("public"), config.root);
// Web Component polyfills and adapters // Web Component polyfills and adapters
copyFileDir( copyFileDir(
@ -40,31 +43,16 @@ function copyStatic() {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/") 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) => { function copyFonts(staticDir) {
copyStatic(); const staticPath = genStaticPath(staticDir);
done(); // 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 const fonts = gulp
.src(staticPath("fonts/**/*.ttf")) .src(staticPath("fonts/**/*.ttf"))
.pipe(zopfli()) .pipe(zopfli())
@ -79,9 +67,44 @@ gulp.task("compress-static", () => {
.pipe(gulp.dest(staticPath("translations"))); .pipe(gulp.dest(staticPath("translations")));
return merge(fonts, polyfills, translations); return merge(fonts, polyfills, translations);
}); }
gulp.task("copy-translations", (done) => { gulp.task("copy-static", (done) => {
copyTranslations(); 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(); done();
}); });

View File

@ -1,6 +1,7 @@
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const paths = require("../paths");
const ICON_PACKAGE_PATH = path.resolve( const ICON_PACKAGE_PATH = path.resolve(
__dirname, __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", gulp.series("gen-icons-hass", "gen-icons-mdi"));
gulp.task("gen-icons-demo", (done) => {
const iconNames = findIcons(path.resolve(paths.demo_dir, "./src"), "hademo");
fs.writeFileSync(
path.resolve(paths.demo_dir, "hademo-icons.html"),
generateIconset("hademo", iconNames)
);
done();
});
module.exports = { module.exports = {
findIcons, findIcons,
generateIconset, 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. // Tasks to run webpack.
const gulp = require("gulp"); const gulp = require("gulp");
const path = require("path");
const webpack = require("webpack"); 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) => { const handler = (done) => (err, stats) => {
if (err) { if (err) {
@ -12,7 +16,7 @@ const handler = (done) => (err, stats) => {
return; return;
} }
console.log(`Build done @ ${new Date().toLocaleTimeString()}`); log(`Build done @ ${new Date().toLocaleTimeString()}`);
if (stats.hasErrors() || stats.hasWarnings()) { if (stats.hasErrors() || stats.hasWarnings()) {
console.log(stats.toString("minimal")); 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([ const compiler = webpack([
createAppConfig({ createAppConfig({
isProdBuild: false, isProdBuild: false,
@ -41,7 +45,7 @@ gulp.task("webpack-watch", () => {
}); });
gulp.task( gulp.task(
"webpack-prod", "webpack-prod-app",
() => () =>
new Promise((resolve) => new Promise((resolve) =>
webpack( 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 = { module.exports = {
polymer_dir: path.resolve(__dirname, ".."), polymer_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"), build_dir: path.resolve(__dirname, "../build"),
root: path.resolve(__dirname, "../hass_frontend"), root: path.resolve(__dirname, "../hass_frontend"),
static: path.resolve(__dirname, "../hass_frontend/static"), static: path.resolve(__dirname, "../hass_frontend/static"),
output: path.resolve(__dirname, "../hass_frontend/frontend_latest"), output: path.resolve(__dirname, "../hass_frontend/frontend_latest"),
output_es5: path.resolve(__dirname, "../hass_frontend/frontend_es5"), 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]; 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 = { const resolve = {
extensions: [".ts", ".js", ".json", ".tsx"], extensions: [".ts", ".js", ".json", ".tsx"],
alias: { 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 = [ const plugins = [
// Ignore moment.js locales // Ignore moment.js locales
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
@ -75,8 +95,6 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
] = `build-translations/output/${key}.json`; ] = `build-translations/output/${key}.json`;
}); });
const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/";
const entry = { const entry = {
app: "./src/entrypoints/app.ts", app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts", authorize: "./src/entrypoints/authorize.ts",
@ -88,28 +106,11 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
}; };
return { return {
mode: isProdBuild ? "production" : "development", mode: genMode(isProdBuild),
devtool: isProdBuild devtool: genDevTool(isProdBuild),
? "cheap-source-map "
: "inline-cheap-module-source-map",
entry, entry,
module: { module: {
rules: [ rules: [babelLoaderConfig({ latestBuild }), cssLoader, htmlLoader],
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
}, },
optimization: optimization(latestBuild), optimization: optimization(latestBuild),
plugins: [ plugins: [
@ -165,20 +166,56 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`; if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`;
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
}, },
chunkFilename: chunkFilename: genChunkFilename(isProdBuild, isStatsBuild),
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
: "[name].chunk.js",
path: latestBuild ? paths.output : paths.output_es5, path: latestBuild ? paths.output : paths.output_es5,
publicPath, publicPath: latestBuild ? "/frontend_latest/" : "/frontend_es5/",
}, },
resolve, 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 = { module.exports = {
resolve, resolve,
plugins, plugins,
optimization, optimization,
createAppConfig, createAppConfig,
createDemoConfig,
}; };

View File

@ -74,9 +74,6 @@
content="https://www.home-assistant.io/images/default-social.png" content="https://www.home-assistant.io/images/default-social.png"
/> />
<title>Home Assistant Demo</title> <title>Home Assistant Demo</title>
<script src="./custom-elements-es5-adapter.js"></script>
<script src="./compatibility.js"></script>
<script src="./main.js" async></script>
<style> <style>
body { body {
font-family: Roboto, Noto, sans-serif; font-family: Roboto, Noto, sans-serif;
@ -98,6 +95,47 @@
<body> <body>
<div id="ha-init-skeleton"></div> <div id="ha-init-skeleton"></div>
<ha-demo></ha-demo> <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> <script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]]; var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function(d, t) { (function(d, t) {

View File

View File

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

View File

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

View File

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

View File

@ -1,90 +1,13 @@
const path = require("path"); const { createDemoConfig } = require("../build-scripts/webpack.js");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const { babelLoaderConfig } = require("../build-scripts/babel.js");
const webpackBase = 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 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; const latestBuild = false;
module.exports = { module.exports = createDemoConfig({
mode: isProd ? "production" : "development", isProdBuild,
devtool: isProd ? "cheap-source-map" : "inline-source-map", isStatsBuild,
entry: { latestBuild,
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",
},
};

View File

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

View File

@ -10,7 +10,7 @@ import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; 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/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonNetwork extends EventsMixin(PolymerElement) { class HassioAddonNetwork extends EventsMixin(PolymerElement) {
static get template() { static get template() {

View File

@ -4,7 +4,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/buttons/ha-call-api-button";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/buttons/ha-call-api-button";
import EventsMixin from "../../../src/mixins/events-mixin"; import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioSupervisorInfo extends EventsMixin(PolymerElement) { class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() { static get template() {

View File

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

View File

@ -6,4 +6,4 @@ set -e
cd "$(dirname "$0")/.." 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")/.." 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( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20190502.0", version="20190507.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", 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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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 { localizeLiteMixin } from "../mixins/localize-lite-mixin";
import "../components/ha-icon-next"; 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import computeStateName from "../common/entity/compute_state_name"; 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 LocalizeMixin from "../mixins/localize-mixin";
import { fetchThumbnailUrlWithCache } from "../data/camera"; 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 computeStateName from "../common/entity/compute_state_name";
import stateMoreInfoType from "../common/entity/state_more_info_type"; import stateMoreInfoType from "../common/entity/state_more_info_type";
import canToggleState from "../common/entity/can_toggle_state"; 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"; import LocalizeMixin from "../mixins/localize-mixin";
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) { class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {

View File

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

View File

@ -9,7 +9,7 @@ import HassMediaPlayerEntity from "../util/hass-media-player-model";
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player"; import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
import computeStateName from "../common/entity/compute_state_name"; 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 LocalizeMixin from "../mixins/localize-mixin";
/* /*

View File

@ -5,7 +5,7 @@ import "../components/ha-card";
import "../components/ha-icon"; import "../components/ha-icon";
import computeStateName from "../common/entity/compute_state_name"; 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) { class HaPlantCard extends EventsMixin(PolymerElement) {
static get template() { static get template() {

View File

@ -6,7 +6,7 @@ import computeStateName from "../common/entity/compute_state_name";
import "../components/ha-card"; import "../components/ha-card";
import "../components/ha-icon"; import "../components/ha-icon";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin"; import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl"; 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) { if (a < b) {
return -1; return -1;
} }
@ -8,3 +8,6 @@ export default (a, b) => {
return 0; 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 // be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the // N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing. // 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; let timeout;
// @ts-ignore
return function(...args) { return function(...args) {
// tslint:disable:no-this-assignment // tslint:disable:no-this-assignment
// @ts-ignore // @ts-ignore
@ -23,4 +29,4 @@ export default function debounce(func, wait, immediate) {
func.apply(context, args); 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button"; import "./ha-progress-button";
import EventsMixin from "../../mixins/events-mixin"; import { EventsMixin } from "../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin

View File

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

View File

@ -27,9 +27,11 @@ class HaCard extends LitElement {
color: var(--primary-text-color); color: var(--primary-text-color);
display: block; display: block;
transition: all 0.3s ease-out; 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); color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit); font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px); font-size: var(--ha-card-header-font-size, 24px);
@ -38,12 +40,31 @@ class HaCard extends LitElement {
padding: 24px 16px 16px; padding: 24px 16px 16px;
display: block; 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 { protected render(): TemplateResult {
return html` return html`
<slot class="header" name="header">${this.header}</slot> ${this.header
? html`
<div class="card-header">${this.header}</div>
`
: html``}
<slot></slot> <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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin

View File

@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
/** /**
* Color-picker custom element * 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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/vaadin-combo-box-light"; 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) { class HaComboBox extends EventsMixin(PolymerElement) {
static get template() { static get template() {

View File

@ -31,7 +31,9 @@ export class HaDateInput extends LitElement {
paper-input { paper-input {
width: 30px; width: 30px;
text-align: center; text-align: center;
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
--paper-input-container-input_-_-moz-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_-_-webkit-appearance: none;
--paper-input-container-input-webkit-spinner_-_margin: 0; --paper-input-container-input-webkit-spinner_-_margin: 0;
--paper-input-container-input-webkit-spinner_-_display: none; --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 { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-paper-slider"; import "./ha-paper-slider";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin

View File

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

View File

@ -4,7 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getAppKey } from "../data/notify_html5"; import { getAppKey } from "../data/notify_html5";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
export const pushSupported = export const pushSupported =
"serviceWorker" in navigator && "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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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 isComponentLoaded from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; 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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin

View File

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

View File

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

View File

@ -1,4 +1,7 @@
import { createCollection } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { compare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
export interface AreaRegistryEntry { export interface AreaRegistryEntry {
area_id: string; area_id: string;
@ -9,9 +12,6 @@ export interface AreaRegistryEntryMutableParams {
name: string; name: string;
} }
export const fetchAreaRegistry = (hass: HomeAssistant) =>
hass.callWS<AreaRegistryEntry[]>({ type: "config/area_registry/list" });
export const createAreaRegistryEntry = ( export const createAreaRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
values: AreaRegistryEntryMutableParams values: AreaRegistryEntryMutableParams
@ -37,3 +37,33 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
type: "config/area_registry/delete", type: "config/area_registry/delete",
area_id: areaId, 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; path: string;
} }
export const hassUrl = `${location.protocol}//${location.host}`;
export const getSignedPath = ( export const getSignedPath = (
hass: HomeAssistant, hass: HomeAssistant,
path: string path: string

View File

@ -1,4 +1,17 @@
import { HomeAssistant } from "../types"; 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 { export interface FieldSchema {
name: string; name: string;
@ -9,7 +22,10 @@ export interface FieldSchema {
export interface ConfigFlowProgress { export interface ConfigFlowProgress {
flow_id: string; flow_id: string;
handler: string; handler: string;
context: { [key: string]: any }; context: {
title_placeholders: { [key: string]: string };
[key: string]: any;
};
} }
export interface ConfigFlowStepForm { export interface ConfigFlowStepForm {
@ -74,3 +90,53 @@ export const getConfigFlowsInProgress = (hass: HomeAssistant) =>
export const getConfigFlowHandlers = (hass: HomeAssistant) => export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers"); 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 { HomeAssistant } from "../types";
import { createCollection } from "home-assistant-js-websocket";
import { debounce } from "../common/util/debounce";
export interface DeviceRegistryEntry { export interface DeviceRegistryEntry {
id: string; id: string;
@ -18,9 +20,6 @@ export interface DeviceRegistryEntryMutableParams {
name_by_user?: string; name_by_user?: string;
} }
export const fetchDeviceRegistry = (hass: HomeAssistant) =>
hass.callWS<DeviceRegistryEntry[]>({ type: "config/device_registry/list" });
export const updateDeviceRegistryEntry = ( export const updateDeviceRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
deviceId: string, deviceId: string,
@ -31,3 +30,33 @@ export const updateDeviceRegistryEntry = (
device_id: deviceId, device_id: deviceId,
...updates, ...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 { HomeAssistant } from "../types";
import computeStateName from "../common/entity/compute_state_name"; import computeStateName from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
export interface EntityRegistryEntry { export interface EntityRegistryEntry {
entity_id: string; entity_id: string;
@ -26,11 +28,6 @@ export const computeEntityRegistryName = (
return state ? computeStateName(state) : null; return state ? computeStateName(state) : null;
}; };
export const fetchEntityRegistry = (
hass: HomeAssistant
): Promise<EntityRegistryEntry[]> =>
hass.callWS<EntityRegistryEntry[]>({ type: "config/entity_registry/list" });
export const updateEntityRegistryEntry = ( export const updateEntityRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,
@ -50,3 +47,33 @@ export const removeEntityRegistryEntry = (
type: "config/entity_registry/remove", type: "config/entity_registry/remove",
entity_id: entityId, 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"; import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
@ -27,6 +27,6 @@ declare global {
} }
} }
export const forwardHaptic = (el: HTMLElement, hapticType: HapticType) => { export const forwardHaptic = (hapticType: HapticType) => {
fireEvent(el, "haptic", hapticType); fireEvent(window, "haptic", hapticType);
}; };

View File

@ -1,14 +1,26 @@
import { handleFetchPromise } from "../util/hass-call-api"; import { handleFetchPromise } from "../util/hass-call-api";
import { HomeAssistant } from "../types";
export interface OnboardingStep { export interface OnboardingUserStepResponse {
step: string; auth_code: string;
done: boolean;
} }
interface UserStepResponse { export interface OnboardingIntegrationStepResponse {
auth_code: string; 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 = () => export const fetchOnboardingOverview = () =>
fetch("/api/onboarding", { credentials: "same-origin" }); fetch("/api/onboarding", { credentials: "same-origin" });
@ -17,11 +29,22 @@ export const onboardUserStep = (params: {
name: string; name: string;
username: string; username: string;
password: string; password: string;
language: string;
}) => }) =>
handleFetchPromise<UserStepResponse>( handleFetchPromise<OnboardingUserStepResponse>(
fetch("/api/onboarding/users", { fetch("/api/onboarding/users", {
method: "POST", method: "POST",
credentials: "same-origin", credentials: "same-origin",
body: JSON.stringify(params), 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-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import "../../components/ha-form"; import "../../components/ha-form";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
@ -37,10 +38,14 @@ import "./step-flow-abort";
import "./step-flow-create-entry"; import "./step-flow-create-entry";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
fetchDeviceRegistry, subscribeDeviceRegistry,
} from "../../data/device_registry"; } from "../../data/device_registry";
import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry"; import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { caseInsensitiveCompare } from "../../common/string/compare";
let instance = 0; let instance = 0;
@ -57,30 +62,19 @@ declare global {
@customElement("dialog-config-flow") @customElement("dialog-config-flow")
class ConfigFlowDialog extends LitElement { class ConfigFlowDialog extends LitElement {
public hass!: HomeAssistant; public hass!: HomeAssistant;
@property() private _params?: HaConfigFlowParams;
@property() @property() private _loading = true;
private _params?: HaConfigFlowParams;
@property()
private _loading = true;
private _instance = instance; private _instance = instance;
@property() private _step:
@property()
private _step:
| ConfigFlowStep | ConfigFlowStep
| undefined | undefined
// Null means we need to pick a config flow // Null means we need to pick a config flow
| null; | null;
@property() private _devices?: DeviceRegistryEntry[];
@property() @property() private _areas?: AreaRegistryEntry[];
private _devices?: DeviceRegistryEntry[]; @property() private _handlers?: string[];
private _unsubAreas?: UnsubscribeFunc;
@property() private _unsubDevices?: UnsubscribeFunc;
private _areas?: AreaRegistryEntry[];
@property()
private _handlers?: string[];
public async showDialog(params: HaConfigFlowParams): Promise<void> { public async showDialog(params: HaConfigFlowParams): Promise<void> {
this._params = params; this._params = params;
@ -95,7 +89,13 @@ class ConfigFlowDialog extends LitElement {
this._loading = true; this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog()); this.updateComplete.then(() => this._scheduleCenterDialog());
try { 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 { } finally {
this._loading = false; this._loading = false;
} }
@ -196,6 +196,10 @@ class ConfigFlowDialog extends LitElement {
this._fetchDevices(this._step.result); this._fetchDevices(this._step.result);
this._fetchAreas(); this._fetchAreas();
} }
if (changedProps.has("_devices") && this._dialog) {
this._scheduleCenterDialog();
}
} }
private _scheduleCenterDialog() { private _scheduleCenterDialog() {
@ -207,16 +211,17 @@ class ConfigFlowDialog extends LitElement {
} }
private async _fetchDevices(configEntryId) { private async _fetchDevices(configEntryId) {
// Wait 5 seconds to give integrations time to find devices this._unsubDevices = subscribeDeviceRegistry(this.hass, (devices) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
const devices = await fetchDeviceRegistry(this.hass);
this._devices = devices.filter((device) => this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId) device.config_entries.includes(configEntryId)
); );
});
} }
private async _fetchAreas() { private async _fetchAreas() {
this._areas = await fetchAreaRegistry(this.hass); this._unsubAreas = subscribeAreaRegistry(this.hass, (areas) => {
this._areas = areas;
});
} }
private async _processStep( private async _processStep(
@ -261,6 +266,14 @@ class ConfigFlowDialog extends LitElement {
this._step = undefined; this._step = undefined;
this._params = undefined; this._params = undefined;
this._devices = 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 { private _openedChanged(ev: PolymerChangedEvent<boolean>): void {

View File

@ -169,6 +169,18 @@ class StepFlowCreateEntry extends LitElement {
.buttons > *:last-child { .buttons > *:last-child {
margin-left: auto; 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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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 LocalizeMixin from "../../../mixins/localize-mixin";
class MoreInfoAlarmControlPanel extends LocalizeMixin( 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 featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature"; 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 LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";

View File

@ -10,7 +10,7 @@ import "../../../components/ha-attributes";
import "../../../components/ha-paper-dropdown-menu"; import "../../../components/ha-paper-dropdown-menu";
import attributeClassNames from "../../../common/entity/attribute_class_names"; 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"; import LocalizeMixin from "../../../mixins/localize-mixin";
/* /*

View File

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

View File

@ -109,12 +109,17 @@ class MoreInfoVacuum extends PolymerElement {
dynamic-align="" dynamic-align=""
label="Fan speed" 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 <template
is="dom-repeat" is="dom-repeat"
items="[[stateObj.attributes.fan_speed_list]]" items="[[stateObj.attributes.fan_speed_list]]"
> >
<paper-item>[[item]]</paper-item> <paper-item item-name$="[[item]]">[[item]]</paper-item>
</template> </template>
</paper-listbox> </paper-listbox>
</ha-paper-dropdown-menu> </ha-paper-dropdown-menu>
@ -150,12 +155,6 @@ class MoreInfoVacuum extends PolymerElement {
stateObj: { stateObj: {
type: Object, type: Object,
}, },
fanSpeedIndex: {
type: Number,
value: -1,
observer: "fanSpeedChanged",
},
}; };
} }
@ -206,17 +205,15 @@ class MoreInfoVacuum extends PolymerElement {
); );
} }
fanSpeedChanged(fanSpeedIndex) { fanSpeedChanged(ev) {
var fanSpeedInput; var oldVal = this.stateObj.attributes.fan_speed;
// Selected Option will transition to '' before transitioning to new value var newVal = ev.detail.value;
if (fanSpeedIndex === "" || fanSpeedIndex === -1) return;
fanSpeedInput = this.stateObj.attributes.fan_speed_list[fanSpeedIndex]; if (!newVal || oldVal === newVal) return;
if (fanSpeedInput === this.stateObj.attributes.fan_speed) return;
this.hass.callService("vacuum", "set_fan_speed", { this.hass.callService("vacuum", "set_fan_speed", {
entity_id: this.stateObj.entity_id, 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 featureClassNames from "../../../common/entity/feature_class_names";
import { supportsFeature } from "../../../common/entity/supports-feature"; 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 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 computeStateDomain from "../../common/entity/compute_state_domain";
import isComponentLoaded from "../../common/config/is_component_loaded"; import isComponentLoaded from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; 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"; import { computeRTL } from "../../common/util/compute_rtl";
const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"]; 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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; 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 LocalizeMixin from "../../mixins/localize-mixin";
import computeStateName from "../../common/entity/compute_state_name"; 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. // properly into iron-meta, which is used to transfer iconsets to iron-icon.
import "../components/ha-iconset-svg"; import "../components/ha-iconset-svg";
import "../layouts/app/home-assistant"; import "../layouts/home-assistant";
setPassiveTouchGestures(true); setPassiveTouchGestures(true);
/* LastPass createElement workaround. See #428 */ /* LastPass createElement workaround. See #428 */

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@
Home Assistant Home Assistant
</div> </div>
<ha-onboarding>Initializing</ha-onboarding> <ha-onboarding></ha-onboarding>
</div> </div>
<%= renderTemplate('_js_base') %> <%= 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 "@polymer/app-route/app-location";
import { html, LitElement, PropertyValues, css, property } from "lit-element"; import { html, LitElement, PropertyValues, css, property } from "lit-element";
import "../home-assistant-main"; import "./home-assistant-main";
import "../ha-init-page"; import "./ha-init-page";
import "../../resources/ha-style"; import "../resources/ha-style";
import { registerServiceWorker } from "../../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import { DEFAULT_PANEL } from "../../common/const"; import { DEFAULT_PANEL } from "../common/const";
import HassBaseMixin from "./hass-base-mixin"; import { Route, HomeAssistant } from "../types";
import AuthMixin from "./auth-mixin"; import { navigate } from "../common/navigate";
import TranslationsMixin from "./translations-mixin"; import { HassElement } from "../state/hass-element";
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";
(LitElement.prototype as any).html = html; (LitElement.prototype as any).html = html;
(LitElement.prototype as any).css = css; (LitElement.prototype as any).css = css;
const ext = <T>(baseClass: T, mixins): T => export class HomeAssistantAppEl extends HassElement {
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
AuthMixin,
ThemesMixin,
TranslationsMixin,
MoreInfoMixin,
SidebarMixin,
DisconnectToastMixin,
ConnectionMixin,
NotificationMixin,
dialogManagerMixin,
urlSyncMixin,
]) {
@property() private _route?: Route; @property() private _route?: Route;
@property() private _error?: boolean; @property() private _error?: boolean;
@property() private _panelUrl?: string; @property() private _panelUrl?: string;
@ -69,6 +44,7 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._initialize();
setTimeout(registerServiceWorker, 1000); setTimeout(registerServiceWorker, 1000);
/* polyfill for paper-dropdown */ /* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"); import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min");
@ -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) { private _routeChanged(ev) {
const route = ev.detail.value as Route; const route = ev.detail.value as Route;
// If it's the first route that we process, // If it's the first route that we process,

View File

@ -1,7 +1,7 @@
import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin"; import { dedupingMixin } from "@polymer/polymer/lib/utils/mixin";
import { PaperDialogBehavior } from "@polymer/paper-dialog-behavior/paper-dialog-behavior"; import { PaperDialogBehavior } from "@polymer/paper-dialog-behavior/paper-dialog-behavior";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import EventsMixin from "./events-mixin"; import { EventsMixin } from "./events-mixin";
/** /**
* @polymerMixin * @polymerMixin
* @appliesMixin EventsMixin * @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. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/* @polymerMixin */ /* @polymerMixin */
export default dedupingMixin( export const EventsMixin = dedupingMixin(
(superClass) => (superClass) =>
class extends superClass { class extends superClass {
/** /**

View File

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

View File

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

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 */ /* eslint-enable */
import BigCalendar from "react-big-calendar"; import BigCalendar from "react-big-calendar";
import moment from "moment"; import moment from "moment";
import EventsMixin from "../../mixins/events-mixin"; import { EventsMixin } from "../../mixins/events-mixin";
import "../../resources/ha-style"; import "../../resources/ha-style";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-toggle-button/paper-toggle-button"; import "@polymer/paper-toggle-button/paper-toggle-button";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-call-api-button"; import "../../../components/buttons/ha-call-api-button";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
@ -13,7 +13,7 @@ import "../ha-config-section";
import "./cloud-webhooks"; import "./cloud-webhooks";
import formatDateTime from "../../../common/datetime/format_date_time"; 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 LocalizeMixin from "../../../mixins/localize-mixin";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { fetchCloudSubscriptionInfo } from "../../../data/cloud"; import { fetchCloudSubscriptionInfo } from "../../../data/cloud";
@ -41,9 +41,6 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding-bottom: 24px; padding-bottom: 24px;
direction: ltr; direction: ltr;
} }
paper-card {
display: block;
}
.account-row { .account-row {
display: flex; display: flex;
padding: 0 16px; padding: 0 16px;
@ -82,7 +79,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
</p> </p>
</div> </div>
<paper-card heading="Nabu Casa Account"> <ha-card header="Nabu Casa Account">
<div class="account-row"> <div class="account-row">
<paper-item-body two-line=""> <paper-item-body two-line="">
[[cloudStatus.email]] [[cloudStatus.email]]
@ -105,7 +102,7 @@ class HaConfigCloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
>Sign out</mwc-button >Sign out</mwc-button
> >
</div> </div>
</paper-card> </ha-card>
</ha-config-section> </ha-config-section>
<ha-config-section is-wide="[[isWide]]"> <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 "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import EventsMixin from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
@ -20,8 +20,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
direction: ltr; direction: ltr;
} }
paper-card { ha-card {
display: block;
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
margin-top: 24px; margin-top: 24px;
@ -47,9 +46,8 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
</style> </style>
<hass-subpage header="Forgot Password"> <hass-subpage header="Forgot Password">
<div class="content"> <div class="content">
<paper-card> <ha-card header="Forgot your password">
<div class="card-content"> <div class="card-content">
<h1>Forgot your password?</h1>
<p> <p>
Enter your email address and we will send you a link to reset Enter your email address and we will send you a link to reset
your password. your password.
@ -72,7 +70,7 @@ class HaConfigCloudForgotPassword extends EventsMixin(PolymerElement) {
>Send reset email</ha-progress-button >Send reset email</ha-progress-button
> >
</div> </div>
</paper-card> </ha-card>
</div> </div>
</hass-subpage> </hass-subpage>
`; `;

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/buttons/ha-call-service-button"; import "../../../components/buttons/ha-call-service-button";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
@ -57,8 +57,8 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
>[[localize('ui.panel.config.core.section.core.introduction')]]</span >[[localize('ui.panel.config.core.section.core.introduction')]]</span
> >
<paper-card <ha-card
heading="[[localize('ui.panel.config.core.section.core.validation.heading')]]" header="[[localize('ui.panel.config.core.section.core.validation.heading')]]"
> >
<div class="card-content"> <div class="card-content">
[[localize('ui.panel.config.core.section.core.validation.introduction')]] [[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> <div id="configLog" class="validate-log">[[validateLog]]</div>
</template> </template>
</div> </div>
</paper-card> </ha-card>
<paper-card <ha-card
heading="[[localize('ui.panel.config.core.section.core.reloading.heading')]]" header="[[localize('ui.panel.config.core.section.core.reloading.heading')]]"
> >
<div class="card-content"> <div class="card-content">
[[localize('ui.panel.config.core.section.core.reloading.introduction')]] [[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')]] >[[localize('ui.panel.config.core.section.core.reloading.script')]]
</ha-call-service-button> </ha-call-service-button>
</div> </div>
</paper-card> </ha-card>
<paper-card <ha-card
heading="[[localize('ui.panel.config.core.section.core.server_management.heading')]]" header="[[localize('ui.panel.config.core.section.core.server_management.heading')]]"
> >
<div class="card-content"> <div class="card-content">
[[localize('ui.panel.config.core.section.core.server_management.introduction')]] [[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')]] >[[localize('ui.panel.config.core.section.core.server_management.stop')]]
</ha-call-service-button> </ha-call-service-button>
</div> </div>
</paper-card> </ha-card>
</ha-config-section> </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 { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import EventsMixin from "../../../../mixins/events-mixin"; import { EventsMixin } from "../../../../mixins/events-mixin";
/* /*
* @appliesMixin EventsMixin * @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-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-card";
import "../../../components/ha-menu-button"; import "../../../components/ha-menu-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
@ -25,12 +25,12 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
static get template() { static get template() {
return html` return html`
<style include="iron-flex ha-style"> <style include="iron-flex ha-style">
ha-card {
overflow: hidden;
}
.content { .content {
padding-bottom: 32px; padding-bottom: 32px;
} }
paper-card {
display: block;
}
a { a {
text-decoration: none; text-decoration: none;
color: var(--primary-text-color); color: var(--primary-text-color);
@ -51,7 +51,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<span slot="introduction">[[localize('ui.panel.config.introduction')]]</span> <span slot="introduction">[[localize('ui.panel.config.introduction')]]</span>
<template is="dom-if" if="[[computeIsLoaded(hass, 'cloud')]]"> <template is="dom-if" if="[[computeIsLoaded(hass, 'cloud')]]">
<paper-card> <ha-card>
<a href='/config/cloud' tabindex="-1"> <a href='/config/cloud' tabindex="-1">
<paper-item> <paper-item>
<paper-item-body two-line=""> <paper-item-body two-line="">
@ -69,11 +69,11 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
</paper-item-body> </paper-item-body>
<ha-icon-next></ha-icon-next> <ha-icon-next></ha-icon-next>
</paper-item> </paper-item>
</paper-card> </ha-card>
</a> </a>
</template> </template>
<paper-card> <ha-card>
<a href='/config/integrations/dashboard' tabindex="-1"> <a href='/config/integrations/dashboard' tabindex="-1">
<paper-item> <paper-item>
<paper-item-body two-line> <paper-item-body two-line>
@ -97,7 +97,7 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
<ha-icon-next></ha-icon-next> <ha-icon-next></ha-icon-next>
</paper-item> </paper-item>
</a> </a>
</paper-card> </ha-card>
<ha-config-navigation hass="[[hass]]"></ha-config-navigation> <ha-config-navigation hass="[[hass]]"></ha-config-navigation>
</ha-config-section> </ha-config-section>

View File

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

View File

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

View File

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

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