mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 12:16:39 +00:00
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:
parent
1c1f9a6a89
commit
1dfb632fc4
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable lit/no-invalid-html */
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@ -95,7 +96,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
|
|||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
<a
|
<a
|
||||||
href="/config/integrations/config_entry/${relatedConfigEntryId}"
|
href="/config/integrations#config_entry=${relatedConfigEntryId}"
|
||||||
@click=${this._close}
|
@click=${this._close}
|
||||||
>
|
>
|
||||||
${this.hass.localize(`component.${entry.domain}.title`)}:
|
${this.hass.localize(`component.${entry.domain}.title`)}:
|
||||||
|
@ -10,6 +10,10 @@ export interface ConfigEntry {
|
|||||||
supports_options: boolean;
|
supports_options: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigEntryMutableParams {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigEntrySystemOptions {
|
export interface ConfigEntrySystemOptions {
|
||||||
disable_new_entities: boolean;
|
disable_new_entities: boolean;
|
||||||
}
|
}
|
||||||
@ -17,6 +21,17 @@ export interface ConfigEntrySystemOptions {
|
|||||||
export const getConfigEntries = (hass: HomeAssistant) =>
|
export const getConfigEntries = (hass: HomeAssistant) =>
|
||||||
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
|
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) =>
|
export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
||||||
hass.callApi<{
|
hass.callApi<{
|
||||||
require_restart: boolean;
|
require_restart: boolean;
|
||||||
|
@ -17,6 +17,9 @@ import type {
|
|||||||
import type { HomeAssistant, Route } from "../types";
|
import type { HomeAssistant, Route } from "../types";
|
||||||
import "./hass-tabs-subpage";
|
import "./hass-tabs-subpage";
|
||||||
import type { PageNavigation } from "./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")
|
@customElement("hass-tabs-subpage-data-table")
|
||||||
export class HaTabsSubpageDataTable extends LitElement {
|
export class HaTabsSubpageDataTable extends LitElement {
|
||||||
@ -62,6 +65,12 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
*/
|
*/
|
||||||
@property({ type: String }) public filter = "";
|
@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.
|
* What path to use when the back button is pressed.
|
||||||
* @type {String}
|
* @type {String}
|
||||||
@ -118,6 +127,24 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
no-underline
|
no-underline
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
></search-input>
|
></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>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
@ -143,8 +170,24 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
no-label-float
|
no-label-float
|
||||||
no-underline
|
no-underline
|
||||||
@value-changed=${this._handleSearchChange}
|
@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>
|
</div>
|
||||||
`
|
`
|
||||||
: html` <div slot="header"></div> `}
|
: html` <div slot="header"></div> `}
|
||||||
@ -157,6 +200,10 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
this.filter = ev.detail.value;
|
this.filter = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter() {
|
||||||
|
navigate(this, window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
ha-data-table {
|
ha-data-table {
|
||||||
@ -171,19 +218,54 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.table-header {
|
.table-header {
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.search-toolbar {
|
.search-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
search-input {
|
search-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
search-input.header {
|
search-input.header {
|
||||||
left: -8px;
|
left: -8px;
|
||||||
top: -7px;
|
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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
if (!this._unsubs && changedProps.has("hass")) {
|
if (!this._unsubs && changedProps.has("hass")) {
|
||||||
|
@ -26,9 +26,17 @@ import {
|
|||||||
findBatteryEntity,
|
findBatteryEntity,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import "../../../components/entity/ha-state-icon";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
import { configSections } from "../ha-panel-config";
|
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")
|
@customElement("ha-config-devices-dashboard")
|
||||||
export class HaConfigDeviceDashboard extends LitElement {
|
export class HaConfigDeviceDashboard extends LitElement {
|
||||||
@ -46,17 +54,53 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
|
|
||||||
@property() public areas!: AreaRegistryEntry[];
|
@property() public areas!: AreaRegistryEntry[];
|
||||||
|
|
||||||
@property() public domain!: string;
|
|
||||||
|
|
||||||
@property() public route!: Route;
|
@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(
|
private _devices = memoizeOne(
|
||||||
(
|
(
|
||||||
devices: DeviceRegistryEntry[],
|
devices: DeviceRegistryEntry[],
|
||||||
entries: ConfigEntry[],
|
entries: ConfigEntry[],
|
||||||
entities: EntityRegistryEntry[],
|
entities: EntityRegistryEntry[],
|
||||||
areas: AreaRegistryEntry[],
|
areas: AreaRegistryEntry[],
|
||||||
domain: string,
|
filters: URLSearchParams,
|
||||||
localize: LocalizeFunc
|
localize: LocalizeFunc
|
||||||
) => {
|
) => {
|
||||||
// Some older installations might have devices pointing at invalid entryIDs
|
// Some older installations might have devices pointing at invalid entryIDs
|
||||||
@ -90,14 +134,15 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
areaLookup[area.area_id] = area;
|
areaLookup[area.area_id] = area;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain) {
|
filters.forEach((value, key) => {
|
||||||
outputDevices = outputDevices.filter((device) =>
|
switch (key) {
|
||||||
device.config_entries.find(
|
case "config_entry":
|
||||||
(entryId) =>
|
outputDevices = outputDevices.filter((device) =>
|
||||||
entryId in entryLookup && entryLookup[entryId].domain === domain
|
device.config_entries.includes(value)
|
||||||
)
|
);
|
||||||
);
|
break;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
outputDevices = outputDevices.map((device) => {
|
outputDevices = outputDevices.map((device) => {
|
||||||
return {
|
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 {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
back-path="/config"
|
.backPath=${this._searchParms.has("historyBack")
|
||||||
|
? undefined
|
||||||
|
: "/config"}
|
||||||
.tabs=${configSections.integrations}
|
.tabs=${configSections.integrations}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.columns=${this._columns(this.narrow)}
|
.columns=${this._columns(this.narrow)}
|
||||||
@ -252,7 +309,12 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
this.entries,
|
this.entries,
|
||||||
this.entities,
|
this.entities,
|
||||||
this.areas,
|
this.areas,
|
||||||
this.domain,
|
this._searchParms,
|
||||||
|
this.hass.localize
|
||||||
|
)}
|
||||||
|
.activeFilters=${this._activeFilters(
|
||||||
|
this.entries,
|
||||||
|
this._searchParms,
|
||||||
this.hass.localize
|
this.hass.localize
|
||||||
)}
|
)}
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { customElement, property, PropertyValues } from "lit-element";
|
import { customElement, property, PropertyValues } from "lit-element";
|
||||||
import { compare } from "../../../common/string/compare";
|
|
||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
subscribeAreaRegistry,
|
subscribeAreaRegistry,
|
||||||
@ -91,9 +90,7 @@ class HaConfigDevices extends HassRouterPage {
|
|||||||
protected updatePageEl(pageEl) {
|
protected updatePageEl(pageEl) {
|
||||||
pageEl.hass = this.hass;
|
pageEl.hass = this.hass;
|
||||||
|
|
||||||
if (this._currentPage === "dashboard") {
|
if (this._currentPage === "device") {
|
||||||
pageEl.domain = this.routeTail.path.substr(1);
|
|
||||||
} else if (this._currentPage === "device") {
|
|
||||||
pageEl.deviceId = this.routeTail.path.substr(1);
|
pageEl.deviceId = this.routeTail.path.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,9 +106,7 @@ class HaConfigDevices extends HassRouterPage {
|
|||||||
|
|
||||||
private _loadData() {
|
private _loadData() {
|
||||||
getConfigEntries(this.hass).then((configEntries) => {
|
getConfigEntries(this.hass).then((configEntries) => {
|
||||||
this._configEntries = configEntries.sort((conf1, conf2) =>
|
this._configEntries = configEntries;
|
||||||
compare(conf1.title, conf2.title)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
if (this._unsubs) {
|
if (this._unsubs) {
|
||||||
return;
|
return;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -49,6 +49,10 @@ import {
|
|||||||
loadEntityEditorDialog,
|
loadEntityEditorDialog,
|
||||||
showEntityEditorDialog,
|
showEntityEditorDialog,
|
||||||
} from "./show-dialog-entity-editor";
|
} 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 {
|
export interface StateEntity extends EntityRegistryEntry {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@ -76,6 +80,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() private _stateEntities: StateEntity[] = [];
|
@property() private _stateEntities: StateEntity[] = [];
|
||||||
|
|
||||||
|
@property() public _entries?: ConfigEntry[];
|
||||||
|
|
||||||
@property() private _showDisabled = false;
|
@property() private _showDisabled = false;
|
||||||
|
|
||||||
@property() private _showUnavailable = true;
|
@property() private _showUnavailable = true;
|
||||||
@ -84,6 +90,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() private _filter = "";
|
@property() private _filter = "";
|
||||||
|
|
||||||
|
@property() private _searchParms = new URLSearchParams(
|
||||||
|
window.location.search
|
||||||
|
);
|
||||||
|
|
||||||
@property() private _selectedEntities: string[] = [];
|
@property() private _selectedEntities: string[] = [];
|
||||||
|
|
||||||
@query("hass-tabs-subpage-data-table")
|
@query("hass-tabs-subpage-data-table")
|
||||||
@ -91,6 +101,44 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private getDialog?: () => DialogEntityEditor | undefined;
|
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(
|
private _columns = memoize(
|
||||||
(narrow, _language): DataTableColumnContainer => {
|
(narrow, _language): DataTableColumnContainer => {
|
||||||
const columns: DataTableColumnContainer = {
|
const columns: DataTableColumnContainer = {
|
||||||
@ -202,6 +250,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
(
|
(
|
||||||
entities: EntityRegistryEntry[],
|
entities: EntityRegistryEntry[],
|
||||||
stateEntities: StateEntity[],
|
stateEntities: StateEntity[],
|
||||||
|
filters: URLSearchParams,
|
||||||
showDisabled: boolean,
|
showDisabled: boolean,
|
||||||
showUnavailable: boolean,
|
showUnavailable: boolean,
|
||||||
showReadOnly: boolean
|
showReadOnly: boolean
|
||||||
@ -212,9 +261,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
const result: EntityRow[] = [];
|
const result: EntityRow[] = [];
|
||||||
|
|
||||||
for (const entry of showReadOnly
|
entities = showReadOnly ? entities.concat(stateEntities) : entities;
|
||||||
? 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 entity = this.hass.states[entry.entity_id];
|
||||||
const unavailable = entity?.state === "unavailable";
|
const unavailable = entity?.state === "unavailable";
|
||||||
const restored = entity?.attributes.restored;
|
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[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
return [
|
return [
|
||||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
@ -277,6 +346,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
if (!this.hass || this._entities === undefined) {
|
if (!this.hass || this._entities === undefined) {
|
||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||||
}
|
}
|
||||||
|
const activeFilters = this._activeFilters(
|
||||||
|
this._searchParms,
|
||||||
|
this.hass.localize,
|
||||||
|
this._entries
|
||||||
|
);
|
||||||
const headerToolbar = this._selectedEntities.length
|
const headerToolbar = this._selectedEntities.length
|
||||||
? html`
|
? html`
|
||||||
<p class="selected-txt">
|
<p class="selected-txt">
|
||||||
@ -345,7 +419,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
no-underline
|
no-underline
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
.filter=${this._filter}
|
.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-menu-button no-animations horizontal-align="right">
|
||||||
<paper-icon-button
|
<paper-icon-button
|
||||||
aria-label=${this.hass!.localize(
|
aria-label=${this.hass!.localize(
|
||||||
@ -393,13 +489,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
back-path="/config"
|
.backPath=${this._searchParms.has("historyBack")
|
||||||
|
? undefined
|
||||||
|
: "/config"}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.integrations}
|
.tabs=${configSections.integrations}
|
||||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||||
.data=${this._filteredEntities(
|
.data=${this._filteredEntities(
|
||||||
this._entities,
|
this._entities,
|
||||||
this._stateEntities,
|
this._stateEntities,
|
||||||
|
this._searchParms,
|
||||||
this._showDisabled,
|
this._showDisabled,
|
||||||
this._showUnavailable,
|
this._showUnavailable,
|
||||||
this._showReadOnly
|
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 {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
hass-loading-screen {
|
hass-loading-screen {
|
||||||
@ -629,7 +736,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.table-header {
|
.table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||||
}
|
}
|
||||||
search-input {
|
search-input {
|
||||||
@ -641,9 +748,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.search-toolbar {
|
.search-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
margin-left: -24px;
|
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
position: relative;
|
||||||
|
top: -8px;
|
||||||
}
|
}
|
||||||
.selected-txt {
|
.selected-txt {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -659,6 +767,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.header-btns > paper-icon-button {
|
.header-btns > paper-icon-button {
|
||||||
margin: 8px;
|
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: "";
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,32 @@
|
|||||||
|
/* eslint-disable lit/no-invalid-html */
|
||||||
import "@polymer/app-route/app-route";
|
import "@polymer/app-route/app-route";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
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 { 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 {
|
import {
|
||||||
AreaRegistryEntry,
|
ConfigEntry,
|
||||||
subscribeAreaRegistry,
|
deleteConfigEntry,
|
||||||
} from "../../../data/area_registry";
|
getConfigEntries,
|
||||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
updateConfigEntry,
|
||||||
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
|
DISCOVERY_SOURCES,
|
||||||
getConfigFlowInProgressCollection,
|
getConfigFlowInProgressCollection,
|
||||||
|
ignoreConfigFlow,
|
||||||
|
localizeConfigFlowTitle,
|
||||||
subscribeConfigFlowInProgress,
|
subscribeConfigFlowInProgress,
|
||||||
} from "../../../data/config_flow";
|
} from "../../../data/config_flow";
|
||||||
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||||
@ -20,22 +38,24 @@ import {
|
|||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
} from "../../../data/entity_registry";
|
} 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 {
|
import {
|
||||||
HassRouterPage,
|
showAlertDialog,
|
||||||
RouterOptions,
|
showConfirmationDialog,
|
||||||
} from "../../../layouts/hass-router-page";
|
showPromptDialog,
|
||||||
import { HomeAssistant } from "../../../types";
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "./config-entry/ha-config-entry-page";
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
import "./ha-config-entries-dashboard";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
declare global {
|
import { configSections } from "../ha-panel-config";
|
||||||
interface HASSDomEvents {
|
import { domainToName } from "../../../data/integration";
|
||||||
"hass-reload-entries": undefined;
|
import { haStyle } from "../../../resources/styles";
|
||||||
}
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-config-integrations")
|
@customElement("ha-config-integrations")
|
||||||
class HaConfigIntegrations extends HassRouterPage {
|
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||||
@property() public hass!: HomeAssistant;
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public narrow!: boolean;
|
@property() public narrow!: boolean;
|
||||||
@ -44,17 +64,7 @@ class HaConfigIntegrations extends HassRouterPage {
|
|||||||
|
|
||||||
@property() public showAdvanced!: boolean;
|
@property() public showAdvanced!: boolean;
|
||||||
|
|
||||||
protected routerOptions: RouterOptions = {
|
@property() public route!: Route;
|
||||||
defaultPage: "dashboard",
|
|
||||||
routes: {
|
|
||||||
dashboard: {
|
|
||||||
tag: "ha-config-entries-dashboard",
|
|
||||||
},
|
|
||||||
config_entry: {
|
|
||||||
tag: "ha-config-entry-page",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@property() private _configEntries: ConfigEntry[] = [];
|
@property() private _configEntries: ConfigEntry[] = [];
|
||||||
|
|
||||||
@ -64,78 +74,14 @@ class HaConfigIntegrations extends HassRouterPage {
|
|||||||
|
|
||||||
@property() private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
|
@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() {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
super.connectedCallback();
|
return [
|
||||||
|
|
||||||
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;
|
|
||||||
}),
|
|
||||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||||
this._entityRegistryEntries = 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 {
|
declare global {
|
||||||
|
@ -157,7 +157,7 @@ export class HuiCardOptions extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
paper-item.delete-item {
|
paper-item.delete-item {
|
||||||
color: var(--google-red-500);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ export const renderMarkdown = (
|
|||||||
...whiteListNormal,
|
...whiteListNormal,
|
||||||
svg: ["xmlns", "height", "width"],
|
svg: ["xmlns", "height", "width"],
|
||||||
path: ["transform", "stroke", "d"],
|
path: ["transform", "stroke", "d"],
|
||||||
|
img: ["src"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
whiteList = whiteListSvg;
|
whiteList = whiteListSvg;
|
||||||
|
@ -51,7 +51,11 @@ export const derivedStyles = {
|
|||||||
|
|
||||||
export const haStyle = css`
|
export const haStyle = css`
|
||||||
:host {
|
: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,
|
app-header-layout,
|
||||||
@ -73,7 +77,25 @@ export const haStyle = css`
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
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 {
|
.secondary {
|
||||||
|
@ -489,6 +489,10 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"header": "Configure Home Assistant",
|
"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.",
|
"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": {
|
"advanced_mode": {
|
||||||
"hint_enable": "Missing config options? Enable advanced mode on",
|
"hint_enable": "Missing config options? Enable advanced mode on",
|
||||||
"link_profile_page": "your profile page"
|
"link_profile_page": "your profile page"
|
||||||
@ -1323,9 +1327,12 @@
|
|||||||
"integrations": {
|
"integrations": {
|
||||||
"caption": "Integrations",
|
"caption": "Integrations",
|
||||||
"description": "Manage and set up integrations",
|
"description": "Manage and set up integrations",
|
||||||
|
"integration": "integration",
|
||||||
"discovered": "Discovered",
|
"discovered": "Discovered",
|
||||||
"configured": "Configured",
|
"configured": "Configured",
|
||||||
"new": "Set up a new integration",
|
"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_integrations": "Not all integrations can be configured via the UI yet.",
|
||||||
"note_about_website_reference": "More are available on the ",
|
"note_about_website_reference": "More are available on the ",
|
||||||
"home_assistant_website": "Home Assistant website",
|
"home_assistant_website": "Home Assistant website",
|
||||||
@ -1333,6 +1340,8 @@
|
|||||||
"none": "Nothing configured yet",
|
"none": "Nothing configured yet",
|
||||||
"integration_not_found": "Integration not found.",
|
"integration_not_found": "Integration not found.",
|
||||||
"details": "Integration details",
|
"details": "Integration details",
|
||||||
|
"rename_dialog": "Edit the name of this config entry",
|
||||||
|
"rename_input_label": "Entry name",
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
"confirm_ignore_title": "Ignore discovery of {name}?",
|
"confirm_ignore_title": "Ignore discovery of {name}?",
|
||||||
@ -1345,11 +1354,12 @@
|
|||||||
"stop_ignore": "Stop ignoring"
|
"stop_ignore": "Stop ignoring"
|
||||||
},
|
},
|
||||||
"config_entry": {
|
"config_entry": {
|
||||||
"settings_button": "Edit settings for {integration}",
|
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||||
"system_options_button": "System options for {integration}",
|
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
|
||||||
"delete_button": "Delete {integration}",
|
"rename": "Rename",
|
||||||
"no_devices": "This integration has no devices.",
|
"options": "Options",
|
||||||
"no_device": "Entities without devices",
|
"system_options": "System options",
|
||||||
|
"delete": "Delete",
|
||||||
"delete_confirm": "Are you sure you want to delete this integration?",
|
"delete_confirm": "Are you sure you want to delete this integration?",
|
||||||
"restart_confirm": "Restart Home Assistant to finish removing this integration",
|
"restart_confirm": "Restart Home Assistant to finish removing this integration",
|
||||||
"manuf": "by {manufacturer}",
|
"manuf": "by {manufacturer}",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user