Browse Source

State Controllers and Dashboard Layouts.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
b60b3144a0
  1. 6
      ui-ngx/src/app/core/api/alias-controller.ts
  2. 20
      ui-ngx/src/app/core/api/widget-api.models.ts
  3. 42
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  4. 18
      ui-ngx/src/app/core/services/utils.service.ts
  5. 5
      ui-ngx/src/app/core/translate/missing-translate-handler.ts
  6. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  7. 76
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  8. 2
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  9. 69
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  10. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  11. 40
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  12. 47
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  13. 132
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  14. 18
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts
  15. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  16. 65
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html
  17. 23
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss
  18. 80
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  19. 20
      ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts
  20. 23
      ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html
  21. 20
      ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss
  22. 255
      ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts
  23. 36
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html
  24. 45
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss
  25. 316
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts
  26. 173
      ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts
  27. 30
      ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts
  28. 122
      ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts
  29. 56
      ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts
  30. 64
      ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts
  31. 73
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  32. 3
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html
  33. 64
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  34. 2
      ui-ngx/src/app/shared/components/footer-fab-buttons.component.html
  35. 18
      ui-ngx/src/app/shared/components/footer-fab-buttons.component.scss
  36. 10
      ui-ngx/src/app/shared/components/footer-fab-buttons.component.ts
  37. 17
      ui-ngx/src/app/shared/models/dashboard.models.ts

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

@ -58,6 +58,9 @@ export class DummyAliasController implements IAliasController {
updateEntityAliases(entityAliases: EntityAliases) {
}
dashboardStateChanged() {
}
}
export class AliasController implements IAliasController {
@ -111,4 +114,7 @@ export class AliasController implements IAliasController {
updateEntityAliases(entityAliases: EntityAliases) {
}
dashboardStateChanged() {
}
}

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

@ -86,6 +86,7 @@ export interface IAliasController {
getEntityAliases(): EntityAliases;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
updateEntityAliases(entityAliases: EntityAliases);
dashboardStateChanged();
[key: string]: any | null;
// TODO:
}
@ -103,12 +104,19 @@ export interface StateParams {
}
export interface IStateController {
getStateParams?: () => StateParams;
openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void;
updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void;
openRightLayout: () => void;
preserveState?: () => void;
// TODO:
getStateParams(): StateParams;
getStateParamsByStateId(stateId: string): StateParams;
openState(id: string, params?: StateParams, openRightLayout?: boolean): void;
updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
resetState(): void;
openRightLayout(): void;
preserveState(): void;
cleanupPreservedStates(): void;
navigatePrevState(index: number): void;
getStateId(): string;
getStateIndex(): number;
getStateIdAtIndex(index: number): string;
getEntityId(entityParamName: string): EntityId;
}
export interface SubscriptionInfo {

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

@ -22,7 +22,9 @@ import {
DashboardLayout,
DashboardStateLayouts,
DashboardState,
DashboardConfiguration
DashboardConfiguration,
DashboardLayoutInfo,
DashboardLayoutsInfo
} from '@shared/models/dashboard.models';
import { isUndefined, isDefined, isString } from '@core/utils';
import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models';
@ -238,6 +240,44 @@ export class DashboardUtilsService {
};
}
public getRootStateId(states: {[id: string]: DashboardState }): string {
for (const stateId of Object.keys(states)) {
const state = states[stateId];
if (state.root) {
return stateId;
}
}
return Object.keys(states)[0];
}
public getStateLayoutsData(dashboard: Dashboard, targetState: string): DashboardLayoutsInfo {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
if (state) {
const allWidgets = dashboardConfiguration.widgets;
const result: DashboardLayoutsInfo = {};
for (const l of Object.keys(state.layouts)) {
const layout: DashboardLayout = state.layouts[l];
if (layout) {
result[l] = {
widgets: [],
widgetLayouts: {},
gridSettings: {}
} as DashboardLayoutInfo;
for (const id of Object.keys(layout.widgets)) {
result[l].widgets.push(allWidgets[id]);
}
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
}
}
return result;
} else {
return null;
}
}
private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {

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

@ -17,7 +17,7 @@
import { Inject, Injectable } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import { isUndefined, isDefined } from '@core/utils';
import { isUndefined, isDefined, deepClone } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants';
@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
const varsRegex = /\$\{([^}]*)\}/g;
@Injectable({
providedIn: 'root'
})
@ -144,6 +146,20 @@ export class UtilsService {
return result;
}
public insertVariable(pattern: string, name: string, value: any): string {
let result = deepClone(pattern);
let match = varsRegex.exec(pattern);
while (match !== null) {
const variable = match[0];
const variableName = match[1];
if (variableName === name) {
result = result.split(variable).join(value);
}
match = varsRegex.exec(pattern);
}
return result;
}
public guid(): string {
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)

5
ui-ngx/src/app/core/translate/missing-translate-handler.ts

@ -15,9 +15,12 @@
///
import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants';
export class TbMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) {
console.warn('Translation for ' + params.key + ' doesn\'t exist');
if (params.key && !params.key.startsWith(customTranslationsPrefix)) {
console.warn('Translation for ' + params.key + ' doesn\'t exist');
}
}
}

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

@ -26,7 +26,7 @@
(contextmenu)="openDashboardContextMenu($event)">
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async">
<gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets">
<div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
fxLayout="column"
class="tb-widget"

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

