New layout for integration config (#5580)

* WIP

* Add filter message to device and entities page

* Lokalize

* Missed 2

* Fixed in #5581

* Change to hash
This commit is contained in:
Bram Kragten 2020-04-22 11:51:50 +02:00 committed by GitHub
parent 1c1f9a6a89
commit 1dfb632fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 964 additions and 1108 deletions

View File

@ -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) {
)}:
</h3>
<a
href="/config/integrations/config_entry/${relatedConfigEntryId}"
href="/config/integrations#config_entry=${relatedConfigEntryId}"
@click=${this._close}
>
${this.hass.localize(`component.${entry.domain}.title`)}:

View File

@ -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<ConfigEntry[]>("GET", "config/config_entries/entry");
export const updateConfigEntry = (
hass: HomeAssistant,
configEntryId: string,
updatedValues: Partial<ConfigEntryMutableParams>
) =>
hass.callWS<ConfigEntry>({
type: "config_entries/update",
entry_id: configEntryId,
...updatedValues,
});
export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callApi<{
require_restart: boolean;

View File

@ -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}
></search-input>
${this.activeFilters
? html`<div class="active-filters">
<div>
<ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
</paper-tooltip>
</div>
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div>
</slot>
</div>
@ -143,8 +170,24 @@ export class HaTabsSubpageDataTable extends LitElement {
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input></div></slot
></slot>
>
</search-input>
${this.activeFilters
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div></slot
></slot
>
</div>
`
: html` <div slot="header"></div> `}
@ -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;
}
`;
}
}

View File

@ -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")) {

View File

@ -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`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.tabs=${configSections.integrations}
.route=${this.route}
.columns=${this._columns(this.narrow)}
@ -252,7 +309,12 @@ export class HaConfigDeviceDashboard extends LitElement {
this.entries,
this.entities,
this.areas,
this.domain,
this._searchParms,
this.hass.localize
)}
.activeFilters=${this._activeFilters(
this.entries,
this._searchParms,
this.hass.localize
)}
@row-click=${this._handleRowClicked}

View File

