Browse Source

Widget API implementation.

pull/2147/head
Igor Kulikov 7 years ago
parent
commit
b48663bab9
  1. 4
      ui-ngx/angular.json
  2. 109
      ui-ngx/package-lock.json
  3. 3
      ui-ngx/package.json
  4. 39
      ui-ngx/src/app/core/api/alias-controller.ts
  5. 42
      ui-ngx/src/app/core/api/datasource.service.ts
  6. 116
      ui-ngx/src/app/core/api/widget-api.models.ts
  7. 396
      ui-ngx/src/app/core/api/widget-subscription.ts
  8. 17
      ui-ngx/src/app/core/http/alarm.service.ts
  9. 39
      ui-ngx/src/app/core/http/dashboard.service.ts
  10. 202
      ui-ngx/src/app/core/http/entity.service.ts
  11. 68
      ui-ngx/src/app/core/services/raf.service.ts
  12. 31
      ui-ngx/src/app/core/services/time.service.ts
  13. 93
      ui-ngx/src/app/core/services/utils.service.ts
  14. 8
      ui-ngx/src/app/core/utils.ts
  15. 7
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  16. 21
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  17. 6
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  18. 6
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  19. 15
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  20. 12
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  21. 4
      ui-ngx/src/app/modules/home/components/widget/widget.component.scss
  22. 443
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  23. 4
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  24. 18
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  25. 3
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  26. 7
      ui-ngx/src/app/shared/components/time/datetime-period.component.ts
  27. 25
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  28. 10
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  29. 65
      ui-ngx/src/app/shared/models/alarm.models.ts
  30. 367
      ui-ngx/src/app/shared/models/material.models.ts
  31. 295
      ui-ngx/src/app/shared/models/time/time.models.ts
  32. 65
      ui-ngx/src/app/shared/models/widget.models.ts
  33. 2
      ui-ngx/src/tsconfig.app.json
  34. 19
      ui-ngx/src/typings.d.ts
  35. 3
      ui-ngx/tsconfig.json

4
ui-ngx/angular.json

@ -31,11 +31,13 @@
{ "glob": "worker-javascript.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"node_modules/jquery.terminal/css/jquery.terminal.min.css"
],
"scripts": [
"node_modules/javascript-detect-element-resize/detect-element-resize.js",
"node_modules/jquery/dist/jquery.min.js",
"node_modules/jquery.terminal/js/jquery.terminal.min.js",
"node_modules/ace-builds/src-min/ace.js",
"node_modules/ace-builds/src-min/ext-language_tools.js",
"node_modules/ace-builds/src-min/ext-searchbox.js",

109
ui-ngx/package-lock.json

@ -1152,6 +1152,15 @@
"@types/jasmine": "*"
}
},
"@types/jquery": {
"version": "3.3.31",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -1176,6 +1185,11 @@
"integrity": "sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA==",
"dev": true
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg=="
},
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@ -2666,6 +2680,17 @@
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
"dev": true
},
"clipboard": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
"integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"cliui": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
@ -3427,6 +3452,21 @@
}
}
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"requires": {
"clone": "^1.0.2"
},
"dependencies": {
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
}
}
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3545,6 +3585,12 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"optional": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -5173,6 +5219,15 @@
}
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"optional": true,
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.1.tgz",
@ -6345,6 +6400,27 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"jquery.terminal": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/jquery.terminal/-/jquery.terminal-2.8.0.tgz",
"integrity": "sha512-veyI105Vvro7MEInnfm7ZivToJCtFl6t2wSiV26CODl+1yv+zkbzibbYqAXQIG9Cpye2DvH0+aOUfSjnzCBV/A==",
"requires": {
"@types/jquery": "3.3.29",
"jquery": "~3",
"prismjs": "^1.16.0",
"wcwidth": "^1.0.1"
},
"dependencies": {
"@types/jquery": {
"version": "3.3.29",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.29.tgz",
"integrity": "sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==",
"requires": {
"@types/sizzle": "*"
}
}
}
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -8315,6 +8391,14 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
},
"prismjs": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.17.1.tgz",
"integrity": "sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q==",
"requires": {
"clipboard": "^2.0.0"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -9049,6 +9133,12 @@
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-4.2.1.tgz",
"integrity": "sha512-PLSp6f5XdhvjCCCO8OjavRfzkSGL3Qmdm7P82bxyU8HDDDBhDV3UckRaYcRa/NDNTYt8YBpzjoLWHUAejmOjLg=="
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"optional": true
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -10138,6 +10228,17 @@
"setimmediate": "^1.0.4"
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"optional": true
},
"tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -10687,6 +10788,14 @@
"minimalistic-assert": "^1.0.0"
}
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"requires": {
"defaults": "^1.0.3"
}
},
"webdriver-js-extender": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz",

3
ui-ngx/package.json

@ -41,12 +41,14 @@
"hammerjs": "^2.0.8",
"javascript-detect-element-resize": "^0.5.3",
"jquery": "^3.4.1",
"jquery.terminal": "^2.8.0",
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"ngx-clipboard": "^12.2.0",
"ngx-translate-messageformat-compiler": "^4.5.0",
"rxjs": "~6.5.2",
"screenfull": "^4.2.1",
"tinycolor2": "^1.4.1",
"tslib": "^1.10.0",
"typeface-roboto": "^0.0.75",
"zone.js": "~0.9.1"
@ -59,6 +61,7 @@
"@angular/language-service": "~8.2.0",
"@types/jasmine": "~3.4.0",
"@types/jasminewd2": "~2.0.6",
"@types/jquery": "^3.3.31",
"@types/node": "~10.14.15",
"codelyzer": "~5.1.0",
"compression-webpack-plugin": "^3.0.0",

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

@ -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.
///
import { IAliasController, AliasInfo } from '@core/api/widget-api.models';
import { Observable, of, Subject } from 'rxjs';
import { Datasource } from '@app/shared/models/widget.models';
import { deepClone } from '@core/utils';
export class DummyAliasController implements IAliasController {
entityAliasesChanged: Observable<Array<string>>;
[key: string]: any | null;
constructor() {
this.entityAliasesChanged = new Subject<Array<string>>().asObservable();
}
getAliasInfo(aliasId): Observable<AliasInfo> {
return of(null);
}
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
return of(deepClone(datasources));
}
}

42
ui-ngx/src/app/core/api/datasource.service.ts

@ -0,0 +1,42 @@
///
/// 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 { Injectable } from '@angular/core';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@app/shared/models/entity-type.models';
export interface DatasourceListener {
entityType: EntityType;
entityId: string;
}
@Injectable({
providedIn: 'root'
})
export class DatasourceService {
constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {}
public subscribeToDatasource(listener: DatasourceListener) {
// TODO:
}
public unsubscribeFromDatasource(listener: DatasourceListener) {
// TODO:
}
}

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

