Dashboard component implementation.

This commit is contained in:
Igor Kulikov 2019-09-03 19:31:16 +03:00
parent 22d8ce7189
commit 2e7070a903
32 changed files with 1753 additions and 47 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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
);
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 { }

View File

@ -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,

View 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;
}
}

View File

@ -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>;
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -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}
)
])
])
];

View File

@ -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>

View File

@ -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,

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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:

View 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;
}

View File

@ -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,

View File

@ -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
View File

@ -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
}
}
},