Dashboard component implementation.
This commit is contained in:
parent
22d8ce7189
commit
2e7070a903
8
ui-ngx/package-lock.json
generated
8
ui-ngx/package-lock.json
generated
@ -1486,6 +1486,14 @@
|
||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||
"dev": true
|
||||
},
|
||||
"angular-gridster2": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/angular-gridster2/-/angular-gridster2-8.1.0.tgz",
|
||||
"integrity": "sha512-O3VrHj5iq2HwqQDlh/8LfPy4aLIRK1Kxm5iCCNOVFmnBLa6F//D5habPsiHRMTEkQ9vD+lFnU7+tORhf/y9LAg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"@ngx-translate/core": "^11.0.1",
|
||||
"@ngx-translate/http-loader": "^4.0.0",
|
||||
"ace-builds": "^1.4.5",
|
||||
"angular-gridster2": "^8.1.0",
|
||||
"compass-sass-mixins": "^0.12.7",
|
||||
"core-js": "^3.1.4",
|
||||
"deep-equal": "^1.0.1",
|
||||
|
||||
@ -21,6 +21,7 @@ import {HttpClient} from '@angular/common/http';
|
||||
import {PageLink} from '@shared/models/page/page-link';
|
||||
import {PageData} from '@shared/models/page/page-data';
|
||||
import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
|
||||
import { WidgetType } from '@shared/models/widget.models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -51,4 +52,10 @@ export class WidgetService {
|
||||
return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
|
||||
}
|
||||
|
||||
public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
|
||||
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<WidgetType>> {
|
||||
return this.http.get<Array<WidgetType>>(`/api/widgetTypes?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
|
||||
defaultHttpOptions(ignoreLoading, ignoreErrors));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
///
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DAY, defaultTimeIntervals, SECOND } from '@shared/models/time/time.models';
|
||||
import { DAY, defaultTimeIntervals, MINUTE, SECOND, Timewindow } from '@shared/models/time/time.models';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {defaultHttpOptions} from '@core/http/http-utils';
|
||||
@ -81,6 +81,9 @@ export class TimeService {
|
||||
const intervals = this.getIntervals(min, max);
|
||||
let minDelta = MAX_INTERVAL;
|
||||
const boundedInterval = intervalMs || min;
|
||||
if (!intervals.length) {
|
||||
return boundedInterval;
|
||||
}
|
||||
let matchedInterval: TimeInterval = intervals[0];
|
||||
intervals.forEach((interval) => {
|
||||
const delta = Math.abs(interval.value - boundedInterval);
|
||||
@ -110,6 +113,10 @@ export class TimeService {
|
||||
return this.boundMaxInterval(max);
|
||||
}
|
||||
|
||||
public defaultTimewindow(): Timewindow {
|
||||
return Timewindow.defaultTimewindow(this);
|
||||
}
|
||||
|
||||
private toBound(value: number, min: number, max: number, defValue: number): number {
|
||||
if (typeof value !== 'undefined') {
|
||||
value = Math.max(value, min);
|
||||
|
||||
@ -52,6 +52,32 @@ export function isLocalUrl(url: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: number) {
|
||||
let currentTime = 0;
|
||||
const increment = 20;
|
||||
const start = element.scrollTop;
|
||||
const to = scrollTop;
|
||||
const duration = delay ? delay : 0;
|
||||
const remaining = to - start;
|
||||
const animateScroll = () => {
|
||||
currentTime += increment;
|
||||
const val = easeInOut(currentTime, start, remaining, duration);
|
||||
element.scrollTop = val;
|
||||
if (currentTime < duration) {
|
||||
setTimeout(animateScroll, increment);
|
||||
}
|
||||
};
|
||||
animateScroll();
|
||||
}
|
||||
|
||||
export function isUndefined(value: any): boolean {
|
||||
return typeof value === 'undefined';
|
||||
}
|
||||
|
||||
export function isDefined(value: any): boolean {
|
||||
return typeof value !== 'undefined';
|
||||
}
|
||||
|
||||
const scrollRegex = /(auto|scroll)/;
|
||||
|
||||
function parentNodes(node: Node, nodes: Node[]): Node[] {
|
||||
@ -95,3 +121,20 @@ function scrollParents(node: Node): Node[] {
|
||||
}
|
||||
return scrollParentNodes;
|
||||
}
|
||||
|
||||
function easeInOut(
|
||||
currentTime: number,
|
||||
startTime: number,
|
||||
remainingTime: number,
|
||||
duration: number) {
|
||||
currentTime /= duration / 2;
|
||||
|
||||
if (currentTime < 1) {
|
||||
return (remainingTime / 2) * currentTime * currentTime + startTime;
|
||||
}
|
||||
|
||||
currentTime--;
|
||||
return (
|
||||
(-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
|
||||
[ngStyle]="options.dashboardStyle"
|
||||
[fxShow]="(loading() | async) && !options.isEdit">
|
||||
<mat-spinner color="warn" mode="indeterminate" diameter="100">
|
||||
</mat-spinner>
|
||||
</div>
|
||||
<div id="gridster-parent"
|
||||
fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
|
||||
(contextmenu)="openDashboardContextMenu($event)">
|
||||
<div [ngClass]="options.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
|
||||
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
|
||||
<gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async">
|
||||
<div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
|
||||
fxLayout="column"
|
||||
class="tb-widget"
|
||||
[ngClass]="{
|
||||
'tb-highlighted': isHighlighted(widget),
|
||||
'tb-not-highlighted': isNotHighlighted(widget),
|
||||
'mat-elevation-z4': widget.dropShadow,
|
||||
'tb-has-timewindow': widget.hasTimewindow
|
||||
}"
|
||||
[ngStyle]="widget.style"
|
||||
(mousedown)="widgetMouseDown($event, widget)"
|
||||
(click)="widgetClicked($event, widget)"
|
||||
(contextmenu)="openWidgetContextMenu($event, widget)">
|
||||
<div fxLayout="row" fxLayoutAlign="space-between start">
|
||||
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" [fxShow]="widget.showWidgetTitlePanel">
|
||||
<div *ngIf="widget.hasWidgetTitleTemplate">
|
||||
TODO:
|
||||
</div>
|
||||
<span [fxShow]="widget.showTitle" [ngStyle]="widget.titleStyle" class="mat-subheading-2 title">
|
||||
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
|
||||
{{widget.title}}
|
||||
</span>
|
||||
<tb-timewindow *ngIf="widget.hasTimewindow" aggregation="{{widget.hasAggregation}}" [ngModel]="widget.widget.config.timewindow"></tb-timewindow>
|
||||
</div>
|
||||
<div [fxShow]="widget.showWidgetActions"
|
||||
class="tb-widget-actions"
|
||||
[ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.hasWidgetTitleTemplate||widget.showTitle||widget.hasAggregation))}"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="start center"
|
||||
(mousedown)="$event.stopPropagation()">
|
||||
<button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
|
||||
[fxShow]="!options.isEdit"
|
||||
(click)="action.onAction($event)"
|
||||
matTooltip="{{ action.displayName }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
<button mat-button mat-icon-button *ngFor="let action of widget.widgetActions"
|
||||
[fxShow]="!options.isEdit && action.show"
|
||||
(click)="action.onAction($event)"
|
||||
matTooltip="{{ action.name | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
<button mat-button mat-icon-button
|
||||
[fxShow]="!options.isEdit && widget.enableFullscreen"
|
||||
(click)="widget.isFullscreen = !widget.isFullscreen"
|
||||
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
|
||||
</button>
|
||||
<button mat-button mat-icon-button
|
||||
[fxShow]="options.isEditActionEnabled && !widget.isFullscreen"
|
||||
(click)="editWidget($event, widget)"
|
||||
matTooltip="{{ 'widget.edit' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-button mat-icon-button
|
||||
[fxShow]="options.isExportActionEnabled && !widget.isFullscreen"
|
||||
(click)="exportWidget($event, widget)"
|
||||
matTooltip="{{ 'widget.export' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>file_download</mat-icon>
|
||||
</button>
|
||||
<button mat-button mat-icon-button
|
||||
[fxShow]="options.isRemoveActionEnabled && !widget.isFullscreen"
|
||||
(click)="removeWidget($event, widget)"
|
||||
matTooltip="{{ 'widget.remove' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div fxFlex fxLayout="column" class="tb-widget-content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Copyright © 2016-2019 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@import '../../../../../scss/constants';
|
||||
|
||||
:host {
|
||||
|
||||
.tb-progress-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 6;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tb-dashboard-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: none;
|
||||
outline: none;
|
||||
|
||||
gridster-item {
|
||||
transition: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
#gridster-child {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
div.tb-widget {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
|
||||
transition: all .2s ease-in-out;
|
||||
|
||||
.tb-widget-title {
|
||||
max-height: 65px;
|
||||
padding-top: 5px;
|
||||
padding-left: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
tb-timewindow {
|
||||
font-size: 14px;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 24px;
|
||||
letter-spacing: .01em;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-widget-actions {
|
||||
z-index: 19;
|
||||
margin: 5px 0 0;
|
||||
|
||||
&-absolute {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
button.mat-icon-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
line-height: 20px;
|
||||
|
||||
mat-icon {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tb-widget-content {
|
||||
tb-widget {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.tb-highlighted {
|
||||
border: 1px solid #039be5;
|
||||
box-shadow: 0 0 20px #039be5;
|
||||
}
|
||||
|
||||
&.tb-not-highlighted {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, OnInit, Input, ViewChild, AfterViewInit, ViewChildren, QueryList, ElementRef } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { AuthUser } from '@shared/models/user.model';
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||
import { Timewindow } from '@shared/models/time/time.models';
|
||||
import { TimeService } from '@core/services/time.service';
|
||||
import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
|
||||
import { GridsterResizable } from 'angular-gridster2/lib/gridsterResizable.service';
|
||||
import { IDashboardComponent, DashboardConfig, DashboardWidget } from '../../models/dashboard-component.models';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { Observable, ReplaySubject, merge } from 'rxjs';
|
||||
import { map, share, tap } from 'rxjs/operators';
|
||||
import { WidgetLayout } from '@shared/models/dashboard.models';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { Widget } from '@app/shared/models/widget.models';
|
||||
import { MatTab } from '@angular/material/tabs';
|
||||
import { animatedScroll, isDefined } from '@app/core/utils';
|
||||
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
|
||||
import { MediaBreakpoints } from '@shared/models/constants';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.scss']
|
||||
})
|
||||
export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit {
|
||||
|
||||
authUser: AuthUser;
|
||||
|
||||
@Input()
|
||||
options: DashboardConfig;
|
||||
|
||||
gridsterOpts: GridsterConfig;
|
||||
|
||||
dashboardLoading = true;
|
||||
|
||||
highlightedMode = false;
|
||||
highlightedWidget: DashboardWidget = null;
|
||||
selectedWidget: DashboardWidget = null;
|
||||
|
||||
isWidgetExpanded = false;
|
||||
isMobileSize = false;
|
||||
|
||||
@ViewChild('gridster', {static: true}) gridster: GridsterComponent;
|
||||
|
||||
@ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>;
|
||||
|
||||
widgets$: Observable<Array<DashboardWidget>>;
|
||||
|
||||
widgets: Array<DashboardWidget>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private timeService: TimeService,
|
||||
private dialogService: DialogService,
|
||||
private breakpointObserver: BreakpointObserver) {
|
||||
super(store);
|
||||
this.authUser = getCurrentAuthUser(store);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.options.dashboardTimewindow) {
|
||||
this.options.dashboardTimewindow = this.timeService.defaultTimewindow();
|
||||
}
|
||||
this.gridsterOpts = {
|
||||
gridType: 'scrollVertical',
|
||||
keepFixedHeightInMobile: true,
|
||||
pushItems: false,
|
||||
swap: false,
|
||||
maxRows: 100,
|
||||
minCols: this.options.columns ? this.options.columns : 24,
|
||||
outerMargin: true,
|
||||
outerMarginLeft: this.options.margins ? this.options.margins[0] : 10,
|
||||
outerMarginRight: this.options.margins ? this.options.margins[0] : 10,
|
||||
outerMarginTop: this.options.margins ? this.options.margins[1] : 10,
|
||||
outerMarginBottom: this.options.margins ? this.options.margins[1] : 10,
|
||||
minItemCols: 1,
|
||||
minItemRows: 1,
|
||||
defaultItemCols: 8,
|
||||
defaultItemRows: 6,
|
||||
resizable: {enabled: this.options.isEdit},
|
||||
draggable: {enabled: this.options.isEdit}
|
||||
};
|
||||
|
||||
this.updateGridsterOpts();
|
||||
|
||||
this.loadDashboard();
|
||||
|
||||
merge(this.breakpointObserver
|
||||
.observe(MediaBreakpoints['gt-sm']), this.options.layoutChange$).subscribe(
|
||||
() => {
|
||||
this.updateGridsterOpts();
|
||||
this.sortWidgets(this.widgets);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadDashboard() {
|
||||
this.widgets$ = this.options.widgetsData.pipe(
|
||||
map(widgetsData => {
|
||||
const dashboardWidgets = new Array<DashboardWidget>();
|
||||
let maxRows = this.gridsterOpts.maxRows;
|
||||
widgetsData.widgets.forEach(
|
||||
(widget) => {
|
||||
let widgetLayout: WidgetLayout;
|
||||
if (widgetsData.widgetLayouts && widget.id) {
|
||||
widgetLayout = widgetsData.widgetLayouts[widget.id];
|
||||
}
|
||||
const dashboardWidget = new DashboardWidget(this, widget, widgetLayout);
|
||||
const bottom = dashboardWidget.y + dashboardWidget.rows;
|
||||
maxRows = Math.max(maxRows, bottom);
|
||||
dashboardWidgets.push(dashboardWidget);
|
||||
}
|
||||
);
|
||||
this.sortWidgets(dashboardWidgets);
|
||||
this.gridsterOpts.maxRows = maxRows;
|
||||
return dashboardWidgets;
|
||||
}),
|
||||
tap((widgets) => {
|
||||
this.widgets = widgets;
|
||||
this.dashboardLoading = false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loadDashboard();
|
||||
}
|
||||
|
||||
sortWidgets(widgets?: Array<DashboardWidget>) {
|
||||
if (widgets) {
|
||||
widgets.sort((widget1, widget2) => {
|
||||
const row1 = widget1.widgetOrder;
|
||||
const row2 = widget2.widgetOrder;
|
||||
let res = row1 - row2;
|
||||
if (res === 0) {
|
||||
res = widget1.x - widget2.x;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
}
|
||||
|
||||
isAutofillHeight(): boolean {
|
||||
if (this.isMobileSize) {
|
||||
return isDefined(this.options.mobileAutofillHeight) ? this.options.mobileAutofillHeight : false;
|
||||
} else {
|
||||
return isDefined(this.options.autofillHeight) ? this.options.autofillHeight : false;
|
||||
}
|
||||
}
|
||||
|
||||
loading(): Observable<boolean> {
|
||||
return this.isLoading$.pipe(
|
||||
map(loading => (!this.options.ignoreLoading && loading) || this.dashboardLoading),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
openDashboardContextMenu($event: Event) {
|
||||
// TODO:
|
||||
// this.dialogService.todo();
|
||||
}
|
||||
|
||||
openWidgetContextMenu($event: Event, widget: DashboardWidget) {
|
||||
// TODO:
|
||||
// this.dialogService.todo();
|
||||
}
|
||||
|
||||
onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) {
|
||||
this.isWidgetExpanded = expanded;
|
||||
}
|
||||
|
||||
widgetMouseDown($event: Event, widget: DashboardWidget) {
|
||||
if (this.options.onWidgetMouseDown) {
|
||||
this.options.onWidgetMouseDown($event, widget.widget);
|
||||
}
|
||||
}
|
||||
|
||||
widgetClicked($event: Event, widget: DashboardWidget) {
|
||||
if (this.options.onWidgetClicked) {
|
||||
this.options.onWidgetClicked($event, widget.widget);
|
||||
}
|
||||
}
|
||||
|
||||
editWidget($event: Event, widget: DashboardWidget) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
if (this.options.isEditActionEnabled && this.options.onEditWidget) {
|
||||
this.options.onEditWidget($event, widget.widget);
|
||||
}
|
||||
}
|
||||
|
||||
exportWidget($event: Event, widget: DashboardWidget) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
if (this.options.isExportActionEnabled && this.options.onExportWidget) {
|
||||
this.options.onExportWidget($event, widget.widget);
|
||||
}
|
||||
}
|
||||
|
||||
removeWidget($event: Event, widget: DashboardWidget) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
if (this.options.isRemoveActionEnabled && this.options.onRemoveWidget) {
|
||||
this.options.onRemoveWidget($event, widget.widget);
|
||||
}
|
||||
}
|
||||
|
||||
highlightWidget(widget: DashboardWidget, delay?: number) {
|
||||
if (!this.highlightedMode || this.highlightedWidget !== widget) {
|
||||
this.highlightedMode = true;
|
||||
this.highlightedWidget = widget;
|
||||
this.scrollToWidget(widget, delay);
|
||||
}
|
||||
}
|
||||
|
||||
selectWidget(widget: DashboardWidget, delay?: number) {
|
||||
if (this.selectedWidget !== widget) {
|
||||
this.selectedWidget = widget;
|
||||
this.scrollToWidget(widget, delay);
|
||||
}
|
||||
}
|
||||
|
||||
resetHighlight() {
|
||||
this.highlightedMode = false;
|
||||
this.highlightedWidget = null;
|
||||
this.selectedWidget = null;
|
||||
}
|
||||
|
||||
isHighlighted(widget: DashboardWidget) {
|
||||
return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget);
|
||||
}
|
||||
|
||||
isNotHighlighted(widget: DashboardWidget) {
|
||||
return this.highlightedMode && this.highlightedWidget !== widget;
|
||||
}
|
||||
|
||||
scrollToWidget(widget: DashboardWidget, delay?: number) {
|
||||
if (this.gridsterItems) {
|
||||
const gridsterItem = this.gridsterItems.find((item => item.item === widget));
|
||||
const offset = (this.gridster.curHeight - gridsterItem.height) / 2;
|
||||
let scrollTop = gridsterItem.top;
|
||||
if (offset > 0) {
|
||||
scrollTop -= offset;
|
||||
}
|
||||
const parentElement = this.gridster.el as HTMLElement;
|
||||
animatedScroll(parentElement, scrollTop, delay);
|
||||
}
|
||||
}
|
||||
|
||||
private updateGridsterOpts() {
|
||||
this.isMobileSize = this.checkIsMobileSize();
|
||||
const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
|
||||
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
|
||||
const rowSize = this.detectRowSize(this.isMobileSize);
|
||||
if (this.gridsterOpts.fixedRowHeight !== rowSize) {
|
||||
this.gridsterOpts.fixedRowHeight = rowSize;
|
||||
}
|
||||
if (this.isAutofillHeight()) {
|
||||
this.gridsterOpts.gridType = 'fit';
|
||||
} else {
|
||||
this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical';
|
||||
}
|
||||
if (this.gridster && this.gridster.options) {
|
||||
this.gridster.optionsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private detectRowSize(isMobile: boolean): number | null {
|
||||
let rowHeight = null;
|
||||
if (!this.isAutofillHeight()) {
|
||||
if (isMobile) {
|
||||
rowHeight = isDefined(this.options.mobileRowHeight) ? this.options.mobileRowHeight : 70;
|
||||
}
|
||||
}
|
||||
return rowHeight;
|
||||
}
|
||||
|
||||
private checkIsMobileSize(): boolean {
|
||||
const isMobileDisabled = this.options.isMobileDisabled === true;
|
||||
let isMobileSize = this.options.isMobile === true && !isMobileDisabled;
|
||||
if (!isMobileSize && !isMobileDisabled) {
|
||||
isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
|
||||
}
|
||||
return isMobileSize;
|
||||
}
|
||||
|
||||
}
|
||||
@ -34,6 +34,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
|
||||
import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component';
|
||||
import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
|
||||
import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
|
||||
import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
|
||||
|
||||
@NgModule({
|
||||
entryComponents: [
|
||||
@ -64,7 +65,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val
|
||||
AlarmDetailsDialogComponent,
|
||||
AttributeTableComponent,
|
||||
AddAttributeDialogComponent,
|
||||
EditAttributeValuePanelComponent
|
||||
EditAttributeValuePanelComponent,
|
||||
DashboardComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -81,7 +83,8 @@ import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-val
|
||||
RelationTableComponent,
|
||||
AlarmTableComponent,
|
||||
AlarmDetailsDialogComponent,
|
||||
AttributeTableComponent
|
||||
AttributeTableComponent,
|
||||
DashboardComponent
|
||||
]
|
||||
})
|
||||
export class HomeComponentsModule { }
|
||||
|
||||
@ -54,9 +54,6 @@ export class HomeComponent extends PageComponent implements OnInit {
|
||||
authUser$: Observable<any>;
|
||||
userDetails$: Observable<User>;
|
||||
userDetailsString: Observable<string>;
|
||||
testUser1$: Observable<User>;
|
||||
testUser2$: Observable<User>;
|
||||
testUser3$: Observable<User>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private authService: AuthService,
|
||||
|
||||
310
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
Normal file
310
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
Normal file
@ -0,0 +1,310 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { GridsterConfig, GridsterItem, GridsterComponent } from 'angular-gridster2';
|
||||
import { Widget, widgetType } from '@app/shared/models/widget.models';
|
||||
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
|
||||
import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
|
||||
import { Timewindow } from '@shared/models/time/time.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { isDefined, isUndefined } from '@app/core/utils';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
|
||||
export interface IAliasController {
|
||||
[key: string]: any | null;
|
||||
// TODO:
|
||||
}
|
||||
|
||||
export interface WidgetsData {
|
||||
widgets: Array<Widget>;
|
||||
widgetLayouts?: WidgetLayouts;
|
||||
}
|
||||
|
||||
export class DashboardConfig {
|
||||
widgetsData?: Observable<WidgetsData>;
|
||||
isEdit: boolean;
|
||||
isEditActionEnabled: boolean;
|
||||
isExportActionEnabled: boolean;
|
||||
isRemoveActionEnabled: boolean;
|
||||
onEditWidget?: ($event: Event, widget: Widget) => void;
|
||||
onExportWidget?: ($event: Event, widget: Widget) => void;
|
||||
onRemoveWidget?: ($event: Event, widget: Widget) => void;
|
||||
onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
|
||||
onWidgetClicked?: ($event: Event, widget: Widget) => void;
|
||||
aliasController?: IAliasController;
|
||||
autofillHeight?: boolean;
|
||||
mobileAutofillHeight?: boolean;
|
||||
dashboardStyle?: {[klass: string]: any} | null;
|
||||
columns?: number;
|
||||
margins?: [number, number];
|
||||
dashboardTimewindow?: Timewindow;
|
||||
ignoreLoading?: boolean;
|
||||
dashboardClass?: string;
|
||||
mobileRowHeight?: number;
|
||||
|
||||
private isMobileValue: boolean;
|
||||
private isMobileDisabledValue: boolean;
|
||||
|
||||
private layoutChange = new EventEmitter();
|
||||
layoutChange$ = this.layoutChange.asObservable();
|
||||
layoutChangeTimeout = null;
|
||||
|
||||
set isMobile(isMobile: boolean) {
|
||||
if (this.isMobileValue !== isMobile) {
|
||||
const changed = isDefined(this.isMobileValue);
|
||||
this.isMobileValue = isMobile;
|
||||
if (changed) {
|
||||
this.notifyLayoutChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
get isMobile(): boolean {
|
||||
return this.isMobileValue;
|
||||
}
|
||||
|
||||
set isMobileDisabled(isMobileDisabled: boolean) {
|
||||
if (this.isMobileDisabledValue !== isMobileDisabled) {
|
||||
const changed = isDefined(this.isMobileDisabledValue);
|
||||
this.isMobileDisabledValue = isMobileDisabled;
|
||||
if (changed) {
|
||||
this.notifyLayoutChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
get isMobileDisabled(): boolean {
|
||||
return this.isMobileDisabledValue;
|
||||
}
|
||||
|
||||
private notifyLayoutChanged() {
|
||||
if (this.layoutChangeTimeout) {
|
||||
clearTimeout(this.layoutChangeTimeout);
|
||||
}
|
||||
this.layoutChangeTimeout = setTimeout(() => {
|
||||
this.doNotifyLayoutChanged();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private doNotifyLayoutChanged() {
|
||||
this.layoutChange.emit();
|
||||
this.layoutChangeTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDashboardComponent {
|
||||
options: DashboardConfig;
|
||||
gridsterOpts: GridsterConfig;
|
||||
gridster: GridsterComponent;
|
||||
isMobileSize: boolean;
|
||||
}
|
||||
|
||||
export class DashboardWidget implements GridsterItem {
|
||||
|
||||
isFullscreen = false;
|
||||
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
padding: string;
|
||||
margin: string;
|
||||
|
||||
title: string;
|
||||
showTitle: boolean;
|
||||
titleStyle: {[klass: string]: any};
|
||||
|
||||
titleIcon: string;
|
||||
showTitleIcon: boolean;
|
||||
titleIconStyle: {[klass: string]: any};
|
||||
|
||||
dropShadow: boolean;
|
||||
enableFullscreen: boolean;
|
||||
|
||||
hasTimewindow: boolean;
|
||||
|
||||
hasAggregation: boolean;
|
||||
|
||||
style: {[klass: string]: any};
|
||||
|
||||
hasWidgetTitleTemplate: boolean;
|
||||
widgetTitleTemplate: string;
|
||||
|
||||
showWidgetTitlePanel: boolean;
|
||||
showWidgetActions: boolean;
|
||||
|
||||
customHeaderActions: Array<WidgetHeaderAction>;
|
||||
widgetActions: Array<WidgetAction>;
|
||||
|
||||
widgetContext: WidgetContext = {};
|
||||
|
||||
constructor(
|
||||
private dashboard: IDashboardComponent,
|
||||
public widget: Widget,
|
||||
private widgetLayout?: WidgetLayout) {
|
||||
this.updateWidgetParams();
|
||||
}
|
||||
|
||||
updateWidgetParams() {
|
||||
this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
|
||||
this.backgroundColor = this.widget.config.backgroundColor || '#fff';
|
||||
this.padding = this.widget.config.padding || '8px';
|
||||
this.margin = this.widget.config.margin || '0px';
|
||||
|
||||
this.title = isDefined(this.widgetContext.widgetTitle)
|
||||
&& this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title;
|
||||
this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true;
|
||||
this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {};
|
||||
|
||||
this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : '';
|
||||
this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false;
|
||||
this.titleIconStyle = {};
|
||||
if (this.widget.config.iconColor) {
|
||||
this.titleIconStyle.color = this.widget.config.iconColor;
|
||||
}
|
||||
if (this.widget.config.iconSize) {
|
||||
this.titleIconStyle.fontSize = this.widget.config.iconSize;
|
||||
}
|
||||
|
||||
this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
|
||||
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
|
||||
|
||||
this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ?
|
||||
(isDefined(this.widget.config.useDashboardTimewindow) ?
|
||||
(!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
|
||||
|| this.widget.config.displayTimewindow)) : false)
|
||||
: false;
|
||||
|
||||
this.hasAggregation = this.widget.type === widgetType.timeseries;
|
||||
|
||||
this.style = {cursor: 'pointer',
|
||||
color: this.color,
|
||||
backgroundColor: this.backgroundColor,
|
||||
padding: this.padding,
|
||||
margin: this.margin};
|
||||
if (this.widget.config.widgetStyle) {
|
||||
this.style = {...this.widget.config.widgetStyle, ...this.style};
|
||||
}
|
||||
|
||||
this.hasWidgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? true : false;
|
||||
this.widgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? this.widgetContext.widgetTitleTemplate : '';
|
||||
|
||||
this.showWidgetTitlePanel = this.widgetContext.hideTitlePanel ? false :
|
||||
this.hasWidgetTitleTemplate || this.showTitle || this.hasTimewindow;
|
||||
|
||||
this.showWidgetActions = this.widgetContext.hideTitlePanel ? false : true;
|
||||
|
||||
this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : [];
|
||||
this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
|
||||
}
|
||||
|
||||
get x(): number {
|
||||
if (this.widgetLayout) {
|
||||
return this.widgetLayout.col;
|
||||
} else {
|
||||
return this.widget.col;
|
||||
}
|
||||
}
|
||||
|
||||
set x(x: number) {
|
||||
if (!this.dashboard.isMobileSize) {
|
||||
if (this.widgetLayout) {
|
||||
this.widgetLayout.col = x;
|
||||
} else {
|
||||
this.widget.col = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
if (this.widgetLayout) {
|
||||
return this.widgetLayout.row;
|
||||
} else {
|
||||
return this.widget.row;
|
||||
}
|
||||
}
|
||||
|
||||
set y(y: number) {
|
||||
if (!this.dashboard.isMobileSize) {
|
||||
if (this.widgetLayout) {
|
||||
this.widgetLayout.row = y;
|
||||
} else {
|
||||
this.widget.row = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get cols(): number {
|
||||
if (this.widgetLayout) {
|
||||
return this.widgetLayout.sizeX;
|
||||
} else {
|
||||
return this.widget.sizeX;
|
||||
}
|
||||
}
|
||||
|
||||
set cols(cols: number) {
|
||||
if (!this.dashboard.isMobileSize) {
|
||||
if (this.widgetLayout) {
|
||||
this.widgetLayout.sizeX = cols;
|
||||
} else {
|
||||
this.widget.sizeX = cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get rows(): number {
|
||||
if (this.dashboard.isMobileSize && !this.dashboard.options.mobileAutofillHeight) {
|
||||
let mobileHeight;
|
||||
if (this.widgetLayout) {
|
||||
mobileHeight = this.widgetLayout.mobileHeight;
|
||||
}
|
||||
if (!mobileHeight && this.widget.config.mobileHeight) {
|
||||
mobileHeight = this.widget.config.mobileHeight;
|
||||
}
|
||||
if (mobileHeight) {
|
||||
return mobileHeight;
|
||||
} else {
|
||||
return this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
|
||||
}
|
||||
} else {
|
||||
if (this.widgetLayout) {
|
||||
return this.widgetLayout.sizeY;
|
||||
} else {
|
||||
return this.widget.sizeY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set rows(rows: number) {
|
||||
if (!this.dashboard.isMobileSize && !this.dashboard.options.autofillHeight) {
|
||||
if (this.widgetLayout) {
|
||||
this.widgetLayout.sizeY = rows;
|
||||
} else {
|
||||
this.widget.sizeY = rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get widgetOrder(): number {
|
||||
let order;
|
||||
if (this.widgetLayout && isDefined(this.widgetLayout.mobileOrder) && this.widgetLayout.mobileOrder >= 0) {
|
||||
order = this.widgetLayout.mobileOrder;
|
||||
} else if (isDefined(this.widget.config.mobileOrder) && this.widget.config.mobileOrder >= 0) {
|
||||
order = this.widget.config.mobileOrder;
|
||||
} else if (this.widgetLayout) {
|
||||
order = this.widgetLayout.row;
|
||||
} else {
|
||||
order = this.widget.row;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@ -14,20 +14,24 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {BaseData} from '@shared/models/base-data';
|
||||
import {TenantId} from '@shared/models/id/tenant-id';
|
||||
import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id';
|
||||
import {WidgetTypeId} from '@shared/models/id/widget-type-id';
|
||||
|
||||
export interface WidgetTypeDescriptor {
|
||||
todo: Array<any>;
|
||||
// TODO:
|
||||
export interface IWidgetAction {
|
||||
icon: string;
|
||||
onAction: ($event: Event) => void;
|
||||
}
|
||||
|
||||
export interface WidgetType extends BaseData<WidgetTypeId> {
|
||||
tenantId: TenantId;
|
||||
bundleAlias: string;
|
||||
alias: string;
|
||||
export interface WidgetHeaderAction extends IWidgetAction {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface WidgetAction extends IWidgetAction {
|
||||
name: string;
|
||||
descriptor: WidgetTypeDescriptor;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetContext {
|
||||
widgetTitleTemplate?: string;
|
||||
hideTitlePanel?: boolean;
|
||||
widgetTitle?: string;
|
||||
customHeaderActions?: Array<WidgetHeaderAction>;
|
||||
widgetActions?: Array<WidgetAction>;
|
||||
}
|
||||
@ -14,13 +14,35 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import { Injectable, NgModule } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import {EntitiesTableComponent} from '../../components/entity/entities-table.component';
|
||||
import {Authority} from '@shared/models/authority.enum';
|
||||
import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
|
||||
import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
|
||||
import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
|
||||
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
|
||||
import { User } from '@shared/models/user.model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { UserService } from '@core/http/user.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
||||
import { WidgetService } from '@core/http/widget.service';
|
||||
|
||||
@Injectable()
|
||||
export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
|
||||
|
||||
constructor(private widgetsService: WidgetService) {
|
||||
}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
|
||||
const widgetsBundleId = route.params.widgetsBundleId;
|
||||
return this.widgetsService.getWidgetsBundle(widgetsBundleId);
|
||||
}
|
||||
}
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -42,6 +64,21 @@ const routes: Routes = [
|
||||
resolve: {
|
||||
entitiesTableConfig: WidgetsBundlesTableConfigResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':widgetsBundleId/widgetTypes',
|
||||
component: WidgetLibraryComponent,
|
||||
data: {
|
||||
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
|
||||
title: 'widget.widget-library',
|
||||
breadcrumb: {
|
||||
labelFunction: ((route, translate) => route.data.widgetsBundle.title),
|
||||
icon: 'now_widgets'
|
||||
} as BreadCrumbConfig
|
||||
},
|
||||
resolve: {
|
||||
widgetsBundle: WidgetsBundleResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -51,7 +88,8 @@ const routes: Routes = [
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
WidgetsBundlesTableConfigResolver
|
||||
WidgetsBundlesTableConfigResolver,
|
||||
WidgetsBundleResolver
|
||||
]
|
||||
})
|
||||
export class WidgetLibraryRoutingModule { }
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center"
|
||||
style="text-transform: uppercase; display: flex; z-index: 1;"
|
||||
class="tb-absolute-fill">
|
||||
<button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
|
||||
<mat-icon class="tb-mat-96">add</mat-icon>
|
||||
{{ 'widget.add-widget-type' | translate }}
|
||||
</button>
|
||||
<span translate *ngIf="isReadOnly"
|
||||
fxLayoutAlign="center center"
|
||||
style="text-transform: uppercase; display: flex;"
|
||||
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
|
||||
</section>
|
||||
<tb-dashboard [options]="dashboardOptions"></tb-dashboard>
|
||||
<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
|
||||
</tb-footer-fab-buttons>
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright © 2016-2019 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
:host {
|
||||
button.tb-add-new-widget {
|
||||
padding-right: 12px;
|
||||
font-size: 24px;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { AuthUser } from '@shared/models/user.model';
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Authority } from '@shared/models/authority.enum';
|
||||
import { NULL_UUID } from '@shared/models/id/has-uuid';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models';
|
||||
import { WidgetService } from '@core/http/widget.service';
|
||||
import { map, share } from 'rxjs/operators';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
|
||||
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
|
||||
import { DashboardConfig } from '@home/models/dashboard-component.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-widget-library',
|
||||
templateUrl: './widget-library.component.html',
|
||||
styleUrls: ['./widget-library.component.scss']
|
||||
})
|
||||
export class WidgetLibraryComponent extends PageComponent implements OnInit {
|
||||
|
||||
authUser: AuthUser;
|
||||
|
||||
isReadOnly: boolean;
|
||||
|
||||
widgetsBundle: WidgetsBundle;
|
||||
|
||||
widgetTypes$: Observable<Array<Widget>>;
|
||||
|
||||
footerFabButtons: FooterFabButtons = {
|
||||
fabTogglerName: 'widget.add-widget-type',
|
||||
fabTogglerIcon: 'add',
|
||||
buttons: [
|
||||
{
|
||||
name: 'widget-type.create-new-widget-type',
|
||||
icon: 'insert_drive_file',
|
||||
onAction: ($event) => {
|
||||
this.addWidgetType($event);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'widget-type.import',
|
||||
icon: 'file_upload',
|
||||
onAction: ($event) => {
|
||||
this.importWidgetType($event);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
dashboardOptions: DashboardConfig = new DashboardConfig();
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private route: ActivatedRoute,
|
||||
private widgetService: WidgetService,
|
||||
private dialogService: DialogService) {
|
||||
super(store);
|
||||
this.dashboardOptions.isEdit = false;
|
||||
this.dashboardOptions.isEditActionEnabled = true;
|
||||
this.dashboardOptions.isExportActionEnabled = true;
|
||||
this.dashboardOptions.onEditWidget = ($event, widget) => { this.openWidgetType($event, widget); };
|
||||
this.dashboardOptions.onExportWidget = ($event, widget) => { this.exportWidgetType($event, widget); };
|
||||
this.dashboardOptions.onRemoveWidget = ($event, widget) => { this.removeWidgetType($event, widget); };
|
||||
|
||||
this.authUser = getCurrentAuthUser(store);
|
||||
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
|
||||
if (this.authUser.authority === Authority.TENANT_ADMIN) {
|
||||
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
|
||||
} else {
|
||||
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
|
||||
}
|
||||
this.dashboardOptions.isRemoveActionEnabled = !this.isReadOnly;
|
||||
this.loadWidgetTypes();
|
||||
this.dashboardOptions.widgetsData = this.widgetTypes$.pipe(
|
||||
map(widgets => ({ widgets })));
|
||||
}
|
||||
|
||||
loadWidgetTypes() {
|
||||
const bundleAlias = this.widgetsBundle.alias;
|
||||
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
|
||||
this.widgetTypes$ = this.widgetService.getBundleWidgetTypes(bundleAlias,
|
||||
isSystem).pipe(
|
||||
map((types) => {
|
||||
types = types.sort((a, b) => {
|
||||
let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
|
||||
if (result === 0) {
|
||||
result = b.createdTime - a.createdTime;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const widgetTypes = new Array<Widget>(types.length);
|
||||
let top = 0;
|
||||
const lastTop = [0, 0, 0];
|
||||
let col = 0;
|
||||
let column = 0;
|
||||
types.forEach((type) => {
|
||||
const widgetTypeInfo = toWidgetInfo(type);
|
||||
const sizeX = 8;
|
||||
const sizeY = Math.floor(widgetTypeInfo.sizeY);
|
||||
const widget: Widget = {
|
||||
typeId: type.id,
|
||||
isSystemType: isSystem,
|
||||
bundleAlias,
|
||||
typeAlias: widgetTypeInfo.alias,
|
||||
type: widgetTypeInfo.type,
|
||||
title: widgetTypeInfo.widgetName,
|
||||
sizeX,
|
||||
sizeY,
|
||||
row: top,
|
||||
col,
|
||||
config: JSON.parse(widgetTypeInfo.defaultConfig)
|
||||
};
|
||||
|
||||
widget.config.title = widgetTypeInfo.widgetName;
|
||||
|
||||
widgetTypes.push(widget);
|
||||
top += sizeY;
|
||||
if (top > lastTop[column] + 10) {
|
||||
lastTop[column] = top;
|
||||
column++;
|
||||
if (column > 2) {
|
||||
column = 0;
|
||||
}
|
||||
top = lastTop[column];
|
||||
col = column * 8;
|
||||
}
|
||||
});
|
||||
return widgetTypes;
|
||||
}
|
||||
),
|
||||
share());
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
addWidgetType($event: Event): void {
|
||||
this.openWidgetType($event);
|
||||
}
|
||||
|
||||
importWidgetType($event: Event): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.dialogService.todo();
|
||||
}
|
||||
|
||||
openWidgetType($event: Event, widget?: Widget): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (widget) {
|
||||
this.dialogService.todo();
|
||||
} else {
|
||||
this.dialogService.todo();
|
||||
}
|
||||
}
|
||||
|
||||
exportWidgetType($event: Event, widget: Widget): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.dialogService.todo();
|
||||
}
|
||||
|
||||
removeWidgetType($event: Event, widget: Widget): void {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.dialogService.todo();
|
||||
}
|
||||
|
||||
}
|
||||
@ -20,13 +20,15 @@ import {SharedModule} from '@shared/shared.module';
|
||||
import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.component';
|
||||
import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
|
||||
import {HomeComponentsModule} from '@modules/home/components/home-components.module';
|
||||
import { WidgetLibraryComponent } from './widget-library.component';
|
||||
|
||||
@NgModule({
|
||||
entryComponents: [
|
||||
WidgetsBundleComponent
|
||||
],
|
||||
declarations: [
|
||||
WidgetsBundleComponent
|
||||
WidgetsBundleComponent,
|
||||
WidgetLibraryComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@ -135,9 +135,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
// TODO:
|
||||
// this.router.navigateByUrl(`customers/${customer.id.id}/users`);
|
||||
this.dialogService.todo();
|
||||
this.router.navigateByUrl(`widgets-bundles/${widgetsBundle.id.id}/widgetTypes`);
|
||||
}
|
||||
|
||||
exportWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {
|
||||
animate,
|
||||
keyframes,
|
||||
query,
|
||||
stagger,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger
|
||||
} from '@angular/animations';
|
||||
|
||||
export const speedDialFabAnimations = [
|
||||
trigger('fabToggler', [
|
||||
state('inactive', style({
|
||||
transform: 'rotate(0deg)'
|
||||
})),
|
||||
state('active', style({
|
||||
transform: 'rotate(225deg)'
|
||||
})),
|
||||
transition('* <=> *', animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
||||
]),
|
||||
trigger('speedDialStagger', [
|
||||
transition('* => *', [
|
||||
|
||||
query(':enter', style({ opacity: 0 }), {optional: true}),
|
||||
|
||||
query(':enter', stagger('40ms',
|
||||
[
|
||||
animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
|
||||
keyframes(
|
||||
[
|
||||
style({opacity: 0, transform: 'translateY(10px)'}),
|
||||
style({opacity: 1, transform: 'translateY(0)'}),
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
), {optional: true}),
|
||||
|
||||
query(':leave',
|
||||
animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
|
||||
keyframes([
|
||||
style({opacity: 1}),
|
||||
style({opacity: 0}),
|
||||
])
|
||||
), {optional: true}
|
||||
)
|
||||
|
||||
])
|
||||
])
|
||||
];
|
||||
@ -16,7 +16,9 @@
|
||||
|
||||
-->
|
||||
<div fxFlex class="tb-breadcrumb" fxLayout="row">
|
||||
<h1 fxFlex fxHide.gt-sm>{{ (lastBreadcrumb$ | async).label | translate }}</h1>
|
||||
<h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
|
||||
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
|
||||
</h1>
|
||||
<span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast">
|
||||
<a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
|
||||
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
|
||||
@ -24,7 +26,7 @@
|
||||
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
|
||||
{{ breadcrumb.icon }}
|
||||
</mat-icon>
|
||||
{{ breadcrumb.label | translate }}
|
||||
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
|
||||
</a>
|
||||
<span *ngSwitchCase="true">
|
||||
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
|
||||
@ -32,7 +34,7 @@
|
||||
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
|
||||
{{ breadcrumb.icon }}
|
||||
</mat-icon>
|
||||
{{ breadcrumb.label | translate }}
|
||||
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
|
||||
</span>
|
||||
<span class="divider" [fxHide]="isLast"> > </span>
|
||||
</span>
|
||||
|
||||
@ -16,9 +16,10 @@
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { BreadCrumb } from './breadcrumb';
|
||||
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
|
||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: '[tb-breadcrumb]',
|
||||
@ -40,7 +41,8 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
constructor(private router: Router,
|
||||
private activatedRoute: ActivatedRoute) {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private translate: TranslateService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -56,15 +58,24 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
|
||||
buildBreadCrumbs(route: ActivatedRouteSnapshot, breadcrumbs: Array<BreadCrumb> = []): Array<BreadCrumb> {
|
||||
let newBreadcrumbs = breadcrumbs;
|
||||
if (route.routeConfig && route.routeConfig.data) {
|
||||
const breadcrumbData = route.routeConfig.data.breadcrumb;
|
||||
if (breadcrumbData && !breadcrumbData.skip) {
|
||||
const label = breadcrumbData.label || 'home.home';
|
||||
const icon = breadcrumbData.icon || 'home';
|
||||
const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig;
|
||||
if (breadcrumbConfig && !breadcrumbConfig.skip) {
|
||||
let label;
|
||||
let ignoreTranslate;
|
||||
if (breadcrumbConfig.labelFunction) {
|
||||
label = breadcrumbConfig.labelFunction(route, this.translate);
|
||||
ignoreTranslate = true;
|
||||
} else {
|
||||
label = breadcrumbConfig.label || 'home.home';
|
||||
ignoreTranslate = false;
|
||||
}
|
||||
const icon = breadcrumbConfig.icon || 'home';
|
||||
const isMdiIcon = icon.startsWith('mdi:');
|
||||
const link = [ '/' + route.url.join('') ];
|
||||
const queryParams = route.queryParams;
|
||||
const breadcrumb = {
|
||||
label,
|
||||
ignoreTranslate,
|
||||
icon,
|
||||
isMdiIcon,
|
||||
link,
|
||||
|
||||
@ -14,13 +14,23 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Params } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, Params } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
export interface BreadCrumb {
|
||||
label: string;
|
||||
ignoreTranslate: boolean;
|
||||
icon: string;
|
||||
isMdiIcon: boolean;
|
||||
link: any[];
|
||||
queryParams: Params;
|
||||
}
|
||||
|
||||
export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService) => string;
|
||||
|
||||
export interface BreadCrumbConfig {
|
||||
labelFunction: BreadCrumbLabelFunction;
|
||||
label: string;
|
||||
icon: string;
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<section fxLayout="row" class="layout-wrap tb-footer-buttons">
|
||||
<div class="fab-container">
|
||||
<button [disabled]="isLoading$ | async"
|
||||
mat-fab class="fab-toggler tb-btn-footer"
|
||||
color="accent"
|
||||
matTooltip="{{ footerFabButtons.fabTogglerName | translate }}"
|
||||
matTooltipPosition="above"
|
||||
(click)="onToggleFab()">
|
||||
<mat-icon [@fabToggler]="{value: fabTogglerState}">{{ footerFabButtons.fabTogglerIcon }}</mat-icon>
|
||||
</button>
|
||||
<div [@speedDialStagger]="buttons.length">
|
||||
<button *ngFor="let btn of buttons"
|
||||
mat-fab
|
||||
color="accent"
|
||||
matTooltip="{{ btn.name | translate }}"
|
||||
matTooltipPosition="above"
|
||||
(click)="btn.onAction($event)">
|
||||
<mat-icon>{{btn.icon}}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright © 2016-2019 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
:host {
|
||||
section.tb-footer-buttons {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
|
||||
.fab-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
button {
|
||||
margin-bottom: 17px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tb-btn-footer {
|
||||
position: relative !important;
|
||||
display: inline-block !important;
|
||||
animation: tbMoveFromBottomFade .3s ease both;
|
||||
|
||||
&.tb-hide {
|
||||
animation: tbMoveToBottomFade .3s ease both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, Input, HostListener } from '@angular/core';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
|
||||
|
||||
export interface FooterFabButton {
|
||||
name: string;
|
||||
icon: string;
|
||||
onAction: ($event: Event) => void;
|
||||
}
|
||||
|
||||
export interface FooterFabButtons {
|
||||
fabTogglerName: string;
|
||||
fabTogglerIcon: string;
|
||||
buttons: Array<FooterFabButton>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tb-footer-fab-buttons',
|
||||
templateUrl: './footer-fab-buttons.component.html',
|
||||
styleUrls: ['./footer-fab-buttons.component.scss'],
|
||||
animations: speedDialFabAnimations
|
||||
})
|
||||
export class FooterFabButtonsComponent extends PageComponent {
|
||||
|
||||
@Input()
|
||||
footerFabButtons: FooterFabButtons;
|
||||
|
||||
buttons: Array<FooterFabButton> = [];
|
||||
fabTogglerState = 'inactive';
|
||||
|
||||
closeTimeout = null;
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
onFocusOut($event) {
|
||||
if (!this.closeTimeout) {
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
this.hideItems();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('focusin', ['$event'])
|
||||
onFocusIn($event) {
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
this.closeTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
showItems() {
|
||||
this.fabTogglerState = 'active';
|
||||
this.buttons = this.footerFabButtons.buttons;
|
||||
}
|
||||
|
||||
hideItems() {
|
||||
this.fabTogglerState = 'inactive';
|
||||
this.buttons = [];
|
||||
}
|
||||
|
||||
onToggleFab() {
|
||||
this.buttons.length ? this.hideItems() : this.showItems();
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
|
||||
set min(min: number) {
|
||||
if (typeof min !== 'undefined' && min !== this.minValue) {
|
||||
this.minValue = min;
|
||||
this.maxValue = Math.max(this.maxValue, this.minValue);
|
||||
this.updateView();
|
||||
}
|
||||
}
|
||||
@ -54,6 +55,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
|
||||
set max(max: number) {
|
||||
if (typeof max !== 'undefined' && max !== this.maxValue) {
|
||||
this.maxValue = max;
|
||||
this.minValue = Math.min(this.minValue, this.maxValue);
|
||||
this.updateView();
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
:host {
|
||||
min-width: 52px;
|
||||
section.tb-timewindow {
|
||||
min-height: 32px;
|
||||
padding: 0 6px;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@ -25,6 +25,19 @@ export interface DashboardInfo extends BaseData<DashboardId> {
|
||||
assignedCustomers: Array<ShortCustomerInfo>;
|
||||
}
|
||||
|
||||
export interface WidgetLayout {
|
||||
sizeX: number;
|
||||
sizeY: number;
|
||||
mobileHeight: number;
|
||||
mobileOrder: number;
|
||||
col: number;
|
||||
row: number;
|
||||
}
|
||||
|
||||
export interface WidgetLayouts {
|
||||
[id: string]: WidgetLayout;
|
||||
}
|
||||
|
||||
export interface DashboardConfiguration {
|
||||
[key: string]: any;
|
||||
// TODO:
|
||||
|
||||
171
ui-ngx/src/app/shared/models/widget.models.ts
Normal file
171
ui-ngx/src/app/shared/models/widget.models.ts
Normal file
@ -0,0 +1,171 @@
|
||||
///
|
||||
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {BaseData} from '@shared/models/base-data';
|
||||
import {TenantId} from '@shared/models/id/tenant-id';
|
||||
import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id';
|
||||
import {WidgetTypeId} from '@shared/models/id/widget-type-id';
|
||||
import { AliasEntityType, EntityType, EntityTypeTranslation } from '@shared/models/entity-type.models';
|
||||
import { Timewindow } from '@shared/models/time/time.models';
|
||||
|
||||
export enum widgetType {
|
||||
timeseries = 'timeseries',
|
||||
latest = 'latest',
|
||||
rpc = 'rpc',
|
||||
alarm = 'alarm'
|
||||
}
|
||||
|
||||
export interface WidgetTypeTemplate {
|
||||
bundleAlias: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export interface WidgetTypeData {
|
||||
name: string;
|
||||
template: WidgetTypeTemplate;
|
||||
}
|
||||
|
||||
export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
|
||||
[
|
||||
[
|
||||
widgetType.timeseries,
|
||||
{
|
||||
name: 'widget.timeseries',
|
||||
template: {
|
||||
bundleAlias: 'charts',
|
||||
alias: 'basic_timeseries'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
widgetType.latest,
|
||||
{
|
||||
name: 'widget.latest-values',
|
||||
template: {
|
||||
bundleAlias: 'cards',
|
||||
alias: 'attributes_card'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
widgetType.rpc,
|
||||
{
|
||||
name: 'widget.rpc',
|
||||
template: {
|
||||
bundleAlias: 'gpio_widgets',
|
||||
alias: 'basic_gpio_control'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
widgetType.alarm,
|
||||
{
|
||||
name: 'widget.alarm',
|
||||
template: {
|
||||
bundleAlias: 'alarm_widgets',
|
||||
alias: 'alarms_table'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
export interface WidgetResource {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface WidgetTypeDescriptor {
|
||||
type: widgetType;
|
||||
resources: Array<WidgetResource>;
|
||||
templateHtml: string;
|
||||
templateCss: string;
|
||||
controllerScript: string;
|
||||
settingsSchema: string;
|
||||
dataKeySettingsSchema: string;
|
||||
defaultConfig: string;
|
||||
sizeX: number;
|
||||
sizeY: number;
|
||||
}
|
||||
|
||||
export interface WidgetType extends BaseData<WidgetTypeId> {
|
||||
tenantId: TenantId;
|
||||
bundleAlias: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
descriptor: WidgetTypeDescriptor;
|
||||
}
|
||||
|
||||
export interface WidgetInfo extends WidgetTypeDescriptor {
|
||||
widgetName: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
|
||||
return {
|
||||
widgetName: widgetTypeEntity.name,
|
||||
alias: widgetTypeEntity.alias,
|
||||
type: widgetTypeEntity.descriptor.type,
|
||||
sizeX: widgetTypeEntity.descriptor.sizeX,
|
||||
sizeY: widgetTypeEntity.descriptor.sizeY,
|
||||
resources: widgetTypeEntity.descriptor.resources,
|
||||
templateHtml: widgetTypeEntity.descriptor.templateHtml,
|
||||
templateCss: widgetTypeEntity.descriptor.templateCss,
|
||||
controllerScript: widgetTypeEntity.descriptor.controllerScript,
|
||||
settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
|
||||
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
|
||||
defaultConfig: widgetTypeEntity.descriptor.defaultConfig
|
||||
};
|
||||
}
|
||||
|
||||
export interface WidgetConfig {
|
||||
title?: string;
|
||||
titleIcon?: string;
|
||||
showTitle?: boolean;
|
||||
showTitleIcon?: boolean;
|
||||
iconColor?: string;
|
||||
iconSize?: number;
|
||||
dropShadow?: boolean;
|
||||
enableFullscreen?: boolean;
|
||||
useDashboardTimewindow?: boolean;
|
||||
displayTimewindow?: boolean;
|
||||
timewindow?: Timewindow;
|
||||
mobileHeight?: number;
|
||||
mobileOrder?: number;
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
padding?: string;
|
||||
margin?: string;
|
||||
widgetStyle?: {[klass: string]: any};
|
||||
titleStyle?: {[klass: string]: any};
|
||||
[key: string]: any;
|
||||
|
||||
// TODO:
|
||||
}
|
||||
|
||||
export interface Widget {
|
||||
id?: string;
|
||||
typeId: WidgetTypeId;
|
||||
isSystemType: boolean;
|
||||
bundleAlias: string;
|
||||
typeAlias: string;
|
||||
type: widgetType;
|
||||
title: string;
|
||||
sizeX: number;
|
||||
sizeY: number;
|
||||
row: number;
|
||||
col: number;
|
||||
config: WidgetConfig;
|
||||
}
|
||||
@ -52,6 +52,7 @@ import {
|
||||
MatTooltipModule
|
||||
} from '@angular/material';
|
||||
import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core';
|
||||
import {GridsterModule} from 'angular-gridster2';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {RouterModule} from '@angular/router';
|
||||
@ -86,6 +87,7 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
|
||||
import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
|
||||
import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
|
||||
import { JsonObjectEditComponent } from './components/json-object-edit.component';
|
||||
import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
@ -102,6 +104,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
|
||||
declarations: [
|
||||
FooterComponent,
|
||||
LogoComponent,
|
||||
FooterFabButtonsComponent,
|
||||
ToastDirective,
|
||||
FullscreenDirective,
|
||||
TbAnchorComponent,
|
||||
@ -167,6 +170,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
|
||||
MatStepperModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
GridsterModule,
|
||||
ClipboardModule,
|
||||
FlexLayoutModule.withConfig({addFlexToParent: false}),
|
||||
FormsModule,
|
||||
@ -177,6 +181,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
|
||||
exports: [
|
||||
FooterComponent,
|
||||
LogoComponent,
|
||||
FooterFabButtonsComponent,
|
||||
ToastDirective,
|
||||
FullscreenDirective,
|
||||
TbAnchorComponent,
|
||||
@ -232,6 +237,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
|
||||
MatStepperModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
GridsterModule,
|
||||
ClipboardModule,
|
||||
FlexLayoutModule,
|
||||
FormsModule,
|
||||
|
||||
@ -351,6 +351,16 @@ $tb-dark-theme: get-tb-dark-theme(
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
&.tb-mat-20 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: scale(0.83);
|
||||
}
|
||||
}
|
||||
&.tb-mat-32 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
41
ui/package-lock.json
generated
41
ui/package-lock.json
generated
@ -6075,7 +6075,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@ -6096,12 +6097,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -6116,17 +6119,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -6243,7 +6249,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -6255,6 +6262,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -6269,6 +6277,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -6276,12 +6285,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@ -6300,6 +6311,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@ -6380,7 +6392,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -6392,6 +6405,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -6477,7 +6491,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@ -6513,6 +6528,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -6532,6 +6548,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@ -6575,12 +6592,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user