diff --git a/README.md b/README.md index c5325209a5..ab25dde882 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ This is the repository for the official [Home Assistant](https://home-assistant. - [View demo of Home Assistant](https://demo.home-assistant.io/) - [More information about Home Assistant](https://home-assistant.io) -- [Frontend development instructions](https://developers.home-assistant.io/docs/en/frontend_index.html) +- [Frontend development instructions](https://developers.home-assistant.io/docs/frontend/development/) ## Development - Initial setup: `script/setup` -- Development: [Instructions](https://developers.home-assistant.io/docs/en/frontend_development.html) +- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/) - Production build: `script/build_frontend` - Gallery: `cd gallery && script/develop_gallery` - Hass.io: [Instructions](https://developers.home-assistant.io/docs/en/hassio_hass.html) diff --git a/package.json b/package.json index 193aefcd0f..4252ee18a7 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@polymer/app-layout": "^3.0.2", "@polymer/app-route": "^3.0.2", "@polymer/app-storage": "^3.0.2", - "@polymer/font-roboto": "^3.0.2", "@polymer/iron-autogrow-textarea": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", @@ -52,15 +51,12 @@ "@polymer/iron-label": "^3.0.1", "@polymer/iron-media-query": "^3.0.1", "@polymer/iron-overlay-behavior": "^3.0.2", - "@polymer/iron-pages": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1", - "@polymer/neon-animation": "^3.0.1", "@polymer/paper-card": "^3.0.1", "@polymer/paper-checkbox": "^3.1.0", "@polymer/paper-dialog": "^3.0.1", "@polymer/paper-dialog-behavior": "^3.0.1", "@polymer/paper-dialog-scrollable": "^3.0.1", - "@polymer/paper-drawer-panel": "^3.0.1", "@polymer/paper-dropdown-menu": "^3.0.1", "@polymer/paper-input": "^3.0.1", "@polymer/paper-item": "^3.0.1", @@ -70,7 +66,6 @@ "@polymer/paper-radio-button": "^3.0.1", "@polymer/paper-radio-group": "^3.0.1", "@polymer/paper-ripple": "^3.0.1", - "@polymer/paper-scroll-header-panel": "^3.0.1", "@polymer/paper-slider": "^3.0.1", "@polymer/paper-spinner": "^3.0.2", "@polymer/paper-styles": "^3.0.1", @@ -81,7 +76,6 @@ "@thomasloven/round-slider": "0.4.1", "@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-date-picker": "^4.0.7", - "@webcomponents/shadycss": "^1.9.0", "@webcomponents/webcomponentsjs": "^2.2.7", "chart.js": "~2.8.0", "chartjs-chart-timeline": "^0.3.0", @@ -107,14 +101,12 @@ "marked": "^0.6.1", "mdn-polyfills": "^5.16.0", "memoize-one": "^5.0.2", - "moment": "^2.24.0", "node-vibrant": "^3.1.5", "proxy-polyfill": "^0.3.1", "regenerator-runtime": "^0.13.2", "resize-observer": "^1.0.0", "roboto-fontface": "^0.10.0", "superstruct": "^0.6.1", - "tslib": "^1.10.0", "unfetch": "^4.1.0", "web-animations-js": "^2.3.2", "workbox-core": "^5.1.3", @@ -149,7 +141,6 @@ "@typescript-eslint/parser": "^2.28.0", "babel-loader": "^8.1.0", "chai": "^4.2.0", - "copy-webpack-plugin": "^5.0.2", "del": "^4.0.0", "eslint": "^6.8.0", "eslint-config-airbnb-typescript": "^7.2.1", @@ -160,16 +151,16 @@ "eslint-plugin-lit": "^1.2.0", "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-wc": "^1.2.0", + "fancy-log": "^1.3.3", "fs-extra": "^7.0.1", "gulp": "^4.0.0", "gulp-foreach": "^0.1.0", - "gulp-insert": "^0.5.0", "gulp-json-transform": "^0.4.6", "gulp-jsonminify": "^1.1.0", "gulp-merge-json": "^1.3.1", "gulp-rename": "^2.0.0", "gulp-zopfli-green": "^3.0.1", - "html-webpack-plugin": "^3.2.0", + "html-minifier": "^4.0.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", "lit-analyzer": "^1.1.10", @@ -178,10 +169,8 @@ "merge-stream": "^1.0.1", "mocha": "^6.0.2", "object-hash": "^2.0.3", - "parse5": "^5.1.0", "prettier": "^2.0.4", "raw-loader": "^2.0.0", - "reify": "^0.18.1", "require-dir": "^1.2.0", "sinon": "^7.3.1", "source-map-url": "^0.4.0", diff --git a/setup.py b/setup.py index 2b0dd2baf3..eb4d8d3d99 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200518.0", + version="20200519.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index cf8407ed66..015f54130e 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -21,7 +21,8 @@ import { nextRender } from "../../common/util/render-status"; import "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox"; import "../ha-icon"; -import { filterSortData } from "./sort-filter"; +import { filterData, sortData } from "./sort-filter"; +import memoizeOne from "memoize-one"; declare global { // for fire event @@ -72,6 +73,10 @@ export interface DataTableRowData { selectable?: boolean; } +export interface SortableColumnContainer { + [key: string]: DataTableSortColumnData; +} + @customElement("ha-data-table") export class HaDataTable extends LitElement { @property({ type: Object }) public columns: DataTableColumnContainer = {}; @@ -109,9 +114,7 @@ export class HaDataTable extends LitElement { private _checkedRows: string[] = []; - private _sortColumns: { - [key: string]: DataTableSortColumnData; - } = {}; + private _sortColumns: SortableColumnContainer = {}; private curRequest = 0; @@ -179,7 +182,7 @@ export class HaDataTable extends LitElement { properties.has("_sortColumn") || properties.has("_sortDirection") ) { - this._filterData(); + this._sortFilterData(); } } @@ -369,20 +372,30 @@ export class HaDataTable extends LitElement { `; } - private async _filterData() { + private async _sortFilterData() { const startTime = new Date().getTime(); this.curRequest++; const curRequest = this.curRequest; - const filterProm = filterSortData( - this.data, - this._sortColumns, - this._filter, - this._sortDirection, - this._sortColumn - ); + let filteredData = this.data; + if (this._filter) { + filteredData = await this._memFilterData( + this.data, + this._sortColumns, + this.filter + ); + } - const [data] = await Promise.all([filterProm, nextRender]); + const prom = this._sortColumn + ? sortData( + filteredData, + this._sortColumns, + this._sortDirection, + this._sortColumn + ) + : filteredData; + + const [data] = await Promise.all([prom, nextRender]); const curTime = new Date().getTime(); const elapsed = curTime - startTime; @@ -396,6 +409,16 @@ export class HaDataTable extends LitElement { this._filteredData = data; } + private _memFilterData = memoizeOne( + async ( + data: DataTableRowData[], + columns: SortableColumnContainer, + filter: string + ): Promise => { + return filterData(data, columns, filter); + } + ); + private _handleHeaderClick(ev: Event) { const columnId = ((ev.target as HTMLElement).closest( ".mdc-data-table__header-cell" diff --git a/src/components/data-table/sort-filter.ts b/src/components/data-table/sort-filter.ts index 766a167fba..9e89e866df 100644 --- a/src/components/data-table/sort-filter.ts +++ b/src/components/data-table/sort-filter.ts @@ -1,26 +1,34 @@ import { wrap } from "comlink"; -type FilterSortDataType = typeof import("./sort_filter_worker").api["filterSortData"]; -type filterSortDataParamTypes = Parameters; +type FilterDataType = typeof import("./sort_filter_worker").api["filterData"]; +type FilterDataParamTypes = Parameters; + +type SortDataType = typeof import("./sort_filter_worker").api["sortData"]; +type SortDataParamTypes = Parameters; let worker: any | undefined; -export const filterSortData = async ( - data: filterSortDataParamTypes[0], - columns: filterSortDataParamTypes[1], - filter: filterSortDataParamTypes[2], - direction: filterSortDataParamTypes[3], - sortColumn: filterSortDataParamTypes[4] -): Promise> => { +export const filterData = async ( + data: FilterDataParamTypes[0], + columns: FilterDataParamTypes[1], + filter: FilterDataParamTypes[2] +): Promise> => { if (!worker) { worker = wrap(new Worker("./sort_filter_worker", { type: "module" })); } - return await worker.filterSortData( - data, - columns, - filter, - direction, - sortColumn - ); + return await worker.filterData(data, columns, filter); +}; + +export const sortData = async ( + data: SortDataParamTypes[0], + columns: SortDataParamTypes[1], + direction: SortDataParamTypes[2], + sortColumn: SortDataParamTypes[3] +): Promise> => { + if (!worker) { + worker = wrap(new Worker("./sort_filter_worker", { type: "module" })); + } + + return await worker.sortData(data, columns, direction, sortColumn); }; diff --git a/src/components/data-table/sort_filter_worker.ts b/src/components/data-table/sort_filter_worker.ts index f7b71bdd2c..de3e1c67b2 100644 --- a/src/components/data-table/sort_filter_worker.ts +++ b/src/components/data-table/sort_filter_worker.ts @@ -5,33 +5,16 @@ import type { DataTableSortColumnData, DataTableRowData, SortingDirection, - HaDataTable, + SortableColumnContainer, } from "./ha-data-table"; -type SortableColumnContainer = HaDataTable["_sortColumns"]; - -const filterSortData = ( - data: DataTableRowData[], - columns: SortableColumnContainer, - filter: string, - direction: SortingDirection, - sortColumn?: string -) => { - const filteredData = filter ? filterData(data, columns, filter) : data; - - if (!sortColumn) { - return filteredData; - } - - return sortData(filteredData, columns, direction, sortColumn); -}; - const filterData = ( data: DataTableRowData[], columns: SortableColumnContainer, filter: string -) => - data.filter((row) => { +) => { + filter = filter.toUpperCase(); + return data.filter((row) => { return Object.entries(columns).some((columnEntry) => { const [key, column] = columnEntry; if (column.filterable) { @@ -46,6 +29,7 @@ const filterData = ( return false; }); }); +}; const sortData = ( data: DataTableRowData[], @@ -85,7 +69,8 @@ const sortData = ( // Export for types export const api = { - filterSortData, + filterData, + sortData, }; expose(api); diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 23f68b1fb6..7f1bfc57c9 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -28,6 +28,8 @@ checkCacheVersion(); const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); +const cachedIcons: { [key: string]: string } = {}; + @customElement("ha-icon") export class HaIcon extends LitElement { @property() public icon?: string; @@ -83,9 +85,15 @@ export class HaIcon extends LitElement { this._legacy = false; - const cachedPath: string = await getIcon(iconName); - if (cachedPath) { - this._path = cachedPath; + if (iconName in cachedIcons) { + this._path = cachedIcons[iconName]; + return; + } + + const databaseIcon: string = await getIcon(iconName); + if (databaseIcon) { + this._path = databaseIcon; + cachedIcons[iconName] = databaseIcon; return; } const chunk = findIconChunk(iconName); @@ -111,6 +119,7 @@ export class HaIcon extends LitElement { private async _setPath(promise: Promise, iconName: string) { const iconPack = await promise; this._path = iconPack[iconName]; + cachedIcons[iconName] = iconPack[iconName]; } static get styles(): CSSResult { diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index b5ba2e6697..162a344daf 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -53,7 +53,7 @@ class HaMarkdown extends UpdatingElement { node.rel = "noreferrer noopener"; // Fire a resize event when images loaded to notify content resized - } else if (node) { + } else if (node instanceof HTMLImageElement) { node.addEventListener("load", this._resize); } } diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 33b293f1d2..b82d7e4dd0 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -72,7 +72,11 @@ export class HaDeviceEntitiesCard extends LitElement { class="show-more" @click=${this._toggleShowDisabled} > - +${disabledEntities.length} disabled entities + ${this.hass.localize( + "ui.panel.config.devices.entities.disabled_entities", + "count", + disabledEntities.length + )} ` : html` @@ -83,7 +87,9 @@ export class HaDeviceEntitiesCard extends LitElement { class="show-more" @click=${this._toggleShowDisabled} > - Hide disabled + ${this.hass.localize( + "ui.panel.config.devices.entities.hide_disabled" + )} ` : ""} diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index 787b8e2556..475e3822a7 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -1,3 +1,6 @@ +import "@material/mwc-fab"; +import "@material/mwc-icon-button"; +import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-listbox/paper-listbox"; @@ -20,7 +23,7 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain" import { navigate } from "../../../common/navigate"; import { compare } from "../../../common/string/compare"; import "../../../components/ha-card"; -import "@material/mwc-fab"; +import "../../../components/ha-svg-icon"; import "../../../components/map/ha-locations-editor"; import type { HaLocationsEditor, @@ -47,8 +50,6 @@ import type { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import { showZoneDetailDialog } from "./show-dialog-zone-detail"; -import "../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-zone") export class HaConfigZone extends SubscribeMixin(LitElement) { @@ -148,17 +149,18 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { @click=${this._itemClicked} .entry=${entry} > - + ${entry.name} ${!this.narrow ? html` - + > + + ` : ""} @@ -176,9 +178,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { ${state.attributes.friendly_name || state.entity_id}
- + > + + ${state.entity_id === "zone.home" ? this.hass.localize( @@ -477,7 +486,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { overflow: hidden; } ha-icon, - ha-icon-button:not([disabled]) { + mwc-icon-button:not([disabled]) { color: var(--secondary-text-color); } .empty { diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js index f1d42046e1..76fecb5d97 100644 --- a/src/panels/developer-tools/state/developer-tools-state.js +++ b/src/panels/developer-tools/state/developer-tools-state.js @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import "@material/mwc-icon-button"; import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-input/paper-input"; import { html } from "@polymer/polymer/lib/utils/html-tag"; @@ -6,11 +7,13 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { safeDump, safeLoad } from "js-yaml"; import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-svg-icon"; import "../../../components/ha-code-editor"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "../../../resources/ha-style"; +import { mdiInformationOutline } from "@mdi/js"; const ERROR_SENTINEL = {}; /* @@ -56,8 +59,9 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { .entities td { padding: 4px; } - .entities ha-icon-button { + .entities mwc-icon-button { --mdc-icon-button-size: 24px; + --mdc-icon-size: 20px; } .entities td:nth-child(3) { white-space: pre-wrap; @@ -149,13 +153,12 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {