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 daeb05e3fa..3cf870f96d 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
@@ -27,12 +27,14 @@ 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';
+import { RelationDialogComponent } from './relation/relation-dialog.component';
@NgModule({
entryComponents: [
AddEntityDialogComponent,
AuditLogDetailsDialogComponent,
- EventTableHeaderComponent
+ EventTableHeaderComponent,
+ RelationDialogComponent
],
declarations:
[
@@ -45,7 +47,8 @@ import { RelationTableComponent } from '@home/components/relation/relation-table
AuditLogDetailsDialogComponent,
EventTableHeaderComponent,
EventTableComponent,
- RelationTableComponent
+ RelationTableComponent,
+ RelationDialogComponent
],
imports: [
CommonModule,
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html
new file mode 100644
index 0000000000..f8b5ff439f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html
@@ -0,0 +1,68 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss
new file mode 100644
index 0000000000..dfbd362f33
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.scss
@@ -0,0 +1,18 @@
+/**
+ * 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 {
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts
new file mode 100644
index 0000000000..5bd216c10f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts
@@ -0,0 +1,134 @@
+///
+/// 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 { Component, Inject, OnInit, SkipSelf } from '@angular/core';
+import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
+import { PageComponent } from '@shared/components/page.component';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
+import {
+ CONTAINS_TYPE,
+ EntityRelation,
+ EntitySearchDirection,
+ RelationTypeGroup
+} from '@shared/models/relation.models';
+import { EntityRelationService } from '@core/http/entity-relation.service';
+import { EntityId } from '@shared/models/id/entity-id';
+import { Observable, forkJoin } from 'rxjs';
+
+export interface RelationDialogData {
+ isAdd: boolean;
+ direction: EntitySearchDirection;
+ relation: EntityRelation;
+}
+
+@Component({
+ selector: 'tb-relation-dialog',
+ templateUrl: './relation-dialog.component.html',
+ providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}],
+ styleUrls: ['./relation-dialog.component.scss']
+})
+export class RelationDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
+
+ relationFormGroup: FormGroup;
+
+ isAdd: boolean;
+ direction: EntitySearchDirection;
+ entitySearchDirection = EntitySearchDirection;
+
+ submitted = false;
+
+ constructor(protected store: Store,
+ @Inject(MAT_DIALOG_DATA) public data: RelationDialogData,
+ private entityRelationService: EntityRelationService,
+ @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
+ public dialogRef: MatDialogRef,
+ public fb: FormBuilder) {
+ super(store);
+ this.isAdd = data.isAdd;
+ this.direction = data.direction;
+ }
+
+ ngOnInit(): void {
+ this.relationFormGroup = this.fb.group({
+ type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]],
+ targetEntityIds: [this.isAdd ? null :
+ [this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from],
+ [Validators.required]],
+ additionalInfo: [this.data.relation.additionalInfo]
+ });
+ if (!this.isAdd) {
+ this.relationFormGroup.get('type').disable();
+ this.relationFormGroup.get('targetEntityIds').disable();
+ }
+ this.relationFormGroup.valueChanges.subscribe(
+ () => {
+ this.submitted = false;
+ }
+ );
+ }
+
+ isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+ const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
+ const customErrorState = !!(control && control.invalid && this.submitted);
+ return originalErrorState || customErrorState;
+ }
+
+ cancel(): void {
+ this.dialogRef.close(false);
+ }
+
+ save(): void {
+ this.submitted = true;
+ if (this.relationFormGroup.valid) {
+ const additionalInfo = this.relationFormGroup.get('additionalInfo').value;
+ if (this.isAdd) {
+ const tasks: Observable[] = [];
+ const type: string = this.relationFormGroup.get('type').value;
+ const entityIds: Array = this.relationFormGroup.get('targetEntityIds').value;
+ entityIds.forEach(entityId => {
+ const relation = {
+ type,
+ additionalInfo,
+ typeGroup: RelationTypeGroup.COMMON
+ } as EntityRelation;
+ if (this.direction === EntitySearchDirection.FROM) {
+ relation.from = this.data.relation.from;
+ relation.to = entityId;
+ } else {
+ relation.from = entityId;
+ relation.to = this.data.relation.to;
+ }
+ tasks.push(this.entityRelationService.saveRelation(relation));
+ });
+ forkJoin(tasks).subscribe(
+ () => {
+ this.dialogRef.close(true);
+ }
+ );
+ } else {
+ const relation: EntityRelation = {...this.data.relation};
+ relation.additionalInfo = additionalInfo;
+ this.entityRelationService.saveRelation(relation).subscribe(
+ () => {
+ this.dialogRef.close(true);
+ }
+ );
+ }
+ }
+ }
+}
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
index fddf1db15a..26f7deb804 100644
--- 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
@@ -26,12 +26,18 @@ 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 { forkJoin, fromEvent, merge, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
-import { EntityRelationInfo, EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models';
+import {
+ EntityRelation,
+ EntityRelationInfo,
+ EntitySearchDirection,
+ entitySearchDirectionTranslations,
+ RelationTypeGroup
+} 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';
+import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
@Component({
selector: 'tb-relation-table',
@@ -201,8 +207,35 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
if ($event) {
$event.stopPropagation();
}
+ let title;
+ let content;
+ if (this.direction === EntitySearchDirection.FROM) {
+ title = this.translate.instant('relation.delete-to-relation-title', {entityName: relation.toName});
+ content = this.translate.instant('relation.delete-to-relation-text', {entityName: relation.toName});
+ } else {
+ title = this.translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName});
+ content = this.translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName});
+ }
- // TODO:
+ this.dialogService.confirm(
+ title,
+ content,
+ this.translate.instant('action.no'),
+ this.translate.instant('action.yes'),
+ true
+ ).subscribe((result) => {
+ if (result) {
+ this.entityRelationService.deleteRelation(
+ relation.from,
+ relation.type,
+ relation.to
+ ).subscribe(
+ () => {
+ this.reloadRelations();
+ }
+ );
+ }
+ });
}
deleteRelations($event: Event) {
@@ -210,16 +243,79 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
$event.stopPropagation();
}
if (this.dataSource.selection.selected.length > 0) {
- // TODO:
+ let title;
+ let content;
+
+ if (this.direction === EntitySearchDirection.FROM) {
+ title = this.translate.instant('relation.delete-to-relations-title', {count: this.dataSource.selection.selected.length});
+ content = this.translate.instant('relation.delete-to-relations-text');
+ } else {
+ title = this.translate.instant('relation.delete-from-relations-title', {count: this.dataSource.selection.selected.length});
+ content = this.translate.instant('relation.delete-from-relations-text');
+ }
+
+ this.dialogService.confirm(
+ title,
+ content,
+ this.translate.instant('action.no'),
+ this.translate.instant('action.yes'),
+ true
+ ).subscribe((result) => {
+ if (result) {
+ const tasks: Observable[] = [];
+ this.dataSource.selection.selected.forEach((relation) => {
+ tasks.push(this.entityRelationService.deleteRelation(
+ relation.from,
+ relation.type,
+ relation.to
+ ));
+ });
+ forkJoin(tasks).subscribe(
+ () => {
+ this.reloadRelations();
+ }
+ );
+ }
+ });
}
}
- openRelationDialog($event: Event, relation: EntityRelationInfo = null) {
+ openRelationDialog($event: Event, relation: EntityRelation = null) {
if ($event) {
$event.stopPropagation();
}
- // TODO:
- }
+ let isAdd = false;
+ if (!relation) {
+ isAdd = true;
+ relation = {
+ from: null,
+ to: null,
+ type: null,
+ typeGroup: RelationTypeGroup.COMMON
+ };
+ if (this.direction === EntitySearchDirection.FROM) {
+ relation.from = this.entityIdValue;
+ } else {
+ relation.to = this.entityIdValue;
+ }
+ }
+
+ this.dialog.open(RelationDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ isAdd,
+ direction: this.direction,
+ relation: {...relation}
+ }
+ }).afterClosed().subscribe(
+ (res) => {
+ if (res) {
+ this.reloadRelations();
+ }
+ }
+ );
+ }
}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html
new file mode 100644
index 0000000000..6a5a117cd3
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+ 0}"
+ fxFlex
+ *ngIf="modelValue.entityType"
+ [required]="required"
+ [entityType]="modelValue.entityType"
+ formControlName="entityIds">
+
+
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss
new file mode 100644
index 0000000000..21da8f6580
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.scss
@@ -0,0 +1,32 @@
+/**
+ * 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 {
+}
+
+:host ::ng-deep {
+ tb-entity-list {
+ &.tb-not-empty {
+ .mat-form-field-flex {
+ padding-top: 0;
+ }
+ }
+ .mat-form-field-flex {
+ .mat-form-field-infix {
+ border-top: 0;
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts
new file mode 100644
index 0000000000..c10d37d9a8
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts
@@ -0,0 +1,170 @@
+///
+/// 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, Component, forwardRef, Input, OnInit} from '@angular/core';
+import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Store} from '@ngrx/store';
+import {AppState} from '@core/core.state';
+import {TranslateService} from '@ngx-translate/core';
+import {AliasEntityType, EntityType, entityTypeTranslations} from '@shared/models/entity-type.models';
+import {EntityService} from '@core/http/entity.service';
+import {EntityId} from '@shared/models/id/entity-id';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
+
+interface EntityListSelectModel {
+ entityType: EntityType | AliasEntityType;
+ ids: Array;
+}
+
+@Component({
+ selector: 'tb-entity-list-select',
+ templateUrl: './entity-list-select.component.html',
+ styleUrls: ['./entity-list-select.component.scss'],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => EntityListSelectComponent),
+ multi: true
+ }]
+})
+
+export class EntityListSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit {
+
+ entityListSelectFormGroup: FormGroup;
+
+ modelValue: EntityListSelectModel = {entityType: null, ids: []};
+
+ @Input()
+ allowedEntityTypes: Array;
+
+ @Input()
+ useAliasEntityTypes: boolean;
+
+ private requiredValue: boolean;
+ get required(): boolean {
+ return this.requiredValue;
+ }
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ @Input()
+ disabled: boolean;
+
+ displayEntityTypeSelect: boolean;
+
+ private defaultEntityType: EntityType | AliasEntityType = null;
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private store: Store,
+ private entityService: EntityService,
+ public translate: TranslateService,
+ private fb: FormBuilder) {
+
+ const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes,
+ this.useAliasEntityTypes);
+ if (entityTypes.length === 1) {
+ this.displayEntityTypeSelect = false;
+ this.defaultEntityType = entityTypes[0];
+ } else {
+ this.displayEntityTypeSelect = true;
+ }
+
+ this.entityListSelectFormGroup = this.fb.group({
+ entityType: [this.defaultEntityType],
+ entityIds: [[]]
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ ngOnInit() {
+ this.entityListSelectFormGroup.get('entityType').valueChanges.subscribe(
+ (value) => {
+ this.updateView(value, this.modelValue.ids);
+ }
+ );
+ this.entityListSelectFormGroup.get('entityIds').valueChanges.subscribe(
+ (values) => {
+ this.updateView(this.modelValue.entityType, values);
+ }
+ );
+ }
+
+ ngAfterViewInit(): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.entityListSelectFormGroup.disable();
+ } else {
+ this.entityListSelectFormGroup.enable();
+ }
+ }
+
+ writeValue(value: Array | null): void {
+ if (value != null && value.length > 0) {
+ const id = value[0];
+ this.modelValue = {
+ entityType: id.entityType,
+ ids: value.map(val => val.id)
+ };
+ } else {
+ this.modelValue = {
+ entityType: this.defaultEntityType,
+ ids: []
+ };
+ }
+ this.entityListSelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: true});
+ this.entityListSelectFormGroup.get('entityIds').patchValue([...this.modelValue.ids], {emitEvent: true});
+ }
+
+ updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array | null) {
+ if (this.modelValue.entityType !== entityType ||
+ !this.compareIds(this.modelValue.ids, entityIds)) {
+ this.modelValue = {
+ entityType,
+ ids: this.modelValue.entityType !== entityType || !entityIds ? [] : [...entityIds]
+ };
+ this.propagateChange(this.toEntityIds(this.modelValue));
+ }
+ }
+
+ compareIds(ids1: Array | null, ids2: Array | null): boolean {
+ if (ids1 !== null && ids2 !== null) {
+ return JSON.stringify(ids1) === JSON.stringify(ids2);
+ } else {
+ return ids1 === ids2;
+ }
+ }
+
+ toEntityIds(modelValue: EntityListSelectModel): Array {
+ if (modelValue !== null && modelValue.entityType && modelValue.ids && modelValue.ids.length > 0) {
+ const entityType = modelValue.entityType;
+ return modelValue.ids.map(id => ({entityType, id}));
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html
index 435427716e..8e0bae6b53 100644
--- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html
+++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html
@@ -16,7 +16,7 @@
-->
-
+
-
+
{{ 'entity.entity-list-empty' | translate }}
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
index 8ab9600dd6..d0af0f533d 100644
--- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
+++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
@@ -14,16 +14,26 @@
/// limitations under the License.
///
-import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild} from '@angular/core';
+import {
+ AfterContentInit,
+ AfterViewInit,
+ Component,
+ ElementRef,
+ forwardRef,
+ Input,
+ OnInit,
+ SkipSelf,
+ ViewChild
+} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
- NG_VALUE_ACCESSOR, NgForm
+ NG_VALUE_ACCESSOR, NgForm, Validators
} from '@angular/forms';
-import {Observable} from 'rxjs';
+import {Observable, of} from 'rxjs';
import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
@@ -34,6 +44,7 @@ import {EntityId} from '@shared/models/id/entity-id';
import {EntityService} from '@core/http/entity.service';
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import { emptyPageData } from '@shared/models/page/page-data';
@Component({
selector: 'tb-entity-list',
@@ -69,7 +80,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
}
@Input()
set required(value: boolean) {
- this.requiredValue = coerceBooleanProperty(value);
+ const newVal = coerceBooleanProperty(value);
+ if (this.requiredValue !== newVal) {
+ this.requiredValue = newVal;
+ this.updateValidators();
+ }
}
@Input()
@@ -77,7 +92,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
@ViewChild('entityInput', {static: false}) entityInput: ElementRef;
@ViewChild('entityAutocomplete', {static: false}) matAutocomplete: MatAutocomplete;
- @ViewChild('chipList', {static: false}) chipList: MatChipList;
+ @ViewChild('chipList', {static: true}) chipList: MatChipList;
entities: Array> = [];
filteredEntities: Observable>>;
@@ -91,10 +106,16 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
private entityService: EntityService,
private fb: FormBuilder) {
this.entityListFormGroup = this.fb.group({
+ entities: [this.entities, this.required ? [Validators.required] : []],
entity: [null]
});
}
+ updateValidators() {
+ this.entityListFormGroup.get('entities').setValidators(this.required ? [Validators.required] : []);
+ this.entityListFormGroup.get('entities').updateValueAndValidity();
+ }
+
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@@ -120,34 +141,39 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
);
}
- ngAfterViewInit(): void {}
+ ngAfterViewInit(): void {
+ }
setDisabledState(isDisabled: boolean): void {
+ const emitEvent = this.disabled !== isDisabled;
this.disabled = isDisabled;
- if (this.disabled) {
- this.entityListFormGroup.disable();
+ if (isDisabled) {
+ this.entityListFormGroup.disable({emitEvent});
} else {
- this.entityListFormGroup.enable();
+ this.entityListFormGroup.enable({emitEvent});
}
}
writeValue(value: Array | null): void {
this.searchText = '';
- if (value != null) {
+ if (value != null && value.length > 0) {
this.modelValue = [...value];
this.entityService.getEntities(this.entityTypeValue, value).subscribe(
(entities) => {
this.entities = entities;
+ this.entityListFormGroup.get('entities').setValue(this.entities);
}
);
} else {
this.entities = [];
+ this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null;
}
}
reset() {
this.entities = [];
+ this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null;
this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true});
this.propagateChange(this.modelValue);
@@ -160,9 +186,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
}
this.modelValue.push(entity.id.id);
this.entities.push(entity);
- if (this.required) {
- this.chipList.errorState = false;
- }
+ this.entityListFormGroup.get('entities').setValue(this.entities);
}
this.propagateChange(this.modelValue);
this.clear();
@@ -172,12 +196,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
const index = this.entities.indexOf(entity);
if (index >= 0) {
this.entities.splice(index, 1);
+ this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
- if (this.required) {
- this.chipList.errorState = true;
- }
}
this.propagateChange(this.modelValue);
this.clear();
@@ -190,7 +212,8 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
fetchEntities(searchText?: string): Observable>> {
this.searchText = searchText;
- return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
+ return this.disabled ? of([]) :
+ this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
50, '', false, true).pipe(
map((data) => data ? data : []));
}
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html
new file mode 100644
index 0000000000..a723cf1c33
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.scss b/ui-ngx/src/app/shared/components/json-object-edit.component.scss
new file mode 100644
index 0000000000..913da86348
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/json-object-edit.component.scss
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ */
+
+/**
+ * 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 {
+ position: relative;
+
+ .fill-height {
+ height: 100%;
+ }
+
+ .tb-json-object-panel {
+ height: 100%;
+ margin-left: 15px;
+ border: 1px solid #c0c0c0;
+
+ #tb-json-input {
+ width: 100%;
+ min-width: 200px;
+ height: 100%;
+
+ &:not(.fill-height) {
+ min-height: 200px;
+ }
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts
new file mode 100644
index 0000000000..72f64e1bb7
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts
@@ -0,0 +1,180 @@
+///
+/// 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 {
+ Attribute,
+ Component,
+ ElementRef,
+ forwardRef,
+ Input,
+ OnInit,
+ ViewChild
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, Validator, NG_VALIDATORS } from '@angular/forms';
+import * as ace from 'ace-builds';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+
+@Component({
+ selector: 'tb-json-object-edit',
+ templateUrl: './json-object-edit.component.html',
+ styleUrls: ['./json-object-edit.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => JsonObjectEditComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => JsonObjectEditComponent),
+ multi: true,
+ }
+ ]
+})
+export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator {
+
+ @ViewChild('jsonEditor', {static: true})
+ jsonEditorElmRef: ElementRef;
+
+ private jsonEditor: ace.Ace.Editor;
+
+ @Input() label: string;
+
+ @Input() disabled: boolean;
+
+ @Input() fillHeight: boolean;
+
+ private requiredValue: boolean;
+ get required(): boolean {
+ return this.requiredValue;
+ }
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ private readonlyValue: boolean;
+ get readonly(): boolean {
+ return this.readonlyValue;
+ }
+ @Input()
+ set readonly(value: boolean) {
+ this.readonlyValue = coerceBooleanProperty(value);
+ }
+
+ fullscreen = false;
+
+ modelValue: any;
+
+ contentValue: string;
+
+ objectValid: boolean;
+
+ private propagateChange = null;
+
+ constructor() {
+ }
+
+ ngOnInit(): void {
+ const editorElement = this.jsonEditorElmRef.nativeElement;
+ let editorOptions: Partial = {
+ mode: 'ace/mode/json',
+ theme: 'ace/theme/github',
+ showGutter: true,
+ showPrintMargin: false,
+ readOnly: this.readonly
+ };
+
+ const advancedOptions = {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ };
+
+ editorOptions = {...editorOptions, ...advancedOptions};
+ this.jsonEditor = ace.edit(editorElement, editorOptions);
+ this.jsonEditor.session.setUseWrapMode(false);
+ this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
+ this.jsonEditor.on('change', () => {
+ this.updateView();
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ public validate(c: FormControl) {
+ return (this.objectValid) ? null : {
+ jsonParseError: {
+ valid: false,
+ },
+ };
+ }
+
+ writeValue(value: any): void {
+ this.modelValue = value;
+ this.contentValue = '';
+ this.objectValid = false;
+ try {
+ if (this.modelValue) {
+ this.contentValue = JSON.stringify(this.modelValue, undefined, 2);
+ this.objectValid = true;
+ } else {
+ this.objectValid = !this.required;
+ }
+ } catch (e) {
+ //
+ }
+ if (this.jsonEditor) {
+ this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
+ }
+ }
+
+ updateView() {
+ const editorValue = this.jsonEditor.getValue();
+ if (this.contentValue !== editorValue) {
+ this.contentValue = editorValue;
+ let data = null;
+ this.objectValid = false;
+ if (this.contentValue && this.contentValue.length > 0) {
+ try {
+ data = JSON.parse(this.contentValue);
+ this.objectValid = true;
+ } catch (ex) {}
+ } else {
+ this.objectValid = !this.required;
+ }
+ this.propagateChange(data);
+ }
+ }
+
+ onFullscreen() {
+ if (this.jsonEditor) {
+ setTimeout(() => {
+ this.jsonEditor.resize();
+ }, 0);
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html
new file mode 100644
index 0000000000..9e0d80c1c8
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+ {{ 'relation.relation-type-required' | translate }}
+
+
diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts
new file mode 100644
index 0000000000..3c1536b361
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts
@@ -0,0 +1,168 @@
+///
+/// Copyright © 2016-2019 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
+import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
+import {Observable, of, throwError, Subscription} from 'rxjs';
+import {PageLink} from '@shared/models/page/page-link';
+import {Direction} from '@shared/models/page/sort-order';
+import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
+import {PageData, emptyPageData} from '@shared/models/page/page-data';
+import {DashboardInfo} from '@app/shared/models/dashboard.models';
+import {DashboardId} from '@app/shared/models/id/dashboard-id';
+import {DashboardService} from '@core/http/dashboard.service';
+import {Store} from '@ngrx/store';
+import {AppState} from '@app/core/core.state';
+import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
+import {Authority} from '@shared/models/authority.enum';
+import {TranslateService} from '@ngx-translate/core';
+import {DeviceService} from '@core/http/device.service';
+import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
+import {BroadcastService} from '@app/core/services/broadcast.service';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
+import {AssetService} from '@core/http/asset.service';
+import {EntityViewService} from '@core/http/entity-view.service';
+import { RelationTypes } from '@app/shared/models/relation.models';
+
+@Component({
+ selector: 'tb-relation-type-autocomplete',
+ templateUrl: './relation-type-autocomplete.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => RelationTypeAutocompleteComponent),
+ multi: true
+ }]
+})
+export class RelationTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
+
+ relationTypeFormGroup: FormGroup;
+
+ modelValue: string | null;
+
+ private requiredValue: boolean;
+ get required(): boolean {
+ return this.requiredValue;
+ }
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ @Input()
+ disabled: boolean;
+
+ @ViewChild('relationTypeInput', {static: true}) relationTypeInput: ElementRef;
+
+ filteredRelationTypes: Observable>;
+
+ private searchText = '';
+
+ private dirty = false;
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private store: Store,
+ private broadcast: BroadcastService,
+ public translate: TranslateService,
+ private fb: FormBuilder) {
+ this.relationTypeFormGroup = this.fb.group({
+ relationType: [null, this.required ? [Validators.required] : []]
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ ngOnInit() {
+
+ this.filteredRelationTypes = this.relationTypeFormGroup.get('relationType').valueChanges
+ .pipe(
+ tap(value => {
+ this.updateView(value);
+ }),
+ // startWith(''),
+ map(value => value ? value : ''),
+ mergeMap(type => this.fetchRelationTypes(type) )
+ );
+ }
+
+ ngAfterViewInit(): void {
+ }
+
+ ngOnDestroy(): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.relationTypeFormGroup.disable({emitEvent: false});
+ } else {
+ this.relationTypeFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: string | null): void {
+ this.searchText = '';
+ this.modelValue = value;
+ this.relationTypeFormGroup.get('relationType').patchValue(value, {emitEvent: false});
+ this.dirty = true;
+ }
+
+ onFocus() {
+ if (this.dirty) {
+ this.relationTypeFormGroup.get('relationType').updateValueAndValidity({onlySelf: true, emitEvent: true});
+ this.dirty = false;
+ }
+ }
+
+ updateView(value: string | null) {
+ if (this.modelValue !== value) {
+ this.modelValue = value;
+ this.propagateChange(this.modelValue);
+ }
+ }
+
+ displayRelationTypeFn(relationType?: string): string | undefined {
+ return relationType ? relationType : undefined;
+ }
+
+ fetchRelationTypes(searchText?: string, strictMatch: boolean = false): Observable> {
+ this.searchText = searchText;
+ return of(RelationTypes).pipe(
+ map(relationTypes => relationTypes.filter( relationType => {
+ if (strictMatch) {
+ return searchText ? relationType === searchText : false;
+ } else {
+ return searchText ? relationType.toUpperCase().startsWith(searchText.toUpperCase()) : true;
+ }
+ }))
+ );
+ }
+
+ clear() {
+ this.relationTypeFormGroup.get('relationType').patchValue(null, {emitEvent: true});
+ setTimeout(() => {
+ this.relationTypeInput.nativeElement.blur();
+ this.relationTypeInput.nativeElement.focus();
+ }, 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 4be0264466..e4e35a481e 100644
--- a/ui-ngx/src/app/shared/models/page/page-link.ts
+++ b/ui-ngx/src/app/shared/models/page/page-link.ts
@@ -96,7 +96,7 @@ export class PageLink {
pageData.totalElements = pageData.data.length;
pageData.totalPages = Math.ceil(pageData.totalElements / this.pageSize);
if (this.sortOrder) {
- pageData.data = pageData.data.sort(this.sort);
+ pageData.data = pageData.data.sort((a, b) => this.sort(a, b));
}
const startIndex = this.pageSize * this.page;
const endIndex = startIndex + this.pageSize;
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index dd102aa6ed..78b3bad20d 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -83,6 +83,9 @@ import {EntitySelectComponent} from './components/entity/entity-select.component
import {DatetimeComponent} from '@shared/components/time/datetime.component';
import {EntityKeysListComponent} from './components/entity/entity-keys-list.component';
import {SocialSharePanelComponent} from './components/socialshare-panel.component';
+import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
+import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
+import { JsonObjectEditComponent } from './components/json-object-edit.component';
@NgModule({
providers: [
@@ -122,7 +125,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent,
EntitySelectComponent,
EntityKeysListComponent,
+ EntityListSelectComponent,
+ RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
+ JsonObjectEditComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@@ -192,7 +198,10 @@ import {SocialSharePanelComponent} from './components/socialshare-panel.componen
EntityTypeSelectComponent,
EntitySelectComponent,
EntityKeysListComponent,
+ EntityListSelectComponent,
+ RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
+ JsonObjectEditComponent,
// ValueInputComponent,
MatButtonModule,
MatCheckboxModule,