@ -37,16 +37,18 @@ import {
DashboardCallbacks,
DashboardWidget,
IDashboardComponent,
WidgetsData
WidgetsData,
DashboardWidgets
} from '../../models/dashboard-component.models';
import { merge, Observable, ReplaySubject, Subject } from 'rxjs';
import { map, share, tap } from 'rxjs/operators';
import { WidgetLayout } from '@shared/models/dashboard.models';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { Widget } from '@app/shared/models/widget.models';
@Component({
selector: 'tb-dashboard',
@ -58,7 +60,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
authUser: AuthUser;
@Input()
widgetsData: Observable<WidgetsData>;
widgets: Array<Widget>;
@Input()
widgetLayouts: WidgetLayouts;
@Input()
callbacks: DashboardCallbacks;
@ -125,8 +130,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
gridsterOpts: GridsterConfig;
dashboardLoading = true;
highlightedMode = false;
highlightedWidget: DashboardWidget = null;
selectedWidget: DashboardWidget = null;
@ -138,9 +141,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>;
widgets$: Observable<Array<DashboardWidget>>;
dashboardLoading = true;
widgets: Array<DashboardWidget>;
dashboardWidgets = new DashboardWidgets(this);
constructor(protected store: Store<AppState>,
private timeService: TimeService,
@ -172,25 +175,26 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemRows: 6,
resizable: {enabled: this.isEdit},
draggable: {enabled: this.isEdit},
itemChangeCallback: item => this.sortWidgets(this.widgets)
itemChangeCallback: item => this.dashboardWidgets.sortWidgets()
};
this.updateMobileOpts();
this.loadDashboard();
this.breakpointObserver
.observe(MediaBreakpoints['gt-sm']).subscribe(
() => {
this.updateMobileOpts();
}
);
this.updateWidgets();
}
ngOnChanges(changes: SimpleChanges): void {
let updateMobileOpts = false;
let updateLayoutOpts = false;
let updateEditingOpts = false;
let updateWidgets = false;
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
@ -200,9 +204,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
updateLayoutOpts = true;
} else if (propName === 'isEdit') {
updateEditingOpts = true;
} else if (['widgets', 'widgetLayouts'].includes(propName)) {
updateWidgets = true;
}
}
}
if (updateWidgets) {
this.updateWidgets();
}
if (updateMobileOpts) {
this.updateMobileOpts();
}
@ -217,50 +226,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
}
loadDashboard() {
this.widgets$ = this.widgetsData.pipe(
map(widgetsData => {
const dashboardWidgets = new Array<DashboardWidget>();
let maxRows = this.gridsterOpts.maxRows;
widgetsData.widgets.forEach(
(widget) => {
let widgetLayout: WidgetLayout;
if (widgetsData.widgetLayouts && widget.id) {
widgetLayout = widgetsData.widgetLayouts[widget.id];
}
const dashboardWidget = new DashboardWidget(this, widget, widgetLayout);
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
dashboardWidgets.push(dashboardWidget);
}
);
this.sortWidgets(dashboardWidgets);
this.gridsterOpts.maxRows = maxRows;
return dashboardWidgets;
}),
tap((widgets) => {
this.widgets = widgets;
this.dashboardLoading = false;
})
);
}
reload() {
this.loadDashboard();
}
sortWidgets(widgets?: Array<DashboardWidget>) {
if (widgets) {
widgets.sort((widget1, widget2) => {
const row1 = widget1.widgetOrder;
const row2 = widget2.widgetOrder;
let res = row1 - row2;
if (res === 0) {
res = widget1.x - widget2.x;
}
return res;
});
}
private updateWidgets() {
this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts);
this.dashboardLoading = false;
}
ngAfterViewInit(): void {

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

@ -119,7 +119,7 @@ export class WidgetComponentService {
} else {
fetchQueue = new Array<Subject<WidgetInfo>>();
this.widgetsInfoFetchQueue.set(key, fetchQueue);
this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe(
this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, true, false).subscribe(
(widgetType) => {
this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject);
},

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

@ -43,6 +43,7 @@ export interface DashboardCallbacks {
export interface IDashboardComponent {
gridsterOpts: GridsterConfig;
gridster: GridsterComponent;
dashboardWidgets: DashboardWidgets;
mobileAutofillHeight: boolean;
isMobileSize: boolean;
autofillHeight: boolean;
@ -54,6 +55,74 @@ export interface IDashboardComponent {
onResetTimewindow(): void;
}
export class DashboardWidgets implements Iterable<DashboardWidget> {
dashboardWidgets: Array<DashboardWidget> = [];
[Symbol.iterator](): Iterator<DashboardWidget> {
return this.dashboardWidgets[Symbol.iterator]();
}
constructor(private dashboard: IDashboardComponent) {
}
setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
let maxRows = this.dashboard.gridsterOpts.maxRows;
this.dashboardWidgets.length = 0;
widgets.forEach((widget) => {
let widgetLayout: WidgetLayout;
if (widgetLayouts && widget.id) {
widgetLayout = widgetLayouts[widget.id];
}
const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
this.dashboardWidgets.push(dashboardWidget);
});
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
}
addWidget(widget: Widget, widgetLayout: WidgetLayout) {
const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
let maxRows = this.dashboard.gridsterOpts.maxRows;
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
this.dashboardWidgets.push(dashboardWidget);
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
}
removeWidget(widget: Widget): boolean {
const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget);
if (index > -1) {
this.dashboardWidgets.splice(index, 1);
let maxRows = this.dashboard.gridsterOpts.maxRows;
this.dashboardWidgets.forEach((dashboardWidget) => {
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
});
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
return true;
}
return false;
}
sortWidgets() {
this.dashboardWidgets.sort((widget1, widget2) => {
const row1 = widget1.widgetOrder;
const row2 = widget2.widgetOrder;
let res = row1 - row2;
if (res === 0) {
res = widget1.x - widget2.x;
}
return res;
});
}
}
export class DashboardWidget implements GridsterItem {
isFullscreen = false;

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

@ -123,7 +123,7 @@ export const MissingWidgetType: WidgetInfo = {
sizeY: 6,
resources: [],
templateHtml: '<div class="tb-widget-error-container">' +
'<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' +
'<div class="tb-widget-error-msg" innerHTML="{{\'widget.widget-type-not-found\' | translate }}"></div>' +
'</div>',
templateCss: '',
controllerScript: 'self.onInit = function() {}',

40
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -24,6 +24,9 @@ import {CustomersTableConfigResolver} from './customers-table-config.resolver';
import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver';
import {DashboardsTableConfigResolver} from '@modules/home/pages/dashboard/dashboards-table-config.resolver';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module';
const routes: Routes = [
{
@ -95,19 +98,42 @@ const routes: Routes = [
},
{
path: ':customerId/dashboards',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.assets',
dashboardsType: 'customer',
breadcrumb: {
label: 'customer.dashboards',
icon: 'dashboard'
}
},
resolve: {
entitiesTableConfig: DashboardsTableConfigResolver
}
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.dashboards',
dashboardsType: 'customer'
},
resolve: {
entitiesTableConfig: DashboardsTableConfigResolver
}
},
{
path: ':dashboardId',
component: DashboardPageComponent,
data: {
breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction,
icon: 'dashboard'
} as BreadCrumbConfig,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'customer.dashboard',
widgetEditMode: false
},
resolve: {
dashboard: DashboardResolver
}
}
]
}
]
}

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

