11 changed files with 846 additions and 7 deletions
@ -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)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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> </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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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:
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
Loading…
Reference in new issue