@ -16,11 +16,23 @@
import { Observable } from 'rxjs';
import { EntityId } from '@app/shared/models/id/entity-id';
import { WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
import {
WidgetActionDescriptor,
widgetType,
LegendConfig,
LegendData,
Datasource,
DatasourceData, DataSet, DatasourceType, KeyInfo
} from '@shared/models/widget.models';
import { TimeService } from '../services/time.service';
import { DeviceService } from '../http/device.service';
import { AlarmService } from '../http/alarm.service';
import { UtilsService } from '@core/services/utils.service';
import { Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { HttpErrorResponse } from '@angular/common/http';
import { DatasourceService } from '@core/api/datasource.service';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void;
@ -28,7 +40,7 @@ export interface TimewindowFunctions {
}
export interface WidgetSubscriptionApi {
createSubscription: (options: WidgetSubscriptionOptions, subscribe: boolean) => Observable<IWidgetSubscription>;
createSubscription: (options: WidgetSubscriptionOptions, subscribe?: boolean) => Observable<IWidgetSubscription>;
createSubscriptionFromInfo: (type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>,
options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean)
=> Observable<IWidgetSubscription>;
@ -52,7 +64,21 @@ export interface WidgetActionsApi {
elementClick: ($event: Event) => void;
}
export interface AliasInfo {
stateEntity?: boolean;
currentEntity?: {
id: string;
entityType: EntityType;
name?: string;
};
[key: string]: any | null;
// TODO:
}
export interface IAliasController {
entityAliasesChanged: Observable<Array<string>>;
getAliasInfo(aliasId): Observable<AliasInfo>;
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>;
[key: string]: any | null;
// TODO:
}
@ -82,39 +108,109 @@ export interface EntityInfo {
}
export interface SubscriptionInfo {
type: DatasourceType;
name?: string;
entityType?: EntityType;
entityId?: string;
entityIds?: Array<string>;
entityName?: string;
entityNamePrefix?: string;
timeseries?: Array<KeyInfo>;
attributes?: Array<KeyInfo>;
functions?: Array<KeyInfo>;
alarmFields?: Array<KeyInfo>;
[key: string]: any;
// TODO:
}
export interface WidgetSubscriptionContext {
timeService: TimeService;
deviceService: DeviceService;
alarmService: AlarmService;
datasourceService: DatasourceService;
utils: UtilsService;
widgetUtils: IWidgetUtils;
dashboardTimewindowApi: TimewindowFunctions;
getServerTimeDiff: Observable<number>;
getServerTimeDiff: () => Observable<number>;
aliasController: IAliasController;
[key: string]: any;
// TODO:
}
export interface WidgetSubscriptionCallbacks {
onDataUpdated?: (subscription: IWidgetSubscription) => void;
onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
dataLoading?: (subscription: IWidgetSubscription) => void;
legendDataUpdated?: (subscription: IWidgetSubscription) => void;
timeWindowUpdated?: (subscription: IWidgetSubscription, timeWindowConfig: Timewindow) => void;
rpcStateChanged?: (subscription: IWidgetSubscription) => void;
onRpcSuccess?: (subscription: IWidgetSubscription) => void;
onRpcFailed?: (subscription: IWidgetSubscription) => void;
onRpcErrorCleared?: (subscription: IWidgetSubscription) => void;
}
export interface WidgetSubscriptionOptions {
type: widgetType;
stateData?: boolean;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;
datasources?: Array<Datasource>;
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;
useDashboardTimewindow?: boolean;
displayTimewindow?: boolean;
timeWindowConfig?: Timewindow;
dashboardTimewindow?: Timewindow;
legendConfig?: LegendConfig;
decimals?: number;
units?: string;
callbacks?: WidgetSubscriptionCallbacks;
[key: string]: any;
// TODO:
}
export interface IWidgetSubscription {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void;
onResetTimewindow: () => void;
id: string;
init$: Observable<IWidgetSubscription>;
ctx: WidgetSubscriptionContext;
type: widgetType;
callbacks: WidgetSubscriptionCallbacks;
sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
loadingData: boolean;
useDashboardTimewindow: boolean;
legendData: LegendData;
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindow?: Timewindow;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;
rpcEnabled?: boolean;
executingRpcRequest?: boolean;
rpcErrorText?: string;
rpcRejection?: HttpErrorResponse;
getFirstEntityInfo(): EntityInfo;
onAliasesChanged(aliasIds: Array<string>): boolean;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void;
onResetTimewindow(): void;
updateTimewindowConfig(newTimewindow: Timewindow): void;
clearRpcError: () => void;
sendOneWayCommand(method: string, params?: any, timeout?: number): Observable<any>;
sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable<any>;
clearRpcError(): void;
getFirstEntityInfo: () => EntityInfo;
subscribe(): void;
destroy(): void;

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

@ -0,0 +1,396 @@
///
/// 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 {
EntityInfo,
IWidgetSubscription,
WidgetSubscriptionCallbacks,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import {
DataSet,
Datasource,
DatasourceData,
LegendConfig,
LegendData,
LegendKey,
LegendKeyData,
widgetType
} from '@app/shared/models/widget.models';
import { HttpErrorResponse } from '@angular/common/http';
import { Timewindow } from '@app/shared/models/time/time.models';
import { Observable, of, ReplaySubject, Subject } 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';
import { deepClone, isDefined } from '@core/utils';
import { AlarmSourceListener } from '@core/http/alarm.service';
import { DatasourceListener } from '@core/api/datasource.service';
export class WidgetSubscription implements IWidgetSubscription {
id: string;
ctx: WidgetSubscriptionContext;
type: widgetType;
callbacks: WidgetSubscriptionCallbacks;
timeWindow: Timewindow;
originalTimewindow: Timewindow;
timeWindowConfig: Timewindow;
subscriptionTimewindow: Timewindow;
useDashboardTimewindow: boolean;
data: Array<DatasourceData>;
datasources: Array<Datasource>;
datasourceListeners: Array<DatasourceListener>;
hiddenData: Array<{ data: DataSet }>;
legendData: LegendData;
legendConfig: LegendConfig;
caulculateLegendData: boolean;
displayLegend: boolean;
stateData: boolean;
decimals: number;
units: string;
alarms: Array<AlarmInfo>;
alarmSource: Datasource;
alarmSearchStatus: AlarmSearchStatus;
alarmsPollingInterval: number;
alarmSourceListener: AlarmSourceListener;
loadingData: boolean;
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
init$: Observable<IWidgetSubscription>;
cafs: {[cafId: string]: CancelAnimationFrame} = {};
targetDeviceAliasId: string;
targetDeviceId: string;
targetDeviceName: string;
executingSubjects: Array<Subject<any>>;
constructor(subscriptionContext: WidgetSubscriptionContext, options: WidgetSubscriptionOptions) {
const subscriptionSubject = new ReplaySubject<IWidgetSubscription>();
this.init$ = subscriptionSubject.asObservable();
this.ctx = subscriptionContext;
this.type = options.type;
this.id = this.ctx.utils.guid();
this.callbacks = options.callbacks;
if (this.type === widgetType.rpc) {
this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || (() => {});
this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || (() => {});
this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || (() => {});
this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || (() => {});
this.targetDeviceAliasIds = options.targetDeviceAliasIds;
this.targetDeviceIds = options.targetDeviceIds;
this.targetDeviceAliasId = null;
this.targetDeviceId = null;
this.rpcRejection = null;
this.rpcErrorText = null;
this.rpcEnabled = false;
this.executingRpcRequest = false;
this.executingSubjects = [];
this.initRpc().subscribe(() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
});
} else if (this.type === widgetType.alarm) {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
this.alarmSource = options.alarmSource;
this.alarmSearchStatus = isDefined(options.alarmSearchStatus) ?
options.alarmSearchStatus : AlarmSearchStatus.ANY;
this.alarmsPollingInterval = isDefined(options.alarmsPollingInterval) ?
options.alarmsPollingInterval : 5000;
this.alarmSourceListener = null;
this.alarms = [];
this.originalTimewindow = null;
this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow;
if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else {
this.timeWindowConfig = deepClone(options.timeWindowConfig);
}
this.subscriptionTimewindow = null;
this.loadingData = false;
this.displayLegend = false;
this.initAlarmSubscription().subscribe(() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
},
() => {
subscriptionSubject.error(null);
});
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {});
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.datasourceListeners = [];
this.data = [];
this.hiddenData = [];
this.originalTimewindow = null;
this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow;
this.stateData = options.stateData;
if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else {
this.timeWindowConfig = deepClone(options.timeWindowConfig);
}
this.subscriptionTimewindow = null;
this.units = options.units || '';
this.decimals = isDefined(options.decimals) ? options.decimals : 2;
this.loadingData = false;
if (options.legendConfig) {
this.legendConfig = options.legendConfig;
this.legendData = {
keys: [],
data: []
};
this.displayLegend = true;
} else {
this.displayLegend = false;
}
this.caulculateLegendData = this.displayLegend &&
this.type === widgetType.timeseries &&
(this.legendConfig.showMin === true ||
this.legendConfig.showMax === true ||
this.legendConfig.showAvg === true ||
this.legendConfig.showTotal === true);
this.initDataSubscription().subscribe(() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
},
() => {
subscriptionSubject.error(null);
});
}
}
private initRpc(): Observable<any> {
const initRpcSubject = new ReplaySubject();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).subscribe(
(aliasInfo) => {
if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType === EntityType.DEVICE) {
this.targetDeviceId = aliasInfo.currentEntity.id;
this.targetDeviceName = aliasInfo.currentEntity.name;
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.utils.widgetEditMode ? true : false;
}
this.callbacks.rpcStateChanged(this);
initRpcSubject.next();
initRpcSubject.complete();
} else {
this.rpcEnabled = false;
this.callbacks.rpcStateChanged(this);
initRpcSubject.next();
initRpcSubject.complete();
}
},
() => {
this.rpcEnabled = false;
this.callbacks.rpcStateChanged(this);
initRpcSubject.next();
initRpcSubject.complete();
}
);
} else {
if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
this.targetDeviceId = this.targetDeviceIds[0];
}
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.utils.widgetEditMode ? true : false;
}
this.callbacks.rpcStateChanged(this);
initRpcSubject.next();
initRpcSubject.complete();
}
return initRpcSubject.asObservable();
}
private initAlarmSubscription(): Observable<any> {
// TODO:
return of(null);
}
private initDataSubscription(): Observable<any> {
const initDataSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.configureData();
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
} else {
this.ctx.aliasController.resolveDatasources(this.datasources).subscribe(
(datasources) => {
this.datasources = datasources;
this.configureData();
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
},
() => {
initDataSubscriptionSubject.error(null);
}
);
}
});
return initDataSubscriptionSubject.asObservable();
}
private configureData() {
let dataIndex = 0;
this.datasources.forEach((datasource) => {
datasource.dataKeys.forEach((dataKey) => {
dataKey.hidden = false;
dataKey.pattern = dataKey.label;
const datasourceData: DatasourceData = {
datasource,
dataKey,
data: []
};
this.data.push(datasourceData);
this.hiddenData.push({data: []});
if (this.displayLegend) {
const legendKey: LegendKey = {
dataKey,
dataIndex: dataIndex++
};
this.legendData.keys.push(legendKey);
const legendKeyData: LegendKeyData = {
min: null,
max: null,
avg: null,
total: null,
hidden: false
};
this.legendData.data.push(legendKeyData);
}
});
});
if (this.displayLegend) {
this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
// TODO:
}
if (this.type === widgetType.timeseries) {
if (this.useDashboardTimewindow) {
// TODO:
} else {
// TODO:
}
}
}
getFirstEntityInfo(): EntityInfo {
return undefined;
}
updateTimewindowConfig(newTimewindow: Timewindow): void {
}
onResetTimewindow(): void {
}
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void {
}
sendOneWayCommand(method: string, params?: any, timeout?: number): Observable<any> {
return undefined;
}
sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable<any> {
return undefined;
}
clearRpcError(): void {
}
subscribe(): void {
// TODO:
this.notifyDataLoaded();
}
destroy(): void {
}
private notifyDataLoading() {
this.loadingData = true;
this.callbacks.dataLoading(this);
}
private notifyDataLoaded() {
this.loadingData = false;
this.callbacks.dataLoading(this);
}
onAliasesChanged(aliasIds: Array<string>): boolean {
return false;
}
private loadStDiff(): Observable<any> {
const loadSubject = new ReplaySubject(1);
if (this.ctx.getServerTimeDiff && this.timeWindow) {
this.ctx.getServerTimeDiff().subscribe(
(stDiff) => {
this.timeWindow.stDiff = stDiff;
loadSubject.next();
loadSubject.complete();
},
() => {
this.timeWindow.stDiff = 0;
loadSubject.next();
loadSubject.complete();
}
);
} else {
if (this.timeWindow) {
this.timeWindow.stDiff = 0;
}
loadSubject.next();
loadSubject.complete();
}
return loadSubject.asObservable();
}
}

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

