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,6 +26,7 @@ import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-di | ||
26 | import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; | 26 | import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; |
27 | import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; | 27 | import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; |
28 | import { EventTableComponent } from '@home/components/event/event-table.component'; | 28 | import { EventTableComponent } from '@home/components/event/event-table.component'; |
29 | +import { RelationTableComponent } from '@home/components/relation/relation-table.component'; | ||
29 | 30 | ||
30 | @NgModule({ | 31 | @NgModule({ |
31 | entryComponents: [ | 32 | entryComponents: [ |
@@ -43,7 +44,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen | @@ -43,7 +44,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen | ||
43 | AuditLogTableComponent, | 44 | AuditLogTableComponent, |
44 | AuditLogDetailsDialogComponent, | 45 | AuditLogDetailsDialogComponent, |
45 | EventTableHeaderComponent, | 46 | EventTableHeaderComponent, |
46 | - EventTableComponent | 47 | + EventTableComponent, |
48 | + RelationTableComponent | ||
47 | ], | 49 | ], |
48 | imports: [ | 50 | imports: [ |
49 | CommonModule, | 51 | CommonModule, |
@@ -56,7 +58,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen | @@ -56,7 +58,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen | ||
56 | EntityDetailsPanelComponent, | 58 | EntityDetailsPanelComponent, |
57 | ContactComponent, | 59 | ContactComponent, |
58 | AuditLogTableComponent, | 60 | AuditLogTableComponent, |
59 | - EventTableComponent | 61 | + EventTableComponent, |
62 | + RelationTableComponent | ||
60 | ] | 63 | ] |
61 | }) | 64 | }) |
62 | export class HomeComponentsModule { } | 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,6 +20,10 @@ | ||
20 | <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id" | 20 | <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id" |
21 | [entityId]="entity.id"></tb-event-table> | 21 | [entityId]="entity.id"></tb-event-table> |
22 | </mat-tab> | 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 | <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN" | 27 | <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN" |
24 | label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab"> | 28 | label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab"> |
25 | <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table> | 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,16 +14,17 @@ | ||
14 | /// limitations under the License. | 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 | data: Array<T>; | 21 | data: Array<T>; |
21 | totalPages: number; | 22 | totalPages: number; |
22 | totalElements: number; | 23 | totalElements: number; |
23 | hasNext: boolean; | 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 | return { | 28 | return { |
28 | data: [], | 29 | data: [], |
29 | totalPages: 0, | 30 | totalPages: 0, |
@@ -15,6 +15,27 @@ | @@ -15,6 +15,27 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { Direction, SortOrder } from '@shared/models/page/sort-order'; | 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 | export class PageLink { | 40 | export class PageLink { |
20 | 41 | ||
@@ -65,6 +86,25 @@ export class PageLink { | @@ -65,6 +86,25 @@ export class PageLink { | ||
65 | return 0; | 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 | export class TimePageLink extends PageLink { | 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,7 +1278,8 @@ | ||
1278 | "any-relation": "Any relation", | 1278 | "any-relation": "Any relation", |
1279 | "relation-filters": "Relation filters", | 1279 | "relation-filters": "Relation filters", |
1280 | "additional-info": "Additional info (JSON)", | 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 | "rulechain": { | 1284 | "rulechain": { |
1284 | "rulechain": "Rule chain", | 1285 | "rulechain": "Rule chain", |
@@ -1749,7 +1750,7 @@ | @@ -1749,7 +1750,7 @@ | ||
1749 | "tr_TR": "Turkish", | 1750 | "tr_TR": "Turkish", |
1750 | "fa_IR": "Persian", | 1751 | "fa_IR": "Persian", |
1751 | "uk_UA": "Ukrainian", | 1752 | "uk_UA": "Ukrainian", |
1752 | - "cs_CZ": "Czech" | 1753 | + "cs_CZ": "Czech" |
1753 | } | 1754 | } |
1754 | } | 1755 | } |
1755 | } | 1756 | } |