diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 7165edf085..629a0fd8df 100644 --- a/ui-ngx/angular.json +++ b/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", diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index 71c03da869..a5f864c71c 100644 --- a/ui-ngx/package-lock.json +++ b/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", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 5548100061..b76d0686c0 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts new file mode 100644 index 0000000000..12e7232b26 --- /dev/null +++ b/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>; + + [key: string]: any | null; + + constructor() { + this.entityAliasesChanged = new Subject>().asObservable(); + } + + getAliasInfo(aliasId): Observable { + return of(null); + } + + resolveDatasources(datasources: Array): Observable> { + return of(deepClone(datasources)); + } +} diff --git a/ui-ngx/src/app/core/api/datasource.service.ts b/ui-ngx/src/app/core/api/datasource.service.ts new file mode 100644 index 0000000000..279466d386 --- /dev/null +++ b/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: + } +} diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 0f035cd195..2b52a73494 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/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; + createSubscription: (options: WidgetSubscriptionOptions, subscribe?: boolean) => Observable; createSubscriptionFromInfo: (type: widgetType, subscriptionsInfo: Array, options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean) => Observable; @@ -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>; + getAliasInfo(aliasId): Observable; + resolveDatasources(datasources: Array): Observable>; [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; + entityName?: string; + entityNamePrefix?: string; + timeseries?: Array; + attributes?: Array; + functions?: Array; + alarmFields?: Array; [key: string]: any; - // TODO: } export interface WidgetSubscriptionContext { timeService: TimeService; deviceService: DeviceService; alarmService: AlarmService; + datasourceService: DatasourceService; utils: UtilsService; widgetUtils: IWidgetUtils; dashboardTimewindowApi: TimewindowFunctions; - getServerTimeDiff: Observable; + getServerTimeDiff: () => Observable; 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; + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + 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; + ctx: WidgetSubscriptionContext; + type: widgetType; + callbacks: WidgetSubscriptionCallbacks; - sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable; - sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable; + loadingData: boolean; + useDashboardTimewindow: boolean; + + legendData: LegendData; + datasources?: Array; + data?: Array; + hiddenData?: Array<{data: DataSet}>; + timeWindow?: Timewindow; + + alarmSource?: Datasource; + alarmSearchStatus?: AlarmSearchStatus; + alarmsPollingInterval?: number; + + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + + rpcEnabled?: boolean; + executingRpcRequest?: boolean; + rpcErrorText?: string; + rpcRejection?: HttpErrorResponse; + + getFirstEntityInfo(): EntityInfo; + + onAliasesChanged(aliasIds: Array): 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; + sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable; + clearRpcError(): void; - getFirstEntityInfo: () => EntityInfo; + subscribe(): void; destroy(): void; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts new file mode 100644 index 0000000000..b4cd76d0c0 --- /dev/null +++ b/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; + datasources: Array; + datasourceListeners: Array; + hiddenData: Array<{ data: DataSet }>; + legendData: LegendData; + legendConfig: LegendConfig; + caulculateLegendData: boolean; + displayLegend: boolean; + stateData: boolean; + decimals: number; + units: string; + + alarms: Array; + alarmSource: Datasource; + alarmSearchStatus: AlarmSearchStatus; + alarmsPollingInterval: number; + alarmSourceListener: AlarmSourceListener; + + loadingData: boolean; + + targetDeviceAliasIds?: Array; + targetDeviceIds?: Array; + + executingRpcRequest: boolean; + rpcEnabled: boolean; + rpcErrorText: string; + rpcRejection: HttpErrorResponse; + + init$: Observable; + + cafs: {[cafId: string]: CancelAnimationFrame} = {}; + + targetDeviceAliasId: string; + targetDeviceId: string; + targetDeviceName: string; + executingSubjects: Array>; + + constructor(subscriptionContext: WidgetSubscriptionContext, options: WidgetSubscriptionOptions) { + const subscriptionSubject = new ReplaySubject(); + 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 { + 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 { + // TODO: + return of(null); + } + + private initDataSubscription(): Observable { + 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 { + return undefined; + } + + sendTwoWayCommand(method: string, params?: any, timeout?: number): Observable { + 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): boolean { + return false; + } + + private loadStDiff(): Observable { + 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(); + } +} diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index bdc2ed7950..ed6d783118 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/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) => void; + }; +} @Injectable({ providedIn: 'root' diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index ee30421842..9bd719663d 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/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; + 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> { @@ -124,4 +138,25 @@ export class DashboardService { return null; } + public getServerTimeDiff(): Observable { + if (this.stDiffSubject) { + return this.stDiffSubject.asObservable(); + } else { + this.stDiffSubject = new ReplaySubject(1); + const url = '/api/dashboard/serverTime'; + const ct1 = Date.now(); + this.http.get(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(); + } + } + } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 1622de0d20..c37f3ecefe 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/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): Observable> { + const observables = new Array>>(); + subscriptionsInfo.forEach((subscriptionInfo) => { + observables.push(this.createDatasourcesFromSubscriptionInfo(subscriptionInfo)); + }); + return forkJoin(observables).pipe( + map((arrayOfDatasources) => { + const result = new Array(); + arrayOfDatasources.forEach((datasources) => { + result.push(...datasources); + }); + this.utils.generateColors(result); + return result; + }) + ); + } + + public createAlarmSourceFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Observable { + 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> { + subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); + if (subscriptionInfo.type === DatasourceType.entity) { + return this.resolveEntitiesFromSubscriptionInfo(subscriptionInfo).pipe( + map((entities) => { + const datasources = new Array(); + 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>> { + if (subscriptionInfo.entityId) { + if (subscriptionInfo.entityName) { + const entity: BaseData = { + 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>>; + 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): 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, type: DataKeyType, datasource: Datasource) { + keyInfos.forEach((keyInfo) => { + const dataKey = this.utils.createKey(keyInfo, type); + datasource.dataKeys.push(dataKey); + }); + } } diff --git a/ui-ngx/src/app/core/services/raf.service.ts b/ui-ngx/src/app/core/services/raf.service.ts new file mode 100644 index 0000000000..b844dd810a --- /dev/null +++ b/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)); + } + } +} diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts index 69bd1f681c..30b15c3b08 100644 --- a/ui-ngx/src/app/core/services/time.service.ts +++ b/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 { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 687f53061a..272b5ce3a7 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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): Array { + 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) { + let index = 0; + datasources.forEach((datasource) => { + datasource.dataKeys.forEach((dataKey) => { + if (!dataKey.color) { + dataKey.color = this.getMaterialColor(index); + } + index++; + }); + }); + } + } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index e6b60dde17..b5022eb7cb 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -294,3 +294,11 @@ function utf8ToBytes(input: string, units?: number): number[] { } return bytes; } + +export function deepClone(obj: T): T { + if (obj) { + return JSON.parse(JSON.stringify(obj)); + } else { + return obj; + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index c554621a85..775ee94c05 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -49,7 +49,11 @@ {{widget.titleIcon}} {{widget.title}} - + +
) { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 3d8adfa186..764fea65e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/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(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 2f943d93d3..4d071ba34d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -15,12 +15,6 @@ limitations under the License. --> -
- Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} -
-
- -
@@ -28,3 +22,9 @@
+
+ Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss index 251c070267..e83243f7d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss +++ b/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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index db0bb2b953..44d56727c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/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(); + constructor(protected store: Store, 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 = []; 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 { + + 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 { - // TODO: - return of(null); + private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable { + const createSubscriptionSubject = new ReplaySubject(); + 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, options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean): Observable { - // TODO: - return of(null); + const createSubscriptionSubject = new ReplaySubject(); + options.type = type; + + if (useDefaultComponents) { + this.defaultComponentsOptions(options); + } else { + if (!options.timeWindowConfig) { + options.useDashboardTimewindow = true; + } + } + let createDatasourcesObservable: Observable | 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; + } + 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 { + 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); diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 4520319c6c..c1a1dc8cea 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/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; @@ -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 { diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 8bc55612e2..3aa9a23bd5 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/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; widgetActions?: Array; + + datasources?: Array; + data?: Array; + 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 { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index a1ba47ee1c..3400d56514 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/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; - aliasController: IAliasController = {}; + aliasController: IAliasController = new DummyAliasController(); constructor(protected store: Store, private route: ActivatedRoute, diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.ts b/ui-ngx/src/app/shared/components/time/datetime-period.component.ts index 8afd0fb8e8..dad8373def 100644 --- a/ui-ngx/src/app/shared/components/time/datetime-period.component.ts +++ b/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) { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index badceb71ea..3dda6cdb80 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index d28629fcdb..fd589a01d6 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/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() { diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 4265041835..6d408e1d22 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/material.models.ts b/ui-ngx/src/app/shared/models/material.models.ts new file mode 100644 index 0000000000..db5b3774b6 --- /dev/null +++ b/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(); + +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; +}); diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 4958082d4f..98af5fbf15 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/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( ] ); +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}; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index daba2d232a..b3e911b298 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/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( 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; + 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}; settings?: WidgetConfigSettings; + alarmSource?: Datasource; + alarmSearchStatus?: AlarmSearchStatus; + alarmsPollingInterval?: number; + datasources?: Array; + targetDeviceAliasIds?: Array; [key: string]: any; // TODO: diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index d5aa739e74..f74d460ff1 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/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", diff --git a/ui-ngx/src/typings.d.ts b/ui-ngx/src/typings.d.ts new file mode 100644 index 0000000000..3dc6e35bc4 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index 8f4576a7bf..4d36a53659 100644 --- a/ui-ngx/tsconfig.json +++ b/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/*"],