@ -28,6 +28,23 @@ import {
AlarmSeverity,
AlarmStatus
} from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Datasource } from '@shared/models/widget.models';
export interface AlarmSourceListener {
id?: string;
alarmSource: Datasource;
alarmsPollingInterval: number;
alarmSearchStatus: AlarmSearchStatus;
alarmsQuery: {
entityType: EntityType;
entityId: string;
alarmSearchStatus: AlarmSearchStatus;
alarmStatus: AlarmStatus;
fetchOriginator?: boolean;
onAlarms?: (alarms: Array<AlarmInfo>) => void;
};
}
@Injectable({
providedIn: 'root'

39
ui-ngx/src/app/core/http/dashboard.service.ts

@ -16,22 +16,36 @@
import {Inject, Injectable} from '@angular/core';
import {defaultHttpOptions} from './http-utils';
import {Observable} from 'rxjs/index';
import { Observable, ReplaySubject, Subject } from 'rxjs/index';
import {HttpClient} from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link';
import {PageData} from '@shared/models/page/page-data';
import {Dashboard, DashboardInfo} from '@shared/models/dashboard.models';
import {WINDOW} from '@core/services/window.service';
import { ActivationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DashboardService {
stDiffSubject: Subject<number>;
constructor(
private http: HttpClient,
private router: Router,
@Inject(WINDOW) private window: Window
) { }
) {
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
() => {
if (this.stDiffSubject) {
this.stDiffSubject.complete();
this.stDiffSubject = null;
}
}
);
}
public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
@ -124,4 +138,25 @@ export class DashboardService {
return null;
}
public getServerTimeDiff(): Observable<number> {
if (this.stDiffSubject) {
return this.stDiffSubject.asObservable();
} else {
this.stDiffSubject = new ReplaySubject<number>(1);
const url = '/api/dashboard/serverTime';
const ct1 = Date.now();
this.http.get<number>(url, defaultHttpOptions(true)).subscribe(
(st) => {
const ct2 = Date.now();
const stDiff = Math.ceil(st - (ct1 + ct2) / 2);
this.stDiffSubject.next(stDiff);
},
() => {
this.stDiffSubject.error(null);
}
);
return this.stDiffSubject.asObservable();
}
}
}

202
ui-ngx/src/app/core/http/entity.service.ts

@ -14,33 +14,35 @@
/// limitations under the License.
///
import {Injectable} from '@angular/core';
import {EMPTY, forkJoin, Observable, of, throwError} from 'rxjs/index';
import {HttpClient} from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link';
import {AliasEntityType, EntityType} from '@shared/models/entity-type.models';
import {BaseData} from '@shared/models/base-data';
import {EntityId} from '@shared/models/id/entity-id';
import {DeviceService} from '@core/http/device.service';
import {TenantService} from '@core/http/tenant.service';
import {CustomerService} from '@core/http/customer.service';
import {UserService} from './user.service';
import {DashboardService} from '@core/http/dashboard.service';
import {Direction} from '@shared/models/page/sort-order';
import {PageData} from '@shared/models/page/page-data';
import {getCurrentAuthUser} from '../auth/auth.selectors';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {Authority} from '@shared/models/authority.enum';
import {Tenant} from '@shared/models/tenant.model';
import {concatMap, expand, map, toArray} from 'rxjs/operators';
import {Customer} from '@app/shared/models/customer.model';
import {AssetService} from '@core/http/asset.service';
import {EntityViewService} from '@core/http/entity-view.service';
import {DataKeyType} from '@shared/models/telemetry/telemetry.models';
import {DeviceInfo} from '@shared/models/device.models';
import {defaultHttpOptions} from '@core/http/http-utils';
import {RuleChainService} from '@core/http/rule-chain.service';
import { Injectable } from '@angular/core';
import { EMPTY, forkJoin, Observable, of, throwError } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { DeviceService } from '@core/http/device.service';
import { TenantService } from '@core/http/tenant.service';
import { CustomerService } from '@core/http/customer.service';
import { UserService } from './user.service';
import { DashboardService } from '@core/http/dashboard.service';
import { Direction } from '@shared/models/page/sort-order';
import { PageData } from '@shared/models/page/page-data';
import { getCurrentAuthUser } from '../auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { Tenant } from '@shared/models/tenant.model';
import { catchError, concatMap, expand, map, toArray } from 'rxjs/operators';
import { Customer } from '@app/shared/models/customer.model';
import { AssetService } from '@core/http/asset.service';
import { EntityViewService } from '@core/http/entity-view.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { defaultHttpOptions } from '@core/http/http-utils';
import { RuleChainService } from '@core/http/rule-chain.service';
import { SubscriptionInfo } from '@core/api/widget-api.models';
import { Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
@Injectable({
providedIn: 'root'
@ -57,7 +59,8 @@ export class EntityService {
private customerService: CustomerService,
private userService: UserService,
private ruleChainService: RuleChainService,
private dashboardService: DashboardService
private dashboardService: DashboardService,
private utils: UtilsService
) { }
private getEntityObservable(entityType: EntityType, entityId: string,
@ -408,4 +411,147 @@ export class EntityService {
)
);
}
public createDatasourcesFromSubscriptionsInfo(subscriptionsInfo: Array<SubscriptionInfo>): Observable<Array<Datasource>> {
const observables = new Array<Observable<Array<Datasource>>>();
subscriptionsInfo.forEach((subscriptionInfo) => {
observables.push(this.createDatasourcesFromSubscriptionInfo(subscriptionInfo));
});
return forkJoin(observables).pipe(
map((arrayOfDatasources) => {
const result = new Array<Datasource>();
arrayOfDatasources.forEach((datasources) => {
result.push(...datasources);
});
this.utils.generateColors(result);
return result;
})
);
}
public createAlarmSourceFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Datasource> {
if (subscriptionInfo.entityId && subscriptionInfo.entityType) {
return this.getEntity(subscriptionInfo.entityType, subscriptionInfo.entityId,
true, true).pipe(
map((entity) => {
const alarmSource = this.createDatasourceFromSubscription(subscriptionInfo, entity);
this.utils.generateColors([alarmSource]);
return alarmSource;
})
);
} else {
return throwError(null);
}
}
private createDatasourcesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<Datasource>> {
subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo);
if (subscriptionInfo.type === DatasourceType.entity) {
return this.resolveEntitiesFromSubscriptionInfo(subscriptionInfo).pipe(
map((entities) => {
const datasources = new Array<Datasource>();
entities.forEach((entity) => {
datasources.push(this.createDatasourceFromSubscription(subscriptionInfo, entity));
});
return datasources;
})
);
} else if (subscriptionInfo.type === DatasourceType.function) {
return of([this.createDatasourceFromSubscription(subscriptionInfo)]);
} else {
return of([]);
}
}
private resolveEntitiesFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable<Array<BaseData<EntityId>>> {
if (subscriptionInfo.entityId) {
if (subscriptionInfo.entityName) {
const entity: BaseData<EntityId> = {
id: {id: subscriptionInfo.entityId, entityType: subscriptionInfo.entityType},
name: subscriptionInfo.entityName
};
return of([entity]);
} else {
return this.getEntity(subscriptionInfo.entityType, subscriptionInfo.entityId,
true, true).pipe(
map((entity) => [entity]),
catchError(e => of([]))
);
}
} else if (subscriptionInfo.entityName || subscriptionInfo.entityNamePrefix || subscriptionInfo.entityIds) {
let entitiesObservable: Observable<Array<BaseData<EntityId>>>;
if (subscriptionInfo.entityName) {
entitiesObservable = this.getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityName,
1, null, true, true);
} else if (subscriptionInfo.entityNamePrefix) {
entitiesObservable = this.getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityNamePrefix,
100, null, true, true);
} else if (subscriptionInfo.entityIds) {
entitiesObservable = this.getEntities(subscriptionInfo.entityType, subscriptionInfo.entityIds, true, true);
}
return entitiesObservable.pipe(
catchError(e => of([]))
);
} else {
return of([]);
}
}
private validateSubscriptionInfo(subscriptionInfo: SubscriptionInfo): SubscriptionInfo {
// @ts-ignore
if (subscriptionInfo.type === 'device') {
subscriptionInfo.type = DatasourceType.entity;
subscriptionInfo.entityType = EntityType.DEVICE;
if (subscriptionInfo.deviceId) {
subscriptionInfo.entityId = subscriptionInfo.deviceId;
} else if (subscriptionInfo.deviceName) {
subscriptionInfo.entityName = subscriptionInfo.deviceName;
} else if (subscriptionInfo.deviceNamePrefix) {
subscriptionInfo.entityNamePrefix = subscriptionInfo.deviceNamePrefix;
} else if (subscriptionInfo.deviceIds) {
subscriptionInfo.entityIds = subscriptionInfo.deviceIds;
}
}
return subscriptionInfo;
}
private createDatasourceFromSubscription(subscriptionInfo: SubscriptionInfo, entity?: BaseData<EntityId>): Datasource {
let datasource: Datasource;
if (subscriptionInfo.type === DatasourceType.entity) {
datasource = {
type: subscriptionInfo.type,
entityName: entity.name,
name: entity.name,
entityType: subscriptionInfo.entityType,
entityId: entity.id.id,
dataKeys: []
};
} else if (subscriptionInfo.type === DatasourceType.function) {
datasource = {
type: subscriptionInfo.type,
name: subscriptionInfo.name || DatasourceType.function,
dataKeys: []
};
}
if (subscriptionInfo.timeseries) {
this.createDatasourceKeys(subscriptionInfo.timeseries, DataKeyType.timeseries, datasource);
}
if (subscriptionInfo.attributes) {
this.createDatasourceKeys(subscriptionInfo.attributes, DataKeyType.attribute, datasource);
}
if (subscriptionInfo.functions) {
this.createDatasourceKeys(subscriptionInfo.functions, DataKeyType.function, datasource);
}
if (subscriptionInfo.alarmFields) {
this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource);
}
return datasource;
}
private createDatasourceKeys(keyInfos: Array<KeyInfo>, type: DataKeyType, datasource: Datasource) {
keyInfos.forEach((keyInfo) => {
const dataKey = this.utils.createKey(keyInfo, type);
datasource.dataKeys.push(dataKey);
});
}
}

68
ui-ngx/src/app/core/services/raf.service.ts

