From 851a3657dbfffa9e220fbf1970c662063e6596a8 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 23 Aug 2019 15:06:42 +0300 Subject: [PATCH] Entity Relations Table --- .../app/core/http/entity-relation.service.ts | 113 +++++++++ .../home/components/home-components.module.ts | 7 +- .../relation/relation-table.component.html | 168 +++++++++++++ .../relation/relation-table.component.scss | 54 +++++ .../relation/relation-table.component.ts | 225 ++++++++++++++++++ .../models/datasource/relation-datasource.ts | 143 +++++++++++ .../pages/device/device-tabs.component.html | 4 + .../src/app/shared/models/page/page-data.ts | 7 +- .../src/app/shared/models/page/page-link.ts | 40 ++++ .../src/app/shared/models/relation.models.ts | 87 +++++++ .../assets/locale/locale.constant-en_US.json | 5 +- 11 files changed, 846 insertions(+), 7 deletions(-) create mode 100644 ui-ngx/src/app/core/http/entity-relation.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/relation/relation-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts create mode 100644 ui-ngx/src/app/shared/models/relation.models.ts diff --git a/ui-ngx/src/app/core/http/entity-relation.service.ts b/ui-ngx/src/app/core/http/entity-relation.service.ts new file mode 100644 index 0000000000..b11b67ec4a --- /dev/null +++ b/ui-ngx/src/app/core/http/entity-relation.service.ts @@ -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 { + return this.http.post('/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 { + return this.http.get(`/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> { + return this.http.get>( + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public findInfoByFrom(fromId: EntityId, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.get>( + `/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> { + return this.http.get>( + `/api/relations?fromId=${fromId.id}&fromType=${fromId.entityType}&relationType=${relationType}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public findByTo(toId: EntityId, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.get>( + `/api/relations?toId=${toId.id}&toType=${toId.entityType}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public findInfoByTo(toId: EntityId, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.get>( + `/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> { + return this.http.get>( + `/api/relations?toId=${toId.id}&toType=${toId.entityType}&relationType=${relationType}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public findByQuery(query: EntityRelationsQuery, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.post>( + '/api/relations', query, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public findInfoByQuery(query: EntityRelationsQuery, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + return this.http.post>( + '/api/relations/info', query, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index d2e7f1e4b8..daeb05e3fa 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -26,6 +26,7 @@ import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-di import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableComponent } from '@home/components/event/event-table.component'; +import { RelationTableComponent } from '@home/components/relation/relation-table.component'; @NgModule({ entryComponents: [ @@ -43,7 +44,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen AuditLogTableComponent, AuditLogDetailsDialogComponent, EventTableHeaderComponent, - EventTableComponent + EventTableComponent, + RelationTableComponent ], imports: [ CommonModule, @@ -56,7 +58,8 @@ import { EventTableComponent } from '@home/components/event/event-table.componen EntityDetailsPanelComponent, ContactComponent, AuditLogTableComponent, - EventTableComponent + EventTableComponent, + RelationTableComponent ] }) export class HomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html new file mode 100644 index 0000000000..183f14aa0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -0,0 +1,168 @@ + +
+
+ +
+ {{(direction == directions.FROM ? + 'relation.from-relations' : 'relation.to-relations') | translate}} + + relation.direction + + + {{ directionTypeTranslations.get(type) | translate }} + + + + + + + +
+
+ +
+ + +   + + + +
+
+ +
+ + {{ translate.get('relation.selected-relations', {count: dataSource.selection.selected.length}) | async }} + + + +
+
+
+ + + + + + + + + + + + + {{ 'relation.type' | translate }} + + {{ relation.type }} + + + + {{ 'relation.to-entity-type' | translate }} + + {{ relation.toEntityTypeName }} + + + + {{ 'relation.to-entity-name' | translate }} + + {{ relation.toName }} + + + + {{ 'relation.from-entity-type' | translate }} + + {{ relation.fromEntityTypeName }} + + + + {{ 'relation.from-entity-name' | translate }} + + {{ relation.fromName }} + + + + + + +
+ + +
+
+
+ + +
+ {{ 'relation.no-relations-text' }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss new file mode 100644 index 0000000000..a2a6b728b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts new file mode 100644 index 0000000000..fddf1db15a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -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, + 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: + } + + +} diff --git a/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts new file mode 100644 index 0000000000..12a5c5252d --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts @@ -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 { + + private relationsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + public selection = new SelectionModel(true, []); + + private allRelations: Observable>; + + constructor(private entityRelationService: EntityRelationService, + private translate: TranslateService) {} + + connect(collectionViewer: CollectionViewer): Observable> { + 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> { + if (reload) { + this.allRelations = null; + } + const result = new ReplaySubject>(); + this.fetchRelations(direction, entityId, pageLink).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.relationsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchRelations(direction: EntitySearchDirection, entityId: EntityId, + pageLink: PageLink): Observable> { + return this.getAllRelations(direction, entityId).pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllRelations(direction: EntitySearchDirection, entityId: EntityId): Observable> { + if (!this.allRelations) { + let relationsObservable: Observable>; + 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 { + const numSelected = this.selection.selected.length; + return this.relationsSubject.pipe( + map((relations) => numSelected === relations.length) + ); + } + + isEmpty(): Observable { + return this.relationsSubject.pipe( + map((relations) => !relations.length) + ); + } + + total(): Observable { + 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(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html index 1b68496f6f..cdcc19e08c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html @@ -20,6 +20,10 @@ + + + diff --git a/ui-ngx/src/app/shared/models/page/page-data.ts b/ui-ngx/src/app/shared/models/page/page-data.ts index 76d1438d4e..64f6e96a19 100644 --- a/ui-ngx/src/app/shared/models/page/page-data.ts +++ b/ui-ngx/src/app/shared/models/page/page-data.ts @@ -14,16 +14,17 @@ /// limitations under the License. /// -import { BaseData, HasId } from '@shared/models/base-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; -export interface PageData> { +export interface PageData { data: Array; totalPages: number; totalElements: number; hasNext: boolean; } -export function emptyPageData>(): PageData { +export function emptyPageData(): PageData { return { data: [], totalPages: 0, diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index e2cf956b2b..4be0264466 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -15,6 +15,27 @@ /// import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; + +export type PageLinkSearchFunction = (entity: T, textSearch: string) => boolean; + +const defaultPageLinkSearchFunction: PageLinkSearchFunction = + (entity: any, textSearch: string) => { + if (textSearch === null || !textSearch.length) { + return true; + } + const expected = ('' + textSearch).toLowerCase(); + for (const key of Object.keys(entity)) { + const val = entity[key]; + if (val !== null && val !== Object(val)) { + const actual = ('' + val).toLowerCase(); + if (actual.indexOf(expected) !== -1) { + return true; + } + } + } + return false; + }; export class PageLink { @@ -65,6 +86,25 @@ export class PageLink { return 0; } + public filterData(data: Array, + searchFunction: PageLinkSearchFunction = defaultPageLinkSearchFunction): PageData { + const pageData = emptyPageData(); + pageData.data = [...data]; + if (this.textSearch && this.textSearch.length) { + pageData.data = pageData.data.filter((entity) => searchFunction(entity, this.textSearch)); + } + pageData.totalElements = pageData.data.length; + pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize); + if (this.sortOrder) { + pageData.data = pageData.data.sort(this.sort); + } + const startIndex = this.pageSize * this.page; + const endIndex = startIndex + this.pageSize; + pageData.data = pageData.data.slice(startIndex, startIndex + this.pageSize); + pageData.hasNext = pageData.totalElements > startIndex + pageData.data.length; + return pageData; + } + } export class TimePageLink extends PageLink { diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts new file mode 100644 index 0000000000..a33e66fd3c --- /dev/null +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -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.FROM, 'relation.search-direction.FROM'], + [EntitySearchDirection.TO, 'relation.search-direction.TO'], + ] +); + +export const directionTypeTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'relation.direction-type.FROM'], + [EntitySearchDirection.TO, 'relation.direction-type.TO'], + ] +); + +export interface EntityTypeFilter { + relationType: string; + entityTypes: Array; +} + +export interface RelationsSearchParameters { + rootId: string; + rootType: EntityType; + direction: EntitySearchDirection; + relationTypeGroup: RelationTypeGroup; + maxLevel: number; +} + +export interface EntityRelationsQuery { + parameters: RelationsSearchParameters; + filters: Array; +} + +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; +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e1f090b863..c87564f227 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1278,7 +1278,8 @@ "any-relation": "Any relation", "relation-filters": "Relation filters", "additional-info": "Additional info (JSON)", - "invalid-additional-info": "Unable to parse additional info json." + "invalid-additional-info": "Unable to parse additional info json.", + "no-relations-text": "No relations found" }, "rulechain": { "rulechain": "Rule chain", @@ -1749,7 +1750,7 @@ "tr_TR": "Turkish", "fa_IR": "Persian", "uk_UA": "Ukrainian", - "cs_CZ": "Czech" + "cs_CZ": "Czech" } } }