@ -85,7 +85,31 @@
fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu>
<!-- TODO -->
<div [fxShow]="isEdit"
fxFlex.xs fxFlex.sm fxLayout="row"
fxLayoutAlign.gt-sm="start center"
fxLayoutAlign="space-between center" fxLayoutGap="12px">
<button mat-button mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
</button>
<button mat-button mat-icon-button
matTooltip="{{'layout.manage' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardLayouts($event)">
<mat-icon>view_compact</mat-icon>
</button>
</div>
<tb-states-component fxFlex.xs fxFlex.sm
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
[dashboardId]="dashboard.id ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[states]="dashboardConfiguration.states">
</tb-states-component>
</div>
</div>
</tb-dashboard-toolbar>
@ -110,7 +134,13 @@
id="tb-main-layout"
[ngStyle]="{width: mainLayoutWidth(),
height: mainLayoutHeight()}">
TODO: MAIN LAYOUT tb-dashboard-layout
<tb-dashboard-layout
[layoutCtx]="layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</div>
<mat-sidenav-container *ngIf="layouts.right.show"
id="tb-right-layout">
@ -123,14 +153,23 @@
position="end"
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">
TODO: RIGHT LAYOUT tb-dashboard-layout
<tb-dashboard-layout style="height: 100%;"
[layoutCtx]="layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</mat-sidenav>
</mat-sidenav-container>
</div>
<!--tb-details-sidenav TODO -->
<!--tb-details-sidenav TODO -->
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<!--md-fab-speed-dial TODO -->
<tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"
relative
[footerFabButtons]="addWidgetFabButtons">
</tb-footer-fab-buttons>
<button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': !isEdit || isAddingWidget}"

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

@ -21,15 +21,21 @@ import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import { Dashboard, DashboardConfiguration, WidgetLayout } from '@app/shared/models/dashboard.models';
import {
Dashboard,
DashboardConfiguration,
WidgetLayout,
DashboardLayoutInfo,
DashboardLayoutsInfo
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, isDefined } from '@app/core/utils';
import {
DashboardContext,
DashboardContext, DashboardPageLayout,
DashboardPageLayoutContext,
DashboardPageLayouts,
DashboardPageScope
DashboardPageScope, IDashboardController
} from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@ -42,6 +48,9 @@ import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller';
import { Subscription } from 'rxjs';
import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
import { IStateController } from '@core/api/widget-api.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@Component({
selector: 'tb-dashboard-page',
@ -49,7 +58,7 @@ import { Subscription } from 'rxjs';
styleUrls: ['./dashboard-page.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DashboardPageComponent extends PageComponent implements OnDestroy {
export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy {
authUser: AuthUser = getCurrentAuthUser(this.store);
@ -94,7 +103,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
widgets: [],
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false
ignoreLoading: false,
ctrl: null
}
},
right: {
@ -104,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
widgets: [],
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false
ignoreLoading: false,
ctrl: null
}
}
};
@ -113,12 +124,31 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
dashboard: null,
dashboardTimewindow: null,
state: null,
stateController: {
openRightLayout: this.openRightLayout.bind(this)
},
stateController: null,
aliasController: null
};
addWidgetFabButtons: FooterFabButtons = {
fabTogglerName: 'dashboard.add-widget',
fabTogglerIcon: 'add',
buttons: [
{
name: 'dashboard.create-new-widget',
icon: 'insert_drive_file',
onAction: ($event) => {
this.addWidget($event);
}
},
{
name: 'dashboard.import-widget',
icon: 'file_upload',
onAction: ($event) => {
this.importWidget($event);
}
}
]
};
private rxSubscriptions = new Array<Subscription>();
get toolbarOpened(): boolean {
@ -140,6 +170,7 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
private route: ActivatedRoute,
private router: Router,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService,
private dialogService: DialogService) {
@ -379,6 +410,38 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
this.dialogService.todo();
}
public manageDashboardStates($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
public manageDashboardLayouts($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
private addWidget($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
private importWidget($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
public currentDashboardIdChanged(dashboardId: string) {
if (!this.widgetEditMode) {
if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) {
@ -397,6 +460,57 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
this.setEditMode(!this.isEdit, true);
}
public openDashboardState(state: string, openRightLayout: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
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, layoutVisibilityChanged);
}
}
private updateLayouts(layoutsData: DashboardLayoutsInfo, layoutVisibilityChanged: boolean) {
for (const l of Object.keys(this.layouts)) {
const layout: DashboardPageLayout = this.layouts[l];
if (layoutsData[l]) {
const layoutInfo: DashboardLayoutInfo = layoutsData[l];
if (layout.layoutCtx.id === 'main') {
layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged);
}
this.updateLayout(layout, layoutInfo);
} else {
this.updateLayout(layout, {widgets: [], widgetLayouts: {}, gridSettings: null});
}
}
}
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) {
layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
layout.layoutCtx.widgets = layoutInfo.widgets;
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
}
layout.layoutCtx.ignoreLoading = true;
}
private setEditMode(isEdit: boolean, revert: boolean) {
this.isEdit = isEdit;
if (this.isEdit) {

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

@ -14,10 +14,11 @@
/// limitations under the License.
///
import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard } from '@app/shared/models/dashboard.models';
import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { Widget } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models';
export declare type DashboardPageScope = 'tenant' | 'customer';
@ -29,11 +30,18 @@ export interface DashboardContext {
stateController: IStateController;
}
export interface IDashboardController {
dashboardCtx: DashboardContext;
openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean);
}
export interface DashboardPageLayoutContext {
id: DashboardLayoutId;
widgets: Array<Widget>;
widgetLayouts: {[id: string]: WidgetLayout};
widgetLayouts: WidgetLayouts;
gridSettings: GridSettings;
ctrl: ILayoutController;
ignoreLoading: boolean;
}
@ -42,7 +50,5 @@ export interface DashboardPageLayout {
layoutCtx: DashboardPageLayoutContext;
}
export interface DashboardPageLayouts {
main: DashboardPageLayout;
right: DashboardPageLayout;
}
export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout};

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

@ -26,6 +26,8 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { DashboardToolbarComponent } from './dashboard-toolbar.component';
import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
@NgModule({
entryComponents: [
@ -40,13 +42,15 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component';
ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent,
DashboardToolbarComponent,
DashboardPageComponent
DashboardPageComponent,
DashboardLayoutComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
StatesControllerModule,
DashboardRoutingModule
]
})

65
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html