@ -0,0 +1,68 @@
///
/// 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 { Inject, Injectable, NgZone } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { WINDOW } from '@core/services/window.service';
export type CancelAnimationFrame = () => void;
@Injectable({
providedIn: 'root'
})
export class RafService {
private rafFunction: (frameCallback: () => void) => CancelAnimationFrame;
private rafSupported: boolean;
constructor(
@Inject(WINDOW) private window: Window,
private ngZone: NgZone
) {
const requestAnimationFrame: (frameCallback: () => void) => number = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame;
const cancelAnimationFrame = window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
// @ts-ignore
window.webkitCancelRequestAnimationFrame;
this.rafSupported = !!requestAnimationFrame;
if (this.rafSupported) {
this.rafFunction = (frameCallback: () => void) => {
const id = requestAnimationFrame(frameCallback);
return () => {
cancelAnimationFrame(id);
};
};
} else {
this.rafFunction = (frameCallback: () => void) => {
const timeoutId = setTimeout(frameCallback, 16.66);
return () => {
clearTimeout(timeoutId);
};
};
}
}
public raf(frameCallback: () => void, runInZone = false): CancelAnimationFrame {
if (runInZone) {
return this.rafFunction(frameCallback);
} else {
return this.ngZone.runOutsideAngular(() => this.rafFunction(frameCallback));
}
}
}

31
ui-ngx/src/app/core/services/time.service.ts

@ -15,11 +15,11 @@
///
import { Injectable } from '@angular/core';
import { DAY, defaultTimeIntervals, MINUTE, SECOND, Timewindow } from '@shared/models/time/time.models';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {defaultHttpOptions} from '@core/http/http-utils';
import {map} from 'rxjs/operators';
import { AggregationType, DAY, defaultTimeIntervals, SECOND, Timewindow, defaultTimewindow } from '@shared/models/time/time.models';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { defaultHttpOptions } from '@core/http/http-utils';
import { map } from 'rxjs/operators';
export interface TimeInterval {
name: string;
@ -95,6 +95,20 @@ export class TimeService {
return matchedInterval.value;
}
public boundIntervalToTimewindow(timewindow: number, intervalMs: number, aggType: AggregationType): number {
if (aggType === AggregationType.NONE) {
return SECOND;
} else {
const min = this.minIntervalLimit(timewindow);
const max = this.maxIntervalLimit(timewindow);
if (intervalMs) {
return this.toBound(intervalMs, min, max, intervalMs);
} else {
return this.boundToPredefinedInterval(min, max, this.avgInterval(timewindow));
}
}
}
public getMaxDatapointsLimit(): number {
return this.maxDatapointsLimit;
}
@ -103,6 +117,11 @@ export class TimeService {
return MIN_LIMIT;
}
public avgInterval(timewindow: number): number {
const avg = timewindow / 200;
return this.boundMinInterval(avg);
}
public minIntervalLimit(timewindowMs: number): number {
const min = timewindowMs / 500;
return this.boundMinInterval(min);
@ -114,7 +133,7 @@ export class TimeService {
}
public defaultTimewindow(): Timewindow {
return Timewindow.defaultTimewindow(this);
return defaultTimewindow(this);
}
private toBound(value: number, min: number, max: number, defValue: number): number {

93
ui-ngx/src/app/core/services/utils.service.ts

@ -17,10 +17,15 @@
import { Inject, Injectable } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import { isUndefined } from '@core/utils';
import { isUndefined, isDefined } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
@Injectable({
providedIn: 'root'
@ -112,4 +117,90 @@ export class UtilsService {
return result;
}
public guid(): string {
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
public validateDatasources(datasources: Array<Datasource>): Array<Datasource> {
datasources.forEach((datasource) => {
// @ts-ignore
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
datasource.entityType = EntityType.DEVICE;
if (datasource.deviceId) {
datasource.entityId = datasource.deviceId;
} else if (datasource.deviceAliasId) {
datasource.entityAliasId = datasource.deviceAliasId;
}
if (datasource.deviceName) {
datasource.entityName = datasource.deviceName;
}
}
if (datasource.type === DatasourceType.entity && datasource.entityId) {
datasource.name = datasource.entityName;
}
});
return datasources;
}
public getMaterialColor(index) {
const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value;
}
public createKey(keyInfo: KeyInfo, type: DataKeyType, index: number = -1): DataKey {
let label;
if (type === DataKeyType.alarm && !keyInfo.label) {
const alarmField = alarmFields[keyInfo.name];
if (alarmField) {
label = this.translate.instant(alarmField.name);
}
}
if (!label) {
label = keyInfo.label || keyInfo.name;
}
const dataKey: DataKey = {
name: keyInfo.name,
type,
label,
funcBody: keyInfo.funcBody,
settings: {},
_hash: Math.random()
};
if (keyInfo.units) {
dataKey.units = keyInfo.units;
}
if (isDefined(keyInfo.decimals)) {
dataKey.decimals = keyInfo.decimals;
}
if (keyInfo.color) {
dataKey.color = keyInfo.color;
} else if (index > -1) {
dataKey.color = this.getMaterialColor(index);
}
if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) {
dataKey.usePostProcessing = true;
dataKey.postFuncBody = keyInfo.postFuncBody;
}
return dataKey;
}
public generateColors(datasources: Array<Datasource>) {
let index = 0;
datasources.forEach((datasource) => {
datasource.dataKeys.forEach((dataKey) => {
if (!dataKey.color) {
dataKey.color = this.getMaterialColor(index);
}
index++;
});
});
}
}

8
ui-ngx/src/app/core/utils.ts

@ -294,3 +294,11 @@ function utf8ToBytes(input: string, units?: number): number[] {
}
return bytes;
}
export function deepClone<T>(obj: T): T {
if (obj) {
return JSON.parse(JSON.stringify(obj));
} else {
return obj;
}
}

7
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -49,7 +49,11 @@
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
{{widget.title}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow" aggregation="{{widget.hasAggregation}}" [ngModel]="widget.widget.config.timewindow"></tb-timewindow>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
[ngModel]="widget.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
<div [fxShow]="widget.showWidgetActions"
class="tb-widget-actions"
@ -103,6 +107,7 @@
</div>
<div fxFlex fxLayout="column" class="tb-widget-content">
<tb-widget fxFlex
#widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isMobile]="isMobileSize"

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

@ -30,7 +30,7 @@ import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Timewindow } from '@shared/models/time/time.models';
import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
import {
@ -43,7 +43,7 @@ import { merge, Observable } from 'rxjs';
import { map, share, tap } from 'rxjs/operators';
import { WidgetLayout } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, isDefined } from '@app/core/utils';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
@ -117,6 +117,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
dashboardTimewindow: Timewindow;
originalDashboardTimewindow: Timewindow;
gridsterOpts: GridsterConfig;
dashboardLoading = true;
@ -260,6 +262,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngAfterViewInit(): void {
}
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void {
if (!this.originalDashboardTimewindow) {
this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow);
}
this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow,
startTimeMs, endTimeMs, interval, this.timeService);
}
onResetTimewindow(): void {
if (this.originalDashboardTimewindow) {
this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow);
this.originalDashboardTimewindow = null;
}
}
isAutofillHeight(): boolean {
if (this.isMobileSize) {
return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false;

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

@ -52,7 +52,7 @@ import {
AddEntityDialogData,
EntityAction
} from '@home/models/entity/entity-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Timewindow, historyInterval } from '@shared/models/time/time.models';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
@ -157,7 +157,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
direction: this.entitiesTableConfig.defaultSortOrder.direction };
if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
this.timewindow = historyInterval(24 * 60 * 60 * 1000);
const currentTime = new Date().getTime();
this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime);
@ -348,7 +348,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
resetSortAndFilter(update: boolean = true) {
this.pageLink.textSearch = null;
if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
this.timewindow = historyInterval(24 * 60 * 60 * 1000);
}
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property);

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

