mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 10:16:46 +00:00
commit
f5b3a82922
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20190917.1",
|
||||
version="20190917.2",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { SubscribeMixin } from "../../../src/mixins/subscribe-mixin";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
@ -68,7 +68,6 @@ export interface DataTabelColumnData {
|
||||
}
|
||||
|
||||
export interface DataTabelRowData {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -402,9 +401,7 @@ export class HaDataTable extends BaseElement {
|
||||
const rowId = (ev.target as HTMLElement)
|
||||
.closest("tr")!
|
||||
.getAttribute("data-row-id")!;
|
||||
fireEvent(this, "row-click", {
|
||||
id: rowId,
|
||||
});
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
}
|
||||
|
||||
private _setRowChecked(rowId: string, checked: boolean) {
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
PropertyDeclarations,
|
||||
} from "lit-element";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface HassSubscribeElement {
|
||||
hassSubscribe(): UnsubscribeFunc[];
|
||||
@ -16,6 +17,7 @@ export const SubscribeMixin = <T extends LitElement>(
|
||||
): Constructor<T & HassSubscribeElement> =>
|
||||
// @ts-ignore
|
||||
class extends superClass {
|
||||
private hass?: HomeAssistant;
|
||||
/* tslint:disable-next-line */
|
||||
private __unsubs?: UnsubscribeFunc[];
|
||||
|
||||
@ -56,7 +58,7 @@ export const SubscribeMixin = <T extends LitElement>(
|
||||
if (
|
||||
this.__unsubs !== undefined ||
|
||||
!((this as unknown) as Element).isConnected ||
|
||||
super.hass === undefined
|
||||
this.hass === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -92,6 +92,17 @@ class HaConfigDashboard extends NavigateMixin(LocalizeMixin(PolymerElement)) {
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
<a href='/config/devices/dashboard' tabindex="-1">
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
[[localize('ui.panel.config.devices.caption')]]
|
||||
<div secondary>
|
||||
[[localize('ui.panel.config.devices.description')]]
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
|
||||
<a href='/config/users' tabindex="-1">
|
||||
<paper-item>
|
||||
|
80
src/panels/config/devices/ha-config-device-page.ts
Normal file
80
src/panels/config/devices/ha-config-device-page.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { property, LitElement, html, customElement } from "lit-element";
|
||||
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
|
||||
import "./ha-device-card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
loadDeviceRegistryDetailDialog,
|
||||
showDeviceRegistryDetailDialog,
|
||||
} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
@customElement("ha-config-device-page")
|
||||
export class HaConfigDevicePage extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public devices!: DeviceRegistryEntry[];
|
||||
@property() public entries!: ConfigEntry[];
|
||||
@property() public entities!: EntityRegistryEntry[];
|
||||
@property() public areas!: AreaRegistryEntry[];
|
||||
@property() public deviceId!: string;
|
||||
|
||||
private _device = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
devices: DeviceRegistryEntry[]
|
||||
): DeviceRegistryEntry | undefined =>
|
||||
devices ? devices.find((device) => device.id === deviceId) : undefined
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const device = this._device(this.deviceId, this.devices);
|
||||
|
||||
if (!device) {
|
||||
return html`
|
||||
<hass-error-screen error="Device not found."></hass-error-screen>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage .header=${device.name_by_user || device.name}>
|
||||
<paper-icon-button
|
||||
slot="toolbar-icon"
|
||||
icon="hass:settings"
|
||||
@click=${this._showSettings}
|
||||
></paper-icon-button>
|
||||
<ha-device-card
|
||||
.hass=${this.hass}
|
||||
.areas=${this.areas}
|
||||
.devices=${this.devices}
|
||||
.device=${device}
|
||||
.entities=${this.entities}
|
||||
hide-settings
|
||||
></ha-device-card>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showSettings() {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this._device(this.deviceId, this.devices)!,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
238
src/panels/config/devices/ha-config-devices-dashboard.ts
Normal file
238
src/panels/config/devices/ha-config-devices-dashboard.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-data-table";
|
||||
import "../../../components/entity/ha-state-icon";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../resources/ha-style";
|
||||
import "../../../components/ha-icon-next";
|
||||
|
||||
import "../ha-config-section";
|
||||
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
// tslint:disable-next-line
|
||||
import {
|
||||
DataTabelColumnContainer,
|
||||
RowClickedEvent,
|
||||
DataTabelRowData,
|
||||
} from "../../../components/ha-data-table";
|
||||
// tslint:disable-next-line
|
||||
import { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
area?: string;
|
||||
integration?: string;
|
||||
battery_entity?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-config-devices-dashboard")
|
||||
export class HaConfigDeviceDashboard 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
|
||||
) => {
|
||||
let outputDevices: DeviceRowData[] = devices;
|
||||
if (domain) {
|
||||
outputDevices = outputDevices.filter(
|
||||
(device) =>
|
||||
entries.find((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)!.domain === domain
|
||||
);
|
||||
}
|
||||
|
||||
outputDevices = outputDevices.map((device) => {
|
||||
const output = { ...device };
|
||||
output.name = device.name_by_user || device.name || "No name";
|
||||
|
||||
output.area =
|
||||
!areas || !device || !device.area_id
|
||||
? "No area"
|
||||
: areas.find((area) => area.area_id === device.area_id)!.name;
|
||||
|
||||
output.integration =
|
||||
!entries || !device || !device.config_entries
|
||||
? "No integration"
|
||||
: entries.find((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)!.domain;
|
||||
|
||||
output.battery_entity = this._batteryEntity(device, entities);
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
return outputDevices;
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTabelColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
device: {
|
||||
title: "Device",
|
||||
sortable: true,
|
||||
filterKey: "name",
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
template: (device: DeviceRowData) => {
|
||||
const battery = device.battery_entity
|
||||
? this.hass.states[device.battery_entity]
|
||||
: undefined;
|
||||
// Have to work on a nice layout for mobile
|
||||
return html`
|
||||
${device.name_by_user || device.name}<br />
|
||||
${device.area} | ${device.integration}<br />
|
||||
${battery
|
||||
? html`
|
||||
${battery.state}%
|
||||
<ha-state-icon
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${battery}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
device_name: {
|
||||
title: "Device",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
},
|
||||
manufacturer: {
|
||||
title: "Manufacturer",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
model: {
|
||||
title: "Model",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
area: {
|
||||
title: "Area",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
integration: {
|
||||
title: "Integration",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
battery: {
|
||||
title: "Battery",
|
||||
sortable: true,
|
||||
type: "numeric",
|
||||
template: (batteryEntity: string) => {
|
||||
const battery = batteryEntity
|
||||
? this.hass.states[batteryEntity]
|
||||
: undefined;
|
||||
return battery
|
||||
? html`
|
||||
${battery.state}%
|
||||
<ha-state-icon
|
||||
.hass=${this.hass!}
|
||||
.stateObj=${battery}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: html`
|
||||
n/a
|
||||
`;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>
|
||||
<ha-data-table
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._devices(
|
||||
this.devices,
|
||||
this.entries,
|
||||
this.entities,
|
||||
this.areas,
|
||||
this.domain
|
||||
).map((device: DeviceRowData) => {
|
||||
// We don't need a lot of this data for mobile view, but kept it for filtering...
|
||||
const data: DataTabelRowData = {
|
||||
device_name: device.name,
|
||||
id: device.id,
|
||||
manufacturer: device.manufacturer,
|
||||
model: device.model,
|
||||
area: device.area,
|
||||
integration: device.integration,
|
||||
};
|
||||
if (this.narrow) {
|
||||
data.device = device;
|
||||
return data;
|
||||
}
|
||||
data.battery = device.battery_entity;
|
||||
return data;
|
||||
})}
|
||||
@row-click=${this._handleRowClicked}
|
||||
></ha-data-table>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _batteryEntity(device, entities): string | undefined {
|
||||
const batteryEntity = entities.find(
|
||||
(entity) =>
|
||||
entity.device_id === device.id &&
|
||||
this.hass.states[entity.entity_id] &&
|
||||
this.hass.states[entity.entity_id].attributes.device_class === "battery"
|
||||
);
|
||||
|
||||
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-config-devices-dashboard": HaConfigDeviceDashboard;
|
||||
}
|
||||
}
|
127
src/panels/config/devices/ha-config-devices.ts
Normal file
127
src/panels/config/devices/ha-config-devices.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import "@polymer/app-route/app-route";
|
||||
|
||||
import "./ha-config-devices-dashboard";
|
||||
import "./ha-config-device-page";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import {
|
||||
subscribeAreaRegistry,
|
||||
AreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../layouts/hass-router-page";
|
||||
import { property, customElement, PropertyValues } from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
|
||||
@customElement("ha-config-devices")
|
||||
class HaConfigDevices extends HassRouterPage {
|
||||
@property() public hass!: HomeAssistant;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ha-config-devices-dashboard",
|
||||
},
|
||||
device: {
|
||||
tag: "ha-config-device-page",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@property() private _configEntries?: ConfigEntry[];
|
||||
@property() private _entityRegistryEntries?: EntityRegistryEntry[];
|
||||
@property() private _deviceRegistryEntries?: DeviceRegistryEntry[];
|
||||
@property() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
private _unsubs?: UnsubscribeFunc[];
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (!this._unsubs && changedProps.has("hass")) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
|
||||
if (this._currentPage === "dashboard") {
|
||||
pageEl.domain = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "device") {
|
||||
pageEl.deviceId = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
pageEl.entities = this._entityRegistryEntries;
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.devices = this._deviceRegistryEntries;
|
||||
pageEl.areas = this._areas;
|
||||
pageEl.narrow = this.narrow;
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries.sort((conf1, conf2) =>
|
||||
compare(conf1.title, conf2.title)
|
||||
);
|
||||
});
|
||||
if (this._unsubs) {
|
||||
return;
|
||||
}
|
||||
this._unsubs = [
|
||||
subscribeAreaRegistry(this.hass.connection, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
this._entityRegistryEntries = entries;
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection, (entries) => {
|
||||
this._deviceRegistryEntries = entries;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-devices": HaConfigDevices;
|
||||
}
|
||||
}
|
@ -6,23 +6,23 @@ import "@polymer/paper-listbox/paper-listbox";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../layouts/hass-subpage";
|
||||
|
||||
import { EventsMixin } from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
import computeStateName from "../../../../common/entity/compute_state_name";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import { compare } from "../../../../common/string/compare";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/entity/state-badge";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../../../data/device_registry";
|
||||
import { subscribeAreaRegistry } from "../../../../data/area_registry";
|
||||
} from "../../../data/device_registry";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import {
|
||||
loadDeviceRegistryDetailDialog,
|
||||
showDeviceRegistryDetailDialog,
|
||||
} from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||
} from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
function computeEntityName(hass, entity) {
|
||||
if (entity.name) return entity.name;
|
||||
@ -83,11 +83,13 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
</style>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<div class="name">[[_deviceName(device)]]</div>
|
||||
<paper-icon-button
|
||||
icon="hass:settings"
|
||||
on-click="_gotoSettings"
|
||||
></paper-icon-button>
|
||||
<template is="dom-if" if="[[!hideSettings]]">
|
||||
<div class="name">[[_deviceName(device)]]</div>
|
||||
<paper-icon-button
|
||||
icon="hass:settings"
|
||||
on-click="_gotoSettings"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="info">
|
||||
@ -154,6 +156,7 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
hideSettings: { type: Boolean, value: false },
|
||||
_childDevices: {
|
||||
type: Array,
|
||||
computed: "_computeChildDevices(device, devices)",
|
@ -48,6 +48,11 @@ class HaPanelConfig extends HassRouterPage {
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core"),
|
||||
},
|
||||
devices: {
|
||||
tag: "ha-config-devices",
|
||||
load: () =>
|
||||
import(/* webpackChunkName: "panel-config-devices" */ "./devices/ha-config-devices"),
|
||||
},
|
||||
server_control: {
|
||||
tag: "ha-config-server-control",
|
||||
load: () =>
|
||||
|
@ -5,7 +5,7 @@ import "../../../../layouts/hass-error-screen";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import { compare } from "../../../../common/string/compare";
|
||||
|
||||
import "./ha-device-card";
|
||||
import "../../devices/ha-device-card";
|
||||
import "./ha-ce-entities-card";
|
||||
import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { property, LitElement, CSSResult, css, html } from "lit-element";
|
||||
@ -106,6 +106,7 @@ class HaConfigEntryPage extends LitElement {
|
||||
icon="hass:delete"
|
||||
@click=${this._removeEntry}
|
||||
></paper-icon-button>
|
||||
|
||||
<div class="content">
|
||||
${configEntryDevices.length === 0 && noDeviceEntities.length === 0
|
||||
? html`
|
||||
|
@ -1,235 +0,0 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/entity/ha-state-icon";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../resources/ha-style";
|
||||
import "../../../components/ha-icon-next";
|
||||
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../ha-config-section";
|
||||
import { EventsMixin } from "../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
loadConfigFlowDialog,
|
||||
showConfigFlowDialog,
|
||||
} from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { localizeConfigFlowTitle } from "../../../data/config_flow";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaConfigManagerDashboard extends LocalizeMixin(
|
||||
EventsMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
mwc-button {
|
||||
top: 3px;
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
.config-entry-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
ha-state-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.configured a {
|
||||
color: var(--primary-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
ha-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
ha-fab[rtl][is-wide] {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-subpage
|
||||
header="[[localize('ui.panel.config.integrations.caption')]]"
|
||||
>
|
||||
<template is="dom-if" if="[[progress.length]]">
|
||||
<ha-config-section>
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.integrations.discovered')]]</span
|
||||
>
|
||||
<ha-card>
|
||||
<template is="dom-repeat" items="[[progress]]">
|
||||
<div class="config-entry-row">
|
||||
<paper-item-body>
|
||||
[[_computeActiveFlowTitle(localize, item)]]
|
||||
</paper-item-body>
|
||||
<mwc-button on-click="_continueFlow"
|
||||
>[[localize('ui.panel.config.integrations.configure')]]</mwc-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</template>
|
||||
|
||||
<ha-config-section class="configured">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.integrations.configured')]]</span
|
||||
>
|
||||
<ha-card>
|
||||
<template is="dom-if" if="[[!entries.length]]">
|
||||
<div class="config-entry-row">
|
||||
<paper-item-body two-line>
|
||||
<div>[[localize('ui.panel.config.integrations.none')]]</div>
|
||||
</paper-item-body>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[entries]]">
|
||||
<a href="/config/integrations/config_entry/[[item.entry_id]]">
|
||||
<paper-item>
|
||||
<paper-item-body two-line>
|
||||
<div>
|
||||
[[_computeIntegrationTitle(localize, item.domain)]]:
|
||||
[[item.title]]
|
||||
</div>
|
||||
<div secondary>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[_computeConfigEntryEntities(hass, item, entities)]]"
|
||||
>
|
||||
<span>
|
||||
<ha-state-icon state-obj="[[item]]"></ha-state-icon>
|
||||
<paper-tooltip position="bottom"
|
||||
>[[_computeStateName(item)]]</paper-tooltip
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</a>
|
||||
</template>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-fab
|
||||
icon="hass:plus"
|
||||
title="[[localize('ui.panel.config.integrations.new')]]"
|
||||
on-click="_createFlow"
|
||||
is-wide$="[[isWide]]"
|
||||
rtl$="[[rtl]]"
|
||||
></ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
|
||||
/**
|
||||
* Existing entries.
|
||||
*/
|
||||
entries: Array,
|
||||
|
||||
/**
|
||||
* Entity Registry entries.
|
||||
*/
|
||||
entities: Array,
|
||||
|
||||
/**
|
||||
* Current flows that are in progress and have not been started by a user.
|
||||
* For example, can be discovered devices that require more config.
|
||||
*/
|
||||
progress: Array,
|
||||
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
loadConfigFlowDialog();
|
||||
}
|
||||
|
||||
_createFlow() {
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
||||
});
|
||||
}
|
||||
|
||||
_continueFlow(ev) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: ev.model.item.flow_id,
|
||||
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
||||
});
|
||||
}
|
||||
|
||||
_computeIntegrationTitle(localize, integration) {
|
||||
return localize(`component.${integration}.config.title`);
|
||||
}
|
||||
|
||||
_computeActiveFlowTitle(localize, flow) {
|
||||
return localizeConfigFlowTitle(localize, flow);
|
||||
}
|
||||
|
||||
_computeConfigEntryEntities(hass, configEntry, entities) {
|
||||
if (!entities) {
|
||||
return [];
|
||||
}
|
||||
const states = [];
|
||||
entities.forEach((entity) => {
|
||||
if (
|
||||
entity.config_entry_id === configEntry.entry_id &&
|
||||
entity.entity_id in hass.states
|
||||
) {
|
||||
states.push(hass.states[entity.entity_id]);
|
||||
}
|
||||
});
|
||||
return states;
|
||||
}
|
||||
|
||||
_computeStateName(stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-entries-dashboard", HaConfigManagerDashboard);
|
237
src/panels/config/integrations/ha-config-entries-dashboard.ts
Normal file
237
src/panels/config/integrations/ha-config-entries-dashboard.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/iron-icon/iron-icon";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/entity/ha-state-icon";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../resources/ha-style";
|
||||
import "../../../components/ha-icon";
|
||||
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../ha-config-section";
|
||||
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
loadConfigFlowDialog,
|
||||
showConfigFlowDialog,
|
||||
} from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { localizeConfigFlowTitle } from "../../../data/config_flow";
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
property,
|
||||
customElement,
|
||||
css,
|
||||
CSSResult,
|
||||
} from "lit-element";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
|
||||
@customElement("ha-config-entries-dashboard")
|
||||
export class HaConfigManagerDashboard extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide = false;
|
||||
|
||||
@property() private entries = [];
|
||||
|
||||
/**
|
||||
* Entity Registry entries.
|
||||
*/
|
||||
@property() private entities: 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 progress = [];
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
loadConfigFlowDialog();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
header=${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||
>
|
||||
${this.progress.length
|
||||
? html`
|
||||
<ha-config-section>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.discovered"
|
||||
)}</span
|
||||
>
|
||||
<ha-card>
|
||||
${this.progress.map(
|
||||
(flow) => html`
|
||||
<div class="config-entry-row">
|
||||
<paper-item-body>
|
||||
${localizeConfigFlowTitle(this.hass.localize, flow)}
|
||||
</paper-item-body>
|
||||
<mwc-button @click=${this._continueFlow}
|
||||
>${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.entities.length
|
||||
? this.entries.map(
|
||||
(item: any, idx) => html`
|
||||
<a
|
||||
href="/config/integrations/config_entry/${item.entry_id}"
|
||||
>
|
||||
<paper-item data-index=${idx}>
|
||||
<paper-item-body two-line>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`component.${item.domain}.config.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></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"
|
||||
title=${this.hass.localize("ui.panel.config.integrations.new")}
|
||||
@click=${this._createFlow}
|
||||
?rtl=${computeRTL(this.hass!)}
|
||||
?isWide=${this.isWide}
|
||||
></ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _createFlow() {
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"),
|
||||
});
|
||||
}
|
||||
|
||||
private _continueFlow(ev) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: ev.model.item.flow_id,
|
||||
dialogClosedCallback: () => fireEvent(this, "hass-reload-entries"),
|
||||
});
|
||||
}
|
||||
|
||||
private _getEntities(configEntry: ConfigEntry): HassEntity[] {
|
||||
if (!this.entities) {
|
||||
return [];
|
||||
}
|
||||
const states: HassEntity[] = [];
|
||||
this.entities.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;
|
||||
}
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
mwc-button {
|
||||
top: 3px;
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
.config-entry-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
}
|
||||
ha-icon {
|
||||
cursor: pointer;
|
||||
margin: 8px;
|
||||
}
|
||||
.configured a {
|
||||
color: var(--primary-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
ha-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
ha-fab[rtl] {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
ha-fab[rtl][is-wide] {
|
||||
bottom: 24px;
|
||||
right: auto;
|
||||
left: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@ -137,7 +137,7 @@ documentContainer.innerHTML = `<custom-style>
|
||||
--mdc-theme-surface: var(--paper-card-background-color, var(--card-background-color));
|
||||
|
||||
/* mwc text styles */
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-on-primary: var(--text-primary-color);
|
||||
--mdc-theme-on-secondary: var(--text-primary-color);
|
||||
--mdc-theme-on-surface: var(--primary-text-color);
|
||||
}
|
||||
|
@ -889,6 +889,10 @@
|
||||
"description_not_login": "Not logged in",
|
||||
"description_features": "Control away from home, integrate with Alexa and Google Assistant."
|
||||
},
|
||||
"devices": {
|
||||
"caption": "Devices",
|
||||
"description": "Manage connected devices"
|
||||
},
|
||||
"entity_registry": {
|
||||
"caption": "Entity Registry",
|
||||
"description": "Overview of all known entities.",
|
||||
@ -920,7 +924,7 @@
|
||||
},
|
||||
"integrations": {
|
||||
"caption": "Integrations",
|
||||
"description": "Manage connected devices and services",
|
||||
"description": "Manage and setup integrations",
|
||||
"discovered": "Discovered",
|
||||
"configured": "Configured",
|
||||
"new": "Set up a new integration",
|
||||
|
Loading…
x
Reference in New Issue
Block a user