@ -0,0 +1,65 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="mat-content" style="position: relative; width: 100%; height: 100%;"
[ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
'background-repeat': 'no-repeat',
'background-attachment': 'scroll',
'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'}">
<section *ngIf="layoutCtx.widgets.length === 0" fxLayoutAlign="center center"
[ngStyle]="{'color': layoutCtx.gridSettings.color}"
style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
class="mat-headline tb-absolute-fill">
<span *ngIf="!isEdit">
{{'dashboard.no-widgets' | translate}}
</span>
<button mat-button *ngIf="isEdit && !widgetEditMode" class="tb-add-new-widget"
(click)="addWidget($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
{{ 'dashboard.add-widget' | translate }}
</button>
</section>
<tb-dashboard #dashboard [dashboardStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
'background-repeat': 'no-repeat',
'background-attachment': 'scroll',
'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'}"
[widgets]="layoutCtx.widgets"
[widgetLayouts]="layoutCtx.widgetLayouts"
[columns]="layoutCtx.gridSettings.columns"
[horizontalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[0] : 10"
[verticalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[1]: 10"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[dashboardTimewindow]="dashboardCtx.dashboardTimewindow"
[isEdit]="isEdit"
[autofillHeight]="layoutCtx.gridSettings.autoFillHeight && !isEdit"
[mobileAutofillHeight]="layoutCtx.gridSettings.mobileAutoFillHeight && !isEdit"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile"
[isMobileDisabled]="widgetEditMode"
[isEditActionEnabled]="isEdit"
[isExportActionEnabled]="isEdit && !widgetEditMode"
[isRemoveActionEnabled]="isEdit && !widgetEditMode"
[ignoreLoading]="layoutCtx.ignoreLoading">
</tb-dashboard>
</div>

23
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
button.tb-add-new-widget {
padding-right: 12px;
font-size: 24px;
border-style: dashed;
border-width: 2px;
}
}

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

@ -0,0 +1,80 @@
///
/// 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, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core';
import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component';
import { ILayoutController } from '@home/pages/dashboard/layout/layout.models';
import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Widget } from '@shared/models/widget.models';
import { WidgetLayouts } from '@shared/models/dashboard.models';
import { GridsterComponent } from 'angular-gridster2';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
@Component({
selector: 'tb-dashboard-layout',
templateUrl: './dashboard-layout.component.html',
styleUrls: ['./dashboard-layout.component.scss']
})
export class DashboardLayoutComponent extends PageComponent implements ILayoutController, OnInit, OnDestroy {
layoutCtxValue: DashboardPageLayoutContext;
@Input()
set layoutCtx(val: DashboardPageLayoutContext) {
this.layoutCtxValue = val;
if (this.layoutCtxValue) {
this.layoutCtxValue.ctrl = this;
}
}
get layoutCtx(): DashboardPageLayoutContext {
return this.layoutCtxValue;
}
@Input()
dashboardCtx: DashboardContext;
@Input()
isEdit: boolean;
@Input()
isMobile: boolean;
@Input()
widgetEditMode: boolean;
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit(): void {
}
ngOnDestroy(): void {
}
reload() {
}
setResizing(layoutVisibilityChanged: boolean) {
}
}

20
ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts

@ -0,0 +1,20 @@
///
/// 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.
///
export interface ILayoutController {
reload();
setResizing(layoutVisibilityChanged: boolean);
}

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

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-select class="default-state-controller" [fxShow]="displayStateSelection()"
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
<mat-option *ngFor="let stateKv of states | keyvalue" [value]="stateKv.key">
{{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
</mat-select>

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

@ -0,0 +1,20 @@
/**
* 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 {
mat-select.default-state-controller {
margin: 0;
}
}

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

@ -0,0 +1,255 @@
///
/// 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,
OnInit,
ViewEncapsulation,
Input,
OnDestroy,
OnChanges,
SimpleChanges,
NgZone
} from '@angular/core';
import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subscription, of } from 'rxjs';
import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
import { DashboardState } from '@shared/models/dashboard.models';
import { IStateControllerComponent, StateControllerState } from './state-controller.models';
import { StateControllerComponent } from './state-controller.component';
import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
import { EntityId } from '@app/shared/models/id/entity-id';
import { UtilsService } from '@core/services/utils.service';
import { base64toObj, objToBase64 } from '@app/core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
import { map } from 'rxjs/operators';
@Component({
selector: 'tb-default-state-controller',
templateUrl: './default-state-controller.component.html',
styleUrls: ['./default-state-controller.component.scss']
})
export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
constructor(protected router: Router,
protected route: ActivatedRoute,
protected statesControllerService: StatesControllerService,
private utils: UtilsService,
private entityService: EntityService,
private dashboardUtils: DashboardUtilsService) {
super(router, route, statesControllerService);
}
ngOnInit(): void {
super.ngOnInit();
}
ngOnDestroy(): void {
super.ngOnDestroy();
}
protected init() {
if (this.preservedState) {
this.stateObject = this.preservedState;
setTimeout(() => {
this.gotoState(this.stateObject[0].id, true);
}, 1);
} else {
const initialState = this.currentState;
this.stateObject = this.parseState(initialState);
setTimeout(() => {
this.gotoState(this.stateObject[0].id, false);
}, 1);
}
}
protected onMobileChanged() {
}
protected onStateIdChanged() {
}
protected onStatesChanged() {
}
protected onStateChanged() {
this.stateObject = this.parseState(this.currentState);
this.gotoState(this.stateObject[0].id, true);
}
protected stateControllerId(): string {
return 'default';
}
public getStateParams(): StateParams {
if (this.stateObject && this.stateObject.length) {
return this.stateObject[this.stateObject.length - 1].params;
} else {
return {};
}
}
public openState(id: string, params?: StateParams, openRightLayout?: boolean): void {
if (this.states && this.states[id]) {
if (!params) {
params = {};
}
const newState: StateObject = {
id,
params
};
this.stateObject[0] = newState;
this.gotoState(this.stateObject[0].id, true, openRightLayout);
}
}
public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void {
if (!id) {
id = this.getStateId();
}
if (this.states && this.states[id]) {
if (!params) {
params = {};
}
const newState: StateObject = {
id,
params
};
this.stateObject[0] = newState;
this.gotoState(this.stateObject[0].id, true, openRightLayout);
}
}
public getEntityId(entityParamName: string): EntityId {
return null;
}
public getStateId(): string {
if (this.stateObject && this.stateObject.length) {
return this.stateObject[this.stateObject.length - 1].id;
} else {
return '';
}
}
public getStateIdAtIndex(index: number): string {
if (this.stateObject && this.stateObject[index]) {
return this.stateObject[index].id;
} else {
return '';
}
}
public getStateIndex(): number {
if (this.stateObject && this.stateObject.length) {
return this.stateObject.length - 1;
} else {
return -1;
}
}
public getStateParamsByStateId(stateId: string): StateParams {
const stateObj = this.getStateObjById(stateId);
if (stateObj) {
return stateObj.params;
} else {
return null;
}
}
public navigatePrevState(index: number): void {
if (index < this.stateObject.length - 1) {
this.stateObject.splice(index + 1, this.stateObject.length - index - 1);
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
}
}
public resetState(): void {
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
this.stateObject = [ { id: rootStateId, params: {} } ];
this.gotoState(rootStateId, true);
}
public getStateName(id: string, state: DashboardState): string {
return this.utils.customTranslation(state.name, id);
}
public displayStateSelection(): boolean {
return this.states && Object.keys(this.states).length > 1;
}
public selectedStateIdChanged() {
this.gotoState(this.stateObject[0].id, true);
}
private parseState(stateBase64: string): StateControllerState {
let result: StateControllerState;
if (stateBase64) {
try {
result = base64toObj(stateBase64);
} catch (e) {
result = [ { id: null, params: {} } ];
}
}
if (!result) {
result = [];
}
if (!result.length) {
result[0] = { id: null, params: {} };
} else if (result.length > 1) {
const newResult = [];
newResult.push(result[result.length - 1]);
result = newResult;
}
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
if (!result[0].id) {
result[0].id = rootStateId;
}
if (!this.states[result[0].id]) {
result[0].id = rootStateId;
}
let i = result.length;
while (i--) {
if (!result[i].id || !this.states[result[i].id]) {
result.splice(i, 1);
}
}
return result;
}
private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
if (this.dashboardCtrl.dashboardCtx.state !== stateId) {
this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
if (update) {
this.updateLocation();
}
}
}
private updateLocation() {
if (this.stateObject[0].id) {
const newState = objToBase64(this.stateObject);
this.updateStateParam(newState);
}
}
private getStateObjById(id: string): StateObject {
return this.stateObject.find((stateObj) => stateObj.id === id);
}
}

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

@ -0,0 +1,36 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="entity-state-controller" fxLayout="row" fxLayoutAlign="start center">
<div *ngIf="!isMobile || stateObject.length === 1" fxLayout="row" fxLayoutAlign="start center">
<span *ngFor="let state of stateObject; index as i; last as isLast" class="state-entry" [ngStyle]="{fontWeight: isLast ? 'bold' : 'normal',
cursor: isLast ? 'default' : 'pointer'}" (click)="navigatePrevState(i)">
{{getStateName(i)}}
<span class='state-divider' [fxHide]="isLast"> > </span>
</span>
</div>
<mat-select *ngIf="isMobile && stateObject.length > 1"
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
<mat-option *ngFor="let state of stateObject; index as i" [value]="i">
{{getStateName(i)}}
</mat-option>
</mat-select>
</div>

45
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.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 {
.entity-state-controller {
.state-divider {
padding-right: 15px;
padding-left: 15px;
overflow: hidden;
font-size: 18px;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.state-entry {
overflow: hidden;
font-size: 18px;
text-overflow: ellipsis;
white-space: nowrap;
outline: none;
}
mat-select {
margin: 0;
.mat-select-value-text {
font-size: 18px;
font-weight: 700;
}
}
}
}

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

@ -0,0 +1,316 @@
///
/// 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,
OnInit,
ViewEncapsulation,
Input,
OnDestroy,
OnChanges,
SimpleChanges,
NgZone
} from '@angular/core';
import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subscription, of } from 'rxjs';
import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
import { DashboardState } from '@shared/models/dashboard.models';
import { IStateControllerComponent, StateControllerState } from './state-controller.models';
import { StateControllerComponent } from './state-controller.component';
import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
import { EntityId } from '@app/shared/models/id/entity-id';
import { UtilsService } from '@core/services/utils.service';
import { base64toObj, objToBase64 } from '@app/core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
import { map } from 'rxjs/operators';
@Component({
selector: 'tb-entity-state-controller',
templateUrl: './entity-state-controller.component.html',
styleUrls: ['./entity-state-controller.component.scss']
})
export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
private selectedStateIndex = -1;
constructor(protected router: Router,
protected route: ActivatedRoute,
protected statesControllerService: StatesControllerService,
private utils: UtilsService,
private entityService: EntityService,
private dashboardUtils: DashboardUtilsService) {
super(router, route, statesControllerService);
}
ngOnInit(): void {
super.ngOnInit();
}
ngOnDestroy(): void {
super.ngOnDestroy();
}
protected init() {
if (this.preservedState) {
this.stateObject = this.preservedState;
this.selectedStateIndex = this.stateObject.length - 1;
setTimeout(() => {
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
}, 1);
} else {
const initialState = this.currentState;
this.stateObject = this.parseState(initialState);
this.selectedStateIndex = this.stateObject.length - 1;
setTimeout(() => {
this.gotoState(this.stateObject[this.stateObject.length - 1].id, false);
}, 1);
}
}
protected onMobileChanged() {
}
protected onStateIdChanged() {
}
protected onStatesChanged() {
}
protected onStateChanged() {
this.stateObject = this.parseState(this.currentState);
this.selectedStateIndex = this.stateObject.length - 1;
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
}
protected stateControllerId(): string {
return 'entity';
}
public getStateParams(): StateParams {
if (this.stateObject && this.stateObject.length) {
return this.stateObject[this.stateObject.length - 1].params;
} else {
return {};
}
}
public openState(id: string, params?: StateParams, openRightLayout?: boolean): void {
if (this.states && this.states[id]) {
this.resolveEntity(params).subscribe(
(entityName) => {
params.entityName = entityName;
const newState: StateObject = {
id,
params
};
this.stateObject.push(newState);
this.selectedStateIndex = this.stateObject.length - 1;
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout);
}
);
}
}
public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void {
if (!id) {
id = this.getStateId();
}
if (this.states && this.states[id]) {
this.resolveEntity(params).subscribe(
(entityName) => {
params.entityName = entityName;
const newState: StateObject = {
id,
params
};
this.stateObject[this.stateObject.length - 1] = newState;
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout);
}
);
}
}
public getEntityId(entityParamName: string): EntityId {
const stateParams = this.getStateParams();
if (!entityParamName || !entityParamName.length) {
return stateParams.entityId;
} else if (stateParams[entityParamName]) {
return stateParams[entityParamName].entityId;
}
return null;
}
public getStateId(): string {
if (this.stateObject && this.stateObject.length) {
return this.stateObject[this.stateObject.length - 1].id;
} else {
return '';
}
}
public getStateIdAtIndex(index: number): string {
if (this.stateObject && this.stateObject[index]) {
return this.stateObject[index].id;
} else {
return '';
}
}
public getStateIndex(): number {
if (this.stateObject && this.stateObject.length) {
return this.stateObject.length - 1;
} else {
return -1;
}
}
public getStateParamsByStateId(stateId: string): StateParams {
const stateObj = this.getStateObjById(stateId);
if (stateObj) {
return stateObj.params;
} else {
return null;
}
}
public navigatePrevState(index: number): void {
if (index < this.stateObject.length - 1) {
this.stateObject.splice(index + 1, this.stateObject.length - index - 1);
this.selectedStateIndex = this.stateObject.length - 1;
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
}
}
public resetState(): void {
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
this.stateObject = [ { id: rootStateId, params: {} } ];
this.gotoState(rootStateId, true);
}
public getStateName(index: number): string {
let result = '';
if (this.stateObject[index]) {
let stateName = this.states[this.stateObject[index].id].name;
stateName = this.utils.customTranslation(stateName, stateName);
const params = this.stateObject[index].params;
const entityName = params && params.entityName ? params.entityName : '';
result = this.utils.insertVariable(stateName, 'entityName', entityName);
for (const prop of Object.keys(params)) {
if (params[prop] && params[prop].entityName) {
result = this.utils.insertVariable(result, prop + ':entityName', params[prop].entityName);
}
}
}
return result;
}
public selectedStateIndexChanged() {
this.navigatePrevState(this.selectedStateIndex);
}
private parseState(stateBase64: string): StateControllerState {
let result: StateControllerState;
if (stateBase64) {
try {
result = base64toObj(stateBase64);
} catch (e) {
result = [ { id: null, params: {} } ];
}
}
if (!result) {
result = [];
}
if (!result.length) {
result[0] = { id: null, params: {} };
}
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
if (!result[0].id) {
result[0].id = rootStateId;
}
if (!this.states[result[0].id]) {
result[0].id = rootStateId;
}
let i = result.length;
while (i--) {
if (!result[i].id || !this.states[result[i].id]) {
result.splice(i, 1);
}
}
return result;
}
private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
if (update) {
this.updateLocation();
}
}
private updateLocation() {
if (this.stateObject[this.stateObject.length - 1].id) {
let newState;
if (this.isDefaultState()) {
newState = null;
} else {
newState = objToBase64(this.stateObject);
}
this.updateStateParam(newState);
}
}
private isDefaultState(): boolean {
if (this.stateObject.length === 1) {
const state = this.stateObject[0];
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
if (state.id === rootStateId && (!state.params || this.isEmpty(state.params))) {
return true;
}
}
return false;
}
private isEmpty(obj: any): boolean {
for (const key of Object.keys(obj)) {
return !Object.prototype.hasOwnProperty.call(obj, key);
}
return true;
}
private resolveEntity(params: StateParams): Observable<string> {
if (params && params.targetEntityParamName) {
params = params[params.targetEntityParamName];
}
if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
if (params.entityName && params.entityName.length) {
return of(params.entityName);
} else {
return this.entityService.getEntity(params.entityId.entityType as EntityType,
params.entityId.id, true, true).pipe(
map((entity) => entity.name)
);
}
} else {
return of('');
}
}
private getStateObjById(id: string): StateObject {
return this.stateObject.find((stateObj) => stateObj.id === id);
}
}

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

@ -0,0 +1,173 @@
///
/// 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 { IStateControllerComponent, StateControllerState } from '@home/pages/dashboard/states/state-controller.models';
import { IDashboardController } from '../dashboard-page.models';
import { DashboardState } from '@app/shared/models/dashboard.models';
import { Subscription } from 'rxjs';
import { OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';
import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
import { EntityId } from '@app/shared/models/id/entity-id';
import { StateParams } from '@app/core/api/widget-api.models';
export abstract class StateControllerComponent implements IStateControllerComponent, OnInit, OnDestroy {
stateObject: StateControllerState = [];
dashboardCtrl: IDashboardController;
preservedState: any;
isMobileValue: boolean;
set isMobile(val: boolean) {
if (this.isMobileValue !== val) {
this.isMobileValue = val;
if (this.inited) {
this.onMobileChanged();
}
}
}
get isMobile(): boolean {
return this.isMobileValue;
}
stateValue: string;
set state(val: string) {
if (this.stateValue !== val) {
this.stateValue = val;
if (this.inited) {
this.onStateIdChanged();
}
}
}
get state(): string {
return this.stateValue;
}
dashboardIdValue: string;
set dashboardId(val: string) {
if (this.dashboardIdValue !== val) {
this.dashboardIdValue = val;
if (this.inited) {
this.init();
}
}
}
get dashboardId(): string {
return this.dashboardIdValue;
}
statesValue: { [id: string]: DashboardState };
set states(val: { [id: string]: DashboardState }) {
if (this.statesValue !== val) {
this.statesValue = val;
if (this.inited) {
this.onStatesChanged();
}
}
}
get states(): { [id: string]: DashboardState } {
return this.statesValue;
}
currentState: string;
private rxSubscriptions = new Array<Subscription>();
private inited = false;
constructor(protected router: Router,
protected route: ActivatedRoute,
protected statesControllerService: StatesControllerService) {
}
ngOnInit(): void {
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();
}
}
}));
this.init();
this.inited = true;
}
ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
protected updateStateParam(newState: string) {
this.currentState = newState;
const queryParams: Params = { state: this.currentState };
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams,
queryParamsHandling: 'merge',
});
}
public openRightLayout(): void {
this.dashboardCtrl.openRightLayout();
}
public preserveState() {
this.statesControllerService.preserveStateControllerState(this.stateControllerId(), this.stateObject);
}
public cleanupPreservedStates() {
this.statesControllerService.cleanupPreservedStates();
}
protected abstract init();
protected abstract onMobileChanged();
protected abstract onStateIdChanged();
protected abstract onStatesChanged();
protected abstract onStateChanged();
protected abstract stateControllerId(): string;
public abstract getEntityId(entityParamName: string): EntityId;
public abstract getStateId(): string;
public abstract getStateIdAtIndex(index: number): string;
public abstract getStateIndex(): number;
public abstract getStateParams(): StateParams;
public abstract getStateParamsByStateId(stateId: string): StateParams;
public abstract navigatePrevState(index: number): void;
public abstract openState(id: string, params?: StateParams, openRightLayout?: boolean): void;
public abstract resetState(): void;
public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
}

30
ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts

@ -0,0 +1,30 @@
///
/// 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 { IStateController, StateObject } from '@core/api/widget-api.models';
import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
import { DashboardState } from '@shared/models/dashboard.models';
export declare type StateControllerState = StateObject[];
export interface IStateControllerComponent extends IStateController {
state: string;
isMobile: boolean;
dashboardCtrl: IDashboardController;
states: {[id: string]: DashboardState };
dashboardId: string;
preservedState: any;
}

122
ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts

@ -0,0 +1,122 @@
///
/// 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 {
ComponentRef,
Directive,
ElementRef,
Input,
OnChanges,
OnInit,
OnDestroy,
SimpleChanges,
ViewContainerRef,
ChangeDetectorRef
} from '@angular/core';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { DashboardState } from '@shared/models/dashboard.models';
import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
import { IStateController } from '@core/api/widget-api.models';
import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models';
@Directive({
selector: 'tb-states-component'
})
export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
@Input()
statesControllerId: string;
@Input()
dashboardCtrl: IDashboardController;
@Input()
dashboardId: string;
@Input()
states: {[id: string]: DashboardState };
@Input()
state: string;
@Input()
isMobile: boolean;
stateControllerComponentRef: ComponentRef<IStateControllerComponent>;
stateControllerComponent: IStateControllerComponent;
constructor(private viewContainerRef: ViewContainerRef,
private statesControllerService: StatesControllerService) {
}
ngOnInit(): void {
this.init();
}
ngOnDestroy(): void {
this.destroy();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'statesControllerId') {
this.reInit();
} else if (propName === 'states') {
this.stateControllerComponent.states = this.states;
} else if (propName === 'dashboardId') {
this.stateControllerComponent.dashboardId = this.dashboardId;
} else if (propName === 'isMobile') {
this.stateControllerComponent.isMobile = this.isMobile;
} else if (propName === 'state') {
this.stateControllerComponent.state = this.state;
}
}
}
}
private reInit() {
this.destroy();
this.init();
}
private init() {
this.viewContainerRef.clear();
let stateControllerData = this.statesControllerService.getStateController(this.statesControllerId);
if (!stateControllerData) {
stateControllerData = this.statesControllerService.getStateController('default');
}
const preservedState = this.statesControllerService.withdrawStateControllerState(this.statesControllerId);
const stateControllerFactory = stateControllerData.factory;
this.stateControllerComponentRef = this.viewContainerRef.createComponent(stateControllerFactory);
this.stateControllerComponent = this.stateControllerComponentRef.instance;
this.dashboardCtrl.dashboardCtx.stateController = this.stateControllerComponent;
this.stateControllerComponent.preservedState = preservedState;
this.stateControllerComponent.dashboardCtrl = this.dashboardCtrl;
this.stateControllerComponent.state = this.state;
this.stateControllerComponent.isMobile = this.isMobile;
this.stateControllerComponent.states = this.states;
this.stateControllerComponent.dashboardId = this.dashboardId;
}
private destroy() {
if (this.stateControllerComponentRef) {
this.stateControllerComponentRef.destroy();
}
}
}

56
ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts

@ -0,0 +1,56 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { StatesControllerService } from './states-controller.service';
import { EntityStateControllerComponent } from './entity-state-controller.component';
import { StatesComponentDirective } from './states-component.directive';
import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module';
import { DefaultStateControllerComponent } from '@home/pages/dashboard/states/default-state-controller.component';
@NgModule({
entryComponents: [
DefaultStateControllerComponent,
EntityStateControllerComponent
],
declarations: [
StatesComponentDirective,
DefaultStateControllerComponent,
EntityStateControllerComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeDialogsModule
],
exports: [
StatesComponentDirective
],
providers: [
StatesControllerService
]
})
export class StatesControllerModule {
constructor(private statesControllerService: StatesControllerService) {
this.statesControllerService.registerStatesController('default', DefaultStateControllerComponent);
this.statesControllerService.registerStatesController('entity', EntityStateControllerComponent);
}
}

64
ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts

@ -0,0 +1,64 @@
///
/// 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 { ComponentFactory, ComponentFactoryResolver, Injectable, Type } from '@angular/core';
import { deepClone } from '@core/utils';
import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models';
export interface StateControllerData {
factory: ComponentFactory<IStateControllerComponent>;
state?: any;
}
@Injectable()
export class StatesControllerService {
statesControllers: {[stateControllerId: string]: StateControllerData} = {};
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
public registerStatesController(stateControllerId: string, stateControllerComponent: Type<IStateControllerComponent>): void {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(stateControllerComponent);
this.statesControllers[stateControllerId] = {
factory: componentFactory
};
}
public getStateControllers(): {[stateControllerId: string]: StateControllerData} {
return this.statesControllers;
}
public getStateController(stateControllerId: string): StateControllerData {
return this.statesControllers[stateControllerId];
}
public preserveStateControllerState(stateControllerId: string, state: any) {
this.statesControllers[stateControllerId].state = deepClone(state);
}
public withdrawStateControllerState(stateControllerId: string): any {
const state = this.statesControllers[stateControllerId].state;
this.statesControllers[stateControllerId].state = null;
return state;
}
public cleanupPreservedStates() {
for (const stateControllerId of Object.keys(this.statesControllers)) {
this.statesControllers[stateControllerId].state = null;
}
}
}

73
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -26,10 +26,12 @@ import { Observable } from 'rxjs';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators';
import { map, share } from 'rxjs/operators';
import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType } from '@app/shared/models/widget.models';
import { Widget, widgetType, WidgetType } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { WidgetsData } from '@home/models/dashboard-component.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
export interface WidgetEditorData {
widgetType: WidgetType;
@ -51,6 +53,69 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
}
}
@Injectable()
export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
constructor(private widgetsService: WidgetService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsData> {
const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle;
const bundleAlias = widgetsBundle.alias;
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
return this.widgetsService.getBundleWidgetTypes(bundleAlias,
isSystem).pipe(
map((types) => {
types = types.sort((a, b) => {
let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
if (result === 0) {
result = b.createdTime - a.createdTime;
}
return result;
});
const widgetTypes = new Array<Widget>(types.length);
let top = 0;
const lastTop = [0, 0, 0];
let col = 0;
let column = 0;
types.forEach((type) => {
const widgetTypeInfo = toWidgetInfo(type);
const sizeX = 8;
const sizeY = Math.floor(widgetTypeInfo.sizeY);
const widget: Widget = {
typeId: type.id,
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
sizeX,
sizeY,
row: top,
col,
config: JSON.parse(widgetTypeInfo.defaultConfig)
};
widget.config.title = widgetTypeInfo.widgetName;
widgetTypes.push(widget);
top += sizeY;
if (top > lastTop[column] + 10) {
lastTop[column] = top;
column++;
if (column > 2) {
column = 0;
}
top = lastTop[column];
col = column * 8;
}
});
return { widgets: widgetTypes };
}
));
}
}
@Injectable()
export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
@ -137,6 +202,9 @@ export const routes: Routes = [
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widget-library'
},
resolve: {
widgetsData: WidgetsTypesDataResolver
}
},
{
@ -183,6 +251,7 @@ export const routes: Routes = [
providers: [
WidgetsBundlesTableConfigResolver,
WidgetsBundleResolver,
WidgetsTypesDataResolver,
WidgetEditorDataResolver,
WidgetEditorAddDataResolver
]

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

@ -28,7 +28,8 @@
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
</section>
<tb-dashboard [aliasController]="aliasController"
[widgetsData]="widgetsData"
[widgets]="widgetsData.widgets"
[widgetLayouts]="widgetsData.widgetLayouts"
[isEdit]="false"
[isEditActionEnabled]="true"
[isExportActionEnabled]="true"

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

@ -55,7 +55,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
widgetsBundle: WidgetsBundle;
widgetTypes$: Observable<Array<Widget>>;
widgetsData: WidgetsData;
footerFabButtons: FooterFabButtons = {
fabTogglerName: 'widget.add-widget-type',
@ -84,8 +84,6 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
onRemoveWidget: this.removeWidgetType.bind(this)
};
widgetsData: Observable<WidgetsData>;
aliasController: IAliasController = new DummyAliasController();
constructor(protected store: Store<AppState>,
@ -98,70 +96,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
this.authUser = getCurrentAuthUser(store);
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
this.widgetsData = this.route.snapshot.data.widgetsData;
if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
this.loadWidgetTypes();
this.widgetsData = this.widgetTypes$.pipe(
map(widgets => ({ widgets })));
}
loadWidgetTypes() {
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.widgetTypes$ = this.widgetService.getBundleWidgetTypes(bundleAlias,
isSystem).pipe(
map((types) => {
types = types.sort((a, b) => {
let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
if (result === 0) {
result = b.createdTime - a.createdTime;
}
return result;
});
const widgetTypes = new Array<Widget>(types.length);
let top = 0;
const lastTop = [0, 0, 0];
let col = 0;
let column = 0;
types.forEach((type) => {
const widgetTypeInfo = toWidgetInfo(type);
const sizeX = 8;
const sizeY = Math.floor(widgetTypeInfo.sizeY);
const widget: Widget = {
typeId: type.id,
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
sizeX,
sizeY,
row: top,
col,
config: JSON.parse(widgetTypeInfo.defaultConfig)
};
widget.config.title = widgetTypeInfo.widgetName;
widgetTypes.push(widget);
top += sizeY;
if (top > lastTop[column] + 10) {
lastTop[column] = top;
column++;
if (column > 2) {
column = 0;
}
top = lastTop[column];
col = column * 8;
}
});
return widgetTypes;
}
),
share());
}
ngOnInit(): void {

2
ui-ngx/src/app/shared/components/footer-fab-buttons.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<section fxLayout="row" class="layout-wrap tb-footer-buttons">
<section fxLayout="row" class="layout-wrap tb-footer-fab-buttons" [ngClass]="{'relative-buttons': relative}">
<div class="fab-container">
<button [disabled]="isLoading$ | async"
mat-fab class="fab-toggler tb-btn-footer"

18
ui-ngx/src/app/shared/components/footer-fab-buttons.component.scss

@ -15,12 +15,14 @@
*/
:host {
section.tb-footer-buttons {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 30;
pointer-events: none;
section.tb-footer-fab-buttons {
&:not(.relative-buttons) {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 30;
pointer-events: none;
}
.fab-container {
display: flex;
@ -30,7 +32,6 @@
display: flex;
flex-direction: column-reverse;
align-items: center;
margin-bottom: 5px;
button {
margin-bottom: 17px;
@ -39,6 +40,9 @@
}
.tb-btn-footer {
&.fab-toggler {
margin-top: 0px;
}
position: relative !important;
display: inline-block !important;
animation: tbMoveFromBottomFade .3s ease both;

10
ui-ngx/src/app/shared/components/footer-fab-buttons.component.ts

@ -19,6 +19,7 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
export interface FooterFabButton {
name: string;
@ -43,6 +44,15 @@ export class FooterFabButtonsComponent extends PageComponent {
@Input()
footerFabButtons: FooterFabButtons;
private relativeValue: boolean;
get relative(): boolean {
return this.relativeValue;
}
@Input()
set relative(value: boolean) {
this.relativeValue = coerceBooleanProperty(value);
}
buttons: Array<FooterFabButton> = [];
fabTogglerState = 'inactive';

17
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -48,6 +48,10 @@ export interface GridSettings {
columns?: number;
margins?: [number, number];
backgroundSizeMode?: string;
backgroundImageUrl?: string;
autoFillHeight?: boolean;
mobileAutoFillHeight?: boolean;
mobileRowHeight?: number;
[key: string]: any;
// TODO:
}
@ -57,12 +61,17 @@ export interface DashboardLayout {
gridSettings: GridSettings;
}
export interface DashboardLayoutInfo {
widgets?: Array<Widget>;
widgetLayouts?: WidgetLayouts;
gridSettings?: GridSettings;
}
export declare type DashboardLayoutId = 'main' | 'right';
export interface DashboardStateLayouts {
main?: DashboardLayout;
right?: DashboardLayout;
}
export declare type DashboardStateLayouts = {[key in DashboardLayoutId]?: DashboardLayout};
export declare type DashboardLayoutsInfo = {[key in DashboardLayoutId]?: DashboardLayoutInfo};
export interface DashboardState {
name: string;

Loading…
Cancel
Save