@ -20,6 +20,7 @@ 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';
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
@ -29,6 +30,11 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
@Input()
errorMessages: string[];
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
[key: string]: any;
constructor(protected store: Store<AppState>) {

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { WidgetService } from '@core/http/widget.service';
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
@ -33,6 +33,11 @@ import { TranslateService } from '@ngx-translate/core';
import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
import { SharedModule } from '@shared/shared.module';
import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';
import { WINDOW } from '@core/services/window.service';
import * as tinycolor from 'tinycolor2';
// declare var jQuery: any;
@Injectable()
export class WidgetComponentService {
@ -48,11 +53,17 @@ export class WidgetComponentService {
private missingWidgetType: WidgetInfo;
private errorWidgetType: WidgetInfo;
constructor(private dynamicComponentFactoryService: DynamicComponentFactoryService,
constructor(@Inject(WINDOW) private window: Window,
private dynamicComponentFactoryService: DynamicComponentFactoryService,
private widgetService: WidgetService,
private utils: UtilsService,
private resources: ResourcesService,
private translate: TranslateService) {
// @ts-ignore
this.window.tinycolor = tinycolor;
// @ts-ignore
this.window.cssjs = cssjs;
this.cssParser.testMode = false;
this.init();
}

12
ui-ngx/src/app/modules/home/components/widget/widget.component.html

@ -15,12 +15,6 @@
limitations under the License.
-->
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>
</div>
<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>
</div>
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
<tb-legend *ngIf="displayLegend && isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
<div fxFlex id="widget-container">
@ -28,3 +22,9 @@
</div>
<tb-legend *ngIf="displayLegend && !isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
</div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>
</div>
<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>
</div>

4
ui-ngx/src/app/modules/home/components/widget/widget.component.scss

@ -44,4 +44,8 @@
color: #f00;
word-wrap: break-word;
}
#widget-container {
min-height: 0;
}
}

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

@ -32,6 +32,7 @@ import {
} from '@angular/core';
import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
import {
Datasource,
LegendConfig,
LegendData,
LegendPosition,
@ -40,16 +41,16 @@ import {
widgetActionSources,
WidgetActionType,
WidgetResource,
widgetType
widgetType,
WidgetTypeParameters
} from '@shared/models/widget.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, throwError } from 'rxjs';
import { isDefined, objToBase64 } from '@core/utils';
import * as $ from 'jquery';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import { isDefined, objToBase64, deepClone } from '@core/utils';
import {
IDynamicWidgetComponent,
WidgetContext,
@ -77,6 +78,13 @@ import { DeviceService } from '@app/core/http/device.service';
import { AlarmService } from '@app/core/http/alarm.service';
import { ExceptionData } from '@shared/models/error.models';
import { WidgetComponentService } from './widget-component.service';
import { Timewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { DashboardService } from '@core/http/dashboard.service';
import { DatasourceService } from '@core/api/datasource.service';
import { WidgetSubscription } from '@core/api/widget-subscription';
import { EntityService } from '@core/http/entity.service';
@Component({
selector: 'tb-widget',
@ -105,6 +113,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
errorMessages: string[];
widgetContext: WidgetContext;
widgetType: any;
typeParameters: WidgetTypeParameters;
widgetTypeInstance: WidgetTypeInstance;
widgetErrorData: ExceptionData;
loadingData: boolean;
@ -124,10 +133,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
subscriptionInited = false;
widgetSizeDetected = false;
cafs: {[cafId: string]: CancelAnimationFrame} = {};
onResizeListener = this.onResize.bind(this);
private cssParser = new cssjs();
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router,
@ -139,8 +152,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private resources: ResourcesService,
private timeService: TimeService,
private deviceService: DeviceService,
private entityService: EntityService,
private alarmService: AlarmService,
private utils: UtilsService) {
private dashboardService: DashboardService,
private datasourceService: DatasourceService,
private utils: UtilsService,
private raf: RafService) {
super(store);
}
@ -212,7 +229,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const descriptors = this.widget.config.actions[actionSourceId];
const actionDescriptors: Array<WidgetActionDescriptor> = [];
descriptors.forEach((descriptor) => {
const actionDescriptor: WidgetActionDescriptor = {...descriptor};
const actionDescriptor: WidgetActionDescriptor = deepClone(descriptor);
actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name);
actionDescriptors.push(actionDescriptor);
});
@ -302,15 +319,18 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.customHeaderActions.push(headerAction);
});
this.subscriptionContext = {
timeService: this.timeService,
deviceService: this.deviceService,
alarmService: this.alarmService,
datasourceService: this.datasourceService,
utils: this.utils,
widgetUtils: this.widgetContext.utils,
dashboardTimewindowApi: null, // TODO:
getServerTimeDiff: null, // TODO:
dashboardTimewindowApi: {
onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard),
onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard)
},
getServerTimeDiff: this.dashboardService.getServerTimeDiff.bind(this.dashboardService),
aliasController: this.dashboard.aliasController
};
@ -331,26 +351,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.destroy();
}
this.subscriptionInited = false;
this.widgetContext.subscriptions = {};
if (this.widgetContext.inited) {
this.widgetContext.inited = false;
// TODO:
try {
this.widgetTypeInstance.onDestroy();
} catch (e) {
this.handleWidgetException(e);
}
}
this.destroyDynamicWidgetComponent();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
@ -366,28 +366,43 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
private onEditModeChanged() {
if (this.widgetContext.isEdit !== this.isEdit) {
this.widgetContext.isEdit = this.isEdit;
if (this.widgetContext.inited) {
// TODO:
}
}
ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
this.onDestroy();
}
private onMobileModeChanged() {
if (this.widgetContext.isMobile !== this.isMobile) {
this.widgetContext.isMobile = this.isMobile;
if (this.widgetContext.inited) {
// TODO:
private onDestroy() {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.destroy();
}
this.subscriptionInited = false;
this.widgetContext.subscriptions = {};
if (this.widgetContext.inited) {
this.widgetContext.inited = false;
for (const cafId of Object.keys(this.cafs)) {
if (this.cafs[cafId]) {
this.cafs[cafId]();
this.cafs[cafId] = null;
}
}
try {
this.widgetTypeInstance.onDestroy();
} catch (e) {
this.handleWidgetException(e);
}
}
this.destroyDynamicWidgetComponent();
}
private onResize() {
if (this.checkSize()) {
if (this.widgetContext.inited) {
// TODO:
public onTimewindowChanged(timewindow: Timewindow) {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
if (!subscription.useDashboardTimewindow) {
subscription.updateTimewindowConfig(timewindow);
}
}
}
@ -398,6 +413,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
elem.classList.add('tb-widget');
elem.classList.add(widgetNamespace);
this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters;
if (!this.widgetType) {
this.widgetTypeInstance = {};
@ -428,19 +444,153 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetTypeInstance.onDestroy = () => {};
}
this.initialize();
this.initialize().subscribe(
() => {
this.onInit();
}
);
}
private isReady(): boolean {
return this.subscriptionInited && this.widgetSizeDetected;
}
private onInit(skipSizeCheck?: boolean) {
if (!this.widgetContext.$containerParent) {
return;
}
if (!skipSizeCheck) {
this.checkSize();
}
if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true;
try {
this.widgetTypeInstance.onInit();
} catch (e) {
this.handleWidgetException(e);
}
if (!this.typeParameters.useCustomDatasources && this.widgetContext.defaultSubscription) {
this.widgetContext.defaultSubscription.subscribe();
}
}
}
private onResize() {
if (this.checkSize()) {
if (this.widgetContext.inited) {
if (this.cafs.resize) {
this.cafs.resize();
this.cafs.resize = null;
}
this.cafs.resize = this.raf.raf(() => {
try {
this.widgetTypeInstance.onResize();
} catch (e) {
this.handleWidgetException(e);
}
});
} else {
this.onInit(true);
}
}
}
private onEditModeChanged() {
if (this.widgetContext.isEdit !== this.isEdit) {
this.widgetContext.isEdit = this.isEdit;
if (this.widgetContext.inited) {
if (this.cafs.editMode) {
this.cafs.editMode();
this.cafs.editMode = null;
}
this.cafs.editMode = this.raf.raf(() => {
try {
this.widgetTypeInstance.onEditModeChanged();
} catch (e) {
this.handleWidgetException(e);
}
});
}
}
}
private onMobileModeChanged() {
if (this.widgetContext.isMobile !== this.isMobile) {
this.widgetContext.isMobile = this.isMobile;
if (this.widgetContext.inited) {
if (this.cafs.mobileMode) {
this.cafs.mobileMode();
this.cafs.mobileMode = null;
}
this.cafs.mobileMode = this.raf.raf(() => {
try {
this.widgetTypeInstance.onMobileModeChanged();
} catch (e) {
this.handleWidgetException(e);
}
});
}
}
}
private reInit() {
this.ngOnDestroy();
this.initialize();
// TODO:
this.onDestroy();
this.configureDynamicWidgetComponent();
if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe(
() => {
this.subscriptionInited = true;
this.onInit();
},
() => {
this.subscriptionInited = true;
this.onInit();
}
);
} else {
this.subscriptionInited = true;
this.onInit();
}
}
private initialize() {
private initialize(): Observable<any> {
const initSubject = new ReplaySubject();
this.rxSubscriptions.push(this.dashboard.aliasController.entityAliasesChanged.subscribe(
(aliasIds) => {
let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
}
if (subscriptionChanged && !this.typeParameters.useCustomDatasources) {
this.reInit();
}
}
));
this.configureDynamicWidgetComponent();
// TODO:
this.loadingData = false;
if (!this.typeParameters.useCustomDatasources) {
// this.cre
this.createDefaultSubscription().subscribe(
() => {
this.subscriptionInited = true;
initSubject.next();
initSubject.complete();
},
() => {
this.subscriptionInited = true;
initSubject.error(null);
}
);
} else {
this.loadingData = false;
this.subscriptionInited = true;
initSubject.next();
initSubject.complete();
}
return initSubject.asObservable();
}
private destroyDynamicWidgetComponent() {
@ -484,16 +634,193 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
}
private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> {
// TODO:
return of(null);
private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable<IWidgetSubscription> {
const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
options.dashboardTimewindow = this.dashboard.dashboardTimewindow;
const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options);
subscription.init$.subscribe(
() => {
this.widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) {
subscription.subscribe();
}
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
}
);
return createSubscriptionSubject.asObservable();
}
private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>,
options: WidgetSubscriptionOptions, useDefaultComponents: boolean,
subscribe: boolean): Observable<IWidgetSubscription> {
// TODO:
return of(null);
const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
options.type = type;
if (useDefaultComponents) {
this.defaultComponentsOptions(options);
} else {
if (!options.timeWindowConfig) {
options.useDashboardTimewindow = true;
}
}
let createDatasourcesObservable: Observable<Array<Datasource> | Datasource>;
if (options.type === widgetType.alarm) {
createDatasourcesObservable = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]);
} else {
createDatasourcesObservable = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
}
createDatasourcesObservable.subscribe(
(result) => {
if (options.type === widgetType.alarm) {
options.alarmSource = result as Datasource;
} else {
options.datasources = result as Array<Datasource>;
}
this.createSubscription(options, subscribe).subscribe(
(subscription) => {
if (useDefaultComponents) {
this.defaultSubscriptionOptions(subscription, options);
}
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
}
);
},
() => {
createSubscriptionSubject.error(null);
}
);
return createSubscriptionSubject.asObservable();
}
private defaultComponentsOptions(options: WidgetSubscriptionOptions) {
options.useDashboardTimewindow = isDefined(this.widget.config.useDashboardTimewindow)
? this.widget.config.useDashboardTimewindow : true;
options.displayTimewindow = isDefined(this.widget.config.displayTimewindow)
? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? this.dashboard.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null;
if (this.displayLegend) {
options.legendConfig = this.legendConfig;
}
options.decimals = this.widgetContext.decimals;
options.units = this.widgetContext.units;
options.callbacks = {
onDataUpdated: () => {
this.widgetTypeInstance.onDataUpdated();
},
onDataUpdateError: (subscription, e) => {
this.handleWidgetException(e);
},
dataLoading: (subscription) => {
if (this.loadingData !== subscription.loadingData) {
this.loadingData = subscription.loadingData;
}
},
legendDataUpdated: (subscription) => {
},
timeWindowUpdated: (subscription, timeWindowConfig) => {
this.widget.config.timewindow = timeWindowConfig;
}
};
}
private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) {
if (this.displayLegend) {
this.legendData = subscription.legendData;
}
}
private createDefaultSubscription(): Observable<any> {
const createSubscriptionSubject = new ReplaySubject();
let options: WidgetSubscriptionOptions;
if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) {
options = {
type: this.widget.type,
stateData: this.typeParameters.stateData
};
if (this.widget.type === widgetType.alarm) {
options.alarmSource = deepClone(this.widget.config.alarmSource);
options.alarmSearchStatus = isDefined(this.widget.config.alarmSearchStatus) ?
this.widget.config.alarmSearchStatus : AlarmSearchStatus.ANY;
options.alarmsPollingInterval = isDefined(this.widget.config.alarmsPollingInterval) ?
this.widget.config.alarmsPollingInterval * 1000 : 5000;
} else {
options.datasources = deepClone(this.widget.config.datasources);
}
this.defaultComponentsOptions(options);
this.createSubscription(options).subscribe(
(subscription) => {
this.defaultSubscriptionOptions(subscription, options);
// backward compatibility
this.widgetContext.datasources = subscription.datasources;
this.widgetContext.data = subscription.data;
this.widgetContext.hiddenData = subscription.hiddenData;
this.widgetContext.timeWindow = subscription.timeWindow;
this.widgetContext.defaultSubscription = subscription;
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
}
);
} else if (this.widget.type === widgetType.rpc) {
this.loadingData = false;
options = {
type: this.widget.type,
targetDeviceAliasIds: this.widget.config.targetDeviceAliasIds
};
options.callbacks = {
rpcStateChanged: (subscription) => {
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;
},
onRpcFailed: (subscription) => {
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;
}
};
this.createSubscription(options).subscribe(
(subscription) => {
this.widgetContext.defaultSubscription = subscription;
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
},
() => {
createSubscriptionSubject.error(null);
}
);
} else if (this.widget.type === widgetType.static) {
this.loadingData = false;
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
} else {
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
}
return createSubscriptionSubject.asObservable();
}
private isNumeric(value: any): boolean {
@ -540,7 +867,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
case WidgetActionType.openDashboardState:
case WidgetActionType.updateDashboardState:
let targetDashboardStateId = descriptor.targetDashboardStateId;
const params = {...this.widgetContext.stateController.getStateParams()};
const params = deepClone(this.widgetContext.stateController.getStateParams());
this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName);
if (type === WidgetActionType.openDashboardState) {
this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);

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

