Merge pull request #10136 from home-assistant/dev

This commit is contained in:
Bram Kragten 2021-10-02 22:41:19 +02:00 committed by GitHub
commit 31b69147f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 555 additions and 179 deletions

View File

@ -30,7 +30,7 @@ jobs:
env: env:
CI: true CI: true
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-demos
- name: Run eslint - name: Run eslint
run: yarn run lint:eslint run: yarn run lint:eslint
- name: Run tsc - name: Run tsc
@ -53,6 +53,8 @@ jobs:
run: yarn install run: yarn install
env: env:
CI: true CI: true
- name: Build resources
run: ./node_modules/.bin/gulp build-translations build-locale-data
- name: Run Tests - name: Run Tests
run: yarn run test run: yarn run test
build: build:

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
# build # build
build build
build-translations/*
hass_frontend/* hass_frontend/*
dist dist

View File

@ -1,5 +1,4 @@
build build
build-translations/*
translations/* translations/*
node_modules/* node_modules/*
hass_frontend/* hass_frontend/*

View File

@ -5,6 +5,7 @@ const env = require("../env");
require("./clean.js"); require("./clean.js");
require("./translations.js"); require("./translations.js");
require("./locale-data.js");
require("./gen-icons-json.js"); require("./gen-icons-json.js");
require("./gather-static.js"); require("./gather-static.js");
require("./compress.js"); require("./compress.js");
@ -26,7 +27,8 @@ gulp.task(
"gen-icons-json", "gen-icons-json",
"gen-pages-dev", "gen-pages-dev",
"gen-index-app-dev", "gen-index-app-dev",
"build-translations" "build-translations",
"build-locale-data"
), ),
"copy-static-app", "copy-static-app",
env.useWDS() env.useWDS()
@ -44,7 +46,7 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean", "clean",
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
// Don't compress running tests // Don't compress running tests

View File

@ -18,7 +18,7 @@ gulp.task(
}, },
"clean-cast", "clean-cast",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast", "copy-static-cast",
"gen-index-cast-dev", "gen-index-cast-dev",
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
@ -33,7 +33,7 @@ gulp.task(
}, },
"clean-cast", "clean-cast",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast", "copy-static-cast",
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
"gen-index-cast-prod" "gen-index-cast-prod"

View File

@ -20,7 +20,12 @@ gulp.task(
}, },
"clean-demo", "clean-demo",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"), gulp.parallel(
"gen-icons-json",
"gen-index-demo-dev",
"build-translations",
"build-locale-data"
),
"copy-static-demo", "copy-static-demo",
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo" env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
) )
@ -35,7 +40,7 @@ gulp.task(
"clean-demo", "clean-demo",
// Cast needs to be backwards compatible and older HA has no translations // Cast needs to be backwards compatible and older HA has no translations
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo", "copy-static-demo",
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
"gen-index-demo-prod" "gen-index-demo-prod"

View File

@ -51,6 +51,7 @@ gulp.task(
gulp.parallel( gulp.parallel(
"gen-icons-json", "gen-icons-json",
"build-translations", "build-translations",
"build-locale-data",
"gather-gallery-demos" "gather-gallery-demos"
), ),
"copy-static-gallery", "copy-static-gallery",
@ -70,6 +71,7 @@ gulp.task(
gulp.parallel( gulp.parallel(
"gen-icons-json", "gen-icons-json",
"build-translations", "build-translations",
"build-locale-data",
"gather-gallery-demos" "gather-gallery-demos"
), ),
"copy-static-gallery", "copy-static-gallery",

View File

@ -22,11 +22,18 @@ function copyTranslations(staticDir) {
// Translation output // Translation output
fs.copySync( fs.copySync(
polyPath("build-translations/output"), polyPath("build/translations/output"),
staticPath("translations") staticPath("translations")
); );
} }
function copyLocaleData(staticDir) {
const staticPath = genStaticPath(staticDir);
// Locale data output
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
}
function copyMdiIcons(staticDir) { function copyMdiIcons(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
@ -84,6 +91,11 @@ function copyMapPanel(staticDir) {
); );
} }
gulp.task("copy-locale-data", async () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
});
gulp.task("copy-translations-app", async () => { gulp.task("copy-translations-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
copyTranslations(staticDir); copyTranslations(staticDir);
@ -94,6 +106,11 @@ gulp.task("copy-translations-supervisor", async () => {
copyTranslations(staticDir); copyTranslations(staticDir);
}); });
gulp.task("copy-locale-data-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
});
gulp.task("copy-static-app", async () => { gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
// Basic static files // Basic static files
@ -103,6 +120,7 @@ gulp.task("copy-static-app", async () => {
copyPolyfills(staticDir); copyPolyfills(staticDir);
copyFonts(staticDir); copyFonts(staticDir);
copyTranslations(staticDir); copyTranslations(staticDir);
copyLocaleData(staticDir);
copyMdiIcons(staticDir); copyMdiIcons(staticDir);
// Panel assets // Panel assets
@ -123,6 +141,7 @@ gulp.task("copy-static-demo", async () => {
copyMapPanel(paths.demo_output_static); copyMapPanel(paths.demo_output_static);
copyFonts(paths.demo_output_static); copyFonts(paths.demo_output_static);
copyTranslations(paths.demo_output_static); copyTranslations(paths.demo_output_static);
copyLocaleData(paths.demo_output_static);
copyMdiIcons(paths.demo_output_static); copyMdiIcons(paths.demo_output_static);
}); });
@ -137,6 +156,7 @@ gulp.task("copy-static-cast", async () => {
copyMapPanel(paths.cast_output_static); copyMapPanel(paths.cast_output_static);
copyFonts(paths.cast_output_static); copyFonts(paths.cast_output_static);
copyTranslations(paths.cast_output_static); copyTranslations(paths.cast_output_static);
copyLocaleData(paths.cast_output_static);
copyMdiIcons(paths.cast_output_static); copyMdiIcons(paths.cast_output_static);
}); });
@ -152,5 +172,6 @@ gulp.task("copy-static-gallery", async () => {
copyMapPanel(paths.gallery_output_static); copyMapPanel(paths.gallery_output_static);
copyFonts(paths.gallery_output_static); copyFonts(paths.gallery_output_static);
copyTranslations(paths.gallery_output_static); copyTranslations(paths.gallery_output_static);
copyLocaleData(paths.gallery_output_static);
copyMdiIcons(paths.gallery_output_static); copyMdiIcons(paths.gallery_output_static);
}); });

View File

@ -24,6 +24,8 @@ gulp.task(
"gen-index-hassio-dev", "gen-index-hassio-dev",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
) )
); );
@ -38,6 +40,8 @@ gulp.task(
"gen-icons-json", "gen-icons-json",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod", "gen-index-hassio-prod",
...// Don't compress running tests ...// Don't compress running tests

View File

@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const del = require("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const paths = require("../paths");
const outDir = "build/locale-data";
gulp.task("clean-locale-data", () => del([outDir]));
gulp.task("ensure-locale-data-build-dir", (done) => {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
done();
});
const modules = {
"intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
};
gulp.task("create-locale-data", (done) => {
const translationMeta = JSON.parse(
fs.readFileSync(
path.join(paths.translations_src, "translationMetadata.json")
)
);
Object.entries(modules).forEach(([module, className]) => {
Object.keys(translationMeta).forEach((lang) => {
try {
const localeData = String(
fs.readFileSync(
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
)
)
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
"im"
),
""
)
.replace(/\)\s*}/im, "");
// make sure we have valid JSON
JSON.parse(localeData);
if (!fs.existsSync(path.join(outDir, module))) {
fs.mkdirSync(path.join(outDir, module), { recursive: true });
}
fs.writeFileSync(
path.join(outDir, `${module}/${lang}.json`),
localeData
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
throw e;
}
}
});
done();
});
});
gulp.task(
"build-locale-data",
gulp.series(
"clean-locale-data",
"ensure-locale-data-build-dir",
"create-locale-data"
)
);

View File

@ -17,7 +17,7 @@ const paths = require("../paths");
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
const workDir = "build-translations"; const workDir = "build/translations";
const fullDir = workDir + "/full"; const fullDir = workDir + "/full";
const coreDir = workDir + "/core"; const coreDir = workDir + "/core";
const outDir = workDir + "/output"; const outDir = workDir + "/output";
@ -121,7 +121,7 @@ gulp.task("clean-translations", () => del([workDir]));
gulp.task("ensure-translations-build-dir", (done) => { gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) { if (!fs.existsSync(workDir)) {
fs.mkdirSync(workDir); fs.mkdirSync(workDir, { recursive: true });
} }
done(); done();
}); });
@ -336,6 +336,14 @@ gulp.task("build-translation-fragment-supervisor", () =>
gulp gulp
.src(fullDir + "/*.json") .src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor)) .pipe(transform((data) => data.supervisor))
.pipe(
rename((filePath) => {
// In dev we create the file with the fake hash in the filename
if (!env.isProdBuild()) {
filePath.basename += "-dev";
}
})
)
.pipe(gulp.dest(workDir + "/supervisor")) .pipe(gulp.dest(workDir + "/supervisor"))
); );

View File

@ -35,26 +35,29 @@ const isWsl =
* listenHost?: string * listenHost?: string
* }} * }}
*/ */
const runDevServer = ({ const runDevServer = async ({
compiler, compiler,
contentBase, contentBase,
port, port,
listenHost = "localhost", listenHost = "localhost",
}) => }) => {
new WebpackDevServer(compiler, { const server = new WebpackDevServer(
open: true, {
watchContentBase: true, open: true,
contentBase, host: listenHost,
}).listen(port, listenHost, (err) => { port,
if (err) { static: {
throw err; directory: contentBase,
} watch: true,
// Server listening },
log( },
"[webpack-dev-server]", compiler
`Project is running at http://localhost:${port}` );
);
}); await server.start();
// Server listening
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
};
const doneHandler = (done) => (err, stats) => { const doneHandler = (done) => (err, stats) => {
if (err) { if (err) {
@ -107,13 +110,13 @@ gulp.task("webpack-prod-app", () =>
) )
); );
gulp.task("webpack-dev-server-demo", () => { gulp.task("webpack-dev-server-demo", () =>
runDevServer({ runDevServer({
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
contentBase: paths.demo_output_root, contentBase: paths.demo_output_root,
port: 8090, port: 8090,
}); })
}); );
gulp.task("webpack-prod-demo", () => gulp.task("webpack-prod-demo", () =>
prodBuild( prodBuild(
@ -123,15 +126,15 @@ gulp.task("webpack-prod-demo", () =>
) )
); );
gulp.task("webpack-dev-server-cast", () => { gulp.task("webpack-dev-server-cast", () =>
runDevServer({ runDevServer({
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
contentBase: paths.cast_output_root, contentBase: paths.cast_output_root,
port: 8080, port: 8080,
// Accessible from the network, because that's how Cast hits it. // Accessible from the network, because that's how Cast hits it.
listenHost: "0.0.0.0", listenHost: "0.0.0.0",
}); })
}); );
gulp.task("webpack-prod-cast", () => gulp.task("webpack-prod-cast", () =>
prodBuild( prodBuild(
@ -148,7 +151,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build/, poll: isWsl }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
@ -164,14 +167,14 @@ gulp.task("webpack-prod-hassio", () =>
) )
); );
gulp.task("webpack-dev-server-gallery", () => { gulp.task("webpack-dev-server-gallery", () =>
runDevServer({ runDevServer({
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't // We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
contentBase: paths.gallery_output_root, contentBase: paths.gallery_output_root,
port: 8100, port: 8100,
}); })
}); );
gulp.task("webpack-prod-gallery", () => gulp.task("webpack-prod-gallery", () =>
prodBuild( prodBuild(

View File

@ -1,3 +1,4 @@
/* eslint-disable lit/no-template-arrow */
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";

View File

@ -1,3 +1,4 @@
/* eslint-disable lit/no-template-arrow */
import { html, css, LitElement, TemplateResult } from "lit"; import { html, css, LitElement, TemplateResult } from "lit";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph"; import "../../../src/components/trace/hat-script-graph";

View File

@ -0,0 +1,212 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-form/ha-form";
import "../../../src/components/ha-card";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import type { HaFormSchema } from "../../../src/components/ha-form/ha-form";
const SCHEMAS: {
title: string;
translations?: Record<string, string>;
error?: Record<string, string>;
schema: HaFormSchema[];
}[] = [
{
title: "Authentication",
translations: {
username: "Username",
password: "Password",
invalid_login: "Invalid login",
},
error: {
base: "invalid_login",
},
schema: [
{
type: "string",
name: "username",
required: true,
},
{
type: "string",
name: "password",
required: true,
},
],
},
{
title: "One of each",
schema: [
{
type: "constant",
value: "Constant Value",
name: "constant",
required: true,
},
{
type: "boolean",
name: "bool",
optional: true,
default: false,
},
{
type: "integer",
name: "int",
optional: true,
default: 10,
},
{
type: "string",
name: "string",
optional: true,
default: "Default",
},
{
type: "select",
options: [
["default", "default"],
["other", "other"],
],
name: "select",
optional: true,
default: "default",
},
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
},
name: "multi",
optional: true,
default: ["default"],
},
],
},
{
title: "Multi select",
schema: [
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
},
name: "multi",
optional: true,
default: ["default"],
},
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
uno: "mas",
one: "more",
and: "another_one",
option: "1000",
},
name: "multi",
optional: true,
default: ["default"],
},
],
},
];
@customElement("demo-ha-form")
class DemoHaForm extends LitElement {
private lightModeData: any = [];
private darkModeData: any = [];
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const translations = info.translations || {};
const computeLabel = (schema) =>
translations[schema.name] || schema.name;
const computeError = (error) => translations[error] || error;
return [
[this.lightModeData, "light"],
[this.darkModeData, "dark"],
].map(
([data, type]) => html`
<div class="row" data-type=${type}>
<ha-card .header=${info.title}>
<div class="card-content">
<ha-form
.data=${data[idx]}
.schema=${info.schema}
.error=${info.error}
.computeError=${computeError}
.computeLabel=${computeLabel}
@value-changed=${(e) => {
data[idx] = e.detail.value;
this.requestUpdate();
}}
></ha-form>
</div>
</ha-card>
<pre>${JSON.stringify(data[idx], undefined, 2)}</pre>
</div>
`
);
})}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.shadowRoot!.querySelectorAll("[data-type=dark]").forEach((el) => {
applyThemesOnElement(
el,
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
});
}
static styles = css`
.row {
margin: 0 auto;
max-width: 800px;
display: flex;
padding: 50px;
background-color: var(--primary-background-color);
}
ha-card {
width: 100%;
max-width: 384px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.row {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-form": DemoHaForm;
}
}

View File

@ -181,9 +181,7 @@ export class SupervisorBackupContent extends LitElement {
> >
<ha-checkbox <ha-checkbox
.checked=${this.homeAssistant} .checked=${this.homeAssistant}
@click=${() => { @click=${this.toggleHomeAssistant}
this.homeAssistant = !this.homeAssistant;
}}
> >
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
@ -272,6 +270,10 @@ export class SupervisorBackupContent extends LitElement {
`; `;
} }
private toggleHomeAssistant() {
this.homeAssistant = !this.homeAssistant;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.partial-picker ha-formfield { .partial-picker ha-formfield {

View File

@ -28,6 +28,7 @@ import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content"; import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import { HassioBackupDialogParams } from "./show-dialog-hassio-backup"; import { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
@customElement("dialog-hassio-backup") @customElement("dialog-hassio-backup")
class HassioBackupDialog class HassioBackupDialog
@ -107,7 +108,7 @@ class HassioBackupDialog
fixed fixed
slot="primaryAction" slot="primaryAction"
@action=${this._handleMenuAction} @action=${this._handleMenuAction}
@closed=${(ev: Event) => ev.stopPropagation()} @closed=${stopPropagation}
> >
<mwc-icon-button slot="trigger" alt="menu"> <mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>

View File

@ -65,32 +65,21 @@ class HassioDatadiskDialog extends LitElement {
open open
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${this.moving
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
@closed=${this.closeDialog} @closed=${this.closeDialog}
?hideActions=${this.moving} ?hideActions=${this.moving}
> >
${this.moving ${this.moving
? html`<slot name="heading"> ? html` <ha-circular-progress alt="Moving" size="large" active>
<h2 id="title" class="header_title">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving"
)}
</h2>
</slot>
<ha-circular-progress alt="Moving" size="large" active>
</ha-circular-progress> </ha-circular-progress>
<p class="progress-text"> <p class="progress-text">
${this.dialogParams.supervisor.localize( ${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving_desc" "dialog.datadisk_move.moving_desc"
)} )}
</p>` </p>`
: html`<slot name="heading"> : html` ${this.devices?.length
<h2 id="title" class="header_title">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.title"
)}
</h2>
</slot>
${this.devices?.length
? html` ? html`
${this.dialogParams.supervisor.localize( ${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.description", "dialog.datadisk_move.description",

View File

@ -184,23 +184,34 @@ class HassioHostInfo extends LitElement {
<mwc-icon-button slot="trigger"> <mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-list-item @click=${() => this._handleMenuAction("hardware")}> <mwc-list-item
.action=${"hardware"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize("system.host.hardware")} ${this.supervisor.localize("system.host.hardware")}
</mwc-list-item> </mwc-list-item>
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("haos")
? html`<mwc-list-item ? html`
@click=${() => this._handleMenuAction("import_from_usb")} <mwc-list-item
.action=${"import_from_usb"}
@click=${this._handleMenuAction}
> >
${this.supervisor.localize("system.host.import_from_usb")} ${this.supervisor.localize("system.host.import_from_usb")}
</mwc-list-item> </mwc-list-item>
${this.supervisor.host.features.includes("os_agent") && ${this.supervisor.host.features.includes("os_agent") &&
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0) atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
? html`<mwc-list-item ? html`
@click=${() => this._handleMenuAction("move_datadisk")} <mwc-list-item
> .action=${"move_datadisk"}
${this.supervisor.localize("system.host.move_datadisk")} @click=${this._handleMenuAction}
</mwc-list-item>` >
: ""} ` ${this.supervisor.localize(
"system.host.move_datadisk"
)}
</mwc-list-item>
`
: ""}
`
: ""} : ""}
</ha-button-menu> </ha-button-menu>
</div> </div>
@ -223,8 +234,8 @@ class HassioHostInfo extends LitElement {
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
}); });
private async _handleMenuAction(action: string) { private async _handleMenuAction(ev) {
switch (action) { switch ((ev.target as any).action) {
case "hardware": case "hardware":
await this._showHardware(); await this._showHardware();
break; break;

View File

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

View File

@ -4,6 +4,7 @@ import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-rel
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types"; import { Resources } from "../../types";
import { getLocalLanguage } from "../../util/hass-translation";
export type LocalizeFunc = (key: string, ...args: any[]) => string; export type LocalizeFunc = (key: string, ...args: any[]) => string;
interface FormatType { interface FormatType {
@ -15,37 +16,32 @@ export interface FormatsType {
time: FormatType; time: FormatType;
} }
let loadedPolyfillLocale: Set<string> | undefined; const loadedPolyfillLocale = new Set();
const polyfillPluralRules = shouldPolyfillPluralRules();
const polyfillRelativeTime = shouldPolyfillRelativeTime();
const polyfillDateTime = shouldPolyfillDateTime();
const polyfills: Promise<any>[] = []; const polyfills: Promise<any>[] = [];
if (__BUILD__ === "latest") { if (__BUILD__ === "latest") {
if (shouldPolyfillLocale()) { if (shouldPolyfillLocale()) {
polyfills.push(import("@formatjs/intl-locale/polyfill")); polyfills.push(import("@formatjs/intl-locale/polyfill"));
} }
if (polyfillPluralRules) { if (shouldPolyfillPluralRules()) {
polyfills.push(import("@formatjs/intl-pluralrules/polyfill")); polyfills.push(import("@formatjs/intl-pluralrules/polyfill"));
polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en"));
} }
if (polyfillRelativeTime) { if (shouldPolyfillRelativeTime()) {
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill"));
} }
if (polyfillDateTime) { if (shouldPolyfillDateTime()) {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
} }
} }
let polyfillLoaded = polyfills.length === 0; export const polyfillsLoaded =
export const polyfillsLoaded = polyfillLoaded polyfills.length === 0
? undefined ? undefined
: Promise.all(polyfills).then(() => { : Promise.all(polyfills).then(() =>
loadedPolyfillLocale = new Set(); // Load the default language
polyfillLoaded = true; loadPolyfillLocales(getLocalLanguage())
// Load English so it becomes the default );
return loadPolyfillLocales("en");
});
/** /**
* Adapted from Polymer app-localize-behavior. * Adapted from Polymer app-localize-behavior.
@ -74,11 +70,11 @@ export const computeLocalize = async (
resources: Resources, resources: Resources,
formats?: FormatsType formats?: FormatsType
): Promise<LocalizeFunc> => { ): Promise<LocalizeFunc> => {
if (!polyfillLoaded) { if (polyfillsLoaded) {
await polyfillsLoaded; await polyfillsLoaded;
} }
loadPolyfillLocales(language); await loadPolyfillLocales(language);
// Everytime any of the parameters change, invalidate the strings cache. // Everytime any of the parameters change, invalidate the strings cache.
cache._localizationCache = {}; cache._localizationCache = {};
@ -132,19 +128,44 @@ export const computeLocalize = async (
}; };
export const loadPolyfillLocales = async (language: string) => { export const loadPolyfillLocales = async (language: string) => {
if (!loadedPolyfillLocale || loadedPolyfillLocale.has(language)) { if (loadedPolyfillLocale.has(language)) {
return; return;
} }
loadedPolyfillLocale.add(language); loadedPolyfillLocale.add(language);
try { try {
if (polyfillPluralRules) { if (
await import(`@formatjs/intl-pluralrules/locale-data/${language}`); Intl.NumberFormat &&
// @ts-ignore
typeof Intl.NumberFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-numberformat/${language}.json`
);
// @ts-ignore
Intl.NumberFormat.__addLocaleData(await result.json());
} }
if (polyfillRelativeTime) { if (
await import(`@formatjs/intl-relativetimeformat/locale-data/${language}`); // @ts-expect-error
Intl.RelativeTimeFormat &&
// @ts-ignore
typeof Intl.RelativeTimeFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-relativetimeformat/${language}.json`
);
// @ts-ignore
Intl.RelativeTimeFormat.__addLocaleData(await result.json());
} }
if (polyfillDateTime) { if (
await import(`@formatjs/intl-datetimeformat/locale-data/${language}`); Intl.DateTimeFormat &&
// @ts-ignore
typeof Intl.DateTimeFormat.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-datetimeformat/${language}.json`
);
// @ts-ignore
Intl.DateTimeFormat.__addLocaleData(await result.json());
} }
} catch (_e) { } catch (_e) {
// Ignore // Ignore

View File

@ -21,6 +21,7 @@ const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
"garage_door", "garage_door",
"gas", "gas",
"lock", "lock",
"motion",
"opening", "opening",
"problem", "problem",
"safety", "safety",

View File

@ -39,8 +39,8 @@ export class HaAreaSelector extends LitElement {
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
no-add no-add
.deviceFilter=${(device) => this._filterDevices(device)} .deviceFilter=${this._filterDevices}
.entityFilter=${(entity) => this._filterEntities(entity)} .entityFilter=${this._filterEntities}
.includeDeviceClasses=${this.selector.area.entity?.device_class .includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class] ? [this.selector.area.entity.device_class]
: undefined} : undefined}
@ -51,16 +51,16 @@ export class HaAreaSelector extends LitElement {
></ha-area-picker>`; ></ha-area-picker>`;
} }
private _filterEntities(entity: EntityRegistryEntry): boolean { private _filterEntities = (entity: EntityRegistryEntry): boolean => {
if (this.selector.area.entity?.integration) { if (this.selector.area.entity?.integration) {
if (entity.platform !== this.selector.area.entity.integration) { if (entity.platform !== this.selector.area.entity.integration) {
return false; return false;
} }
} }
return true; return true;
} };
private _filterDevices(device: DeviceRegistryEntry): boolean { private _filterDevices = (device: DeviceRegistryEntry): boolean => {
if ( if (
this.selector.area.device?.manufacturer && this.selector.area.device?.manufacturer &&
device.manufacturer !== this.selector.area.device.manufacturer device.manufacturer !== this.selector.area.device.manufacturer
@ -84,7 +84,7 @@ export class HaAreaSelector extends LitElement {
} }
} }
return true; return true;
} };
private async _loadConfigEntries() { private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter( this._configEntries = (await getConfigEntries(this.hass)).filter(

View File

@ -3,7 +3,7 @@ import {
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
export enum LightColorModes { export const enum LightColorModes {
UNKNOWN = "unknown", UNKNOWN = "unknown",
ONOFF = "onoff", ONOFF = "onoff",
BRIGHTNESS = "brightness", BRIGHTNESS = "brightness",

View File

@ -2,7 +2,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { DeviceRegistryEntry } from "./device_registry"; import { DeviceRegistryEntry } from "./device_registry";
export enum InclusionStrategy { export const enum InclusionStrategy {
/** /**
* Always uses Security S2 if supported, otherwise uses Security S0 for certain devices which don't work without encryption and uses no encryption otherwise. * Always uses Security S2 if supported, otherwise uses Security S0 for certain devices which don't work without encryption and uses no encryption otherwise.
* *
@ -83,6 +83,7 @@ export interface ZWaveJSNodeStatus {
node_id: number; node_id: number;
ready: boolean; ready: boolean;
status: number; status: number;
is_secure: boolean | string;
} }
export interface ZwaveJSNodeMetadata { export interface ZwaveJSNodeMetadata {
@ -154,7 +155,7 @@ export interface ZWaveJSRemovedNode {
label: string; label: string;
} }
export enum NodeStatus { export const enum NodeStatus {
Unknown, Unknown,
Asleep, Asleep,
Awake, Awake,

View File

@ -116,6 +116,14 @@ export class HaDeviceInfoZWaveJS extends LitElement {
? this.hass.localize("ui.common.yes") ? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")} : this.hass.localize("ui.common.no")}
</div> </div>
<div>
${this.hass.localize("ui.panel.config.zwave_js.device_info.is_secure")}:
${this._node.is_secure === true
? this.hass.localize("ui.common.yes")
: this._node.is_secure === false
? this.hass.localize("ui.common.no")
: this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")}
</div>
`; `;
} }

View File

@ -102,7 +102,7 @@ class PanelDeveloperTools extends LitElement {
} }
developer-tools-router { developer-tools-router {
display: block; display: block;
height: calc(100vh - 112px); height: calc(100vh - 104px);
} }
ha-tabs { ha-tabs {
margin-left: max(env(safe-area-inset-left), 24px); margin-left: max(env(safe-area-inset-left), 24px);

View File

@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table";
@ -32,63 +33,70 @@ class HaPanelDevStatistics extends LitElement {
this._validateStatistics(); this._validateStatistics();
} }
private _columns: DataTableColumnContainer = { /* eslint-disable lit/no-template-arrow */
state: { private _columns = memoizeOne(
title: "Entity", (localize): DataTableColumnContainer => ({
sortable: true, state: {
filterable: true, title: "Entity",
grows: true, sortable: true,
template: (entityState, data: any) => filterable: true,
html`${entityState grows: true,
? computeStateName(entityState) template: (entityState, data: any) =>
: data.statistic_id}`, html`${entityState
}, ? computeStateName(entityState)
statistic_id: { : data.statistic_id}`,
title: "Statistic id", },
sortable: true, statistic_id: {
filterable: true, title: "Statistic id",
hidden: this.narrow, sortable: true,
width: "30%", filterable: true,
}, hidden: this.narrow,
unit_of_measurement: { width: "30%",
title: "Unit", },
sortable: true, unit_of_measurement: {
filterable: true, title: "Unit",
width: "10%", sortable: true,
}, filterable: true,
issues: { width: "10%",
title: "Issue", },
sortable: true, issues: {
filterable: true, title: "Issue",
direction: "asc", sortable: true,
width: "30%", filterable: true,
template: (issues) => direction: "asc",
html`${issues width: "30%",
? issues.map( template: (issues) =>
(issue) => html`${issues
this.hass.localize( ? issues.map(
`ui.panel.developer-tools.tabs.statistics.issues.${issue.type}`, (issue) =>
issue.data localize(
) || issue.type `ui.panel.developer-tools.tabs.statistics.issues.${issue.type}`,
) issue.data
: ""}`, ) || issue.type
}, )
fix: { : ""}`,
title: "", },
template: (_, data: any) => fix: {
html`${data.issues title: "",
? html`<mwc-button @click=${this._fixIssue} .data=${data.issues} template: (_, data: any) =>
>Fix issue</mwc-button html`${data.issues
>` ? html`<mwc-button
: ""}`, @click=${(ev) => this._fixIssue(ev)}
width: "113px", .data=${data.issues}
}, >
}; Fix issue
</mwc-button>`
: ""}`,
width: "113px",
},
})
);
/* eslint-enable lit/no-template-arrow */
protected render() { protected render() {
return html` return html`
<ha-data-table <ha-data-table
.columns=${this._columns} .columns=${this._columns(this.hass.localize)}
.data=${this._data} .data=${this._data}
noDataText="No issues found!" noDataText="No issues found!"
id="statistic_id" id="statistic_id"
@ -123,11 +131,11 @@ class HaPanelDevStatistics extends LitElement {
if (issue.type === "unsupported_unit") { if (issue.type === "unsupported_unit") {
showAlertDialog(this, { showAlertDialog(this, {
title: "Unsupported unit", title: "Unsupported unit",
text: html`The unit of your entity is not a suppported unit for the text: html`The unit of your entity is not a supported unit for the
device class of the entity, ${issue.data.device_class}. device class of the entity, ${issue.data.device_class}.
<br />Statistics can not be generated until this entity has a <br />Statistics can not be generated until this entity has a
supported unit. <br /><br />If this unit was provided by an supported unit.<br /><br />If this unit was provided by an
integration, this is a bug. Please report an issue. <br /><br />If you integration, this is a bug. Please report an issue.<br /><br />If you
have set this unit yourself, and want to have statistics generated, have set this unit yourself, and want to have statistics generated,
make sure the unit matched the device class. The supported units are make sure the unit matched the device class. The supported units are
documented in the documented in the

View File

@ -3,6 +3,7 @@ import {
ChartDataset, ChartDataset,
ChartOptions, ChartOptions,
ParsedDataType, ParsedDataType,
ScatterDataPoint,
} from "chart.js"; } from "chart.js";
import { getRelativePosition } from "chart.js/helpers"; import { getRelativePosition } from "chart.js/helpers";
import { addHours } from "date-fns"; import { addHours } from "date-fns";
@ -21,11 +22,7 @@ import {
import "../../../../components/chart/ha-chart-base"; import "../../../../components/chart/ha-chart-base";
import type HaChartBase from "../../../../components/chart/ha-chart-base"; import type HaChartBase from "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
DeviceConsumptionEnergyPreference,
EnergyData,
getEnergyDataCollection,
} from "../../../../data/energy";
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
fetchStatistics, fetchStatistics,
@ -52,8 +49,6 @@ export class HuiEnergyDevicesGraphCard
@query("ha-chart-base") private _chart?: HaChartBase; @query("ha-chart-base") private _chart?: HaChartBase;
private _deviceConsumptionPrefs: DeviceConsumptionEnergyPreference[] = [];
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
getEnergyDataCollection(this.hass, { getEnergyDataCollection(this.hass, {
@ -110,11 +105,11 @@ export class HuiEnergyDevicesGraphCard
ticks: { ticks: {
autoSkip: false, autoSkip: false,
callback: (index) => { callback: (index) => {
const devicePref = this._deviceConsumptionPrefs[index]; const entityId = (
const entity = this.hass.states[devicePref.stat_consumption]; this._chartData.datasets[0].data[index] as ScatterDataPoint
return entity ).y;
? computeStateName(entity) const entity = this.hass.states[entityId];
: devicePref.stat_consumption; return entity ? computeStateName(entity) : entityId;
}, },
}, },
}, },
@ -160,8 +155,6 @@ export class HuiEnergyDevicesGraphCard
); );
private async _getStatistics(energyData: EnergyData): Promise<void> { private async _getStatistics(energyData: EnergyData): Promise<void> {
this._deviceConsumptionPrefs = energyData.prefs.device_consumption;
this._data = await fetchStatistics( this._data = await fetchStatistics(
this.hass, this.hass,
addHours(energyData.start, -1), addHours(energyData.start, -1),

View File

@ -23,7 +23,7 @@ const DEFAULT_FILTER = "grayscale(100%)";
const MAX_IMAGE_WIDTH = 640; const MAX_IMAGE_WIDTH = 640;
const ASPECT_RATIO_DEFAULT = 9 / 16; const ASPECT_RATIO_DEFAULT = 9 / 16;
enum LoadState { const enum LoadState {
Loading = 1, Loading = 1,
Loaded = 2, Loaded = 2,
Error = 3, Error = 3,

View File

@ -31,6 +31,9 @@ const REDIRECTS: Redirects = {
developer_events: { developer_events: {
redirect: "/developer-tools/event", redirect: "/developer-tools/event",
}, },
developer_statistics: {
redirect: "/developer-tools/statistics",
},
config: { config: {
redirect: "/config", redirect: "/config",
}, },

View File

@ -1,4 +1,4 @@
import * as translationMetadata_ from "../../build-translations/translationMetadata.json"; import * as translationMetadata_ from "../../build/translations/translationMetadata.json";
import { TranslationMetadata } from "../types.js"; import { TranslationMetadata } from "../types.js";
export const translationMetadata = (translationMetadata_ as any) export const translationMetadata = (translationMetadata_ as any)

View File

@ -2761,7 +2761,9 @@
"device_config": "Configure Device", "device_config": "Configure Device",
"reinterview_device": "Re-interview Device", "reinterview_device": "Re-interview Device",
"heal_node": "Heal Device", "heal_node": "Heal Device",
"remove_failed": "Remove Failed Device" "remove_failed": "Remove Failed Device",
"is_secure": "Secure",
"unknown": "Unknown"
}, },
"node_config": { "node_config": {
"header": "Z-Wave Device Configuration", "header": "Z-Wave Device Configuration",