@ -1,6 +1,5 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, PropertyValues } from "lit-element";
import { compare } from "../../../common/string/compare";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
@ -91,9 +90,7 @@ class HaConfigDevices extends HassRouterPage {
protected updatePageEl(pageEl) {
pageEl.hass = this.hass;
if (this._currentPage === "dashboard") {
pageEl.domain = this.routeTail.path.substr(1);
} else if (this._currentPage === "device") {
if (this._currentPage === "device") {
pageEl.deviceId = this.routeTail.path.substr(1);
}
@ -109,9 +106,7 @@ class HaConfigDevices extends HassRouterPage {
private _loadData() {
getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries.sort((conf1, conf2) =>
compare(conf1.title, conf2.title)
);
this._configEntries = configEntries;
});
if (this._unsubs) {
return;

View File

@ -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 || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
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}<br />
${device.area} | ${device.integration}<br />
${battery && !isNaN(battery.state as any)
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: ""}
`;
},
},
}
: {
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}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: html` - `;
},
},
}
);
protected render(): TemplateResult {
return html`
<ha-data-table
.columns=${this._columns(this.narrow)}
.data=${this._devices(
this.devices,
this.entries,
this.entities,
this.areas,
this.domain,
this.hass.localize
)}
.noDataText=${this.hass.localize(
"ui.panel.config.devices.data_table.no_devices"
)}
@row-click=${this._handleRowClicked}
auto-height
></ha-data-table>
`;
}
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;
}
}

View File

@ -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` <hass-loading-screen></hass-loading-screen> `;
}
const activeFilters = this._activeFilters(
this._searchParms,
this.hass.localize,
this._entries
);
const headerToolbar = this._selectedEntities.length
? html`
<p class="selected-txt">
@ -345,7 +419,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
></search-input>
></search-input
>${activeFilters
? html`<div class="active-filters">
${this.narrow
? html` <div>
<ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip position="left">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${activeFilters.join(", ")}
</paper-tooltip>
</div>`
: `${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)} ${activeFilters.join(", ")}`}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
<paper-menu-button no-animations horizontal-align="right">
<paper-icon-button
aria-label=${this.hass!.localize(
@ -393,13 +489,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.integrations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._filteredEntities(
this._entities,
this._stateEntities,
this._searchParms,
this._showDisabled,
this._showUnavailable,
this._showReadOnly
@ -586,6 +685,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
});
}
private async _loadConfigEntries() {
this._entries = await getConfigEntries(this.hass);
}
private _clearFilter() {
navigate(this, window.location.pathname, true);
}
static get styles(): CSSResult {
return css`
hass-loading-screen {
@ -629,7 +736,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.table-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
align-items: center;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
search-input {
@ -641,9 +748,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.search-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-left: -24px;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -8px;
}
.selected-txt {
font-weight: bold;
@ -659,6 +767,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.header-btns > 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: "";
}
`;
}
}

View File

@ -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`
<style>
ha-card {
margin-top: 8px;
padding-bottom: 8px;
}
paper-icon-item {
cursor: pointer;
padding-top: 4px;
padding-bottom: 4px;
}
</style>
<ha-card header="[[heading]]">
<template is="dom-repeat" items="[[entities]]" as="entity">
<paper-icon-item on-click="_openMoreInfo">
<state-badge
state-obj="[[_computeStateObj(entity, hass)]]"
slot="item-icon"
></state-badge>
<paper-item-body>
<div class="name">[[_computeEntityName(entity, hass)]]</div>
<div class="secondary entity-id">[[entity.entity_id]]</div>
</paper-item-body>
</paper-icon-item>
</template>
</ha-card>
`;
}
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);

View File

@ -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`
<hass-error-screen
error="${this.hass.localize(
"ui.panel.config.integrations.integration_not_found"
)}"
></hass-error-screen>
`;
}
const configEntryDevices = this._computeConfigEntryDevices(
configEntry,
this.deviceRegistryEntries
);
const noDeviceEntities = this._computeNoDeviceEntities(
configEntry,
this.entityRegistryEntries
);
return html`
<hass-subpage .header=${configEntry.title}>
${configEntry.supports_options
? html`
<paper-icon-button
slot="toolbar-icon"
icon="hass:settings"
@click=${this._showSettings}
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.settings_button",
"integration",
configEntry.title
)}
></paper-icon-button>
`
: ""}
<paper-icon-button
slot="toolbar-icon"
icon="hass:message-settings-variant"
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options_button",
"integration",
configEntry.title
)}
@click=${this._showSystemOptions}
></paper-icon-button>
<paper-icon-button
slot="toolbar-icon"
icon="hass:delete"
title=${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_button",
"integration",
configEntry.title
)}
@click=${this._confirmRemoveEntry}
></paper-icon-button>
<div class="content">
${configEntryDevices.length === 0 && noDeviceEntities.length === 0
? html`
<p>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.no_devices"
)}
</p>
`
: html`
<ha-devices-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.devices=${configEntryDevices}
.entries=${this.configEntries}
.entities=${this.entityRegistryEntries}
.areas=${this.areas}
></ha-devices-data-table>
`}
${noDeviceEntities.length > 0
? html`
<ha-ce-entities-card
.heading=${this.hass.localize(
"ui.panel.config.integrations.config_entry.no_device"
)}
.entities=${noDeviceEntities}
.hass=${this.hass}
.narrow=${this.narrow}
></ha-ce-entities-card>
`
: ""}
</div>
</hass-subpage>
`;
}
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);

View File

@ -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`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.integrations}
>
<paper-menu-button
close-on-activate
no-animations
horizontal-align="right"
horizontal-offset="-5"
slot="toolbar-icon"
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
alt="menu"
></paper-icon-button>
<paper-listbox
slot="dropdown-content"
role="listbox"
selected="{{selectedItem}}"
>
<paper-item @tap=${this._toggleShowIgnored}>
${this.hass.localize(
this._showIgnored
? "ui.panel.config.integrations.ignore.hide_ignored"
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
${this._showIgnored
? html`
<ha-config-section>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}</span
>
<ha-card>
${this.configEntries
.filter((item) => item.source === "ignore")
.map(
(item: ConfigEntry) => html`
<paper-item>
<paper-item-body>
${this.hass.localize(
`component.${item.domain}.title`
)}
</paper-item-body>
<paper-icon-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
icon="hass:delete"
aria-label=${this.hass.localize(
"ui.panel.config.integrations.details"
)}
></paper-icon-button>
</paper-item>
`
)}
</ha-card>
</ha-config-section>
`
: ""}
${this.configEntriesInProgress.length
? html`
<ha-config-section>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.integrations.discovered"
)}</span
>
<ha-card>
${this.configEntriesInProgress.map(
(flow) => html`
<div class="config-entry-row">
<paper-item-body>
${localizeConfigFlowTitle(this.hass.localize, flow)}
</paper-item-body>
${DISCOVERY_SOURCES.includes(flow.context.source) &&
flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.flow=${flow}
>
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
`
: ""}
<mwc-button
@click=${this._continueFlow}
.flowId=${flow.flow_id}
>${this.hass.localize(
"ui.panel.config.integrations.configure"
)}</mwc-button
>
</div>
`
)}
</ha-card>
</ha-config-section>
`
: ""}
<ha-config-section class="configured">
<span slot="header">
${this.hass.localize("ui.panel.config.integrations.configured")}
</span>
<ha-card>
${this.entityRegistryEntries.length
? this.configEntries.map((item: any, idx) =>
item.source === "ignore"
? ""
: html`
<a
href="/config/integrations/config_entry/${item.entry_id}"
>
<paper-item data-index=${idx}>
<img
src="https://brands.home-assistant.io/${item.domain}/icon.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<paper-item-body two-line>
<div>
${this.hass.localize(
`component.${item.domain}.title`
)}:
${item.title}
</div>
<div secondary>
${this._getEntities(item).map(
(entity) => html`
<span>
<ha-state-icon
.stateObj=${entity}
></ha-state-icon>
<paper-tooltip position="bottom"
>${computeStateName(
entity
)}</paper-tooltip
>
</span>
`
)}
</div>
</paper-item-body>
<ha-icon-next
aria-label=${this.hass.localize(
"ui.panel.config.integrations.details"
)}
></ha-icon-next>
</paper-item>
</a>
`
)
: html`
<div class="config-entry-row">
<paper-item-body two-line>
<div>
${this.hass.localize(
"ui.panel.config.integrations.none"
)}
</div>
</paper-item-body>
</div>
`}
</ha-card>
</ha-config-section>
<ha-fab
icon="hass:plus"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
?rtl=${computeRTL(this.hass!)}
?narrow=${this.narrow}
></ha-fab>
</hass-tabs-subpage>
`;
}
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;
}
`;
}
}

View File

@ -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`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.integrations}
>
<paper-menu-button
close-on-activate
no-animations
horizontal-align="right"
horizontal-offset="-5"
slot="toolbar-icon"
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
alt="menu"
></paper-icon-button>
<paper-listbox
slot="dropdown-content"
role="listbox"
selected="{{selectedItem}}"
>
<paper-item @tap=${this._toggleShowIgnored}>
${this.hass.localize(
this._showIgnored
? "ui.panel.config.integrations.ignore.hide_ignored"
: "ui.panel.config.integrations.ignore.show_ignored"
)}
</paper-item>
</paper-listbox>
</paper-menu-button>
<div class="container">
${this._showIgnored
? this._configEntries
.filter((item) => item.source === "ignore")
.map(
(item: ConfigEntry) => html`
<ha-card class="ignored">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignored"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${domainToName(this.hass.localize, item.domain)}
</h2>
<mwc-button
@click=${this._removeIgnoredIntegration}
.entry=${item}
aria-label=${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}
>${this.hass.localize(
"ui.panel.config.integrations.ignore.stop_ignore"
)}</mwc-button
>
</div>
</ha-card>
`
)
: ""}
${this._configEntriesInProgress.length
? this._configEntriesInProgress.map(
(flow) => html`
<ha-card class="discovered">
<div class="header">
${this.hass.localize(
"ui.panel.config.integrations.discovered"
)}
</div>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${flow.handler}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h2>
${localizeConfigFlowTitle(this.hass.localize, flow)}
</h2>
<mwc-button
unelevated
@click=${this._continueFlow}
.flowId=${flow.flow_id}
>
${this.hass.localize(
"ui.panel.config.integrations.configure"
)}
</mwc-button>
${DISCOVERY_SOURCES.includes(flow.context.source) &&
flow.context.unique_id
? html`
<mwc-button
@click=${this._ignoreFlow}
.flow=${flow}
>
${this.hass.localize(
"ui.panel.config.integrations.ignore.ignore"
)}
</mwc-button>
`
: ""}
</div>
</ha-card>
`
)
: ""}
${this._configEntries.length
? this._configEntries.map((item: any) => {
const devices = this._getDevices(item);
const entities = this._getEntities(item);
return item.source === "ignore"
? ""
: html`
<ha-card
class="integration"
.configEntry=${item}
.id=${item.entry_id}
>
<div class="card-content">
<div class="image">
<img
src="https://brands.home-assistant.io/${item.domain}/logo.png"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</div>
<h1>
${domainToName(this.hass.localize, item.domain)}
</h1>
<h2>
${item.title}
</h2>
${devices.length || entities.length
? html`
<div>
${devices.length
? html`
<a
href="/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
devices.length
)}</a
>
`
: ""}
${devices.length && entities.length
? "and"
: ""}
${entities.length
? html`
<a
href="/config/entities?historyBack=1&config_entry=${item.entry_id}"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
entities.length
)}</a
>
`
: ""}
</div>
`
: ""}
</div>
<div class="card-actions">
<div>
<mwc-button @click=${this._editEntryName}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}</mwc-button
>
${item.supports_options
? html`
<mwc-button @click=${this._showOptions}
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.options"
)}</mwc-button
>
`
: ""}
</div>
<paper-menu-button
horizontal-align="right"
vertical-align="top"
vertical-offset="40"
close-on-activate
>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.options"
)}
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @tap=${this._showSystemOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}</paper-item
>
<paper-item
class="warning"
@tap=${this._removeIntegration}
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}</paper-item
>
</paper-listbox>
</paper-menu-button>
</div>
</ha-card>
`;
})
: html`
<ha-card>
<div class="card-content">
<h1>
${this.hass.localize("ui.panel.config.integrations.none")}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.integrations.no_integrations"
)}
</p>
<mwc-button @click=${this._createFlow} unelevated
>${this.hass.localize(
"ui.panel.config.integrations.add"
)}</mwc-button
>
</div>
</ha-card>
`}
</div>
<ha-fab
icon="hass:plus"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
?rtl=${computeRTL(this.hass!)}
?narrow=${this.narrow}
></ha-fab>
</hass-tabs-subpage>
`;
}
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 {

View File

@ -157,7 +157,7 @@ export class HuiCardOptions extends LitElement {
}
paper-item.delete-item {
color: var(--google-red-500);
color: var(--error-color);
}
`;
}

View File

@ -32,6 +32,7 @@ export const renderMarkdown = (
...whiteListNormal,
svg: ["xmlns", "height", "width"],
path: ["transform", "stroke", "d"],
img: ["src"],
};
}
whiteList = whiteListSvg;

View File

@ -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 {

View File

@ -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}",