mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Add search to network visualization graphs (#29908)
* Add search highlight to ZHA graph
* Move logic upstream and extend search to zwave and bluetooth
* Move search down to avoid collisions with graph legend
* Fix mobile; simplify code
* Apply highlights directly on search callback
* Revert "Move search down to avoid collisions with graph legend"
This reverts commit 4578aec9c3.
* Move legend down
* Make search bar shrink to avoid overlapping buttons
* Move search bar to topbar on mobile
* Fix inset
* Fix small controlls position
* Apply suggestion from @MindFreeze
---------
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -280,18 +280,23 @@ export class HaChartBase extends LitElement {
|
||||
<div class="chart"></div>
|
||||
</div>
|
||||
${this._renderLegend()}
|
||||
<div class="chart-controls ${classMap({ small: this.smallControls })}">
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
<div class="top-controls ${classMap({ small: this.smallControls })}">
|
||||
<slot name="search"></slot>
|
||||
<div
|
||||
class="chart-controls ${classMap({ small: this.smallControls })}"
|
||||
>
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1116,16 +1121,35 @@ export class HaChartBase extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-controls {
|
||||
.top-controls {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 4px;
|
||||
top: var(--ha-space-4);
|
||||
inset-inline-start: var(--ha-space-4);
|
||||
inset-inline-end: var(--ha-space-1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ha-space-2);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
::slotted([slot="search"]) {
|
||||
flex: 1 1 250px;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
margin-inline-start: auto;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.top-controls.small {
|
||||
top: 0;
|
||||
}
|
||||
.chart-controls.small {
|
||||
top: 0;
|
||||
flex-direction: row;
|
||||
}
|
||||
.chart-controls ha-icon-button,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
@@ -76,8 +78,20 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
params: TopLevelFormatterParams
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Optional callback that returns additional searchable strings for a node.
|
||||
* These are matched against the search filter in addition to the node's name and context.
|
||||
*/
|
||||
@property({ attribute: false }) public searchableAttributes?: (
|
||||
nodeId: string
|
||||
) => string[];
|
||||
|
||||
@property({ attribute: false }) public searchFilter = "";
|
||||
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _highlightedNodes?: Set<string>;
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled = true;
|
||||
@@ -117,6 +131,9 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const hasHighlightedNodes =
|
||||
this._highlightedNodes && this._highlightedNodes.size > 0;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
@@ -124,12 +141,14 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
this._physicsEnabled,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile
|
||||
isMobile,
|
||||
hasHighlightedNodes
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
>
|
||||
<slot name="search" slot="search"></slot>
|
||||
<slot name="button" slot="button"></slot>
|
||||
<ha-icon-button
|
||||
slot="button"
|
||||
@@ -165,7 +184,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
...category,
|
||||
icon: category.symbol,
|
||||
})),
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
},
|
||||
dataZoom: {
|
||||
type: "inside",
|
||||
@@ -175,13 +194,56 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
deepEqual
|
||||
);
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("searchFilter")) {
|
||||
const filter = this.searchFilter;
|
||||
if (!filter) {
|
||||
this._highlightedNodes = undefined;
|
||||
} else {
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
const matchingIds = new Set<string>();
|
||||
for (const node of this.data.nodes) {
|
||||
if (this._nodeMatchesFilter(node, lowerFilter)) {
|
||||
matchingIds.add(node.id);
|
||||
}
|
||||
}
|
||||
this._highlightedNodes = matchingIds;
|
||||
}
|
||||
this._applyHighlighting();
|
||||
this._updateMouseoverHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private _nodeMatchesFilter(node: NetworkNode, lowerFilter: string): boolean {
|
||||
if (node.name?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.context?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.id?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (this.searchableAttributes) {
|
||||
const extraValues = this.searchableAttributes(node.id);
|
||||
for (const value of extraValues) {
|
||||
if (value?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getSeries = memoizeOne(
|
||||
(
|
||||
data: NetworkData,
|
||||
physicsEnabled: boolean,
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean,
|
||||
isMobile: boolean
|
||||
isMobile: boolean,
|
||||
hasHighlightedNodes?: boolean
|
||||
) => ({
|
||||
id: "network",
|
||||
type: "graph",
|
||||
@@ -214,7 +276,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
focus: hasHighlightedNodes ? "self" : isMobile ? "none" : "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
@@ -362,6 +424,68 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _applyHighlighting() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
// Reset all nodes to normal opacity first
|
||||
chart.dispatchAction({ type: "downplay" });
|
||||
|
||||
const highlighted = this._highlightedNodes;
|
||||
if (!highlighted || highlighted.size === 0) {
|
||||
return;
|
||||
}
|
||||
const dataIndices: number[] = [];
|
||||
this.data.nodes.forEach((node, index) => {
|
||||
if (highlighted.has(node.id)) {
|
||||
dataIndices.push(index);
|
||||
}
|
||||
});
|
||||
if (dataIndices.length > 0) {
|
||||
chart.dispatchAction({ type: "highlight", dataIndex: dataIndices });
|
||||
}
|
||||
}
|
||||
|
||||
private _emphasisGuardHandler?: () => void;
|
||||
|
||||
private _updateMouseoverHandler() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When there are highlighted nodes, re-apply highlighting on hover
|
||||
// and mouseout to prevent hover from overriding the search state
|
||||
if (this._highlightedNodes && this._highlightedNodes.size > 0) {
|
||||
if (this._emphasisGuardHandler) {
|
||||
// Guard already set
|
||||
return;
|
||||
}
|
||||
this._emphasisGuardHandler = () => {
|
||||
this._applyHighlighting();
|
||||
};
|
||||
chart.on("mouseover", this._emphasisGuardHandler);
|
||||
chart.on("mouseout", this._emphasisGuardHandler);
|
||||
} else {
|
||||
if (!this._emphasisGuardHandler) {
|
||||
return;
|
||||
}
|
||||
chart.off("mouseover", this._emphasisGuardHandler);
|
||||
chart.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._emphasisGuardHandler) {
|
||||
this._baseChart?.chart?.off("mouseover", this._emphasisGuardHandler);
|
||||
this._baseChart?.chart?.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _togglePhysics() {
|
||||
this._saveNodePositions();
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { relativeTime } from "../../../../../common/datetime/relative_time";
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/search-input-outlined";
|
||||
import type {
|
||||
BluetoothDeviceData,
|
||||
BluetoothScannersDetails,
|
||||
@@ -60,6 +61,8 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
|
||||
@state() private _sourceDevices: Record<string, DeviceRegistryEntry> = {};
|
||||
|
||||
@state() private _searchFilter = "";
|
||||
|
||||
private _unsub_advertisements?: UnsubscribeFunc;
|
||||
|
||||
private _unsub_scanners?: UnsubscribeFunc;
|
||||
@@ -126,16 +129,56 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
)}
|
||||
back-path="/config/bluetooth/dashboard"
|
||||
>
|
||||
${this.narrow
|
||||
? html`<div slot="header">
|
||||
<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
.searchFilter=${this._searchFilter}
|
||||
.data=${this._formatNetworkData(this._data, this._scanners)}
|
||||
.searchableAttributes=${this._getSearchableAttributes}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-network-graph>
|
||||
>
|
||||
${!this.narrow
|
||||
? html`<search-input-outlined
|
||||
slot="search"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>`
|
||||
: nothing}
|
||||
</ha-network-graph>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getSearchableAttributes = (nodeId: string): string[] => {
|
||||
const attributes: string[] = [];
|
||||
const device = this._sourceDevices[nodeId];
|
||||
if (device?.manufacturer) {
|
||||
attributes.push(device.manufacturer);
|
||||
}
|
||||
if (device?.model) {
|
||||
attributes.push(device.model);
|
||||
}
|
||||
const scanner = this._scanners[nodeId];
|
||||
if (scanner?.name) {
|
||||
attributes.push(scanner.name);
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
this._searchFilter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _getRssiColorVar = memoizeOne((rssi: number): string => {
|
||||
for (const [threshold, colorVar] of RSSI_COLOR_THRESHOLDS) {
|
||||
if (rssi > threshold) {
|
||||
@@ -347,6 +390,13 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
ha-network-graph {
|
||||
height: 100%;
|
||||
}
|
||||
[slot="header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
search-input-outlined {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import type {
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
import type { NetworkData } from "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/search-input-outlined";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { fetchDevices, refreshTopology } from "../../../../../data/zha";
|
||||
@@ -38,6 +39,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@state()
|
||||
private _devices: ZHADevice[] = [];
|
||||
|
||||
@state() private _searchFilter = "";
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
@@ -55,12 +58,31 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<div slot="header">
|
||||
<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
.searchFilter=${this._searchFilter}
|
||||
.data=${this._networkData}
|
||||
.searchableAttributes=${this._getSearchableAttributes}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`<search-input-outlined
|
||||
slot="search"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
slot="button"
|
||||
class="refresh-button"
|
||||
@@ -84,6 +106,34 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _getSearchableAttributes = (nodeId: string): string[] => {
|
||||
const device = this._devices.find((d) => d.ieee === nodeId);
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
const attributes: string[] = [];
|
||||
if (device.user_given_name) {
|
||||
attributes.push(device.user_given_name);
|
||||
}
|
||||
if (device.manufacturer) {
|
||||
attributes.push(device.manufacturer);
|
||||
}
|
||||
if (device.model) {
|
||||
attributes.push(device.model);
|
||||
}
|
||||
if (device.device_type) {
|
||||
attributes.push(device.device_type);
|
||||
}
|
||||
if (device.nwk != null) {
|
||||
attributes.push(formatAsPaddedHex(device.nwk));
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
this._searchFilter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data, name } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
@@ -154,6 +204,13 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
ha-network-graph {
|
||||
height: 100%;
|
||||
}
|
||||
[slot="header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
search-input-outlined {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/search-input-outlined";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type {
|
||||
ZWaveJSNodeStatisticsUpdatedMessage,
|
||||
@@ -49,6 +50,8 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _devices: Record<string, DeviceRegistryEntry> = {};
|
||||
|
||||
@state() private _searchFilter = "";
|
||||
|
||||
public hassSubscribe() {
|
||||
const devices = Object.values(this.hass.devices).filter((device) =>
|
||||
device.config_entries.some((entry) => entry === this.configEntryId)
|
||||
@@ -80,15 +83,35 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
back-path="/config/zwave_js/dashboard?config_entry=${this
|
||||
.configEntryId}"
|
||||
>
|
||||
${this.narrow
|
||||
? html`<div slot="header">
|
||||
<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
.searchFilter=${this._searchFilter}
|
||||
.data=${this._getNetworkData(
|
||||
this._nodeStatuses,
|
||||
this._nodeStatistics
|
||||
)}
|
||||
.searchableAttributes=${this._getSearchableAttributes}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-network-graph>
|
||||
>
|
||||
${!this.narrow
|
||||
? html`<search-input-outlined
|
||||
slot="search"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._searchFilter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input-outlined>`
|
||||
: nothing}
|
||||
</ha-network-graph>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -105,6 +128,31 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
this._nodeStatuses = nodeStatuses;
|
||||
}
|
||||
|
||||
private _getSearchableAttributes = (nodeId: string): string[] => {
|
||||
const device = this._devices[Number(nodeId)];
|
||||
const nodeStatus = this._nodeStatuses[Number(nodeId)];
|
||||
const attributes: string[] = [];
|
||||
if (device?.manufacturer) {
|
||||
attributes.push(device.manufacturer);
|
||||
}
|
||||
if (device?.model) {
|
||||
attributes.push(device.model);
|
||||
}
|
||||
if (nodeStatus) {
|
||||
const statusText = this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_status.${nodeStatus.status}` as any
|
||||
);
|
||||
if (statusText) {
|
||||
attributes.push(statusText);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
this._searchFilter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
@@ -330,6 +378,13 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
ha-network-graph {
|
||||
height: 100%;
|
||||
}
|
||||
[slot="header"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
search-input-outlined {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user