mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +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 {
|
||||
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`)}:
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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")) {
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
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: "";
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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 { 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 {
|
||||
|
@ -157,7 +157,7 @@ export class HuiCardOptions extends LitElement {
|
||||
}
|
||||
|
||||
paper-item.delete-item {
|
||||
color: var(--google-red-500);
|
||||
color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export const renderMarkdown = (
|
||||
...whiteListNormal,
|
||||
svg: ["xmlns", "height", "width"],
|
||||
path: ["transform", "stroke", "d"],
|
||||
img: ["src"],
|
||||
};
|
||||
}
|
||||
whiteList = whiteListSvg;
|
||||
|
@ -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 {
|
||||
|
@ -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}",
|
||||
|
Loading…
x
Reference in New Issue
Block a user