Commit 2e7070a9039b5bbaaa5f598feea88b7a3df62b26
1 parent
22d8ce71
Dashboard component implementation.
Showing
32 changed files
with
1752 additions
and
46 deletions
@@ -1486,6 +1486,14 @@ | @@ -1486,6 +1486,14 @@ | ||
1486 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", | 1486 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", |
1487 | "dev": true | 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 | "ansi-colors": { | 1497 | "ansi-colors": { |
1490 | "version": "3.2.4", | 1498 | "version": "3.2.4", |
1491 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", | 1499 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", |
@@ -32,6 +32,7 @@ | @@ -32,6 +32,7 @@ | ||
32 | "@ngx-translate/core": "^11.0.1", | 32 | "@ngx-translate/core": "^11.0.1", |
33 | "@ngx-translate/http-loader": "^4.0.0", | 33 | "@ngx-translate/http-loader": "^4.0.0", |
34 | "ace-builds": "^1.4.5", | 34 | "ace-builds": "^1.4.5", |
35 | + "angular-gridster2": "^8.1.0", | ||
35 | "compass-sass-mixins": "^0.12.7", | 36 | "compass-sass-mixins": "^0.12.7", |
36 | "core-js": "^3.1.4", | 37 | "core-js": "^3.1.4", |
37 | "deep-equal": "^1.0.1", | 38 | "deep-equal": "^1.0.1", |
@@ -21,6 +21,7 @@ import {HttpClient} from '@angular/common/http'; | @@ -21,6 +21,7 @@ import {HttpClient} from '@angular/common/http'; | ||
21 | import {PageLink} from '@shared/models/page/page-link'; | 21 | import {PageLink} from '@shared/models/page/page-link'; |
22 | import {PageData} from '@shared/models/page/page-data'; | 22 | import {PageData} from '@shared/models/page/page-data'; |
23 | import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; | 23 | import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; |
24 | +import { WidgetType } from '@shared/models/widget.models'; | ||
24 | 25 | ||
25 | @Injectable({ | 26 | @Injectable({ |
26 | providedIn: 'root' | 27 | providedIn: 'root' |
@@ -51,4 +52,10 @@ export class WidgetService { | @@ -51,4 +52,10 @@ export class WidgetService { | ||
51 | return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | 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,7 +15,7 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { Injectable } from '@angular/core'; | 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 | import {HttpClient} from '@angular/common/http'; | 19 | import {HttpClient} from '@angular/common/http'; |
20 | import {Observable} from 'rxjs'; | 20 | import {Observable} from 'rxjs'; |
21 | import {defaultHttpOptions} from '@core/http/http-utils'; | 21 | import {defaultHttpOptions} from '@core/http/http-utils'; |
@@ -81,6 +81,9 @@ export class TimeService { | @@ -81,6 +81,9 @@ export class TimeService { | ||
81 | const intervals = this.getIntervals(min, max); | 81 | const intervals = this.getIntervals(min, max); |
82 | let minDelta = MAX_INTERVAL; | 82 | let minDelta = MAX_INTERVAL; |
83 | const boundedInterval = intervalMs || min; | 83 | const boundedInterval = intervalMs || min; |
84 | + if (!intervals.length) { | ||
85 | + return boundedInterval; | ||
86 | + } | ||
84 | let matchedInterval: TimeInterval = intervals[0]; | 87 | let matchedInterval: TimeInterval = intervals[0]; |
85 | intervals.forEach((interval) => { | 88 | intervals.forEach((interval) => { |
86 | const delta = Math.abs(interval.value - boundedInterval); | 89 | const delta = Math.abs(interval.value - boundedInterval); |
@@ -110,6 +113,10 @@ export class TimeService { | @@ -110,6 +113,10 @@ export class TimeService { | ||
110 | return this.boundMaxInterval(max); | 113 | return this.boundMaxInterval(max); |
111 | } | 114 | } |
112 | 115 | ||
116 | + public defaultTimewindow(): Timewindow { | ||
117 | + return Timewindow.defaultTimewindow(this); | ||
118 | + } | ||
119 | + | ||
113 | private toBound(value: number, min: number, max: number, defValue: number): number { | 120 | private toBound(value: number, min: number, max: number, defValue: number): number { |
114 | if (typeof value !== 'undefined') { | 121 | if (typeof value !== 'undefined') { |
115 | value = Math.max(value, min); | 122 | value = Math.max(value, min); |
@@ -52,6 +52,32 @@ export function isLocalUrl(url: string): boolean { | @@ -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 | const scrollRegex = /(auto|scroll)/; | 81 | const scrollRegex = /(auto|scroll)/; |
56 | 82 | ||
57 | function parentNodes(node: Node, nodes: Node[]): Node[] { | 83 | function parentNodes(node: Node, nodes: Node[]): Node[] { |
@@ -95,3 +121,20 @@ function scrollParents(node: Node): Node[] { | @@ -95,3 +121,20 @@ function scrollParents(node: Node): Node[] { | ||
95 | } | 121 | } |
96 | return scrollParentNodes; | 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,6 +34,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail | ||
34 | import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component'; | 34 | import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component'; |
35 | import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component'; | 35 | import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component'; |
36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; | 36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; |
37 | +import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; | ||
37 | 38 | ||
38 | @NgModule({ | 39 | @NgModule({ |
39 | entryComponents: [ | 40 | entryComponents: [ |
@@ -64,7 +65,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val | @@ -64,7 +65,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val | ||
64 | AlarmDetailsDialogComponent, | 65 | AlarmDetailsDialogComponent, |
65 | AttributeTableComponent, | 66 | AttributeTableComponent, |
66 | AddAttributeDialogComponent, | 67 | AddAttributeDialogComponent, |
67 | - EditAttributeValuePanelComponent | 68 | + EditAttributeValuePanelComponent, |
69 | + DashboardComponent | ||
68 | ], | 70 | ], |
69 | imports: [ | 71 | imports: [ |
70 | CommonModule, | 72 | CommonModule, |
@@ -81,7 +83,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val | @@ -81,7 +83,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val | ||
81 | RelationTableComponent, | 83 | RelationTableComponent, |
82 | AlarmTableComponent, | 84 | AlarmTableComponent, |
83 | AlarmDetailsDialogComponent, | 85 | AlarmDetailsDialogComponent, |
84 | - AttributeTableComponent | 86 | + AttributeTableComponent, |
87 | + DashboardComponent | ||
85 | ] | 88 | ] |
86 | }) | 89 | }) |
87 | export class HomeComponentsModule { } | 90 | export class HomeComponentsModule { } |
@@ -54,9 +54,6 @@ export class HomeComponent extends PageComponent implements OnInit { | @@ -54,9 +54,6 @@ export class HomeComponent extends PageComponent implements OnInit { | ||
54 | authUser$: Observable<any>; | 54 | authUser$: Observable<any>; |
55 | userDetails$: Observable<User>; | 55 | userDetails$: Observable<User>; |
56 | userDetailsString: Observable<string>; | 56 | userDetailsString: Observable<string>; |
57 | - testUser1$: Observable<User>; | ||
58 | - testUser2$: Observable<User>; | ||
59 | - testUser3$: Observable<User>; | ||
60 | 57 | ||
61 | constructor(protected store: Store<AppState>, | 58 | constructor(protected store: Store<AppState>, |
62 | private authService: AuthService, | 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,20 +14,24 @@ | ||
14 | /// limitations under the License. | 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 | name: string; | 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,13 +14,35 @@ | ||
14 | /// limitations under the License. | 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 | import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; | 20 | import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; |
21 | import {Authority} from '@shared/models/authority.enum'; | 21 | import {Authority} from '@shared/models/authority.enum'; |
22 | import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; | 22 | import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; |
23 | import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; | 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 | const routes: Routes = [ | 47 | const routes: Routes = [ |
26 | { | 48 | { |
@@ -42,6 +64,21 @@ const routes: Routes = [ | @@ -42,6 +64,21 @@ const routes: Routes = [ | ||
42 | resolve: { | 64 | resolve: { |
43 | entitiesTableConfig: WidgetsBundlesTableConfigResolver | 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,7 +88,8 @@ const routes: Routes = [ | ||
51 | imports: [RouterModule.forChild(routes)], | 88 | imports: [RouterModule.forChild(routes)], |
52 | exports: [RouterModule], | 89 | exports: [RouterModule], |
53 | providers: [ | 90 | providers: [ |
54 | - WidgetsBundlesTableConfigResolver | 91 | + WidgetsBundlesTableConfigResolver, |
92 | + WidgetsBundleResolver | ||
55 | ] | 93 | ] |
56 | }) | 94 | }) |
57 | export class WidgetLibraryRoutingModule { } | 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,13 +20,15 @@ import {SharedModule} from '@shared/shared.module'; | ||
20 | import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component'; | 20 | import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component'; |
21 | import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module'; | 21 | import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module'; |
22 | import {HomeComponentsModule} from '@modules/home/components/home-components.module'; | 22 | import {HomeComponentsModule} from '@modules/home/components/home-components.module'; |
23 | +import { WidgetLibraryComponent } from './widget-library.component'; | ||
23 | 24 | ||
24 | @NgModule({ | 25 | @NgModule({ |
25 | entryComponents: [ | 26 | entryComponents: [ |
26 | WidgetsBundleComponent | 27 | WidgetsBundleComponent |
27 | ], | 28 | ], |
28 | declarations: [ | 29 | declarations: [ |
29 | - WidgetsBundleComponent | 30 | + WidgetsBundleComponent, |
31 | + WidgetLibraryComponent | ||
30 | ], | 32 | ], |
31 | imports: [ | 33 | imports: [ |
32 | CommonModule, | 34 | CommonModule, |
@@ -135,9 +135,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon | @@ -135,9 +135,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon | ||
135 | if ($event) { | 135 | if ($event) { |
136 | $event.stopPropagation(); | 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 | exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { | 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,7 +16,9 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div fxFlex class="tb-breadcrumb" fxLayout="row"> | 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 | <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast"> | 22 | <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast"> |
21 | <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams"> | 23 | <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams"> |
22 | <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> | 24 | <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> |
@@ -24,7 +26,7 @@ | @@ -24,7 +26,7 @@ | ||
24 | <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> | 26 | <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> |
25 | {{ breadcrumb.icon }} | 27 | {{ breadcrumb.icon }} |
26 | </mat-icon> | 28 | </mat-icon> |
27 | - {{ breadcrumb.label | translate }} | 29 | + {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} |
28 | </a> | 30 | </a> |
29 | <span *ngSwitchCase="true"> | 31 | <span *ngSwitchCase="true"> |
30 | <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> | 32 | <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> |
@@ -32,7 +34,7 @@ | @@ -32,7 +34,7 @@ | ||
32 | <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> | 34 | <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> |
33 | {{ breadcrumb.icon }} | 35 | {{ breadcrumb.icon }} |
34 | </mat-icon> | 36 | </mat-icon> |
35 | - {{ breadcrumb.label | translate }} | 37 | + {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} |
36 | </span> | 38 | </span> |
37 | <span class="divider" [fxHide]="isLast"> > </span> | 39 | <span class="divider" [fxHide]="isLast"> > </span> |
38 | </span> | 40 | </span> |
@@ -16,9 +16,10 @@ | @@ -16,9 +16,10 @@ | ||
16 | 16 | ||
17 | import { Component, OnDestroy, OnInit } from '@angular/core'; | 17 | import { Component, OnDestroy, OnInit } from '@angular/core'; |
18 | import { BehaviorSubject, Subject } from 'rxjs'; | 18 | import { BehaviorSubject, Subject } from 'rxjs'; |
19 | -import { BreadCrumb } from './breadcrumb'; | 19 | +import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; |
20 | import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; | 20 | import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; |
21 | import { distinctUntilChanged, filter, map } from 'rxjs/operators'; | 21 | import { distinctUntilChanged, filter, map } from 'rxjs/operators'; |
22 | +import { TranslateService } from '@ngx-translate/core'; | ||
22 | 23 | ||
23 | @Component({ | 24 | @Component({ |
24 | selector: '[tb-breadcrumb]', | 25 | selector: '[tb-breadcrumb]', |
@@ -40,7 +41,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { | @@ -40,7 +41,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { | ||
40 | ); | 41 | ); |
41 | 42 | ||
42 | constructor(private router: Router, | 43 | constructor(private router: Router, |
43 | - private activatedRoute: ActivatedRoute) { | 44 | + private activatedRoute: ActivatedRoute, |
45 | + private translate: TranslateService) { | ||
44 | } | 46 | } |
45 | 47 | ||
46 | ngOnInit(): void { | 48 | ngOnInit(): void { |
@@ -56,15 +58,24 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { | @@ -56,15 +58,24 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { | ||
56 | buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> { | 58 | buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> { |
57 | let newBreadcrumbs = breadcrumbs; | 59 | let newBreadcrumbs = breadcrumbs; |
58 | if (route.routeConfig && route.routeConfig.data) { | 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 | const isMdiIcon = icon.startsWith('mdi:'); | 73 | const isMdiIcon = icon.startsWith('mdi:'); |
64 | const link = [ '/' + route.url.join('') ]; | 74 | const link = [ '/' + route.url.join('') ]; |
65 | const queryParams = route.queryParams; | 75 | const queryParams = route.queryParams; |
66 | const breadcrumb = { | 76 | const breadcrumb = { |
67 | label, | 77 | label, |
78 | + ignoreTranslate, | ||
68 | icon, | 79 | icon, |
69 | isMdiIcon, | 80 | isMdiIcon, |
70 | link, | 81 | link, |
@@ -14,13 +14,23 @@ | @@ -14,13 +14,23 @@ | ||
14 | /// limitations under the License. | 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 | export interface BreadCrumb { | 20 | export interface BreadCrumb { |
20 | label: string; | 21 | label: string; |
22 | + ignoreTranslate: boolean; | ||
21 | icon: string; | 23 | icon: string; |
22 | isMdiIcon: boolean; | 24 | isMdiIcon: boolean; |
23 | link: any[]; | 25 | link: any[]; |
24 | queryParams: Params; | 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,6 +46,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { | ||
46 | set min(min: number) { | 46 | set min(min: number) { |
47 | if (typeof min !== 'undefined' && min !== this.minValue) { | 47 | if (typeof min !== 'undefined' && min !== this.minValue) { |
48 | this.minValue = min; | 48 | this.minValue = min; |
49 | + this.maxValue = Math.max(this.maxValue, this.minValue); | ||
49 | this.updateView(); | 50 | this.updateView(); |
50 | } | 51 | } |
51 | } | 52 | } |
@@ -54,6 +55,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { | @@ -54,6 +55,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { | ||
54 | set max(max: number) { | 55 | set max(max: number) { |
55 | if (typeof max !== 'undefined' && max !== this.maxValue) { | 56 | if (typeof max !== 'undefined' && max !== this.maxValue) { |
56 | this.maxValue = max; | 57 | this.maxValue = max; |
58 | + this.minValue = Math.min(this.minValue, this.maxValue); | ||
57 | this.updateView(); | 59 | this.updateView(); |
58 | } | 60 | } |
59 | } | 61 | } |
@@ -14,7 +14,11 @@ | @@ -14,7 +14,11 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | :host { | 16 | :host { |
17 | + min-width: 52px; | ||
17 | section.tb-timewindow { | 18 | section.tb-timewindow { |
19 | + min-height: 32px; | ||
20 | + padding: 0 6px; | ||
21 | + | ||
18 | span { | 22 | span { |
19 | overflow: hidden; | 23 | overflow: hidden; |
20 | text-overflow: ellipsis; | 24 | text-overflow: ellipsis; |
@@ -25,6 +25,19 @@ export interface DashboardInfo extends BaseData<DashboardId> { | @@ -25,6 +25,19 @@ export interface DashboardInfo extends BaseData<DashboardId> { | ||
25 | assignedCustomers: Array<ShortCustomerInfo>; | 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 | export interface DashboardConfiguration { | 41 | export interface DashboardConfiguration { |
29 | [key: string]: any; | 42 | [key: string]: any; |
30 | // TODO: | 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,6 +52,7 @@ import { | ||
52 | MatTooltipModule | 52 | MatTooltipModule |
53 | } from '@angular/material'; | 53 | } from '@angular/material'; |
54 | import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core'; | 54 | import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core'; |
55 | +import {GridsterModule} from 'angular-gridster2'; | ||
55 | import {FlexLayoutModule} from '@angular/flex-layout'; | 56 | import {FlexLayoutModule} from '@angular/flex-layout'; |
56 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; | 57 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; |
57 | import {RouterModule} from '@angular/router'; | 58 | import {RouterModule} from '@angular/router'; |
@@ -86,6 +87,7 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen | @@ -86,6 +87,7 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen | ||
86 | import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; | 87 | import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; |
87 | import { EntityListSelectComponent } from './components/entity/entity-list-select.component'; | 88 | import { EntityListSelectComponent } from './components/entity/entity-list-select.component'; |
88 | import { JsonObjectEditComponent } from './components/json-object-edit.component'; | 89 | import { JsonObjectEditComponent } from './components/json-object-edit.component'; |
90 | +import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; | ||
89 | 91 | ||
90 | @NgModule({ | 92 | @NgModule({ |
91 | providers: [ | 93 | providers: [ |
@@ -102,6 +104,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | @@ -102,6 +104,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | ||
102 | declarations: [ | 104 | declarations: [ |
103 | FooterComponent, | 105 | FooterComponent, |
104 | LogoComponent, | 106 | LogoComponent, |
107 | + FooterFabButtonsComponent, | ||
105 | ToastDirective, | 108 | ToastDirective, |
106 | FullscreenDirective, | 109 | FullscreenDirective, |
107 | TbAnchorComponent, | 110 | TbAnchorComponent, |
@@ -167,6 +170,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | @@ -167,6 +170,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | ||
167 | MatStepperModule, | 170 | MatStepperModule, |
168 | MatAutocompleteModule, | 171 | MatAutocompleteModule, |
169 | MatChipsModule, | 172 | MatChipsModule, |
173 | + GridsterModule, | ||
170 | ClipboardModule, | 174 | ClipboardModule, |
171 | FlexLayoutModule.withConfig({addFlexToParent: false}), | 175 | FlexLayoutModule.withConfig({addFlexToParent: false}), |
172 | FormsModule, | 176 | FormsModule, |
@@ -177,6 +181,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | @@ -177,6 +181,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | ||
177 | exports: [ | 181 | exports: [ |
178 | FooterComponent, | 182 | FooterComponent, |
179 | LogoComponent, | 183 | LogoComponent, |
184 | + FooterFabButtonsComponent, | ||
180 | ToastDirective, | 185 | ToastDirective, |
181 | FullscreenDirective, | 186 | FullscreenDirective, |
182 | TbAnchorComponent, | 187 | TbAnchorComponent, |
@@ -232,6 +237,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | @@ -232,6 +237,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component | ||
232 | MatStepperModule, | 237 | MatStepperModule, |
233 | MatAutocompleteModule, | 238 | MatAutocompleteModule, |
234 | MatChipsModule, | 239 | MatChipsModule, |
240 | + GridsterModule, | ||
235 | ClipboardModule, | 241 | ClipboardModule, |
236 | FlexLayoutModule, | 242 | FlexLayoutModule, |
237 | FormsModule, | 243 | FormsModule, |
@@ -351,6 +351,16 @@ $tb-dark-theme: get-tb-dark-theme( | @@ -351,6 +351,16 @@ $tb-dark-theme: get-tb-dark-theme( | ||
351 | 351 | ||
352 | .mat-icon { | 352 | .mat-icon { |
353 | vertical-align: middle; | 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 | &.tb-mat-32 { | 364 | &.tb-mat-32 { |
355 | width: 32px; | 365 | width: 32px; |
356 | height: 32px; | 366 | height: 32px; |
@@ -6075,7 +6075,8 @@ | @@ -6075,7 +6075,8 @@ | ||
6075 | "ansi-regex": { | 6075 | "ansi-regex": { |
6076 | "version": "2.1.1", | 6076 | "version": "2.1.1", |
6077 | "bundled": true, | 6077 | "bundled": true, |
6078 | - "dev": true | 6078 | + "dev": true, |
6079 | + "optional": true | ||
6079 | }, | 6080 | }, |
6080 | "aproba": { | 6081 | "aproba": { |
6081 | "version": "1.2.0", | 6082 | "version": "1.2.0", |
@@ -6096,12 +6097,14 @@ | @@ -6096,12 +6097,14 @@ | ||
6096 | "balanced-match": { | 6097 | "balanced-match": { |
6097 | "version": "1.0.0", | 6098 | "version": "1.0.0", |
6098 | "bundled": true, | 6099 | "bundled": true, |
6099 | - "dev": true | 6100 | + "dev": true, |
6101 | + "optional": true | ||
6100 | }, | 6102 | }, |
6101 | "brace-expansion": { | 6103 | "brace-expansion": { |
6102 | "version": "1.1.11", | 6104 | "version": "1.1.11", |
6103 | "bundled": true, | 6105 | "bundled": true, |
6104 | "dev": true, | 6106 | "dev": true, |
6107 | + "optional": true, | ||
6105 | "requires": { | 6108 | "requires": { |
6106 | "balanced-match": "^1.0.0", | 6109 | "balanced-match": "^1.0.0", |
6107 | "concat-map": "0.0.1" | 6110 | "concat-map": "0.0.1" |
@@ -6116,17 +6119,20 @@ | @@ -6116,17 +6119,20 @@ | ||
6116 | "code-point-at": { | 6119 | "code-point-at": { |
6117 | "version": "1.1.0", | 6120 | "version": "1.1.0", |
6118 | "bundled": true, | 6121 | "bundled": true, |
6119 | - "dev": true | 6122 | + "dev": true, |
6123 | + "optional": true | ||
6120 | }, | 6124 | }, |
6121 | "concat-map": { | 6125 | "concat-map": { |
6122 | "version": "0.0.1", | 6126 | "version": "0.0.1", |
6123 | "bundled": true, | 6127 | "bundled": true, |
6124 | - "dev": true | 6128 | + "dev": true, |
6129 | + "optional": true | ||
6125 | }, | 6130 | }, |
6126 | "console-control-strings": { | 6131 | "console-control-strings": { |
6127 | "version": "1.1.0", | 6132 | "version": "1.1.0", |
6128 | "bundled": true, | 6133 | "bundled": true, |
6129 | - "dev": true | 6134 | + "dev": true, |
6135 | + "optional": true | ||
6130 | }, | 6136 | }, |
6131 | "core-util-is": { | 6137 | "core-util-is": { |
6132 | "version": "1.0.2", | 6138 | "version": "1.0.2", |
@@ -6243,7 +6249,8 @@ | @@ -6243,7 +6249,8 @@ | ||
6243 | "inherits": { | 6249 | "inherits": { |
6244 | "version": "2.0.3", | 6250 | "version": "2.0.3", |
6245 | "bundled": true, | 6251 | "bundled": true, |
6246 | - "dev": true | 6252 | + "dev": true, |
6253 | + "optional": true | ||
6247 | }, | 6254 | }, |
6248 | "ini": { | 6255 | "ini": { |
6249 | "version": "1.3.5", | 6256 | "version": "1.3.5", |
@@ -6255,6 +6262,7 @@ | @@ -6255,6 +6262,7 @@ | ||
6255 | "version": "1.0.0", | 6262 | "version": "1.0.0", |
6256 | "bundled": true, | 6263 | "bundled": true, |
6257 | "dev": true, | 6264 | "dev": true, |
6265 | + "optional": true, | ||
6258 | "requires": { | 6266 | "requires": { |
6259 | "number-is-nan": "^1.0.0" | 6267 | "number-is-nan": "^1.0.0" |
6260 | } | 6268 | } |
@@ -6269,6 +6277,7 @@ | @@ -6269,6 +6277,7 @@ | ||
6269 | "version": "3.0.4", | 6277 | "version": "3.0.4", |
6270 | "bundled": true, | 6278 | "bundled": true, |
6271 | "dev": true, | 6279 | "dev": true, |
6280 | + "optional": true, | ||
6272 | "requires": { | 6281 | "requires": { |
6273 | "brace-expansion": "^1.1.7" | 6282 | "brace-expansion": "^1.1.7" |
6274 | } | 6283 | } |
@@ -6276,12 +6285,14 @@ | @@ -6276,12 +6285,14 @@ | ||
6276 | "minimist": { | 6285 | "minimist": { |
6277 | "version": "0.0.8", | 6286 | "version": "0.0.8", |
6278 | "bundled": true, | 6287 | "bundled": true, |
6279 | - "dev": true | 6288 | + "dev": true, |
6289 | + "optional": true | ||
6280 | }, | 6290 | }, |
6281 | "minipass": { | 6291 | "minipass": { |
6282 | "version": "2.3.5", | 6292 | "version": "2.3.5", |
6283 | "bundled": true, | 6293 | "bundled": true, |
6284 | "dev": true, | 6294 | "dev": true, |
6295 | + "optional": true, | ||
6285 | "requires": { | 6296 | "requires": { |
6286 | "safe-buffer": "^5.1.2", | 6297 | "safe-buffer": "^5.1.2", |
6287 | "yallist": "^3.0.0" | 6298 | "yallist": "^3.0.0" |
@@ -6300,6 +6311,7 @@ | @@ -6300,6 +6311,7 @@ | ||
6300 | "version": "0.5.1", | 6311 | "version": "0.5.1", |
6301 | "bundled": true, | 6312 | "bundled": true, |
6302 | "dev": true, | 6313 | "dev": true, |
6314 | + "optional": true, | ||
6303 | "requires": { | 6315 | "requires": { |
6304 | "minimist": "0.0.8" | 6316 | "minimist": "0.0.8" |
6305 | } | 6317 | } |
@@ -6380,7 +6392,8 @@ | @@ -6380,7 +6392,8 @@ | ||
6380 | "number-is-nan": { | 6392 | "number-is-nan": { |
6381 | "version": "1.0.1", | 6393 | "version": "1.0.1", |
6382 | "bundled": true, | 6394 | "bundled": true, |
6383 | - "dev": true | 6395 | + "dev": true, |
6396 | + "optional": true | ||
6384 | }, | 6397 | }, |
6385 | "object-assign": { | 6398 | "object-assign": { |
6386 | "version": "4.1.1", | 6399 | "version": "4.1.1", |
@@ -6392,6 +6405,7 @@ | @@ -6392,6 +6405,7 @@ | ||
6392 | "version": "1.4.0", | 6405 | "version": "1.4.0", |
6393 | "bundled": true, | 6406 | "bundled": true, |
6394 | "dev": true, | 6407 | "dev": true, |
6408 | + "optional": true, | ||
6395 | "requires": { | 6409 | "requires": { |
6396 | "wrappy": "1" | 6410 | "wrappy": "1" |
6397 | } | 6411 | } |
@@ -6477,7 +6491,8 @@ | @@ -6477,7 +6491,8 @@ | ||
6477 | "safe-buffer": { | 6491 | "safe-buffer": { |
6478 | "version": "5.1.2", | 6492 | "version": "5.1.2", |
6479 | "bundled": true, | 6493 | "bundled": true, |
6480 | - "dev": true | 6494 | + "dev": true, |
6495 | + "optional": true | ||
6481 | }, | 6496 | }, |
6482 | "safer-buffer": { | 6497 | "safer-buffer": { |
6483 | "version": "2.1.2", | 6498 | "version": "2.1.2", |
@@ -6513,6 +6528,7 @@ | @@ -6513,6 +6528,7 @@ | ||
6513 | "version": "1.0.2", | 6528 | "version": "1.0.2", |
6514 | "bundled": true, | 6529 | "bundled": true, |
6515 | "dev": true, | 6530 | "dev": true, |
6531 | + "optional": true, | ||
6516 | "requires": { | 6532 | "requires": { |
6517 | "code-point-at": "^1.0.0", | 6533 | "code-point-at": "^1.0.0", |
6518 | "is-fullwidth-code-point": "^1.0.0", | 6534 | "is-fullwidth-code-point": "^1.0.0", |
@@ -6532,6 +6548,7 @@ | @@ -6532,6 +6548,7 @@ | ||
6532 | "version": "3.0.1", | 6548 | "version": "3.0.1", |
6533 | "bundled": true, | 6549 | "bundled": true, |
6534 | "dev": true, | 6550 | "dev": true, |
6551 | + "optional": true, | ||
6535 | "requires": { | 6552 | "requires": { |
6536 | "ansi-regex": "^2.0.0" | 6553 | "ansi-regex": "^2.0.0" |
6537 | } | 6554 | } |
@@ -6575,12 +6592,14 @@ | @@ -6575,12 +6592,14 @@ | ||
6575 | "wrappy": { | 6592 | "wrappy": { |
6576 | "version": "1.0.2", | 6593 | "version": "1.0.2", |
6577 | "bundled": true, | 6594 | "bundled": true, |
6578 | - "dev": true | 6595 | + "dev": true, |
6596 | + "optional": true | ||
6579 | }, | 6597 | }, |
6580 | "yallist": { | 6598 | "yallist": { |
6581 | "version": "3.0.3", | 6599 | "version": "3.0.3", |
6582 | "bundled": true, | 6600 | "bundled": true, |
6583 | - "dev": true | 6601 | + "dev": true, |
6602 | + "optional": true | ||
6584 | } | 6603 | } |
6585 | } | 6604 | } |
6586 | }, | 6605 | }, |