diff --git a/.eslintrc.json b/.eslintrc.json index 49546398a7..9cfa58084a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -75,13 +75,16 @@ "object-curly-newline": 0, "default-case": 0, "wc/no-self-class": 0, + "no-shadow": 0, "@typescript-eslint/camelcase": 0, - "@typescript-eslint/ban-ts-ignore": 0, + "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-unused-vars": 0, - "@typescript-eslint/explicit-function-return-type": 0 + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-shadow": ["error"] }, "plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"], "processor": "disable/disable" diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md deleted file mode 100644 index 640740f5ac..0000000000 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Request a feature for the UI, Frontend or Lovelace -about: Request an new feature for the Home Assistant frontend. -labels: feature request ---- - - - -## The request - - - -## The alternatives - - - -## Additional information diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b7fc0dc4fb..7468455df2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: Request a feature for the UI, Frontend or Lovelace + url: https://github.com/home-assistant/frontend/discussions/category_choices + about: Request an new feature for the Home Assistant frontend. - name: Report a bug that is NOT related to the UI, Frontend or Lovelace url: https://github.com/home-assistant/core/issues about: This is the issue tracker for our frontend. Please report other issues with the backend repository. diff --git a/README.md b/README.md index c5b6edde12..c793fb3453 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ A complete guide can be found at the following [link](https://www.home-assistant Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. -We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices. +We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices. diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index 50e24db416..c5056cc8b6 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -52,7 +52,14 @@ module.exports.terserOptions = (latestBuild) => ({ module.exports.babelOptions = ({ latestBuild }) => ({ babelrc: false, presets: [ - !latestBuild && [require("@babel/preset-env").default, { modules: false }], + !latestBuild && [ + require("@babel/preset-env").default, + { + modules: false, + useBuiltIns: "entry", + corejs: "3.6", + }, + ], require("@babel/preset-typescript").default, ].filter(Boolean), plugins: [ @@ -62,7 +69,9 @@ module.exports.babelOptions = ({ latestBuild }) => ({ { loose: true, useBuiltIns: true }, ], // Only support the syntax, Webpack will handle it. - "@babel/syntax-dynamic-import", + "@babel/plugin-syntax-import-meta", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-syntax-top-level-await", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator", [ diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index ee2fdc406e..dbee791d4c 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -2,7 +2,6 @@ const webpack = require("webpack"); const path = require("path"); const TerserPlugin = require("terser-webpack-plugin"); const ManifestPlugin = require("webpack-manifest-plugin"); -const WorkerPlugin = require("worker-plugin"); const paths = require("./paths.js"); const bundle = require("./bundle"); @@ -30,7 +29,7 @@ const createWebpackConfig = ({ module: { rules: [ { - test: /\.js$|\.ts$/, + test: /\.m?js$|\.ts$/, exclude: bundle.babelExclude(), use: { loader: "babel-loader", @@ -54,8 +53,10 @@ const createWebpackConfig = ({ }), ], }, + experiments: { + topLevelAwait: true, + }, plugins: [ - new WorkerPlugin(), new ManifestPlugin({ // Only include the JS of entrypoints filter: (file) => file.isInitial && !file.name.endsWith(".map"), @@ -110,6 +111,22 @@ const createWebpackConfig = ({ } return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`; }, + environment: { + // The environment supports arrow functions ('() => { ... }'). + arrowFunction: latestBuild, + // The environment supports BigInt as literal (123n). + bigIntLiteral: false, + // The environment supports const and let for variable declarations. + const: latestBuild, + // The environment supports destructuring ('{ a, b } = obj'). + destructuring: latestBuild, + // The environment supports an async import() function to import EcmaScript modules. + dynamicImport: latestBuild, + // The environment supports 'for of' iteration ('for (const x of array) { ... }'). + forOf: latestBuild, + // The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...'). + module: latestBuild, + }, chunkFilename: isProdBuild && !isStatsBuild ? "chunk.[chunkhash].js" diff --git a/cast/src/launcher/layout/hc-layout.ts b/cast/src/launcher/layout/hc-layout.ts index f9d82223b2..1c7a850a30 100644 --- a/cast/src/launcher/layout/hc-layout.ts +++ b/cast/src/launcher/layout/hc-layout.ts @@ -30,7 +30,7 @@ class HcLayout extends LitElement {
-
+

