Showing
11 changed files
with
846 additions
and
7 deletions
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> </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 | } | ... | ... |