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