52 changed files with 2123 additions and 268 deletions
@ -0,0 +1,40 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>{{ title }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button |
||||
|
[mat-dialog-close]="false" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content fxLayout="column"> |
||||
|
<div #eventContentEditor class="tb-event-content"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
[mat-dialog-close]="false" cdkFocusInitial> |
||||
|
{{ 'action.close' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.tb-event-content { |
||||
|
width: 100%; |
||||
|
min-width: 400px; |
||||
|
height: 100%; |
||||
|
min-height: 50px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
///
|
||||
|
/// 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, ElementRef, Inject, OnInit, Renderer2, ViewChild } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
|
||||
|
import * as ace from 'ace-builds'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { ContentType, contentTypesMap } from '@shared/models/constants'; |
||||
|
|
||||
|
export interface EventContentDialogData { |
||||
|
content: string; |
||||
|
title: string; |
||||
|
contentType: ContentType; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-event-content-dialog', |
||||
|
templateUrl: './event-content-dialog.component.html', |
||||
|
styleUrls: ['./event-content-dialog.component.scss'] |
||||
|
}) |
||||
|
export class EventContentDialogComponent extends DialogComponent<EventContentDialogData> implements OnInit { |
||||
|
|
||||
|
@ViewChild('eventContentEditor', {static: true}) |
||||
|
eventContentEditorElmRef: ElementRef; |
||||
|
private eventContentEditor: ace.Ace.Editor; |
||||
|
|
||||
|
content: string; |
||||
|
title: string; |
||||
|
contentType: ContentType; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EventContentDialogData, |
||||
|
public dialogRef: MatDialogRef<EventContentDialogComponent>, |
||||
|
private renderer: Renderer2) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.content = this.data.content; |
||||
|
this.title = this.data.title; |
||||
|
this.contentType = this.data.contentType; |
||||
|
|
||||
|
this.eventContentEditor = this.createEditor(this.eventContentEditorElmRef, this.content); |
||||
|
} |
||||
|
|
||||
|
createEditor(editorElementRef: ElementRef, content: string): ace.Ace.Editor { |
||||
|
const editorElement = editorElementRef.nativeElement; |
||||
|
let mode = 'java'; |
||||
|
if (this.contentType) { |
||||
|
mode = contentTypesMap.get(this.contentType).code; |
||||
|
if (this.contentType === ContentType.JSON && content) { |
||||
|
content = js_beautify(content, {indent_size: 4}); |
||||
|
} |
||||
|
} |
||||
|
let editorOptions: Partial<ace.Ace.EditorOptions> = { |
||||
|
mode: `ace/mode/${mode}`, |
||||
|
theme: 'ace/theme/github', |
||||
|
showGutter: false, |
||||
|
showPrintMargin: false, |
||||
|
readOnly: true |
||||
|
}; |
||||
|
|
||||
|
const advancedOptions = { |
||||
|
enableSnippets: false, |
||||
|
enableBasicAutocompletion: false, |
||||
|
enableLiveAutocompletion: false |
||||
|
}; |
||||
|
|
||||
|
editorOptions = {...editorOptions, ...advancedOptions}; |
||||
|
const editor = ace.edit(editorElement, editorOptions); |
||||
|
editor.session.setUseWrapMode(false); |
||||
|
editor.setValue(content, -1); |
||||
|
this.updateEditorSize(editorElement, content, editor); |
||||
|
return editor; |
||||
|
} |
||||
|
|
||||
|
updateEditorSize(editorElement: any, content: string, editor: ace.Ace.Editor) { |
||||
|
let newHeight = 400; |
||||
|
let newWidth = 600; |
||||
|
if (content && content.length > 0) { |
||||
|
const lines = content.split('\n'); |
||||
|
newHeight = 16 * lines.length + 16; |
||||
|
let maxLineLength = 0; |
||||
|
lines.forEach((row) => { |
||||
|
const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); |
||||
|
const lineLength = line.length; |
||||
|
maxLineLength = Math.max(maxLineLength, lineLength); |
||||
|
}); |
||||
|
newWidth = 8 * maxLineLength + 16; |
||||
|
} |
||||
|
// newHeight = Math.min(400, newHeight);
|
||||
|
this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); |
||||
|
this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); |
||||
|
this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); |
||||
|
editor.resize(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="column" class="mat-content mat-padding"> |
||||
|
<label class="tb-title" translate>entity.columns-to-display</label> |
||||
|
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="column.display" *ngFor="let column of columns" |
||||
|
(ngModelChange)="update()"> |
||||
|
{{ column.title }} |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-width: 300px; |
||||
|
overflow: hidden; |
||||
|
background: #fff; |
||||
|
border-radius: 4px; |
||||
|
box-shadow: |
||||
|
0 7px 8px -4px rgba(0, 0, 0, .2), |
||||
|
0 13px 19px 2px rgba(0, 0, 0, .14), |
||||
|
0 5px 24px 4px rgba(0, 0, 0, .12); |
||||
|
|
||||
|
.mat-content { |
||||
|
overflow: hidden; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
.mat-padding { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
///
|
||||
|
/// 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, Inject, InjectionToken } from '@angular/core'; |
||||
|
import { DisplayColumn } from '@home/components/widget/lib/table-widget.models'; |
||||
|
|
||||
|
export const DISPLAY_COLUMNS_PANEL_DATA = new InjectionToken<any>('DisplayColumnsPanelData'); |
||||
|
|
||||
|
export interface DisplayColumnsPanelData { |
||||
|
columns: DisplayColumn[]; |
||||
|
columnsUpdated: (columns: DisplayColumn[]) => void; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-display-columns-panel', |
||||
|
templateUrl: './display-columns-panel.component.html', |
||||
|
styleUrls: ['./display-columns-panel.component.scss'] |
||||
|
}) |
||||
|
export class DisplayColumnsPanelComponent { |
||||
|
|
||||
|
columns: DisplayColumn[]; |
||||
|
|
||||
|
constructor(@Inject(DISPLAY_COLUMNS_PANEL_DATA) public data: DisplayColumnsPanelData) { |
||||
|
this.columns = this.data.columns; |
||||
|
} |
||||
|
|
||||
|
public update() { |
||||
|
this.data.columnsUpdated(this.columns); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 class="tb-entity-table tb-absolute-fill"> |
||||
|
<div fxFlex fxLayout="column" class="tb-absolute-fill"> |
||||
|
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<button mat-button mat-icon-button |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label> </mat-label> |
||||
|
<input #searchInput matInput |
||||
|
[(ngModel)]="pageLink.textSearch" |
||||
|
placeholder="{{ 'entity.search' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-button mat-icon-button (click)="exitFilterMode()" |
||||
|
matTooltip="{{ 'action.close' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<div fxFlex class="table-container"> |
||||
|
<mat-table [dataSource]="entityDatasource" |
||||
|
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
||||
|
<ng-container [matColumnDef]="column.label" *ngFor="let column of columns; trackBy: trackByColumnLabel;"> |
||||
|
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let entity;" |
||||
|
[innerHTML]="cellContent(entity, column)" |
||||
|
[ngStyle]="cellStyle(entity, column)"> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (actionCellDescriptors.length * 36) + 'px', maxWidth: (actionCellDescriptors.length * 36) + 'px' }"> |
||||
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
*ngFor="let actionDescriptor of actionCellDescriptors" |
||||
|
matTooltip="{{ actionDescriptor.displayName }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="onActionButtonClick($event, entity, actionDescriptor)"> |
||||
|
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div fxHide fxShow.lt-lg> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="$event.stopPropagation(); ctx.detectChanges();" |
||||
|
[matMenuTriggerFor]="cellActionsMenu"> |
||||
|
<mat-icon class="material-icons">more_vert</mat-icon> |
||||
|
</button> |
||||
|
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
||||
|
<button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
(click)="onActionButtonClick($event, entity, actionDescriptor)"> |
||||
|
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
||||
|
<span>{{ actionDescriptor.displayName }}</span> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}" |
||||
|
*matRowDef="let entity; columns: displayedColumns;" |
||||
|
(click)="onRowClick($event, entity)"></mat-row> |
||||
|
</mat-table> |
||||
|
<span [fxShow]="entityDatasource.isEmpty() | async" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate>entity.no-entities-prompt</span> |
||||
|
</div> |
||||
|
<mat-divider *ngIf="displayPagination"></mat-divider> |
||||
|
<mat-paginator *ngIf="displayPagination" |
||||
|
[length]="entityDatasource.total() | async" |
||||
|
[pageIndex]="pageLink.page" |
||||
|
[pageSize]="pageLink.pageSize" |
||||
|
[pageSizeOptions]="pageSizeOptions"></mat-paginator> |
||||
|
</div> |
||||
|
</div> |
||||
@ -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. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
.tb-entity-table { |
||||
|
.mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar { |
||||
|
background: transparent; |
||||
|
} |
||||
|
mat-toolbar { |
||||
|
height: 39px; |
||||
|
max-height: 39px; |
||||
|
.mat-toolbar-tools { |
||||
|
height: 39px; |
||||
|
max-height: 39px; |
||||
|
} |
||||
|
} |
||||
|
.table-container { |
||||
|
overflow: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow { |
||||
|
opacity: 1 !important; |
||||
|
} |
||||
@ -0,0 +1,710 @@ |
|||||
|
///
|
||||
|
/// 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 { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
DataKey, |
||||
|
Datasource, |
||||
|
DatasourceData, |
||||
|
DatasourceType, |
||||
|
WidgetActionDescriptor, |
||||
|
WidgetConfig |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { IWidgetSubscription } from '@core/api/widget-api.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { deepClone, isDefined, isNumber } from '@core/utils'; |
||||
|
import * as tinycolor_ from 'tinycolor2'; |
||||
|
import cssjs from '@core/css/css'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; |
||||
|
import { DataSource } from '@angular/cdk/typings/collections'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { CollectionViewer } from '@angular/cdk/collections'; |
||||
|
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; |
||||
|
import { MatPaginator } from '@angular/material/paginator'; |
||||
|
import { MatSort } from '@angular/material/sort'; |
||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
||||
|
import { |
||||
|
CellContentInfo, |
||||
|
CellStyleInfo, |
||||
|
DisplayColumn, |
||||
|
EntityColumn, |
||||
|
EntityData, |
||||
|
getEntityValue |
||||
|
} from '@home/components/widget/lib/table-widget.models'; |
||||
|
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
||||
|
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; |
||||
|
import { |
||||
|
DISPLAY_COLUMNS_PANEL_DATA, |
||||
|
DisplayColumnsPanelComponent, |
||||
|
DisplayColumnsPanelData |
||||
|
} from '@home/components/widget/lib/display-columns-panel.component'; |
||||
|
|
||||
|
const tinycolor = tinycolor_; |
||||
|
|
||||
|
interface EntitiesTableWidgetSettings { |
||||
|
entitiesTitle: string; |
||||
|
enableSearch: boolean; |
||||
|
enableSelectColumnDisplay: boolean; |
||||
|
displayEntityName: boolean; |
||||
|
entityNameColumnTitle: string; |
||||
|
displayEntityType: boolean; |
||||
|
displayPagination: boolean; |
||||
|
defaultPageSize: number; |
||||
|
defaultSortOrder: string; |
||||
|
} |
||||
|
|
||||
|
interface EntitiesTableDataKeySettings { |
||||
|
columnWidth: string; |
||||
|
useCellStyleFunction: boolean; |
||||
|
cellStyleFunction: string; |
||||
|
useCellContentFunction: boolean; |
||||
|
cellContentFunction: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entities-table-widget', |
||||
|
templateUrl: './entities-table-widget.component.html', |
||||
|
styleUrls: ['./entities-table-widget.component.scss'] |
||||
|
}) |
||||
|
export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
||||
|
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; |
||||
|
@ViewChild(MatSort, {static: false}) sort: MatSort; |
||||
|
|
||||
|
public displayPagination = true; |
||||
|
public pageSizeOptions; |
||||
|
public pageLink: PageLink; |
||||
|
public textSearchMode = false; |
||||
|
public columns: Array<EntityColumn> = []; |
||||
|
public displayedColumns: string[] = []; |
||||
|
public actionCellDescriptors: WidgetActionDescriptor[]; |
||||
|
public entityDatasource: EntityDatasource; |
||||
|
|
||||
|
private settings: EntitiesTableWidgetSettings; |
||||
|
private widgetConfig: WidgetConfig; |
||||
|
private subscription: IWidgetSubscription; |
||||
|
|
||||
|
private defaultPageSize = 10; |
||||
|
private defaultSortOrder = 'entityName'; |
||||
|
|
||||
|
private contentsInfo: {[key: string]: CellContentInfo} = {}; |
||||
|
private stylesInfo: {[key: string]: CellStyleInfo} = {}; |
||||
|
private columnWidth: {[key: string]: string} = {}; |
||||
|
|
||||
|
private searchAction: WidgetAction = { |
||||
|
name: 'action.search', |
||||
|
show: true, |
||||
|
icon: 'search', |
||||
|
onAction: () => { |
||||
|
this.enterFilterMode(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private columnDisplayAction: WidgetAction = { |
||||
|
name: 'entity.columns-to-display', |
||||
|
show: true, |
||||
|
icon: 'view_column', |
||||
|
onAction: ($event) => { |
||||
|
this.editColumnsToDisplay($event); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private elementRef: ElementRef, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private utils: UtilsService, |
||||
|
private translate: TranslateService, |
||||
|
private domSanitizer: DomSanitizer) { |
||||
|
super(store); |
||||
|
|
||||
|
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); |
||||
|
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.entitiesTableWidget = this; |
||||
|
this.settings = this.ctx.settings; |
||||
|
this.widgetConfig = this.ctx.widgetConfig; |
||||
|
this.subscription = this.ctx.defaultSubscription; |
||||
|
this.initializeConfig(); |
||||
|
this.updateDatasources(); |
||||
|
this.ctx.updateWidgetParams(); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
fromEvent(this.searchInputField.nativeElement, 'keyup') |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged(), |
||||
|
tap(() => { |
||||
|
if (this.displayPagination) { |
||||
|
this.paginator.pageIndex = 0; |
||||
|
} |
||||
|
this.updateData(); |
||||
|
}) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
|
||||
|
if (this.displayPagination) { |
||||
|
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
||||
|
} |
||||
|
(this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) |
||||
|
.pipe( |
||||
|
tap(() => this.updateData()) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
this.updateData(); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
this.entityDatasource.updateEntitiesData(this.subscription.data); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
private initializeConfig() { |
||||
|
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; |
||||
|
|
||||
|
let entitiesTitle: string; |
||||
|
|
||||
|
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) { |
||||
|
entitiesTitle = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle); |
||||
|
} else { |
||||
|
entitiesTitle = this.translate.instant('entity.entities'); |
||||
|
} |
||||
|
|
||||
|
const datasource = this.subscription.datasources[0]; |
||||
|
this.ctx.widgetTitle = this.utils.createLabelFromDatasource(datasource, entitiesTitle); |
||||
|
|
||||
|
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; |
||||
|
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; |
||||
|
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; |
||||
|
|
||||
|
const pageSize = this.settings.defaultPageSize; |
||||
|
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { |
||||
|
this.defaultPageSize = pageSize; |
||||
|
} |
||||
|
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3]; |
||||
|
|
||||
|
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { |
||||
|
this.defaultSortOrder = this.settings.defaultSortOrder; |
||||
|
} |
||||
|
|
||||
|
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; |
||||
|
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder); |
||||
|
|
||||
|
const origColor = this.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; |
||||
|
const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; |
||||
|
const defaultColor = tinycolor(origColor); |
||||
|
const mdDark = defaultColor.setAlpha(0.87).toRgbString(); |
||||
|
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); |
||||
|
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); |
||||
|
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); |
||||
|
|
||||
|
const cssString = |
||||
|
'.mat-input-element::placeholder {\n' + |
||||
|
' color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n' + |
||||
|
'.mat-input-element::-moz-placeholder {\n' + |
||||
|
' color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n' + |
||||
|
'.mat-input-element::-webkit-input-placeholder {\n' + |
||||
|
' color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n' + |
||||
|
'.mat-input-element:-ms-input-placeholder {\n' + |
||||
|
' color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n' + |
||||
|
'mat-toolbar.mat-table-toolbar {\n'+ |
||||
|
'color: ' + mdDark + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'mat-toolbar.mat-table-toolbar button.mat-icon-button mat-icon {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-header-row {\n'+ |
||||
|
'background-color: ' + origBackgroundColor + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-header-cell {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+ |
||||
|
'color: ' + mdDarkDisabled + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-row, .mat-table .mat-header-row {\n'+ |
||||
|
'border-bottom-color: '+mdDarkDivider+';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-row:not(.tb-current-entity):not(:hover) .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+ |
||||
|
'background-color: ' + origBackgroundColor + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-cell {\n'+ |
||||
|
'color: ' + mdDark + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-divider {\n'+ |
||||
|
'border-top-color: ' + mdDarkDivider + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-paginator {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-paginator button.mat-icon-button {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+ |
||||
|
'color: ' + mdDarkDisabled + ';\n'+ |
||||
|
'}\n'+ |
||||
|
'.mat-paginator .mat-select-value {\n'+ |
||||
|
'color: ' + mdDarkSecondary + ';\n'+ |
||||
|
'}'; |
||||
|
|
||||
|
const cssParser = new cssjs(); |
||||
|
cssParser.testMode = false; |
||||
|
const namespace = 'entities-table-' + this.utils.hashCode(cssString); |
||||
|
cssParser.cssPreviewNamespace = namespace; |
||||
|
cssParser.createStyleElement(namespace, cssString); |
||||
|
$(this.elementRef.nativeElement).addClass(namespace); |
||||
|
} |
||||
|
|
||||
|
private updateDatasources() { |
||||
|
|
||||
|
this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton'); |
||||
|
|
||||
|
const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true; |
||||
|
let entityNameColumnTitle: string; |
||||
|
if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) { |
||||
|
entityNameColumnTitle = this.utils.customTranslation(this.settings.entityNameColumnTitle, this.settings.entityNameColumnTitle); |
||||
|
} else { |
||||
|
entityNameColumnTitle = this.translate.instant('entity.entity-name'); |
||||
|
} |
||||
|
const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true; |
||||
|
|
||||
|
if (displayEntityName) { |
||||
|
this.displayedColumns.push('entityName'); |
||||
|
this.columns.push( |
||||
|
{ |
||||
|
name: 'entityName', |
||||
|
label: 'entityName', |
||||
|
title: entityNameColumnTitle |
||||
|
} as EntityColumn |
||||
|
); |
||||
|
this.contentsInfo['entityName'] = { |
||||
|
useCellContentFunction: false |
||||
|
}; |
||||
|
this.stylesInfo['entityName'] = { |
||||
|
useCellStyleFunction: false |
||||
|
}; |
||||
|
this.columnWidth['entityName'] = '100px'; |
||||
|
} |
||||
|
if (displayEntityType) { |
||||
|
this.displayedColumns.push('entityType'); |
||||
|
this.columns.push( |
||||
|
{ |
||||
|
name: 'entityType', |
||||
|
label: 'entityType', |
||||
|
title: this.translate.instant('entity.entity-type'), |
||||
|
} as EntityColumn |
||||
|
); |
||||
|
this.contentsInfo['entityType'] = { |
||||
|
useCellContentFunction: false |
||||
|
}; |
||||
|
this.stylesInfo['entityType'] = { |
||||
|
useCellStyleFunction: false |
||||
|
}; |
||||
|
this.columnWidth['entityType'] = '100px'; |
||||
|
} |
||||
|
|
||||
|
const dataKeys: Array<DataKey> = []; |
||||
|
|
||||
|
const datasource = this.subscription.datasources[0]; |
||||
|
|
||||
|
if (datasource) { |
||||
|
datasource.dataKeys.forEach((_dataKey) => { |
||||
|
const dataKey: EntityColumn = deepClone(_dataKey) as EntityColumn; |
||||
|
if (dataKey.type === DataKeyType.function) { |
||||
|
dataKey.name = dataKey.label; |
||||
|
} |
||||
|
dataKeys.push(dataKey); |
||||
|
|
||||
|
dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); |
||||
|
const keySettings: EntitiesTableDataKeySettings = dataKey.settings; |
||||
|
|
||||
|
let cellStyleFunction: Function = null; |
||||
|
let useCellStyleFunction = false; |
||||
|
|
||||
|
if (keySettings.useCellStyleFunction === true) { |
||||
|
if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { |
||||
|
try { |
||||
|
cellStyleFunction = new Function('value', keySettings.cellStyleFunction); |
||||
|
useCellStyleFunction = true; |
||||
|
} catch (e) { |
||||
|
cellStyleFunction = null; |
||||
|
useCellStyleFunction = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.stylesInfo[dataKey.label] = { |
||||
|
useCellStyleFunction, |
||||
|
cellStyleFunction |
||||
|
}; |
||||
|
|
||||
|
let cellContentFunction: Function = null; |
||||
|
let useCellContentFunction = false; |
||||
|
|
||||
|
if (keySettings.useCellContentFunction === true) { |
||||
|
if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { |
||||
|
try { |
||||
|
cellContentFunction = new Function('value, entity, ctx', keySettings.cellContentFunction); |
||||
|
useCellContentFunction = true; |
||||
|
} catch (e) { |
||||
|
cellContentFunction = null; |
||||
|
useCellContentFunction = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.contentsInfo[dataKey.label] = { |
||||
|
useCellContentFunction, |
||||
|
cellContentFunction, |
||||
|
units: dataKey.units, |
||||
|
decimals: dataKey.decimals |
||||
|
}; |
||||
|
|
||||
|
const columnWidth = isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; |
||||
|
this.columnWidth[dataKey.label] = columnWidth; |
||||
|
this.displayedColumns.push(dataKey.label); |
||||
|
this.columns.push(dataKey); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (this.actionCellDescriptors.length) { |
||||
|
this.displayedColumns.push('actions'); |
||||
|
} |
||||
|
this.entityDatasource = new EntityDatasource( |
||||
|
this.translate, dataKeys, this.subscription.datasources); |
||||
|
} |
||||
|
|
||||
|
private editColumnsToDisplay($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const target = $event.target || $event.srcElement || $event.currentTarget; |
||||
|
const config = new OverlayConfig(); |
||||
|
config.backdropClass = 'cdk-overlay-transparent-backdrop'; |
||||
|
config.hasBackdrop = true; |
||||
|
const connectedPosition: ConnectedPosition = { |
||||
|
originX: 'end', |
||||
|
originY: 'bottom', |
||||
|
overlayX: 'end', |
||||
|
overlayY: 'top' |
||||
|
}; |
||||
|
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) |
||||
|
.withPositions([connectedPosition]); |
||||
|
|
||||
|
const overlayRef = this.overlay.create(config); |
||||
|
overlayRef.backdropClick().subscribe(() => { |
||||
|
overlayRef.dispose(); |
||||
|
}); |
||||
|
|
||||
|
const columns: DisplayColumn[] = this.columns.map(column => { |
||||
|
return { |
||||
|
title: column.title, |
||||
|
label: column.label, |
||||
|
display: this.displayedColumns.indexOf(column.label) > -1 |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const injectionTokens = new WeakMap<any, any>([ |
||||
|
[DISPLAY_COLUMNS_PANEL_DATA, { |
||||
|
columns, |
||||
|
columnsUpdated: (newColumns) => { |
||||
|
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.label); |
||||
|
this.displayedColumns.push('actions'); |
||||
|
} |
||||
|
} as DisplayColumnsPanelData], |
||||
|
[OverlayRef, overlayRef] |
||||
|
]); |
||||
|
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); |
||||
|
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, |
||||
|
this.viewContainerRef, injector)); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
private enterFilterMode() { |
||||
|
this.textSearchMode = true; |
||||
|
this.pageLink.textSearch = ''; |
||||
|
this.ctx.hideTitlePanel = true; |
||||
|
this.ctx.detectChanges(true); |
||||
|
setTimeout(() => { |
||||
|
this.searchInputField.nativeElement.focus(); |
||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
||||
|
}, 10); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode() { |
||||
|
this.textSearchMode = false; |
||||
|
this.pageLink.textSearch = null; |
||||
|
if (this.displayPagination) { |
||||
|
this.paginator.pageIndex = 0; |
||||
|
} |
||||
|
this.updateData(); |
||||
|
this.ctx.hideTitlePanel = false; |
||||
|
this.ctx.detectChanges(true); |
||||
|
} |
||||
|
|
||||
|
private updateData() { |
||||
|
if (this.displayPagination) { |
||||
|
this.pageLink.page = this.paginator.pageIndex; |
||||
|
this.pageLink.pageSize = this.paginator.pageSize; |
||||
|
} else { |
||||
|
this.pageLink.page = 0; |
||||
|
} |
||||
|
this.pageLink.sortOrder.property = this.sort.active; |
||||
|
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
||||
|
this.entityDatasource.loadEntities(this.pageLink); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public trackByColumnLabel(index, column: EntityColumn) { |
||||
|
return column.label; |
||||
|
} |
||||
|
|
||||
|
public headerStyle(key: EntityColumn): any { |
||||
|
return this.widthStyle(key); |
||||
|
} |
||||
|
|
||||
|
public cellStyle(entity: EntityData, key: EntityColumn): any { |
||||
|
let style: any = {}; |
||||
|
if (entity && key) { |
||||
|
const styleInfo = this.stylesInfo[key.label]; |
||||
|
const value = getEntityValue(entity, key); |
||||
|
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { |
||||
|
try { |
||||
|
style = styleInfo.cellStyleFunction(value); |
||||
|
} catch (e) { |
||||
|
style = {}; |
||||
|
} |
||||
|
} else { |
||||
|
style = this.defaultStyle(key, value); |
||||
|
} |
||||
|
} |
||||
|
const widthStyle = this.widthStyle(key); |
||||
|
style = {...style, ...widthStyle}; |
||||
|
return style; |
||||
|
} |
||||
|
|
||||
|
private widthStyle(key: EntityColumn): any { |
||||
|
let style: any = {}; |
||||
|
const columnWidth = this.columnWidth[key.label]; |
||||
|
if (columnWidth !== "0px") { |
||||
|
style.minWidth = columnWidth; |
||||
|
style.width = columnWidth; |
||||
|
} else { |
||||
|
style.minWidth = "auto"; |
||||
|
style.width = "auto"; |
||||
|
} |
||||
|
return style; |
||||
|
} |
||||
|
|
||||
|
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { |
||||
|
let strContent = ''; |
||||
|
if (entity && key) { |
||||
|
const contentInfo = this.contentsInfo[key.label]; |
||||
|
const value = getEntityValue(entity, key); |
||||
|
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { |
||||
|
if (isDefined(value)) { |
||||
|
strContent = '' + value; |
||||
|
} |
||||
|
var content = strContent; |
||||
|
try { |
||||
|
content = contentInfo.cellContentFunction(value, entity, this.ctx); |
||||
|
} catch (e) { |
||||
|
content = strContent; |
||||
|
} |
||||
|
} else { |
||||
|
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; |
||||
|
const units = contentInfo.units || this.ctx.widgetConfig.units; |
||||
|
content = this.ctx.utils.formatValue(value, decimals, units, true); |
||||
|
} |
||||
|
return this.domSanitizer.bypassSecurityTrustHtml(content); |
||||
|
} else { |
||||
|
return strContent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onRowClick($event: Event, entity: EntityData) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.entityDatasource.toggleCurrentEntity(entity); |
||||
|
const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick'); |
||||
|
if (descriptors.length) { |
||||
|
let entityId; |
||||
|
let entityName; |
||||
|
if (entity) { |
||||
|
entityId = entity.id; |
||||
|
entityName = entity.entityName; |
||||
|
} |
||||
|
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onActionButtonClick($event: Event, entity: EntityData, actionDescriptor: WidgetActionDescriptor) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
let entityId; |
||||
|
let entityName; |
||||
|
if (entity) { |
||||
|
entityId = entity.id; |
||||
|
entityName = entity.entityName; |
||||
|
} |
||||
|
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName); |
||||
|
} |
||||
|
|
||||
|
private defaultStyle(key: EntityColumn, value: any): any { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
class EntityDatasource implements DataSource<EntityData> { |
||||
|
|
||||
|
private entitiesSubject = new BehaviorSubject<EntityData[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<EntityData>>(emptyPageData<EntityData>()); |
||||
|
|
||||
|
private allEntities: Array<EntityData> = []; |
||||
|
private allEntitiesSubject = new BehaviorSubject<EntityData[]>([]); |
||||
|
private allEntities$: Observable<Array<EntityData>> = this.allEntitiesSubject.asObservable(); |
||||
|
|
||||
|
private currentEntity: EntityData = null; |
||||
|
|
||||
|
constructor( |
||||
|
private translate: TranslateService, |
||||
|
private dataKeys: Array<DataKey>, |
||||
|
datasources: Array<Datasource> |
||||
|
) { |
||||
|
|
||||
|
for (const datasource of datasources) { |
||||
|
if (datasource.type === DatasourceType.entity && !datasource.entityId) { |
||||
|
continue; |
||||
|
} |
||||
|
const entity: EntityData = { |
||||
|
id: {} as EntityId, |
||||
|
entityName: datasource.entityName |
||||
|
}; |
||||
|
if (datasource.entityId) { |
||||
|
entity.id.id = datasource.entityId; |
||||
|
} |
||||
|
if (datasource.entityType) { |
||||
|
entity.id.entityType = datasource.entityType; |
||||
|
entity.entityType = this.translate.instant(entityTypeTranslations.get(datasource.entityType).type); |
||||
|
} else { |
||||
|
entity.entityType = ''; |
||||
|
} |
||||
|
this.dataKeys.forEach((dataKey) => { |
||||
|
entity[dataKey.label] = ''; |
||||
|
}); |
||||
|
this.allEntities.push(entity); |
||||
|
} |
||||
|
this.allEntitiesSubject.next(this.allEntities); |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> { |
||||
|
return this.entitiesSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.entitiesSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadEntities(pageLink: PageLink) { |
||||
|
this.fetchEntities(pageLink).pipe( |
||||
|
catchError(() => of(emptyPageData<EntityData>())), |
||||
|
).subscribe( |
||||
|
(pageData) => { |
||||
|
this.entitiesSubject.next(pageData.data); |
||||
|
this.pageDataSubject.next(pageData); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateEntitiesData(data: DatasourceData[]) { |
||||
|
for (let i = 0; i < this.allEntities.length; i++) { |
||||
|
const entity = this.allEntities[i]; |
||||
|
for (let a = 0; a < this.dataKeys.length; a++) { |
||||
|
const dataKey = this.dataKeys[a]; |
||||
|
const index = i * this.dataKeys.length + a; |
||||
|
const keyData = data[index].data; |
||||
|
if (keyData && keyData.length && keyData[0].length > 1) { |
||||
|
const value = keyData[0][1]; |
||||
|
entity[dataKey.label] = value; |
||||
|
} else { |
||||
|
entity[dataKey.label] = ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.allEntitiesSubject.next(this.allEntities); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.entitiesSubject.pipe( |
||||
|
map((entities) => !entities.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public toggleCurrentEntity(entity: EntityData): boolean { |
||||
|
if (this.currentEntity !== entity) { |
||||
|
this.currentEntity = entity; |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public isCurrentEntity(entity: EntityData): boolean { |
||||
|
return (this.currentEntity && entity && this.currentEntity.id && entity.id) && |
||||
|
(this.currentEntity.id.id === entity.id.id); |
||||
|
} |
||||
|
|
||||
|
private fetchEntities(pageLink: PageLink): Observable<PageData<EntityData>> { |
||||
|
return this.allEntities$.pipe( |
||||
|
map((data) => pageLink.filterData(data)) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
///
|
||||
|
/// 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 { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { DataKey } from '@shared/models/widget.models'; |
||||
|
|
||||
|
export interface EntityData { |
||||
|
id: EntityId; |
||||
|
entityName: string; |
||||
|
entityType?: string; |
||||
|
[key: string]: any; |
||||
|
} |
||||
|
|
||||
|
export interface EntityColumn extends DataKey { |
||||
|
title: string; |
||||
|
} |
||||
|
|
||||
|
export interface DisplayColumn { |
||||
|
title: string; |
||||
|
label: string; |
||||
|
display: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface CellContentInfo { |
||||
|
useCellContentFunction: boolean; |
||||
|
cellContentFunction?: Function; |
||||
|
units?: string; |
||||
|
decimals?: number; |
||||
|
} |
||||
|
|
||||
|
export interface CellStyleInfo { |
||||
|
useCellStyleFunction: boolean; |
||||
|
cellStyleFunction?: Function; |
||||
|
} |
||||
|
|
||||
|
export function getEntityValue(entity: any, key: DataKey): any { |
||||
|
return getDescendantProp(entity, key.label); |
||||
|
} |
||||
|
|
||||
|
export function getDescendantProp(obj: any, path: string): any { |
||||
|
return path.split('.').reduce((acc, part) => acc && acc[part], obj) |
||||
|
} |
||||
Loading…
Reference in new issue