Home Assistant Cast${this.subtitle ? ` – ${this.subtitle}` : ""} ${this.auth ? html` @@ -44,7 +44,7 @@ class HcLayout extends LitElement {

` : ""} -
+
diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index c188e516b7..67c4c50849 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -23,9 +23,9 @@ import { hassioStyle } from "../resources/hassio-style"; class HassioAddonRepositoryEl extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public repo!: HassioAddonRepository; + @property({ attribute: false }) public repo!: HassioAddonRepository; - @property() public addons!: HassioAddonInfo[]; + @property({ attribute: false }) public addons!: HassioAddonInfo[]; @property() public filter!: string; @@ -78,18 +78,18 @@ class HassioAddonRepositoryEl extends LitElement { .title=${addon.name} .description=${addon.description} .available=${addon.available} - .icon=${addon.installed && addon.installed !== addon.version + .icon=${addon.installed && addon.update_available ? mdiArrowUpBoldCircle : mdiPuzzle} .iconTitle=${addon.installed - ? addon.installed !== addon.version + ? addon.update_available ? "New version available" : "Add-on is installed" : addon.available ? "Add-on is not installed" : "Add-on is not available on your system"} .iconClass=${addon.installed - ? addon.installed !== addon.version + ? addon.update_available ? "update" : "installed" : !addon.available @@ -104,7 +104,7 @@ class HassioAddonRepositoryEl extends LitElement { : undefined} .showTopbar=${addon.installed || !addon.available} .topbarClass=${addon.installed - ? addon.installed !== addon.version + ? addon.update_available ? "update" : "installed" : !addon.available diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index baf2fef515..fc15fc38dc 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -11,6 +11,7 @@ import { PropertyValues, } from "lit-element"; import { html, TemplateResult } from "lit-html"; +import { atLeastVersion } from "../../../src/common/config/version"; import "../../../src/common/search/search-input"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-svg-icon"; @@ -24,6 +25,7 @@ import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../src/types"; +import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addon-repository"; @@ -98,14 +100,14 @@ class HassioAddonStore extends LitElement { main-page .tabs=${supervisorTabs} > - Add-on store + Add-on Store - + Repositories @@ -113,6 +115,12 @@ class HassioAddonStore extends LitElement { Reload + ${this.hass.userData?.showAdvanced && + atLeastVersion(this.hass.config.version, 0, 117) + ? html` + Registries + ` + : ""} ${repos.length === 0 ? html`` @@ -157,6 +165,9 @@ class HassioAddonStore extends LitElement { case 1: this.refreshData(); break; + case 2: + this._manageRegistries(); + break; } } @@ -173,6 +184,10 @@ class HassioAddonStore extends LitElement { }); } + private async _manageRegistries() { + showRegistriesDialog(this); + } + private async _loadData() { try { const addonsInfo = await fetchHassioAddonsInfo(this.hass); diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts index 415e3e3e91..beb8b6a749 100644 --- a/hassio/src/addon-view/config/hassio-addon-config.ts +++ b/hassio/src/addon-view/config/hassio-addon-config.ts @@ -39,13 +39,11 @@ class HassioAddonConfig extends LitElement { @property({ type: Boolean }) private _configHasChanged = false; - @query("ha-yaml-editor") private _editor!: HaYamlEditor; + @property({ type: Boolean }) private _valid = true; + + @query("ha-yaml-editor", true) private _editor!: HaYamlEditor; protected render(): TemplateResult { - const editor = this._editor; - // If editor not rendered, don't show the error. - const valid = editor ? editor.isValid : true; - return html`

${this.addon.name}

@@ -54,7 +52,7 @@ class HassioAddonConfig extends LitElement { @value-changed=${this._configChanged} > ${this._error ? html`
${this._error}
` : ""} - ${valid ? "" : html`
Invalid YAML
`} + ${this._valid ? "" : html`
Invalid YAML
`}
@@ -62,7 +60,7 @@ class HassioAddonConfig extends LitElement { Save @@ -78,9 +76,9 @@ class HassioAddonConfig extends LitElement { } } - private _configChanged(): void { + private _configChanged(ev): void { this._configHasChanged = true; - this.requestUpdate(); + this._valid = ev.detail.isValid; } private async _resetTapped(ev: CustomEvent): Promise { diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 1de072ee99..91b3410d67 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -69,7 +69,7 @@ const STAGE_ICON = { const PERMIS_DESC = { stage: { title: "Add-on Stage", - description: `Add-ons can have one of three stages:\n\n **Stable**: These are add-ons ready to be used in production.\n\n **Experimental**: These may contain bugs, and may be unfinished.\n\n **Deprecated**: These add-ons will no longer receive any updates.`, + description: `Add-ons can have one of three stages:\n\n **Stable**: These are add-ons ready to be used in production.\n\n **Experimental**: These may contain bugs, and may be unfinished.\n\n **Deprecated**: These add-ons will no longer receive any updates.`, }, rating: { title: "Add-on Security Rating", @@ -135,7 +135,7 @@ class HassioAddonInfo extends LitElement { protected render(): TemplateResult { return html` - ${this._computeUpdateAvailable + ${this.addon.update_available ? html`
@@ -178,7 +178,7 @@ class HassioAddonInfo extends LitElement { ${!this.addon.protected ? html` -
Warning: Protection mode is disabled!
+

Warning: Protection mode is disabled!

Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
@@ -202,14 +202,14 @@ class HassioAddonInfo extends LitElement { ` : html` `} ` @@ -283,7 +283,7 @@ class HassioAddonInfo extends LitElement { label="host" description="" > - + ` : ""} @@ -295,7 +295,7 @@ class HassioAddonInfo extends LitElement { label="hardware" description="" > - + ` : ""} @@ -307,7 +307,7 @@ class HassioAddonInfo extends LitElement { label="hass" description="" > - + ` : ""} @@ -319,7 +319,7 @@ class HassioAddonInfo extends LitElement { label="hassio" .description=${this.addon.hassio_role} > - + ` : ""} @@ -331,7 +331,7 @@ class HassioAddonInfo extends LitElement { label="docker" description="" > - + ` : ""} @@ -343,7 +343,7 @@ class HassioAddonInfo extends LitElement { label="host pid" description="" > - + ` : ""} @@ -356,7 +356,7 @@ class HassioAddonInfo extends LitElement { label="apparmor" description="" > - + ` : ""} @@ -368,7 +368,7 @@ class HassioAddonInfo extends LitElement { label="auth" description="" > - + ` : ""} @@ -381,7 +381,7 @@ class HassioAddonInfo extends LitElement { description="" > ` @@ -609,15 +609,6 @@ class HassioAddonInfo extends LitElement { return this.addon?.state === "started"; } - private get _computeUpdateAvailable(): boolean | "" { - return ( - this.addon && - !this.addon.detached && - this.addon.version && - this.addon.version !== this.addon.version_latest - ); - } - private get _pathWebui(): string | null { return ( this.addon.webui && @@ -798,10 +789,10 @@ class HassioAddonInfo extends LitElement { ); if (!validate.data.valid) { await showConfirmationDialog(this, { - title: "Failed to start addon - configruation validation faled!", + title: "Failed to start addon - configuration validation failed!", text: validate.data.message.split(" Got ")[0], confirm: () => this._openConfiguration(), - confirmText: "Go to configruation", + confirmText: "Go to configuration", dismissText: "Cancel", }); button.progress = false; diff --git a/hassio/src/components/hassio-card-content.ts b/hassio/src/components/hassio-card-content.ts index ee57a98ab8..1123aa9f5a 100644 --- a/hassio/src/components/hassio-card-content.ts +++ b/hassio/src/components/hassio-card-content.ts @@ -50,7 +50,7 @@ class HassioCardContent extends LitElement { ` : html` diff --git a/hassio/src/components/hassio-upload-snapshot.ts b/hassio/src/components/hassio-upload-snapshot.ts index 94c7828ad7..5740e0621e 100644 --- a/hassio/src/components/hassio-upload-snapshot.ts +++ b/hassio/src/components/hassio-upload-snapshot.ts @@ -1,4 +1,3 @@ -import "../../../src/components/ha-file-upload"; import "@material/mwc-icon-button/mwc-icon-button"; import { mdiFolderUpload } from "@mdi/js"; import "@polymer/iron-input/iron-input"; @@ -12,13 +11,15 @@ import { } from "lit-element"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/ha-circular-progress"; +import "../../../src/components/ha-file-upload"; import "../../../src/components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { HassioSnapshot, uploadSnapshot, } from "../../../src/data/hassio/snapshot"; -import { HomeAssistant } from "../../../src/types"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; +import { HomeAssistant } from "../../../src/types"; declare global { interface HASSDomEvents { @@ -65,7 +66,7 @@ export class HassioUploadSnapshot extends LitElement { } catch (err) { showAlertDialog(this, { title: "Upload failed", - text: err.toString(), + text: extractApiErrorMessage(err), confirmText: "ok", }); } finally { diff --git a/hassio/src/dashboard/hassio-addons.ts b/hassio/src/dashboard/hassio-addons.ts index 8c6963cac9..717a6397a3 100644 --- a/hassio/src/dashboard/hassio-addons.ts +++ b/hassio/src/dashboard/hassio-addons.ts @@ -52,22 +52,21 @@ class HassioAddons extends LitElement { .title=${addon.name} .description=${addon.description} available - .showTopbar=${addon.installed !== addon.version} + .showTopbar=${addon.update_available} topbarClass="update" - .icon=${addon.installed !== addon.version + .icon=${addon.update_available! ? mdiArrowUpBoldCircle : mdiPuzzle} .iconTitle=${addon.state !== "started" ? "Add-on is stopped" - : addon.installed !== addon.version + : addon.update_available! ? "New version available" : "Add-on is running"} - .iconClass=${addon.installed && - addon.installed !== addon.version + .iconClass=${addon.update_available ? addon.state === "started" ? "update" : "update stopped" - : addon.installed && addon.state === "started" + : addon.state === "started" ? "running" : "stopped"} .iconImage=${atLeastVersion( diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index a95eafbd76..9f8fa97d37 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -5,11 +5,11 @@ import { CSSResult, customElement, html, - internalProperty, LitElement, property, TemplateResult, } from "lit-element"; +import memoizeOne from "memoize-one"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; import "../../../src/components/ha-svg-icon"; @@ -35,29 +35,30 @@ import { hassioStyle } from "../resources/hassio-style"; export class HassioUpdate extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo; + @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo; @property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo; - @property() public supervisorInfo: HassioSupervisorInfo; + @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo; - @internalProperty() private _error?: string; + private _pendingUpdates = memoizeOne( + ( + core?: HassioHomeAssistantInfo, + supervisor?: HassioSupervisorInfo, + os?: HassioHassOSInfo + ): number => { + return [core, supervisor, os].filter( + (value) => !!value && value?.update_available + ).length; + } + ); protected render(): TemplateResult { - const updatesAvailable: number = [ + const updatesAvailable = this._pendingUpdates( this.hassInfo, this.supervisorInfo, - this.hassOsInfo, - ].filter((value) => { - return ( - !!value && - (value.version_latest - ? value.version !== value.version_latest - : value.version_latest - ? value.version !== value.version_latest - : false) - ); - }).length; + this.hassOsInfo + ); if (!updatesAvailable) { return html``; @@ -65,9 +66,6 @@ export class HassioUpdate extends LitElement { return html`
- ${this._error - ? html`
Error: ${this._error}
` - : ""}

${updatesAvailable > 1 ? "Updates Available 🎉" @@ -76,26 +74,24 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard( "Home Assistant Core", - this.hassInfo.version, - this.hassInfo.version_latest, + this.hassInfo!, "hassio/homeassistant/update", `https://${ - this.hassInfo.version_latest.includes("b") ? "rc" : "www" - }.home-assistant.io/latest-release-notes/`, - mdiHomeAssistant + this.hassInfo?.version_latest.includes("b") ? "rc" : "www" + }.home-assistant.io/latest-release-notes/` )} ${this._renderUpdateCard( "Supervisor", - this.supervisorInfo.version, - this.supervisorInfo.version_latest, + this.supervisorInfo!, "hassio/supervisor/update", - `https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}` + `https://github.com//home-assistant/hassio/releases/tag/${ + this.supervisorInfo!.version_latest + }` )} ${this.hassOsInfo ? this._renderUpdateCard( "Operating System", - this.hassOsInfo.version, - this.hassOsInfo.version_latest, + this.hassOsInfo, "hassio/os/update", `https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}` ) @@ -107,28 +103,22 @@ export class HassioUpdate extends LitElement { private _renderUpdateCard( name: string, - curVersion: string, - lastVersion: string, + object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo, apiPath: string, - releaseNotesUrl: string, - icon?: string + releaseNotesUrl: string ): TemplateResult { - if (!lastVersion || lastVersion === curVersion) { + if (!object.update_available) { return html``; } return html`
- ${icon - ? html` -
- -
- ` - : ""} -
${name} ${lastVersion}
+
+ +
+
${name} ${object.version_latest}
- You are currently running version ${curVersion} + You are currently running version ${object.version}
@@ -138,7 +128,7 @@ export class HassioUpdate extends LitElement { Update diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 1ab958891b..8569b8bc60 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -39,7 +39,8 @@ import type { HomeAssistant } from "../../../../src/types"; import { HassioNetworkDialogParams } from "./show-dialog-network"; @customElement("dialog-hassio-network") -export class DialogHassioNetwork extends LitElement implements HassDialog { +export class DialogHassioNetwork extends LitElement + implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @internalProperty() private _prosessing = false; diff --git a/hassio/src/dialogs/registries/dialog-hassio-registries.ts b/hassio/src/dialogs/registries/dialog-hassio-registries.ts new file mode 100644 index 0000000000..fef6e3e0a8 --- /dev/null +++ b/hassio/src/dialogs/registries/dialog-hassio-registries.ts @@ -0,0 +1,245 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-icon-button/mwc-icon-button"; +import "@material/mwc-list/mwc-list-item"; +import { mdiDelete } from "@mdi/js"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import "../../../../src/components/ha-circular-progress"; +import { createCloseHeading } from "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; +import { + addHassioDockerRegistry, + fetchHassioDockerRegistries, + removeHassioDockerRegistry, +} from "../../../../src/data/hassio/docker"; +import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; +import type { HomeAssistant } from "../../../../src/types"; + +@customElement("dialog-hassio-registries") +class HassioRegistriesDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) private _registries?: { + registry: string; + username: string; + }[]; + + @internalProperty() private _registry?: string; + + @internalProperty() private _username?: string; + + @internalProperty() private _password?: string; + + @internalProperty() private _opened = false; + + @internalProperty() private _addingRegistry = false; + + protected render(): TemplateResult { + return html` + +
+ ${this._addingRegistry + ? html` + + + + + + Add registry + + ` + : html`${this._registries?.length + ? this._registries.map((entry) => { + return html` + + ${entry.registry} + Username: ${entry.username} + + + + + `; + }) + : html` + + No registries configured + + `} + + Add new registry + `} +
+
+ `; + } + + private _inputChanged(ev: Event) { + const target = ev.currentTarget as PaperInputElement; + this[`_${target.name}`] = target.value; + } + + public async showDialog(_dialogParams: any): Promise { + this._opened = true; + await this._loadRegistries(); + await this.updateComplete; + } + + public closeDialog(): void { + this._addingRegistry = false; + this._opened = false; + } + + public focus(): void { + this.updateComplete.then(() => + (this.shadowRoot?.querySelector( + "[dialogInitialFocus]" + ) as HTMLElement)?.focus() + ); + } + + private async _loadRegistries(): Promise { + const registries = await fetchHassioDockerRegistries(this.hass); + this._registries = Object.keys(registries!.registries).map((key) => ({ + registry: key, + username: registries.registries[key].username, + })); + } + + private _addRegistry(): void { + this._addingRegistry = true; + } + + private async _addNewRegistry(): Promise { + const data = {}; + data[this._registry!] = { + username: this._username, + password: this._password, + }; + + try { + await addHassioDockerRegistry(this.hass, data); + await this._loadRegistries(); + this._addingRegistry = false; + } catch (err) { + showAlertDialog(this, { + title: "Failed to add registry", + text: extractApiErrorMessage(err), + }); + } + } + + private async _removeRegistry(ev: Event): Promise { + const entry = (ev.currentTarget as any).entry; + + try { + await removeHassioDockerRegistry(this.hass, entry.registry); + await this._loadRegistries(); + } catch (err) { + showAlertDialog(this, { + title: "Failed to remove registry", + text: extractApiErrorMessage(err), + }); + } + } + + static get styles(): CSSResult[] { + return [ + haStyle, + haStyleDialog, + css` + ha-dialog.button-left { + --justify-action-buttons: flex-start; + } + paper-icon-item { + cursor: pointer; + } + .form { + color: var(--primary-text-color); + } + .option { + border: 1px solid var(--divider-color); + border-radius: 4px; + margin-top: 4px; + } + mwc-button { + margin-left: 8px; + } + mwc-icon-button { + color: var(--error-color); + margin: -10px; + } + mwc-list-item { + cursor: default; + } + mwc-list-item span[slot="secondary"] { + color: var(--secondary-text-color); + } + ha-paper-dropdown-menu { + display: block; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-hassio-registries": HassioRegistriesDialog; + } +} diff --git a/hassio/src/dialogs/registries/show-dialog-registries.ts b/hassio/src/dialogs/registries/show-dialog-registries.ts new file mode 100644 index 0000000000..a9e871d17e --- /dev/null +++ b/hassio/src/dialogs/registries/show-dialog-registries.ts @@ -0,0 +1,13 @@ +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "./dialog-hassio-registries"; + +export const showRegistriesDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-hassio-registries", + dialogImport: () => + import( + /* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries" + ), + dialogParams: {}, + }); +}; diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts index 150fc2b181..2d8d028cdf 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -39,7 +39,7 @@ class HassioRepositoriesDialog extends LitElement { @property({ attribute: false }) private _dialogParams?: HassioRepositoryDialogParams; - @query("#repository_input") private _optionInput?: PaperInputElement; + @query("#repository_input", true) private _optionInput?: PaperInputElement; @internalProperty() private _opened = false; @@ -91,7 +91,7 @@ class HassioRepositoriesDialog extends LitElement { title="Remove" @click=${this._removeRepository} > - + `; diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts index ed0532d29f..1f657bf0d8 100644 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts @@ -19,7 +19,7 @@ import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload" @customElement("dialog-hassio-snapshot-upload") export class DialogHassioSnapshotUpload extends LitElement - implements HassDialog { + implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @internalProperty() private _params?: HassioSnapshotUploadDialogParams; diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts index 790ebc467c..d3524bc4ed 100755 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts @@ -1,6 +1,7 @@ import "@material/mwc-button"; import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js"; -import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; +import "@polymer/paper-checkbox/paper-checkbox"; +import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-input/paper-input"; import { css, @@ -196,7 +197,7 @@ class HassioSnapshotDialog extends LitElement { @click=${this._downloadClicked} slot="primaryAction" > - + Download Snapshot ` : ""} @@ -205,7 +206,7 @@ class HassioSnapshotDialog extends LitElement { @click=${this._partialRestoreClicked} slot="secondaryAction" > - + Restore Selected ${this._snapshot.type === "full" @@ -214,7 +215,7 @@ class HassioSnapshotDialog extends LitElement { @click=${this._fullRestoreClicked} slot="secondaryAction" > - + Wipe & restore ` @@ -224,7 +225,10 @@ class HassioSnapshotDialog extends LitElement { @click=${this._deleteClicked} slot="secondaryAction" > - + Delete Snapshot ` : ""} @@ -440,6 +444,19 @@ class HassioSnapshotDialog extends LitElement { return; } + if (window.location.href.includes("ui.nabu.casa")) { + const confirm = await showConfirmationDialog(this, { + title: "Potential slow download", + text: + "Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?", + confirmText: "continue", + dismissText: "cancel", + }); + if (!confirm) { + return; + } + } + const name = this._computeName.replace(/[^a-z0-9]+/gi, "_"); const a = document.createElement("a"); a.href = signedPath.path; diff --git a/hassio/src/hassio-panel-router.ts b/hassio/src/hassio-panel-router.ts index 00830423b0..6cad299188 100644 --- a/hassio/src/hassio-panel-router.ts +++ b/hassio/src/hassio-panel-router.ts @@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage { @property({ type: Boolean }) public narrow!: boolean; - @property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo; + @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo; @property({ attribute: false }) public hassioInfo!: HassioInfo; - @property({ attribute: false }) public hostInfo: HassioHostInfo; + @property({ attribute: false }) public hostInfo?: HassioHostInfo; - @property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo; + @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts index a3ecdaf784..52168d1b3f 100644 --- a/hassio/src/hassio-router.ts +++ b/hassio/src/hassio-router.ts @@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage { }, }; - @internalProperty() private _supervisorInfo: HassioSupervisorInfo; + @internalProperty() private _supervisorInfo?: HassioSupervisorInfo; - @internalProperty() private _hostInfo: HassioHostInfo; + @internalProperty() private _hostInfo?: HassioHostInfo; @internalProperty() private _hassioInfo?: HassioInfo; @internalProperty() private _hassOsInfo?: HassioHassOSInfo; - @internalProperty() private _hassInfo: HassioHomeAssistantInfo; + @internalProperty() private _hassInfo?: HassioHomeAssistantInfo; protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); diff --git a/hassio/src/hassio-tabs.ts b/hassio/src/hassio-tabs.ts index f5dbaf4499..7c7601a6b3 100644 --- a/hassio/src/hassio-tabs.ts +++ b/hassio/src/hassio-tabs.ts @@ -8,7 +8,7 @@ export const supervisorTabs: PageNavigation[] = [ iconPath: mdiViewDashboard, }, { - name: "Add-on store", + name: "Add-on Store", path: `/hassio/store`, iconPath: mdiStore, }, diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts index 8780d9617d..ec899c7094 100644 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ b/hassio/src/ingress-view/hassio-ingress-view.ts @@ -57,7 +57,7 @@ class HassioIngressView extends LitElement { aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")} @click=${this._toggleMenu} > - +
${this._addon.name}
diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index 8f040c906b..96dd6caecf 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -117,7 +117,7 @@ class HassioSnapshots extends LitElement { @action=${this._handleAction} > - + Reload @@ -131,7 +131,7 @@ class HassioSnapshots extends LitElement {

- Create snapshot + Create Snapshot

Snapshots allow you to easily backup and restore all data of your @@ -219,7 +219,7 @@ class HassioSnapshots extends LitElement {

-

Available snapshots

+

Available Snapshots

${this._snapshots === undefined ? undefined diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 94400402ae..5a2c5776bc 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -87,7 +87,7 @@ class HassioHostInfo extends LitElement { ${this.hostInfo.features.includes("network") ? html` - IP address + IP Address ${primaryIpAddress} @@ -103,13 +103,13 @@ class HassioHostInfo extends LitElement { - Operating system + Operating System ${this.hostInfo.operating_system} - ${this.hostInfo.version !== this.hostInfo.version_latest && - this.hostInfo.features.includes("hassos") + ${this.hostInfo.features.includes("hassos") && + this.hassOsInfo.update_available ? html` { const curHostname: string = this.hostInfo.hostname; const hostname = await showPromptDialog(this, { - title: "Change hostname", + title: "Change Hostname", inputLabel: "Please enter a new hostname:", inputType: "string", defaultValue: curHostname, diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index fcec51d607..9a1fa07f5f 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -7,18 +7,21 @@ import { property, TemplateResult, } from "lit-element"; +import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-switch"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; +import { fetchHassioResolution } from "../../../src/data/hassio/resolution"; import { + fetchHassioSupervisorInfo, HassioSupervisorInfo as HassioSupervisorInfoType, reloadSupervisor, setSupervisorOption, SupervisorOptions, updateSupervisor, - fetchHassioSupervisorInfo, } from "../../../src/data/hassio/supervisor"; import { showAlertDialog, @@ -26,14 +29,42 @@ import { } from "../../../src/dialogs/generic/show-dialog-box"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; +import { documentationUrl } from "../../../src/util/documentation-url"; import { hassioStyle } from "../resources/hassio-style"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; + +const ISSUES = { + container: { + title: "Containers known to cause issues", + url: "/more-info/unsupported/container", + }, + dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" }, + docker_configuration: { + title: "Docker Configuration", + url: "/more-info/unsupported/docker_configuration", + }, + docker_version: { + title: "Docker Version", + url: "/more-info/unsupported/docker_version", + }, + lxc: { title: "LXC", url: "/more-info/unsupported/lxc" }, + network_manager: { + title: "Network Manager", + url: "/more-info/unsupported/network_manager", + }, + os: { title: "Operating System", url: "/more-info/unsupported/os" }, + privileged: { + title: "Supervisor is not privileged", + url: "/more-info/unsupported/privileged", + }, + systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" }, +}; @customElement("hassio-supervisor-info") class HassioSupervisorInfo extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public supervisorInfo!: HassioSupervisorInfoType; + @property({ attribute: false }) + public supervisorInfo!: HassioSupervisorInfoType; @property() public hostInfo!: HassioHostInfoType; @@ -51,12 +82,12 @@ class HassioSupervisorInfo extends LitElement { - Newest version + Newest Version ${this.supervisorInfo.version_latest} - ${this.supervisorInfo.version !== this.supervisorInfo.version_latest + ${this.supervisorInfo.update_available ? html` - Share diagnostics + Share Diagnostics
Share crash reports and diagnostic information. @@ -118,24 +149,19 @@ class HassioSupervisorInfo extends LitElement { ` : html`
You are running an unsupported installation. - - Learn More - + Learn more +
`}
Reload @@ -181,7 +207,7 @@ class HassioSupervisorInfo extends LitElement { }; await setSupervisorOption(this.hass, data); await reloadSupervisor(this.hass); - this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); + fireEvent(this, "hass-api-called", { success: true, response: null }); } catch (err) { showAlertDialog(this, { title: "Failed to set supervisor option", @@ -212,7 +238,7 @@ class HassioSupervisorInfo extends LitElement { button.progress = true; const confirmed = await showConfirmationDialog(this, { - title: "Update supervisor", + title: "Update Supervisor", text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`, confirmText: "update", dismissText: "cancel", @@ -249,6 +275,32 @@ class HassioSupervisorInfo extends LitElement { }); } + private async _unsupportedDialog(): Promise { + const resolution = await fetchHassioResolution(this.hass); + await showAlertDialog(this, { + title: "You are running an unsupported installation", + text: html`Below is a list of issues found with your installation, click + on the links to learn how you can resolve the issues.

+ `, + }); + } + private async _toggleDiagnostics(): Promise { try { const data: SupervisorOptions = { diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts index 88528eb431..2e1e2162aa 100644 --- a/hassio/src/system/hassio-supervisor-log.ts +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -76,7 +76,7 @@ class HassioSupervisorLog extends LitElement { ${this.hass.userData?.showAdvanced ? html` +
${metrics.map((metric) => - this._renderMetric(metric.description, metric.value ?? 0) + this._renderMetric( + metric.description, + metric.value ?? 0, + metric.tooltip + ) )}
@@ -77,13 +88,17 @@ class HassioSystemMetrics extends LitElement { this._loadData(); } - private _renderMetric(description: string, value: number): TemplateResult { + private _renderMetric( + description: string, + value: number, + tooltip?: string + ): TemplateResult { const roundedValue = roundWithOneDecimal(value); return html` ${description} -
+
${roundedValue}% @@ -155,6 +170,7 @@ class HassioSystemMetrics extends LitElement { } .value { width: 42px; + padding-right: 4px; } `, ]; diff --git a/package.json b/package.json index e176dd4e88..0a0002bec7 100644 --- a/package.json +++ b/package.json @@ -22,28 +22,29 @@ "author": "Paulus Schoutsen (http://paulusschoutsen.nl)", "license": "Apache-2.0", "dependencies": { - "@formatjs/intl-pluralrules": "^1.5.8", + "@formatjs/intl-getcanonicallocales": "^1.4.6", + "@formatjs/intl-pluralrules": "^3.4.10", "@fullcalendar/common": "5.1.0", "@fullcalendar/core": "5.1.0", "@fullcalendar/daygrid": "5.1.0", "@fullcalendar/interaction": "5.1.0", "@fullcalendar/list": "5.1.0", - "@material/chips": "=8.0.0-canary.096a7a066.0", - "@material/circular-progress": "=8.0.0-canary.a78ceb112.0", - "@material/mwc-button": "^0.18.0", - "@material/mwc-checkbox": "^0.18.0", - "@material/mwc-dialog": "^0.18.0", - "@material/mwc-fab": "^0.18.0", - "@material/mwc-formfield": "^0.18.0", - "@material/mwc-icon-button": "^0.18.0", - "@material/mwc-list": "^0.18.0", - "@material/mwc-menu": "^0.18.0", - "@material/mwc-radio": "^0.18.0", - "@material/mwc-ripple": "^0.18.0", - "@material/mwc-switch": "^0.18.0", - "@material/mwc-tab": "^0.18.0", - "@material/mwc-tab-bar": "^0.18.0", - "@material/top-app-bar": "=8.0.0-canary.096a7a066.0", + "@material/chips": "=8.0.0-canary.774dcfc8e.0", + "@material/mwc-button": "^0.19.0", + "@material/mwc-checkbox": "^0.19.0", + "@material/mwc-circular-progress": "^0.19.0", + "@material/mwc-dialog": "^0.19.0", + "@material/mwc-fab": "^0.19.0", + "@material/mwc-formfield": "^0.19.0", + "@material/mwc-icon-button": "^0.19.0", + "@material/mwc-list": "^0.19.0", + "@material/mwc-menu": "^0.19.0", + "@material/mwc-radio": "^0.19.0", + "@material/mwc-ripple": "^0.19.0", + "@material/mwc-switch": "^0.19.0", + "@material/mwc-tab": "^0.19.0", + "@material/mwc-tab-bar": "^0.19.0", + "@material/top-app-bar": "=8.0.0-canary.774dcfc8e.0", "@mdi/js": "5.6.55", "@mdi/svg": "5.6.55", "@polymer/app-layout": "^3.0.2", @@ -77,7 +78,7 @@ "@polymer/paper-toast": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.1.0", - "@thomasloven/round-slider": "0.5.0", + "@thomasloven/round-slider": "0.5.2", "@types/chromecast-caf-sender": "^1.0.3", "@types/sortablejs": "^1.10.6", "@vaadin/vaadin-combo-box": "^5.0.10", @@ -88,11 +89,11 @@ "chartjs-chart-timeline": "^0.3.0", "codemirror": "^5.49.0", "comlink": "^4.3.0", + "core-js": "^3.6.5", "cpx": "^1.5.0", "cropperjs": "^1.5.7", "deep-clone-simple": "^1.1.1", "deep-freeze": "^0.0.1", - "es6-object-assign": "^1.1.0", "fecha": "^4.2.0", "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", @@ -103,15 +104,16 @@ "js-yaml": "^3.13.1", "leaflet": "^1.4.0", "leaflet-draw": "^1.0.4", - "lit-element": "^2.3.1", - "lit-html": "^1.2.1", + "lit-element": "^2.4.0", + "lit-html": "^1.3.0", "lit-virtualizer": "^0.4.2", "marked": "^1.1.1", "mdn-polyfills": "^5.16.0", "memoize-one": "^5.0.2", - "node-vibrant": "^3.1.5", + "node-vibrant": "^3.1.6", "proxy-polyfill": "^0.3.1", "punycode": "^2.1.1", + "qrcode": "^1.4.4", "regenerator-runtime": "^0.13.2", "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", @@ -128,16 +130,18 @@ "xss": "^1.0.6" }, "devDependencies": { - "@babel/core": "^7.9.0", - "@babel/plugin-external-helpers": "^7.8.3", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-proposal-decorators": "^7.8.3", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.9.5", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", + "@babel/core": "^7.11.6", + "@babel/plugin-external-helpers": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-decorators": "^7.10.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.9.5", - "@babel/preset-typescript": "^7.9.0", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/preset-env": "^7.11.5", + "@babel/preset-typescript": "^7.10.4", "@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-json": "^4.0.3", "@rollup/plugin-node-resolve": "^7.1.3", @@ -154,8 +158,8 @@ "@types/mocha": "^7.0.2", "@types/resize-observer-browser": "^0.1.3", "@types/webspeechapi": "^0.0.29", - "@typescript-eslint/eslint-plugin": "^2.28.0", - "@typescript-eslint/parser": "^2.28.0", + "@typescript-eslint/eslint-plugin": "^4.4.0", + "@typescript-eslint/parser": "^4.4.0", "babel-loader": "^8.1.0", "chai": "^4.2.0", "del": "^4.0.0", @@ -180,7 +184,7 @@ "html-minifier": "^4.0.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", - "lit-analyzer": "^1.2.0", + "lit-analyzer": "^1.2.1", "lodash.template": "^4.5.0", "magic-string": "^0.25.7", "map-stream": "^0.0.7", @@ -201,29 +205,24 @@ "source-map-url": "^0.4.0", "systemjs": "^6.3.2", "terser-webpack-plugin": "^3.0.6", - "ts-lit-plugin": "^1.2.0", + "ts-lit-plugin": "^1.2.1", "ts-mocha": "^7.0.0", - "typescript": "^3.8.3", + "typescript": "^4.0.3", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", - "webpack": "^4.40.2", - "webpack-cli": "^3.3.9", + "webpack": "5.0.0-rc.3", + "webpack-cli": "4.0.0-rc.0", "webpack-dev-server": "^3.10.3", - "webpack-manifest-plugin": "^2.0.4", - "workbox-build": "^5.1.3", - "worker-plugin": "^4.0.3" + "webpack-manifest-plugin": "3.0.0-rc.0", + "workbox-build": "^5.1.3" }, "_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page", "_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569", "resolutions": { "@webcomponents/webcomponentsjs": "^2.2.10", "@polymer/polymer": "3.1.0", - "lit-html": "1.2.1", - "lit-element": "2.3.1", - "@material/animation": "8.0.0-canary.096a7a066.0", - "@material/base": "8.0.0-canary.096a7a066.0", - "@material/feature-targeting": "8.0.0-canary.096a7a066.0", - "@material/theme": "8.0.0-canary.096a7a066.0" + "lit-html": "1.3.0", + "lit-element": "2.4.0" }, "main": "src/home-assistant.js", "husky": { diff --git a/setup.py b/setup.py index aeca082009..6077924a32 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20201001.2", + version="20201021.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 4ad4b3330b..a4234c56ca 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -200,7 +200,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { private _redirect(authCode: string) { // OAuth 2: 3.1.2 we need to retain query component of a redirect URI - let url = this.redirectUri!!; + let url = this.redirectUri!; if (!url.includes("?")) { url += "?"; } else if (!url.endsWith("&")) { diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts index c2a761394b..ef3b8b50c7 100644 --- a/src/common/datetime/relative_time.ts +++ b/src/common/datetime/relative_time.ts @@ -38,13 +38,11 @@ export default function relativeTime( roundedDelta = Math.round(delta); } - const timeDesc = localize( - `ui.components.relative_time.duration.${unit}`, + return localize( + options.includeTense === false + ? `ui.components.relative_time.duration.${unit}` + : `ui.components.relative_time.${tense}_duration.${unit}`, "count", roundedDelta ); - - return options.includeTense === false - ? timeDesc - : localize(`ui.components.relative_time.${tense}`, "time", timeDesc); } diff --git a/src/common/dom/dynamic-element-directive.ts b/src/common/dom/dynamic-element-directive.ts index 8f05f437ef..fe863e48b8 100644 --- a/src/common/dom/dynamic-element-directive.ts +++ b/src/common/dom/dynamic-element-directive.ts @@ -10,10 +10,7 @@ export const dynamicElement = directive( let element = part.value as HTMLElement | undefined; - if ( - element !== undefined && - tag.toUpperCase() === (element as HTMLElement).tagName - ) { + if (tag === element?.localName) { if (properties) { Object.entries(properties).forEach(([key, value]) => { element![key] = value; diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts index 1e0c5c5c7a..1ae025a2df 100644 --- a/src/common/entity/binary_sensor_icon.ts +++ b/src/common/entity/binary_sensor_icon.ts @@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => { case "problem": case "safety": case "smoke": - return is_off ? "hass:shield-check" : "hass:alert"; + return is_off ? "hass:check-circle" : "hass:alert-circle"; case "heat": return is_off ? "hass:thermometer" : "hass:fire"; case "light": diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index 1d52dbcde6..97a52b1864 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -51,21 +51,17 @@ class SearchInput extends LitElement { @value-changed=${this._filterInputChanged} .noLabelFloat=${this.noLabelFloat} > - + + + ${this.filter && html` - + `} diff --git a/src/common/string/filter/char-code.ts b/src/common/string/filter/char-code.ts new file mode 100644 index 0000000000..faa7210898 --- /dev/null +++ b/src/common/string/filter/char-code.ts @@ -0,0 +1,244 @@ +// MIT License + +// Copyright (c) 2015 - present Microsoft Corporation + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, +} diff --git a/src/common/string/filter/filter.ts b/src/common/string/filter/filter.ts new file mode 100644 index 0000000000..d3471433ab --- /dev/null +++ b/src/common/string/filter/filter.ts @@ -0,0 +1,463 @@ +/* eslint-disable no-console */ +// MIT License + +// Copyright (c) 2015 - present Microsoft Corporation + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { CharCode } from "./char-code"; + +const _debug = false; + +export interface Match { + start: number; + end: number; +} + +const _maxLen = 128; + +function initTable() { + const table: number[][] = []; + const row: number[] = [0]; + for (let i = 1; i <= _maxLen; i++) { + row.push(-i); + } + for (let i = 0; i <= _maxLen; i++) { + const thisRow = row.slice(0); + thisRow[0] = -i; + table.push(thisRow); + } + return table; +} + +function isSeparatorAtPos(value: string, index: number): boolean { + if (index < 0 || index >= value.length) { + return false; + } + const code = value.charCodeAt(index); + switch (code) { + case CharCode.Underline: + case CharCode.Dash: + case CharCode.Period: + case CharCode.Space: + case CharCode.Slash: + case CharCode.Backslash: + case CharCode.SingleQuote: + case CharCode.DoubleQuote: + case CharCode.Colon: + case CharCode.DollarSign: + return true; + default: + return false; + } +} + +function isWhitespaceAtPos(value: string, index: number): boolean { + if (index < 0 || index >= value.length) { + return false; + } + const code = value.charCodeAt(index); + switch (code) { + case CharCode.Space: + case CharCode.Tab: + return true; + default: + return false; + } +} + +function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean { + return word[pos] !== wordLow[pos]; +} + +function isPatternInWord( + patternLow: string, + patternPos: number, + patternLen: number, + wordLow: string, + wordPos: number, + wordLen: number +): boolean { + while (patternPos < patternLen && wordPos < wordLen) { + if (patternLow[patternPos] === wordLow[wordPos]) { + patternPos += 1; + } + wordPos += 1; + } + return patternPos === patternLen; // pattern must be exhausted +} + +enum Arrow { + Top = 0b1, + Diag = 0b10, + Left = 0b100, +} + +/** + * A tuple of three values. + * 0. the score + * 1. the matches encoded as bitmask (2^53) + * 2. the offset at which matching started + */ +export type FuzzyScore = [number, number, number]; + +interface FilterGlobals { + _matchesCount: number; + _topMatch2: number; + _topScore: number; + _wordStart: number; + _firstMatchCanBeWeak: boolean; + _table: number[][]; + _scores: number[][]; + _arrows: Arrow[][]; +} + +function initGlobals(): FilterGlobals { + return { + _matchesCount: 0, + _topMatch2: 0, + _topScore: 0, + _wordStart: 0, + _firstMatchCanBeWeak: false, + _table: initTable(), + _scores: initTable(), + _arrows: initTable(), + }; +} + +export function fuzzyScore( + pattern: string, + patternLow: string, + patternStart: number, + word: string, + wordLow: string, + wordStart: number, + firstMatchCanBeWeak: boolean +): FuzzyScore | undefined { + const globals = initGlobals(); + const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length; + const wordLen = word.length > _maxLen ? _maxLen : word.length; + + if ( + patternStart >= patternLen || + wordStart >= wordLen || + patternLen - patternStart > wordLen - wordStart + ) { + return undefined; + } + + // Run a simple check if the characters of pattern occur + // (in order) at all in word. If that isn't the case we + // stop because no match will be possible + if ( + !isPatternInWord( + patternLow, + patternStart, + patternLen, + wordLow, + wordStart, + wordLen + ) + ) { + return undefined; + } + + let row = 1; + let column = 1; + let patternPos = patternStart; + let wordPos = wordStart; + + let hasStrongFirstMatch = false; + + // There will be a match, fill in tables + for ( + row = 1, patternPos = patternStart; + patternPos < patternLen; + row++, patternPos++ + ) { + for ( + column = 1, wordPos = wordStart; + wordPos < wordLen; + column++, wordPos++ + ) { + const score = _doScore( + pattern, + patternLow, + patternPos, + patternStart, + word, + wordLow, + wordPos + ); + + if (patternPos === patternStart && score > 1) { + hasStrongFirstMatch = true; + } + + globals._scores[row][column] = score; + + const diag = + globals._table[row - 1][column - 1] + (score > 1 ? 1 : score); + const top = globals._table[row - 1][column] + -1; + const left = globals._table[row][column - 1] + -1; + + if (left >= top) { + // left or diag + if (left > diag) { + globals._table[row][column] = left; + globals._arrows[row][column] = Arrow.Left; + } else if (left === diag) { + globals._table[row][column] = left; + globals._arrows[row][column] = Arrow.Left || Arrow.Diag; + } else { + globals._table[row][column] = diag; + globals._arrows[row][column] = Arrow.Diag; + } + } else if (top > diag) { + globals._table[row][column] = top; + globals._arrows[row][column] = Arrow.Top; + } else if (top === diag) { + globals._table[row][column] = top; + globals._arrows[row][column] = Arrow.Top || Arrow.Diag; + } else { + globals._table[row][column] = diag; + globals._arrows[row][column] = Arrow.Diag; + } + } + } + + if (_debug) { + printTables(pattern, patternStart, word, wordStart, globals); + } + + if (!hasStrongFirstMatch && !firstMatchCanBeWeak) { + return undefined; + } + + globals._matchesCount = 0; + globals._topScore = -100; + globals._wordStart = wordStart; + globals._firstMatchCanBeWeak = firstMatchCanBeWeak; + + _findAllMatches2( + row - 1, + column - 1, + patternLen === wordLen ? 1 : 0, + 0, + false, + globals + ); + if (globals._matchesCount === 0) { + return undefined; + } + + return [globals._topScore, globals._topMatch2, wordStart]; +} + +function _doScore( + pattern: string, + patternLow: string, + patternPos: number, + patternStart: number, + word: string, + wordLow: string, + wordPos: number +) { + if (patternLow[patternPos] !== wordLow[wordPos]) { + return -1; + } + if (wordPos === patternPos - patternStart) { + // common prefix: `foobar <-> foobaz` + // ^^^^^ + if (pattern[patternPos] === word[wordPos]) { + return 7; + } + return 5; + } + + if ( + isUpperCaseAtPos(wordPos, word, wordLow) && + (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow)) + ) { + // hitting upper-case: `foo <-> forOthers` + // ^^ ^ + if (pattern[patternPos] === word[wordPos]) { + return 7; + } + return 5; + } + + if ( + isSeparatorAtPos(wordLow, wordPos) && + (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1)) + ) { + // hitting a separator: `. <-> foo.bar` + // ^ + return 5; + } + + if ( + isSeparatorAtPos(wordLow, wordPos - 1) || + isWhitespaceAtPos(wordLow, wordPos - 1) + ) { + // post separator: `foo <-> bar_foo` + // ^^^ + return 5; + } + return 1; +} + +function printTable( + table: number[][], + pattern: string, + patternLen: number, + word: string, + wordLen: number +): string { + function pad(s: string, n: number, _pad = " ") { + while (s.length < n) { + s = _pad + s; + } + return s; + } + let ret = ` | |${word + .split("") + .map((c) => pad(c, 3)) + .join("|")}\n`; + + for (let i = 0; i <= patternLen; i++) { + if (i === 0) { + ret += " |"; + } else { + ret += `${pattern[i - 1]}|`; + } + ret += + table[i] + .slice(0, wordLen + 1) + .map((n) => pad(n.toString(), 3)) + .join("|") + "\n"; + } + return ret; +} + +function printTables( + pattern: string, + patternStart: number, + word: string, + wordStart: number, + globals: FilterGlobals +): void { + pattern = pattern.substr(patternStart); + word = word.substr(wordStart); + console.log( + printTable(globals._table, pattern, pattern.length, word, word.length) + ); + console.log( + printTable(globals._arrows, pattern, pattern.length, word, word.length) + ); + console.log( + printTable(globals._scores, pattern, pattern.length, word, word.length) + ); +} + +function _findAllMatches2( + row: number, + column: number, + total: number, + matches: number, + lastMatched: boolean, + globals: FilterGlobals +): void { + if (globals._matchesCount >= 10 || total < -25) { + // stop when having already 10 results, or + // when a potential alignment as already 5 gaps + return; + } + + let simpleMatchCount = 0; + + while (row > 0 && column > 0) { + const score = globals._scores[row][column]; + const arrow = globals._arrows[row][column]; + + if (arrow === Arrow.Left) { + // left -> no match, skip a word character + column -= 1; + if (lastMatched) { + total -= 5; // new gap penalty + } else if (matches !== 0) { + total -= 1; // gap penalty after first match + } + lastMatched = false; + simpleMatchCount = 0; + } else if (arrow && Arrow.Diag) { + if (arrow && Arrow.Left) { + // left + _findAllMatches2( + row, + column - 1, + matches !== 0 ? total - 1 : total, // gap penalty after first match + matches, + lastMatched, + globals + ); + } + + // diag + total += score; + row -= 1; + column -= 1; + lastMatched = true; + + // match -> set a 1 at the word pos + matches += 2 ** (column + globals._wordStart); + + // count simple matches and boost a row of + // simple matches when they yield in a + // strong match. + if (score === 1) { + simpleMatchCount += 1; + + if (row === 0 && !globals._firstMatchCanBeWeak) { + // when the first match is a weak + // match we discard it + return; + } + } else { + // boost + total += 1 + simpleMatchCount * (score - 1); + simpleMatchCount = 0; + } + } else { + return; + } + } + + total -= column >= 3 ? 9 : column * 3; // late start penalty + + // dynamically keep track of the current top score + // and insert the current best score at head, the rest at tail + globals._matchesCount += 1; + if (total > globals._topScore) { + globals._topScore = total; + globals._topMatch2 = matches; + } +} + +// #endregion diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts new file mode 100644 index 0000000000..f8ea1f40cf --- /dev/null +++ b/src/common/string/filter/sequence-matching.ts @@ -0,0 +1,66 @@ +import { fuzzyScore } from "./filter"; + +/** + * Determine whether a sequence of letters exists in another string, + * in that order, allowing for skipping. Ex: "chdr" exists in "chandelier") + * + * @param {string} filter - Sequence of letters to check for + * @param {string} word - Word to check for sequence + * + * @return {number} Score representing how well the word matches the filter. Return of 0 means no match. + */ + +export const fuzzySequentialMatch = (filter: string, ...words: string[]) => { + let topScore = 0; + + for (const word of words) { + const scores = fuzzyScore( + filter, + filter.toLowerCase(), + 0, + word, + word.toLowerCase(), + 0, + true + ); + + if (!scores) { + continue; + } + + // The VS Code implementation of filter treats a score of "0" as just barely a match + // But we will typically use this matcher in a .filter(), which interprets 0 as a failure. + // By shifting all scores up by 1, we allow "0" matches, while retaining score precedence + const score = scores[0] + 1; + + if (score > topScore) { + topScore = score; + } + } + return topScore; +}; + +export interface ScorableTextItem { + score?: number; + text: string; + altText?: string; +} + +type FuzzyFilterSort = ( + filter: string, + items: T[] +) => T[]; + +export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { + return items + .map((item) => { + item.score = item.altText + ? fuzzySequentialMatch(filter, item.text, item.altText) + : fuzzySequentialMatch(filter, item.text); + return item; + }) + .filter((item) => item.score === undefined || item.score > 0) + .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => + scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 + ); +}; diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index 696a765872..df7b993b1d 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -1,4 +1,5 @@ import IntlMessageFormat from "intl-messageformat"; +import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill"; import { Resources } from "../../types"; export type LocalizeFunc = (key: string, ...args: any[]) => string; @@ -12,8 +13,8 @@ export interface FormatsType { time: FormatType; } -if (!Intl.PluralRules) { - import("@formatjs/intl-pluralrules/polyfill-locales"); +if (shouldPolyfill()) { + await import("@formatjs/intl-pluralrules/polyfill-locales"); } /** diff --git a/src/common/util/debounce.ts b/src/common/util/debounce.ts index fedd679e2e..557d596540 100644 --- a/src/common/util/debounce.ts +++ b/src/common/util/debounce.ts @@ -5,7 +5,7 @@ // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. // eslint-disable-next-line: ban-types -export const debounce = ( +export const debounce = unknown>( func: T, wait, immediate = false diff --git a/src/common/util/subscribe-one.ts b/src/common/util/subscribe-one.ts index f86bfb7388..51bfa1081d 100644 --- a/src/common/util/subscribe-one.ts +++ b/src/common/util/subscribe-one.ts @@ -2,7 +2,10 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; export const subscribeOne = async ( conn: Connection, - subscribe: (conn: Connection, onChange: (items: T) => void) => UnsubscribeFunc + subscribe: ( + conn2: Connection, + onChange: (items: T) => void + ) => UnsubscribeFunc ) => new Promise((resolve) => { const unsub = subscribe(conn, (items) => { diff --git a/src/common/util/throttle.ts b/src/common/util/throttle.ts index 1cd98ea188..4832a2709b 100644 --- a/src/common/util/throttle.ts +++ b/src/common/util/throttle.ts @@ -5,7 +5,7 @@ // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `false for leading`. To disable execution on the trailing edge, ditto. -export const throttle = ( +export const throttle = unknown>( func: T, wait: number, leading = true, diff --git a/src/components/buttons/ha-progress-button.ts b/src/components/buttons/ha-progress-button.ts index a446d456fc..c6c325fdab 100644 --- a/src/components/buttons/ha-progress-button.ts +++ b/src/components/buttons/ha-progress-button.ts @@ -21,7 +21,7 @@ class HaProgressButton extends LitElement { @property({ type: Boolean }) public raised = false; - @query("mwc-button") private _button?: Button; + @query("mwc-button", true) private _button?: Button; public render(): TemplateResult { return html` diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index db83db5a6d..1b8530dcec 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -73,13 +73,17 @@ export interface DataTableColumnData extends DataTableSortColumnData { hidden?: boolean; } +type ClonedDataTableColumnData = Omit & { + title?: string; +}; + export interface DataTableRowData { [key: string]: any; selectable?: boolean; } export interface SortableColumnContainer { - [key: string]: DataTableSortColumnData; + [key: string]: ClonedDataTableColumnData; } @customElement("ha-data-table") @@ -90,6 +94,8 @@ export class HaDataTable extends LitElement { @property({ type: Boolean }) public selectable = false; + @property({ type: Boolean }) public clickable = false; + @property({ type: Boolean }) public hasFab = false; @property({ type: Boolean, attribute: "auto-height" }) @@ -101,6 +107,9 @@ export class HaDataTable extends LitElement { @property({ type: String }) public searchLabel?: string; + @property({ type: Boolean, attribute: "no-label-float" }) + public noLabelFloat? = false; + @property({ type: String }) public filter = ""; @internalProperty() private _filterable = false; @@ -113,9 +122,9 @@ export class HaDataTable extends LitElement { @internalProperty() private _filteredData: DataTableRowData[] = []; - @query("slot[name='header']") private _header!: HTMLSlotElement; + @internalProperty() private _headerHeight = 0; - @query(".mdc-data-table__table") private _table!: HTMLDivElement; + @query("slot[name='header']") private _header!: HTMLSlotElement; private _checkableRowsCount?: number; @@ -166,11 +175,13 @@ export class HaDataTable extends LitElement { } const clonedColumns: DataTableColumnContainer = deepClone(this.columns); - Object.values(clonedColumns).forEach((column: DataTableColumnData) => { - delete column.title; - delete column.type; - delete column.template; - }); + Object.values(clonedColumns).forEach( + (column: ClonedDataTableColumnData) => { + delete column.title; + delete column.type; + delete column.template; + } + ); this._sortColumns = clonedColumns; } @@ -206,6 +217,7 @@ export class HaDataTable extends LitElement {
` @@ -220,7 +232,7 @@ export class HaDataTable extends LitElement { style=${styleMap({ height: this.autoHeight ? `${(this._filteredData.length || 1) * 53 + 57}px` - : `calc(100% - ${this._header?.clientHeight}px)`, + : `calc(100% - ${this._headerHeight}px)`, })} >
@@ -317,12 +329,13 @@ export class HaDataTable extends LitElement {
> => { if (!worker) { - worker = wrap(new Worker("./sort_filter_worker", { type: "module" })); + worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url))); } return await worker.filterData(data, columns, filter); @@ -29,7 +29,7 @@ export const sortData = async ( sortColumn: SortDataParamTypes[3] ): Promise> => { if (!worker) { - worker = wrap(new Worker("./sort_filter_worker", { type: "module" })); + worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url))); } return await worker.sortData(data, columns, direction, sortColumn); diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index cc0e7d3f29..70211da366 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -1,7 +1,7 @@ +// @ts-nocheck import Vue from "vue"; import wrap from "@vue/web-component-wrapper"; import DateRangePicker from "vue2-daterange-picker"; -// @ts-ignore import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; import { fireEvent } from "../common/dom/fire_event"; import { Constructor } from "../types"; @@ -35,7 +35,6 @@ const Component = Vue.extend({ }, }, render(createElement) { - // @ts-ignore return createElement(DateRangePicker, { props: { "time-picker": true, @@ -52,7 +51,6 @@ const Component = Vue.extend({ endDate: this.endDate, }, callback: (value) => { - // @ts-ignore fireEvent(this.$el as HTMLElement, "change", value); }, expression: "dateRange", diff --git a/src/components/device/ha-device-action-picker.ts b/src/components/device/ha-device-action-picker.ts index eaca169438..e8e50f5b47 100644 --- a/src/components/device/ha-device-action-picker.ts +++ b/src/components/device/ha-device-action-picker.ts @@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; @customElement("ha-device-action-picker") class HaDeviceActionPicker extends HaDeviceAutomationPicker { - protected NO_AUTOMATION_TEXT = "No actions"; + protected get NO_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.actions.no_actions" + ); + } - protected UNKNOWN_AUTOMATION_TEXT = "Unknown action"; + protected get UNKNOWN_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.actions.unknown_action" + ); + } constructor() { super( diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index 540cfc5c9d..fe56884a09 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -33,16 +33,24 @@ export abstract class HaDeviceAutomationPicker< @property() public value?: T; - protected NO_AUTOMATION_TEXT = "No automations"; - - protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation"; - @internalProperty() private _automations: T[] = []; // Trigger an empty render so we start with a clean DOM. // paper-listbox does not like changing things around. @internalProperty() private _renderEmpty = false; + protected get NO_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.actions.no_actions" + ); + } + + protected get UNKNOWN_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.actions.unknown_action" + ); + } + private _localizeDeviceAutomation: ( hass: HomeAssistant, automation: T diff --git a/src/components/device/ha-device-condition-picker.ts b/src/components/device/ha-device-condition-picker.ts index dbe95c8f6a..89fefbafaf 100644 --- a/src/components/device/ha-device-condition-picker.ts +++ b/src/components/device/ha-device-condition-picker.ts @@ -11,9 +11,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; class HaDeviceConditionPicker extends HaDeviceAutomationPicker< DeviceCondition > { - protected NO_AUTOMATION_TEXT = "No conditions"; + protected get NO_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.conditions.no_conditions" + ); + } - protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition"; + protected get UNKNOWN_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.conditions.unknown_condition" + ); + } constructor() { super( diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts index ec140426ae..d1ff66d37e 100644 --- a/src/components/device/ha-device-trigger-picker.ts +++ b/src/components/device/ha-device-trigger-picker.ts @@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; @customElement("ha-device-trigger-picker") class HaDeviceTriggerPicker extends HaDeviceAutomationPicker { - protected NO_AUTOMATION_TEXT = "No triggers"; + protected get NO_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.triggers.no_triggers" + ); + } - protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger"; + protected get UNKNOWN_AUTOMATION_TEXT() { + return this.hass.localize( + "ui.panel.config.devices.automation.triggers.unknown_trigger" + ); + } constructor() { super( diff --git a/src/components/entity/ha-chart-base.js b/src/components/entity/ha-chart-base.js index 7df570747c..e02470bd08 100644 --- a/src/components/entity/ha-chart-base.js +++ b/src/components/entity/ha-chart-base.js @@ -71,14 +71,24 @@ class HaChartBase extends mixinBehaviors( margin: 5px 0 0 0; width: 100%; } + .chartTooltip ul { + margin: 0 3px; + } .chartTooltip li { display: block; white-space: pre-line; } + .chartTooltip li::first-line { + line-height: 0; + } .chartTooltip .title { text-align: center; font-weight: 500; } + .chartTooltip .beforeBody { + text-align: center; + font-weight: 300; + } .chartLegend li { display: inline-block; padding: 0 6px; @@ -133,6 +143,9 @@ class HaChartBase extends mixinBehaviors( style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px" >
[[tooltip.title]]
+
    +