Commit 851a3657dbfffa9e220fbf1970c662063e6596a8

Authored by Igor Kulikov
1 parent fea3c368

Entity Relations Table

  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { EntityRelation, EntityRelationInfo, EntityRelationsQuery } from '@shared/models/relation.models';
  22 +import { EntityId } from '@app/shared/models/id/entity-id';
  23 +
  24 +@Injectable({
  25 + providedIn: 'root'
  26 +})
  27 +export class EntityRelationService {
  28 +
  29 + constructor(
  30 + private http: HttpClient
  31 + ) { }
  32 +
  33 + public saveRelation(relation: EntityRelation, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<EntityRelation> {
  34 + return this.http.post<EntityRelation>('/api/relation', relation, defaultHttpOptions(ignoreLoading, ignoreErrors));
  35 + }
  36 +
  37 + public deleteRelation(fromId: EntityId, relationType: string, toId: EntityId,
  38 + ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  39 + return this.http.delete(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` +
  40 + `&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`,
  41 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  42 + }
  43 +
  44 + public deleteRelations(entityId: EntityId,
  45 + ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  46 + return this.http.delete(`/api/relations?entityId=${entityId.id}&entityType=${entityId.entityType}`,
  47 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  48 + }
  49 +
  50 + public getRelation(fromId: EntityId, relationType: string, toId: EntityId,
  51 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<EntityRelation> {
  52 + return this.http.get<EntityRelation>(`/api/relation?fromId=${fromId.id}&fromType=${fromId.entityType}` +
  53 + `&relationType=${relationType}&toId=${toId.id}&toType=${toId.entityType}`,
  54 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  55 + }
  56 +
  57 + public findByFrom(fromId: EntityId,
  58 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
  59 + return this.http.get<Array<EntityRelation>>(
  60 + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}`,
  61 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  62 + }
  63 +
  64 + public findInfoByFrom(fromId: EntityId,
  65 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
  66 + return this.http.get<Array<EntityRelationInfo>>(
  67 + `/api/relations/info?fromId=${fromId.id}&fromType=${fromId.entityType}`,
  68 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  69 + }
  70 +
  71 + public findByFromAndType(fromId: EntityId, relationType: string,
  72 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
  73 + return this.http.get<Array<EntityRelation>>(
  74 + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}&relationType=${relationType}`,
  75 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  76 + }
  77 +
  78 + public findByTo(toId: EntityId,
  79 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
  80 + return this.http.get<Array<EntityRelation>>(
  81 + `/api/relations?toId=${toId.id}&toType=${toId.entityType}`,
  82 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  83 + }
  84 +
  85 + public findInfoByTo(toId: EntityId,
  86 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
  87 + return this.http.get<Array<EntityRelationInfo>>(
  88 + `/api/relations/info?toId=${toId.id}&toType=${toId.entityType}`,
  89 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  90 + }
  91 +
  92 + public findByToAndType(toId: EntityId, relationType: string,
  93 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
  94 + return this.http.get<Array<EntityRelation>>(
  95 + `/api/relations?toId=${toId.id}&toType=${toId.entityType}&relationType=${relationType}`,
  96 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  97 + }
  98 +
  99 + public findByQuery(query: EntityRelationsQuery,
  100 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelation>> {
  101 + return this.http.post<Array<EntityRelation>>(
  102 + '/api/relations', query,
  103 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  104 + }
  105 +
  106 + public findInfoByQuery(query: EntityRelationsQuery,
  107 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntityRelationInfo>> {
  108 + return this.http.post<Array<EntityRelationInfo>>(
  109 + '/api/relations/info', query,
  110 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  111 + }
  112 +
  113 +}
... ...
... ... @@ -26,6 +26,7 @@ import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-di
26 26 import { AuditLogTableComponent } from './audit-log/audit-log-table.component';
27 27 import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
28 28 import { EventTableComponent } from '@home/components/event/event-table.component';
  29 +import { RelationTableComponent } from '@home/components/relation/relation-table.component';
29 30
30 31 @NgModule({
31 32 entryComponents: [
... ... @@ -43,7 +44,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen
43 44 AuditLogTableComponent,
44 45 AuditLogDetailsDialogComponent,
45 46 EventTableHeaderComponent,
46   - EventTableComponent
  47 + EventTableComponent,
  48 + RelationTableComponent
47 49 ],
48 50 imports: [
49 51 CommonModule,
... ... @@ -56,7 +58,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen
56 58 EntityDetailsPanelComponent,
57 59 ContactComponent,
58 60 AuditLogTableComponent,
59   - EventTableComponent
  61 + EventTableComponent,
  62 + RelationTableComponent
60 63 ]
61 64 })
62 65 export class HomeComponentsModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="mat-padding tb-entity-table tb-absolute-fill">
  19 + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
  20 + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
  21 + <div class="mat-toolbar-tools">
  22 + <span class="tb-entity-table-title">{{(direction == directions.FROM ?
  23 + 'relation.from-relations' : 'relation.to-relations') | translate}}</span>
  24 + <mat-form-field class="mat-block tb-relation-direction" style="width: 200px;">
  25 + <mat-label translate>relation.direction</mat-label>
  26 + <mat-select matInput [ngModel]="direction"
  27 + (ngModelChange)="directionChanged($event)">
  28 + <mat-option *ngFor="let type of directionTypes" [value]="type">
  29 + {{ directionTypeTranslations.get(type) | translate }}
  30 + </mat-option>
  31 + </mat-select>
  32 + </mat-form-field>
  33 + <span fxFlex></span>
  34 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  35 + (click)="addRelation($event)"
  36 + matTooltip="{{ 'action.add' | translate }}"
  37 + matTooltipPosition="above">
  38 + <mat-icon>add</mat-icon>
  39 + </button>
  40 + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="reloadRelations()"
  41 + matTooltip="{{ 'action.refresh' | translate }}"
  42 + matTooltipPosition="above">
  43 + <mat-icon>refresh</mat-icon>
  44 + </button>
  45 + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
  46 + matTooltip="{{ 'action.search' | translate }}"
  47 + matTooltipPosition="above">
  48 + <mat-icon>search</mat-icon>
  49 + </button>
  50 + </div>
  51 + </mat-toolbar>
  52 + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
  53 + <div class="mat-toolbar-tools">
  54 + <button mat-button mat-icon-button
  55 + matTooltip="{{ 'action.search' | translate }}"
  56 + matTooltipPosition="above">
  57 + <mat-icon>search</mat-icon>
  58 + </button>
  59 + <mat-form-field fxFlex>
  60 + <mat-label>&nbsp;</mat-label>
  61 + <input #searchInput matInput
  62 + [(ngModel)]="pageLink.textSearch"
  63 + placeholder="{{ 'common.enter-search' | translate }}"/>
  64 + </mat-form-field>
  65 + <button mat-button mat-icon-button (click)="exitFilterMode()"
  66 + matTooltip="{{ 'action.close' | translate }}"
  67 + matTooltipPosition="above">
  68 + <mat-icon>close</mat-icon>
  69 + </button>
  70 + </div>
  71 + </mat-toolbar>
  72 + <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
  73 + <div class="mat-toolbar-tools">
  74 + <span>
  75 + {{ translate.get('relation.selected-relations', {count: dataSource.selection.selected.length}) | async }}
  76 + </span>
  77 + <span fxFlex></span>
  78 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  79 + matTooltip="{{ 'action.delete' | translate }}"
  80 + matTooltipPosition="above"
  81 + (click)="deleteRelations($event)">
  82 + <mat-icon>delete</mat-icon>
  83 + </button>
  84 + </div>
  85 + </mat-toolbar>
  86 + <div fxFlex class="table-container">
  87 + <mat-table [dataSource]="dataSource"
  88 + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  89 + <ng-container matColumnDef="select" sticky>
  90 + <mat-header-cell *matHeaderCellDef>
  91 + <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
  92 + [checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
  93 + [indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
  94 + </mat-checkbox>
  95 + </mat-header-cell>
  96 + <mat-cell *matCellDef="let relation">
  97 + <mat-checkbox (click)="$event.stopPropagation()"
  98 + (change)="$event ? dataSource.selection.toggle(relation) : null"
  99 + [checked]="dataSource.selection.isSelected(relation)">
  100 + </mat-checkbox>
  101 + </mat-cell>
  102 + </ng-container>
  103 + <ng-container matColumnDef="type">
  104 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.type' | translate }} </mat-header-cell>
  105 + <mat-cell *matCellDef="let relation">
  106 + {{ relation.type }}
  107 + </mat-cell>
  108 + </ng-container>
  109 + <ng-container *ngIf="direction === directions.FROM" matColumnDef="toEntityTypeName">
  110 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.to-entity-type' | translate }} </mat-header-cell>
  111 + <mat-cell *matCellDef="let relation">
  112 + {{ relation.toEntityTypeName }}
  113 + </mat-cell>
  114 + </ng-container>
  115 + <ng-container *ngIf="direction === directions.FROM" matColumnDef="toName">
  116 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
  117 + <mat-cell *matCellDef="let relation">
  118 + {{ relation.toName }}
  119 + </mat-cell>
  120 + </ng-container>
  121 + <ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">
  122 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.from-entity-type' | translate }} </mat-header-cell>
  123 + <mat-cell *matCellDef="let relation">
  124 + {{ relation.fromEntityTypeName }}
  125 + </mat-cell>
  126 + </ng-container>
  127 + <ng-container *ngIf="direction === directions.TO" matColumnDef="fromName">
  128 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'relation.from-entity-name' | translate }} </mat-header-cell>
  129 + <mat-cell *matCellDef="let relation">
  130 + {{ relation.fromName }}
  131 + </mat-cell>
  132 + </ng-container>
  133 + <ng-container matColumnDef="actions" stickyEnd>
  134 + <mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
  135 + </mat-header-cell>
  136 + <mat-cell *matCellDef="let relation" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
  137 + <div fxFlex fxLayout="row" fxLayoutAlign="end">
  138 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  139 + matTooltip="{{ 'relation.edit' | translate }}"
  140 + matTooltipPosition="above"
  141 + (click)="editRelation($event, relation)">
  142 + <mat-icon>edit</mat-icon>
  143 + </button>
  144 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  145 + matTooltip="{{ 'relation.delete' | translate }}"
  146 + matTooltipPosition="above"
  147 + (click)="deleteRelation($event, relation)">
  148 + <mat-icon>delete</mat-icon>
  149 + </button>
  150 + </div>
  151 + </mat-cell>
  152 + </ng-container>
  153 + <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
  154 + <mat-row [ngClass]="{'mat-row-select': true,
  155 + 'mat-selected': dataSource.selection.isSelected(relation)}"
  156 + *matRowDef="let relation; columns: displayedColumns;" (click)="dataSource.selection.toggle(relation)"></mat-row>
  157 + </mat-table>
  158 + <span [fxShow]="dataSource.isEmpty() | async"
  159 + fxLayoutAlign="center center"
  160 + class="no-data-found" translate>{{ 'relation.no-relations-text' }}</span>
  161 + </div>
  162 + <mat-divider></mat-divider>
  163 + <mat-paginator [length]="dataSource.total() | async"
  164 + [pageIndex]="pageLink.page"
  165 + [pageSize]="pageLink.pageSize"
  166 + [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  167 + </div>
  168 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + .tb-entity-table {
  20 + .tb-entity-table-content {
  21 + width: 100%;
  22 + height: 100%;
  23 + background: #fff;
  24 +
  25 + .tb-entity-table-title {
  26 + padding-right: 20px;
  27 + white-space: nowrap;
  28 + overflow: hidden;
  29 + text-overflow: ellipsis;
  30 + }
  31 +
  32 + .table-container {
  33 + overflow: auto;
  34 + }
  35 + }
  36 + }
  37 +}
  38 +
  39 +:host ::ng-deep {
  40 + .mat-sort-header-sorted .mat-sort-header-arrow {
  41 + opacity: 1 !important;
  42 + }
  43 + mat-form-field.tb-relation-direction {
  44 + font-size: 16px;
  45 +
  46 + .mat-form-field-wrapper {
  47 + padding-bottom: 0;
  48 + }
  49 +
  50 + .mat-form-field-underline {
  51 + bottom: 0;
  52 + }
  53 + }
  54 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { PageLink } from '@shared/models/page/page-link';
  20 +import { MatPaginator } from '@angular/material/paginator';
  21 +import { MatSort } from '@angular/material/sort';
  22 +import { Store } from '@ngrx/store';
  23 +import { AppState } from '@core/core.state';
  24 +import { TranslateService } from '@ngx-translate/core';
  25 +import { MatDialog } from '@angular/material/dialog';
  26 +import { DialogService } from '@core/services/dialog.service';
  27 +import { EntityRelationService } from '@core/http/entity-relation.service';
  28 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  29 +import { fromEvent, merge } from 'rxjs';
  30 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
  31 +import { EntityRelationInfo, EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models';
  32 +import { EntityId } from '@shared/models/id/entity-id';
  33 +import { RelationsDatasource } from '../../models/datasource/relation-datasource';
  34 +import { DebugEventType, EventType } from '@shared/models/event.models';
  35 +
  36 +@Component({
  37 + selector: 'tb-relation-table',
  38 + templateUrl: './relation-table.component.html',
  39 + styleUrls: ['./relation-table.component.scss'],
  40 + changeDetection: ChangeDetectionStrategy.OnPush
  41 +})
  42 +export class RelationTableComponent extends PageComponent implements AfterViewInit, OnInit {
  43 +
  44 + directions = EntitySearchDirection;
  45 +
  46 + directionTypes = Object.keys(EntitySearchDirection);
  47 +
  48 + directionTypeTranslations = entitySearchDirectionTranslations;
  49 +
  50 + displayedColumns: string[];
  51 + direction: EntitySearchDirection;
  52 + pageLink: PageLink;
  53 + textSearchMode = false;
  54 + dataSource: RelationsDatasource;
  55 +
  56 + activeValue = false;
  57 + dirtyValue = false;
  58 + entityIdValue: EntityId;
  59 +
  60 + viewsInited = false;
  61 +
  62 + @Input()
  63 + set active(active: boolean) {
  64 + if (this.activeValue !== active) {
  65 + this.activeValue = active;
  66 + if (this.activeValue && this.dirtyValue) {
  67 + this.dirtyValue = false;
  68 + if (this.viewsInited) {
  69 + this.updateData(true);
  70 + }
  71 + }
  72 + }
  73 + }
  74 +
  75 + @Input()
  76 + set entityId(entityId: EntityId) {
  77 + if (this.entityIdValue !== entityId) {
  78 + this.entityIdValue = entityId;
  79 + if (this.viewsInited) {
  80 + this.resetSortAndFilter(this.activeValue);
  81 + if (!this.activeValue) {
  82 + this.dirtyValue = true;
  83 + }
  84 + }
  85 + }
  86 + }
  87 +
  88 + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
  89 +
  90 + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
  91 + @ViewChild(MatSort, {static: false}) sort: MatSort;
  92 +
  93 + constructor(protected store: Store<AppState>,
  94 + private entityRelationService: EntityRelationService,
  95 + public translate: TranslateService,
  96 + public dialog: MatDialog,
  97 + private dialogService: DialogService) {
  98 + super(store);
  99 + this.dirtyValue = !this.activeValue;
  100 + const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
  101 + this.direction = EntitySearchDirection.FROM;
  102 + this.pageLink = new PageLink(10, 0, null, sortOrder);
  103 + this.dataSource = new RelationsDatasource(this.entityRelationService, this.translate);
  104 + this.updateColumns();
  105 + }
  106 +
  107 + ngOnInit() {
  108 + }
  109 +
  110 + updateColumns() {
  111 + if (this.direction === EntitySearchDirection.FROM) {
  112 + this.displayedColumns = ['select', 'type', 'toEntityTypeName', 'toName', 'actions'];
  113 + } else {
  114 + this.displayedColumns = ['select', 'type', 'fromEntityTypeName', 'fromName', 'actions'];
  115 + }
  116 + }
  117 +
  118 + directionChanged(direction: EntitySearchDirection) {
  119 + this.direction = direction;
  120 + this.updateColumns();
  121 + this.updateData(true);
  122 + }
  123 +
  124 + ngAfterViewInit() {
  125 +
  126 + fromEvent(this.searchInputField.nativeElement, 'keyup')
  127 + .pipe(
  128 + debounceTime(150),
  129 + distinctUntilChanged(),
  130 + tap(() => {
  131 + this.paginator.pageIndex = 0;
  132 + this.updateData();
  133 + })
  134 + )
  135 + .subscribe();
  136 +
  137 + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
  138 +
  139 + merge(this.sort.sortChange, this.paginator.page)
  140 + .pipe(
  141 + tap(() => this.updateData())
  142 + )
  143 + .subscribe();
  144 +
  145 + this.viewsInited = true;
  146 + if (this.activeValue && this.entityIdValue) {
  147 + this.updateData(true);
  148 + }
  149 + }
  150 +
  151 + updateData(reload: boolean = false) {
  152 + this.pageLink.page = this.paginator.pageIndex;
  153 + this.pageLink.pageSize = this.paginator.pageSize;
  154 + this.pageLink.sortOrder.property = this.sort.active;
  155 + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
  156 + this.dataSource.loadRelations(this.direction, this.entityIdValue, this.pageLink, reload);
  157 + }
  158 +
  159 + enterFilterMode() {
  160 + this.textSearchMode = true;
  161 + this.pageLink.textSearch = '';
  162 + setTimeout(() => {
  163 + this.searchInputField.nativeElement.focus();
  164 + this.searchInputField.nativeElement.setSelectionRange(0, 0);
  165 + }, 10);
  166 + }
  167 +
  168 + exitFilterMode() {
  169 + this.textSearchMode = false;
  170 + this.pageLink.textSearch = null;
  171 + this.paginator.pageIndex = 0;
  172 + this.updateData();
  173 + }
  174 +
  175 + resetSortAndFilter(update: boolean = true) {
  176 + this.direction = EntitySearchDirection.FROM;
  177 + this.updateColumns();
  178 + this.pageLink.textSearch = null;
  179 + this.paginator.pageIndex = 0;
  180 + const sortable = this.sort.sortables.get('type');
  181 + this.sort.active = sortable.id;
  182 + this.sort.direction = 'asc';
  183 + if (update) {
  184 + this.updateData(true);
  185 + }
  186 + }
  187 +
  188 + reloadRelations() {
  189 + this.updateData(true);
  190 + }
  191 +
  192 + addRelation($event: Event) {
  193 + this.openRelationDialog($event);
  194 + }
  195 +
  196 + editRelation($event: Event, relation: EntityRelationInfo) {
  197 + this.openRelationDialog($event, relation);
  198 + }
  199 +
  200 + deleteRelation($event: Event, relation: EntityRelationInfo) {
  201 + if ($event) {
  202 + $event.stopPropagation();
  203 + }
  204 +
  205 + // TODO:
  206 + }
  207 +
  208 + deleteRelations($event: Event) {
  209 + if ($event) {
  210 + $event.stopPropagation();
  211 + }
  212 + if (this.dataSource.selection.selected.length > 0) {
  213 + // TODO:
  214 + }
  215 + }
  216 +
  217 + openRelationDialog($event: Event, relation: EntityRelationInfo = null) {
  218 + if ($event) {
  219 + $event.stopPropagation();
  220 + }
  221 + // TODO:
  222 + }
  223 +
  224 +
  225 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
  18 +import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models';
  19 +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
  20 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  21 +import { SelectionModel } from '@angular/cdk/collections';
  22 +import { EntityRelationService } from '@core/http/entity-relation.service';
  23 +import { PageLink } from '@shared/models/page/page-link';
  24 +import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators';
  25 +import { EntityId } from '@app/shared/models/id/entity-id';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { entityTypeTranslations } from '@shared/models/entity-type.models';
  28 +
  29 +export class RelationsDatasource implements DataSource<EntityRelationInfo> {
  30 +
  31 + private relationsSubject = new BehaviorSubject<EntityRelationInfo[]>([]);
  32 + private pageDataSubject = new BehaviorSubject<PageData<EntityRelationInfo>>(emptyPageData<EntityRelationInfo>());
  33 +
  34 + public pageData$ = this.pageDataSubject.asObservable();
  35 +
  36 + public selection = new SelectionModel<EntityRelationInfo>(true, []);
  37 +
  38 + private allRelations: Observable<Array<EntityRelationInfo>>;
  39 +
  40 + constructor(private entityRelationService: EntityRelationService,
  41 + private translate: TranslateService) {}
  42 +
  43 + connect(collectionViewer: CollectionViewer): Observable<EntityRelationInfo[] | ReadonlyArray<EntityRelationInfo>> {
  44 + return this.relationsSubject.asObservable();
  45 + }
  46 +
  47 + disconnect(collectionViewer: CollectionViewer): void {
  48 + this.relationsSubject.complete();
  49 + this.pageDataSubject.complete();
  50 + }
  51 +
  52 + loadRelations(direction: EntitySearchDirection, entityId: EntityId,
  53 + pageLink: PageLink, reload: boolean = false): Observable<PageData<EntityRelationInfo>> {
  54 + if (reload) {
  55 + this.allRelations = null;
  56 + }
  57 + const result = new ReplaySubject<PageData<EntityRelationInfo>>();
  58 + this.fetchRelations(direction, entityId, pageLink).pipe(
  59 + tap(() => {
  60 + this.selection.clear();
  61 + }),
  62 + catchError(() => of(emptyPageData<EntityRelationInfo>())),
  63 + ).subscribe(
  64 + (pageData) => {
  65 + this.relationsSubject.next(pageData.data);
  66 + this.pageDataSubject.next(pageData);
  67 + result.next(pageData);
  68 + }
  69 + );
  70 + return result;
  71 + }
  72 +
  73 + fetchRelations(direction: EntitySearchDirection, entityId: EntityId,
  74 + pageLink: PageLink): Observable<PageData<EntityRelationInfo>> {
  75 + return this.getAllRelations(direction, entityId).pipe(
  76 + map((data) => pageLink.filterData(data))
  77 + );
  78 + }
  79 +
  80 + getAllRelations(direction: EntitySearchDirection, entityId: EntityId): Observable<Array<EntityRelationInfo>> {
  81 + if (!this.allRelations) {
  82 + let relationsObservable: Observable<Array<EntityRelationInfo>>;
  83 + switch (direction) {
  84 + case EntitySearchDirection.FROM:
  85 + relationsObservable = this.entityRelationService.findInfoByFrom(entityId);
  86 + break;
  87 + case EntitySearchDirection.TO:
  88 + relationsObservable = this.entityRelationService.findInfoByTo(entityId);
  89 + break;
  90 + }
  91 + this.allRelations = relationsObservable.pipe(
  92 + map(relations => {
  93 + relations.forEach(relation => {
  94 + if (direction === EntitySearchDirection.FROM) {
  95 + relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type);
  96 + } else {
  97 + relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type);
  98 + }
  99 + });
  100 + return relations;
  101 + }),
  102 + publishReplay(1),
  103 + refCount()
  104 + );
  105 + }
  106 + return this.allRelations;
  107 + }
  108 +
  109 + isAllSelected(): Observable<boolean> {
  110 + const numSelected = this.selection.selected.length;
  111 + return this.relationsSubject.pipe(
  112 + map((relations) => numSelected === relations.length)
  113 + );
  114 + }
  115 +
  116 + isEmpty(): Observable<boolean> {
  117 + return this.relationsSubject.pipe(
  118 + map((relations) => !relations.length)
  119 + );
  120 + }
  121 +
  122 + total(): Observable<number> {
  123 + return this.pageDataSubject.pipe(
  124 + map((pageData) => pageData.totalElements)
  125 + );
  126 + }
  127 +
  128 + masterToggle() {
  129 + this.relationsSubject.pipe(
  130 + tap((relations) => {
  131 + const numSelected = this.selection.selected.length;
  132 + if (numSelected === relations.length) {
  133 + this.selection.clear();
  134 + } else {
  135 + relations.forEach(row => {
  136 + this.selection.select(row);
  137 + });
  138 + }
  139 + }),
  140 + take(1)
  141 + ).subscribe();
  142 + }
  143 +}
... ...
... ... @@ -20,6 +20,10 @@
20 20 <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
21 21 [entityId]="entity.id"></tb-event-table>
22 22 </mat-tab>
  23 +<mat-tab *ngIf="entity"
  24 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  25 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  26 +</mat-tab>
23 27 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
24 28 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
25 29 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -14,16 +14,17 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { BaseData, HasId } from '@shared/models/base-data';
  17 +import { PageLink } from '@shared/models/page/page-link';
  18 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
18 19
19   -export interface PageData<T extends BaseData<HasId>> {
  20 +export interface PageData<T> {
20 21 data: Array<T>;
21 22 totalPages: number;
22 23 totalElements: number;
23 24 hasNext: boolean;
24 25 }
25 26
26   -export function emptyPageData<T extends BaseData<HasId>>(): PageData<T> {
  27 +export function emptyPageData<T>(): PageData<T> {
27 28 return {
28 29 data: [],
29 30 totalPages: 0,
... ...
... ... @@ -15,6 +15,27 @@
15 15 ///
16 16
17 17 import { Direction, SortOrder } from '@shared/models/page/sort-order';
  18 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  19 +
  20 +export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
  21 +
  22 +const defaultPageLinkSearchFunction: PageLinkSearchFunction<any> =
  23 + (entity: any, textSearch: string) => {
  24 + if (textSearch === null || !textSearch.length) {
  25 + return true;
  26 + }
  27 + const expected = ('' + textSearch).toLowerCase();
  28 + for (const key of Object.keys(entity)) {
  29 + const val = entity[key];
  30 + if (val !== null && val !== Object(val)) {
  31 + const actual = ('' + val).toLowerCase();
  32 + if (actual.indexOf(expected) !== -1) {
  33 + return true;
  34 + }
  35 + }
  36 + }
  37 + return false;
  38 + };
18 39
19 40 export class PageLink {
20 41
... ... @@ -65,6 +86,25 @@ export class PageLink {
65 86 return 0;
66 87 }
67 88
  89 + public filterData<T>(data: Array<T>,
  90 + searchFunction: PageLinkSearchFunction<T> = defaultPageLinkSearchFunction): PageData<T> {
  91 + const pageData = emptyPageData<T>();
  92 + pageData.data = [...data];
  93 + if (this.textSearch && this.textSearch.length) {
  94 + pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch));
  95 + }
  96 + pageData.totalElements = pageData.data.length;
  97 + pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
  98 + if (this.sortOrder) {
  99 + pageData.data = pageData.data.sort(this.sort);
  100 + }
  101 + const startIndex = this.pageSize * this.page;
  102 + const endIndex = startIndex + this.pageSize;
  103 + pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize);
  104 + pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length;
  105 + return pageData;
  106 + }
  107 +
68 108 }
69 109
70 110 export class TimePageLink extends PageLink {
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { EntityId } from '@shared/models/id/entity-id';
  18 +import { EntityType } from '@shared/models/entity-type.models';
  19 +import { ActionStatus } from '@shared/models/audit-log.models';
  20 +
  21 +export const CONTAINS_TYPE = 'Contains';
  22 +export const MANAGES_TYPE = 'Manages';
  23 +
  24 +export const RelationTypes = [
  25 + CONTAINS_TYPE,
  26 + MANAGES_TYPE
  27 +];
  28 +
  29 +export enum RelationTypeGroup {
  30 + COMMON = 'COMMON',
  31 + ALARM = 'ALARM',
  32 + DASHBOARD = 'DASHBOARD',
  33 + RULE_CHAIN = 'RULE_CHAIN',
  34 + RULE_NODE = 'RULE_NODE',
  35 +}
  36 +
  37 +export enum EntitySearchDirection {
  38 + FROM = 'FROM',
  39 + TO = 'TO'
  40 +}
  41 +
  42 +export const entitySearchDirectionTranslations = new Map<EntitySearchDirection, string>(
  43 + [
  44 + [EntitySearchDirection.FROM, 'relation.search-direction.FROM'],
  45 + [EntitySearchDirection.TO, 'relation.search-direction.TO'],
  46 + ]
  47 +);
  48 +
  49 +export const directionTypeTranslations = new Map<EntitySearchDirection, string>(
  50 + [
  51 + [EntitySearchDirection.FROM, 'relation.direction-type.FROM'],
  52 + [EntitySearchDirection.TO, 'relation.direction-type.TO'],
  53 + ]
  54 +);
  55 +
  56 +export interface EntityTypeFilter {
  57 + relationType: string;
  58 + entityTypes: Array<EntityType>;
  59 +}
  60 +
  61 +export interface RelationsSearchParameters {
  62 + rootId: string;
  63 + rootType: EntityType;
  64 + direction: EntitySearchDirection;
  65 + relationTypeGroup: RelationTypeGroup;
  66 + maxLevel: number;
  67 +}
  68 +
  69 +export interface EntityRelationsQuery {
  70 + parameters: RelationsSearchParameters;
  71 + filters: Array<EntityTypeFilter>;
  72 +}
  73 +
  74 +export interface EntityRelation {
  75 + from: EntityId;
  76 + to: EntityId;
  77 + type: string;
  78 + typeGroup: RelationTypeGroup;
  79 + additionalInfo?: any;
  80 +}
  81 +
  82 +export interface EntityRelationInfo extends EntityRelation {
  83 + fromName: string;
  84 + toEntityTypeName?: string;
  85 + toName: string;
  86 + fromEntityTypeName?: string;
  87 +}
... ...
... ... @@ -1278,7 +1278,8 @@
1278 1278 "any-relation": "Any relation",
1279 1279 "relation-filters": "Relation filters",
1280 1280 "additional-info": "Additional info (JSON)",
1281   - "invalid-additional-info": "Unable to parse additional info json."
  1281 + "invalid-additional-info": "Unable to parse additional info json.",
  1282 + "no-relations-text": "No relations found"
1282 1283 },
1283 1284 "rulechain": {
1284 1285 "rulechain": "Rule chain",
... ... @@ -1749,7 +1750,7 @@
1749 1750 "tr_TR": "Turkish",
1750 1751 "fa_IR": "Persian",
1751 1752 "uk_UA": "Ukrainian",
1752   - "cs_CZ": "Czech"
  1753 + "cs_CZ": "Czech"
1753 1754 }
1754 1755 }
1755 1756 }
... ...