Commit 2e7070a9039b5bbaaa5f598feea88b7a3df62b26

Authored by Igor Kulikov
1 parent 22d8ce71

Dashboard component implementation.

Showing 32 changed files with 1752 additions and 46 deletions
... ... @@ -1486,6 +1486,14 @@
1486 1486 "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
1487 1487 "dev": true
1488 1488 },
  1489 + "angular-gridster2": {
  1490 + "version": "8.1.0",
  1491 + "resolved": "https://registry.npmjs.org/angular-gridster2/-/angular-gridster2-8.1.0.tgz",
  1492 + "integrity": "sha512-O3VrHj5iq2HwqQDlh/8LfPy4aLIRK1Kxm5iCCNOVFmnBLa6F//D5habPsiHRMTEkQ9vD+lFnU7+tORhf/y9LAg==",
  1493 + "requires": {
  1494 + "tslib": "^1.9.0"
  1495 + }
  1496 + },
1489 1497 "ansi-colors": {
1490 1498 "version": "3.2.4",
1491 1499 "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
... ...
... ... @@ -32,6 +32,7 @@
32 32 "@ngx-translate/core": "^11.0.1",
33 33 "@ngx-translate/http-loader": "^4.0.0",
34 34 "ace-builds": "^1.4.5",
  35 + "angular-gridster2": "^8.1.0",
35 36 "compass-sass-mixins": "^0.12.7",
36 37 "core-js": "^3.1.4",
37 38 "deep-equal": "^1.0.1",
... ...
... ... @@ -21,6 +21,7 @@ import {HttpClient} from '@angular/common/http';
21 21 import {PageLink} from '@shared/models/page/page-link';
22 22 import {PageData} from '@shared/models/page/page-data';
23 23 import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
  24 +import { WidgetType } from '@shared/models/widget.models';
24 25
25 26 @Injectable({
26 27 providedIn: 'root'
... ... @@ -51,4 +52,10 @@ export class WidgetService {
51 52 return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
52 53 }
53 54
  55 + public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
  56 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<WidgetType>> {
  57 + return this.http.get<Array<WidgetType>>(`/api/widgetTypes?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
  58 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  59 + }
  60 +
54 61 }
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 17 import { Injectable } from '@angular/core';
18   -import { DAY, defaultTimeIntervals, SECOND } from '@shared/models/time/time.models';
  18 +import { DAY, defaultTimeIntervals, MINUTE, SECOND, Timewindow } from '@shared/models/time/time.models';
19 19 import {HttpClient} from '@angular/common/http';
20 20 import {Observable} from 'rxjs';
21 21 import {defaultHttpOptions} from '@core/http/http-utils';
... ... @@ -81,6 +81,9 @@ export class TimeService {
81 81 const intervals = this.getIntervals(min, max);
82 82 let minDelta = MAX_INTERVAL;
83 83 const boundedInterval = intervalMs || min;
  84 + if (!intervals.length) {
  85 + return boundedInterval;
  86 + }
84 87 let matchedInterval: TimeInterval = intervals[0];
85 88 intervals.forEach((interval) => {
86 89 const delta = Math.abs(interval.value - boundedInterval);
... ... @@ -110,6 +113,10 @@ export class TimeService {
110 113 return this.boundMaxInterval(max);
111 114 }
112 115
  116 + public defaultTimewindow(): Timewindow {
  117 + return Timewindow.defaultTimewindow(this);
  118 + }
  119 +
113 120 private toBound(value: number, min: number, max: number, defValue: number): number {
114 121 if (typeof value !== 'undefined') {
115 122 value = Math.max(value, min);
... ...
... ... @@ -52,6 +52,32 @@ export function isLocalUrl(url: string): boolean {
52 52 }
53 53 }
54 54
  55 +export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: number) {
  56 + let currentTime = 0;
  57 + const increment = 20;
  58 + const start = element.scrollTop;
  59 + const to = scrollTop;
  60 + const duration = delay ? delay : 0;
  61 + const remaining = to - start;
  62 + const animateScroll = () => {
  63 + currentTime += increment;
  64 + const val = easeInOut(currentTime, start, remaining, duration);
  65 + element.scrollTop = val;
  66 + if (currentTime < duration) {
  67 + setTimeout(animateScroll, increment);
  68 + }
  69 + };
  70 + animateScroll();
  71 +}
  72 +
  73 +export function isUndefined(value: any): boolean {
  74 + return typeof value === 'undefined';
  75 +}
  76 +
  77 +export function isDefined(value: any): boolean {
  78 + return typeof value !== 'undefined';
  79 +}
  80 +
55 81 const scrollRegex = /(auto|scroll)/;
56 82
57 83 function parentNodes(node: Node, nodes: Node[]): Node[] {
... ... @@ -95,3 +121,20 @@ function scrollParents(node: Node): Node[] {
95 121 }
96 122 return scrollParentNodes;
97 123 }
  124 +
  125 +function easeInOut(
  126 + currentTime: number,
  127 + startTime: number,
  128 + remainingTime: number,
  129 + duration: number) {
  130 + currentTime /= duration / 2;
  131 +
  132 + if (currentTime < 1) {
  133 + return (remainingTime / 2) * currentTime * currentTime + startTime;
  134 + }
  135 +
  136 + currentTime--;
  137 + return (
  138 + (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime
  139 + );
  140 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
  19 + [ngStyle]="options.dashboardStyle"
  20 + [fxShow]="(loading() | async) && !options.isEdit">
  21 + <mat-spinner color="warn" mode="indeterminate" diameter="100">
  22 + </mat-spinner>
  23 +</div>
  24 +<div id="gridster-parent"
  25 + fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
  26 + (contextmenu)="openDashboardContextMenu($event)">
  27 + <div [ngClass]="options.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
  28 + <gridster #gridster id="gridster-child" [options]="gridsterOpts">
  29 + <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async">
  30 + <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
  31 + fxLayout="column"
  32 + class="tb-widget"
  33 + [ngClass]="{
  34 + 'tb-highlighted': isHighlighted(widget),
  35 + 'tb-not-highlighted': isNotHighlighted(widget),
  36 + 'mat-elevation-z4': widget.dropShadow,
  37 + 'tb-has-timewindow': widget.hasTimewindow
  38 + }"
  39 + [ngStyle]="widget.style"
  40 + (mousedown)="widgetMouseDown($event, widget)"
  41 + (click)="widgetClicked($event, widget)"
  42 + (contextmenu)="openWidgetContextMenu($event, widget)">
  43 + <div fxLayout="row" fxLayoutAlign="space-between start">
  44 + <div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" [fxShow]="widget.showWidgetTitlePanel">
  45 + <div *ngIf="widget.hasWidgetTitleTemplate">
  46 + TODO:
  47 + </div>
  48 + <span [fxShow]="widget.showTitle" [ngStyle]="widget.titleStyle" class="mat-subheading-2 title">
  49 + <mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
  50 + {{widget.title}}
  51 + </span>
  52 + <tb-timewindow *ngIf="widget.hasTimewindow" aggregation="{{widget.hasAggregation}}" [ngModel]="widget.widget.config.timewindow"></tb-timewindow>
  53 + </div>
  54 + <div [fxShow]="widget.showWidgetActions"
  55 + class="tb-widget-actions"
  56 + [ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.hasWidgetTitleTemplate||widget.showTitle||widget.hasAggregation))}"
  57 + fxLayout="row"
  58 + fxLayoutAlign="start center"
  59 + (mousedown)="$event.stopPropagation()">
  60 + <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
  61 + [fxShow]="!options.isEdit"
  62 + (click)="action.onAction($event)"
  63 + matTooltip="{{ action.displayName }}"
  64 + matTooltipPosition="above">
  65 + <mat-icon>{{ action.icon }}</mat-icon>
  66 + </button>
  67 + <button mat-button mat-icon-button *ngFor="let action of widget.widgetActions"
  68 + [fxShow]="!options.isEdit && action.show"
  69 + (click)="action.onAction($event)"
  70 + matTooltip="{{ action.name | translate }}"
  71 + matTooltipPosition="above">
  72 + <mat-icon>{{ action.icon }}</mat-icon>
  73 + </button>
  74 + <button mat-button mat-icon-button
  75 + [fxShow]="!options.isEdit && widget.enableFullscreen"
  76 + (click)="widget.isFullscreen = !widget.isFullscreen"
  77 + matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  78 + matTooltipPosition="above">
  79 + <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  80 + </button>
  81 + <button mat-button mat-icon-button
  82 + [fxShow]="options.isEditActionEnabled && !widget.isFullscreen"
  83 + (click)="editWidget($event, widget)"
  84 + matTooltip="{{ 'widget.edit' | translate }}"
  85 + matTooltipPosition="above">
  86 + <mat-icon>edit</mat-icon>
  87 + </button>
  88 + <button mat-button mat-icon-button
  89 + [fxShow]="options.isExportActionEnabled && !widget.isFullscreen"
  90 + (click)="exportWidget($event, widget)"
  91 + matTooltip="{{ 'widget.export' | translate }}"
  92 + matTooltipPosition="above">
  93 + <mat-icon>file_download</mat-icon>
  94 + </button>
  95 + <button mat-button mat-icon-button
  96 + [fxShow]="options.isRemoveActionEnabled && !widget.isFullscreen"
  97 + (click)="removeWidget($event, widget)"
  98 + matTooltip="{{ 'widget.remove' | translate }}"
  99 + matTooltipPosition="above">
  100 + <mat-icon>close</mat-icon>
  101 + </button>
  102 + </div>
  103 + </div>
  104 + <div fxFlex fxLayout="column" class="tb-widget-content">
  105 +
  106 + </div>
  107 + </div>
  108 + </gridster-item>
  109 + </gridster>
  110 + </div>
  111 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import '../../../../../scss/constants';
  17 +
  18 +:host {
  19 +
  20 + .tb-progress-cover {
  21 + position: absolute;
  22 + top: 0;
  23 + right: 0;
  24 + bottom: 0;
  25 + left: 0;
  26 + z-index: 6;
  27 + opacity: 1;
  28 + }
  29 +
  30 + .tb-dashboard-content {
  31 + position: absolute;
  32 + top: 0;
  33 + right: 0;
  34 + bottom: 0;
  35 + left: 0;
  36 + background: none;
  37 + outline: none;
  38 +
  39 + gridster-item {
  40 + transition: none;
  41 + overflow: visible;
  42 + }
  43 + }
  44 +
  45 + #gridster-child {
  46 + background: none;
  47 + }
  48 +}
  49 +
  50 +div.tb-widget {
  51 + position: relative;
  52 + height: 100%;
  53 + margin: 0;
  54 + overflow: hidden;
  55 + outline: none;
  56 +
  57 + transition: all .2s ease-in-out;
  58 +
  59 + .tb-widget-title {
  60 + max-height: 65px;
  61 + padding-top: 5px;
  62 + padding-left: 5px;
  63 + overflow: hidden;
  64 +
  65 + tb-timewindow {
  66 + font-size: 14px;
  67 + opacity: .85;
  68 + }
  69 +
  70 + .title {
  71 + width: 100%;
  72 + overflow: hidden;
  73 + text-overflow: ellipsis;
  74 + line-height: 24px;
  75 + letter-spacing: .01em;
  76 + margin: 0;
  77 + }
  78 + }
  79 +
  80 + .tb-widget-actions {
  81 + z-index: 19;
  82 + margin: 5px 0 0;
  83 +
  84 + &-absolute {
  85 + position: absolute;
  86 + top: 3px;
  87 + right: 8px;
  88 + }
  89 +
  90 + button.mat-icon-button {
  91 + width: 32px;
  92 + min-width: 32px;
  93 + height: 32px;
  94 + min-height: 32px;
  95 + padding: 0 !important;
  96 + margin: 0 !important;
  97 + line-height: 20px;
  98 +
  99 + mat-icon {
  100 + width: 20px;
  101 + min-width: 20px;
  102 + height: 20px;
  103 + min-height: 20px;
  104 + font-size: 20px;
  105 + line-height: 20px;
  106 + }
  107 + }
  108 + }
  109 +
  110 + .tb-widget-content {
  111 + tb-widget {
  112 + position: relative;
  113 + width: 100%;
  114 + }
  115 + }
  116 +
  117 + &.tb-highlighted {
  118 + border: 1px solid #039be5;
  119 + box-shadow: 0 0 20px #039be5;
  120 + }
  121 +
  122 + &.tb-not-highlighted {
  123 + opacity: .5;
  124 + }
  125 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit, Input, ViewChild, AfterViewInit, ViewChildren, QueryList, ElementRef } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { AuthUser } from '@shared/models/user.model';
  22 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  23 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  24 +import { Timewindow } from '@shared/models/time/time.models';
  25 +import { TimeService } from '@core/services/time.service';
  26 +import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
  27 +import { GridsterResizable } from 'angular-gridster2/lib/gridsterResizable.service';
  28 +import { IDashboardComponent, DashboardConfig, DashboardWidget } from '../../models/dashboard-component.models';
  29 +import { MatSort } from '@angular/material/sort';
  30 +import { Observable, ReplaySubject, merge } from 'rxjs';
  31 +import { map, share, tap } from 'rxjs/operators';
  32 +import { WidgetLayout } from '@shared/models/dashboard.models';
  33 +import { DialogService } from '@core/services/dialog.service';
  34 +import { Widget } from '@app/shared/models/widget.models';
  35 +import { MatTab } from '@angular/material/tabs';
  36 +import { animatedScroll, isDefined } from '@app/core/utils';
  37 +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
  38 +import { MediaBreakpoints } from '@shared/models/constants';
  39 +
  40 +@Component({
  41 + selector: 'tb-dashboard',
  42 + templateUrl: './dashboard.component.html',
  43 + styleUrls: ['./dashboard.component.scss']
  44 +})
  45 +export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit {
  46 +
  47 + authUser: AuthUser;
  48 +
  49 + @Input()
  50 + options: DashboardConfig;
  51 +
  52 + gridsterOpts: GridsterConfig;
  53 +
  54 + dashboardLoading = true;
  55 +
  56 + highlightedMode = false;
  57 + highlightedWidget: DashboardWidget = null;
  58 + selectedWidget: DashboardWidget = null;
  59 +
  60 + isWidgetExpanded = false;
  61 + isMobileSize = false;
  62 +
  63 + @ViewChild('gridster', {static: true}) gridster: GridsterComponent;
  64 +
  65 + @ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>;
  66 +
  67 + widgets$: Observable<Array<DashboardWidget>>;
  68 +
  69 + widgets: Array<DashboardWidget>;
  70 +
  71 + constructor(protected store: Store<AppState>,
  72 + private timeService: TimeService,
  73 + private dialogService: DialogService,
  74 + private breakpointObserver: BreakpointObserver) {
  75 + super(store);
  76 + this.authUser = getCurrentAuthUser(store);
  77 + }
  78 +
  79 + ngOnInit(): void {
  80 + if (!this.options.dashboardTimewindow) {
  81 + this.options.dashboardTimewindow = this.timeService.defaultTimewindow();
  82 + }
  83 + this.gridsterOpts = {
  84 + gridType: 'scrollVertical',
  85 + keepFixedHeightInMobile: true,
  86 + pushItems: false,
  87 + swap: false,
  88 + maxRows: 100,
  89 + minCols: this.options.columns ? this.options.columns : 24,
  90 + outerMargin: true,
  91 + outerMarginLeft: this.options.margins ? this.options.margins[0] : 10,
  92 + outerMarginRight: this.options.margins ? this.options.margins[0] : 10,
  93 + outerMarginTop: this.options.margins ? this.options.margins[1] : 10,
  94 + outerMarginBottom: this.options.margins ? this.options.margins[1] : 10,
  95 + minItemCols: 1,
  96 + minItemRows: 1,
  97 + defaultItemCols: 8,
  98 + defaultItemRows: 6,
  99 + resizable: {enabled: this.options.isEdit},
  100 + draggable: {enabled: this.options.isEdit}
  101 + };
  102 +
  103 + this.updateGridsterOpts();
  104 +
  105 + this.loadDashboard();
  106 +
  107 + merge(this.breakpointObserver
  108 + .observe(MediaBreakpoints['gt-sm']), this.options.layoutChange$).subscribe(
  109 + () => {
  110 + this.updateGridsterOpts();
  111 + this.sortWidgets(this.widgets);
  112 + }
  113 + );
  114 + }
  115 +
  116 + loadDashboard() {
  117 + this.widgets$ = this.options.widgetsData.pipe(
  118 + map(widgetsData => {
  119 + const dashboardWidgets = new Array<DashboardWidget>();
  120 + let maxRows = this.gridsterOpts.maxRows;
  121 + widgetsData.widgets.forEach(
  122 + (widget) => {
  123 + let widgetLayout: WidgetLayout;
  124 + if (widgetsData.widgetLayouts && widget.id) {
  125 + widgetLayout = widgetsData.widgetLayouts[widget.id];
  126 + }
  127 + const dashboardWidget = new DashboardWidget(this, widget, widgetLayout);
  128 + const bottom = dashboardWidget.y + dashboardWidget.rows;
  129 + maxRows = Math.max(maxRows, bottom);
  130 + dashboardWidgets.push(dashboardWidget);
  131 + }
  132 + );
  133 + this.sortWidgets(dashboardWidgets);
  134 + this.gridsterOpts.maxRows = maxRows;
  135 + return dashboardWidgets;
  136 + }),
  137 + tap((widgets) => {
  138 + this.widgets = widgets;
  139 + this.dashboardLoading = false;
  140 + })
  141 + );
  142 + }
  143 +
  144 + reload() {
  145 + this.loadDashboard();
  146 + }
  147 +
  148 + sortWidgets(widgets?: Array<DashboardWidget>) {
  149 + if (widgets) {
  150 + widgets.sort((widget1, widget2) => {
  151 + const row1 = widget1.widgetOrder;
  152 + const row2 = widget2.widgetOrder;
  153 + let res = row1 - row2;
  154 + if (res === 0) {
  155 + res = widget1.x - widget2.x;
  156 + }
  157 + return res;
  158 + });
  159 + }
  160 + }
  161 +
  162 + ngAfterViewInit(): void {
  163 + }
  164 +
  165 + isAutofillHeight(): boolean {
  166 + if (this.isMobileSize) {
  167 + return isDefined(this.options.mobileAutofillHeight) ? this.options.mobileAutofillHeight : false;
  168 + } else {
  169 + return isDefined(this.options.autofillHeight) ? this.options.autofillHeight : false;
  170 + }
  171 + }
  172 +
  173 + loading(): Observable<boolean> {
  174 + return this.isLoading$.pipe(
  175 + map(loading => (!this.options.ignoreLoading && loading) || this.dashboardLoading),
  176 + share()
  177 + );
  178 + }
  179 +
  180 + openDashboardContextMenu($event: Event) {
  181 + // TODO:
  182 + // this.dialogService.todo();
  183 + }
  184 +
  185 + openWidgetContextMenu($event: Event, widget: DashboardWidget) {
  186 + // TODO:
  187 + // this.dialogService.todo();
  188 + }
  189 +
  190 + onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) {
  191 + this.isWidgetExpanded = expanded;
  192 + }
  193 +
  194 + widgetMouseDown($event: Event, widget: DashboardWidget) {
  195 + if (this.options.onWidgetMouseDown) {
  196 + this.options.onWidgetMouseDown($event, widget.widget);
  197 + }
  198 + }
  199 +
  200 + widgetClicked($event: Event, widget: DashboardWidget) {
  201 + if (this.options.onWidgetClicked) {
  202 + this.options.onWidgetClicked($event, widget.widget);
  203 + }
  204 + }
  205 +
  206 + editWidget($event: Event, widget: DashboardWidget) {
  207 + if ($event) {
  208 + $event.stopPropagation();
  209 + }
  210 + if (this.options.isEditActionEnabled && this.options.onEditWidget) {
  211 + this.options.onEditWidget($event, widget.widget);
  212 + }
  213 + }
  214 +
  215 + exportWidget($event: Event, widget: DashboardWidget) {
  216 + if ($event) {
  217 + $event.stopPropagation();
  218 + }
  219 + if (this.options.isExportActionEnabled && this.options.onExportWidget) {
  220 + this.options.onExportWidget($event, widget.widget);
  221 + }
  222 + }
  223 +
  224 + removeWidget($event: Event, widget: DashboardWidget) {
  225 + if ($event) {
  226 + $event.stopPropagation();
  227 + }
  228 + if (this.options.isRemoveActionEnabled && this.options.onRemoveWidget) {
  229 + this.options.onRemoveWidget($event, widget.widget);
  230 + }
  231 + }
  232 +
  233 + highlightWidget(widget: DashboardWidget, delay?: number) {
  234 + if (!this.highlightedMode || this.highlightedWidget !== widget) {
  235 + this.highlightedMode = true;
  236 + this.highlightedWidget = widget;
  237 + this.scrollToWidget(widget, delay);
  238 + }
  239 + }
  240 +
  241 + selectWidget(widget: DashboardWidget, delay?: number) {
  242 + if (this.selectedWidget !== widget) {
  243 + this.selectedWidget = widget;
  244 + this.scrollToWidget(widget, delay);
  245 + }
  246 + }
  247 +
  248 + resetHighlight() {
  249 + this.highlightedMode = false;
  250 + this.highlightedWidget = null;
  251 + this.selectedWidget = null;
  252 + }
  253 +
  254 + isHighlighted(widget: DashboardWidget) {
  255 + return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget);
  256 + }
  257 +
  258 + isNotHighlighted(widget: DashboardWidget) {
  259 + return this.highlightedMode && this.highlightedWidget !== widget;
  260 + }
  261 +
  262 + scrollToWidget(widget: DashboardWidget, delay?: number) {
  263 + if (this.gridsterItems) {
  264 + const gridsterItem = this.gridsterItems.find((item => item.item === widget));
  265 + const offset = (this.gridster.curHeight - gridsterItem.height) / 2;
  266 + let scrollTop = gridsterItem.top;
  267 + if (offset > 0) {
  268 + scrollTop -= offset;
  269 + }
  270 + const parentElement = this.gridster.el as HTMLElement;
  271 + animatedScroll(parentElement, scrollTop, delay);
  272 + }
  273 + }
  274 +
  275 + private updateGridsterOpts() {
  276 + this.isMobileSize = this.checkIsMobileSize();
  277 + const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
  278 + this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
  279 + const rowSize = this.detectRowSize(this.isMobileSize);
  280 + if (this.gridsterOpts.fixedRowHeight !== rowSize) {
  281 + this.gridsterOpts.fixedRowHeight = rowSize;
  282 + }
  283 + if (this.isAutofillHeight()) {
  284 + this.gridsterOpts.gridType = 'fit';
  285 + } else {
  286 + this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical';
  287 + }
  288 + if (this.gridster && this.gridster.options) {
  289 + this.gridster.optionsChanged();
  290 + }
  291 + }
  292 +
  293 + private detectRowSize(isMobile: boolean): number | null {
  294 + let rowHeight = null;
  295 + if (!this.isAutofillHeight()) {
  296 + if (isMobile) {
  297 + rowHeight = isDefined(this.options.mobileRowHeight) ? this.options.mobileRowHeight : 70;
  298 + }
  299 + }
  300 + return rowHeight;
  301 + }
  302 +
  303 + private checkIsMobileSize(): boolean {
  304 + const isMobileDisabled = this.options.isMobileDisabled === true;
  305 + let isMobileSize = this.options.isMobile === true && !isMobileDisabled;
  306 + if (!isMobileSize && !isMobileDisabled) {
  307 + isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
  308 + }
  309 + return isMobileSize;
  310 + }
  311 +
  312 +}
... ...
... ... @@ -34,6 +34,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
34 34 import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component';
35 35 import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
36 36 import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
  37 +import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
37 38
38 39 @NgModule({
39 40 entryComponents: [
... ... @@ -64,7 +65,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val
64 65 AlarmDetailsDialogComponent,
65 66 AttributeTableComponent,
66 67 AddAttributeDialogComponent,
67   - EditAttributeValuePanelComponent
  68 + EditAttributeValuePanelComponent,
  69 + DashboardComponent
68 70 ],
69 71 imports: [
70 72 CommonModule,
... ... @@ -81,7 +83,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val
81 83 RelationTableComponent,
82 84 AlarmTableComponent,
83 85 AlarmDetailsDialogComponent,
84   - AttributeTableComponent
  86 + AttributeTableComponent,
  87 + DashboardComponent
85 88 ]
86 89 })
87 90 export class HomeComponentsModule { }
... ...
... ... @@ -54,9 +54,6 @@ export class HomeComponent extends PageComponent implements OnInit {
54 54 authUser$: Observable<any>;
55 55 userDetails$: Observable<User>;
56 56 userDetailsString: Observable<string>;
57   - testUser1$: Observable<User>;
58   - testUser2$: Observable<User>;
59   - testUser3$: Observable<User>;
60 57
61 58 constructor(protected store: Store<AppState>,
62 59 private authService: AuthService,
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { GridsterConfig, GridsterItem, GridsterComponent } from 'angular-gridster2';
  18 +import { Widget, widgetType } from '@app/shared/models/widget.models';
  19 +import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
  20 +import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
  21 +import { Timewindow } from '@shared/models/time/time.models';
  22 +import { Observable } from 'rxjs';
  23 +import { isDefined, isUndefined } from '@app/core/utils';
  24 +import { EventEmitter } from '@angular/core';
  25 +
  26 +export interface IAliasController {
  27 + [key: string]: any | null;
  28 + // TODO:
  29 +}
  30 +
  31 +export interface WidgetsData {
  32 + widgets: Array<Widget>;
  33 + widgetLayouts?: WidgetLayouts;
  34 +}
  35 +
  36 +export class DashboardConfig {
  37 + widgetsData?: Observable<WidgetsData>;
  38 + isEdit: boolean;
  39 + isEditActionEnabled: boolean;
  40 + isExportActionEnabled: boolean;
  41 + isRemoveActionEnabled: boolean;
  42 + onEditWidget?: ($event: Event, widget: Widget) => void;
  43 + onExportWidget?: ($event: Event, widget: Widget) => void;
  44 + onRemoveWidget?: ($event: Event, widget: Widget) => void;
  45 + onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
  46 + onWidgetClicked?: ($event: Event, widget: Widget) => void;
  47 + aliasController?: IAliasController;
  48 + autofillHeight?: boolean;
  49 + mobileAutofillHeight?: boolean;
  50 + dashboardStyle?: {[klass: string]: any} | null;
  51 + columns?: number;
  52 + margins?: [number, number];
  53 + dashboardTimewindow?: Timewindow;
  54 + ignoreLoading?: boolean;
  55 + dashboardClass?: string;
  56 + mobileRowHeight?: number;
  57 +
  58 + private isMobileValue: boolean;
  59 + private isMobileDisabledValue: boolean;
  60 +
  61 + private layoutChange = new EventEmitter();
  62 + layoutChange$ = this.layoutChange.asObservable();
  63 + layoutChangeTimeout = null;
  64 +
  65 + set isMobile(isMobile: boolean) {
  66 + if (this.isMobileValue !== isMobile) {
  67 + const changed = isDefined(this.isMobileValue);
  68 + this.isMobileValue = isMobile;
  69 + if (changed) {
  70 + this.notifyLayoutChanged();
  71 + }
  72 + }
  73 + }
  74 + get isMobile(): boolean {
  75 + return this.isMobileValue;
  76 + }
  77 +
  78 + set isMobileDisabled(isMobileDisabled: boolean) {
  79 + if (this.isMobileDisabledValue !== isMobileDisabled) {
  80 + const changed = isDefined(this.isMobileDisabledValue);
  81 + this.isMobileDisabledValue = isMobileDisabled;
  82 + if (changed) {
  83 + this.notifyLayoutChanged();
  84 + }
  85 + }
  86 + }
  87 + get isMobileDisabled(): boolean {
  88 + return this.isMobileDisabledValue;
  89 + }
  90 +
  91 + private notifyLayoutChanged() {
  92 + if (this.layoutChangeTimeout) {
  93 + clearTimeout(this.layoutChangeTimeout);
  94 + }
  95 + this.layoutChangeTimeout = setTimeout(() => {
  96 + this.doNotifyLayoutChanged();
  97 + }, 0);
  98 + }
  99 +
  100 + private doNotifyLayoutChanged() {
  101 + this.layoutChange.emit();
  102 + this.layoutChangeTimeout = null;
  103 + }
  104 +}
  105 +
  106 +export interface IDashboardComponent {
  107 + options: DashboardConfig;
  108 + gridsterOpts: GridsterConfig;
  109 + gridster: GridsterComponent;
  110 + isMobileSize: boolean;
  111 +}
  112 +
  113 +export class DashboardWidget implements GridsterItem {
  114 +
  115 + isFullscreen = false;
  116 +
  117 + color: string;
  118 + backgroundColor: string;
  119 + padding: string;
  120 + margin: string;
  121 +
  122 + title: string;
  123 + showTitle: boolean;
  124 + titleStyle: {[klass: string]: any};
  125 +
  126 + titleIcon: string;
  127 + showTitleIcon: boolean;
  128 + titleIconStyle: {[klass: string]: any};
  129 +
  130 + dropShadow: boolean;
  131 + enableFullscreen: boolean;
  132 +
  133 + hasTimewindow: boolean;
  134 +
  135 + hasAggregation: boolean;
  136 +
  137 + style: {[klass: string]: any};
  138 +
  139 + hasWidgetTitleTemplate: boolean;
  140 + widgetTitleTemplate: string;
  141 +
  142 + showWidgetTitlePanel: boolean;
  143 + showWidgetActions: boolean;
  144 +
  145 + customHeaderActions: Array<WidgetHeaderAction>;
  146 + widgetActions: Array<WidgetAction>;
  147 +
  148 + widgetContext: WidgetContext = {};
  149 +
  150 + constructor(
  151 + private dashboard: IDashboardComponent,
  152 + public widget: Widget,
  153 + private widgetLayout?: WidgetLayout) {
  154 + this.updateWidgetParams();
  155 + }
  156 +
  157 + updateWidgetParams() {
  158 + this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
  159 + this.backgroundColor = this.widget.config.backgroundColor || '#fff';
  160 + this.padding = this.widget.config.padding || '8px';
  161 + this.margin = this.widget.config.margin || '0px';
  162 +
  163 + this.title = isDefined(this.widgetContext.widgetTitle)
  164 + && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title;
  165 + this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true;
  166 + this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {};
  167 +
  168 + this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : '';
  169 + this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false;
  170 + this.titleIconStyle = {};
  171 + if (this.widget.config.iconColor) {
  172 + this.titleIconStyle.color = this.widget.config.iconColor;
  173 + }
  174 + if (this.widget.config.iconSize) {
  175 + this.titleIconStyle.fontSize = this.widget.config.iconSize;
  176 + }
  177 +
  178 + this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
  179 + this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
  180 +
  181 + this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ?
  182 + (isDefined(this.widget.config.useDashboardTimewindow) ?
  183 + (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
  184 + || this.widget.config.displayTimewindow)) : false)
  185 + : false;
  186 +
  187 + this.hasAggregation = this.widget.type === widgetType.timeseries;
  188 +
  189 + this.style = {cursor: 'pointer',
  190 + color: this.color,
  191 + backgroundColor: this.backgroundColor,
  192 + padding: this.padding,
  193 + margin: this.margin};
  194 + if (this.widget.config.widgetStyle) {
  195 + this.style = {...this.widget.config.widgetStyle, ...this.style};
  196 + }
  197 +
  198 + this.hasWidgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? true : false;
  199 + this.widgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? this.widgetContext.widgetTitleTemplate : '';
  200 +
  201 + this.showWidgetTitlePanel = this.widgetContext.hideTitlePanel ? false :
  202 + this.hasWidgetTitleTemplate || this.showTitle || this.hasTimewindow;
  203 +
  204 + this.showWidgetActions = this.widgetContext.hideTitlePanel ? false : true;
  205 +
  206 + this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : [];
  207 + this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
  208 + }
  209 +
  210 + get x(): number {
  211 + if (this.widgetLayout) {
  212 + return this.widgetLayout.col;
  213 + } else {
  214 + return this.widget.col;
  215 + }
  216 + }
  217 +
  218 + set x(x: number) {
  219 + if (!this.dashboard.isMobileSize) {
  220 + if (this.widgetLayout) {
  221 + this.widgetLayout.col = x;
  222 + } else {
  223 + this.widget.col = x;
  224 + }
  225 + }
  226 + }
  227 +
  228 + get y(): number {
  229 + if (this.widgetLayout) {
  230 + return this.widgetLayout.row;
  231 + } else {
  232 + return this.widget.row;
  233 + }
  234 + }
  235 +
  236 + set y(y: number) {
  237 + if (!this.dashboard.isMobileSize) {
  238 + if (this.widgetLayout) {
  239 + this.widgetLayout.row = y;
  240 + } else {
  241 + this.widget.row = y;
  242 + }
  243 + }
  244 + }
  245 +
  246 + get cols(): number {
  247 + if (this.widgetLayout) {
  248 + return this.widgetLayout.sizeX;
  249 + } else {
  250 + return this.widget.sizeX;
  251 + }
  252 + }
  253 +
  254 + set cols(cols: number) {
  255 + if (!this.dashboard.isMobileSize) {
  256 + if (this.widgetLayout) {
  257 + this.widgetLayout.sizeX = cols;
  258 + } else {
  259 + this.widget.sizeX = cols;
  260 + }
  261 + }
  262 + }
  263 +
  264 + get rows(): number {
  265 + if (this.dashboard.isMobileSize && !this.dashboard.options.mobileAutofillHeight) {
  266 + let mobileHeight;
  267 + if (this.widgetLayout) {
  268 + mobileHeight = this.widgetLayout.mobileHeight;
  269 + }
  270 + if (!mobileHeight && this.widget.config.mobileHeight) {
  271 + mobileHeight = this.widget.config.mobileHeight;
  272 + }
  273 + if (mobileHeight) {
  274 + return mobileHeight;
  275 + } else {
  276 + return this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
  277 + }
  278 + } else {
  279 + if (this.widgetLayout) {
  280 + return this.widgetLayout.sizeY;
  281 + } else {
  282 + return this.widget.sizeY;
  283 + }
  284 + }
  285 + }
  286 +
  287 + set rows(rows: number) {
  288 + if (!this.dashboard.isMobileSize && !this.dashboard.options.autofillHeight) {
  289 + if (this.widgetLayout) {
  290 + this.widgetLayout.sizeY = rows;
  291 + } else {
  292 + this.widget.sizeY = rows;
  293 + }
  294 + }
  295 + }
  296 +
  297 + get widgetOrder(): number {
  298 + let order;
  299 + if (this.widgetLayout && isDefined(this.widgetLayout.mobileOrder) && this.widgetLayout.mobileOrder >= 0) {
  300 + order = this.widgetLayout.mobileOrder;
  301 + } else if (isDefined(this.widget.config.mobileOrder) && this.widget.config.mobileOrder >= 0) {
  302 + order = this.widget.config.mobileOrder;
  303 + } else if (this.widgetLayout) {
  304 + order = this.widgetLayout.row;
  305 + } else {
  306 + order = this.widget.row;
  307 + }
  308 + return order;
  309 + }
  310 +}
... ...
ui-ngx/src/app/modules/home/models/widget-component.models.ts renamed from ui-ngx/src/app/shared/models/widget-type.models.ts
... ... @@ -14,20 +14,24 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {BaseData} from '@shared/models/base-data';
18   -import {TenantId} from '@shared/models/id/tenant-id';
19   -import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id';
20   -import {WidgetTypeId} from '@shared/models/id/widget-type-id';
  17 +export interface IWidgetAction {
  18 + icon: string;
  19 + onAction: ($event: Event) => void;
  20 +}
21 21
22   -export interface WidgetTypeDescriptor {
23   - todo: Array<any>;
24   - // TODO:
  22 +export interface WidgetHeaderAction extends IWidgetAction {
  23 + displayName: string;
25 24 }
26 25
27   -export interface WidgetType extends BaseData<WidgetTypeId> {
28   - tenantId: TenantId;
29   - bundleAlias: string;
30   - alias: string;
  26 +export interface WidgetAction extends IWidgetAction {
31 27 name: string;
32   - descriptor: WidgetTypeDescriptor;
  28 + show: boolean;
  29 +}
  30 +
  31 +export interface WidgetContext {
  32 + widgetTitleTemplate?: string;
  33 + hideTitlePanel?: boolean;
  34 + widgetTitle?: string;
  35 + customHeaderActions?: Array<WidgetHeaderAction>;
  36 + widgetActions?: Array<WidgetAction>;
33 37 }
... ...
... ... @@ -14,13 +14,35 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {NgModule} from '@angular/core';
18   -import {RouterModule, Routes} from '@angular/router';
  17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 20 import {EntitiesTableComponent} from '../../components/entity/entities-table.component';
21 21 import {Authority} from '@shared/models/authority.enum';
22 22 import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
23 23 import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
  24 +import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
  25 +import { BreadCrumbConfig } from '@shared/components/breadcrumb';
  26 +import { User } from '@shared/models/user.model';
  27 +import { Store } from '@ngrx/store';
  28 +import { AppState } from '@core/core.state';
  29 +import { UserService } from '@core/http/user.service';
  30 +import { Observable } from 'rxjs';
  31 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  32 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  33 +import { WidgetService } from '@core/http/widget.service';
  34 +
  35 +@Injectable()
  36 +export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
  37 +
  38 + constructor(private widgetsService: WidgetService) {
  39 + }
  40 +
  41 + resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
  42 + const widgetsBundleId = route.params.widgetsBundleId;
  43 + return this.widgetsService.getWidgetsBundle(widgetsBundleId);
  44 + }
  45 +}
24 46
25 47 const routes: Routes = [
26 48 {
... ... @@ -42,6 +64,21 @@ const routes: Routes = [
42 64 resolve: {
43 65 entitiesTableConfig: WidgetsBundlesTableConfigResolver
44 66 }
  67 + },
  68 + {
  69 + path: ':widgetsBundleId/widgetTypes',
  70 + component: WidgetLibraryComponent,
  71 + data: {
  72 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  73 + title: 'widget.widget-library',
  74 + breadcrumb: {
  75 + labelFunction: ((route, translate) => route.data.widgetsBundle.title),
  76 + icon: 'now_widgets'
  77 + } as BreadCrumbConfig
  78 + },
  79 + resolve: {
  80 + widgetsBundle: WidgetsBundleResolver
  81 + }
45 82 }
46 83 ]
47 84 }
... ... @@ -51,7 +88,8 @@ const routes: Routes = [
51 88 imports: [RouterModule.forChild(routes)],
52 89 exports: [RouterModule],
53 90 providers: [
54   - WidgetsBundlesTableConfigResolver
  91 + WidgetsBundlesTableConfigResolver,
  92 + WidgetsBundleResolver
55 93 ]
56 94 })
57 95 export class WidgetLibraryRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center"
  19 + style="text-transform: uppercase; display: flex; z-index: 1;"
  20 + class="tb-absolute-fill">
  21 + <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
  22 + <mat-icon class="tb-mat-96">add</mat-icon>
  23 + {{ 'widget.add-widget-type' | translate }}
  24 + </button>
  25 + <span translate *ngIf="isReadOnly"
  26 + fxLayoutAlign="center center"
  27 + style="text-transform: uppercase; display: flex;"
  28 + class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
  29 +</section>
  30 +<tb-dashboard [options]="dashboardOptions"></tb-dashboard>
  31 +<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
  32 +</tb-footer-fab-buttons>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + button.tb-add-new-widget {
  19 + padding-right: 12px;
  20 + font-size: 24px;
  21 + border-style: dashed;
  22 + border-width: 2px;
  23 + }
  24 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { AuthUser } from '@shared/models/user.model';
  22 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  23 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  24 +import { ActivatedRoute } from '@angular/router';
  25 +import { Authority } from '@shared/models/authority.enum';
  26 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  27 +import { Observable, of } from 'rxjs';
  28 +import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models';
  29 +import { WidgetService } from '@core/http/widget.service';
  30 +import { map, share } from 'rxjs/operators';
  31 +import { DialogService } from '@core/services/dialog.service';
  32 +import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
  33 +import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
  34 +import { DashboardConfig } from '@home/models/dashboard-component.models';
  35 +
  36 +@Component({
  37 + selector: 'tb-widget-library',
  38 + templateUrl: './widget-library.component.html',
  39 + styleUrls: ['./widget-library.component.scss']
  40 +})
  41 +export class WidgetLibraryComponent extends PageComponent implements OnInit {
  42 +
  43 + authUser: AuthUser;
  44 +
  45 + isReadOnly: boolean;
  46 +
  47 + widgetsBundle: WidgetsBundle;
  48 +
  49 + widgetTypes$: Observable<Array<Widget>>;
  50 +
  51 + footerFabButtons: FooterFabButtons = {
  52 + fabTogglerName: 'widget.add-widget-type',
  53 + fabTogglerIcon: 'add',
  54 + buttons: [
  55 + {
  56 + name: 'widget-type.create-new-widget-type',
  57 + icon: 'insert_drive_file',
  58 + onAction: ($event) => {
  59 + this.addWidgetType($event);
  60 + }
  61 + },
  62 + {
  63 + name: 'widget-type.import',
  64 + icon: 'file_upload',
  65 + onAction: ($event) => {
  66 + this.importWidgetType($event);
  67 + }
  68 + }
  69 + ]
  70 + };
  71 +
  72 + dashboardOptions: DashboardConfig = new DashboardConfig();
  73 +
  74 + constructor(protected store: Store<AppState>,
  75 + private route: ActivatedRoute,
  76 + private widgetService: WidgetService,
  77 + private dialogService: DialogService) {
  78 + super(store);
  79 + this.dashboardOptions.isEdit = false;
  80 + this.dashboardOptions.isEditActionEnabled = true;
  81 + this.dashboardOptions.isExportActionEnabled = true;
  82 + this.dashboardOptions.onEditWidget = ($event, widget) => { this.openWidgetType($event, widget); };
  83 + this.dashboardOptions.onExportWidget = ($event, widget) => { this.exportWidgetType($event, widget); };
  84 + this.dashboardOptions.onRemoveWidget = ($event, widget) => { this.removeWidgetType($event, widget); };
  85 +
  86 + this.authUser = getCurrentAuthUser(store);
  87 + this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
  88 + if (this.authUser.authority === Authority.TENANT_ADMIN) {
  89 + this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
  90 + } else {
  91 + this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
  92 + }
  93 + this.dashboardOptions.isRemoveActionEnabled = !this.isReadOnly;
  94 + this.loadWidgetTypes();
  95 + this.dashboardOptions.widgetsData = this.widgetTypes$.pipe(
  96 + map(widgets => ({ widgets })));
  97 + }
  98 +
  99 + loadWidgetTypes() {
  100 + const bundleAlias = this.widgetsBundle.alias;
  101 + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
  102 + this.widgetTypes$ = this.widgetService.getBundleWidgetTypes(bundleAlias,
  103 + isSystem).pipe(
  104 + map((types) => {
  105 + types = types.sort((a, b) => {
  106 + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
  107 + if (result === 0) {
  108 + result = b.createdTime - a.createdTime;
  109 + }
  110 + return result;
  111 + });
  112 + const widgetTypes = new Array<Widget>(types.length);
  113 + let top = 0;
  114 + const lastTop = [0, 0, 0];
  115 + let col = 0;
  116 + let column = 0;
  117 + types.forEach((type) => {
  118 + const widgetTypeInfo = toWidgetInfo(type);
  119 + const sizeX = 8;
  120 + const sizeY = Math.floor(widgetTypeInfo.sizeY);
  121 + const widget: Widget = {
  122 + typeId: type.id,
  123 + isSystemType: isSystem,
  124 + bundleAlias,
  125 + typeAlias: widgetTypeInfo.alias,
  126 + type: widgetTypeInfo.type,
  127 + title: widgetTypeInfo.widgetName,
  128 + sizeX,
  129 + sizeY,
  130 + row: top,
  131 + col,
  132 + config: JSON.parse(widgetTypeInfo.defaultConfig)
  133 + };
  134 +
  135 + widget.config.title = widgetTypeInfo.widgetName;
  136 +
  137 + widgetTypes.push(widget);
  138 + top += sizeY;
  139 + if (top > lastTop[column] + 10) {
  140 + lastTop[column] = top;
  141 + column++;
  142 + if (column > 2) {
  143 + column = 0;
  144 + }
  145 + top = lastTop[column];
  146 + col = column * 8;
  147 + }
  148 + });
  149 + return widgetTypes;
  150 + }
  151 + ),
  152 + share());
  153 + }
  154 +
  155 + ngOnInit(): void {
  156 + }
  157 +
  158 + addWidgetType($event: Event): void {
  159 + this.openWidgetType($event);
  160 + }
  161 +
  162 + importWidgetType($event: Event): void {
  163 + if (event) {
  164 + event.stopPropagation();
  165 + }
  166 + this.dialogService.todo();
  167 + }
  168 +
  169 + openWidgetType($event: Event, widget?: Widget): void {
  170 + if (event) {
  171 + event.stopPropagation();
  172 + }
  173 + if (widget) {
  174 + this.dialogService.todo();
  175 + } else {
  176 + this.dialogService.todo();
  177 + }
  178 + }
  179 +
  180 + exportWidgetType($event: Event, widget: Widget): void {
  181 + if (event) {
  182 + event.stopPropagation();
  183 + }
  184 + this.dialogService.todo();
  185 + }
  186 +
  187 + removeWidgetType($event: Event, widget: Widget): void {
  188 + if (event) {
  189 + event.stopPropagation();
  190 + }
  191 + this.dialogService.todo();
  192 + }
  193 +
  194 +}
... ...
... ... @@ -20,13 +20,15 @@ import {SharedModule} from '@shared/shared.module';
20 20 import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component';
21 21 import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
22 22 import {HomeComponentsModule} from '@modules/home/components/home-components.module';
  23 +import { WidgetLibraryComponent } from './widget-library.component';
23 24
24 25 @NgModule({
25 26 entryComponents: [
26 27 WidgetsBundleComponent
27 28 ],
28 29 declarations: [
29   - WidgetsBundleComponent
  30 + WidgetsBundleComponent,
  31 + WidgetLibraryComponent
30 32 ],
31 33 imports: [
32 34 CommonModule,
... ...
... ... @@ -135,9 +135,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
135 135 if ($event) {
136 136 $event.stopPropagation();
137 137 }
138   - // TODO:
139   - // this.router.navigateByUrl(`customers/${customer.id.id}/users`);
140   - this.dialogService.todo();
  138 + this.router.navigateByUrl(`widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
141 139 }
142 140
143 141 exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + animate,
  19 + keyframes,
  20 + query,
  21 + stagger,
  22 + state,
  23 + style,
  24 + transition,
  25 + trigger
  26 +} from '@angular/animations';
  27 +
  28 +export const speedDialFabAnimations = [
  29 + trigger('fabToggler', [
  30 + state('inactive', style({
  31 + transform: 'rotate(0deg)'
  32 + })),
  33 + state('active', style({
  34 + transform: 'rotate(225deg)'
  35 + })),
  36 + transition('* <=> *', animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
  37 + ]),
  38 + trigger('speedDialStagger', [
  39 + transition('* => *', [
  40 +
  41 + query(':enter', style({ opacity: 0 }), {optional: true}),
  42 +
  43 + query(':enter', stagger('40ms',
  44 + [
  45 + animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
  46 + keyframes(
  47 + [
  48 + style({opacity: 0, transform: 'translateY(10px)'}),
  49 + style({opacity: 1, transform: 'translateY(0)'}),
  50 + ]
  51 + )
  52 + )
  53 + ]
  54 + ), {optional: true}),
  55 +
  56 + query(':leave',
  57 + animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
  58 + keyframes([
  59 + style({opacity: 1}),
  60 + style({opacity: 0}),
  61 + ])
  62 + ), {optional: true}
  63 + )
  64 +
  65 + ])
  66 + ])
  67 +];
... ...
... ... @@ -16,7 +16,9 @@
16 16
17 17 -->
18 18 <div fxFlex class="tb-breadcrumb" fxLayout="row">
19   - <h1 fxFlex fxHide.gt-sm>{{ (lastBreadcrumb$ | async).label | translate }}</h1>
  19 + <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
  20 + {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
  21 + </h1>
20 22 <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast">
21 23 <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
22 24 <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
... ... @@ -24,7 +26,7 @@
24 26 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
25 27 {{ breadcrumb.icon }}
26 28 </mat-icon>
27   - {{ breadcrumb.label | translate }}
  29 + {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
28 30 </a>
29 31 <span *ngSwitchCase="true">
30 32 <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
... ... @@ -32,7 +34,7 @@
32 34 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
33 35 {{ breadcrumb.icon }}
34 36 </mat-icon>
35   - {{ breadcrumb.label | translate }}
  37 + {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
36 38 </span>
37 39 <span class="divider" [fxHide]="isLast"> > </span>
38 40 </span>
... ...
... ... @@ -16,9 +16,10 @@
16 16
17 17 import { Component, OnDestroy, OnInit } from '@angular/core';
18 18 import { BehaviorSubject, Subject } from 'rxjs';
19   -import { BreadCrumb } from './breadcrumb';
  19 +import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
20 20 import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
21 21 import { distinctUntilChanged, filter, map } from 'rxjs/operators';
  22 +import { TranslateService } from '@ngx-translate/core';
22 23
23 24 @Component({
24 25 selector: '[tb-breadcrumb]',
... ... @@ -40,7 +41,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
40 41 );
41 42
42 43 constructor(private router: Router,
43   - private activatedRoute: ActivatedRoute) {
  44 + private activatedRoute: ActivatedRoute,
  45 + private translate: TranslateService) {
44 46 }
45 47
46 48 ngOnInit(): void {
... ... @@ -56,15 +58,24 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
56 58 buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
57 59 let newBreadcrumbs = breadcrumbs;
58 60 if (route.routeConfig && route.routeConfig.data) {
59   - const breadcrumbData = route.routeConfig.data.breadcrumb;
60   - if (breadcrumbData && !breadcrumbData.skip) {
61   - const label = breadcrumbData.label || 'home.home';
62   - const icon = breadcrumbData.icon || 'home';
  61 + const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig;
  62 + if (breadcrumbConfig && !breadcrumbConfig.skip) {
  63 + let label;
  64 + let ignoreTranslate;
  65 + if (breadcrumbConfig.labelFunction) {
  66 + label = breadcrumbConfig.labelFunction(route, this.translate);
  67 + ignoreTranslate = true;
  68 + } else {
  69 + label = breadcrumbConfig.label || 'home.home';
  70 + ignoreTranslate = false;
  71 + }
  72 + const icon = breadcrumbConfig.icon || 'home';
63 73 const isMdiIcon = icon.startsWith('mdi:');
64 74 const link = [ '/' + route.url.join('') ];
65 75 const queryParams = route.queryParams;
66 76 const breadcrumb = {
67 77 label,
  78 + ignoreTranslate,
68 79 icon,
69 80 isMdiIcon,
70 81 link,
... ...
... ... @@ -14,13 +14,23 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Params } from '@angular/router';
  17 +import { ActivatedRouteSnapshot, Params } from '@angular/router';
  18 +import { TranslateService } from '@ngx-translate/core';
18 19
19 20 export interface BreadCrumb {
20 21 label: string;
  22 + ignoreTranslate: boolean;
21 23 icon: string;
22 24 isMdiIcon: boolean;
23 25 link: any[];
24 26 queryParams: Params;
25 27 }
26 28
  29 +export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService) => string;
  30 +
  31 +export interface BreadCrumbConfig {
  32 + labelFunction: BreadCrumbLabelFunction;
  33 + label: string;
  34 + icon: string;
  35 + skip: boolean;
  36 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section fxLayout="row" class="layout-wrap tb-footer-buttons">
  19 + <div class="fab-container">
  20 + <button [disabled]="isLoading$ | async"
  21 + mat-fab class="fab-toggler tb-btn-footer"
  22 + color="accent"
  23 + matTooltip="{{ footerFabButtons.fabTogglerName | translate }}"
  24 + matTooltipPosition="above"
  25 + (click)="onToggleFab()">
  26 + <mat-icon [@fabToggler]="{value: fabTogglerState}">{{ footerFabButtons.fabTogglerIcon }}</mat-icon>
  27 + </button>
  28 + <div [@speedDialStagger]="buttons.length">
  29 + <button *ngFor="let btn of buttons"
  30 + mat-fab
  31 + color="accent"
  32 + matTooltip="{{ btn.name | translate }}"
  33 + matTooltipPosition="above"
  34 + (click)="btn.onAction($event)">
  35 + <mat-icon>{{btn.icon}}</mat-icon>
  36 + </button>
  37 + </div>
  38 + </div>
  39 +</section>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + section.tb-footer-buttons {
  19 + position: fixed;
  20 + right: 20px;
  21 + bottom: 20px;
  22 + z-index: 30;
  23 + pointer-events: none;
  24 +
  25 + .fab-container {
  26 + display: flex;
  27 + flex-direction: column-reverse;
  28 + align-items: center;
  29 + > div {
  30 + display: flex;
  31 + flex-direction: column-reverse;
  32 + align-items: center;
  33 + margin-bottom: 5px;
  34 +
  35 + button {
  36 + margin-bottom: 17px;
  37 + }
  38 + }
  39 + }
  40 +
  41 + .tb-btn-footer {
  42 + position: relative !important;
  43 + display: inline-block !important;
  44 + animation: tbMoveFromBottomFade .3s ease both;
  45 +
  46 + &.tb-hide {
  47 + animation: tbMoveToBottomFade .3s ease both;
  48 + }
  49 + }
  50 + }
  51 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input, HostListener } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
  22 +
  23 +export interface FooterFabButton {
  24 + name: string;
  25 + icon: string;
  26 + onAction: ($event: Event) => void;
  27 +}
  28 +
  29 +export interface FooterFabButtons {
  30 + fabTogglerName: string;
  31 + fabTogglerIcon: string;
  32 + buttons: Array<FooterFabButton>;
  33 +}
  34 +
  35 +@Component({
  36 + selector: 'tb-footer-fab-buttons',
  37 + templateUrl: './footer-fab-buttons.component.html',
  38 + styleUrls: ['./footer-fab-buttons.component.scss'],
  39 + animations: speedDialFabAnimations
  40 +})
  41 +export class FooterFabButtonsComponent extends PageComponent {
  42 +
  43 + @Input()
  44 + footerFabButtons: FooterFabButtons;
  45 +
  46 + buttons: Array<FooterFabButton> = [];
  47 + fabTogglerState = 'inactive';
  48 +
  49 + closeTimeout = null;
  50 +
  51 + @HostListener('focusout', ['$event'])
  52 + onFocusOut($event) {
  53 + if (!this.closeTimeout) {
  54 + this.closeTimeout = setTimeout(() => {
  55 + this.hideItems();
  56 + }, 100);
  57 + }
  58 + }
  59 +
  60 + @HostListener('focusin', ['$event'])
  61 + onFocusIn($event) {
  62 + if (this.closeTimeout) {
  63 + clearTimeout(this.closeTimeout);
  64 + this.closeTimeout = null;
  65 + }
  66 + }
  67 +
  68 + constructor(protected store: Store<AppState>) {
  69 + super(store);
  70 + }
  71 +
  72 + showItems() {
  73 + this.fabTogglerState = 'active';
  74 + this.buttons = this.footerFabButtons.buttons;
  75 + }
  76 +
  77 + hideItems() {
  78 + this.fabTogglerState = 'inactive';
  79 + this.buttons = [];
  80 + }
  81 +
  82 + onToggleFab() {
  83 + this.buttons.length ? this.hideItems() : this.showItems();
  84 + }
  85 +}
... ...
... ... @@ -46,6 +46,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
46 46 set min(min: number) {
47 47 if (typeof min !== 'undefined' && min !== this.minValue) {
48 48 this.minValue = min;
  49 + this.maxValue = Math.max(this.maxValue, this.minValue);
49 50 this.updateView();
50 51 }
51 52 }
... ... @@ -54,6 +55,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
54 55 set max(max: number) {
55 56 if (typeof max !== 'undefined' && max !== this.maxValue) {
56 57 this.maxValue = max;
  58 + this.minValue = Math.min(this.minValue, this.maxValue);
57 59 this.updateView();
58 60 }
59 61 }
... ...
... ... @@ -14,7 +14,11 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
  17 + min-width: 52px;
17 18 section.tb-timewindow {
  19 + min-height: 32px;
  20 + padding: 0 6px;
  21 +
18 22 span {
19 23 overflow: hidden;
20 24 text-overflow: ellipsis;
... ...
... ... @@ -25,6 +25,19 @@ export interface DashboardInfo extends BaseData<DashboardId> {
25 25 assignedCustomers: Array<ShortCustomerInfo>;
26 26 }
27 27
  28 +export interface WidgetLayout {
  29 + sizeX: number;
  30 + sizeY: number;
  31 + mobileHeight: number;
  32 + mobileOrder: number;
  33 + col: number;
  34 + row: number;
  35 +}
  36 +
  37 +export interface WidgetLayouts {
  38 + [id: string]: WidgetLayout;
  39 +}
  40 +
28 41 export interface DashboardConfiguration {
29 42 [key: string]: any;
30 43 // TODO:
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {BaseData} from '@shared/models/base-data';
  18 +import {TenantId} from '@shared/models/id/tenant-id';
  19 +import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id';
  20 +import {WidgetTypeId} from '@shared/models/id/widget-type-id';
  21 +import { AliasEntityType, EntityType, EntityTypeTranslation } from '@shared/models/entity-type.models';
  22 +import { Timewindow } from '@shared/models/time/time.models';
  23 +
  24 +export enum widgetType {
  25 + timeseries = 'timeseries',
  26 + latest = 'latest',
  27 + rpc = 'rpc',
  28 + alarm = 'alarm'
  29 +}
  30 +
  31 +export interface WidgetTypeTemplate {
  32 + bundleAlias: string;
  33 + alias: string;
  34 +}
  35 +
  36 +export interface WidgetTypeData {
  37 + name: string;
  38 + template: WidgetTypeTemplate;
  39 +}
  40 +
  41 +export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
  42 + [
  43 + [
  44 + widgetType.timeseries,
  45 + {
  46 + name: 'widget.timeseries',
  47 + template: {
  48 + bundleAlias: 'charts',
  49 + alias: 'basic_timeseries'
  50 + }
  51 + }
  52 + ],
  53 + [
  54 + widgetType.latest,
  55 + {
  56 + name: 'widget.latest-values',
  57 + template: {
  58 + bundleAlias: 'cards',
  59 + alias: 'attributes_card'
  60 + }
  61 + }
  62 + ],
  63 + [
  64 + widgetType.rpc,
  65 + {
  66 + name: 'widget.rpc',
  67 + template: {
  68 + bundleAlias: 'gpio_widgets',
  69 + alias: 'basic_gpio_control'
  70 + }
  71 + }
  72 + ],
  73 + [
  74 + widgetType.alarm,
  75 + {
  76 + name: 'widget.alarm',
  77 + template: {
  78 + bundleAlias: 'alarm_widgets',
  79 + alias: 'alarms_table'
  80 + }
  81 + }
  82 + ]
  83 + ]
  84 +);
  85 +
  86 +export interface WidgetResource {
  87 + url: string;
  88 +}
  89 +
  90 +export interface WidgetTypeDescriptor {
  91 + type: widgetType;
  92 + resources: Array<WidgetResource>;
  93 + templateHtml: string;
  94 + templateCss: string;
  95 + controllerScript: string;
  96 + settingsSchema: string;
  97 + dataKeySettingsSchema: string;
  98 + defaultConfig: string;
  99 + sizeX: number;
  100 + sizeY: number;
  101 +}
  102 +
  103 +export interface WidgetType extends BaseData<WidgetTypeId> {
  104 + tenantId: TenantId;
  105 + bundleAlias: string;
  106 + alias: string;
  107 + name: string;
  108 + descriptor: WidgetTypeDescriptor;
  109 +}
  110 +
  111 +export interface WidgetInfo extends WidgetTypeDescriptor {
  112 + widgetName: string;
  113 + alias: string;
  114 +}
  115 +
  116 +export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
  117 + return {
  118 + widgetName: widgetTypeEntity.name,
  119 + alias: widgetTypeEntity.alias,
  120 + type: widgetTypeEntity.descriptor.type,
  121 + sizeX: widgetTypeEntity.descriptor.sizeX,
  122 + sizeY: widgetTypeEntity.descriptor.sizeY,
  123 + resources: widgetTypeEntity.descriptor.resources,
  124 + templateHtml: widgetTypeEntity.descriptor.templateHtml,
  125 + templateCss: widgetTypeEntity.descriptor.templateCss,
  126 + controllerScript: widgetTypeEntity.descriptor.controllerScript,
  127 + settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
  128 + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
  129 + defaultConfig: widgetTypeEntity.descriptor.defaultConfig
  130 + };
  131 +}
  132 +
  133 +export interface WidgetConfig {
  134 + title?: string;
  135 + titleIcon?: string;
  136 + showTitle?: boolean;
  137 + showTitleIcon?: boolean;
  138 + iconColor?: string;
  139 + iconSize?: number;
  140 + dropShadow?: boolean;
  141 + enableFullscreen?: boolean;
  142 + useDashboardTimewindow?: boolean;
  143 + displayTimewindow?: boolean;
  144 + timewindow?: Timewindow;
  145 + mobileHeight?: number;
  146 + mobileOrder?: number;
  147 + color?: string;
  148 + backgroundColor?: string;
  149 + padding?: string;
  150 + margin?: string;
  151 + widgetStyle?: {[klass: string]: any};
  152 + titleStyle?: {[klass: string]: any};
  153 + [key: string]: any;
  154 +
  155 + // TODO:
  156 +}
  157 +
  158 +export interface Widget {
  159 + id?: string;
  160 + typeId: WidgetTypeId;
  161 + isSystemType: boolean;
  162 + bundleAlias: string;
  163 + typeAlias: string;
  164 + type: widgetType;
  165 + title: string;
  166 + sizeX: number;
  167 + sizeY: number;
  168 + row: number;
  169 + col: number;
  170 + config: WidgetConfig;
  171 +}
... ...
... ... @@ -52,6 +52,7 @@ import {
52 52 MatTooltipModule
53 53 } from '@angular/material';
54 54 import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core';
  55 +import {GridsterModule} from 'angular-gridster2';
55 56 import {FlexLayoutModule} from '@angular/flex-layout';
56 57 import {FormsModule, ReactiveFormsModule} from '@angular/forms';
57 58 import {RouterModule} from '@angular/router';
... ... @@ -86,6 +87,7 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
86 87 import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
87 88 import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
88 89 import { JsonObjectEditComponent } from './components/json-object-edit.component';
  90 +import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
89 91
90 92 @NgModule({
91 93 providers: [
... ... @@ -102,6 +104,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
102 104 declarations: [
103 105 FooterComponent,
104 106 LogoComponent,
  107 + FooterFabButtonsComponent,
105 108 ToastDirective,
106 109 FullscreenDirective,
107 110 TbAnchorComponent,
... ... @@ -167,6 +170,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
167 170 MatStepperModule,
168 171 MatAutocompleteModule,
169 172 MatChipsModule,
  173 + GridsterModule,
170 174 ClipboardModule,
171 175 FlexLayoutModule.withConfig({addFlexToParent: false}),
172 176 FormsModule,
... ... @@ -177,6 +181,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
177 181 exports: [
178 182 FooterComponent,
179 183 LogoComponent,
  184 + FooterFabButtonsComponent,
180 185 ToastDirective,
181 186 FullscreenDirective,
182 187 TbAnchorComponent,
... ... @@ -232,6 +237,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
232 237 MatStepperModule,
233 238 MatAutocompleteModule,
234 239 MatChipsModule,
  240 + GridsterModule,
235 241 ClipboardModule,
236 242 FlexLayoutModule,
237 243 FormsModule,
... ...
... ... @@ -351,6 +351,16 @@ $tb-dark-theme: get-tb-dark-theme(
351 351
352 352 .mat-icon {
353 353 vertical-align: middle;
  354 + &.tb-mat-20 {
  355 + width: 20px;
  356 + height: 20px;
  357 + font-size: 20px;
  358 + svg {
  359 + width: 24px;
  360 + height: 24px;
  361 + transform: scale(0.83);
  362 + }
  363 + }
354 364 &.tb-mat-32 {
355 365 width: 32px;
356 366 height: 32px;
... ...
... ... @@ -6075,7 +6075,8 @@
6075 6075 "ansi-regex": {
6076 6076 "version": "2.1.1",
6077 6077 "bundled": true,
6078   - "dev": true
  6078 + "dev": true,
  6079 + "optional": true
6079 6080 },
6080 6081 "aproba": {
6081 6082 "version": "1.2.0",
... ... @@ -6096,12 +6097,14 @@
6096 6097 "balanced-match": {
6097 6098 "version": "1.0.0",
6098 6099 "bundled": true,
6099   - "dev": true
  6100 + "dev": true,
  6101 + "optional": true
6100 6102 },
6101 6103 "brace-expansion": {
6102 6104 "version": "1.1.11",
6103 6105 "bundled": true,
6104 6106 "dev": true,
  6107 + "optional": true,
6105 6108 "requires": {
6106 6109 "balanced-match": "^1.0.0",
6107 6110 "concat-map": "0.0.1"
... ... @@ -6116,17 +6119,20 @@
6116 6119 "code-point-at": {
6117 6120 "version": "1.1.0",
6118 6121 "bundled": true,
6119   - "dev": true
  6122 + "dev": true,
  6123 + "optional": true
6120 6124 },
6121 6125 "concat-map": {
6122 6126 "version": "0.0.1",
6123 6127 "bundled": true,
6124   - "dev": true
  6128 + "dev": true,
  6129 + "optional": true
6125 6130 },
6126 6131 "console-control-strings": {
6127 6132 "version": "1.1.0",
6128 6133 "bundled": true,
6129   - "dev": true
  6134 + "dev": true,
  6135 + "optional": true
6130 6136 },
6131 6137 "core-util-is": {
6132 6138 "version": "1.0.2",
... ... @@ -6243,7 +6249,8 @@
6243 6249 "inherits": {
6244 6250 "version": "2.0.3",
6245 6251 "bundled": true,
6246   - "dev": true
  6252 + "dev": true,
  6253 + "optional": true
6247 6254 },
6248 6255 "ini": {
6249 6256 "version": "1.3.5",
... ... @@ -6255,6 +6262,7 @@
6255 6262 "version": "1.0.0",
6256 6263 "bundled": true,
6257 6264 "dev": true,
  6265 + "optional": true,
6258 6266 "requires": {
6259 6267 "number-is-nan": "^1.0.0"
6260 6268 }
... ... @@ -6269,6 +6277,7 @@
6269 6277 "version": "3.0.4",
6270 6278 "bundled": true,
6271 6279 "dev": true,
  6280 + "optional": true,
6272 6281 "requires": {
6273 6282 "brace-expansion": "^1.1.7"
6274 6283 }
... ... @@ -6276,12 +6285,14 @@
6276 6285 "minimist": {
6277 6286 "version": "0.0.8",
6278 6287 "bundled": true,
6279   - "dev": true
  6288 + "dev": true,
  6289 + "optional": true
6280 6290 },
6281 6291 "minipass": {
6282 6292 "version": "2.3.5",
6283 6293 "bundled": true,
6284 6294 "dev": true,
  6295 + "optional": true,
6285 6296 "requires": {
6286 6297 "safe-buffer": "^5.1.2",
6287 6298 "yallist": "^3.0.0"
... ... @@ -6300,6 +6311,7 @@
6300 6311 "version": "0.5.1",
6301 6312 "bundled": true,
6302 6313 "dev": true,
  6314 + "optional": true,
6303 6315 "requires": {
6304 6316 "minimist": "0.0.8"
6305 6317 }
... ... @@ -6380,7 +6392,8 @@
6380 6392 "number-is-nan": {
6381 6393 "version": "1.0.1",
6382 6394 "bundled": true,
6383   - "dev": true
  6395 + "dev": true,
  6396 + "optional": true
6384 6397 },
6385 6398 "object-assign": {
6386 6399 "version": "4.1.1",
... ... @@ -6392,6 +6405,7 @@
6392 6405 "version": "1.4.0",
6393 6406 "bundled": true,
6394 6407 "dev": true,
  6408 + "optional": true,
6395 6409 "requires": {
6396 6410 "wrappy": "1"
6397 6411 }
... ... @@ -6477,7 +6491,8 @@
6477 6491 "safe-buffer": {
6478 6492 "version": "5.1.2",
6479 6493 "bundled": true,
6480   - "dev": true
  6494 + "dev": true,
  6495 + "optional": true
6481 6496 },
6482 6497 "safer-buffer": {
6483 6498 "version": "2.1.2",
... ... @@ -6513,6 +6528,7 @@
6513 6528 "version": "1.0.2",
6514 6529 "bundled": true,
6515 6530 "dev": true,
  6531 + "optional": true,
6516 6532 "requires": {
6517 6533 "code-point-at": "^1.0.0",
6518 6534 "is-fullwidth-code-point": "^1.0.0",
... ... @@ -6532,6 +6548,7 @@
6532 6548 "version": "3.0.1",
6533 6549 "bundled": true,
6534 6550 "dev": true,
  6551 + "optional": true,
6535 6552 "requires": {
6536 6553 "ansi-regex": "^2.0.0"
6537 6554 }
... ... @@ -6575,12 +6592,14 @@
6575 6592 "wrappy": {
6576 6593 "version": "1.0.2",
6577 6594 "bundled": true,
6578   - "dev": true
  6595 + "dev": true,
  6596 + "optional": true
6579 6597 },
6580 6598 "yallist": {
6581 6599 "version": "3.0.3",
6582 6600 "bundled": true,
6583   - "dev": true
  6601 + "dev": true,
  6602 + "optional": true
6584 6603 }
6585 6604 }
6586 6605 },
... ...