diff --git a/src/components/ha-related-items.ts b/src/components/ha-related-items.ts index 6077f0c284..3a4e59a3ea 100644 --- a/src/components/ha-related-items.ts +++ b/src/components/ha-related-items.ts @@ -1,3 +1,4 @@ +/* eslint-disable lit/no-invalid-html */ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -95,7 +96,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { )}: ${this.hass.localize(`component.${entry.domain}.title`)}: diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 9d5388c5bf..527838ff3b 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -10,6 +10,10 @@ export interface ConfigEntry { supports_options: boolean; } +export interface ConfigEntryMutableParams { + title: string; +} + export interface ConfigEntrySystemOptions { disable_new_entities: boolean; } @@ -17,6 +21,17 @@ export interface ConfigEntrySystemOptions { export const getConfigEntries = (hass: HomeAssistant) => hass.callApi("GET", "config/config_entries/entry"); +export const updateConfigEntry = ( + hass: HomeAssistant, + configEntryId: string, + updatedValues: Partial +) => + hass.callWS({ + type: "config_entries/update", + entry_id: configEntryId, + ...updatedValues, + }); + export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) => hass.callApi<{ require_restart: boolean; diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index d761ec0e6c..add5bc19d3 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -17,6 +17,9 @@ import type { import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; +import "@material/mwc-button/mwc-button"; +import { navigate } from "../common/navigate"; +import "@polymer/paper-tooltip/paper-tooltip"; @customElement("hass-tabs-subpage-data-table") export class HaTabsSubpageDataTable extends LitElement { @@ -62,6 +65,12 @@ export class HaTabsSubpageDataTable extends LitElement { */ @property({ type: String }) public filter = ""; + /** + * List of strings that show what the data is currently filtered by. + * @type {Array} + */ + @property({ type: Array }) public activeFilters?; + /** * What path to use when the back button is pressed. * @type {String} @@ -118,6 +127,24 @@ export class HaTabsSubpageDataTable extends LitElement { no-underline @value-changed=${this._handleSearchChange} > + ${this.activeFilters + ? html`
+
+ + + ${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} + ${this.activeFilters.join(", ")} + +
+ ${this.hass.localize( + "ui.panel.config.filtering.clear" + )} +
` + : ""} @@ -143,8 +170,24 @@ export class HaTabsSubpageDataTable extends LitElement { no-label-float no-underline @value-changed=${this._handleSearchChange} - > + > + + ${this.activeFilters + ? html`
+ ${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} + ${this.activeFilters.join(", ")} + ${this.hass.localize( + "ui.panel.config.filtering.clear" + )} +
` + : ""} + ` : html`
`} @@ -157,6 +200,10 @@ export class HaTabsSubpageDataTable extends LitElement { this.filter = ev.detail.value; } + private _clearFilter() { + navigate(this, window.location.pathname); + } + static get styles(): CSSResult { return css` ha-data-table { @@ -171,19 +218,54 @@ export class HaTabsSubpageDataTable extends LitElement { .table-header { border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); padding: 0 16px; + display: flex; + align-items: center; } .search-toolbar { + display: flex; + align-items: center; color: var(--secondary-text-color); padding: 0 16px; } search-input { position: relative; top: 2px; + flex-grow: 1; } search-input.header { left: -8px; top: -7px; } + .active-filters { + color: var(--primary-text-color); + position: relative; + display: flex; + align-items: center; + padding: 2px 2px 2px 8px; + margin-left: 4px; + font-size: 14px; + } + .active-filters ha-icon { + color: var(--primary-color); + } + .active-filters mwc-button { + margin-left: 8px; + } + .active-filters::before { + background-color: var(--primary-color); + opacity: 0.12; + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ""; + } + .search-toolbar .active-filters { + top: -8px; + right: -16px; + } `; } } diff --git a/src/panels/config/areas/ha-config-areas.ts b/src/panels/config/areas/ha-config-areas.ts index 2e1fbcaa09..3e4bf1c0f4 100644 --- a/src/panels/config/areas/ha-config-areas.ts +++ b/src/panels/config/areas/ha-config-areas.ts @@ -68,13 +68,6 @@ class HaConfigAreas extends HassRouterPage { } } - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - this.addEventListener("hass-reload-entries", () => { - this._loadData(); - }); - } - protected updated(changedProps: PropertyValues) { super.updated(changedProps); if (!this._unsubs && changedProps.has("hass")) { diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index a89a036a77..a14c7c3d13 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -26,9 +26,17 @@ import { findBatteryEntity, } from "../../../data/entity_registry"; import "../../../layouts/hass-tabs-subpage-data-table"; +import "../../../components/entity/ha-state-icon"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; -import { DeviceRowData } from "./ha-devices-data-table"; +import { domainToName } from "../../../data/integration"; + +interface DeviceRowData extends DeviceRegistryEntry { + device?: DeviceRowData; + area?: string; + integration?: string; + battery_entity?: string; +} @customElement("ha-config-devices-dashboard") export class HaConfigDeviceDashboard extends LitElement { @@ -46,17 +54,53 @@ export class HaConfigDeviceDashboard extends LitElement { @property() public areas!: AreaRegistryEntry[]; - @property() public domain!: string; - @property() public route!: Route; + @property() private _searchParms = new URLSearchParams( + window.location.search + ); + + private _activeFilters = memoizeOne( + ( + entries: ConfigEntry[], + filters: URLSearchParams, + localize: LocalizeFunc + ): string[] | undefined => { + const filterTexts: string[] = []; + filters.forEach((value, key) => { + switch (key) { + case "config_entry": { + const configEntry = entries.find( + (entry) => entry.entry_id === value + ); + if (!configEntry) { + break; + } + const integrationName = domainToName(localize, configEntry.domain); + filterTexts.push( + `${this.hass.localize( + "ui.panel.config.integrations.integration" + )} ${integrationName}${ + integrationName !== configEntry.title + ? `: ${configEntry.title}` + : "" + }` + ); + break; + } + } + }); + return filterTexts.length ? filterTexts : undefined; + } + ); + private _devices = memoizeOne( ( devices: DeviceRegistryEntry[], entries: ConfigEntry[], entities: EntityRegistryEntry[], areas: AreaRegistryEntry[], - domain: string, + filters: URLSearchParams, localize: LocalizeFunc ) => { // Some older installations might have devices pointing at invalid entryIDs @@ -90,14 +134,15 @@ export class HaConfigDeviceDashboard extends LitElement { areaLookup[area.area_id] = area; } - if (domain) { - outputDevices = outputDevices.filter((device) => - device.config_entries.find( - (entryId) => - entryId in entryLookup && entryLookup[entryId].domain === domain - ) - ); - } + filters.forEach((value, key) => { + switch (key) { + case "config_entry": + outputDevices = outputDevices.filter((device) => + device.config_entries.includes(value) + ); + break; + } + }); outputDevices = outputDevices.map((device) => { return { @@ -238,12 +283,24 @@ export class HaConfigDeviceDashboard extends LitElement { } ); + public constructor() { + super(); + window.addEventListener("location-changed", () => { + this._searchParms = new URLSearchParams(window.location.search); + }); + window.addEventListener("popstate", () => { + this._searchParms = new URLSearchParams(window.location.search); + }); + } + protected render(): TemplateResult { return html` { - this._configEntries = configEntries.sort((conf1, conf2) => - compare(conf1.title, conf2.title) - ); + this._configEntries = configEntries; }); if (this._unsubs) { return; diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts deleted file mode 100644 index cbe8cd7e69..0000000000 --- a/src/panels/config/devices/ha-devices-data-table.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { - customElement, - html, - LitElement, - property, - TemplateResult, -} from "lit-element"; -import memoizeOne from "memoize-one"; -import { navigate } from "../../../common/navigate"; -import { LocalizeFunc } from "../../../common/translations/localize"; -import "../../../components/data-table/ha-data-table"; -import type { - DataTableColumnContainer, - DataTableRowData, - RowClickedEvent, -} from "../../../components/data-table/ha-data-table"; -import "../../../components/entity/ha-state-icon"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; -import type { ConfigEntry } from "../../../data/config_entries"; -import { - computeDeviceName, - DeviceEntityLookup, - DeviceRegistryEntry, -} from "../../../data/device_registry"; -import { - EntityRegistryEntry, - findBatteryEntity, -} from "../../../data/entity_registry"; -import type { HomeAssistant } from "../../../types"; - -export interface DeviceRowData extends DeviceRegistryEntry { - device?: DeviceRowData; - area?: string; - integration?: string; - battery_entity?: string; -} - -@customElement("ha-devices-data-table") -export class HaDevicesDataTable extends LitElement { - @property() public hass!: HomeAssistant; - - @property() public narrow = false; - - @property() public devices!: DeviceRegistryEntry[]; - - @property() public entries!: ConfigEntry[]; - - @property() public entities!: EntityRegistryEntry[]; - - @property() public areas!: AreaRegistryEntry[]; - - @property() public domain!: string; - - private _devices = memoizeOne( - ( - devices: DeviceRegistryEntry[], - entries: ConfigEntry[], - entities: EntityRegistryEntry[], - areas: AreaRegistryEntry[], - domain: string, - localize: LocalizeFunc - ) => { - // Some older installations might have devices pointing at invalid entryIDs - // So we guard for that. - - let outputDevices: DeviceRowData[] = devices; - - const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; - for (const device of devices) { - deviceLookup[device.id] = device; - } - - const deviceEntityLookup: DeviceEntityLookup = {}; - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - - const entryLookup: { [entryId: string]: ConfigEntry } = {}; - for (const entry of entries) { - entryLookup[entry.entry_id] = entry; - } - - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - - if (domain) { - outputDevices = outputDevices.filter((device) => - device.config_entries.find( - (entryId) => - entryId in entryLookup && entryLookup[entryId].domain === domain - ) - ); - } - - outputDevices = outputDevices.map((device) => { - return { - ...device, - name: computeDeviceName( - device, - this.hass, - deviceEntityLookup[device.id] - ), - model: device.model || "", - manufacturer: device.manufacturer || "", - area: device.area_id ? areaLookup[device.area_id].name : "No area", - integration: device.config_entries.length - ? device.config_entries - .filter((entId) => entId in entryLookup) - .map( - (entId) => - localize(`component.${entryLookup[entId].domain}.title`) || - entryLookup[entId].domain - ) - .join(", ") - : "No integration", - battery_entity: this._batteryEntity(device.id, deviceEntityLookup), - }; - }); - - return outputDevices; - } - ); - - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => - narrow - ? { - name: { - title: "Device", - sortable: true, - filterable: true, - direction: "asc", - grows: true, - template: (name, device: DataTableRowData) => { - const battery = device.battery_entity - ? this.hass.states[device.battery_entity] - : undefined; - // Have to work on a nice layout for mobile - return html` - ${name}
- ${device.area} | ${device.integration}
- ${battery && !isNaN(battery.state as any) - ? html` - ${battery.state}% - - ` - : ""} - `; - }, - }, - } - : { - name: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.device" - ), - sortable: true, - filterable: true, - direction: "asc", - grows: true, - }, - manufacturer: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.manufacturer" - ), - sortable: true, - filterable: true, - width: "15%", - }, - model: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.model" - ), - sortable: true, - filterable: true, - width: "15%", - }, - area: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.area" - ), - sortable: true, - filterable: true, - width: "15%", - }, - integration: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.integration" - ), - sortable: true, - filterable: true, - width: "15%", - }, - battery_entity: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.battery" - ), - sortable: true, - type: "numeric", - width: "15%", - maxWidth: "90px", - template: (batteryEntity: string) => { - const battery = batteryEntity - ? this.hass.states[batteryEntity] - : undefined; - return battery && !isNaN(battery.state as any) - ? html` - ${battery.state}% - - ` - : html` - `; - }, - }, - } - ); - - protected render(): TemplateResult { - return html` - - `; - } - - private _batteryEntity( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - const batteryEntity = findBatteryEntity( - this.hass, - deviceEntityLookup[deviceId] || [] - ); - return batteryEntity ? batteryEntity.entity_id : undefined; - } - - private _handleRowClicked(ev: CustomEvent) { - const deviceId = (ev.detail as RowClickedEvent).id; - navigate(this, `/config/devices/device/${deviceId}`); - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-devices-data-table": HaDevicesDataTable; - } -} diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 75fcdfe1f6..3ccdf57475 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -49,6 +49,10 @@ import { loadEntityEditorDialog, showEntityEditorDialog, } from "./show-dialog-entity-editor"; +import { getConfigEntries, ConfigEntry } from "../../../data/config_entries"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { domainToName } from "../../../data/integration"; +import { navigate } from "../../../common/navigate"; export interface StateEntity extends EntityRegistryEntry { readonly?: boolean; @@ -76,6 +80,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @property() private _stateEntities: StateEntity[] = []; + @property() public _entries?: ConfigEntry[]; + @property() private _showDisabled = false; @property() private _showUnavailable = true; @@ -84,6 +90,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @property() private _filter = ""; + @property() private _searchParms = new URLSearchParams( + window.location.search + ); + @property() private _selectedEntities: string[] = []; @query("hass-tabs-subpage-data-table") @@ -91,6 +101,44 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { private getDialog?: () => DialogEntityEditor | undefined; + private _activeFilters = memoize( + ( + filters: URLSearchParams, + localize: LocalizeFunc, + entries?: ConfigEntry[] + ): string[] | undefined => { + const filterTexts: string[] = []; + filters.forEach((value, key) => { + switch (key) { + case "config_entry": { + if (!entries) { + this._loadConfigEntries(); + break; + } + const configEntry = entries.find( + (entry) => entry.entry_id === value + ); + if (!configEntry) { + break; + } + const integrationName = domainToName(localize, configEntry.domain); + filterTexts.push( + `${this.hass.localize( + "ui.panel.config.integrations.integration" + )} ${integrationName}${ + integrationName !== configEntry.title + ? `: ${configEntry.title}` + : "" + }` + ); + break; + } + } + }); + return filterTexts.length ? filterTexts : undefined; + } + ); + private _columns = memoize( (narrow, _language): DataTableColumnContainer => { const columns: DataTableColumnContainer = { @@ -202,6 +250,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ( entities: EntityRegistryEntry[], stateEntities: StateEntity[], + filters: URLSearchParams, showDisabled: boolean, showUnavailable: boolean, showReadOnly: boolean @@ -212,9 +261,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { const result: EntityRow[] = []; - for (const entry of showReadOnly - ? entities.concat(stateEntities) - : entities) { + entities = showReadOnly ? entities.concat(stateEntities) : entities; + + filters.forEach((value, key) => { + switch (key) { + case "config_entry": + entities = entities.filter( + (entity) => entity.config_entry_id === value + ); + break; + } + }); + + for (const entry of entities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === "unavailable"; const restored = entity?.attributes.restored; @@ -253,6 +312,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } ); + public constructor() { + super(); + window.addEventListener("location-changed", () => { + this._searchParms = new URLSearchParams(window.location.search); + }); + window.addEventListener("popstate", () => { + this._searchParms = new URLSearchParams(window.location.search); + }); + } + public hassSubscribe(): UnsubscribeFunc[] { return [ subscribeEntityRegistry(this.hass.connection!, (entities) => { @@ -277,6 +346,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { if (!this.hass || this._entities === undefined) { return html` `; } + const activeFilters = this._activeFilters( + this._searchParms, + this.hass.localize, + this._entries + ); const headerToolbar = this._selectedEntities.length ? html`

@@ -345,7 +419,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { no-underline @value-changed=${this._handleSearchChange} .filter=${this._filter} - > + >${activeFilters + ? html`

+ ${this.narrow + ? html`
+ + + ${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} + ${activeFilters.join(", ")} + +
` + : `${this.hass.localize( + "ui.panel.config.filtering.filtering_by" + )} ${activeFilters.join(", ")}`} + ${this.hass.localize( + "ui.panel.config.filtering.clear" + )} +
` + : ""} paper-icon-button { margin: 8px; } + .active-filters { + color: var(--primary-text-color); + position: relative; + display: flex; + align-items: center; + padding: 2px 2px 2px 8px; + margin-left: 4px; + font-size: 14px; + } + .active-filters ha-icon { + color: var(--primary-color); + } + .active-filters mwc-button { + margin-left: 8px; + } + .active-filters::before { + background-color: var(--primary-color); + opacity: 0.12; + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ""; + } `; } } diff --git a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js deleted file mode 100644 index 343c801bf1..0000000000 --- a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js +++ /dev/null @@ -1,74 +0,0 @@ -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../../components/entity/state-badge"; -import "../../../../components/ha-card"; -import { computeEntityRegistryName } from "../../../../data/entity_registry"; -import "../../../../layouts/hass-subpage"; -import { EventsMixin } from "../../../../mixins/events-mixin"; -import LocalizeMixIn from "../../../../mixins/localize-mixin"; - -/* - * @appliesMixin LocalizeMixIn - * @appliesMixin EventsMixin - */ -class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - - `; - } - - static get properties() { - return { - heading: String, - entities: Array, - hass: Object, - }; - } - - _computeStateObj(entity, hass) { - return hass.states[entity.entity_id]; - } - - _computeEntityName(entity, hass) { - return ( - computeEntityRegistryName(hass, entity) || - `(${this.localize( - "ui.panel.config.integrations.config_entry.entity_unavailable" - )})` - ); - } - - _openMoreInfo(ev) { - this.fire("hass-more-info", { entityId: ev.model.entity.entity_id }); - } -} - -customElements.define("ha-ce-entities-card", HaCeEntitiesCard); diff --git a/src/panels/config/integrations/config-entry/ha-config-entry-page.ts b/src/panels/config/integrations/config-entry/ha-config-entry-page.ts deleted file mode 100755 index d02eaa66a3..0000000000 --- a/src/panels/config/integrations/config-entry/ha-config-entry-page.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { css, CSSResult, html, LitElement, property } from "lit-element"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { navigate } from "../../../../common/navigate"; -import { AreaRegistryEntry } from "../../../../data/area_registry"; -import { - ConfigEntry, - deleteConfigEntry, -} from "../../../../data/config_entries"; -import { DeviceRegistryEntry } from "../../../../data/device_registry"; -import { EntityRegistryEntry } from "../../../../data/entity_registry"; -import { showConfigEntrySystemOptionsDialog } from "../../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; -import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../../dialogs/generic/show-dialog-box"; -import "../../../../layouts/hass-error-screen"; -import "../../../../layouts/hass-subpage"; -import { HomeAssistant } from "../../../../types"; -import "../../devices/ha-devices-data-table"; -import "./ha-ce-entities-card"; - -class HaConfigEntryPage extends LitElement { - @property() public hass!: HomeAssistant; - - @property() public narrow!: boolean; - - @property() public configEntryId!: string; - - @property() public configEntries!: ConfigEntry[]; - - @property() public entityRegistryEntries!: EntityRegistryEntry[]; - - @property() public deviceRegistryEntries!: DeviceRegistryEntry[]; - - @property() public areas!: AreaRegistryEntry[]; - - private get _configEntry(): ConfigEntry | undefined { - return this.configEntries - ? this.configEntries.find( - (entry) => entry.entry_id === this.configEntryId - ) - : undefined; - } - - private _computeConfigEntryDevices = memoizeOne( - (configEntry: ConfigEntry, devices: DeviceRegistryEntry[]) => { - if (!devices) { - return []; - } - return devices.filter((device) => - device.config_entries.includes(configEntry.entry_id) - ); - } - ); - - private _computeNoDeviceEntities = memoizeOne( - (configEntry: ConfigEntry, entities: EntityRegistryEntry[]) => { - if (!entities) { - return []; - } - return entities.filter( - (ent) => !ent.device_id && ent.config_entry_id === configEntry.entry_id - ); - } - ); - - protected render() { - const configEntry = this._configEntry; - - if (!configEntry) { - return html` - - `; - } - - const configEntryDevices = this._computeConfigEntryDevices( - configEntry, - this.deviceRegistryEntries - ); - - const noDeviceEntities = this._computeNoDeviceEntities( - configEntry, - this.entityRegistryEntries - ); - - return html` - - ${configEntry.supports_options - ? html` - - ` - : ""} - - - -
- ${configEntryDevices.length === 0 && noDeviceEntities.length === 0 - ? html` -

- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.no_devices" - )} -

- ` - : html` - - `} - ${noDeviceEntities.length > 0 - ? html` - - ` - : ""} -
-
- `; - } - - private _showSettings() { - showOptionsFlowDialog(this, this._configEntry!); - } - - private _showSystemOptions() { - showConfigEntrySystemOptionsDialog(this, { - entry: this._configEntry!, - }); - } - - private _confirmRemoveEntry() { - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.delete_confirm" - ), - confirm: () => this._removeEntry(), - }); - } - - private _removeEntry() { - deleteConfigEntry(this.hass, this.configEntryId).then((result) => { - fireEvent(this, "hass-reload-entries"); - if (result.require_restart) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.restart_confirm" - ), - }); - } - navigate(this, "/config/integrations/dashboard", true); - }); - } - - static get styles(): CSSResult { - return css` - .content { - padding: 4px; - } - p { - text-align: center; - } - ha-devices-data-table { - width: 100%; - } - `; - } -} - -customElements.define("ha-config-entry-page", HaConfigEntryPage); diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.ts b/src/panels/config/integrations/ha-config-entries-dashboard.ts deleted file mode 100644 index 2fac604f49..0000000000 --- a/src/panels/config/integrations/ha-config-entries-dashboard.ts +++ /dev/null @@ -1,402 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/iron-icon/iron-icon"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import "@polymer/paper-tooltip/paper-tooltip"; -import { HassEntity } from "home-assistant-js-websocket"; -import { - css, - CSSResult, - customElement, - html, - LitElement, - property, - TemplateResult, -} from "lit-element"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import { computeRTL } from "../../../common/util/compute_rtl"; -import "../../../components/entity/ha-state-icon"; -import "../../../components/ha-card"; -import "../../../components/ha-fab"; -import "../../../components/ha-icon"; -import "../../../components/ha-icon-next"; -import { ConfigEntry, deleteConfigEntry } from "../../../data/config_entries"; -import { - DISCOVERY_SOURCES, - ignoreConfigFlow, - localizeConfigFlowTitle, -} from "../../../data/config_flow"; -import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; -import { EntityRegistryEntry } from "../../../data/entity_registry"; -import { - loadConfigFlowDialog, - showConfigFlowDialog, -} from "../../../dialogs/config-flow/show-dialog-config-flow"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; -import "../../../layouts/hass-tabs-subpage"; -import "../../../resources/ha-style"; -import { HomeAssistant, Route } from "../../../types"; -import "../ha-config-section"; -import { configSections } from "../ha-panel-config"; - -@customElement("ha-config-entries-dashboard") -export class HaConfigManagerDashboard extends LitElement { - @property() public hass!: HomeAssistant; - - @property() public showAdvanced!: boolean; - - @property() public isWide!: boolean; - - @property() public narrow!: boolean; - - @property() public route!: Route; - - @property() private configEntries!: ConfigEntry[]; - - /** - * Entity Registry entries. - */ - @property() private entityRegistryEntries!: EntityRegistryEntry[]; - - /** - * Current flows that are in progress and have not been started by a user. - * For example, can be discovered devices that require more config. - */ - @property() private configEntriesInProgress!: DataEntryFlowProgress[]; - - @property() private _showIgnored = false; - - public connectedCallback() { - super.connectedCallback(); - loadConfigFlowDialog(); - } - - protected render(): TemplateResult { - return html` - - - - - - ${this.hass.localize( - this._showIgnored - ? "ui.panel.config.integrations.ignore.hide_ignored" - : "ui.panel.config.integrations.ignore.show_ignored" - )} - - - - - ${this._showIgnored - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignored" - )} - - ${this.configEntries - .filter((item) => item.source === "ignore") - .map( - (item: ConfigEntry) => html` - - - ${this.hass.localize( - `component.${item.domain}.title` - )} - - - - ` - )} - - - ` - : ""} - ${this.configEntriesInProgress.length - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.discovered" - )} - - ${this.configEntriesInProgress.map( - (flow) => html` -
- - ${localizeConfigFlowTitle(this.hass.localize, flow)} - - ${DISCOVERY_SOURCES.includes(flow.context.source) && - flow.context.unique_id - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignore" - )} - - ` - : ""} - ${this.hass.localize( - "ui.panel.config.integrations.configure" - )} -
- ` - )} -
-
- ` - : ""} - - - - ${this.hass.localize("ui.panel.config.integrations.configured")} - - - ${this.entityRegistryEntries.length - ? this.configEntries.map((item: any, idx) => - item.source === "ignore" - ? "" - : html` -
- - - -
- ${this.hass.localize( - `component.${item.domain}.title` - )}: - ${item.title} -
-
- ${this._getEntities(item).map( - (entity) => html` - - - ${computeStateName( - entity - )} - - ` - )} -
-
- -
-
- ` - ) - : html` -
- -
- ${this.hass.localize( - "ui.panel.config.integrations.none" - )} -
-
-
- `} - - - - - - `; - } - - private _createFlow() { - showConfigFlowDialog(this, { - dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"), - showAdvanced: this.showAdvanced, - }); - } - - private _continueFlow(ev: Event) { - showConfigFlowDialog(this, { - continueFlowId: (ev.target! as any).flowId, - dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"), - }); - } - - private _ignoreFlow(ev: Event) { - const flow = (ev.target! as any).flow; - showConfirmationDialog(this, { - title: this.hass!.localize( - "ui.panel.config.integrations.ignore.confirm_ignore_title", - "name", - localizeConfigFlowTitle(this.hass.localize, flow) - ), - text: this.hass!.localize( - "ui.panel.config.integrations.ignore.confirm_ignore" - ), - confirmText: this.hass!.localize( - "ui.panel.config.integrations.ignore.ignore" - ), - confirm: () => { - ignoreConfigFlow(this.hass, flow.flow_id); - fireEvent(this, "hass-reload-entries"); - }, - }); - } - - private _toggleShowIgnored() { - this._showIgnored = !this._showIgnored; - } - - private async _removeIgnoredIntegration(ev: Event) { - const entry = (ev.target! as any).entry; - showConfirmationDialog(this, { - title: this.hass!.localize( - "ui.panel.config.integrations.ignore.confirm_delete_ignore_title", - "name", - this.hass.localize(`component.${entry.domain}.title`) - ), - text: this.hass!.localize( - "ui.panel.config.integrations.ignore.confirm_delete_ignore" - ), - confirmText: this.hass!.localize( - "ui.panel.config.integrations.ignore.stop_ignore" - ), - confirm: async () => { - const result = await deleteConfigEntry(this.hass, entry.entry_id); - if (result.require_restart) { - alert( - this.hass.localize( - "ui.panel.config.integrations.config_entry.restart_confirm" - ) - ); - } - fireEvent(this, "hass-reload-entries"); - }, - }); - } - - private _getEntities(configEntry: ConfigEntry): HassEntity[] { - if (!this.entityRegistryEntries) { - return []; - } - const states: HassEntity[] = []; - this.entityRegistryEntries.forEach((entity) => { - if ( - entity.config_entry_id === configEntry.entry_id && - entity.entity_id in this.hass.states - ) { - states.push(this.hass.states[entity.entity_id]); - } - }); - return states; - } - - private _onImageLoad(ev) { - ev.target.style.visibility = "initial"; - } - - private _onImageError(ev) { - ev.target.style.visibility = "hidden"; - } - - static get styles(): CSSResult { - return css` - mwc-button { - align-self: center; - } - .config-entry-row { - display: flex; - padding: 0 16px; - } - ha-icon { - cursor: pointer; - margin: 8px; - } - .configured { - padding-bottom: 24px; - } - .configured a { - color: var(--primary-text-color); - text-decoration: none; - } - ha-fab { - position: fixed; - bottom: 16px; - right: 16px; - z-index: 1; - } - ha-fab[narrow] { - bottom: 84px; - } - ha-fab[rtl] { - right: auto; - left: 16px; - } - .overflow { - width: 56px; - } - img { - width: 50px; - margin-right: 16px; - } - `; - } -} diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index abf79ff220..09aa9847e4 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -1,14 +1,32 @@ +/* eslint-disable lit/no-invalid-html */ import "@polymer/app-route/app-route"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { customElement, property, PropertyValues } from "lit-element"; +import { + customElement, + property, + PropertyValues, + LitElement, + TemplateResult, + html, + CSSResult, + css, +} from "lit-element"; import { compare } from "../../../common/string/compare"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import "../../../components/entity/ha-state-icon"; +import "../../../components/ha-card"; +import "../../../components/ha-fab"; import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../../../data/area_registry"; -import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; + ConfigEntry, + deleteConfigEntry, + getConfigEntries, + updateConfigEntry, +} from "../../../data/config_entries"; import { + DISCOVERY_SOURCES, getConfigFlowInProgressCollection, + ignoreConfigFlow, + localizeConfigFlowTitle, subscribeConfigFlowInProgress, } from "../../../data/config_flow"; import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; @@ -20,22 +38,24 @@ import { EntityRegistryEntry, subscribeEntityRegistry, } from "../../../data/entity_registry"; +import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { - HassRouterPage, - RouterOptions, -} from "../../../layouts/hass-router-page"; -import { HomeAssistant } from "../../../types"; -import "./config-entry/ha-config-entry-page"; -import "./ha-config-entries-dashboard"; - -declare global { - interface HASSDomEvents { - "hass-reload-entries": undefined; - } -} + showAlertDialog, + showConfirmationDialog, + showPromptDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-tabs-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { HomeAssistant, Route } from "../../../types"; +import { configSections } from "../ha-panel-config"; +import { domainToName } from "../../../data/integration"; +import { haStyle } from "../../../resources/styles"; +import { afterNextRender } from "../../../common/util/render-status"; @customElement("ha-config-integrations") -class HaConfigIntegrations extends HassRouterPage { +class HaConfigIntegrations extends SubscribeMixin(LitElement) { @property() public hass!: HomeAssistant; @property() public narrow!: boolean; @@ -44,17 +64,7 @@ class HaConfigIntegrations extends HassRouterPage { @property() public showAdvanced!: boolean; - protected routerOptions: RouterOptions = { - defaultPage: "dashboard", - routes: { - dashboard: { - tag: "ha-config-entries-dashboard", - }, - config_entry: { - tag: "ha-config-entry-page", - }, - }, - }; + @property() public route!: Route; @property() private _configEntries: ConfigEntry[] = []; @@ -64,78 +74,14 @@ class HaConfigIntegrations extends HassRouterPage { @property() private _deviceRegistryEntries: DeviceRegistryEntry[] = []; - @property() private _areas: AreaRegistryEntry[] = []; + @property() private _showIgnored = false; - private _unsubs?: UnsubscribeFunc[]; + @property() private _searchParms = new URLSearchParams( + window.location.hash.substring(1) + ); - public connectedCallback() { - super.connectedCallback(); - - if (!this.hass) { - return; - } - this._loadData(); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubs) { - while (this._unsubs.length) { - this._unsubs.pop()!(); - } - this._unsubs = undefined; - } - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - this.addEventListener("hass-reload-entries", () => { - this._loadData(); - getConfigFlowInProgressCollection(this.hass.connection).refresh(); - }); - // For config entries. Also loading config flow ones for add integration - this.hass.loadBackendTranslation("title", undefined, true); - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (!this._unsubs && changedProps.has("hass")) { - this._loadData(); - } - } - - protected updatePageEl(pageEl) { - pageEl.hass = this.hass; - - pageEl.entityRegistryEntries = this._entityRegistryEntries; - pageEl.configEntries = this._configEntries; - pageEl.narrow = this.narrow; - pageEl.isWide = this.isWide; - pageEl.showAdvanced = this.showAdvanced; - pageEl.route = this.routeTail; - if (this._currentPage === "dashboard") { - pageEl.configEntriesInProgress = this._configEntriesInProgress; - return; - } - - pageEl.configEntryId = this.routeTail.path.substr(1); - pageEl.deviceRegistryEntries = this._deviceRegistryEntries; - pageEl.areas = this._areas; - } - - private _loadData() { - getConfigEntries(this.hass).then((configEntries) => { - this._configEntries = configEntries.sort((conf1, conf2) => - compare(conf1.domain + conf1.title, conf2.domain + conf2.title) - ); - }); - if (this._unsubs) { - return; - } - this._unsubs = [ - subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }), + public hassSubscribe(): UnsubscribeFunc[] { + return [ subscribeEntityRegistry(this.hass.connection, (entries) => { this._entityRegistryEntries = entries; }), @@ -153,6 +99,563 @@ class HaConfigIntegrations extends HassRouterPage { }), ]; } + + protected firstUpdated(changed: PropertyValues) { + super.firstUpdated(changed); + this._loadConfigEntries(); + this.hass.loadBackendTranslation("title", undefined, true); + } + + protected updated(changed: PropertyValues) { + super.updated(changed); + if ( + this._searchParms.has("config_entry") && + changed.has("_configEntries") && + !(changed.get("_configEntries") as ConfigEntry[]).length && + this._configEntries.length + ) { + afterNextRender(() => { + const card = this.shadowRoot!.getElementById( + this._searchParms.get("config_entry")! + ); + if (card) { + card.scrollIntoView(); + card.classList.add("highlight"); + } + }); + } + } + + protected render(): TemplateResult { + return html` + + + + + + ${this.hass.localize( + this._showIgnored + ? "ui.panel.config.integrations.ignore.hide_ignored" + : "ui.panel.config.integrations.ignore.show_ignored" + )} + + + + +
+ ${this._showIgnored + ? this._configEntries + .filter((item) => item.source === "ignore") + .map( + (item: ConfigEntry) => html` + +
+ ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignored" + )} +
+
+
+ +
+

+ ${domainToName(this.hass.localize, item.domain)} +

+ ${this.hass.localize( + "ui.panel.config.integrations.ignore.stop_ignore" + )} +
+
+ ` + ) + : ""} + ${this._configEntriesInProgress.length + ? this._configEntriesInProgress.map( + (flow) => html` + +
+ ${this.hass.localize( + "ui.panel.config.integrations.discovered" + )} +
+
+
+ +
+

+ ${localizeConfigFlowTitle(this.hass.localize, flow)} +

+ + ${this.hass.localize( + "ui.panel.config.integrations.configure" + )} + + ${DISCOVERY_SOURCES.includes(flow.context.source) && + flow.context.unique_id + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignore" + )} + + ` + : ""} +
+
+ ` + ) + : ""} + ${this._configEntries.length + ? this._configEntries.map((item: any) => { + const devices = this._getDevices(item); + const entities = this._getEntities(item); + return item.source === "ignore" + ? "" + : html` + +
+
+ +
+

+ ${domainToName(this.hass.localize, item.domain)} +

+

+ ${item.title} +

+ ${devices.length || entities.length + ? html` +
+ ${devices.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.devices", + "count", + devices.length + )} + ` + : ""} + ${devices.length && entities.length + ? "and" + : ""} + ${entities.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.entities", + "count", + entities.length + )} + ` + : ""} +
+ ` + : ""} +
+
+
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.rename" + )} + ${item.supports_options + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.options" + )} + ` + : ""} +
+ + + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.system_options" + )} + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.delete" + )} + + +
+
+ `; + }) + : html` + +
+

+ ${this.hass.localize("ui.panel.config.integrations.none")} +

+

+ ${this.hass.localize( + "ui.panel.config.integrations.no_integrations" + )} +

+ ${this.hass.localize( + "ui.panel.config.integrations.add" + )} +
+
+ `} +
+ +
+ `; + } + + private _loadConfigEntries() { + getConfigEntries(this.hass).then((configEntries) => { + this._configEntries = configEntries.sort((conf1, conf2) => + compare(conf1.domain + conf1.title, conf2.domain + conf2.title) + ); + }); + } + + private _createFlow() { + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._loadConfigEntries(); + getConfigFlowInProgressCollection(this.hass.connection).refresh(); + }, + showAdvanced: this.showAdvanced, + }); + // For config entries. Also loading config flow ones for add integration + this.hass.loadBackendTranslation("title", undefined, true); + } + + private _continueFlow(ev: Event) { + showConfigFlowDialog(this, { + continueFlowId: (ev.target! as any).flowId, + dialogClosedCallback: () => { + this._loadConfigEntries(); + getConfigFlowInProgressCollection(this.hass.connection).refresh(); + }, + }); + } + + private _ignoreFlow(ev: Event) { + const flow = (ev.target! as any).flow; + showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_ignore_title", + "name", + localizeConfigFlowTitle(this.hass.localize, flow) + ), + text: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_ignore" + ), + confirmText: this.hass!.localize( + "ui.panel.config.integrations.ignore.ignore" + ), + confirm: () => { + ignoreConfigFlow(this.hass, flow.flow_id); + getConfigFlowInProgressCollection(this.hass.connection).refresh(); + }, + }); + } + + private _toggleShowIgnored() { + this._showIgnored = !this._showIgnored; + } + + private async _removeIgnoredIntegration(ev: Event) { + const entry = (ev.target! as any).entry; + showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_delete_ignore_title", + "name", + this.hass.localize(`component.${entry.domain}.config.title`) + ), + text: this.hass!.localize( + "ui.panel.config.integrations.ignore.confirm_delete_ignore" + ), + confirmText: this.hass!.localize( + "ui.panel.config.integrations.ignore.stop_ignore" + ), + confirm: async () => { + const result = await deleteConfigEntry(this.hass, entry.entry_id); + if (result.require_restart) { + alert( + this.hass.localize( + "ui.panel.config.integrations.config_entry.restart_confirm" + ) + ); + } + this._loadConfigEntries(); + }, + }); + } + + private _getEntities(configEntry: ConfigEntry): EntityRegistryEntry[] { + if (!this._entityRegistryEntries) { + return []; + } + return this._entityRegistryEntries.filter( + (entity) => entity.config_entry_id === configEntry.entry_id + ); + } + + private _getDevices(configEntry: ConfigEntry): DeviceRegistryEntry[] { + if (!this._deviceRegistryEntries) { + return []; + } + return this._deviceRegistryEntries.filter((device) => + device.config_entries.includes(configEntry.entry_id) + ); + } + + private _onImageLoad(ev) { + ev.target.style.visibility = "initial"; + } + + private _onImageError(ev) { + ev.target.style.visibility = "hidden"; + } + + private _showOptions(ev) { + showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry); + } + + private _showSystemOptions(ev) { + showConfigEntrySystemOptionsDialog(this, { + entry: ev.target.closest("ha-card").configEntry, + }); + } + + private async _editEntryName(ev) { + const configEntry = ev.target.closest("ha-card").configEntry; + const newName = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), + defaultValue: configEntry.title, + inputLabel: this.hass.localize( + "ui.panel.config.integrations.rename_input_label" + ), + }); + if (!newName) { + return; + } + const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, { + title: newName, + }); + this._configEntries = this._configEntries!.map((entry) => + entry.entry_id === newEntry.entry_id ? newEntry : entry + ); + } + + private async _removeIntegration(ev) { + const entryId = ev.target.closest("ha-card").configEntry.entry_id; + + const confirmed = await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm" + ), + }); + + if (!confirmed) { + return; + } + + deleteConfigEntry(this.hass, entryId).then((result) => { + this._configEntries = this._configEntries.filter( + (entry) => entry.entry_id !== entryId + ); + + if (result.require_restart) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.restart_confirm" + ), + }); + } + }); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-gap: 16px 16px; + padding: 16px; + margin-bottom: 64px; + } + ha-card { + display: flex; + flex-direction: column; + justify-content: space-between; + } + ha-card.highlight { + border: 1px solid var(--accent-color); + } + .discovered { + border: 1px solid var(--primary-color); + } + .discovered .header { + background: var(--primary-color); + color: var(--text-primary-color); + padding: 8px; + text-align: center; + } + .ignored { + border: 1px solid var(--light-theme-disabled-color); + } + .ignored .header { + background: var(--light-theme-disabled-color); + color: var(--text-primary-color); + padding: 8px; + text-align: center; + } + .card-content { + padding: 16px; + text-align: center; + } + ha-card.integration .card-content { + padding-bottom: 3px; + } + .card-actions { + border-top: none; + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 5px; + } + .helper { + display: inline-block; + height: 100%; + vertical-align: middle; + } + .image { + display: flex; + align-items: center; + justify-content: center; + height: 60px; + margin-bottom: 16px; + vertical-align: middle; + } + img { + max-height: 60px; + max-width: 90%; + } + a { + color: var(--primary-color); + } + h1 { + margin-bottom: 0; + } + h2 { + margin-top: 0; + } + ha-fab { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 1; + } + ha-fab[narrow] { + bottom: 84px; + } + ha-fab[rtl] { + right: auto; + left: 16px; + } + paper-menu-button { + color: var(--secondary-text-color); + padding: 0; + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 5f006a5ce7..3b22b8074a 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -157,7 +157,7 @@ export class HuiCardOptions extends LitElement { } paper-item.delete-item { - color: var(--google-red-500); + color: var(--error-color); } `; } diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index fd2ee988e3..324b667b08 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -32,6 +32,7 @@ export const renderMarkdown = ( ...whiteListNormal, svg: ["xmlns", "height", "width"], path: ["transform", "stroke", "d"], + img: ["src"], }; } whiteList = whiteListSvg; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index b4e38a73a1..bf7520d65f 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -51,7 +51,11 @@ export const derivedStyles = { export const haStyle = css` :host { - @apply --paper-font-body1; + font-family: var(--paper-font-body1_-_font-family); + -webkit-font-smoothing: var(--paper-font-body1_-_-webkit-font-smoothing); + font-size: var(--paper-font-body1_-_font-size); + font-weight: var(--paper-font-body1_-_font-weight); + line-height: var(--paper-font-body1_-_line-height); } app-header-layout, @@ -73,7 +77,25 @@ export const haStyle = css` } h1 { - @apply --paper-font-title; + font-family: var(--paper-font-title_-_font-family); + -webkit-font-smoothing: var(--paper-font-title_-_-webkit-font-smoothing); + white-space: var(--paper-font-title_-_white-space); + overflow: var(--paper-font-title_-_overflow); + text-overflow: var(--paper-font-title_-_text-overflow); + font-size: var(--paper-font-title_-_font-size); + font-weight: var(--paper-font-title_-_font-weight); + line-height: var(--paper-font-title_-_line-height); + } + + h2 { + font-family: var(--paper-font-subhead_-_font-family); + -webkit-font-smoothing: var(--paper-font-subhead_-_-webkit-font-smoothing); + white-space: var(--paper-font-subhead_-_white-space); + overflow: var(--paper-font-subhead_-_overflow); + text-overflow: var(--paper-font-subhead_-_text-overflow); + font-size: var(--paper-font-subhead_-_font-size); + font-weight: var(--paper-font-subhead_-_font-weight); + line-height: var(--paper-font-subhead_-_line-height); } .secondary { diff --git a/src/translations/en.json b/src/translations/en.json index f39c9260fa..9f9df680cc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -489,6 +489,10 @@ "config": { "header": "Configure Home Assistant", "introduction": "Here it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.", + "filtering": { + "filtering_by": "Filtering by", + "clear": "Clear" + }, "advanced_mode": { "hint_enable": "Missing config options? Enable advanced mode on", "link_profile_page": "your profile page" @@ -1323,9 +1327,12 @@ "integrations": { "caption": "Integrations", "description": "Manage and set up integrations", + "integration": "integration", "discovered": "Discovered", "configured": "Configured", "new": "Set up a new integration", + "add_integration": "Add integration", + "no_integrations": "Seems like you don't have any integations configured yet. Click on the button below to add your first integration!", "note_about_integrations": "Not all integrations can be configured via the UI yet.", "note_about_website_reference": "More are available on the ", "home_assistant_website": "Home Assistant website", @@ -1333,6 +1340,8 @@ "none": "Nothing configured yet", "integration_not_found": "Integration not found.", "details": "Integration details", + "rename_dialog": "Edit the name of this config entry", + "rename_input_label": "Entry name", "ignore": { "ignore": "Ignore", "confirm_ignore_title": "Ignore discovery of {name}?", @@ -1345,11 +1354,12 @@ "stop_ignore": "Stop ignoring" }, "config_entry": { - "settings_button": "Edit settings for {integration}", - "system_options_button": "System options for {integration}", - "delete_button": "Delete {integration}", - "no_devices": "This integration has no devices.", - "no_device": "Entities without devices", + "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", + "entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}", + "rename": "Rename", + "options": "Options", + "system_options": "System options", + "delete": "Delete", "delete_confirm": "Are you sure you want to delete this integration?", "restart_confirm": "Restart Home Assistant to finish removing this integration", "manuf": "by {manufacturer}",