Entity Relations Table

This commit is contained in:
Igor Kulikov 2019-08-23 15:06:42 +03:00
parent fea3c368a3
commit 851a3657db
11 changed files with 846 additions and 7 deletions

View File

@ -0,0 +1,113 @@
///
/// 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 { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { EntityRelation, EntityRelationInfo, EntityRelationsQuery } from '@shared/models/relation.models';
import { EntityId } from '@app/shared/models/id/entity-id';
@Injectable({
providedIn: 'root'
})
export class EntityRelationService {
constructor(
private http: HttpClient
) { }
public saveRelation(relation: EntityRelation, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<EntityRelation> {
return this.http.post<EntityRelation>('/api/relation', relation, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteRelation(fromId: EntityId, relationType: string, toId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` +
`&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteRelations(entityId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/relations?entityId=${entityId.id}&entityType=${entityId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getRelation(fromId: EntityId, relationType: string, toId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<EntityRelation> {
return this.http.get<EntityRelation>(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` +
`&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findByFrom(fromId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
return this.http.get<Array<EntityRelation>>(
`/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findInfoByFrom(fromId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
return this.http.get<Array<EntityRelationInfo>>(
`/api/relations/info?fromId=${fromId.id}&fromType=${fromId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findByFromAndType(fromId: EntityId, relationType: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
return this.http.get<Array<EntityRelation>>(
`/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}&relationType=${relationType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findByTo(toId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
return this.http.get<Array<EntityRelation>>(
`/api/relations?toId=${toId.id}&toType=${toId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findInfoByTo(toId: EntityId,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
return this.http.get<Array<EntityRelationInfo>>(
`/api/relations/info?toId=${toId.id}&toType=${toId.entityType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findByToAndType(toId: EntityId, relationType: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
return this.http.get<Array<EntityRelation>>(
`/api/relations?toId=${toId.id}&toType=${toId.entityType}&relationType=${relationType}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findByQuery(query: EntityRelationsQuery,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
return this.http.post<Array<EntityRelation>>(
'/api/relations', query,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public findInfoByQuery(query: EntityRelationsQuery,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
return this.http.post<Array<EntityRelationInfo>>(
'/api/relations/info', query,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

View File

@ -26,6 +26,7 @@ import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-di
import { AuditLogTableComponent } from './audit-log/audit-log-table.component';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EventTableComponent } from '@home/components/event/event-table.component';
import { RelationTableComponent } from '@home/components/relation/relation-table.component';
@NgModule({
entryComponents: [
@ -43,7 +44,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen
AuditLogTableComponent,
AuditLogDetailsDialogComponent,
EventTableHeaderComponent,
EventTableComponent
EventTableComponent,
RelationTableComponent
],
imports: [
CommonModule,
@ -56,7 +58,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen
EntityDetailsPanelComponent,
ContactComponent,
AuditLogTableComponent,
EventTableComponent
EventTableComponent,
RelationTableComponent
]
})
export class HomeComponentsModule { }

View File

@ -0,0 +1,168 @@
<!--
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="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-title">{{(direction == directions.FROM ?
'relation.from-relations' : 'relation.to-relations') | translate}}</span>
<mat-form-field class="mat-block tb-relation-direction" style="width: 200px;">
<mat-label translate>relation.direction</mat-label>
<mat-select matInput [ngModel]="direction"
(ngModelChange)="directionChanged($event)">
<mat-option *ngFor="let type of directionTypes" [value]="type">
{{ directionTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
(click)="addRelation($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="reloadRelations()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
<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>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ 'common.enter-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>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span>
{{ translate.get('relation.selected-relations', {count: dataSource.selection.selected.length}) | async }}
</span>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteRelations($event)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
[indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let relation">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? dataSource.selection.toggle(relation) : null"
[checked]="dataSource.selection.isSelected(relation)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.type }}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toEntityTypeName">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.to-entity-type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.toEntityTypeName }}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toName">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.toName }}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.from-entity-type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.fromEntityTypeName }}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromName">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.from-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.fromName }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
</mat-header-cell>
<mat-cell *matCellDef="let relation" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'relation.edit' | translate }}"
matTooltipPosition="above"
(click)="editRelation($event, relation)">
<mat-icon>edit</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'relation.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteRelation($event, relation)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'mat-selected': dataSource.selection.isSelected(relation)}"
*matRowDef="let relation; columns: displayedColumns;" (click)="dataSource.selection.toggle(relation)"></mat-row>
</mat-table>
<span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>{{ 'relation.no-relations-text' }}</span>
</div>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
</div>
</div>

View File

@ -0,0 +1,54 @@
/**
* 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 {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
.tb-entity-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
}
}
}
}
:host ::ng-deep {
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
mat-form-field.tb-relation-direction {
font-size: 16px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
}
}

View File

@ -0,0 +1,225 @@
///
/// 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, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { fromEvent, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { EntityRelationInfo, EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models';
import { EntityId } from '@shared/models/id/entity-id';
import { RelationsDatasource } from '../../models/datasource/relation-datasource';
import { DebugEventType, EventType } from '@shared/models/event.models';
@Component({
selector: 'tb-relation-table',
templateUrl: './relation-table.component.html',
styleUrls: ['./relation-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RelationTableComponent extends PageComponent implements AfterViewInit, OnInit {
directions = EntitySearchDirection;
directionTypes = Object.keys(EntitySearchDirection);
directionTypeTranslations = entitySearchDirectionTranslations;
displayedColumns: string[];
direction: EntitySearchDirection;
pageLink: PageLink;
textSearchMode = false;
dataSource: RelationsDatasource;
activeValue = false;
dirtyValue = false;
entityIdValue: EntityId;
viewsInited = false;
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
this.activeValue = active;
if (this.activeValue && this.dirtyValue) {
this.dirtyValue = false;
if (this.viewsInited) {
this.updateData(true);
}
}
}
}
@Input()
set entityId(entityId: EntityId) {
if (this.entityIdValue !== entityId) {
this.entityIdValue = entityId;
if (this.viewsInited) {
this.resetSortAndFilter(this.activeValue);
if (!this.activeValue) {
this.dirtyValue = true;
}
}
}
}
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
constructor(protected store: Store<AppState>,
private entityRelationService: EntityRelationService,
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
this.direction = EntitySearchDirection.FROM;
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new RelationsDatasource(this.entityRelationService, this.translate);
this.updateColumns();
}
ngOnInit() {
}
updateColumns() {
if (this.direction === EntitySearchDirection.FROM) {
this.displayedColumns = ['select', 'type', 'toEntityTypeName', 'toName', 'actions'];
} else {
this.displayedColumns = ['select', 'type', 'fromEntityTypeName', 'fromName', 'actions'];
}
}
directionChanged(direction: EntitySearchDirection) {
this.direction = direction;
this.updateColumns();
this.updateData(true);
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.updateData();
})
)
.subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.viewsInited = true;
if (this.activeValue && this.entityIdValue) {
this.updateData(true);
}
}
updateData(reload: boolean = false) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.dataSource.loadRelations(this.direction, this.entityIdValue, this.pageLink, reload);
}
enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
this.updateData();
}
resetSortAndFilter(update: boolean = true) {
this.direction = EntitySearchDirection.FROM;
this.updateColumns();
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get('type');
this.sort.active = sortable.id;
this.sort.direction = 'asc';
if (update) {
this.updateData(true);
}
}
reloadRelations() {
this.updateData(true);
}
addRelation($event: Event) {
this.openRelationDialog($event);
}
editRelation($event: Event, relation: EntityRelationInfo) {
this.openRelationDialog($event, relation);
}
deleteRelation($event: Event, relation: EntityRelationInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
deleteRelations($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (this.dataSource.selection.selected.length > 0) {
// TODO:
}
}
openRelationDialog($event: Event, relation: EntityRelationInfo = null) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
}

View File

@ -0,0 +1,143 @@
///
/// 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 { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models';
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { SelectionModel } from '@angular/cdk/collections';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators';
import { EntityId } from '@app/shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
export class RelationsDatasource implements DataSource<EntityRelationInfo> {
private relationsSubject = new BehaviorSubject<EntityRelationInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<EntityRelationInfo>>(emptyPageData<EntityRelationInfo>());
public pageData$ = this.pageDataSubject.asObservable();
public selection = new SelectionModel<EntityRelationInfo>(true, []);
private allRelations: Observable<Array<EntityRelationInfo>>;
constructor(private entityRelationService: EntityRelationService,
private translate: TranslateService) {}
connect(collectionViewer: CollectionViewer): Observable<EntityRelationInfo[] | ReadonlyArray<EntityRelationInfo>> {
return this.relationsSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.relationsSubject.complete();
this.pageDataSubject.complete();
}
loadRelations(direction: EntitySearchDirection, entityId: EntityId,
pageLink: PageLink, reload: boolean = false): Observable<PageData<EntityRelationInfo>> {
if (reload) {
this.allRelations = null;
}
const result = new ReplaySubject<PageData<EntityRelationInfo>>();
this.fetchRelations(direction, entityId, pageLink).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<EntityRelationInfo>())),
).subscribe(
(pageData) => {
this.relationsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
}
);
return result;
}
fetchRelations(direction: EntitySearchDirection, entityId: EntityId,
pageLink: PageLink): Observable<PageData<EntityRelationInfo>> {
return this.getAllRelations(direction, entityId).pipe(
map((data) => pageLink.filterData(data))
);
}
getAllRelations(direction: EntitySearchDirection, entityId: EntityId): Observable<Array<EntityRelationInfo>> {
if (!this.allRelations) {
let relationsObservable: Observable<Array<EntityRelationInfo>>;
switch (direction) {
case EntitySearchDirection.FROM:
relationsObservable = this.entityRelationService.findInfoByFrom(entityId);
break;
case EntitySearchDirection.TO:
relationsObservable = this.entityRelationService.findInfoByTo(entityId);
break;
}
this.allRelations = relationsObservable.pipe(
map(relations => {
relations.forEach(relation => {
if (direction === EntitySearchDirection.FROM) {
relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type);
} else {
relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type);
}
});
return relations;
}),
publishReplay(1),
refCount()
);
}
return this.allRelations;
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.relationsSubject.pipe(
map((relations) => numSelected === relations.length)
);
}
isEmpty(): Observable<boolean> {
return this.relationsSubject.pipe(
map((relations) => !relations.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
masterToggle() {
this.relationsSubject.pipe(
tap((relations) => {
const numSelected = this.selection.selected.length;
if (numSelected === relations.length) {
this.selection.clear();
} else {
relations.forEach(row => {
this.selection.select(row);
});
}
}),
take(1)
).subscribe();
}
}

View File

@ -20,6 +20,10 @@
<tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>

View File

@ -14,16 +14,17 @@
/// limitations under the License.
///
import { BaseData, HasId } from '@shared/models/base-data';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
export interface PageData<T extends BaseData<HasId>> {
export interface PageData<T> {
data: Array<T>;
totalPages: number;
totalElements: number;
hasNext: boolean;
}
export function emptyPageData<T extends BaseData<HasId>>(): PageData<T> {
export function emptyPageData<T>(): PageData<T> {
return {
data: [],
totalPages: 0,

View File

@ -15,6 +15,27 @@
///
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
const defaultPageLinkSearchFunction: PageLinkSearchFunction<any> =
(entity: any, textSearch: string) => {
if (textSearch === null || !textSearch.length) {
return true;
}
const expected = ('' + textSearch).toLowerCase();
for (const key of Object.keys(entity)) {
const val = entity[key];
if (val !== null && val !== Object(val)) {
const actual = ('' + val).toLowerCase();
if (actual.indexOf(expected) !== -1) {
return true;
}
}
}
return false;
};
export class PageLink {
@ -65,6 +86,25 @@ export class PageLink {
return 0;
}
public filterData<T>(data: Array<T>,
searchFunction: PageLinkSearchFunction<T> = defaultPageLinkSearchFunction): PageData<T> {
const pageData = emptyPageData<T>();
pageData.data = [...data];
if (this.textSearch && this.textSearch.length) {
pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch));
}
pageData.totalElements = pageData.data.length;
pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
if (this.sortOrder) {
pageData.data = pageData.data.sort(this.sort);
}
const startIndex = this.pageSize * this.page;
const endIndex = startIndex + this.pageSize;
pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
return pageData;
}
}
export class TimePageLink extends PageLink {

View File

@ -0,0 +1,87 @@
///
/// 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 { EntityType } from '@shared/models/entity-type.models';
import { ActionStatus } from '@shared/models/audit-log.models';
export const CONTAINS_TYPE = 'Contains';
export const MANAGES_TYPE = 'Manages';
export const RelationTypes = [
CONTAINS_TYPE,
MANAGES_TYPE
];
export enum RelationTypeGroup {
COMMON = 'COMMON',
ALARM = 'ALARM',
DASHBOARD = 'DASHBOARD',
RULE_CHAIN = 'RULE_CHAIN',
RULE_NODE = 'RULE_NODE',
}
export enum EntitySearchDirection {
FROM = 'FROM',
TO = 'TO'
}
export const entitySearchDirectionTranslations = new Map<EntitySearchDirection, string>(
[
[EntitySearchDirection.FROM, 'relation.search-direction.FROM'],
[EntitySearchDirection.TO, 'relation.search-direction.TO'],
]
);
export const directionTypeTranslations = new Map<EntitySearchDirection, string>(
[
[EntitySearchDirection.FROM, 'relation.direction-type.FROM'],
[EntitySearchDirection.TO, 'relation.direction-type.TO'],
]
);
export interface EntityTypeFilter {
relationType: string;
entityTypes: Array<EntityType>;
}
export interface RelationsSearchParameters {
rootId: string;
rootType: EntityType;
direction: EntitySearchDirection;
relationTypeGroup: RelationTypeGroup;
maxLevel: number;
}
export interface EntityRelationsQuery {
parameters: RelationsSearchParameters;
filters: Array<EntityTypeFilter>;
}
export interface EntityRelation {
from: EntityId;
to: EntityId;
type: string;
typeGroup: RelationTypeGroup;
additionalInfo?: any;
}
export interface EntityRelationInfo extends EntityRelation {
fromName: string;
toEntityTypeName?: string;
toName: string;
fromEntityTypeName?: string;
}

View File

@ -1278,7 +1278,8 @@
"any-relation": "Any relation",
"relation-filters": "Relation filters",
"additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json."
"invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found"
},
"rulechain": {
"rulechain": "Rule chain",