Browse Source

Manage dashboard states.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
de60fedfa3
  1. 19
      ui-ngx/src/app/core/api/widget-api.models.ts
  2. 36
      ui-ngx/src/app/core/api/widget-subscription.ts
  3. 37
      ui-ngx/src/app/core/http/dashboard.service.ts
  4. 2
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  5. 24
      ui-ngx/src/app/core/utils.ts
  6. 2
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts
  7. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  8. 6
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  9. 143
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  10. 2
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  11. 116
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  12. 1
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  13. 72
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  14. 8
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts
  15. 10
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  16. 2
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  17. 69
      ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html
  18. 145
      ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts
  19. 8
      ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss
  20. 8
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss
  21. 153
      ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html
  22. 100
      ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts
  23. 45
      ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss
  24. 244
      ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts
  25. 16
      ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts
  26. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -39,6 +39,7 @@ import { EntityInfo } from '@app/shared/models/entity.models';
import { Type } from '@angular/core';
import { AssetService } from '@core/http/asset.service';
import { DialogService } from '@core/services/dialog.service';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -148,7 +149,19 @@ export interface SubscriptionInfo {
deviceIds?: Array<string>;
}
export interface WidgetSubscriptionContext {
export class WidgetSubscriptionContext {
constructor(private dashboard: IDashboardComponent) {}
get aliasController(): IAliasController {
return this.dashboard.aliasController;
}
dashboardTimewindowApi: TimewindowFunctions = {
onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard),
onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard)
};
timeService: TimeService;
deviceService: DeviceService;
alarmService: AlarmService;
@ -156,11 +169,7 @@ export interface WidgetSubscriptionContext {
utils: UtilsService;
raf: RafService;
widgetUtils: IWidgetUtils;
dashboardTimewindowApi: TimewindowFunctions;
getServerTimeDiff: () => Observable<number>;
aliasController: IAliasController;
[key: string]: any;
// TODO:
}
export interface WidgetSubscriptionCallbacks {

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

@ -382,6 +382,13 @@ export class WidgetSubscription implements IWidgetSubscription {
}
onAliasesChanged(aliasIds: Array<string>): boolean {
if (this.type === widgetType.rpc) {
return this.checkRpcTarget(aliasIds);
} else if (this.type === widgetType.alarm) {
return this.checkAlarmSource(aliasIds);
} else {
return this.checkSubscriptions(aliasIds);
}
return false;
}
@ -566,6 +573,35 @@ export class WidgetSubscription implements IWidgetSubscription {
// TODO:
}
private checkRpcTarget(aliasIds: Array<string>): boolean {
if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) {
return true;
} else {
return false;
}
}
private checkAlarmSource(aliasIds: Array<string>): boolean {
if (this.alarmSource && this.alarmSource.entityAliasId) {
return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1;
} else {
return false;
}
}
private checkSubscriptions(aliasIds: Array<string>): boolean {
let subscriptionsChanged = false;
for (const listener of this.datasourceListeners) {
if (listener.datasource.entityAliasId) {
if (aliasIds.indexOf(listener.datasource.entityAliasId) > -1) {
subscriptionsChanged = true;
break;
}
}
}
return subscriptionsChanged;
}
destroy(): void {
this.unsubscribe();
for (const cafId of Object.keys(this.cafs)) {

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

@ -22,26 +22,29 @@ 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';
import { ActivationEnd, NavigationEnd, Router } from '@angular/router';
import { filter, map, publishReplay, refCount } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DashboardService {
stDiffSubject: Subject<number>;
stDiffObservable: Observable<number>;
currentUrl: string;
constructor(
private http: HttpClient,
private router: Router,
@Inject(WINDOW) private window: Window
) {
this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
this.currentUrl = this.router.url.split('?')[0];
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(
() => {
if (this.stDiffSubject) {
this.stDiffSubject.complete();
this.stDiffSubject = null;
const newUrl = this.router.url.split('?')[0];
if (this.currentUrl !== newUrl) {
this.stDiffObservable = null;
this.currentUrl = newUrl;
}
}
);
@ -139,24 +142,20 @@ export class DashboardService {
}
public getServerTimeDiff(): Observable<number> {
if (this.stDiffSubject) {
return this.stDiffSubject.asObservable();
} else {
this.stDiffSubject = new ReplaySubject<number>(1);
if (!this.stDiffObservable) {
const url = '/api/dashboard/serverTime';
const ct1 = Date.now();
this.http.get<number>(url, defaultHttpOptions(true)).subscribe(
(st) => {
this.stDiffObservable = this.http.get<number>(url, defaultHttpOptions(true)).pipe(
map((st) => {
const ct2 = Date.now();
const stDiff = Math.ceil(st - (ct1 + ct2) / 2);
this.stDiffSubject.next(stDiff);
},
() => {
this.stDiffSubject.error(null);
}
return stDiff;
}),
publishReplay(1),
refCount()
);
return this.stDiffSubject.asObservable();
}
return this.stDiffObservable;
}
}

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

@ -479,7 +479,7 @@ export class DashboardUtilsService {
}
}
private removeUnusedWidgets(dashboard: Dashboard) {
public removeUnusedWidgets(dashboard: Dashboard) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const widgets = dashboardConfiguration.widgets;

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

@ -99,10 +99,34 @@ export function isNumber(value: any): boolean {
return typeof value === 'number';
}
export function isNumeric(value: any): boolean {
return (value - parseFloat( value ) + 1) >= 0;
}
export function isString(value: any): boolean {
return typeof value === 'string';
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefined(value) &&
value != null && isNumeric(value)) {
let formatted: string | number = Number(value);
if (isDefined(dec)) {
formatted = formatted.toFixed(dec);
}
if (!showZeroDecimals) {
formatted = (Number(formatted) * 1);
}
formatted = formatted.toString();
if (isDefined(units) && units.length > 0) {
formatted += ' ' + units;
}
return formatted;
} else {
return value;
}
}
export function deleteNullProperties(obj: any) {
if (isUndefined(obj) || obj == null) {
return;

2
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts

@ -52,7 +52,7 @@ export class AliasesEntitySelectPanelComponent {
const resolvedEntities = this.entityAliasesInfo[aliasId].resolvedEntities;
const selected = resolvedEntities.find((entity) => entity.id === selectedId);
if (selected) {
this.data.aliasController.updateCurrentAliasEntity(aliasId, selected[0]);
this.data.aliasController.updateCurrentAliasEntity(aliasId, selected);
}
}

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

@ -148,8 +148,7 @@
#widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isMobile]="isMobileSize"
[dashboard]="this">
[isMobile]="isMobileSize">
</tb-widget>
</div>
</div>

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

@ -180,6 +180,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.gridsterOpts = {
gridType: 'scrollVertical',
keepFixedHeightInMobile: true,
disableWarnings: false,
disableAutoPositionOnConflict: false,
pushItems: false,
swap: false,
maxRows: 100,
@ -228,7 +230,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
ngDoCheck() {
this.dashboardWidgets.doCheck();
if (!this.optionsChangeNotificationsPaused) {
this.dashboardWidgets.doCheck();
}
}
ngOnChanges(changes: SimpleChanges): void {

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

@ -115,9 +115,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
@Input()
isMobile: boolean;
@Input()
dashboard: IDashboardComponent;
@Input()
dashboardWidget: DashboardWidget;
@ -146,11 +143,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
subscriptionContext: WidgetSubscriptionContext;
subscriptionInited = false;
destroyed = false;
widgetSizeDetected = false;
cafs: {[cafId: string]: CancelAnimationFrame} = {};
onResizeListener = this.onResize.bind(this);
onResizeListener = null;
private cssParser = new cssjs();
@ -252,30 +250,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext = this.dashboardWidget.widgetContext;
this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.inited = false;
this.widgetContext.hideTitlePanel = false;
this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isMobile = this.isMobile;
this.widgetContext.dashboard = this.dashboard;
this.widgetContext.widgetConfig = this.widget.config;
this.widgetContext.settings = this.widget.config.settings;
this.widgetContext.units = this.widget.config.units || '';
this.widgetContext.decimals = isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2;
this.widgetContext.subscriptions = {};
this.widgetContext.defaultSubscription = null;
this.widgetContext.dashboardTimewindow = this.dashboard.dashboardTimewindow;
this.widgetContext.timewindowFunctions = {
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
if (this.widgetContext.defaultSubscription) {
this.widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval);
}
},
onResetTimewindow: () => {
if (this.widgetContext.defaultSubscription) {
this.widgetContext.defaultSubscription.onResetTimewindow();
}
}
};
this.widgetContext.subscriptionApi = {
createSubscription: this.createSubscription.bind(this),
createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this),
@ -287,33 +264,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
};
this.widgetContext.controlApi = {
sendOneWayCommand: (method, params, timeout) => {
if (this.widgetContext.defaultSubscription) {
return this.widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
} else {
return of(null);
}
},
sendTwoWayCommand: (method, params, timeout) => {
if (this.widgetContext.defaultSubscription) {
return this.widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
} else {
return of(null);
}
}
};
this.widgetContext.utils = {
formatValue: this.formatValue.bind(this)
};
this.widgetContext.actionsApi = {
actionDescriptorsBySourceId,
getActionDescriptors: this.getActionDescriptors.bind(this),
handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this)
};
this.widgetContext.stateController = this.dashboard.stateController;
this.widgetContext.aliasController = this.dashboard.aliasController;
this.widgetContext.customHeaderActions = [];
const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value);
@ -333,21 +290,15 @@ 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,
raf: this.raf,
widgetUtils: this.widgetContext.utils,
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
};
this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard);
this.subscriptionContext.timeService = this.timeService;
this.subscriptionContext.deviceService = this.deviceService;
this.subscriptionContext.alarmService = this.alarmService;
this.subscriptionContext.datasourceService = this.datasourceService;
this.subscriptionContext.utils = this.utils;
this.subscriptionContext.raf = this.raf;
this.subscriptionContext.widgetUtils = this.widgetContext.utils;
this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService);
this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe(
(widgetInfo) => {
@ -382,6 +333,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
ngOnDestroy(): void {
this.destroyed = true;
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
@ -481,7 +433,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private onInit(skipSizeCheck?: boolean) {
if (!this.widgetContext.$containerParent) {
if (!this.widgetContext.$containerParent || this.destroyed) {
return;
}
if (!skipSizeCheck) {
@ -565,17 +517,35 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private reInit() {
if (this.cafs.reinit) {
this.cafs.reinit();
this.cafs.reinit = null;
}
this.cafs.reinit = this.raf.raf(() => {
this.reInitImpl();
});
}
private reInitImpl() {
this.onDestroy();
this.configureDynamicWidgetComponent();
if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe(
() => {
this.subscriptionInited = true;
this.onInit();
if (this.destroyed) {
this.onDestroy();
} else {
this.subscriptionInited = true;
this.onInit();
}
},
() => {
this.subscriptionInited = true;
this.onInit();
if (this.destroyed) {
this.onDestroy();
} else {
this.subscriptionInited = true;
this.onInit();
}
}
);
} else {
@ -588,7 +558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const initSubject = new ReplaySubject();
this.rxSubscriptions.push(this.dashboard.aliasController.entityAliasesChanged.subscribe(
this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe(
(aliasIds) => {
let subscriptionChanged = false;
for (const id of Object.keys(this.widgetContext.subscriptions)) {
@ -601,7 +571,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
));
this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
@ -634,9 +604,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private destroyDynamicWidgetComponent() {
if (this.widgetContext.$containerParent) {
if (this.widgetContext.$containerParent && this.onResizeListener) {
// @ts-ignore
removeResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
this.onResizeListener = null;
}
if (this.dynamicWidgetComponentRef) {
this.dynamicWidgetComponentRef.destroy();
@ -661,7 +632,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container'));
this.widgetContext.$container = $('> ng-component', containerElement);
// this.widgetContext.$container = $('> ng-component:not([id="container"])', containerElement);
this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement);
this.widgetContext.$container.css('display', 'block');
this.widgetContext.$container.css('user-select', 'none');
this.widgetContext.$container.attr('id', 'container');
@ -672,13 +644,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.$container.css('width', this.widgetContext.width + 'px');
}
this.onResizeListener = this.onResize.bind(this);
// @ts-ignore
addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
}
private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable<IWidgetSubscription> {
const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
options.dashboardTimewindow = this.dashboard.dashboardTimewindow;
options.dashboardTimewindow = this.widgetContext.dashboardTimewindow;
const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options);
subscription.init$.subscribe(
() => {
@ -747,7 +720,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
? 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.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null;
if (this.displayLegend) {
options.legendConfig = this.legendConfig;
@ -875,30 +848,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
return createSubscriptionSubject.asObservable();
}
private isNumeric(value: any): boolean {
return (value - parseFloat( value ) + 1) >= 0;
}
private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefined(value) &&
value != null && this.isNumeric(value)) {
let formatted: string | number = Number(value);
if (isDefined(dec)) {
formatted = formatted.toFixed(dec);
}
if (!showZeroDecimals) {
formatted = (Number(formatted) * 1);
}
formatted = formatted.toString();
if (isDefined(units) && units.length > 0) {
formatted += ' ' + units;
}
return formatted;
} else {
return value;
}
}
private getActionDescriptors(actionSourceId: string): Array<WidgetActionDescriptor> {
let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
if (!result) {

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

@ -302,7 +302,7 @@ export class DashboardWidget implements GridsterItem {
customHeaderActions: Array<WidgetHeaderAction>;
widgetActions: Array<WidgetAction>;
widgetContext: WidgetContext = {};
widgetContext = new WidgetContext(this.dashboard, this.widget);
widgetId: string;

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

@ -26,7 +26,8 @@ import {
WidgetType,
widgetType,
WidgetTypeDescriptor,
WidgetTypeParameters
WidgetTypeParameters,
Widget
} from '@shared/models/widget.models';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import {
@ -34,10 +35,10 @@ import {
IStateController,
IWidgetSubscription,
IWidgetUtils,
RpcApi, SubscriptionEntityInfo,
RpcApi, SubscriptionEntityInfo, SubscriptionInfo,
TimewindowFunctions,
WidgetActionsApi,
WidgetSubscriptionApi
WidgetSubscriptionApi, WidgetSubscriptionContext, WidgetSubscriptionOptions
} from '@core/api/widget-api.models';
import { ComponentFactory, Type } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
@ -49,6 +50,9 @@ import { DeviceService } from '@core/http/device.service';
import { AssetService } from '@app/core/http/asset.service';
import { DialogService } from '@core/services/dialog.service';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { isDefined, formatValue } from '@core/utils';
import { Observable, of, ReplaySubject } from 'rxjs';
import { WidgetSubscription } from '@core/api/widget-subscription';
export interface IWidgetAction {
name: string;
@ -65,30 +69,89 @@ export interface WidgetAction extends IWidgetAction {
show: boolean;
}
export interface WidgetContext {
inited?: boolean;
$container?: JQuery<any>;
$containerParent?: JQuery<any>;
width?: number;
height?: number;
$scope?: IDynamicWidgetComponent;
isEdit?: boolean;
isMobile?: boolean;
dashboard?: IDashboardComponent;
widgetConfig?: WidgetConfig;
settings?: any;
units?: string;
decimals?: number;
subscriptions?: {[id: string]: IWidgetSubscription};
defaultSubscription?: IWidgetSubscription;
dashboardTimewindow?: Timewindow;
timewindowFunctions?: TimewindowFunctions;
export class WidgetContext {
constructor(public dashboard: IDashboardComponent,
private widget: Widget) {}
get stateController(): IStateController {
return this.dashboard.stateController;
}
get aliasController(): IAliasController {
return this.dashboard.aliasController;
}
get dashboardTimewindow(): Timewindow {
return this.dashboard.dashboardTimewindow;
}
get widgetConfig(): WidgetConfig {
return this.widget.config;
}
get settings(): any {
return this.widget.config.settings;
}
get units(): string {
return this.widget.config.units || '';
}
get decimals(): number {
return isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2;
}
inited = false;
subscriptions: {[id: string]: IWidgetSubscription} = {};
defaultSubscription: IWidgetSubscription = null;
timewindowFunctions: TimewindowFunctions = {
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
if (this.defaultSubscription) {
this.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval);
}
},
onResetTimewindow: () => {
if (this.defaultSubscription) {
this.defaultSubscription.onResetTimewindow();
}
}
};
controlApi: RpcApi = {
sendOneWayCommand: (method, params, timeout) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendOneWayCommand(method, params, timeout);
} else {
return of(null);
}
},
sendTwoWayCommand: (method, params, timeout) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout);
} else {
return of(null);
}
}
};
utils: IWidgetUtils = {
formatValue
};
$container: JQuery<any>;
$containerParent: JQuery<any>;
width: number;
height: number;
$scope: IDynamicWidgetComponent;
isEdit: boolean;
isMobile: boolean;
subscriptionApi?: WidgetSubscriptionApi;
controlApi?: RpcApi;
utils?: IWidgetUtils;
actionsApi?: WidgetActionsApi;
stateController?: IStateController;
aliasController?: IAliasController;
activeEntityInfo?: SubscriptionEntityInfo;
datasources?: Array<Datasource>;
@ -96,7 +159,8 @@ export interface WidgetContext {
hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow;
hideTitlePanel?: boolean;
hideTitlePanel = false;
widgetTitleTemplate?: string;
widgetTitle?: string;
customHeaderActions?: Array<WidgetHeaderAction>;

1
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -141,6 +141,7 @@
height: rightLayoutHeight(),
borderLeft: 'none'}"
disableClose="true"
[@.disabled]="!isMobile"
position="end"
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">

72
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -26,7 +26,7 @@ import {
DashboardConfiguration,
DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo,
DashboardLayoutsInfo, DashboardState,
DashboardStateLayouts, GridSettings,
WidgetLayout
} from '@app/shared/models/dashboard.models';
@ -79,6 +79,10 @@ import {
DashboardSettingsDialogComponent,
DashboardSettingsDialogData
} from '@home/pages/dashboard/dashboard-settings-dialog.component';
import {
ManageDashboardStatesDialogComponent,
ManageDashboardStatesDialogData
} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component';
@Component({
selector: 'tb-dashboard-page',
@ -130,6 +134,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addingLayoutCtx: DashboardPageLayoutContext;
dashboardCtx: DashboardContext = {
getDashboard: () => this.dashboard,
dashboardTimewindow: null,
state: null,
stateController: null,
aliasController: null,
runChangeDetection: this.runChangeDetection.bind(this)
};
layouts: DashboardPageLayouts = {
main: {
show: false,
@ -157,15 +171,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
};
dashboardCtx: DashboardContext = {
dashboard: null,
dashboardTimewindow: null,
state: null,
stateController: null,
aliasController: null,
runChangeDetection: this.runChangeDetection.bind(this)
};
addWidgetFabButtons: FooterFabButtons = {
fabTogglerName: 'dashboard.add-widget',
fabTogglerIcon: 'add',
@ -255,13 +260,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboard = data.dashboard;
this.dashboardConfiguration = this.dashboard.configuration;
this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard);
this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard);
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx);
this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx);
this.widgetEditMode = data.widgetEditMode;
this.singlePageMode = data.singlePageMode;
this.dashboardCtx.dashboard = this.dashboard;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService,
() => this.dashboardCtx.stateController,
@ -514,8 +518,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.dialog.open<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogData,
{[id: string]: DashboardState }>(ManageDashboardStatesDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
states: deepClone(this.dashboard.configuration.states)
}
}).afterClosed().subscribe((states) => {
if (states) {
this.updateStates(states);
}
});
}
public manageDashboardLayouts($event: Event) {
@ -541,6 +555,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayouts();
}
private updateStates(states: {[id: string]: DashboardState }) {
this.dashboard.configuration.states = states;
this.dashboardUtils.removeUnusedWidgets(this.dashboard);
let targetState = this.dashboardCtx.state;
if (!this.dashboard.configuration.states[targetState]) {
targetState = this.dashboardUtils.getRootStateId(this.dashboardConfiguration.states);
}
this.openDashboardState(targetState);
}
private importWidget($event: Event) {
if ($event) {
$event.stopPropagation();
@ -577,20 +601,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (layoutsData) {
this.dashboardCtx.state = state;
this.dashboardCtx.aliasController.dashboardStateChanged();
let layoutVisibilityChanged = false;
for (const l of Object.keys(this.layouts)) {
const layout: DashboardPageLayout = this.layouts[l];
let showLayout;
if (layoutsData[l]) {
showLayout = true;
} else {
showLayout = false;
}
if (layout.show !== showLayout) {
layout.show = showLayout;
layoutVisibilityChanged = !this.isMobile;
}
}
this.isRightLayoutOpened = openRightLayout ? true : false;
this.updateLayouts(layoutsData);
}
@ -603,9 +613,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
for (const l of Object.keys(this.layouts)) {
const layout: DashboardPageLayout = this.layouts[l];
if (layoutsData[l]) {
layout.show = true;
const layoutInfo: DashboardLayoutInfo = layoutsData[l];
this.updateLayout(layout, layoutInfo);
} else {
layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null});
}
}

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

@ -30,7 +30,7 @@ export declare type DashboardPageScope = 'tenant' | 'customer';
export interface DashboardContext {
state: string;
dashboard: Dashboard;
getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow;
aliasController: IAliasController;
stateController: IStateController;
@ -79,7 +79,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> {
private loaded = false;
constructor(private dashboard: Dashboard) {
constructor(private dashboardCtx: DashboardContext) {
}
size() {
@ -115,7 +115,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> {
[Symbol.iterator](): Iterator<Widget> {
let pointer = 0;
const widgetIds = this.widgetIds;
const dashboard = this.dashboard;
const dashboard = this.dashboardCtx.getDashboard();
return {
next(value?: any): IteratorResult<Widget> {
if (pointer < widgetIds.length) {
@ -145,7 +145,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> {
}
private widgetById(widgetId: string): Widget {
return this.dashboard.configuration.widgets[widgetId];
return this.dashboardCtx.getDashboard().configuration.widgets[widgetId];
}
}

10
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -34,6 +34,8 @@ import { AddWidgetDialogComponent } from './add-widget-dialog.component';
import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component';
import { SelectTargetLayoutDialogComponent } from './layout/select-target-layout-dialog.component';
import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component';
import { ManageDashboardStatesDialogComponent } from './states/manage-dashboard-states-dialog.component';
import { DashboardStateDialogComponent } from './states/dashboard-state-dialog.component';
@NgModule({
entryComponents: [
@ -44,7 +46,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co
AddWidgetDialogComponent,
ManageDashboardLayoutsDialogComponent,
SelectTargetLayoutDialogComponent,
DashboardSettingsDialogComponent
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent
],
declarations: [
DashboardFormComponent,
@ -59,7 +63,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co
AddWidgetDialogComponent,
ManageDashboardLayoutsDialogComponent,
SelectTargetLayoutDialogComponent,
DashboardSettingsDialogComponent
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent
],
imports: [
CommonModule,

2
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts

@ -147,7 +147,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.hotkeysService.add(
new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.dashboard,
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) {
event.preventDefault();
this.pasteWidgetReference(event);

69
ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html

@ -0,0 +1,69 @@
<!--
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.
-->
<form #stateForm="ngForm" [formGroup]="stateFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-state' }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state-name</mat-label>
<input required matInput formControlName="name">
<mat-error *ngIf="stateFormGroup.get('name').hasError('required')">
{{ 'dashboard.state-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state-id</mat-label>
<input required matInput formControlName="id">
<mat-error *ngIf="stateFormGroup.get('id').hasError('required')">
{{ 'dashboard.state-id-required' | translate }}
</mat-error>
<mat-error *ngIf="stateFormGroup.get('id').hasError('stateExists')">
{{ 'dashboard.state-id-exists' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="root">
{{ 'dashboard.is-root-state' | translate }}
</mat-checkbox>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || stateFormGroup.invalid || !stateFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

145
ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts

@ -0,0 +1,145 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardState } from '@app/shared/models/dashboard.models';
import { MatDialog } from '@angular/material/dialog';
import { DashboardStateInfo } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models';
import { TranslateService } from '@ngx-translate/core';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export interface DashboardStateDialogData {
states: {[id: string]: DashboardState };
state: DashboardStateInfo;
isAdd: boolean;
}
@Component({
selector: 'tb-dashboard-state-dialog',
templateUrl: './dashboard-state-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}],
styleUrls: []
})
export class DashboardStateDialogComponent extends
DialogComponent<DashboardStateDialogComponent, DashboardStateInfo>
implements OnInit, ErrorStateMatcher {
stateFormGroup: FormGroup;
states: {[id: string]: DashboardState };
state: DashboardStateInfo;
prevStateId: string;
stateIdTouched: boolean;
isAdd: boolean;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardStateDialogComponent, DashboardStateInfo>,
private fb: FormBuilder,
private translate: TranslateService,
private dashboardUtils: DashboardUtilsService,
private dialog: MatDialog) {
super(store, router, dialogRef);
this.states = this.data.states;
this.isAdd = this.data.isAdd;
if (this.isAdd) {
this.state = {id: '', ...this.dashboardUtils.createDefaultState('', false)};
this.prevStateId = '';
} else {
this.state = this.data.state;
this.prevStateId = this.state.id;
}
this.stateFormGroup = this.fb.group({
name: [this.state.name, [Validators.required]],
id: [this.state.id, [Validators.required, this.validateDuplicateStateId()]],
root: [this.state.root, []],
});
this.stateFormGroup.get('name').valueChanges.subscribe((name: string) => {
this.checkStateName(name);
});
this.stateFormGroup.get('id').valueChanges.subscribe((id: string) => {
this.stateIdTouched = true;
});
}
private checkStateName(name: string) {
if (name && !this.stateIdTouched && this.isAdd) {
this.stateFormGroup.get('id').setValue(
name.toLowerCase().replace(/\W/g, '_'),
{ emitEvent: false }
);
}
}
private validateDuplicateStateId(): ValidatorFn {
return (c: FormControl) => {
const newStateId: string = c.value;
if (newStateId) {
const existing = this.states[newStateId];
if (existing && newStateId !== this.prevStateId) {
return {
stateExists: true
};
}
}
return null;
};
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.state = {...this.state, ...this.stateFormGroup.value};
this.state.id = this.state.id.trim();
this.dialogRef.close(this.state);
}
}

8
ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss

@ -18,3 +18,11 @@
margin: 0;
}
}
:host ::ng-deep {
mat-select.default-state-controller {
.mat-select-value {
max-width: 200px;
}
}
}

8
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss

@ -26,6 +26,7 @@
}
.state-entry {
pointer-events: all;
overflow: hidden;
font-size: 18px;
text-overflow: ellipsis;
@ -35,7 +36,14 @@
mat-select {
margin: 0;
}
}
}
:host ::ng-deep {
mat-select {
.mat-select-value {
max-width: 200px;
.mat-select-value-text {
font-size: 18px;
font-weight: 700;

153
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html

@ -0,0 +1,153 @@
<!--
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.
-->
<form #statesForm="ngForm" [formGroup]="statesFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>dashboard.manage-states</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
<div class="manage-dashboard-states" fxLayout="column">
<div class="tb-entity-table">
<div fxLayout="column" class="tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>dashboard.states</span>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
(click)="addState($event)"
matTooltip="{{ 'dashboard.add-state' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
type="button"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
type="button"
matTooltip="{{ 'dashboard.search-states' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
[ngModelOptions]="{standalone: true}"
placeholder="{{ 'dashboard.search-states' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
type="button"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="table-container">
<mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let state">
{{ state.name }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-id' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let state">
{{ state.id }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="root">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.is-root-state' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let state">
<mat-icon class="material-icons mat-icon">{{state.root ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
</mat-header-cell>
<mat-cell *matCellDef="let state" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
<div fxFlex fxLayout="row">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
matTooltip="{{ 'dashboard.edit-state' | translate }}"
matTooltipPosition="above"
(click)="editState($event, state)">
<mat-icon>edit</mat-icon>
</button>
<button [fxShow]="!state.root" mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
matTooltip="{{ 'dashboard.delete-state' | translate }}"
matTooltipPosition="above"
(click)="deleteState($event, state)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select"
*matRowDef="let state; columns: displayedColumns;"></mat-row>
</mat-table>
<span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>{{ 'dashboard.no-states-text' }}</span>
</div>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[5, 10, 15]"></mat-paginator>
</div>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || statesFormGroup.invalid || !statesFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

100
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts

@ -0,0 +1,100 @@
///
/// 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 { DashboardState } from '@shared/models/dashboard.models';
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { WidgetActionDescriptorInfo } from '@home/components/widget/action/manage-widget-actions.component.models';
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, publishReplay, refCount } from 'rxjs/operators';
export interface DashboardStateInfo extends DashboardState {
id: string;
}
export class DashboardStatesDatasource implements DataSource<DashboardStateInfo> {
private statesSubject = new BehaviorSubject<DashboardStateInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<DashboardStateInfo>>(emptyPageData<DashboardStateInfo>());
public pageData$ = this.pageDataSubject.asObservable();
private allStates: Observable<Array<DashboardStateInfo>>;
constructor(private states: {[id: string]: DashboardState }) {
}
connect(collectionViewer: CollectionViewer): Observable<DashboardStateInfo[] | ReadonlyArray<DashboardStateInfo>> {
return this.statesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.statesSubject.complete();
this.pageDataSubject.complete();
}
loadStates(pageLink: PageLink, reload: boolean = false): Observable<PageData<DashboardStateInfo>> {
if (reload) {
this.allStates = null;
}
const result = new ReplaySubject<PageData<DashboardStateInfo>>();
this.fetchStates(pageLink).pipe(
catchError(() => of(emptyPageData<DashboardStateInfo>())),
).subscribe(
(pageData) => {
this.statesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
}
);
return result;
}
fetchStates(pageLink: PageLink): Observable<PageData<DashboardStateInfo>> {
return this.getAllStates().pipe(
map((data) => pageLink.filterData(data))
);
}
getAllStates(): Observable<Array<DashboardStateInfo>> {
if (!this.allStates) {
const states: DashboardStateInfo[] = [];
for (const id of Object.keys(this.states)) {
const state = this.states[id];
states.push({id, ...state});
}
this.allStates = of(states).pipe(
publishReplay(1),
refCount()
);
}
return this.allStates;
}
isEmpty(): Observable<boolean> {
return this.statesSubject.pipe(
map((states) => !states.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
}

45
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.manage-dashboard-states {
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
.tb-entity-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
}
}
}
}
}
:host ::ng-deep {
.manage-dashboard-states {
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
}
}

244
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts

@ -0,0 +1,244 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardState } from '@app/shared/models/dashboard.models';
import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import {
WidgetActionDescriptorInfo,
WidgetActionsDatasource
} from '@home/components/widget/action/manage-widget-actions.component.models';
import {
DashboardStateInfo,
DashboardStatesDatasource
} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { fromEvent, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import {
WidgetActionDialogComponent,
WidgetActionDialogData
} from '@home/components/widget/action/widget-action-dialog.component';
import { deepClone } from '@core/utils';
import {
DashboardStateDialogComponent,
DashboardStateDialogData
} from '@home/pages/dashboard/states/dashboard-state-dialog.component';
export interface ManageDashboardStatesDialogData {
states: {[id: string]: DashboardState };
}
@Component({
selector: 'tb-manage-dashboard-states-dialog',
templateUrl: './manage-dashboard-states-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}],
styleUrls: ['./manage-dashboard-states-dialog.component.scss']
})
export class ManageDashboardStatesDialogComponent extends
DialogComponent<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>
implements OnInit, ErrorStateMatcher, AfterViewInit {
statesFormGroup: FormGroup;
states: {[id: string]: DashboardState };
displayedColumns: string[];
pageLink: PageLink;
textSearchMode = false;
dataSource: DashboardStatesDatasource;
submitted = false;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>,
private fb: FormBuilder,
private translate: TranslateService,
private dialogs: DialogService,
private dialog: MatDialog) {
super(store, router, dialogRef);
this.states = this.data.states;
this.statesFormGroup = this.fb.group({});
const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC };
this.pageLink = new PageLink(5, 0, null, sortOrder);
this.displayedColumns = ['name', 'id', 'root', 'actions'];
this.dataSource = new DashboardStatesDatasource(this.states);
}
ngOnInit(): void {
this.dataSource.loadStates(this.pageLink);
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.updateData();
})
)
.subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => this.updateData())
)
.subscribe();
}
updateData(reload: boolean = false) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.dataSource.loadStates(this.pageLink, reload);
}
addState($event: Event) {
this.openStateDialog($event);
}
editState($event: Event, state: DashboardStateInfo) {
this.openStateDialog($event, state);
}
deleteState($event: Event, state: DashboardStateInfo) {
if ($event) {
$event.stopPropagation();
}
const title = this.translate.instant('dashboard.delete-state-title');
const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name});
this.dialogs.confirm(title, content, this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe(
(res) => {
if (res) {
delete this.states[state.id];
this.onStatesUpdated();
}
}
);
}
enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
this.updateData();
}
openStateDialog($event: Event, state: DashboardStateInfo = null) {
if ($event) {
$event.stopPropagation();
}
const isAdd = state === null;
let prevStateId = null;
if (!isAdd) {
prevStateId = state.id;
}
this.dialog.open<DashboardStateDialogComponent, DashboardStateDialogData,
DashboardStateInfo>(DashboardStateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
states: this.states,
state: deepClone(state)
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.saveState(res, prevStateId);
}
}
);
}
saveState(state: DashboardStateInfo, prevStateId: string) {
const newState: DashboardState = {
name: state.name,
root: state.root,
layouts: state.layouts
};
if (prevStateId) {
this.states[prevStateId] = newState;
} else {
this.states[state.id] = newState;
}
if (state.root) {
for (const id of Object.keys(this.states)) {
const otherState = this.states[id];
if (id !== state.id) {
otherState.root = false;
}
}
}
this.onStatesUpdated();
}
private onStatesUpdated() {
this.statesFormGroup.markAsDirty();
this.updateData(true);
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.dialogRef.close(this.states);
}
}

16
ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts

@ -84,6 +84,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
currentState: string;
currentUrl: string;
private rxSubscriptions = new Array<Subscription>();
private inited = false;
@ -94,12 +96,16 @@ export abstract class StateControllerComponent implements IStateControllerCompon
}
ngOnInit(): void {
this.currentUrl = this.router.url.split('?')[0];
this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
const newState = paramMap.get('state');
if (this.currentState !== newState) {
this.currentState = newState;
if (this.inited) {
this.onStateChanged();
const newUrl = this.router.url.split('?')[0];
if (this.currentUrl === newUrl) {
const newState = paramMap.get('state');
if (this.currentState !== newState) {
this.currentState = newState;
if (this.inited) {
this.onStateChanged();
}
}
}
}));

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -557,6 +557,7 @@
"edit-state": "Edit dashboard state",
"delete-state": "Delete dashboard state",
"add-state": "Add dashboard state",
"no-states-text": "No states found",
"state": "Dashboard state",
"state-name": "Name",
"state-name-required": "Dashboard state name is required.",

Loading…
Cancel
Save