@ -23,7 +23,7 @@ import { Observable } from 'rxjs';
import { isDefined, isUndefined } from '@app/core/utils';
import { EventEmitter } from '@angular/core';
import { EntityId } from '@app/shared/models/id/entity-id';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { IAliasController, IStateController, TimewindowFunctions } from '@app/core/api/widget-api.models';
export interface WidgetsData {
widgets: Array<Widget>;
@ -49,6 +49,8 @@ export interface IDashboardComponent {
dashboardTimewindow: Timewindow;
aliasController: IAliasController;
stateController: IStateController;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void;
onResetTimewindow(): void;
}
export class DashboardWidget implements GridsterItem {

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

@ -17,6 +17,8 @@
import { ExceptionData } from '@shared/models/error.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import {
DataSet,
Datasource, DatasourceData,
WidgetActionDescriptor,
WidgetActionSource,
WidgetConfig,
@ -40,6 +42,7 @@ import {
WidgetSubscriptionApi
} from '@core/api/widget-api.models';
import { ComponentFactory } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
export interface IWidgetAction {
name: string;
@ -86,11 +89,20 @@ export interface WidgetContext {
widgetTitle?: string;
customHeaderActions?: Array<WidgetHeaderAction>;
widgetActions?: Array<WidgetAction>;
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindow?: Timewindow;
}
export interface IDynamicWidgetComponent {
widgetContext: WidgetContext;
errorMessages: string[];
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
[key: string]: any;
}
@ -120,7 +132,8 @@ export const MissingWidgetType: WidgetInfo = {
'"title": "Widget type not found",\n' +
'"datasources": [],\n' +
'"settings": {}\n' +
'}\n'
'}\n',
typeParameters: {}
};
export const ErrorWidgetType: WidgetInfo = {
@ -142,7 +155,8 @@ export const ErrorWidgetType: WidgetInfo = {
'"title": "Widget failed to load",\n' +
'"datasources": [],\n' +
'"settings": {}\n' +
'}\n'
'}\n',
typeParameters: {}
};
export interface WidgetTypeInstance {

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

@ -33,6 +33,7 @@ import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.comp
import { DashboardCallbacks, 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';
@Component({
selector: 'tb-widget-library',
@ -78,7 +79,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
widgetsData: Observable<WidgetsData>;
aliasController: IAliasController = {};
aliasController: IAliasController = new DummyAliasController();
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,

7
ui-ngx/src/app/shared/components/time/datetime-period.component.ts

@ -94,9 +94,10 @@ export class DatetimePeriodComponent implements OnInit, ControlValueAccessor {
updateView() {
let value: FixedWindow = null;
if (this.startDate && this.endDate) {
value = new FixedWindow();
value.startTimeMs = this.startDate.getTime();
value.endTimeMs = this.endDate.getTime();
value = {
startTimeMs: this.startDate.getTime(),
endTimeMs: this.endDate.getTime()
};
}
this.modelValue = value;
if (!this.propagateChange) {

25
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -145,18 +145,21 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
update() {
const timewindowFormValue = this.timewindowForm.value;
this.timewindow.realtime = new IntervalWindow();
this.timewindow.realtime.timewindowMs = timewindowFormValue.realtime.timewindowMs;
this.timewindow.realtime.interval = timewindowFormValue.realtime.interval;
this.timewindow.history = new HistoryWindow();
this.timewindow.history.historyType = timewindowFormValue.history.historyType;
this.timewindow.history.timewindowMs = timewindowFormValue.history.timewindowMs;
this.timewindow.history.interval = timewindowFormValue.history.interval;
this.timewindow.history.fixedTimewindow = timewindowFormValue.history.fixedTimewindow;
this.timewindow.realtime = {
timewindowMs: timewindowFormValue.realtime.timewindowMs,
interval: timewindowFormValue.realtime.interval
};
this.timewindow.history = {
historyType: timewindowFormValue.history.historyType,
timewindowMs: timewindowFormValue.history.timewindowMs,
interval: timewindowFormValue.history.interval,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow
};
if (this.aggregation) {
this.timewindow.aggregation = new Aggregation();
this.timewindow.aggregation.type = timewindowFormValue.aggregation.type;
this.timewindow.aggregation.limit = timewindowFormValue.aggregation.limit;
this.timewindow.aggregation = {
type: timewindowFormValue.aggregation.type,
limit: timewindowFormValue.aggregation.limit
};
}
this.result = this.timewindow;
this.overlayRef.dispose();

10
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -29,7 +29,8 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-
import {
HistoryWindowType,
Timewindow,
TimewindowType
TimewindowType,
initModelFromDefaultTimewindow, cloneSelectedTimewindow
} from '@shared/models/time/time.models';
import { DatePipe } from '@angular/common';
import {
@ -50,6 +51,7 @@ import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
import { TimeService } from '@core/services/time.service';
import { TooltipPosition } from '@angular/material/typings/tooltip';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-timewindow',
@ -206,7 +208,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
const injector = this._createTimewindowPanelInjector(
overlayRef,
{
timewindow: this.innerValue.clone(),
timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly,
aggregation: this.aggregation
}
@ -242,12 +244,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
}
writeValue(obj: Timewindow): void {
this.innerValue = Timewindow.initModelFromDefaultTimewindow(obj, this.timeService);
this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService);
this.updateDisplayValue();
}
notifyChanged() {
this.propagateChange(this.innerValue.cloneSelectedTimewindow());
this.propagateChange(cloneSelectedTimewindow(this.innerValue));
}
updateDisplayValue() {

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

@ -103,6 +103,71 @@ export interface AlarmInfo extends Alarm {
originatorName: string;
}
export interface AlarmField {
keyName: string;
value: string;
name: string;
time?: boolean;
}
export const alarmFields: {[fieldName: string]: AlarmField} = {
createdTime: {
keyName: 'createdTime',
value: 'createdTime',
name: 'alarm.created-time',
time: true
},
startTime: {
keyName: 'startTime',
value: 'startTs',
name: 'alarm.start-time',
time: true
},
endTime: {
keyName: 'endTime',
value: 'endTs',
name: 'alarm.end-time',
time: true
},
ackTime: {
keyName: 'ackTime',
value: 'ackTs',
name: 'alarm.ack-time',
time: true
},
clearTime: {
keyName: 'clearTime',
value: 'clearTs',
name: 'alarm.clear-time',
time: true
},
originator: {
keyName: 'originator',
value: 'originatorName',
name: 'alarm.originator'
},
originatorType: {
keyName: 'originatorType',
value: 'originator.entityType',
name: 'alarm.originator-type'
},
type: {
keyName: 'type',
value: 'type',
name: 'alarm.type'
},
severity: {
keyName: 'severity',
value: 'severity',
name: 'alarm.severity'
},
status: {
keyName: 'status',
value: 'status',
name: 'alarm.status'
}
};
export class AlarmQuery {
affectedEntityId: EntityId;

367
ui-ngx/src/app/shared/models/material.models.ts

@ -0,0 +1,367 @@
///
/// 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 * as tinycolor from 'tinycolor2';
export interface MaterialColorItem {
value: string;
group: string;
label: string;
isDark: boolean;
}
export const materialColorPalette: {[palette: string]: {[spectrum: string]: string}} = {
red: {
50: '#ffebee',
100: '#ffcdd2',
200: '#ef9a9a',
300: '#e57373',
400: '#ef5350',
500: '#f44336',
600: '#e53935',
700: '#d32f2f',
800: '#c62828',
900: '#b71c1c',
A100: '#ff8a80',
A200: '#ff5252',
A400: '#ff1744',
A700: '#d50000'
},
pink: {
50: '#fce4ec',
100: '#f8bbd0',
200: '#f48fb1',
300: '#f06292',
400: '#ec407a',
500: '#e91e63',
600: '#d81b60',
700: '#c2185b',
800: '#ad1457',
900: '#880e4f',
A100: '#ff80ab',
A200: '#ff4081',
A400: '#f50057',
A700: '#c51162'
},
purple: {
50: '#f3e5f5',
100: '#e1bee7',
200: '#ce93d8',
300: '#ba68c8',
400: '#ab47bc',
500: '#9c27b0',
600: '#8e24aa',
700: '#7b1fa2',
800: '#6a1b9a',
900: '#4a148c',
A100: '#ea80fc',
A200: '#e040fb',
A400: '#d500f9',
A700: '#aa00ff'
},
'deep-purple': {
50: '#ede7f6',
100: '#d1c4e9',
200: '#b39ddb',
300: '#9575cd',
400: '#7e57c2',
500: '#673ab7',
600: '#5e35b1',
700: '#512da8',
800: '#4527a0',
900: '#311b92',
A100: '#b388ff',
A200: '#7c4dff',
A400: '#651fff',
A700: '#6200ea'
},
indigo: {
50: '#e8eaf6',
100: '#c5cae9',
200: '#9fa8da',
300: '#7986cb',
400: '#5c6bc0',
500: '#3f51b5',
600: '#3949ab',
700: '#303f9f',
800: '#283593',
900: '#1a237e',
A100: '#8c9eff',
A200: '#536dfe',
A400: '#3d5afe',
A700: '#304ffe'
},
blue: {
50: '#e3f2fd',
100: '#bbdefb',
200: '#90caf9',
300: '#64b5f6',
400: '#42a5f5',
500: '#2196f3',
600: '#1e88e5',
700: '#1976d2',
800: '#1565c0',
900: '#0d47a1',
A100: '#82b1ff',
A200: '#448aff',
A400: '#2979ff',
A700: '#2962ff'
},
'light-blue': {
50: '#e1f5fe',
100: '#b3e5fc',
200: '#81d4fa',
300: '#4fc3f7',
400: '#29b6f6',
500: '#03a9f4',
600: '#039be5',
700: '#0288d1',
800: '#0277bd',
900: '#01579b',
A100: '#80d8ff',
A200: '#40c4ff',
A400: '#00b0ff',
A700: '#0091ea'
},
cyan: {
50: '#e0f7fa',
100: '#b2ebf2',
200: '#80deea',
300: '#4dd0e1',
400: '#26c6da',
500: '#00bcd4',
600: '#00acc1',
700: '#0097a7',
800: '#00838f',
900: '#006064',
A100: '#84ffff',
A200: '#18ffff',
A400: '#00e5ff',
A700: '#00b8d4'
},
teal: {
50: '#e0f2f1',
100: '#b2dfdb',
200: '#80cbc4',
300: '#4db6ac',
400: '#26a69a',
500: '#009688',
600: '#00897b',
700: '#00796b',
800: '#00695c',
900: '#004d40',
A100: '#a7ffeb',
A200: '#64ffda',
A400: '#1de9b6',
A700: '#00bfa5'
},
green: {
50: '#e8f5e9',
100: '#c8e6c9',
200: '#a5d6a7',
300: '#81c784',
400: '#66bb6a',
500: '#4caf50',
600: '#43a047',
700: '#388e3c',
800: '#2e7d32',
900: '#1b5e20',
A100: '#b9f6ca',
A200: '#69f0ae',
A400: '#00e676',
A700: '#00c853'
},
'light-green': {
50: '#f1f8e9',
100: '#dcedc8',
200: '#c5e1a5',
300: '#aed581',
400: '#9ccc65',
500: '#8bc34a',
600: '#7cb342',
700: '#689f38',
800: '#558b2f',
900: '#33691e',
A100: '#ccff90',
A200: '#b2ff59',
A400: '#76ff03',
A700: '#64dd17'
},
lime: {
50: '#f9fbe7',
100: '#f0f4c3',
200: '#e6ee9c',
300: '#dce775',
400: '#d4e157',
500: '#cddc39',
600: '#c0ca33',
700: '#afb42b',
800: '#9e9d24',
900: '#827717',
A100: '#f4ff81',
A200: '#eeff41',
A400: '#c6ff00',
A700: '#aeea00'
},
yellow: {
50: '#fffde7',
100: '#fff9c4',
200: '#fff59d',
300: '#fff176',
400: '#ffee58',
500: '#ffeb3b',
600: '#fdd835',
700: '#fbc02d',
800: '#f9a825',
900: '#f57f17',
A100: '#ffff8d',
A200: '#ffff00',
A400: '#ffea00',
A700: '#ffd600'
},
amber: {
50: '#fff8e1',
100: '#ffecb3',
200: '#ffe082',
300: '#ffd54f',
400: '#ffca28',
500: '#ffc107',
600: '#ffb300',
700: '#ffa000',
800: '#ff8f00',
900: '#ff6f00',
A100: '#ffe57f',
A200: '#ffd740',
A400: '#ffc400',
A700: '#ffab00'
},
orange: {
50: '#fff3e0',
100: '#ffe0b2',
200: '#ffcc80',
300: '#ffb74d',
400: '#ffa726',
500: '#ff9800',
600: '#fb8c00',
700: '#f57c00',
800: '#ef6c00',
900: '#e65100',
A100: '#ffd180',
A200: '#ffab40',
A400: '#ff9100',
A700: '#ff6d00'
},
'deep-orange': {
50: '#fbe9e7',
100: '#ffccbc',
200: '#ffab91',
300: '#ff8a65',
400: '#ff7043',
500: '#ff5722',
600: '#f4511e',
700: '#e64a19',
800: '#d84315',
900: '#bf360c',
A100: '#ff9e80',
A200: '#ff6e40',
A400: '#ff3d00',
A700: '#dd2c00'
},
brown: {
50: '#efebe9',
100: '#d7ccc8',
200: '#bcaaa4',
300: '#a1887f',
400: '#8d6e63',
500: '#795548',
600: '#6d4c41',
700: '#5d4037',
800: '#4e342e',
900: '#3e2723',
A100: '#d7ccc8',
A200: '#bcaaa4',
A400: '#8d6e63',
A700: '#5d4037'
},
grey: {
50: '#fafafa',
100: '#f5f5f5',
200: '#eeeeee',
300: '#e0e0e0',
400: '#bdbdbd',
500: '#9e9e9e',
600: '#757575',
700: '#616161',
800: '#424242',
900: '#212121',
A100: '#ffffff',
A200: '#000000',
A400: '#303030',
A700: '#616161'
},
'blue-grey': {
50: '#eceff1',
100: '#cfd8dc',
200: '#b0bec5',
300: '#90a4ae',
400: '#78909c',
500: '#607d8b',
600: '#546e7a',
700: '#455a64',
800: '#37474f',
900: '#263238',
A100: '#cfd8dc',
A200: '#b0bec5',
A400: '#78909c',
A700: '#455a64'
}
};
export const materialColors = new Array<MaterialColorItem>();
const colorPalettes = ['blue', 'green', 'red', 'amber', 'blue-grey', 'purple', 'light-green',
'indigo', 'pink', 'yellow', 'light-blue', 'orange', 'deep-purple', 'lime', 'teal', 'brown', 'cyan', 'deep-orange', 'grey'];
const colorSpectrum = ['500', 'A700', '600', '700', '800', '900', '300', '400', 'A200', 'A400'];
for (const key of Object.keys(materialColorPalette)) {
const value = materialColorPalette[key];
for (const label of Object.keys(value)) {
if (colorSpectrum.indexOf(label) > -1) {
const colorValue = value[label];
const color = tinycolor(colorValue);
const isDark = color.isDark();
const colorItem = {
value: color.toHexString(),
group: key,
label,
isDark
};
materialColors.push(colorItem);
}
}
}
materialColors.sort((colorItem1, colorItem2) => {
const spectrumIndex1 = colorSpectrum.indexOf(colorItem1.label);
const spectrumIndex2 = colorSpectrum.indexOf(colorItem2.label);
let result = spectrumIndex1 - spectrumIndex2;
if (result === 0) {
const paletteIndex1 = colorPalettes.indexOf(colorItem1.group);
const paletteIndex2 = colorPalettes.indexOf(colorItem2.group);
result = paletteIndex1 - paletteIndex2;
}
return result;
});

295
ui-ngx/src/app/shared/models/time/time.models.ts

@ -15,6 +15,7 @@
///
import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined } from '@app/core/utils';
export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
@ -31,163 +32,19 @@ export enum HistoryWindowType {
FIXED
}
export class Timewindow {
displayValue?: string;
selectedTab?: TimewindowType;
realtime?: IntervalWindow;
history?: HistoryWindow;
aggregation?: Aggregation;
public static historyInterval(timewindowMs: number): Timewindow {
const timewindow = new Timewindow();
timewindow.history = new HistoryWindow();
timewindow.history.timewindowMs = timewindowMs;
return timewindow;
}
public static defaultTimewindow(timeService: TimeService): Timewindow {
const currentTime = new Date().getTime();
const timewindow = new Timewindow();
timewindow.displayValue = '';
timewindow.selectedTab = TimewindowType.REALTIME;
timewindow.realtime = new IntervalWindow();
timewindow.realtime.interval = SECOND;
timewindow.realtime.timewindowMs = MINUTE;
timewindow.history = new HistoryWindow();
timewindow.history.historyType = HistoryWindowType.LAST_INTERVAL;
timewindow.history.interval = SECOND;
timewindow.history.timewindowMs = MINUTE;
timewindow.history.fixedTimewindow = new FixedWindow();
timewindow.history.fixedTimewindow.startTimeMs = currentTime - DAY;
timewindow.history.fixedTimewindow.endTimeMs = currentTime;
timewindow.aggregation = new Aggregation();
timewindow.aggregation.type = AggregationType.AVG;
timewindow.aggregation.limit = Math.floor(timeService.getMaxDatapointsLimit() / 2);
return timewindow;
}
public static initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow {
const model = Timewindow.defaultTimewindow(timeService);
if (value) {
if (value.realtime) {
model.selectedTab = TimewindowType.REALTIME;
if (typeof value.realtime.interval !== 'undefined') {
model.realtime.interval = value.realtime.interval;
}
model.realtime.timewindowMs = value.realtime.timewindowMs;
} else {
model.selectedTab = TimewindowType.HISTORY;
if (typeof value.history.interval !== 'undefined') {
model.history.interval = value.history.interval;
}
if (typeof value.history.timewindowMs !== 'undefined') {
model.history.historyType = HistoryWindowType.LAST_INTERVAL;
model.history.timewindowMs = value.history.timewindowMs;
} else {
model.history.historyType = HistoryWindowType.FIXED;
model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
}
}
if (value.aggregation) {
if (value.aggregation.type) {
model.aggregation.type = value.aggregation.type;
}
model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
}
}
return model;
}
public clone(): Timewindow {
const cloned = new Timewindow();
cloned.displayValue = this.displayValue;
cloned.selectedTab = this.selectedTab;
cloned.realtime = this.realtime ? this.realtime.clone() : null;
cloned.history = this.history ? this.history.clone() : null;
cloned.aggregation = this.aggregation ? this.aggregation.clone() : null;
return cloned;
}
public cloneSelectedTimewindow(): Timewindow {
const cloned = new Timewindow();
if (typeof this.selectedTab !== 'undefined') {
if (this.selectedTab === TimewindowType.REALTIME) {
cloned.realtime = this.realtime ? this.realtime.clone() : null;
} else if (this.selectedTab === TimewindowType.HISTORY) {
cloned.history = this.history ? this.history.cloneSelectedTimewindow() : null;
}
}
cloned.aggregation = this.aggregation ? this.aggregation.clone() : null;
return cloned;
}
}
export class IntervalWindow {
export interface IntervalWindow {
interval?: number;
timewindowMs?: number;
public clone(): IntervalWindow {
const cloned = new IntervalWindow();
cloned.interval = this.interval;
cloned.timewindowMs = this.timewindowMs;
return cloned;
}
}
export class FixedWindow {
export interface FixedWindow {
startTimeMs: number;
endTimeMs: number;
public clone(): FixedWindow {
const cloned = new FixedWindow();
cloned.startTimeMs = this.startTimeMs;
cloned.endTimeMs = this.endTimeMs;
return cloned;
}
}
export class HistoryWindow extends IntervalWindow {
export interface HistoryWindow extends IntervalWindow {
historyType?: HistoryWindowType;
fixedTimewindow?: FixedWindow;
public clone(): HistoryWindow {
const cloned = new HistoryWindow();
cloned.historyType = this.historyType;
if (this.fixedTimewindow) {
cloned.fixedTimewindow = this.fixedTimewindow.clone();
}
cloned.interval = this.interval;
cloned.timewindowMs = this.timewindowMs;
return cloned;
}
public cloneSelectedTimewindow(): HistoryWindow {
const cloned = new HistoryWindow();
if (typeof this.historyType !== 'undefined') {
cloned.interval = this.interval;
if (this.historyType === HistoryWindowType.LAST_INTERVAL) {
cloned.timewindowMs = this.timewindowMs;
} else if (this.historyType === HistoryWindowType.FIXED) {
cloned.fixedTimewindow = this.fixedTimewindow ? this.fixedTimewindow.clone() : null;
}
}
return cloned;
}
}
export class Aggregation {
type: AggregationType;
limit: number;
public clone(): Aggregation {
const cloned = new Aggregation();
cloned.type = this.type;
cloned.limit = this.limit;
return cloned;
}
}
export enum AggregationType {
@ -210,6 +67,150 @@ export const aggregationTranslations = new Map<AggregationType, string>(
]
);
export interface Aggregation {
type: AggregationType;
limit: number;
}
export interface Timewindow {
displayValue?: string;
selectedTab?: TimewindowType;
realtime?: IntervalWindow;
history?: HistoryWindow;
aggregation?: Aggregation;
stDiff?: number;
}
export function historyInterval(timewindowMs: number): Timewindow {
const timewindow: Timewindow = {
history: {
timewindowMs
}
};
return timewindow;
}
export function defaultTimewindow(timeService: TimeService): Timewindow {
const currentTime = new Date().getTime();
const timewindow: Timewindow = {
displayValue: '',
selectedTab: TimewindowType.REALTIME,
realtime: {
interval: SECOND,
timewindowMs: MINUTE
},
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: MINUTE,
fixedTimewindow: {
startTimeMs: currentTime - DAY,
endTimeMs: currentTime
}
},
aggregation: {
type: AggregationType.AVG,
limit: Math.floor(timeService.getMaxDatapointsLimit() / 2)
}
};
return timewindow;
}
export function initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow {
const model = defaultTimewindow(timeService);
if (value) {
if (value.realtime) {
model.selectedTab = TimewindowType.REALTIME;
if (isDefined(value.realtime.interval)) {
model.realtime.interval = value.realtime.interval;
}
model.realtime.timewindowMs = value.realtime.timewindowMs;
} else {
model.selectedTab = TimewindowType.HISTORY;
if (isDefined(value.history.interval)) {
model.history.interval = value.history.interval;
}
if (isDefined(value.history.timewindowMs)) {
model.history.historyType = HistoryWindowType.LAST_INTERVAL;
model.history.timewindowMs = value.history.timewindowMs;
} else {
model.history.historyType = HistoryWindowType.FIXED;
model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
}
}
if (value.aggregation) {
if (value.aggregation.type) {
model.aggregation.type = value.aggregation.type;
}
model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
}
}
return model;
}
export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, endTimeMs: number,
interval: number, timeService: TimeService): Timewindow {
if (timewindow.history) {
interval = isDefined(interval) ? interval : timewindow.history.interval;
} else if (timewindow.realtime) {
interval = timewindow.realtime.interval;
} else {
interval = 0;
}
let aggType: AggregationType;
let limit: number;
if (timewindow.aggregation) {
aggType = timewindow.aggregation.type || AggregationType.AVG;
limit = timewindow.aggregation.limit || timeService.getMaxDatapointsLimit();
} else {
aggType = AggregationType.AVG;
limit = timeService.getMaxDatapointsLimit();
}
const historyTimewindow: Timewindow = {
history: {
fixedTimewindow: {
startTimeMs,
endTimeMs
},
interval: timeService.boundIntervalToTimewindow(endTimeMs - startTimeMs, interval, AggregationType.AVG)
},
aggregation: {
type: aggType,
limit
}
};
return historyTimewindow;
}
export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
const cloned: Timewindow = {};
if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab;
if (timewindow.selectedTab === TimewindowType.REALTIME) {
cloned.realtime = deepClone(timewindow.realtime);
} else if (timewindow.selectedTab === TimewindowType.HISTORY) {
cloned.history = deepClone(timewindow.history);
}
}
cloned.aggregation = deepClone(timewindow.aggregation);
return cloned;
}
export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): HistoryWindow {
const cloned: HistoryWindow = {};
if (isDefined(historyWindow.historyType)) {
cloned.historyType = historyWindow.historyType;
cloned.interval = historyWindow.interval;
if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
cloned.timewindowMs = historyWindow.timewindowMs;
} else if (historyWindow.historyType === HistoryWindowType.FIXED) {
cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
}
}
return cloned;
}
export interface TimeInterval {
name: string;
translateParams: {[key: string]: any};

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

@ -18,12 +18,17 @@ import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { Data } from '@angular/router';
import { DataKeyType } from './telemetry/telemetry.models';
export enum widgetType {
timeseries = 'timeseries',
latest = 'latest',
rpc = 'rpc',
alarm = 'alarm'
alarm = 'alarm',
static = 'static'
}
export interface WidgetTypeTemplate {
@ -77,6 +82,16 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
alias: 'alarms_table'
}
}
],
[
widgetType.static,
{
name: 'widget.static',
template: {
bundleAlias: 'cards',
alias: 'html_card'
}
}
]
]
);
@ -174,14 +189,50 @@ export interface LegendConfig {
showTotal: boolean;
}
export interface DataKey {
label: string;
color: string;
export interface KeyInfo {
name: string;
label?: string;
color?: string;
funcBody?: string;
postFuncBody?: string;
units?: string;
decimals?: number;
}
export interface DataKey extends KeyInfo {
type: DataKeyType;
pattern?: string;
settings?: any;
usePostProcessing?: boolean;
hidden?: boolean;
_hash?: number;
}
export enum DatasourceType {
function = 'function',
entity = 'entity'
}
export interface Datasource {
type: DatasourceType;
name?: string;
dataKeys?: Array<DataKey>;
entityType?: EntityType;
entityId?: string;
entityName?: string;
entityAliasId?: string;
[key: string]: any;
// TODO:
}
export type DataSet = [number, any][];
export interface DatasourceData {
datasource: Datasource;
dataKey: DataKey;
data: DataSet;
}
export interface LegendKey {
dataKey: DataKey;
dataIndex: number;
@ -192,6 +243,7 @@ export interface LegendKeyData {
max: number;
avg: number;
total: number;
hidden: boolean;
}
export interface LegendData {
@ -264,6 +316,11 @@ export interface WidgetConfig {
decimals?: number;
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: WidgetConfigSettings;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;
datasources?: Array<Datasource>;
targetDeviceAliasIds?: Array<string>;
[key: string]: any;
// TODO:

2
ui-ngx/src/tsconfig.app.json

@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": ["node"]
"types": ["node", "jquery"]
},
"exclude": [
"test.ts",

19
ui-ngx/src/typings.d.ts

@ -0,0 +1,19 @@
///
/// 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.
///
interface JQuery {
terminal(options?: any): any;
}

3
ui-ngx/tsconfig.json

@ -12,7 +12,8 @@
"importHelpers": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
"node_modules/@types",
"src/typings.d.ts"
],
"paths": {
"@app/*": ["src/app/*"],

Loading…
Cancel
Save