Skip to content

Commit

Permalink
fix: Fix slow opening nodes and other issues with select2
Browse files Browse the repository at this point in the history
Fixes #827, fixes #839
  • Loading branch information
zachowj committed Jan 30, 2023
1 parent 6a1c7df commit ec97ddd
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 73 deletions.
86 changes: 46 additions & 40 deletions src/editor/components/EntitySelector.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { HassEntity } from 'home-assistant-js-websocket';

import { byPropertiesOf } from '../../helpers/sort';
import { openEntityFilter } from '../editors/entity-filter';
import { getEntities, getUiSettings } from '../haserver';
import { disableSelect2OpenOnRemove } from '../utils';
import {
createCustomIdListByProperty,
createSelect2Options,
isSelect2Initialized,
Select2Data,
Select2AjaxEndpoints,
Tags,
} from './select2';

Expand All @@ -18,17 +14,20 @@ export default class EntitySelector {
#$filterType: JQuery<HTMLElement>;
#$filterButton: JQuery<HTMLElement>;
#entityId: string | string[];
#select2Data: Select2Data[];
#serverId?: string;

constructor({
filterTypeSelector,
entityId,
serverId,
}: {
filterTypeSelector: string;
entityId: string | string[];
serverId?: string;
}) {
this.#$filterType = $(filterTypeSelector);
this.#entityId = entityId;
this.#serverId = serverId;

this.#buildElements();
this.init();
Expand All @@ -55,13 +54,40 @@ export default class EntitySelector {
return id;
}

#generateEntityList() {
const entities = getEntities();
const ids = !Array.isArray(this.#entityId)
? [this.#entityId]
: this.#entityId;

const options = ids.reduce((acc: JQuery<HTMLElement>[], id: string) => {
const text =
getUiSettings().entitySelector === 'id'
? id
: entities.find((e) => e.entity_id === id)?.attributes
.friendly_name ?? id;
acc.push(
$('<option />', {
value: id,
text,
selected: true,
})
);
return acc;
}, []);

return options;
}

#buildElements() {
const $formRow = this.#$filterType.parent();
const $div = $('<div />', {
class: 'ha-entity-selector',
});
const filterType = this.#$filterType.val() as string;
this.#$select = $(`<select />`, {
id: 'ha-entity-selector',
multiple: filterType === 'list',
});
this.#$filter = $('<input />', {
type: 'text',
Expand All @@ -75,10 +101,15 @@ export default class EntitySelector {
$div.append([this.#$select, this.#$filter, this.#$filterButton])
);
this.#$filterType.appendTo($div);

// Populate select with selected ids
if (this.#entityId !== '') {
this.#$select.append(this.#generateEntityList());
}
}

init() {
this.#$filterType.on('change', this.#showHide.bind(this));
this.#$filterType.on('change', this.#showHideFilterOptions.bind(this));

this.#$filterButton.on('click', () => {
openEntityFilter({
Expand All @@ -91,12 +122,11 @@ export default class EntitySelector {
});
});

this.#generateEntityList();
this.#initSelect2(Array.isArray(this.#entityId));
this.#$filter.val(this.#entityId);
}

#showHide() {
#showHideFilterOptions() {
const filter = this.#$filterType.val() as string;
switch (filter) {
case 'exact':
Expand All @@ -114,41 +144,17 @@ export default class EntitySelector {
}
}

#generateEntityList() {
const entities = Object.values(getEntities());
const entityIds = !this.#select2Data
? Array.isArray(this.#entityId)
? this.#entityId
: [this.#entityId]
: this.#$select.select2('data').map((e) => e.id);

const data = entities
.map((e): Select2Data => {
return {
id: e.entity_id,
text: e.attributes.friendly_name ?? e.entity_id,
title: e.entity_id,
selected: entityIds.includes(e.entity_id),
};
})
.sort(byPropertiesOf<Select2Data>(['text']))
.concat(
createCustomIdListByProperty<HassEntity>(entityIds, entities, {
property: 'entity_id',
includeUnknownIds: true,
})
);
this.#select2Data = data;
}

#initSelect2(multiple = false) {
this.#$select
.select2(
createSelect2Options({
data: this.#select2Data.filter((e) => e.id?.length > 0),
tags: Tags.Custom,
multiple,
displayIds: getUiSettings().entitySelector === 'id',
ajax: {
endpoint: Select2AjaxEndpoints.Entities,
serverId: this.#serverId,
},
})
)
.maximizeSelect2Height();
Expand All @@ -163,9 +169,9 @@ export default class EntitySelector {
}
}

serverChanged() {
serverChanged(serverId: string) {
this.#serverId = serverId;
this.#$select.empty();
this.#generateEntityList();
this.#showHide();
this.#showHideFilterOptions();
}
}
29 changes: 25 additions & 4 deletions src/editor/components/select2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { containsMustache, isNodeRedEnvVar } from '../../helpers/utils';
import { isjQuery } from '../utils';

export enum Tags {
Any,
Custom,
None,
Any = 'any',
Custom = 'custom',
None = 'none',
}

export enum Select2AjaxEndpoints {
Entities = 'entitiesSelect2',
}

export interface Select2Data {
Expand All @@ -16,6 +20,11 @@ export interface Select2Data {
title?: string;
}

interface Select2AjaxOptions {
endpoint: Select2AjaxEndpoints;
serverId?: string;
}

export const select2DefaultOptions: Options = {
theme: 'nodered',
templateResult: (item: any) => {
Expand Down Expand Up @@ -53,12 +62,14 @@ export const createSelect2Options = ({
customTags = [],
displayIds = false,
data,
ajax,
}: {
multiple?: boolean;
tags?: Tags;
customTags?: string[];
displayIds?: boolean;
data?: Select2Data[];
ajax?: Select2AjaxOptions;
}) => {
const opts = {
...select2DefaultOptions,
Expand All @@ -67,6 +78,13 @@ export const createSelect2Options = ({
dropdownAutoWidth: true,
};

if (ajax?.serverId) {
opts.ajax = {
url: `homeassistant/${ajax.endpoint}/${ajax.serverId}`,
dataType: 'json',
delay: 250,
};
}
opts.tags = tags === Tags.Any || tags === Tags.Custom;

if (tags === Tags.Custom) {
Expand All @@ -91,12 +109,15 @@ export const createSelect2Options = ({
text: params.term,
};
}

return null;
};
}

if (displayIds) {
opts.templateSelection = (selection) => {
return selection.id.toString();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return selection.id!.toString();
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/editor/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function sortFriendlyName(a: Record<string, any>, b: Record<string, any>) {
return aName.localeCompare(bName);
}

export function getAreaNameById($serverId: string, areaId: string) {
export function getAreaNameById($serverId: string, areaId?: string) {
const areas = getAreas($serverId);
if (areaId && areas?.length) {
const area = areas.find((a) => a.area_id === areaId);
Expand Down
9 changes: 6 additions & 3 deletions src/editor/haserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function setDefault() {
export function init(
n: EditorNodeInstance<HassNodeProperties>,
server: string,
onChange?: () => void
onChange?: (serverId: string) => void
) {
$server = $(server);
node = n;
Expand All @@ -40,10 +40,13 @@ export function init(
setTimeout(() => {
$server.on('change', () => {
serverId = $server.val() as string;
onChange?.();
onChange?.(serverId);
});
}, 500);
}
export function getSelectedServerId() {
return serverId;
}

export function autocomplete(type: string, callback: (items: any) => void) {
// If a server is selected populate drop downs
Expand Down Expand Up @@ -112,7 +115,7 @@ export const getAreaById = (areaId: string): HassArea | undefined => {
}
};

export const getAreaNameById = (areaId: string): string => {
export const getAreaNameById = (areaId?: string): string => {
return haData.getAreaNameById(serverId, areaId);
};

Expand Down
90 changes: 90 additions & 0 deletions src/helpers/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { RED } from '../globals';
import Websocket from '../homeAssistant/Websocket';
import {
HassAreas,
HassDevices,
HassEntityRegistryEntry,
} from '../types/home-assistant';

export function getAreaNameByEntityId(
entityId: string,
areas: HassAreas,
devices: HassDevices,
entities: HassEntityRegistryEntry[]
) {
const areaId = getAreaIdByEntityId(entityId, areas, devices, entities);
return getAreaNameByAreaId(areaId, areas);
}

function getAreaNameByAreaId(areaId: string | null, areas: HassAreas) {
if (areaId && areas?.length) {
const area = areas.find((a) => a.area_id === areaId);
if (area) {
return area.name;
}
}

return RED._('ha-device.ui.no_area');
}

function getAreaIdByEntityId(
entityId: string,
areas: HassAreas,
devices: HassDevices,
entities: HassEntityRegistryEntry[]
) {
const entity = getEntityById(entityId, entities);

if (entity?.area_id) {
return entity.area_id;
}

if (areas?.length) {
const area = areas.find((area) => {
const device = getDeviceByEntityId(entityId, devices, entities);
return device?.area_id === area.area_id;
});

if (area) {
return area.area_id;
}
}

return null;
}

function getDeviceByEntityId(
entityId: string,
devices: HassDevices,
entities: HassEntityRegistryEntry[]
) {
const entity = getEntityById(entityId, entities);

if (entity?.device_id && devices?.length) {
const device = devices.find((device) => entity.device_id === device.id);
if (device) {
return device;
}
}

return null;
}

function getEntityById(
entityId: string,
entities: HassEntityRegistryEntry[]
): HassEntityRegistryEntry | null {
if (entityId && entities) {
return entities.find((e) => e.entity_id === entityId) ?? null;
}

return null;
}

export function getRegistryData(HassWS: Websocket) {
return {
areas: HassWS.getAreas(),
devices: HassWS.getDevices(),
entities: HassWS.getEntities(),
};
}
8 changes: 8 additions & 0 deletions src/homeAssistant/Websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ export default class Websocket {
this.#emitEvent('ha_client:close');
}

getAreas(): HassAreas {
return this.areas;
}

getDevices(): HassDevices {
return this.devices;
}
Expand Down Expand Up @@ -580,6 +584,10 @@ export default class Websocket {
return results.extra_fields;
}

getEntities() {
return this.entities;
}

getStates(): HassEntities;
getStates(entityId?: string): HassEntity | null;
getStates(entityId?: unknown): unknown {
Expand Down
Loading

0 comments on commit ec97ddd

Please sign in to comment.