thingsboard/ui-ngx/src/app/core/services/item-buffer.service.ts
2024-12-06 17:07:15 +02:00

582 lines
21 KiB
TypeScript

///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models';
import {
Datasource,
DatasourceType,
TargetDeviceType,
Widget,
WidgetPosition,
WidgetSize,
widgetType
} from '@shared/models/widget.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils';
import { UtilsService } from '@core/services/utils.service';
import { Observable, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleChainImport } from '@shared/models/rule-chain.models';
import { Filter, FilterInfo, Filters, FiltersInfo } from '@shared/models/query/query.models';
const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
const RULE_NODES = 'rule_nodes';
const RULE_CHAIN_IMPORT = 'rule_chain_import';
export interface WidgetItem {
widget: Widget;
aliasesInfo: AliasesInfo;
filtersInfo: FiltersInfo;
originalSize: WidgetSize;
originalColumns: number;
}
export interface WidgetReference {
dashboardId: string;
sourceState: string;
sourceLayout: DashboardLayoutId;
widgetId: string;
originalSize: WidgetSize;
originalColumns: number;
breakpoint: string;
}
export interface RuleNodeConnection {
isInputSource: boolean;
fromIndex: number;
toIndex: number;
label: string;
labels: string[];
}
export interface RuleNodesReference {
nodes: FcRuleNode[];
connections: RuleNodeConnection[];
originX?: number;
originY?: number;
}
@Injectable({
providedIn: 'root'
})
export class ItemBufferService {
private namespace = 'tbBufferStore';
private delimiter = '.';
constructor(private dashboardUtils: DashboardUtilsService,
private ruleChainService: RuleChainService,
private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: BreakpointId): WidgetItem {
const aliasesInfo: AliasesInfo = {
datasourceAliases: {},
targetDeviceAlias: null
};
const filtersInfo: FiltersInfo = {
datasourceFilters: {}
};
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) {
let entityAlias: EntityAlias;
if (datasources) {
for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[i];
if ((datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount ||
datasource.type === DatasourceType.alarmCount) && datasource.entityAliasId) {
entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
}
}
}
}
if (widget.config.targetDevice?.type === TargetDeviceType.entity && widget.config.targetDevice.entityAliasId) {
const targetDeviceAliasId = widget.config.targetDevice.entityAliasId;
entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId];
if (entityAlias) {
aliasesInfo.targetDeviceAlias = this.prepareAliasInfo(entityAlias);
}
}
}
if (widget.config && dashboard.configuration
&& dashboard.configuration.filters) {
let filter: Filter;
if (datasources) {
for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[i];
if ((datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount ||
datasource.type === DatasourceType.alarmCount) && datasource.filterId) {
filter = dashboard.configuration.filters[datasource.filterId];
if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);
}
}
}
}
}
return {
widget,
aliasesInfo,
filtersInfo,
originalSize,
originalColumns
};
}
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId) {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem);
}
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: BreakpointId): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference);
}
public hasWidget(): boolean {
return this.storeHas(WIDGET_ITEM);
}
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId, breakpoint: string): boolean {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout || widgetReference.breakpoint !== breakpoint)
&& dashboard.configuration.widgets[widgetReference.widgetId]) {
return true;
}
}
}
return false;
}
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
if (widgetItem) {
const widget = widgetItem.widget;
const aliasesInfo = widgetItem.aliasesInfo;
const filtersInfo = widgetItem.filtersInfo;
const originalColumns = widgetItem.originalColumns;
const originalSize = widgetItem.originalSize;
let targetRow = -1;
let targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
widget.id = this.utils.guid();
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
return throwError('Failed to read widget from buffer!');
}
}
public pasteWidgetReference(targetDashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition): Observable<Widget> {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
if (widget) {
const originalColumns = widgetReference.originalColumns;
const originalSize = widgetReference.originalSize;
let targetRow = -1;
let targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null,
null, null, null, originalColumns,
originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
return throwError('Failed to read widget reference from buffer!');
}
} else {
return throwError('Failed to read widget reference from buffer!');
}
}
public addWidgetToDashboard(dashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, widget: Widget,
aliasesInfo: AliasesInfo,
filtersInfo: FiltersInfo,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void,
originalColumns: number,
originalSize: WidgetSize,
row: number,
column: number,
breakpoint = 'default'): Observable<Dashboard> {
let theDashboard: Dashboard;
if (dashboard) {
theDashboard = dashboard;
} else {
theDashboard = {};
}
theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard);
let callAliasUpdateFunction = false;
let callFilterUpdateFunction = false;
if (aliasesInfo) {
const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo);
const aliasesUpdated = !isEqual(newEntityAliases, theDashboard.configuration.entityAliases);
if (aliasesUpdated) {
theDashboard.configuration.entityAliases = newEntityAliases;
if (onAliasesUpdateFunction) {
callAliasUpdateFunction = true;
}
}
}
if (filtersInfo) {
const newFilters = this.updateFilters(theDashboard, widget, filtersInfo);
const filtersUpdated = !isEqual(newFilters, theDashboard.configuration.filters);
if (filtersUpdated) {
theDashboard.configuration.filters = newFilters;
if (onFiltersUpdateFunction) {
callFilterUpdateFunction = true;
}
}
}
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column, breakpoint);
if (callAliasUpdateFunction) {
onAliasesUpdateFunction();
}
if (callFilterUpdateFunction) {
onFiltersUpdateFunction();
}
return of(theDashboard);
}
public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) {
const ruleNodes: RuleNodesReference = {
nodes: [],
connections: []
};
let top = -1;
let left = -1;
let bottom = -1;
let right = -1;
for (let i = 0; i < nodes.length; i++) {
const origNode = nodes[i];
const node: FcRuleNode = {
id: '',
connectors: [],
additionalInfo: origNode.additionalInfo,
configuration: origNode.configuration,
debugSettings: origNode.debugSettings,
x: origNode.x,
y: origNode.y,
name: origNode.name,
componentClazz: origNode.component.clazz,
ruleChainType: origNode.ruleChainType
};
if (origNode.targetRuleChainId) {
node.targetRuleChainId = origNode.targetRuleChainId;
}
if (origNode.error) {
node.error = origNode.error;
}
if (isDefinedAndNotNull(origNode.singletonMode)) {
node.singletonMode = origNode.singletonMode;
}
if (isDefinedAndNotNull(origNode.queueName)) {
node.queueName = origNode.queueName;
}
ruleNodes.nodes.push(node);
if (i === 0) {
top = node.y;
left = node.x;
bottom = node.y + 50;
right = node.x + 170;
} else {
top = Math.min(top, node.y);
left = Math.min(left, node.x);
bottom = Math.max(bottom, node.y + 50);
right = Math.max(right, node.x + 170);
}
}
ruleNodes.originX = left + (right - left) / 2;
ruleNodes.originY = top + (bottom - top) / 2;
connections.forEach(connection => {
ruleNodes.connections.push(connection);
});
this.storeSet(RULE_NODES, ruleNodes);
}
public hasRuleNodes(): boolean {
return this.storeHas(RULE_NODES);
}
public pasteRuleNodes(x: number, y: number): RuleNodesReference {
const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES);
if (ruleNodes) {
const deltaX = x - ruleNodes.originX;
const deltaY = y - ruleNodes.originY;
for (const node of ruleNodes.nodes) {
const component = this.ruleChainService.getRuleNodeComponentByClazz(node.ruleChainType, node.componentClazz);
if (component) {
let icon = ruleNodeTypeDescriptors.get(component.type).icon;
let iconUrl: string = null;
if (component.configurationDescriptor.nodeDefinition.icon) {
icon = component.configurationDescriptor.nodeDefinition.icon;
}
if (component.configurationDescriptor.nodeDefinition.iconUrl) {
iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
}
delete node.componentClazz;
node.component = component;
node.nodeClass = ruleNodeTypeDescriptors.get(component.type).nodeClass;
node.icon = icon;
node.iconUrl = iconUrl;
node.connectors = [];
node.x = Math.round(node.x + deltaX);
node.y = Math.round(node.y + deltaY);
} else {
return null;
}
}
return ruleNodes;
}
return null;
}
public hasRuleChainImport(): boolean {
return this.storeHas(RULE_CHAIN_IMPORT);
}
public storeRuleChainImport(ruleChainImport: RuleChainImport): void {
this.storeSet(RULE_CHAIN_IMPORT, ruleChainImport);
}
public getRuleChainImport(): RuleChainImport {
const ruleChainImport: RuleChainImport = this.storeGet(RULE_CHAIN_IMPORT);
this.storeRemove(RULE_CHAIN_IMPORT);
return ruleChainImport;
}
private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo {
return {
alias: entityAlias.alias,
filter: entityAlias.filter
};
}
private prepareFilterInfo(filter: Filter): FilterInfo {
return {
filter: filter.filter,
keyFilters: filter.keyFilters,
editable: filter.editable
};
}
private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId): WidgetReference {
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return {
dashboardId: dashboard.id.id,
sourceState,
sourceLayout,
widgetId: widget.id,
originalSize,
originalColumns,
breakpoint
};
}
private updateAliases(dashboard: Dashboard, widget: Widget, aliasesInfo: AliasesInfo): EntityAliases {
const entityAliases = deepClone(dashboard.configuration.entityAliases);
let aliasInfo: EntityAliasInfo;
let newAliasId: string;
for (const datasourceIndexStr of Object.keys(aliasesInfo.datasourceAliases)) {
const datasourceIndex = Number(datasourceIndexStr);
aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
newAliasId = this.getEntityAliasId(entityAliases, aliasInfo);
if (widget.type === widgetType.alarm) {
widget.config.alarmSource.entityAliasId = newAliasId;
} else {
widget.config.datasources[datasourceIndex].entityAliasId = newAliasId;
}
}
if (aliasesInfo.targetDeviceAlias) {
aliasInfo = aliasesInfo.targetDeviceAlias;
newAliasId = this.getEntityAliasId(entityAliases, aliasInfo);
if (widget.config.targetDevice?.type !== TargetDeviceType.entity) {
widget.config.targetDevice = {
type: TargetDeviceType.entity
};
}
widget.config.targetDevice.entityAliasId = newAliasId;
}
return entityAliases;
}
private updateFilters(dashboard: Dashboard, widget: Widget, filtersInfo: FiltersInfo): Filters {
const filters = deepClone(dashboard.configuration.filters);
let filterInfo: FilterInfo;
let newFilterId: string;
for (const datasourceIndexStr of Object.keys(filtersInfo.datasourceFilters)) {
const datasourceIndex = Number(datasourceIndexStr);
filterInfo = filtersInfo.datasourceFilters[datasourceIndex];
newFilterId = this.getFilterId(filters, filterInfo);
if (widget.type === widgetType.alarm) {
widget.config.alarmSource.filterId = newFilterId;
} else {
widget.config.datasources[datasourceIndex].filterId = newFilterId;
}
}
return filters;
}
private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean {
return isEqual(alias1.filter, alias2.filter);
}
private getEntityAliasId(entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string {
let newAliasId: string;
for (const aliasId of Object.keys(entityAliases)) {
if (this.isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) {
newAliasId = aliasId;
break;
}
}
if (!newAliasId) {
const newAliasName = this.createEntityAliasName(entityAliases, aliasInfo.alias);
newAliasId = this.utils.guid();
entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter};
}
return newAliasId;
}
private createEntityAliasName(entityAliases: EntityAliases, alias: string): string {
let c = 0;
let newAlias = alias;
let unique = false;
while (!unique) {
unique = true;
for (const entAliasId of Object.keys(entityAliases)) {
const entAlias = entityAliases[entAliasId];
if (newAlias === entAlias.alias) {
c++;
newAlias = alias + c;
unique = false;
}
}
}
return newAlias;
}
private isFilterEqual(filter1: FilterInfo, filter2: FilterInfo): boolean {
return isEqual(filter1.keyFilters, filter2.keyFilters);
}
private getFilterId(filters: Filters, filterInfo: FilterInfo): string {
let newFilterId: string;
for (const filterId of Object.keys(filters)) {
if (this.isFilterEqual(filters[filterId], filterInfo)) {
newFilterId = filterId;
break;
}
}
if (!newFilterId) {
const newFilterName = this.createFilterName(filters, filterInfo.filter);
newFilterId = this.utils.guid();
filters[newFilterId] = {id: newFilterId, filter: newFilterName,
keyFilters: filterInfo.keyFilters, editable: filterInfo.editable};
}
return newFilterId;
}
private createFilterName(filters: Filters, filter: string): string {
let c = 0;
let newFilter = filter;
let unique = false;
while (!unique) {
unique = true;
for (const entFilterId of Object.keys(filters)) {
const entFilter = filters[entFilterId];
if (newFilter === entFilter.filter) {
c++;
newFilter = filter + c;
unique = false;
}
}
}
return newFilter;
}
private storeSet(key: string, elem: any) {
localStorage.setItem(this.getNamespacedKey(key), JSON.stringify(elem));
}
private storeGet(key: string): any {
let obj = null;
const saved = localStorage.getItem(this.getNamespacedKey(key));
try {
if (typeof saved === 'undefined' || saved === 'undefined') {
obj = undefined;
} else {
obj = JSON.parse(saved);
}
} catch (e) {
this.storeRemove(key);
}
return obj;
}
private storeHas(key: string): boolean {
const saved = localStorage.getItem(this.getNamespacedKey(key));
return typeof saved !== 'undefined' && saved !== 'undefined' && saved !== null;
}
private storeRemove(key: string) {
localStorage.removeItem(this.getNamespacedKey(key));
}
private getNamespacedKey(key: string): string {
return [this.namespace, key].join(this.delimiter);
}
}