Browse Source

Finish RuleChain page. Implement missing widget subscription methods. Implement Entities table widget.

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
4982f0e6a4
  1. 10
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  3. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  4. 51
      ui-ngx/src/app/core/api/alias-controller.ts
  5. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  6. 216
      ui-ngx/src/app/core/api/widget-subscription.ts
  7. 153
      ui-ngx/src/app/core/http/alarm.service.ts
  8. 8
      ui-ngx/src/app/core/http/device.service.ts
  9. 25
      ui-ngx/src/app/core/http/rule-chain.service.ts
  10. 18
      ui-ngx/src/app/core/services/item-buffer.service.ts
  11. 7
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  12. 13
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts
  13. 1
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  14. 28
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  15. 51
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  16. 40
      ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html
  17. 23
      ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss
  18. 116
      ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts
  19. 75
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  20. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  21. 1
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  22. 83
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  23. 22
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  24. 24
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html
  25. 36
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss
  26. 43
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts
  27. 96
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  28. 39
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss
  29. 710
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  30. 55
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  31. 6
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  32. 65
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  33. 8
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  34. 4
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  35. 32
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  36. 5
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts
  37. 11
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  38. 16
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  39. 111
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  40. 45
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts
  41. 15
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  42. 31
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  43. 2
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts
  44. 23
      ui-ngx/src/app/shared/models/alarm.models.ts
  45. 8
      ui-ngx/src/app/shared/models/event.models.ts
  46. 11
      ui-ngx/src/app/shared/models/page/page-link.ts
  47. 16
      ui-ngx/src/app/shared/models/page/sort-order.ts
  48. 5
      ui-ngx/src/app/shared/models/rule-chain.models.ts
  49. 8
      ui-ngx/src/app/shared/models/rule-node.models.ts
  50. 2
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  51. 2
      ui-ngx/src/app/shared/models/widget.models.ts
  52. 7
      ui-ngx/src/styles.scss

10
application/src/main/data/json/system/widget_bundles/cards.json

@ -29,11 +29,11 @@
"sizeX": 7.5,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-entities-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-entities-table-widget>",
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}"
}
},
@ -134,4 +134,4 @@
}
}
]
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.io.IOException;
import java.util.List;
@Slf4j
@RuleNode(
@ -53,9 +54,12 @@ import java.io.IOException;
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
private static ObjectMapper mapper = new ObjectMapper();
private List<String> relationTypes;
@Override
protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
TbCreateAlarmNodeConfiguration nodeConfiguration = TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
relationTypes = nodeConfiguration.getRelationTypes();
return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -19,6 +19,9 @@ import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import java.util.Collections;
import java.util.List;
@Data
public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbCreateAlarmNodeConfiguration> {
@ -26,6 +29,8 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
private boolean propagate;
private boolean useMessageAlarmData;
private List<String> relationTypes;
@Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
@ -38,6 +43,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
configuration.setSeverity(AlarmSeverity.CRITICAL);
configuration.setPropagate(false);
configuration.setUseMessageAlarmData(false);
configuration.setRelationTypes(Collections.emptyList());
return configuration;
}
}

51
ui-ngx/src/app/core/api/alias-controller.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { AliasInfo, IAliasController, IStateController, StateEntityInfo, StateControllerHolder } from '@core/api/widget-api.models';
import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models';
import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models';
import { deepClone } from '@core/utils';
@ -23,54 +23,7 @@ import { UtilsService } from '@core/services/utils.service';
import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
import * as equal from 'deep-equal';
import { map, tap } from 'rxjs/operators';
export class DummyAliasController implements IAliasController {
entityAliasesChanged: Observable<Array<string>>;
entityAliasResolved: Observable<string>;
[key: string]: any | null;
constructor() {
this.entityAliasesChanged = new Subject<Array<string>>().asObservable();
this.entityAliasResolved = new Subject<string>().asObservable();
}
getAliasInfo(aliasId): Observable<AliasInfo> {
return of(null);
}
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource> {
return of(deepClone(alarmSource));
}
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
return of(deepClone(datasources));
}
getEntityAliases(): EntityAliases {
return undefined;
}
getInstantAliasInfo(aliasId: string): AliasInfo {
return undefined;
}
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) {
}
updateEntityAliases(entityAliases: EntityAliases) {
}
dashboardStateChanged() {
}
getEntityAliasId(aliasName: string): string {
return null;
}
}
import { map } from 'rxjs/operators';
export class AliasController implements IAliasController {

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -259,5 +259,4 @@ export interface IWidgetSubscription {
destroy(): void;
[key: string]: any;
// TODO:
}

216
ui-ngx/src/app/core/api/widget-subscription.ts

@ -41,7 +41,7 @@ import {
toHistoryTimewindow,
WidgetTimewindow
} from '@app/shared/models/time/time.models';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
@ -78,7 +78,20 @@ export class WidgetSubscription implements IWidgetSubscription {
alarms: Array<AlarmInfo>;
alarmSource: Datasource;
alarmSearchStatus: AlarmSearchStatus;
private alarmSearchStatusValue: AlarmSearchStatus;
set alarmSearchStatus(value: AlarmSearchStatus) {
if (this.alarmSearchStatusValue !== value) {
this.alarmSearchStatusValue = value;
this.onAlarmSearchStatusChanged();
}
}
get alarmSearchStatus(): AlarmSearchStatus {
return this.alarmSearchStatusValue;
}
alarmsPollingInterval: number;
alarmSourceListener: AlarmSourceListener;
@ -136,7 +149,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
this.alarmSource = options.alarmSource;
this.alarmSearchStatus = isDefined(options.alarmSearchStatus) ?
this.alarmSearchStatusValue = isDefined(options.alarmSearchStatus) ?
options.alarmSearchStatus : AlarmSearchStatus.ANY;
this.alarmsPollingInterval = isDefined(options.alarmsPollingInterval) ?
options.alarmsPollingInterval : 5000;
@ -262,8 +275,30 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private initAlarmSubscription(): Observable<any> {
// TODO:
return of(null);
const initAlarmSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.configureAlarmsData();
initAlarmSubscriptionSubject.next();
initAlarmSubscriptionSubject.complete();
} else {
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe(
(alarmSource) => {
this.alarmSource = alarmSource;
this.configureAlarmsData();
initAlarmSubscriptionSubject.next();
initAlarmSubscriptionSubject.complete();
},
(err) => {
initAlarmSubscriptionSubject.error(err);
}
);
}
});
return initAlarmSubscriptionSubject.asObservable();
}
private configureAlarmsData() {
}
private initDataSubscription(): Observable<any> {
@ -417,6 +452,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
private onAlarmSearchStatusChanged() {
if (this.type === widgetType.alarm) {
this.update();
}
}
updateDataVisibility(index: number): void {
if (this.displayLegend) {
const hidden = this.legendData.keys[index].dataKey.hidden;
@ -465,17 +506,125 @@ export class WidgetSubscription implements IWidgetSubscription {
}
sendOneWayCommand(method: string, params?: any, timeout?: number): Observable<any> {
// TODO:
return undefined;
return this.sendCommand(true, method, params, timeout);
}
sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable<any> {
// TODO:
return undefined;
return this.sendCommand(false, method, params, timeout);
}
clearRpcError(): void {
// TODO:
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
}
sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number): Observable<any> {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else {
if (this.rpcRejection && this.rpcRejection.status !== 408) {
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
}
const requestBody: any = {
method,
params
};
if (timeout && timeout > 0) {
requestBody.timeout = timeout;
}
const rpcSubject: Subject<any> = new ReplaySubject<any>();
this.executingRpcRequest = true;
this.callbacks.rpcStateChanged(this);
if (this.ctx.utils.widgetEditMode) {
setTimeout(() => {
this.executingRpcRequest = false;
this.callbacks.rpcStateChanged(this);
if (oneWayElseTwoWay) {
rpcSubject.next();
rpcSubject.complete();
} else {
rpcSubject.next(requestBody);
rpcSubject.complete();
}
}, 500);
} else {
this.executingSubjects.push(rpcSubject);
const targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand;
targetSendFunction(this.targetDeviceId, requestBody).subscribe((responseBody) => {
this.rpcRejection = null;
this.rpcErrorText = null;
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.onRpcSuccess(this);
rpcSubject.next(responseBody);
rpcSubject.complete();
},
(rejection: HttpErrorResponse) => {
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.rpcStateChanged(this);
if (!this.executingRpcRequest || rejection.status === 408) {
this.rpcRejection = rejection;
if (rejection.status === 408) {
this.rpcErrorText = 'Device is offline.';
} else {
this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
const error = this.extractRejectionErrorText(rejection);
if (error) {
this.rpcErrorText += '</br>';
this.rpcErrorText += error;
}
}
this.callbacks.onRpcFailed(this);
}
rpcSubject.error(rejection);
});
}
return rpcSubject.asObservable();
}
}
private extractRejectionErrorText(rejection: HttpErrorResponse) {
let error = null;
if (rejection.error) {
error = rejection.error;
try {
error = rejection.error ? JSON.parse(rejection.error) : null;
} catch (e) {}
}
if (error && !error.message) {
error = this.prepareMessageFromData(error);
} else if (error && error.message) {
error = error.message;
}
return error;
}
private prepareMessageFromData(data) {
if (typeof data === 'object' && data.constructor === ArrayBuffer) {
const msg = String.fromCharCode.apply(null, new Uint8Array(data));
try {
const msgObj = JSON.parse(msg);
if (msgObj.message) {
return msgObj.message;
} else {
return msg;
}
} catch (e) {
return msg;
}
} else {
return data;
}
}
update() {
@ -550,8 +699,34 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private alarmsSubscribe() {
// TODO:
this.notifyDataLoaded();
this.notifyDataLoading();
if (this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
this.alarmSourceListener = {
subscriptionTimewindow: this.subscriptionTimewindow,
alarmSource: this.alarmSource,
alarmSearchStatus: this.alarmSearchStatus,
alarmsPollingInterval: this.alarmsPollingInterval,
alarmsUpdated: alarms => this.alarmsUpdated(alarms)
};
this.alarms = null;
this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener);
let forceUpdate = false;
if (this.alarmSource.unresolvedStateEntity ||
(this.alarmSource.type === DatasourceType.entity && !this.alarmSource.entityId)
) {
forceUpdate = true;
}
if (forceUpdate) {
this.notifyDataLoaded();
this.onDataUpdated();
}
}
@ -570,7 +745,10 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private alarmsUnsubscribe() {
// TODO:
if (this.alarmSourceListener) {
this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener);
this.alarmSourceListener = null;
}
}
private checkRpcTarget(aliasIds: Array<string>): boolean {
@ -684,6 +862,18 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
private alarmsUpdated(alarms: Array<AlarmInfo>) {
this.notifyDataLoaded();
const updated = !this.alarms || !deepEqual(this.alarms, alarms);
this.alarms = alarms;
if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
this.updateTimewindow();
}
if (updated) {
this.onDataUpdated();
}
}
private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) {
const dataKey = this.legendData.keys[dataIndex].dataKey;
const decimals = isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;

153
ui-ngx/src/app/core/http/alarm.service.ts

@ -15,8 +15,8 @@
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs/index';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
@ -26,24 +26,42 @@ import {
AlarmQuery,
AlarmSearchStatus,
AlarmSeverity,
AlarmStatus
AlarmStatus,
simulatedAlarm
} from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Datasource } from '@shared/models/widget.models';
import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { TimePageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { concatMap, expand, map, toArray } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
import Timeout = NodeJS.Timeout;
interface AlarmSourceListenerQuery {
entityType: EntityType;
entityId: string;
alarmSearchStatus: AlarmSearchStatus;
alarmStatus: AlarmStatus;
fetchOriginator?: boolean;
limit?: number;
interval?: number;
startTime?: number;
endTime?: number;
onAlarms?: (alarms: Array<AlarmInfo>) => void;
}
export interface AlarmSourceListener {
id?: string;
subscriptionTimewindow: SubscriptionTimewindow;
alarmSource: Datasource;
alarmsPollingInterval: number;
alarmSearchStatus: AlarmSearchStatus;
alarmsQuery: {
entityType: EntityType;
entityId: string;
alarmSearchStatus: AlarmSearchStatus;
alarmStatus: AlarmStatus;
fetchOriginator?: boolean;
onAlarms?: (alarms: Array<AlarmInfo>) => void;
};
alarmsUpdated: (alarms: Array<AlarmInfo>) => void;
lastUpdateTs?: number;
alarmsQuery?: AlarmSourceListenerQuery;
pollTimer?: Timeout;
}
@Injectable({
@ -51,8 +69,11 @@ export interface AlarmSourceListener {
})
export class AlarmService {
private alarmSourceListeners: {[id: string]: AlarmSourceListener} = {};
constructor(
private http: HttpClient
private http: HttpClient,
private utils: UtilsService
) { }
public getAlarm(alarmId: string, config?: RequestConfig): Observable<Alarm> {
@ -92,4 +113,110 @@ export class AlarmService {
return this.http.get<AlarmSeverity>(url,
defaultHttpOptionsFromConfig(config));
}
public subscribeForAlarms(alarmSourceListener: AlarmSourceListener): void {
alarmSourceListener.id = this.utils.guid();
this.alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
const alarmSource = alarmSourceListener.alarmSource;
if (alarmSource.type === DatasourceType.function) {
setTimeout(() => {
alarmSourceListener.alarmsUpdated([simulatedAlarm]);
}, 0);
} else if (alarmSource.entityType && alarmSource.entityId) {
const pollingInterval = alarmSourceListener.alarmsPollingInterval;
alarmSourceListener.alarmsQuery = {
entityType: alarmSource.entityType,
entityId: alarmSource.entityId,
alarmSearchStatus: alarmSourceListener.alarmSearchStatus,
alarmStatus: null
};
const originatorKeys = alarmSource.dataKeys.filter(dataKey => dataKey.name.toLocaleLowerCase().includes('originator'));
if (originatorKeys.length) {
alarmSourceListener.alarmsQuery.fetchOriginator = true;
}
const subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
if (subscriptionTimewindow.realtimeWindowMs) {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
} else {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
}
alarmSourceListener.alarmsQuery.onAlarms = (alarms) => {
if (subscriptionTimewindow.realtimeWindowMs) {
const now = Date.now();
if (alarmSourceListener.lastUpdateTs) {
const interval = now - alarmSourceListener.lastUpdateTs;
alarmSourceListener.alarmsQuery.startTime += interval;
}
alarmSourceListener.lastUpdateTs = now;
}
alarmSourceListener.alarmsUpdated(alarms);
};
this.onPollAlarms(alarmSourceListener.alarmsQuery);
alarmSourceListener.pollTimer = setInterval(this.onPollAlarms.bind(this), pollingInterval, alarmSourceListener.alarmsQuery);
}
}
public unsubscribeFromAlarms(alarmSourceListener: AlarmSourceListener): void {
if (alarmSourceListener && alarmSourceListener.id) {
if (alarmSourceListener.pollTimer) {
clearInterval(alarmSourceListener.pollTimer);
alarmSourceListener.pollTimer = null;
}
delete this.alarmSourceListeners[alarmSourceListener.id];
}
}
private onPollAlarms(alarmsQuery: AlarmSourceListenerQuery): void {
this.getAlarmsByAlarmSourceQuery(alarmsQuery).subscribe((alarms) => {
alarmsQuery.onAlarms(alarms);
});
}
private getAlarmsByAlarmSourceQuery(alarmsQuery: AlarmSourceListenerQuery): Observable<Array<AlarmInfo>> {
const time = Date.now();
let pageLink: TimePageLink;
const sortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC};
if (alarmsQuery.limit) {
pageLink = new TimePageLink(alarmsQuery.limit, 0,
null,
sortOrder);
} else if (alarmsQuery.interval) {
pageLink = new TimePageLink(100, 0,
null,
sortOrder, time - alarmsQuery.interval);
} else if (alarmsQuery.startTime) {
pageLink = new TimePageLink(100, 0,
null,
sortOrder, Math.round(alarmsQuery.startTime));
if (alarmsQuery.endTime) {
pageLink.endTime = Math.round(alarmsQuery.endTime);
}
}
return this.fetchAlarms(alarmsQuery, pageLink);
}
private fetchAlarms(query: AlarmSourceListenerQuery,
pageLink: TimePageLink): Observable<Array<AlarmInfo>> {
const alarmQuery = new AlarmQuery(
{id: query.entityId, entityType: query.entityType},
pageLink,
query.alarmSearchStatus,
query.alarmStatus,
query.fetchOriginator);
return this.getAlarms(alarmQuery, {ignoreLoading: true}).pipe(
expand((data) => {
if (data.hasNext && !query.limit) {
alarmQuery.pageLink.page += 1;
return this.getAlarms(alarmQuery, {ignoreLoading: true});
} else {
return EMPTY;
}
}),
map((data) => data.data),
concatMap((data) => data),
toArray(),
map((data) => data.sort((a, b) => alarmQuery.pageLink.sort(a, b))),
);
}
}

8
ui-ngx/src/app/core/http/device.service.ts

@ -113,6 +113,14 @@ export class DeviceService {
return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
public sendOneWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable<any> {
return this.http.post<Device>(`/api/plugins/rpc/oneway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
}
public sendTwoWayRpcCommand(deviceId: string, requestBody: any, config?: RequestConfig): Observable<any> {
return this.http.post<Device>(`/api/plugins/rpc/twoway/${deviceId}`, requestBody, defaultHttpOptionsFromConfig(config));
}
public findByQuery(query: DeviceSearchQuery,
config?: RequestConfig): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config));

25
ui-ngx/src/app/core/http/rule-chain.service.ts

@ -82,14 +82,7 @@ export class RuleChainService {
public getResolvedRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable<ResolvedRuleChainMetaData> {
return this.getRuleChainMetadata(ruleChainId, config).pipe(
mergeMap((ruleChainMetaData) => {
return this.resolveTargetRuleChains(ruleChainMetaData.ruleChainConnections).pipe(
map((targetRuleChainsMap) => {
const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...ruleChainMetaData, targetRuleChainsMap};
return resolvedRuleChainMetadata;
})
);
})
mergeMap((ruleChainMetaData) => this.resolveRuleChainMetadata(ruleChainMetaData))
);
}
@ -100,13 +93,15 @@ export class RuleChainService {
public saveAndGetResolvedRuleChainMetadata(ruleChainMetaData: RuleChainMetaData,
config?: RequestConfig): Observable<ResolvedRuleChainMetaData> {
return this.saveRuleChainMetadata(ruleChainMetaData, config).pipe(
mergeMap((savedRuleChainMetaData) => {
return this.resolveTargetRuleChains(savedRuleChainMetaData.ruleChainConnections).pipe(
map((targetRuleChainsMap) => {
const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...savedRuleChainMetaData, targetRuleChainsMap};
return resolvedRuleChainMetadata;
})
);
mergeMap((savedRuleChainMetaData) => this.resolveRuleChainMetadata(savedRuleChainMetaData))
);
}
public resolveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData): Observable<ResolvedRuleChainMetaData> {
return this.resolveTargetRuleChains(ruleChainMetaData.ruleChainConnections).pipe(
map((targetRuleChainsMap) => {
const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...ruleChainMetaData, targetRuleChainsMap};
return resolvedRuleChainMetadata;
})
);
}

18
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -26,10 +26,14 @@ import { Observable, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
import { FcRuleEdge, 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 { Simulate } from 'react-dom/test-utils';
import error = Simulate.error;
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;
@ -314,6 +318,20 @@ export class ItemBufferService {
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 getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
let originalColumns = 24;
let gridSettings = null;

7
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -19,7 +19,8 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input, NgZone,
Input,
NgZone,
OnInit,
ViewChild,
ViewContainerRef
@ -65,7 +66,7 @@ import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController, IStateController, StateParams } from '@core/api/widget-api.models';
import { AliasController, DummyAliasController } from '@core/api/alias-controller';
import { AliasController } from '@core/api/alias-controller';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@ -73,8 +74,6 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetService } from '@core/http/widget.service';
import { toWidgetInfo } from '../../models/widget-component.models';
import { EntityService } from '@core/http/entity.service';
import { SelectTargetLayoutDialogComponent } from '@home/pages/dashboard/layout/select-target-layout-dialog.component';
import { DashboardLayoutId } from '@shared/models/dashboard.models';
import {
AddWidgetToDashboardDialogComponent,
AddWidgetToDashboardDialogData

13
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts

@ -14,19 +14,10 @@
/// limitations under the License.
///
import {
Component,
ElementRef,
Inject,
OnInit,
Renderer2,
ViewChild
} from '@angular/core';
import { Component, ElementRef, Inject, OnInit, Renderer2, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { ActionStatus, AuditLog } from '@shared/models/audit-log.models';
import * as ace from 'ace-builds';
@ -57,8 +48,6 @@ export class AuditLogDetailsDialogComponent extends DialogComponent<AuditLogDeta
actionData: string;
actionFailureDetails: string;
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AuditLogDetailsDialogData,

1
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -275,6 +275,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private updateWidgets() {
this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts);
this.dashboardWidgets.doCheck();
this.dashboardLoading = false;
}

28
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -152,18 +152,26 @@
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns; trackBy: trackByColumnKey; let col = index">
<mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column, col)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;">
<mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row, col)"
[ngStyle]="cellStyle(entity, column, row, col)">
<button *ngIf="column.actionDescriptor; let actionDescriptor" mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled(entity)"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
[matTooltip]="cellTooltip(entity, column, row)"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)">
<mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style">
{{actionDescriptor.icon}}</mat-icon>
[innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column, row)">
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;">
<mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index"
[ngStyle]="cellStyle(entity, column, row)">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="column.actionDescriptor.isEnabled(entity)"
matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}"
matTooltipPosition="above"
(click)="column.actionDescriptor.onAction($event, entity)">
<mat-icon [svgIcon]="column.actionDescriptor.mdiIcon" [ngStyle]="column.actionDescriptor.style">
{{column.actionDescriptor.icon}}</mat-icon>
</button>
</mat-cell>
</ng-container>

51
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -43,7 +43,7 @@ import {
EntityTableConfig,
GroupActionDescriptor,
HeaderActionDescriptor,
EntityColumn
EntityColumn, EntityActionTableColumn
} from '@home/models/entity/entities-table-config.models';
import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service';
@ -55,6 +55,8 @@ import {
import { Timewindow, historyInterval } from '@shared/models/time/time.models';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { instanceOf } from 'prop-types';
import { isDefined, isDefinedAndNotNull, isUndefined } from '@core/utils';
@Component({
selector: 'tb-entities-table',
@ -73,12 +75,15 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
groupActionDescriptors: Array<GroupActionDescriptor<BaseData<HasId>>>;
cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>;
columns: Array<EntityColumn<BaseData<HasId>>>;
actionColumns: Array<EntityActionTableColumn<BaseData<HasId>>>;
entityColumns: Array<EntityTableColumn<BaseData<HasId>>>;
displayedColumns: string[];
headerCellStyleCache: Array<any> = [];
cellContentCache: Array<SafeHtml> = [];
cellTooltipCache: Array<string> = [];
cellStyleCache: Array<any> = [];
@ -230,6 +235,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
private dataLoaded() {
this.headerCellStyleCache.length = 0;
this.cellContentCache.length = 0;
this.cellTooltipCache.length = 0;
this.cellStyleCache.length = 0;
}
@ -360,14 +366,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
}
columnsUpdated(resetData: boolean = false) {
this.columns = [...this.entitiesTableConfig.columns];
this.entityColumns = this.entitiesTableConfig.columns.filter(
(column) => column instanceof EntityTableColumn)
.map(column => column as EntityTableColumn<BaseData<HasId>>);
this.actionColumns = this.entitiesTableConfig.columns.filter(
(column) => column instanceof EntityActionTableColumn)
.map(column => column as EntityActionTableColumn<BaseData<HasId>>);
this.displayedColumns = [];
if (this.selectionEnabled) {
this.displayedColumns.push('select');
}
this.columns.forEach(
this.entitiesTableConfig.columns.forEach(
(column) => {
this.displayedColumns.push(column.key);
}
@ -375,14 +386,15 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
this.displayedColumns.push('actions');
this.headerCellStyleCache.length = 0;
this.cellContentCache.length = 0;
this.cellTooltipCache.length = 0;
this.cellStyleCache.length = 0;
if (resetData) {
this.dataSource.reset();
}
}
headerCellStyle(column: EntityColumn<BaseData<HasId>>, col: number) {
const index = col;
headerCellStyle(column: EntityColumn<BaseData<HasId>>) {
const index = this.entitiesTableConfig.columns.indexOf(column);
let res = this.headerCellStyleCache[index];
if (!res) {
if (column instanceof EntityTableColumn) {
@ -395,9 +407,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
return res;
}
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number, col: number) {
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) {
if (column instanceof EntityTableColumn) {
const index = row * this.columns.length + col;
const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellContentCache[index];
if (!res) {
res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
@ -409,8 +422,26 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
}
}
cellStyle(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number, col: number) {
const index = row * this.columns.length + col;
cellTooltip(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) {
if (column instanceof EntityTableColumn) {
const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellTooltipCache[index];
if (isUndefined(res)) {
res = column.cellTooltipFunction(entity, column.key);
res = isDefined(res) ? res : null;
this.cellTooltipCache[index] = res;
} else {
return res !== null ? res : undefined;
}
} else {
return undefined;
}
}
cellStyle(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) {
const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellStyleCache[index];
if (!res) {
if (column instanceof EntityTableColumn) {

40
ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2019 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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>{{ title }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
[mat-dialog-close]="false"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content fxLayout="column">
<div #eventContentEditor class="tb-event-content">
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>

23
ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.scss

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
.tb-event-content {
width: 100%;
min-width: 400px;
height: 100%;
min-height: 50px;
}
}

116
ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2019 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 { Component, ElementRef, Inject, OnInit, Renderer2, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import * as ace from 'ace-builds';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ContentType, contentTypesMap } from '@shared/models/constants';
export interface EventContentDialogData {
content: string;
title: string;
contentType: ContentType;
}
@Component({
selector: 'tb-event-content-dialog',
templateUrl: './event-content-dialog.component.html',
styleUrls: ['./event-content-dialog.component.scss']
})
export class EventContentDialogComponent extends DialogComponent<EventContentDialogData> implements OnInit {
@ViewChild('eventContentEditor', {static: true})
eventContentEditorElmRef: ElementRef;
private eventContentEditor: ace.Ace.Editor;
content: string;
title: string;
contentType: ContentType;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EventContentDialogData,
public dialogRef: MatDialogRef<EventContentDialogComponent>,
private renderer: Renderer2) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.content = this.data.content;
this.title = this.data.title;
this.contentType = this.data.contentType;
this.eventContentEditor = this.createEditor(this.eventContentEditorElmRef, this.content);
}
createEditor(editorElementRef: ElementRef, content: string): ace.Ace.Editor {
const editorElement = editorElementRef.nativeElement;
let mode = 'java';
if (this.contentType) {
mode = contentTypesMap.get(this.contentType).code;
if (this.contentType === ContentType.JSON && content) {
content = js_beautify(content, {indent_size: 4});
}
}
let editorOptions: Partial<ace.Ace.EditorOptions> = {
mode: `ace/mode/${mode}`,
theme: 'ace/theme/github',
showGutter: false,
showPrintMargin: false,
readOnly: true
};
const advancedOptions = {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
};
editorOptions = {...editorOptions, ...advancedOptions};
const editor = ace.edit(editorElement, editorOptions);
editor.session.setUseWrapMode(false);
editor.setValue(content, -1);
this.updateEditorSize(editorElement, content, editor);
return editor;
}
updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) {
let newHeight = 400;
let newWidth = 600;
if (content && content.length > 0) {
const lines = content.split('\n');
newHeight = 16 * lines.length + 16;
let maxLineLength = 0;
lines.forEach((row) => {
const line = row.replace(/\t/g, ' ').replace(/\n/g, '');
const lineLength = line.length;
maxLineLength = Math.max(maxLineLength, lineLength);
});
newWidth = 8 * maxLineLength + 16;
}
// newHeight = Math.min(400, newHeight);
this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px');
this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px');
editor.resize();
}
}

75
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -32,8 +32,12 @@ import { EntityTypeResource } from '@shared/models/entity-type.models';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Direction } from '@shared/models/page/sort-order';
import { MsgDataType } from '@shared/models/rule-node.models';
import { DialogService } from '@core/services/dialog.service';
import { ContentType } from '@shared/models/constants';
import {
EventContentDialogComponent,
EventContentDialogData
} from '@home/components/event/event-content-dialog.component';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
@ -157,34 +161,62 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
case DebugEventType.DEBUG_RULE_NODE:
case DebugEventType.DEBUG_RULE_CHAIN:
this.columns.push(
new EntityTableColumn<Event>('type', 'event.type', '100%',
(entity) => entity.body.type, entity => ({}), false),
new EntityTableColumn<Event>('entity', 'event.entity', '100%',
(entity) => entity.body.entityName, entity => ({}), false),
new EntityTableColumn<Event>('type', 'event.type', '40px',
(entity) => entity.body.type, entity => ({
padding: '0 12px 0 0',
}), false, key => ({
padding: '0 12px 0 0'
})),
new EntityTableColumn<Event>('entity', 'event.entity', '100px',
(entity) => entity.body.entityName, entity => ({
padding: '0 12px 0 0',
}), false, key => ({
padding: '0 12px 0 0'
})),
new EntityTableColumn<Event>('msgId', 'event.message-id', '100%',
(entity) => entity.body.msgId, entity => ({}), false),
(entity) => entity.body.msgId, entity => ({
whiteSpace: 'nowrap',
padding: '0 12px 0 0',
textOverflow: 'ellipsis',
display: 'inline-block',
lineHeight: '48px',
}), false, key => ({
padding: '0 12px 0 0'
}),
entity => entity.body.msgId),
new EntityTableColumn<Event>('msgType', 'event.message-type', '100%',
(entity) => entity.body.msgType, entity => ({}), false),
new EntityTableColumn<Event>('relationType', 'event.relation-type', '100%',
(entity) => entity.body.relationType, entity => ({}), false),
(entity) => entity.body.msgType, entity => ({
whiteSpace: 'nowrap',
padding: '0 12px 0 0',
textOverflow: 'ellipsis',
display: 'inline-block',
lineHeight: '48px',
}), false, key => ({
padding: '0 12px 0 0'
}),
entity => entity.body.msgType),
new EntityTableColumn<Event>('relationType', 'event.relation-type', '100px',
(entity) => entity.body.relationType, entity => ({padding: '0 12px 0 0',}),false, key => ({
padding: '0 12px 0 0'
})),
new EntityActionTableColumn<Event>('data', 'event.data',
{
name: this.translate.instant('action.view'),
icon: 'more_horiz',
isEnabled: (entity) => entity.body.data && entity.body.data.length > 0,
isEnabled: (entity) => entity.body.data ? entity.body.data.length > 0 : false,
onAction: ($event, entity) => this.showContent($event, entity.body.data,
'event.data', entity.body.dataType)
},
'60px'),
'40px'),
new EntityActionTableColumn<Event>('metadata', 'event.metadata',
{
name: this.translate.instant('action.view'),
icon: 'more_horiz',
isEnabled: (entity) => entity.body.metadata && entity.body.metadata.length > 0,
isEnabled: (entity) => entity.body.metadata ? entity.body.metadata.length > 0 : false,
onAction: ($event, entity) => this.showContent($event, entity.body.metadata,
'event.metadata', MsgDataType.JSON)
'event.metadata', ContentType.JSON)
},
'60px'),
'40px'),
new EntityActionTableColumn<Event>('error', 'event.error',
{
name: this.translate.instant('action.view'),
@ -193,7 +225,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.error,
'event.error')
},
'60px')
'40px')
);
break;
}
@ -202,11 +234,18 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}
}
showContent($event: MouseEvent, content: string, title: string, contentType: MsgDataType = null): void {
showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null): void {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.dialog.open<EventContentDialogComponent, EventContentDialogData>(EventContentDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
content,
title,
contentType
}
});
}
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -63,11 +63,13 @@ import { ImportDialogComponent } from './import-export/import-dialog.component';
import { AddWidgetToDashboardDialogComponent } from './attribute/add-widget-to-dashboard-dialog.component';
import { ImportDialogCsvComponent } from './import-export/import-dialog-csv.component';
import { TableColumnsAssignmentComponent } from './import-export/table-columns-assignment.component';
import { EventContentDialogComponent } from '@home/components/event/event-content-dialog.component';
@NgModule({
entryComponents: [
AddEntityDialogComponent,
AuditLogDetailsDialogComponent,
EventContentDialogComponent,
EventTableHeaderComponent,
RelationDialogComponent,
AlarmTableHeaderComponent,
@ -94,6 +96,7 @@ import { TableColumnsAssignmentComponent } from './import-export/table-columns-a
ContactComponent,
AuditLogTableComponent,
AuditLogDetailsDialogComponent,
EventContentDialogComponent,
EventTableHeaderComponent,
EventTableComponent,
RelationTableComponent,

1
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -19,6 +19,7 @@ import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { ActionType } from '@shared/models/audit-log.models';
import { RuleChain, RuleChainMetaData } from '@shared/models/rule-chain.models';
export interface ImportWidgetResult {
widget: Widget;

83
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -44,15 +44,16 @@ import {
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
import { CsvToJsonConfig, ImportWidgetResult, WidgetsBundleItem, CsvToJsonResult } from './import-export.models';
import { ImportWidgetResult, WidgetsBundleItem } from './import-export.models';
import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ImportDialogCsvComponent, ImportDialogCsvData } from './import-dialog-csv.component';
import { ImportEntityData, ImportEntitiesResultInfo } from '@shared/models/entity.models';
import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
// @dynamic
@Injectable()
@ -66,6 +67,7 @@ export class ImportExportService {
private dashboardUtils: DashboardUtilsService,
private widgetService: WidgetService,
private entityService: EntityService,
private ruleChainService: RuleChainService,
private utils: UtilsService,
private itembuffer: ItemBufferService,
private dialog: MatDialog) {
@ -365,6 +367,81 @@ export class ImportExportService {
);
}
public exportRuleChain(ruleChainId: string) {
this.ruleChainService.getRuleChain(ruleChainId).pipe(
mergeMap(ruleChain => {
return this.ruleChainService.getRuleChainMetadata(ruleChainId).pipe(
map((ruleChainMetaData) => {
const ruleChainExport: RuleChainImport = {
ruleChain: this.prepareRuleChain(ruleChain),
metadata: this.prepareRuleChainMetaData(ruleChainMetaData)
};
return ruleChainExport;
})
);
})
).subscribe((ruleChainExport) => {
let name = ruleChainExport.ruleChain.name;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(ruleChainExport, name + '.json');
},
(e) => {
this.handleExportError(e, 'rulechain.export-failed-error');
}
);
}
public importRuleChain(): Observable<RuleChainImport> {
return this.openImportDialog('rulechain.import', 'rulechain.rulechain-file').pipe(
mergeMap((ruleChainImport: RuleChainImport) => {
if (!this.validateImportedRuleChain(ruleChainImport)) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('rulechain.invalid-rulechain-file-error'),
type: 'error'}));
throw new Error('Invalid rule chain file');
} else {
return this.ruleChainService.resolveRuleChainMetadata(ruleChainImport.metadata).pipe(
map((resolvedMetadata) => {
ruleChainImport.resolvedMetadata = resolvedMetadata;
return ruleChainImport;
})
);
}
}),
catchError((err) => {
return of(null);
})
);
}
private prepareRuleChain(ruleChain: RuleChain): RuleChain {
ruleChain = this.prepareExport(ruleChain);
if (ruleChain.firstRuleNodeId) {
ruleChain.firstRuleNodeId = null;
}
ruleChain.root = false;
return ruleChain;
}
private prepareRuleChainMetaData(ruleChainMetaData: RuleChainMetaData) {
delete ruleChainMetaData.ruleChainId;
for (let i = 0; i < ruleChainMetaData.nodes.length; i++) {
var node = ruleChainMetaData.nodes[i];
delete node.ruleChainId;
ruleChainMetaData.nodes[i] = this.prepareExport(node);
}
return ruleChainMetaData;
}
private validateImportedRuleChain(ruleChainImport: RuleChainImport): boolean {
if (isUndefined(ruleChainImport.ruleChain)
|| isUndefined(ruleChainImport.metadata)
|| isUndefined(ruleChainImport.ruleChain.name)) {
return false;
}
return true;
}
private sumObject(obj1: any, obj2: any): any {
Object.keys(obj2).map((key) => {
if (isObject(obj2[key])) {

22
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -15,22 +15,17 @@
///
import { PageComponent } from '@shared/components/page.component';
import { Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { Inject, Injector, Input, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models';
import { ExceptionData } from '@shared/models/error.models';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { DeviceService } from '@core/http/device.service';
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
@Input()
widgetContext: WidgetContext;
@Input()
errorMessages: string[];
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
@ -39,8 +34,19 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
[key: string]: any;
constructor(@Inject(RafService) public raf: RafService,
@Inject(Store) protected store: Store<AppState>) {
@Inject(Store) protected store: Store<AppState>,
@Inject(Injector) private $injector: Injector,
@Inject('widgetContext') public readonly ctx: WidgetContext,
@Inject('errorMessages') public readonly errorMessages: string[]) {
super(store);
this.ctx.$injector = $injector;
this.ctx.$scope = this;
if (this.ctx.defaultSubscription) {
this.executingRpcRequest = this.ctx.defaultSubscription.executingRpcRequest;
this.rpcEnabled = this.ctx.defaultSubscription.rpcEnabled;
this.rpcErrorText = this.ctx.defaultSubscription.rpcErrorText;
this.rpcRejection = this.ctx.defaultSubscription.rpcRejection;
}
}
ngOnInit() {

24
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.html

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2019 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.
-->
<div fxLayout="column" class="mat-content mat-padding">
<label class="tb-title" translate>entity.columns-to-display</label>
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="column.display" *ngFor="let column of columns"
(ngModelChange)="update()">
{{ column.title }}
</mat-checkbox>
</div>

36
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
width: 100%;
height: 100%;
min-width: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.ts

@ -0,0 +1,43 @@
///
/// Copyright © 2016-2019 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 { Component, Inject, InjectionToken } from '@angular/core';
import { DisplayColumn } from '@home/components/widget/lib/table-widget.models';
export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken<any>('DisplayColumnsPanelData');
export interface DisplayColumnsPanelData {
columns: DisplayColumn[];
columnsUpdated: (columns: DisplayColumn[]) => void;
}
@Component({
selector: 'tb-display-columns-panel',
templateUrl: './display-columns-panel.component.html',
styleUrls: ['./display-columns-panel.component.scss']
})
export class DisplayColumnsPanelComponent {
columns: DisplayColumn[];
constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) {
this.columns = this.data.columns;
}
public update() {
this.data.columnsUpdated(this.columns);
}
}

96
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -0,0 +1,96 @@
<!--
Copyright © 2016-2019 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.
-->
<div class="tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-absolute-fill">
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ 'entity.search' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<mat-table [dataSource]="entityDatasource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container [matColumnDef]="column.label" *ngFor="let column of columns; trackBy: trackByColumnLabel;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity;"
[innerHTML]="cellContent(entity, column)"
[ngStyle]="cellStyle(entity, column)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }">
</mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
*ngFor="let actionDescriptor of actionCellDescriptors"
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, entity, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors"
[disabled]="isLoading$ | async"
(click)="onActionButtonClick($event, entity, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}"
*matRowDef="let entity; columns: displayedColumns;"
(click)="onRowClick($event, entity)"></mat-row>
</mat-table>
<span [fxShow]="entityDatasource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>entity.no-entities-prompt</span>
</div>
<mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator *ngIf="displayPagination"
[length]="entityDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
</div>
</div>

39
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
width: 100%;
height: 100%;
.tb-entity-table {
.mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar {
background: transparent;
}
mat-toolbar {
height: 39px;
max-height: 39px;
.mat-toolbar-tools {
height: 39px;
max-height: 39px;
}
}
.table-container {
overflow: auto;
}
}
}
:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}

710
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -0,0 +1,710 @@
///
/// Copyright © 2016-2019 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 { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
import {
DataKey,
Datasource,
DatasourceData,
DatasourceType,
WidgetActionDescriptor,
WidgetConfig
} from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber } from '@core/utils';
import * as tinycolor_ from 'tinycolor2';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { DataSource } from '@angular/cdk/typings/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { CollectionViewer } from '@angular/cdk/collections';
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import {
CellContentInfo,
CellStyleInfo,
DisplayColumn,
EntityColumn,
EntityData,
getEntityValue
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent,
DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component';
const tinycolor = tinycolor_;
interface EntitiesTableWidgetSettings {
entitiesTitle: string;
enableSearch: boolean;
enableSelectColumnDisplay: boolean;
displayEntityName: boolean;
entityNameColumnTitle: string;
displayEntityType: boolean;
displayPagination: boolean;
defaultPageSize: number;
defaultSortOrder: string;
}
interface EntitiesTableDataKeySettings {
columnWidth: string;
useCellStyleFunction: boolean;
cellStyleFunction: string;
useCellContentFunction: boolean;
cellContentFunction: string;
}
@Component({
selector: 'tb-entities-table-widget',
templateUrl: './entities-table-widget.component.html',
styleUrls: ['./entities-table-widget.component.scss']
})
export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
@Input()
ctx: WidgetContext;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
public displayPagination = true;
public pageSizeOptions;
public pageLink: PageLink;
public textSearchMode = false;
public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = [];
public actionCellDescriptors: WidgetActionDescriptor[];
public entityDatasource: EntityDatasource;
private settings: EntitiesTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private defaultPageSize = 10;
private defaultSortOrder = 'entityName';
private contentsInfo: {[key: string]: CellContentInfo} = {};
private stylesInfo: {[key: string]: CellStyleInfo} = {};
private columnWidth: {[key: string]: string} = {};
private searchAction: WidgetAction = {
name: 'action.search',
show: true,
icon: 'search',
onAction: () => {
this.enterFilterMode();
}
};
private columnDisplayAction: WidgetAction = {
name: 'entity.columns-to-display',
show: true,
icon: 'view_column',
onAction: ($event) => {
this.editColumnsToDisplay($event);
}
};
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private translate: TranslateService,
private domSanitizer: DomSanitizer) {
super(store);
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
}
ngOnInit(): void {
this.ctx.$scope.entitiesTableWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.initializeConfig();
this.updateDatasources();
this.ctx.updateWidgetParams();
}
ngAfterViewInit(): void {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
})
)
.subscribe();
if (this.displayPagination) {
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
}
(this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.updateData();
}
public onDataUpdated() {
this.entityDatasource.updateEntitiesData(this.subscription.data);
this.ctx.detectChanges();
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
let entitiesTitle: string;
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
entitiesTitle = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle);
} else {
entitiesTitle = this.translate.instant('entity.entities');
}
const datasource = this.subscription.datasources[0];
this.ctx.widgetTitle = this.utils.createLabelFromDatasource(datasource, entitiesTitle);
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
this.defaultPageSize = pageSize;
}
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
const origColor = this.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)';
const defaultColor = tinycolor(origColor);
const mdDark = defaultColor.setAlpha(0.87).toRgbString();
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString =
'.mat-input-element::placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-moz-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-webkit-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element:-ms-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'mat-toolbar.mat-table-toolbar {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'mat-toolbar.mat-table-toolbar button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-row {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-header-cell {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-table .mat-row, .mat-table .mat-header-row {\n'+
'border-bottom-color: '+mdDarkDivider+';\n'+
'}\n'+
'.mat-table .mat-row:not(.tb-current-entity):not(:hover) .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-cell {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-divider {\n'+
'border-top-color: ' + mdDarkDivider + ';\n'+
'}\n'+
'.mat-paginator {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-paginator .mat-select-value {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}';
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'entities-table-' + this.utils.hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateDatasources() {
this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
let entityNameColumnTitle: string;
if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) {
entityNameColumnTitle = this.utils.customTranslation(this.settings.entityNameColumnTitle, this.settings.entityNameColumnTitle);
} else {
entityNameColumnTitle = this.translate.instant('entity.entity-name');
}
const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true;
if (displayEntityName) {
this.displayedColumns.push('entityName');
this.columns.push(
{
name: 'entityName',
label: 'entityName',
title: entityNameColumnTitle
} as EntityColumn
);
this.contentsInfo['entityName'] = {
useCellContentFunction: false
};
this.stylesInfo['entityName'] = {
useCellStyleFunction: false
};
this.columnWidth['entityName'] = '100px';
}
if (displayEntityType) {
this.displayedColumns.push('entityType');
this.columns.push(
{
name: 'entityType',
label: 'entityType',
title: this.translate.instant('entity.entity-type'),
} as EntityColumn
);
this.contentsInfo['entityType'] = {
useCellContentFunction: false
};
this.stylesInfo['entityType'] = {
useCellStyleFunction: false
};
this.columnWidth['entityType'] = '100px';
}
const dataKeys: Array<DataKey> = [];
const datasource = this.subscription.datasources[0];
if (datasource) {
datasource.dataKeys.forEach((_dataKey) => {
const dataKey: EntityColumn = deepClone(_dataKey) as EntityColumn;
if (dataKey.type === DataKeyType.function) {
dataKey.name = dataKey.label;
}
dataKeys.push(dataKey);
dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
const keySettings: EntitiesTableDataKeySettings = dataKey.settings;
let cellStyleFunction: Function = null;
let useCellStyleFunction = false;
if (keySettings.useCellStyleFunction === true) {
if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
try {
cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
useCellStyleFunction = true;
} catch (e) {
cellStyleFunction = null;
useCellStyleFunction = false;
}
}
}
this.stylesInfo[dataKey.label] = {
useCellStyleFunction,
cellStyleFunction
};
let cellContentFunction: Function = null;
let useCellContentFunction = false;
if (keySettings.useCellContentFunction === true) {
if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
try {
cellContentFunction = new Function('value, entity, ctx', keySettings.cellContentFunction);
useCellContentFunction = true;
} catch (e) {
cellContentFunction = null;
useCellContentFunction = false;
}
}
}
this.contentsInfo[dataKey.label] = {
useCellContentFunction,
cellContentFunction,
units: dataKey.units,
decimals: dataKey.decimals
};
const columnWidth = isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
this.columnWidth[dataKey.label] = columnWidth;
this.displayedColumns.push(dataKey.label);
this.columns.push(dataKey);
});
}
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.entityDatasource = new EntityDatasource(
this.translate, dataKeys, this.subscription.datasources);
}
private editColumnsToDisplay($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const columns: DisplayColumn[] = this.columns.map(column => {
return {
title: column.title,
label: column.label,
display: this.displayedColumns.indexOf(column.label) > -1
}
});
const injectionTokens = new WeakMap<any, any>([
[DISPLAY_COLUMNS_PANEL_DATA, {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.label);
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
}
private enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
this.ctx.hideTitlePanel = true;
this.ctx.detectChanges(true);
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
this.ctx.hideTitlePanel = false;
this.ctx.detectChanges(true);
}
private updateData() {
if (this.displayPagination) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
} else {
this.pageLink.page = 0;
}
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.entityDatasource.loadEntities(this.pageLink);
this.ctx.detectChanges();
}
public trackByColumnLabel(index, column: EntityColumn) {
return column.label;
}
public headerStyle(key: EntityColumn): any {
return this.widthStyle(key);
}
public cellStyle(entity: EntityData, key: EntityColumn): any {
let style: any = {};
if (entity && key) {
const styleInfo = this.stylesInfo[key.label];
const value = getEntityValue(entity, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value);
} catch (e) {
style = {};
}
} else {
style = this.defaultStyle(key, value);
}
}
const widthStyle = this.widthStyle(key);
style = {...style, ...widthStyle};
return style;
}
private widthStyle(key: EntityColumn): any {
let style: any = {};
const columnWidth = this.columnWidth[key.label];
if (columnWidth !== "0px") {
style.minWidth = columnWidth;
style.width = columnWidth;
} else {
style.minWidth = "auto";
style.width = "auto";
}
return style;
}
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
let strContent = '';
if (entity && key) {
const contentInfo = this.contentsInfo[key.label];
const value = getEntityValue(entity, key);
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
if (isDefined(value)) {
strContent = '' + value;
}
var content = strContent;
try {
content = contentInfo.cellContentFunction(value, entity, this.ctx);
} catch (e) {
content = strContent;
}
} else {
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
const units = contentInfo.units || this.ctx.widgetConfig.units;
content = this.ctx.utils.formatValue(value, decimals, units, true);
}
return this.domSanitizer.bypassSecurityTrustHtml(content);
} else {
return strContent;
}
}
public onRowClick($event: Event, entity: EntityData) {
if ($event) {
$event.stopPropagation();
}
this.entityDatasource.toggleCurrentEntity(entity);
const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick');
if (descriptors.length) {
let entityId;
let entityName;
if (entity) {
entityId = entity.id;
entityName = entity.entityName;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
}
}
public onActionButtonClick($event: Event, entity: EntityData, actionDescriptor: WidgetActionDescriptor) {
if ($event) {
$event.stopPropagation();
}
let entityId;
let entityName;
if (entity) {
entityId = entity.id;
entityName = entity.entityName;
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName);
}
private defaultStyle(key: EntityColumn, value: any): any {
return {};
}
}
class EntityDatasource implements DataSource<EntityData> {
private entitiesSubject = new BehaviorSubject<EntityData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>());
private allEntities: Array<EntityData> = [];
private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]);
private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable();
private currentEntity: EntityData = null;
constructor(
private translate: TranslateService,
private dataKeys: Array<DataKey>,
datasources: Array<Datasource>
) {
for (const datasource of datasources) {
if (datasource.type === DatasourceType.entity && !datasource.entityId) {
continue;
}
const entity: EntityData = {
id: {} as EntityId,
entityName: datasource.entityName
};
if (datasource.entityId) {
entity.id.id = datasource.entityId;
}
if (datasource.entityType) {
entity.id.entityType = datasource.entityType;
entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type);
} else {
entity.entityType = '';
}
this.dataKeys.forEach((dataKey) => {
entity[dataKey.label] = '';
});
this.allEntities.push(entity);
}
this.allEntitiesSubject.next(this.allEntities);
}
connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> {
return this.entitiesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entitiesSubject.complete();
this.pageDataSubject.complete();
}
loadEntities(pageLink: PageLink) {
this.fetchEntities(pageLink).pipe(
catchError(() => of(emptyPageData<EntityData>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
);
}
updateEntitiesData(data: DatasourceData[]) {
for (let i = 0; i < this.allEntities.length; i++) {
const entity = this.allEntities[i];
for (let a = 0; a < this.dataKeys.length; a++) {
const dataKey = this.dataKeys[a];
const index = i * this.dataKeys.length + a;
const keyData = data[index].data;
if (keyData && keyData.length && keyData[0].length > 1) {
const value = keyData[0][1];
entity[dataKey.label] = value;
} else {
entity[dataKey.label] = '';
}
}
}
this.allEntitiesSubject.next(this.allEntities);
}
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
map((entities) => !entities.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
public toggleCurrentEntity(entity: EntityData): boolean {
if (this.currentEntity !== entity) {
this.currentEntity = entity;
return true;
} else {
return false;
}
}
public isCurrentEntity(entity: EntityData): boolean {
return (this.currentEntity && entity && this.currentEntity.id && entity.id) &&
(this.currentEntity.id.id === entity.id.id);
}
private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> {
return this.allEntities$.pipe(
map((data) => pageLink.filterData(data))
);
}
}

55
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -0,0 +1,55 @@
///
/// Copyright © 2016-2019 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 { EntityId } from '@shared/models/id/entity-id';
import { DataKey } from '@shared/models/widget.models';
export interface EntityData {
id: EntityId;
entityName: string;
entityType?: string;
[key: string]: any;
}
export interface EntityColumn extends DataKey {
title: string;
}
export interface DisplayColumn {
title: string;
label: string;
display: boolean;
}
export interface CellContentInfo {
useCellContentFunction: boolean;
cellContentFunction?: Function;
units?: string;
decimals?: number;
}
export interface CellStyleInfo {
useCellStyleFunction: boolean;
cellStyleFunction?: Function;
}
export function getEntityValue(entity: any, key: DataKey): any {
return getDescendantProp(entity, key.label);
}
export function getDescendantProp(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => acc && acc[part], obj)
}

6
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -19,18 +19,24 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { LegendComponent } from '@home/components/widget/legend.component';
import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
@NgModule({
entryComponents: [
DisplayColumnsPanelComponent
],
declarations:
[
DisplayColumnsPanelComponent,
EntitiesTableWidgetComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
EntitiesTableWidgetComponent
]
})
export class WidgetComponentsModule { }

65
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -27,7 +27,7 @@ import {
NgZone,
OnChanges,
OnDestroy,
OnInit,
OnInit, ReflectiveInjector,
SimpleChanges, Type,
ViewChild,
ViewContainerRef,
@ -93,12 +93,14 @@ import { EntityService } from '@core/http/entity.service';
import { AssetService } from '@core/http/asset.service';
import { DialogService } from '@core/services/dialog.service';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { DatePipe } from '@angular/common';
const ServicesMap = new Map<string, Type<any>>();
ServicesMap.set('deviceService', DeviceService);
ServicesMap.set('assetService', AssetService);
ServicesMap.set('dialogs', DialogService);
ServicesMap.set('customDialog', CustomDialogService);
ServicesMap.set('date', DatePipe);
@Component({
selector: 'tb-widget',
@ -249,6 +251,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
this.widgetContext = this.dashboardWidget.widgetContext;
this.widgetContext.changeDetector = this.cd;
this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isMobile = this.isMobile;
@ -420,6 +423,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.initialize().subscribe(
() => {
this.cd.detectChanges();
this.onInit();
},
(err) => {
@ -579,13 +583,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
));
this.configureDynamicWidgetComponent();
if (!this.typeParameters.useCustomDatasources) {
// this.cre
this.createDefaultSubscription().subscribe(
() => {
this.subscriptionInited = true;
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
},
@ -597,6 +599,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} else {
this.loadingData = false;
this.subscriptionInited = true;
this.configureDynamicWidgetComponent();
initSubject.next();
initSubject.complete();
}
@ -621,15 +624,24 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private configureDynamicWidgetComponent() {
this.widgetContentContainer.clear();
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory);
const injector: Injector = Injector.create(
{
providers: [
{
provide: 'widgetContext',
useValue: this.widgetContext
},
{
provide: 'errorMessages',
useValue: this.errorMessages
}
],
parent: this.injector
}
);
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
this.dynamicWidgetComponent.widgetContext = this.widgetContext;
this.dynamicWidgetComponent.errorMessages = this.errorMessages;
this.widgetContext.$scope = this.dynamicWidgetComponent;
this.widgetContext.$scope.$injector = this.injector;
const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container'));
// this.widgetContext.$container = $('> ng-component:not([id="container"])', containerElement);
@ -810,22 +822,30 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
};
options.callbacks = {
rpcStateChanged: (subscription) => {
this.dynamicWidgetComponent.rpcEnabled = subscription.rpcEnabled;
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcEnabled = subscription.rpcEnabled;
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
}
},
onRpcSuccess: (subscription) => {
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
}
},
onRpcFailed: (subscription) => {
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
}
},
onRpcErrorCleared: (subscription) => {
this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null;
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null;
}
}
};
this.createSubscription(options).subscribe(
@ -897,12 +917,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} else {
url = `/dashboards/${targetDashboardId}?state=${state}`;
}
const urlTree = this.router.parseUrl(url);
this.router.navigateByUrl(url);
break;
case WidgetActionType.custom:
const customFunction = descriptor.customFunction;
if (isDefined(customFunction) && customFunction.length > 0) {
if (customFunction && customFunction.length > 0) {
try {
if (!additionalParams) {
additionalParams = {};

8
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -17,11 +17,11 @@
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { Widget, widgetType, WidgetPosition } from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs';
import { guid, isDefined, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IterableDiffer, KeyValueDiffer, NgZone } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import * as deepEqual from 'deep-equal';
@ -264,7 +264,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
}
export class DashboardWidget implements GridsterItem {
export class DashboardWidget implements GridsterItem, IDashboardWidget {
highlighted = false;
selected = false;
@ -302,7 +302,7 @@ export class DashboardWidget implements GridsterItem {
customHeaderActions: Array<WidgetHeaderAction>;
widgetActions: Array<WidgetAction>;
widgetContext = new WidgetContext(this.dashboard, this.widget);
widgetContext = new WidgetContext(this.dashboard, this, this.widget);
widgetId: string;

4
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -41,6 +41,7 @@ export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAct
export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>;
export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string;
export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined;
export type HeaderCellStyleFunction<T extends BaseData<HasId>> = (key: string) => object;
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
@ -86,7 +87,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTabl
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
public cellStyleFunction: CellStyleFunction<T> = () => ({}),
public sortable: boolean = true,
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({})) {
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined) {
super('content', key, title, maxWidth, sortable);
}
}

32
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -40,7 +40,7 @@ import {
WidgetActionsApi,
WidgetSubscriptionApi, WidgetSubscriptionContext, WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import { ComponentFactory, Type } from '@angular/core';
import { ChangeDetectorRef, ComponentFactory, Injector, NgZone, Type } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -69,9 +69,14 @@ export interface WidgetAction extends IWidgetAction {
show: boolean;
}
export interface IDashboardWidget {
updateWidgetParams();
}
export class WidgetContext {
constructor(public dashboard: IDashboardComponent,
private dashboardWidget: IDashboardWidget,
private widget: Widget) {}
get stateController(): IStateController {
@ -102,6 +107,25 @@ export class WidgetContext {
return isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2;
}
set changeDetector(cd: ChangeDetectorRef) {
this._changeDetector = cd;
}
private _changeDetector: ChangeDetectorRef;
detectChanges(updateWidgetParams: boolean = false) {
if (updateWidgetParams) {
this.dashboardWidget.updateWidgetParams();
}
this._changeDetector.detectChanges();
}
updateWidgetParams() {
setTimeout(() => {
this.dashboardWidget.updateWidgetParams();
}, 0);
}
inited = false;
subscriptions: {[id: string]: IWidgetSubscription} = {};
@ -167,11 +191,13 @@ export class WidgetContext {
widgetActions?: Array<WidgetAction>;
servicesMap?: Map<string, Type<any>>;
$injector?: Injector;
}
export interface IDynamicWidgetComponent {
widgetContext: WidgetContext;
errorMessages: string[];
readonly ctx: WidgetContext;
readonly errorMessages: string[];
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;

5
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts

@ -36,6 +36,7 @@ import { RuleChainService } from '@core/http/rule-chain.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-rule-node-config',
@ -134,7 +135,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
}
writeValue(value: RuleNodeConfiguration): void {
this.configuration = value;
this.configuration = deepClone(value);
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
@ -145,7 +146,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
this.updateModel(configuration);
});
} else {
this.ruleNodeConfigFormGroup.get('configuration').patchValue(value, {emitEvent: false});
this.ruleNodeConfigFormGroup.get('configuration').patchValue(this.configuration, {emitEvent: false});
this.changeSubscription = this.ruleNodeConfigFormGroup.get('configuration').valueChanges.subscribe(
(configuration: RuleNodeConfiguration) => {
this.updateModel(configuration);

11
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -111,16 +111,21 @@
<div class="details-buttons">
<div [tb-help]="helpLinkIdForRuleNodeType()"></div>
</div>
<mat-tab-group fxFlex class="tb-absolute-fill" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab-group fxFlex class="tb-absolute-fill tb-rulenode-details" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab label="{{ 'rulenode.details' | translate }}">
<tb-rule-node #tbRuleNode
[ruleNode]="editingRuleNode"
[ruleChainId]="ruleChain.id.id"
[ruleChainId]="ruleChain.id?.id"
[isEdit]="true"
[isReadOnly]="false">
</tb-rule-node>
</mat-tab>
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}">
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}" #eventsTab="matTab">
<tb-event-table [active]="eventsTab.isActive"
[debugEventTypes]="[debugEventTypes.DEBUG_RULE_NODE]"
[defaultEventType]="debugEventTypes.DEBUG_RULE_NODE"
[tenantId]="ruleChain.tenantId.id"
[entityId]="editingRuleNode.ruleNodeId"></tb-event-table>
</mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">

16
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss

@ -19,6 +19,22 @@
.tb-rulechain {
width: 100%;
height: 100%;
.mat-tab-group.tb-rulenode-details {
> .mat-tab-body-wrapper {
position: absolute;
top: 49px;
left: 0;
right: 0;
bottom: 0;
}
> .mat-tab-header {
.mat-tab-label {
min-width: 40px;
}
}
}
button.mat-button.mat-icon-button.tb-fullscreen-button {
position: absolute;
top: 10px;

111
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -38,9 +38,11 @@ import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute, Router } from '@angular/router';
import {
inputNodeComponent,
inputNodeComponent, NodeConnectionInfo,
ResolvedRuleChainMetaData,
RuleChain,
RuleChainConnectionInfo, RuleChainImport,
RuleChainMetaData,
ruleChainNodeComponent
} from '@shared/models/rule-chain.models';
import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart';
@ -50,6 +52,7 @@ import {
FcRuleNodeType,
getRuleNodeHelpLink,
LinkLabel,
RuleNode,
RuleNodeComponentDescriptor,
RuleNodeType,
ruleNodeTypeDescriptors,
@ -66,8 +69,10 @@ import { RuleNodeLinkComponent } from './rule-node-link.component';
import { DialogComponent } from '@shared/components/dialog.component';
import { MatMenuTrigger } from '@angular/material/menu';
import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
import { Hotkey } from 'angular2-hotkeys';
import { EntityType } from '@shared/models/entity-type.models';
import Timeout = NodeJS.Timeout;
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
import { DebugEventType, EventType } from '@shared/models/event.models';
@Component({
selector: 'tb-rulechain-page',
@ -94,6 +99,10 @@ export class RuleChainPageComponent extends PageComponent
@ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
eventTypes = EventType;
debugEventTypes = DebugEventType;
ruleChainMenuPosition = { x: '0px', y: '0px' };
contextMenuEvent: MouseEvent;
@ -260,13 +269,15 @@ export class RuleChainPageComponent extends PageComponent
private init() {
this.initHotKeys();
this.ruleChain = this.route.snapshot.data.ruleChain;
if (this.route.snapshot.data.import && !this.ruleChain) {
this.router.navigateByUrl('ruleChains');
return;
}
this.isImport = this.route.snapshot.data.import;
this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData;
if (this.isImport) {
const ruleChainImport: RuleChainImport = this.itembuffer.getRuleChainImport();
this.ruleChain = ruleChainImport.ruleChain;
this.ruleChainMetaData = ruleChainImport.resolvedMetadata;
} else {
this.ruleChain = this.route.snapshot.data.ruleChain;
this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData;
}
this.ruleNodeComponents = this.route.snapshot.data.ruleNodeComponents;
for (const type of ruleNodeTypesLibrary) {
const desc = ruleNodeTypeDescriptors.get(type);
@ -564,7 +575,7 @@ export class RuleChainPageComponent extends PageComponent
if (!ruleChainNode) {
ruleChainNode = {
id: 'rule-chain-node-' + this.nextNodeID++,
name: ruleChain.name ? name : 'Unresolved',
name: ruleChain.name ? ruleChain.name : 'Unresolved',
targetRuleChainId: ruleChain.name ? ruleChainConnection.targetRuleChainId.id : null,
error: ruleChain.name ? undefined : this.translate.instant('rulenode.invalid-target-rulechain'),
additionalInfo: ruleChainConnection.additionalInfo,
@ -833,7 +844,6 @@ export class RuleChainPageComponent extends PageComponent
}
onModelChanged() {
console.log('Model changed!');
this.isDirtyValue = true;
this.validate();
}
@ -1162,7 +1172,86 @@ export class RuleChainPageComponent extends PageComponent
}
saveRuleChain() {
// TODO:
let saveRuleChainObservable: Observable<RuleChain>;
if (this.isImport) {
saveRuleChainObservable = this.ruleChainService.saveRuleChain(this.ruleChain);
} else {
saveRuleChainObservable = of(this.ruleChain);
}
saveRuleChainObservable.subscribe((ruleChain) => {
this.ruleChain = ruleChain;
const ruleChainMetaData: RuleChainMetaData = {
ruleChainId: this.ruleChain.id,
nodes: [],
connections: [],
ruleChainConnections: []
};
const nodes: FcRuleNode[] = [];
this.ruleChainModel.nodes.forEach((node) => {
if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) {
const ruleNode: RuleNode = {
id: node.ruleNodeId,
type: node.component.clazz,
name: node.name,
configuration: node.configuration,
additionalInfo: node.additionalInfo ? node.additionalInfo : {},
debugMode: node.debugMode
};
ruleNode.additionalInfo.layoutX = Math.round(node.x);
ruleNode.additionalInfo.layoutY = Math.round(node.y);
ruleChainMetaData.nodes.push(ruleNode);
nodes.push(node);
}
});
const firstNodeEdge = this.ruleChainModel.edges.find((edge) => edge.source === this.inputConnectorId+'');
if (firstNodeEdge) {
const firstNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(firstNodeEdge.destination);
ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
}
this.ruleChainModel.edges.forEach((edge) => {
const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source);
const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination);
if (sourceNode.component.type !== RuleNodeType.INPUT) {
const fromIndex = nodes.indexOf(sourceNode);
if (destNode.component.type === RuleNodeType.RULE_CHAIN) {
const ruleChainConnection = {
fromIndex,
targetRuleChainId: {entityType: EntityType.RULE_CHAIN, id: destNode.targetRuleChainId},
additionalInfo: destNode.additionalInfo ? destNode.additionalInfo : {}
} as RuleChainConnectionInfo;
ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x);
ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y);
ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
edge.labels.forEach((label) => {
const newRuleChainConnection = deepClone(ruleChainConnection);
newRuleChainConnection.type = label;
ruleChainMetaData.ruleChainConnections.push(newRuleChainConnection);
});
} else {
const toIndex = nodes.indexOf(destNode);
const nodeConnection = {
fromIndex,
toIndex
} as NodeConnectionInfo;
edge.labels.forEach((label) => {
const newNodeConnection = deepClone(nodeConnection);
newNodeConnection.type = label;
ruleChainMetaData.connections.push(newNodeConnection);
});
}
}
});
this.ruleChainService.saveAndGetResolvedRuleChainMetadata(ruleChainMetaData).subscribe((ruleChainMetaData) => {
this.ruleChainMetaData = ruleChainMetaData;
if (this.isImport) {
this.isDirtyValue = false;
this.isImport = false;
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
} else {
this.createRuleChainModel();
}
});
});
}
revertRuleChain() {

45
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts

@ -16,7 +16,14 @@
import * as AngularCore from '@angular/core';
import { Injectable, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
import {
ActivatedRouteSnapshot,
CanActivate,
Resolve, Router,
RouterModule,
RouterStateSnapshot,
Routes, UrlTree
} from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
@ -41,6 +48,7 @@ import * as TranslateCore from '@ngx-translate/core';
import * as TbCore from '@core/public-api';
import * as TbShared from '@shared/public-api';
import * as TbHomeComponents from '@home/components/public-api';
import { ItemBufferService } from '@core/public-api';
declare const SystemJS;
@ -98,6 +106,23 @@ export class RuleNodeComponentsResolver implements Resolve<Array<RuleNodeCompone
}
}
@Injectable()
export class RuleChainImportGuard implements CanActivate {
constructor(private itembuffer: ItemBufferService,
private router: Router) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (this.itembuffer.hasRuleChainImport()) {
return true;
} else {
return this.router.parseUrl('ruleChains');
}
}
}
export const ruleChainBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => {
let label: string = component.ruleChain.name;
if (component.ruleChain.root) {
@ -153,6 +178,7 @@ const routes: Routes = [
{
path: 'ruleChain/import',
component: RuleChainPageComponent,
canActivate: [RuleChainImportGuard],
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
@ -164,8 +190,6 @@ const routes: Routes = [
import: true
},
resolve: {
ruleChain: 'importRuleChain',
ruleChainMetaData: 'importRuleChainMetadata',
ruleNodeComponents: RuleNodeComponentsResolver
}
}
@ -182,20 +206,7 @@ const routes: Routes = [
RuleChainResolver,
ResolvedRuleChainMetaDataResolver,
RuleNodeComponentsResolver,
{
provide: 'importRuleChain',
useValue: (route: ActivatedRouteSnapshot) => {
const ruleChainImport: RuleChainImport = route.params.ruleChainImport;
return ruleChainImport.ruleChain;
}
},
{
provide: 'importRuleChainMetadata',
useValue: (route: ActivatedRouteSnapshot) => {
const ruleChainImport: RuleChainImport = route.params.ruleChainImport;
return ruleChainImport.metadata;
}
}
RuleChainImportGuard
]
})
export class RuleChainRoutingModule { }

15
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -32,6 +32,8 @@ import {RuleChainService} from '@core/http/rule-chain.service';
import {RuleChainComponent} from '@modules/home/pages/rulechain/rulechain.component';
import {DialogService} from '@core/services/dialog.service';
import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { ItemBufferService } from '@core/services/item-buffer.service';
@Injectable()
export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<RuleChain>> {
@ -40,6 +42,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
constructor(private ruleChainService: RuleChainService,
private dialogService: DialogService,
private importExport: ImportExportService,
private itembuffer: ItemBufferService,
private translate: TranslateService,
private datePipe: DatePipe,
private router: Router) {
@ -120,8 +124,12 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.importRuleChain().subscribe((ruleChainImport) => {
if (ruleChainImport) {
this.itembuffer.storeRuleChainImport(ruleChainImport);
this.router.navigateByUrl(`ruleChains/ruleChain/import`);
}
});
}
openRuleChain($event: Event, ruleChain: RuleChain) {
@ -135,8 +143,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.importExport.exportRuleChain(ruleChain.id.id);
}
setRootRuleChain($event: Event, ruleChain: RuleChain) {

31
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
@ -24,25 +24,21 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActivatedRoute, Router } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Observable, of, Subscription } from 'rxjs';
import { of } from 'rxjs';
import { Widget, widgetType } from '@app/shared/models/widget.models';
import { WidgetService } from '@core/http/widget.service';
import { map, mergeMap, share } from 'rxjs/operators';
import { map, mergeMap } from 'rxjs/operators';
import { DialogService } from '@core/services/dialog.service';
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models';
import { IAliasController } from '@app/core/api/widget-api.models';
import { toWidgetInfo } from '@home/models/widget-component.models';
import { DummyAliasController } from '@core/api/alias-controller';
import {
DeviceCredentialsDialogComponent,
DeviceCredentialsDialogData
} from '@home/pages/device/device-credentials-dialog.component';
import { DeviceCredentials } from '@shared/models/device.models';
import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models';
import { AliasController } from '@core/api/alias-controller';
import { MatDialog } from '@angular/material/dialog';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
@Component({
selector: 'tb-widget-library',
@ -86,7 +82,14 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
onRemoveWidget: this.removeWidgetType.bind(this)
};
aliasController: IAliasController = new DummyAliasController();
aliasController: IAliasController = new AliasController(this.utils,
this.entityService,
() => { return {
getStateParams(): StateParams {
return {};
}
} as IStateController},
{});
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
@ -97,7 +100,9 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
private dialogService: DialogService,
private importExport: ImportExportService,
private dialog: MatDialog,
private translate: TranslateService) {
private translate: TranslateService,
private utils: UtilsService,
private entityService: EntityService) {
super(store);
this.authUser = getCurrentAuthUser(this.store);

2
ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts

@ -190,7 +190,7 @@ export class EntityTypeListComponent implements ControlValueAccessor, OnInit, Af
this.entityTypeList = [];
value.forEach((entityType) => {
this.entityTypeList.push({
name: this.translate.instant(entityTypeTranslations.get(entityType).type),
name: entityTypeTranslations.has(entityType) ? this.translate.instant(entityTypeTranslations.get(entityType).type) : 'Unknown',
value: entityType
});
});

23
ui-ngx/src/app/shared/models/alarm.models.ts

@ -22,6 +22,8 @@ import {AlarmId} from '@shared/models/id/alarm-id';
import {EntityId} from '@shared/models/id/entity-id';
import { ActionStatus } from '@shared/models/audit-log.models';
import { TimePageLink } from '@shared/models/page/page-link';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
export enum AlarmSeverity {
CRITICAL = 'CRITICAL',
@ -103,6 +105,27 @@ export interface AlarmInfo extends Alarm {
originatorName: string;
}
export const simulatedAlarm: AlarmInfo = {
tenantId: new TenantId(NULL_UUID),
createdTime: new Date().getTime(),
startTs: new Date().getTime(),
endTs: 0,
ackTs: 0,
clearTs: 0,
originatorName: 'Simulated',
originator: {
entityType: EntityType.DEVICE,
id: "1"
},
type: 'TEMPERATURE',
severity: AlarmSeverity.MAJOR,
status: AlarmStatus.ACTIVE_UNACK,
details: {
message: "Temperature is high!"
},
propagate: false
};
export interface AlarmField {
keyName: string;
value: string;

8
ui-ngx/src/app/shared/models/event.models.ts

@ -14,15 +14,11 @@
/// limitations under the License.
///
import { ActionStatus, ActionType } from '@shared/models/audit-log.models';
import { BaseData } from '@shared/models/base-data';
import { AuditLogId } from '@shared/models/id/audit-log-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { EntityId } from '@shared/models/id/entity-id';
import { UserId } from '@shared/models/id/user-id';
import { EventId } from './id/event-id';
import { MsgDataType } from './rule-node.models';
import { ContentType } from '@shared/models/constants';
export enum EventType {
ERROR = 'ERROR',
@ -72,7 +68,7 @@ export interface DebugRuleNodeEventBody extends BaseEventBody {
msgId: string;
msgType: string;
relationType: string;
dataType: MsgDataType;
dataType: ContentType;
data: string;
metadata: string;
error: string;

11
ui-ngx/src/app/shared/models/page/page-link.ts

@ -94,14 +94,15 @@ export class PageLink {
pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch));
}
pageData.totalElements = pageData.data.length;
pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
pageData.totalPages = this.pageSize === Number.POSITIVE_INFINITY ? 1 : Math.ceil(pageData.totalElements / this.pageSize);
if (this.sortOrder) {
pageData.data = pageData.data.sort((a, b) => this.sort(a, b));
}
const startIndex = this.pageSize * this.page;
const endIndex = startIndex + this.pageSize;
pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
if (this.pageSize !== Number.POSITIVE_INFINITY) {
const startIndex = this.pageSize * this.page;
pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
}
return pageData;
}

16
ui-ngx/src/app/shared/models/page/sort-order.ts

@ -24,3 +24,19 @@ export enum Direction {
ASC = 'ASC',
DESC = 'DESC'
}
export function sortOrderFromString(strSortOrder: string): SortOrder {
let property: string;
let direction = Direction.ASC;
if (strSortOrder.startsWith('-')) {
direction = Direction.DESC;
property = strSortOrder.substring(1);
} else {
if (strSortOrder.startsWith('+')) {
property = strSortOrder.substring(1);
} else {
property = strSortOrder;
}
}
return {property, direction};
}

5
ui-ngx/src/app/shared/models/rule-chain.models.ts

@ -33,7 +33,7 @@ export interface RuleChain extends BaseData<RuleChainId> {
export interface RuleChainMetaData {
ruleChainId: RuleChainId;
firstNodeIndex: number;
firstNodeIndex?: number;
nodes: Array<RuleNode>;
connections: Array<NodeConnectionInfo>;
ruleChainConnections: Array<RuleChainConnectionInfo>;
@ -45,7 +45,8 @@ export interface ResolvedRuleChainMetaData extends RuleChainMetaData {
export interface RuleChainImport {
ruleChain: RuleChain;
metadata: ResolvedRuleChainMetaData;
metadata: RuleChainMetaData;
resolvedMetadata?: ResolvedRuleChainMetaData;
}
export interface NodeConnectionInfo {

8
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -26,18 +26,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormGroup } from '@angular/forms';
export enum MsgDataType {
JSON = 'JSON',
TEXT = 'TEXT',
BINARY = 'BINARY'
}
export interface RuleNodeConfiguration {
[key: string]: any;
}
export interface RuleNode extends BaseData<RuleNodeId> {
ruleChainId: RuleChainId;
ruleChainId?: RuleChainId;
type: string;
name: string;
debugMode: boolean;

2
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -229,7 +229,7 @@ export interface TelemetryService {
export class TelemetrySubscriber {
private dataSubject = new ReplaySubject<SubscriptionUpdate>();
private dataSubject = new ReplaySubject<SubscriptionUpdate>(1);
private reconnectSubject = new Subject();
private zone: NgZone;

2
ui-ngx/src/app/shared/models/widget.models.ts

@ -363,8 +363,6 @@ export interface WidgetConfig {
datasources?: Array<Datasource>;
targetDeviceAliasIds?: Array<string>;
[key: string]: any;
// TODO:
}
export interface Widget {

7
ui-ngx/src/styles.scss

@ -509,10 +509,10 @@ mat-label {
.mat-row {
transition: background-color .2s;
&:hover:not(.tb-current-entity) {
background-color: #f4f4f4;
background-color: rgba(221, 221, 221, 0.3);
}
&.tb-current-entity {
background-color: #e9e9e9;
background-color: rgba(221, 221, 221, 0.65);
}
}
@ -561,6 +561,9 @@ mat-label {
text-overflow: ellipsis;
white-space: nowrap;
}
&.mat-table-sticky {
background: transparent;
}
}
.mat-cell, .mat-footer-cell {

Loading…
Cancel
Save