53 changed files with 3607 additions and 106 deletions
@ -0,0 +1,76 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<form #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 600px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<div fxFlex fxLayout="column"> |
|||
<div fxLayout="row"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>alias.name</mat-label> |
|||
<input matInput formControlName="alias" required> |
|||
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')"> |
|||
{{ 'alias.name-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')"> |
|||
{{ 'alias.duplicate-alias' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<section class="tb-resolve-multiple-switch" fxLayout="column" fxLayoutAlign="start center"> |
|||
<label class="tb-small resolve-multiple-label" translate>alias.resolve-multiple</label> |
|||
<mat-slide-toggle class="resolve-multiple-switch" |
|||
formControlName="resolveMultiple"> |
|||
</mat-slide-toggle> |
|||
</section> |
|||
</div> |
|||
<tb-entity-filter formControlName="filter" |
|||
[resolveMultiple]="entityAliasFormGroup.get('resolveMultiple').value" |
|||
(resolveMultipleChanged)="entityAliasFormGroup.get('resolveMultiple').patchValue($event)" |
|||
[allowedEntityTypes]="allowedEntityTypes"> |
|||
</tb-entity-filter> |
|||
<tb-error [error]="entityAliasFormGroup.hasError('noEntityMatched') |
|||
? translate.instant('alias.entity-filter-no-entity-matched') : ''"></tb-error> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty"> |
|||
{{ (isAdd ? 'action.add' : 'action.save') | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* 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 { |
|||
.tb-resolve-multiple-switch { |
|||
padding-left: 10px; |
|||
|
|||
.resolve-multiple-switch { |
|||
margin: 0; |
|||
} |
|||
|
|||
.resolve-multiple-label { |
|||
margin: 5px 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
///
|
|||
/// 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 { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
AbstractControl, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NgForm, Validators, ValidatorFn |
|||
} from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { AttributeData } from '@shared/models/telemetry/telemetry.models'; |
|||
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; |
|||
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; |
|||
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { Observable, of } from 'rxjs'; |
|||
|
|||
export interface EntityAliasDialogData { |
|||
isAdd: boolean; |
|||
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
entityAliases: EntityAliases | Array<EntityAlias>; |
|||
alias?: EntityAlias; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-alias-dialog', |
|||
templateUrl: './entity-alias-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}], |
|||
styleUrls: ['./entity-alias-dialog.component.scss'] |
|||
}) |
|||
export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialogComponent, EntityAlias> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
isAdd: boolean; |
|||
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
entityAliases: Array<EntityAlias>; |
|||
|
|||
alias: EntityAlias; |
|||
|
|||
entityAliasFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<EntityAliasDialogComponent, EntityAlias>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private entityService: EntityService) { |
|||
super(store, router, dialogRef); |
|||
this.isAdd = data.isAdd; |
|||
this.allowedEntityTypes = data.allowedEntityTypes; |
|||
if (Array.isArray(data.entityAliases)) { |
|||
this.entityAliases = data.entityAliases; |
|||
} else { |
|||
this.entityAliases = []; |
|||
for (const aliasId of Object.keys(data.entityAliases)) { |
|||
this.entityAliases.push(data.entityAliases[aliasId]); |
|||
} |
|||
} |
|||
if (this.isAdd) { |
|||
this.alias = { |
|||
id: null, |
|||
alias: '', |
|||
filter: { |
|||
resolveMultiple: false |
|||
} |
|||
}; |
|||
} else { |
|||
this.alias = data.alias; |
|||
} |
|||
|
|||
this.entityAliasFormGroup = this.fb.group({ |
|||
alias: [this.alias.alias, [this.validateDuplicateAliasName(), Validators.required]], |
|||
resolveMultiple: [this.alias.filter.resolveMultiple], |
|||
filter: [this.alias.filter, Validators.required] |
|||
}); |
|||
} |
|||
|
|||
validateDuplicateAliasName(): ValidatorFn { |
|||
return (c: FormControl) => { |
|||
const newAlias = c.value; |
|||
const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias); |
|||
if (found) { |
|||
if (this.isAdd || this.alias.id !== found.id) { |
|||
return { |
|||
duplicateAliasName: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
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(null); |
|||
} |
|||
|
|||
private validate(): Observable<any> { |
|||
return this.entityService.resolveAliasFilter(this.alias.filter, null, 1, true); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
this.alias.alias = this.entityAliasFormGroup.get('alias').value; |
|||
this.alias.filter = this.entityAliasFormGroup.get('filter').value; |
|||
this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value; |
|||
this.validate().subscribe(() => { |
|||
if (this.isAdd) { |
|||
this.alias.id = this.utils.guid(); |
|||
} |
|||
this.dialogRef.close(this.alias); |
|||
}, |
|||
() => { |
|||
this.entityAliasFormGroup.setErrors({ |
|||
noEntityMatched: true |
|||
}); |
|||
const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => { |
|||
this.entityAliasFormGroup.setErrors(null); |
|||
changesSubscriptuion.unsubscribe(); |
|||
}); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<form #entityAliasesForm="ngForm" [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 700px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ title | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div class="tb-aliases-header" fxFlex fxLayout="row" fxLayoutAlign="start center"> |
|||
<span fxFlex="5"></span> |
|||
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
|||
<span class="tb-header-label" translate fxFlex="150px">alias.name</span> |
|||
<span class="tb-header-label" translate fxFlex style="padding-left: 10px;">alias.entity-filter</span> |
|||
<span class="tb-header-label" translate fxFlex="120px" style="padding-left: 10px;">alias.resolve-multiple</span> |
|||
<span style="min-width: 80px;"></span> |
|||
</div> |
|||
</div> |
|||
<mat-divider></mat-divider> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" |
|||
formArrayName="entityAliases" |
|||
*ngFor="let entityAliasControl of entityAliasesFormGroup.get('entityAliases').controls; let $index = index"> |
|||
<span fxFlex="5">{{$index + 1}}.</span> |
|||
<div class="mat-elevation-z4 tb-alias" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
|||
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex="150px"> |
|||
<mat-label></mat-label> |
|||
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}"> |
|||
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')"> |
|||
{{ 'entity.alias-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-entity-filter-view fxFlex style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')"> |
|||
</tb-entity-filter-view> |
|||
<section fxFlex="120px" style="padding-left: 10px;" |
|||
class="tb-resolve-multiple-switch" |
|||
fxLayout="column" |
|||
fxLayoutAlign="center center"> |
|||
<mat-slide-toggle class="resolve-multiple-switch" |
|||
[formControl]="entityAliasControl.get('resolveMultiple')"> |
|||
</mat-slide-toggle> |
|||
</section> |
|||
<button [disabled]="isLoading$ | async" |
|||
mat-button mat-icon-button color="primary" |
|||
style="min-width: 40px;" |
|||
type="button" |
|||
(click)="editAlias($index)" |
|||
matTooltip="{{ 'alias.edit' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button [disabled]="isLoading$ | async" |
|||
mat-button mat-icon-button color="primary" |
|||
style="min-width: 40px;" |
|||
type="button" |
|||
(click)="removeAlias($index)" |
|||
matTooltip="{{ 'entity.remove-alias' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="addAlias()" |
|||
[fxShow]="!disableAdd" |
|||
[disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'alias.add' | translate }}" |
|||
matTooltipPosition="above"> |
|||
{{ 'alias.add' | translate }} |
|||
</button> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* 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 { |
|||
|
|||
.tb-aliases-header { |
|||
min-height: 40px; |
|||
padding: 0 34px 0 34px; |
|||
margin: 5px; |
|||
|
|||
.tb-header-label { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, .570588); |
|||
} |
|||
} |
|||
|
|||
.tb-alias { |
|||
padding: 0 0 0 10px; |
|||
margin: 5px; |
|||
|
|||
.tb-resolve-multiple-switch { |
|||
padding-left: 10px; |
|||
|
|||
.resolve-multiple-switch { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-dialog-content { |
|||
padding-top: 0 !important; |
|||
padding-bottom: 0 !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,264 @@ |
|||
///
|
|||
/// 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 { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
AbstractControl, |
|||
FormArray, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NgForm, Validators |
|||
} from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { AttributeData } from '@shared/models/telemetry/telemetry.models'; |
|||
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; |
|||
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; |
|||
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component'; |
|||
|
|||
export interface EntityAliasesDialogData { |
|||
entityAliases: EntityAliases; |
|||
widgets: Array<Widget>; |
|||
isSingleEntityAlias: boolean; |
|||
isSingleWidget?: boolean; |
|||
allowedEntityTypes?: Array<AliasEntityType>; |
|||
disableAdd?: boolean; |
|||
singleEntityAlias?: EntityAlias; |
|||
customTitle?: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-aliases-dialog', |
|||
templateUrl: './entity-aliases-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}], |
|||
styleUrls: ['./entity-aliases-dialog.component.scss'] |
|||
}) |
|||
export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
title: string; |
|||
disableAdd: boolean; |
|||
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
|
|||
aliasToWidgetsMap: {[aliasId: string]: Array<string>} = {}; |
|||
|
|||
entityAliasesFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<EntityAliasesDialogComponent, EntityAliases>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private dialogs: DialogService, |
|||
private dialog: MatDialog) { |
|||
super(store, router, dialogRef); |
|||
this.title = data.customTitle ? data.customTitle : 'entity.aliases'; |
|||
this.disableAdd = this.data.disableAdd; |
|||
this.allowedEntityTypes = this.data.allowedEntityTypes; |
|||
|
|||
if (data.widgets) { |
|||
let widgetsTitleList: Array<string>; |
|||
if (this.data.isSingleWidget && this.data.widgets.length === 1) { |
|||
const widget = this.data.widgets[0]; |
|||
widgetsTitleList = [widget.config.title]; |
|||
for (const aliasId of Object.keys(this.data.entityAliases)) { |
|||
this.aliasToWidgetsMap[aliasId] = widgetsTitleList; |
|||
} |
|||
} else { |
|||
this.data.widgets.forEach((widget) => { |
|||
if (widget.type === widgetType.rpc) { |
|||
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { |
|||
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; |
|||
widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; |
|||
if (!widgetsTitleList) { |
|||
widgetsTitleList = []; |
|||
this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; |
|||
} |
|||
widgetsTitleList.push(widget.config.title); |
|||
} |
|||
} else { |
|||
const datasources = this.utils.validateDatasources(widget.config.datasources); |
|||
datasources.forEach((datasource) => { |
|||
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { |
|||
widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; |
|||
if (!widgetsTitleList) { |
|||
widgetsTitleList = []; |
|||
this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList; |
|||
} |
|||
widgetsTitleList.push(widget.config.title); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
const entityAliasControls: Array<AbstractControl> = []; |
|||
for (const aliasId of Object.keys(this.data.entityAliases)) { |
|||
const entityAlias = this.data.entityAliases[aliasId]; |
|||
let filter = entityAlias.filter; |
|||
if (!filter) { |
|||
filter = { |
|||
resolveMultiple: false |
|||
}; |
|||
} |
|||
if (!filter.resolveMultiple) { |
|||
filter.resolveMultiple = false; |
|||
} |
|||
entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias)); |
|||
} |
|||
|
|||
this.entityAliasesFormGroup = this.fb.group({ |
|||
entityAliases: this.fb.array(entityAliasControls) |
|||
}); |
|||
} |
|||
|
|||
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { |
|||
const aliasFormControl = this.fb.group({ |
|||
id: [aliasId], |
|||
alias: [entityAlias ? entityAlias.alias : null, [Validators.required]], |
|||
filter: [entityAlias ? entityAlias.filter : null], |
|||
resolveMultiple: [entityAlias ? entityAlias.filter.resolveMultiple : false] |
|||
}); |
|||
aliasFormControl.get('resolveMultiple').valueChanges.subscribe((resolveMultiple: boolean) => { |
|||
(aliasFormControl.get('filter').value as EntityAliasFilter).resolveMultiple = resolveMultiple; |
|||
}); |
|||
return aliasFormControl; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
removeAlias(index: number) { |
|||
const entityAlias = (this.entityAliasesFormGroup.get('entityAliases').value as any[])[index]; |
|||
const widgetsTitleList = this.aliasToWidgetsMap[entityAlias.id]; |
|||
if (widgetsTitleList) { |
|||
let widgetsListHtml = ''; |
|||
for (const widgetTitle of widgetsTitleList) { |
|||
widgetsListHtml += '<br/>\'' + widgetTitle + '\''; |
|||
} |
|||
const message = this.translate.instant('entity.unable-delete-entity-alias-text', |
|||
{entityAlias: entityAlias.alias, widgetsList: widgetsListHtml}); |
|||
this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'), |
|||
message, this.translate.instant('action.close'), true); |
|||
} else { |
|||
(this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index); |
|||
} |
|||
} |
|||
|
|||
public addAlias() { |
|||
this.openAliasDialog(-1); |
|||
} |
|||
|
|||
public editAlias(index: number) { |
|||
this.openAliasDialog(index); |
|||
} |
|||
|
|||
private openAliasDialog(index: number) { |
|||
const isAdd = index === -1; |
|||
let alias; |
|||
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; |
|||
if (!isAdd) { |
|||
alias = aliasesArray[index]; |
|||
} |
|||
this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData, |
|||
EntityAlias>(EntityAliasDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
isAdd, |
|||
allowedEntityTypes: this.allowedEntityTypes, |
|||
entityAliases: aliasesArray, |
|||
alias: isAdd ? null : deepClone(alias) |
|||
} |
|||
}).afterClosed().subscribe((entityAlias) => { |
|||
if (entityAlias) { |
|||
if (isAdd) { |
|||
(this.entityAliasesFormGroup.get('entityAliases') as FormArray) |
|||
.push(this.createEntityAliasFormControl(entityAlias.id, entityAlias)); |
|||
} else { |
|||
const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index); |
|||
aliasFormControl.get('alias').patchValue(entityAlias.alias); |
|||
aliasFormControl.get('filter').patchValue(entityAlias.filter); |
|||
aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
const entityAliases: EntityAliases = {}; |
|||
const uniqueAliasList: {[alias: string]: string} = {}; |
|||
|
|||
let valid = true; |
|||
let message: string; |
|||
|
|||
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; |
|||
for (const aliasValue of aliasesArray) { |
|||
const aliasId: string = aliasValue.id; |
|||
const alias: string = aliasValue.alias; |
|||
const filter: EntityAliasFilter = aliasValue.filter; |
|||
if (uniqueAliasList[alias]) { |
|||
valid = false; |
|||
message = this.translate.instant('entity.duplicate-alias-error', {alias}); |
|||
break; |
|||
} else if (!filter || !filter.type) { |
|||
valid = false; |
|||
message = this.translate.instant('entity.missing-entity-filter-error', {alias}); |
|||
break; |
|||
} else { |
|||
uniqueAliasList[alias] = alias; |
|||
entityAliases[aliasId] = {id: aliasId, alias, filter}; |
|||
} |
|||
} |
|||
if (valid) { |
|||
this.dialogRef.close(entityAliases); |
|||
} else { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message, |
|||
type: 'error' |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="column" class="tb-entity-filter-view"> |
|||
<div *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div> |
|||
<ng-template #filterView> |
|||
<div fxLayout="column"> |
|||
<div class="entity-filter-value">{{ filterDisplayValue }}</div> |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* 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 { |
|||
.tb-entity-filter-view { |
|||
.entity-filter-empty { |
|||
font-size: 14px; |
|||
line-height: 16px; |
|||
color: rgba(221, 44, 0, .87); |
|||
} |
|||
|
|||
.entity-filter-type { |
|||
font-size: 14px; |
|||
line-height: 16px; |
|||
color: rgba(0, 0, 0, .570588); |
|||
} |
|||
|
|||
.entity-filter-value { |
|||
font-size: 14px; |
|||
line-height: 16px; |
|||
color: rgba(0, 0, 0, .570588); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,230 @@ |
|||
///
|
|||
/// 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, forwardRef } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { AliasFilterType, EntityAliasFilter } from '@shared/models/alias.models'; |
|||
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-filter-view', |
|||
templateUrl: './entity-filter-view.component.html', |
|||
styleUrls: ['./entity-filter-view.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityFilterViewComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class EntityFilterViewComponent implements ControlValueAccessor { |
|||
|
|||
constructor(private translate: TranslateService) {} |
|||
|
|||
filterDisplayValue: string; |
|||
filter: EntityAliasFilter; |
|||
|
|||
registerOnChange(fn: any): void { |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
} |
|||
|
|||
writeValue(filter: EntityAliasFilter): void { |
|||
this.filter = filter; |
|||
if (this.filter && this.filter.type) { |
|||
let entityType: EntityType | AliasEntityType; |
|||
let prefix: string; |
|||
let allEntitiesText; |
|||
let anyRelationText; |
|||
let relationTypeText; |
|||
let rootEntityText; |
|||
let directionText; |
|||
switch (this.filter.type) { |
|||
case AliasFilterType.singleEntity: |
|||
entityType = this.filter.singleEntity.entityType; |
|||
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, |
|||
{count: 1}); |
|||
break; |
|||
case AliasFilterType.entityList: |
|||
entityType = this.filter.entityType; |
|||
const count = this.filter.entityList.length; |
|||
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, |
|||
{count}); |
|||
break; |
|||
case AliasFilterType.entityName: |
|||
entityType = this.filter.entityType; |
|||
prefix = this.filter.entityNameFilter; |
|||
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith, |
|||
{prefix}); |
|||
break; |
|||
case AliasFilterType.stateEntity: |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); |
|||
break; |
|||
case AliasFilterType.assetType: |
|||
const assetType = this.filter.assetType; |
|||
prefix = this.filter.assetNameFilter; |
|||
if (prefix && prefix.length) { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description', |
|||
{assetType, prefix}); |
|||
} else { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description', |
|||
{assetType}); |
|||
} |
|||
break; |
|||
case AliasFilterType.deviceType: |
|||
const deviceType = this.filter.deviceType; |
|||
prefix = this.filter.deviceNameFilter; |
|||
if (prefix && prefix.length) { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description', |
|||
{deviceType, prefix}); |
|||
} else { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description', |
|||
{deviceType}); |
|||
} |
|||
break; |
|||
case AliasFilterType.entityViewType: |
|||
const entityViewType = this.filter.entityViewType; |
|||
prefix = this.filter.entityViewNameFilter; |
|||
if (prefix && prefix.length) { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description', |
|||
{entityViewType, prefix}); |
|||
} else { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description', |
|||
{entityViewType}); |
|||
} |
|||
break; |
|||
case AliasFilterType.relationsQuery: |
|||
allEntitiesText = this.translate.instant('alias.all-entities'); |
|||
anyRelationText = this.translate.instant('alias.any-relation'); |
|||
if (this.filter.rootStateEntity) { |
|||
rootEntityText = this.translate.instant('alias.state-entity'); |
|||
} else { |
|||
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); |
|||
} |
|||
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); |
|||
const relationFilters = this.filter.filters; |
|||
if (relationFilters && relationFilters.length) { |
|||
const relationFiltersDisplayValues = []; |
|||
relationFilters.forEach((relationFilter) => { |
|||
let entitiesText; |
|||
if (relationFilter.entityTypes && relationFilter.entityTypes.length) { |
|||
const entitiesNamesList = []; |
|||
relationFilter.entityTypes.forEach((filterEntityType) => { |
|||
entitiesNamesList.push( |
|||
this.translate.instant(entityTypeTranslations.get(filterEntityType).typePlural) |
|||
); |
|||
}); |
|||
entitiesText = entitiesNamesList.join(', '); |
|||
} else { |
|||
entitiesText = allEntitiesText; |
|||
} |
|||
if (relationFilter.relationType && relationFilter.relationType.length) { |
|||
relationTypeText = `'${relationFilter.relationType}'`; |
|||
} else { |
|||
relationTypeText = anyRelationText; |
|||
} |
|||
const relationFilterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', |
|||
{ |
|||
entities: entitiesText, |
|||
relationType: relationTypeText, |
|||
direction: directionText, |
|||
rootEntity: rootEntityText |
|||
} |
|||
); |
|||
relationFiltersDisplayValues.push(relationFilterDisplayValue); |
|||
}); |
|||
this.filterDisplayValue = relationFiltersDisplayValues.join(', '); |
|||
} else { |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', |
|||
{ |
|||
entities: allEntitiesText, |
|||
relationType: anyRelationText, |
|||
direction: directionText, |
|||
rootEntity: rootEntityText |
|||
} |
|||
); |
|||
} |
|||
break; |
|||
case AliasFilterType.assetSearchQuery: |
|||
case AliasFilterType.deviceSearchQuery: |
|||
case AliasFilterType.entityViewSearchQuery: |
|||
allEntitiesText = this.translate.instant('alias.all-entities'); |
|||
anyRelationText = this.translate.instant('alias.any-relation'); |
|||
if (this.filter.rootStateEntity) { |
|||
rootEntityText = this.translate.instant('alias.state-entity'); |
|||
} else { |
|||
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); |
|||
} |
|||
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); |
|||
if (this.filter.relationType && this.filter.relationType.length) { |
|||
relationTypeText = `'${filter.relationType}'`; |
|||
} else { |
|||
relationTypeText = anyRelationText; |
|||
} |
|||
|
|||
const translationValues: any = { |
|||
relationType: relationTypeText, |
|||
direction: directionText, |
|||
rootEntity: rootEntityText |
|||
}; |
|||
|
|||
if (this.filter.type === AliasFilterType.assetSearchQuery) { |
|||
const assetTypesQuoted = []; |
|||
this.filter.assetTypes.forEach((filterAssetType) => { |
|||
assetTypesQuoted.push(`'${filterAssetType}'`); |
|||
}); |
|||
const assetTypesText = assetTypesQuoted.join(', '); |
|||
translationValues.assetTypes = assetTypesText; |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-search-query-description', |
|||
translationValues |
|||
); |
|||
} else if (this.filter.type === AliasFilterType.deviceSearchQuery) { |
|||
const deviceTypesQuoted = []; |
|||
this.filter.deviceTypes.forEach((filterDeviceType) => { |
|||
deviceTypesQuoted.push(`'${filterDeviceType}'`); |
|||
}); |
|||
const deviceTypesText = deviceTypesQuoted.join(', '); |
|||
translationValues.deviceTypes = deviceTypesText; |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description', |
|||
translationValues |
|||
); |
|||
} else if (this.filter.type === AliasFilterType.entityViewSearchQuery) { |
|||
const entityViewTypesQuoted = []; |
|||
this.filter.entityViewTypes.forEach((filterEntityViewType) => { |
|||
entityViewTypesQuoted.push(`'${filterEntityViewType}'`); |
|||
}); |
|||
const entityViewTypesText = entityViewTypesQuoted.join(', '); |
|||
translationValues.entityViewTypes = entityViewTypesText; |
|||
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-search-query-description', |
|||
translationValues |
|||
); |
|||
} |
|||
break; |
|||
default: |
|||
this.filterDisplayValue = this.filter.type; |
|||
break; |
|||
} |
|||
} else { |
|||
this.filterDisplayValue = ''; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,244 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div fxLayout="column" [formGroup]="entityFilterFormGroup" class="tb-entity-filter"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>alias.filter-type</mat-label> |
|||
<mat-select required matInput formControlName="type"> |
|||
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type"> |
|||
{{aliasFilterTypeTranslations.get(type) | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')"> |
|||
{{ 'alias.filter-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<section fxLayout="column" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.singleEntity"> |
|||
<tb-entity-select required |
|||
useAliasEntityTypes="true" |
|||
formControlName="singleEntity"> |
|||
</tb-entity-select> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.entityList"> |
|||
<tb-entity-type-select required |
|||
showLabel |
|||
[allowedEntityTypes]="allowedEntityTypes" |
|||
formControlName="entityType"> |
|||
</tb-entity-type-select> |
|||
<tb-entity-list required |
|||
[entityType]="filterFormGroup.get('entityType').value" |
|||
formControlName="entityList"> |
|||
</tb-entity-list> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.entityName"> |
|||
<tb-entity-type-select required |
|||
showLabel |
|||
[allowedEntityTypes]="allowedEntityTypes" |
|||
formControlName="entityType"> |
|||
</tb-entity-type-select> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>entity.name-starts-with</mat-label> |
|||
<input matInput formControlName="entityNameFilter" required> |
|||
<mat-error *ngIf="filterFormGroup.get('entityNameFilter').hasError('required')"> |
|||
{{ 'entity.entity-name-filter-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.stateEntity"> |
|||
<mat-form-field floatLabel="always" class="mat-block"> |
|||
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
|||
<input matInput formControlName="stateEntityParamName" |
|||
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
|||
</mat-form-field> |
|||
<div fxLayout="column"> |
|||
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
|||
<tb-entity-select fxFlex |
|||
useAliasEntityTypes="true" |
|||
formControlName="defaultStateEntity"> |
|||
</tb-entity-select> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.assetType"> |
|||
<tb-entity-subtype-autocomplete required |
|||
formControlName="assetType" |
|||
[entityType]="entityType.ASSET"> |
|||
</tb-entity-subtype-autocomplete> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>asset.name-starts-with</mat-label> |
|||
<input matInput formControlName="assetNameFilter"> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.deviceType"> |
|||
<tb-entity-subtype-autocomplete required |
|||
formControlName="deviceType" |
|||
[entityType]="entityType.DEVICE"> |
|||
</tb-entity-subtype-autocomplete> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>device.name-starts-with</mat-label> |
|||
<input matInput formControlName="deviceNameFilter"> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.entityViewType"> |
|||
<tb-entity-subtype-autocomplete required |
|||
formControlName="entityViewType" |
|||
[entityType]="entityType.ENTITY_VIEW"> |
|||
</tb-entity-subtype-autocomplete> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>entity-view.name-starts-with</mat-label> |
|||
<input matInput formControlName="entityViewNameFilter"> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.relationsQuery"> |
|||
<section fxLayout="column" id="relationsQueryFilter"> |
|||
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label> |
|||
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;"> |
|||
<mat-slide-toggle class="root-state-entity-switch" |
|||
formControlName="rootStateEntity"> |
|||
</mat-slide-toggle> |
|||
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> |
|||
</section> |
|||
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value"> |
|||
<tb-entity-select fxFlex |
|||
required |
|||
useAliasEntityTypes="true" |
|||
formControlName="rootEntity"> |
|||
</tb-entity-select> |
|||
</div> |
|||
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value"> |
|||
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;"> |
|||
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
|||
<input matInput formControlName="stateEntityParamName" |
|||
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
|||
</mat-form-field> |
|||
<div fxFlex fxLayout="column"> |
|||
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
|||
<tb-entity-select fxFlex |
|||
useAliasEntityTypes="true" |
|||
formControlName="defaultStateEntity"> |
|||
</tb-entity-select> |
|||
</div> |
|||
</div> |
|||
<div fxFlex fxLayoutGap="8px" fxLayout="row"> |
|||
<mat-form-field class="mat-block" style="min-width: 100px;"> |
|||
<mat-label translate>relation.direction</mat-label> |
|||
<mat-select required matInput formControlName="direction"> |
|||
<mat-option *ngFor="let type of directionTypes" [value]="type"> |
|||
{{ directionTypeTranslations.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex floatLabel="always" class="mat-block"> |
|||
<mat-label translate>alias.max-relation-level</mat-label> |
|||
<input matInput |
|||
type="number" |
|||
min="1" |
|||
step="1" |
|||
placeholder="{{ 'alias.unlimited-level' | translate }}" |
|||
formControlName="maxLevel"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="mat-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div> |
|||
<tb-relation-filters |
|||
[allowedEntityTypes]="allowedEntityTypes" |
|||
formControlName="filters"> |
|||
</tb-relation-filters> |
|||
</section> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery || |
|||
entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery || |
|||
entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ? |
|||
entityFilterFormGroup.get('type').value : ''"> |
|||
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label> |
|||
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;"> |
|||
<mat-slide-toggle class="root-state-entity-switch" |
|||
formControlName="rootStateEntity"> |
|||
</mat-slide-toggle> |
|||
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> |
|||
</section> |
|||
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value"> |
|||
<tb-entity-select fxFlex |
|||
required |
|||
useAliasEntityTypes="true" |
|||
formControlName="rootEntity"> |
|||
</tb-entity-select> |
|||
</div> |
|||
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value"> |
|||
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;"> |
|||
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
|||
<input matInput formControlName="stateEntityParamName" |
|||
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
|||
</mat-form-field> |
|||
<div fxFlex fxLayout="column"> |
|||
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
|||
<tb-entity-select fxFlex |
|||
useAliasEntityTypes="true" |
|||
formControlName="defaultStateEntity"> |
|||
</tb-entity-select> |
|||
</div> |
|||
</div> |
|||
<div fxFlex fxLayoutGap="8px" fxLayout="row"> |
|||
<mat-form-field class="mat-block" style="min-width: 100px;"> |
|||
<mat-label translate>relation.direction</mat-label> |
|||
<mat-select required matInput formControlName="direction"> |
|||
<mat-option *ngFor="let type of directionTypes" [value]="type"> |
|||
{{ directionTypeTranslations.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex floatLabel="always" class="mat-block"> |
|||
<mat-label translate>alias.max-relation-level</mat-label> |
|||
<input matInput |
|||
type="number" |
|||
min="1" |
|||
step="1" |
|||
placeholder="{{ 'alias.unlimited-level' | translate }}" |
|||
formControlName="maxLevel"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="mat-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div> |
|||
<tb-relation-type-autocomplete |
|||
fxFlex |
|||
formControlName="relationType"> |
|||
</tb-relation-type-autocomplete> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery"> |
|||
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div> |
|||
<tb-entity-subtype-list |
|||
required |
|||
[entityType]="entityType.ASSET" |
|||
formControlName="assetTypes"> |
|||
</tb-entity-subtype-list> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery"> |
|||
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div> |
|||
<tb-entity-subtype-list |
|||
required |
|||
[entityType]="entityType.DEVICE" |
|||
formControlName="deviceTypes"> |
|||
</tb-entity-subtype-list> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery"> |
|||
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>entity-view.entity-view-types</div> |
|||
<tb-entity-subtype-list |
|||
required |
|||
[entityType]="entityType.ENTITY_VIEW" |
|||
formControlName="entityViewTypes"> |
|||
</tb-entity-subtype-list> |
|||
</ng-template> |
|||
</section> |
|||
</div> |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* 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 { |
|||
.tb-entity-filter { |
|||
#relationsQueryFilter { |
|||
padding-top: 20px; |
|||
|
|||
tb-entity-select { |
|||
min-height: 92px; |
|||
} |
|||
} |
|||
|
|||
.tb-root-state-entity-switch { |
|||
padding-left: 10px; |
|||
|
|||
.root-state-entity-switch { |
|||
margin: 0; |
|||
} |
|||
|
|||
.root-state-entity-label { |
|||
margin: 5px 0 5px 10px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,218 @@ |
|||
///
|
|||
/// 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, FormControl, |
|||
FormGroup, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; |
|||
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-filter', |
|||
templateUrl: './entity-filter.component.html', |
|||
styleUrls: ['./entity-filter.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityFilterComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class EntityFilterComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
|
|||
@Input() resolveMultiple: boolean; |
|||
|
|||
@Output() resolveMultipleChanged: EventEmitter<boolean> = new EventEmitter<boolean>(); |
|||
|
|||
entityFilterFormGroup: FormGroup; |
|||
filterFormGroup: FormGroup; |
|||
|
|||
aliasFilterTypes: Array<AliasFilterType>; |
|||
|
|||
aliasFilterType = AliasFilterType; |
|||
aliasFilterTypeTranslations = aliasFilterTypeTranslationMap; |
|||
entityType = EntityType; |
|||
|
|||
directionTypes = Object.keys(EntitySearchDirection); |
|||
directionTypeTranslations = entitySearchDirectionTranslations; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private entityService: EntityService, |
|||
private fb: FormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
|
|||
this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes); |
|||
|
|||
this.entityFilterFormGroup = this.fb.group({ |
|||
type: [null, [Validators.required]] |
|||
}); |
|||
this.entityFilterFormGroup.get('type').valueChanges.subscribe((type: AliasFilterType) => { |
|||
this.filterTypeChanged(type); |
|||
}); |
|||
this.entityFilterFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
this.filterFormGroup = this.fb.group({}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(filter: EntityAliasFilter): void { |
|||
if (!filter) { |
|||
filter = { |
|||
type: null, |
|||
resolveMultiple: this.resolveMultiple |
|||
}; |
|||
} |
|||
this.entityFilterFormGroup.get('type').patchValue(filter.type, {emitEvent: false}); |
|||
if (filter && filter.type) { |
|||
this.updateFilterFormGroup(filter.type, filter); |
|||
} |
|||
} |
|||
|
|||
private updateFilterFormGroup(type: AliasFilterType, filter?: EntityAliasFilter) { |
|||
switch (type) { |
|||
case AliasFilterType.singleEntity: |
|||
this.filterFormGroup = this.fb.group({ |
|||
singleEntity: [filter ? filter.singleEntity : null, [Validators.required]] |
|||
}); |
|||
break; |
|||
case AliasFilterType.entityList: |
|||
this.filterFormGroup = this.fb.group({ |
|||
entityType: [filter ? filter.entityType : null, [Validators.required]], |
|||
entityList: [filter ? filter.entityList : [], [Validators.required]], |
|||
}); |
|||
break; |
|||
case AliasFilterType.entityName: |
|||
this.filterFormGroup = this.fb.group({ |
|||
entityType: [filter ? filter.entityType : null, [Validators.required]], |
|||
entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]], |
|||
}); |
|||
break; |
|||
case AliasFilterType.stateEntity: |
|||
this.filterFormGroup = this.fb.group({ |
|||
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], |
|||
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], |
|||
}); |
|||
break; |
|||
case AliasFilterType.assetType: |
|||
this.filterFormGroup = this.fb.group({ |
|||
assetType: [filter ? filter.assetType : null, [Validators.required]], |
|||
assetNameFilter: [filter ? filter.assetNameFilter : '', []], |
|||
}); |
|||
break; |
|||
case AliasFilterType.deviceType: |
|||
this.filterFormGroup = this.fb.group({ |
|||
deviceType: [filter ? filter.deviceType : null, [Validators.required]], |
|||
deviceNameFilter: [filter ? filter.deviceNameFilter : '', []], |
|||
}); |
|||
break; |
|||
case AliasFilterType.entityViewType: |
|||
this.filterFormGroup = this.fb.group({ |
|||
entityViewType: [filter ? filter.entityViewType : null, [Validators.required]], |
|||
entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []], |
|||
}); |
|||
break; |
|||
case AliasFilterType.relationsQuery: |
|||
case AliasFilterType.assetSearchQuery: |
|||
case AliasFilterType.deviceSearchQuery: |
|||
case AliasFilterType.entityViewSearchQuery: |
|||
this.filterFormGroup = this.fb.group({ |
|||
rootStateEntity: [filter ? filter.rootStateEntity : false, []], |
|||
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], |
|||
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], |
|||
rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]], |
|||
direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]], |
|||
maxLevel: [filter ? filter.maxLevel : 1, []], |
|||
}); |
|||
this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => { |
|||
this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); |
|||
this.filterFormGroup.get('rootEntity').updateValueAndValidity(); |
|||
}); |
|||
if (type === AliasFilterType.relationsQuery) { |
|||
this.filterFormGroup.addControl('filters', |
|||
this.fb.control(filter ? filter.filters : [], [])); |
|||
} else { |
|||
this.filterFormGroup.addControl('relationType', |
|||
this.fb.control(filter ? filter.relationType : null, [])); |
|||
if (type === AliasFilterType.assetSearchQuery) { |
|||
this.filterFormGroup.addControl('assetTypes', |
|||
this.fb.control(filter ? filter.assetTypes : [], [Validators.required])); |
|||
} else if (type === AliasFilterType.deviceSearchQuery) { |
|||
this.filterFormGroup.addControl('deviceTypes', |
|||
this.fb.control(filter ? filter.deviceTypes : [], [Validators.required])); |
|||
} else if (type === AliasFilterType.entityViewSearchQuery) { |
|||
this.filterFormGroup.addControl('entityViewTypes', |
|||
this.fb.control(filter ? filter.entityViewTypes : [], [Validators.required])); |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
this.filterFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
private filterTypeChanged(type: AliasFilterType) { |
|||
let resolveMultiple = true; |
|||
if (type === AliasFilterType.singleEntity || type === AliasFilterType.stateEntity) { |
|||
resolveMultiple = false; |
|||
} |
|||
if (this.resolveMultiple !== resolveMultiple) { |
|||
this.resolveMultipleChanged.emit(resolveMultiple); |
|||
} |
|||
this.updateFilterFormGroup(type); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let filter = null; |
|||
if (this.entityFilterFormGroup.valid && this.filterFormGroup.valid) { |
|||
filter = { |
|||
type: this.entityFilterFormGroup.get('type').value, |
|||
resolveMultiple: this.resolveMultiple |
|||
}; |
|||
filter = {...filter, ...this.filterFormGroup.value}; |
|||
} |
|||
this.propagateChange(filter); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup"> |
|||
<div class="header" [fxShow]="relationFiltersFormGroup.get('relationFilters').length"> |
|||
<div fxLayout="row" fxLayoutAlign="start center"> |
|||
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span> |
|||
<span class="cell" fxFlex translate>entity.entity-types</span> |
|||
<span class="cell" style="width: 40px; min-width: 40px;"> </span> |
|||
</div> |
|||
</div> |
|||
<div class="body" [fxShow]="relationFiltersFormGroup.get('relationFilters').length"> |
|||
<div class="row" fxFlex fxLayout="row" |
|||
fxLayoutAlign="start center" formArrayName="relationFilters" |
|||
*ngFor="let relationFilterControl of relationFiltersFormGroup.get('relationFilters').controls; let $index = index"> |
|||
<div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center"> |
|||
<tb-relation-type-autocomplete |
|||
class="cell" style="width: 200px; min-width: 200px;" |
|||
[formControl]="relationFilterControl.get('relationType')"> |
|||
</tb-relation-type-autocomplete> |
|||
<tb-entity-type-list class="cell" fxFlex |
|||
[allowedEntityTypes]="allowedEntityTypes" |
|||
[formControl]="relationFilterControl.get('entityTypes')"> |
|||
</tb-entity-type-list> |
|||
<button mat-button mat-icon-button color="primary" |
|||
type="button" |
|||
style="width: 40px; min-width: 40px;" |
|||
(click)="removeFilter($index)" |
|||
[disabled]="isLoading$ | async" |
|||
matTooltip="{{ 'relation.remove-relation-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="any-filter" [fxShow]="!relationFiltersFormGroup.get('relationFilters').length"> |
|||
<span fxLayoutAlign="center center" |
|||
class="tb-prompt" translate>relation.any-relation</span> |
|||
</div> |
|||
<div> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="isLoading$ | async" |
|||
(click)="addFilter()" |
|||
type="button" |
|||
matTooltip="{{ 'relation.add-relation-filter' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>add</mat-icon> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,67 @@ |
|||
/** |
|||
* 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 { |
|||
.tb-relation-filters { |
|||
.header { |
|||
padding-right: 5px; |
|||
padding-bottom: 5px; |
|||
padding-left: 5px; |
|||
|
|||
.cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
font-size: 12px; |
|||
font-weight: 700; |
|||
color: rgba(0, 0, 0, .54); |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
|
|||
.body { |
|||
max-height: 300px; |
|||
padding-right: 5px; |
|||
padding-bottom: 20px; |
|||
padding-left: 5px; |
|||
overflow: auto; |
|||
|
|||
.row { |
|||
padding-top: 5px; |
|||
} |
|||
|
|||
.cell { |
|||
padding-right: 5px; |
|||
padding-left: 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-relation-filters { |
|||
.cell { |
|||
.mat-form-field { |
|||
.mat-form-field-infix { |
|||
border-top: none; |
|||
} |
|||
&.mat-form-field-appearance-standard { |
|||
.mat-form-field-flex { |
|||
padding-top: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
///
|
|||
/// 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, FormArray, |
|||
FormBuilder, FormControl, |
|||
FormGroup, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; |
|||
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Subscription } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-relation-filters', |
|||
templateUrl: './relation-filters.component.html', |
|||
styleUrls: ['./relation-filters.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => RelationFiltersComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
|
|||
relationFiltersFormGroup: FormGroup; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
private valueChangeSubscription: Subscription = null; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.relationFiltersFormGroup = this.fb.group({}); |
|||
this.relationFiltersFormGroup.addControl('relationFilters', |
|||
this.fb.array([])); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState?(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(filters: Array<EntityTypeFilter>): void { |
|||
if (this.valueChangeSubscription) { |
|||
this.valueChangeSubscription.unsubscribe(); |
|||
} |
|||
const relationFiltersControls: Array<AbstractControl> = []; |
|||
if (filters && filters.length) { |
|||
filters.forEach((filter) => { |
|||
relationFiltersControls.push(this.createRelationFilterFormGroup(filter)); |
|||
}); |
|||
} |
|||
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls)); |
|||
this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
public removeFilter(index: number) { |
|||
(this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index); |
|||
} |
|||
|
|||
public addFilter() { |
|||
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; |
|||
const filter: EntityTypeFilter = { |
|||
relationType: null, |
|||
entityTypes: [] |
|||
}; |
|||
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); |
|||
} |
|||
|
|||
private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl { |
|||
return this.fb.group({ |
|||
relationType: [filter ? filter.relationType : null], |
|||
entityTypes: [filter ? filter.entityTypes : []] |
|||
}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const filters: Array<EntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value; |
|||
this.propagateChange(filters); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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 ::ng-deep { |
|||
.mat-form-field { |
|||
.mat-form-field-infix { |
|||
border-top: none; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<mat-form-field appearance="standard" [formGroup]="entitySubtypeListFormGroup" class="mat-block"> |
|||
<mat-chip-list #chipList formControlName="entitySubtypeList"> |
|||
<mat-chip |
|||
*ngFor="let entitySubtype of entitySubtypeList" |
|||
[selectable]="!disabled" |
|||
[removable]="!disabled" |
|||
(removed)="remove(entitySubtype)"> |
|||
{{entitySubtype}} |
|||
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
|||
</mat-chip> |
|||
<input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}" |
|||
style="max-width: 200px;" |
|||
#entitySubtypeInput |
|||
(focusin)="onFocus()" |
|||
formControlName="entitySubtype" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
[matAutocomplete]="entitySubtypeAutocomplete" |
|||
[matChipInputFor]="chipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
(matChipInputTokenEnd)="chipAdd($event)"> |
|||
</mat-chip-list> |
|||
<mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
(optionSelected)="selected($event)" |
|||
[displayWith]="displayEntitySubtypeFn"> |
|||
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype"> |
|||
<span [innerHTML]="entitySubtype | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null"> |
|||
<span> |
|||
{{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }} |
|||
</span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')"> |
|||
{{ subtypeListEmptyText | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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 ::ng-deep { |
|||
.mat-form-field { |
|||
.mat-form-field-infix { |
|||
border-top: none; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,317 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterContentInit, |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, OnDestroy, |
|||
OnInit, |
|||
SkipSelf, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NG_VALUE_ACCESSOR, NgForm, Validators |
|||
} from '@angular/forms'; |
|||
import { Observable, of, Subscription, throwError } from 'rxjs'; |
|||
import { map, mergeMap, startWith, tap, share, pairwise, filter, publishReplay, refCount } from 'rxjs/operators'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@app/core/core.state'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import { AliasEntityType, EntitySubtype, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import {BaseData} from '@shared/models/base-data'; |
|||
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'; |
|||
import { AssetService } from '@core/http/asset.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { EntityViewService } from '@core/http/entity-view.service'; |
|||
import { BroadcastService } from '@core/services/broadcast.service'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-subtype-list', |
|||
templateUrl: './entity-subtype-list.component.html', |
|||
styleUrls: ['./entity-subtype-list.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntitySubTypeListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
entitySubtypeListFormGroup: FormGroup; |
|||
|
|||
modelValue: Array<string> | null; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.requiredValue !== newVal) { |
|||
this.requiredValue = newVal; |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
entityType: EntityType; |
|||
|
|||
@ViewChild('entitySubtypeInput', {static: false}) entitySubtypeInput: ElementRef<HTMLInputElement>; |
|||
@ViewChild('entitySubtypeAutocomplete', {static: false}) entitySubtypeAutocomplete: MatAutocomplete; |
|||
@ViewChild('chipList', {static: true}) chipList: MatChipList; |
|||
|
|||
entitySubtypeList: Array<string> = []; |
|||
filteredEntitySubtypeList: Observable<Array<string>>; |
|||
entitySubtypes: Observable<Array<string>>; |
|||
|
|||
private broadcastSubscription: Subscription; |
|||
|
|||
placeholder: string; |
|||
secondaryPlaceholder: string; |
|||
noSubtypesMathingText: string; |
|||
subtypeListEmptyText: string; |
|||
|
|||
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
private searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private broadcast: BroadcastService, |
|||
public translate: TranslateService, |
|||
private assetService: AssetService, |
|||
private deviceService: DeviceService, |
|||
private entityViewService: EntityViewService, |
|||
private fb: FormBuilder) { |
|||
this.entitySubtypeListFormGroup = this.fb.group({ |
|||
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], |
|||
entitySubtype: [null] |
|||
}); |
|||
} |
|||
|
|||
updateValidators() { |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []); |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
|
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type') |
|||
: this.translate.instant('asset.any-asset'); |
|||
this.secondaryPlaceholder = '+' + this.translate.instant('asset.asset-type'); |
|||
this.noSubtypesMathingText = 'asset.no-asset-types-matching'; |
|||
this.subtypeListEmptyText = 'asset.asset-type-list-empty'; |
|||
this.broadcastSubscription = this.broadcast.on('assetSaved', () => { |
|||
this.entitySubtypes = null; |
|||
}); |
|||
break; |
|||
case EntityType.DEVICE: |
|||
this.placeholder = this.required ? this.translate.instant('device.enter-device-type') |
|||
: this.translate.instant('device.any-device'); |
|||
this.secondaryPlaceholder = '+' + this.translate.instant('device.device-type'); |
|||
this.noSubtypesMathingText = 'device.no-device-types-matching'; |
|||
this.subtypeListEmptyText = 'device.device-type-list-empty'; |
|||
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { |
|||
this.entitySubtypes = null; |
|||
}); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type') |
|||
: this.translate.instant('entity-view.any-entity-view'); |
|||
this.secondaryPlaceholder = '+' + this.translate.instant('entity-view.entity-view-type'); |
|||
this.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching'; |
|||
this.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty'; |
|||
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { |
|||
this.entitySubtypes = null; |
|||
}); |
|||
break; |
|||
} |
|||
|
|||
this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges |
|||
.pipe( |
|||
map(value => value ? value : ''), |
|||
mergeMap(name => this.fetchEntitySubtypes(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.broadcastSubscription) { |
|||
this.broadcastSubscription.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.entitySubtypeListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.entitySubtypeListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<string> | null): void { |
|||
this.searchText = ''; |
|||
if (value != null && value.length > 0) { |
|||
this.modelValue = [...value]; |
|||
this.entitySubtypeList = [...value]; |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
|||
} else { |
|||
this.entitySubtypeList = []; |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
|||
this.modelValue = null; |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
add(entitySubtype: string): void { |
|||
if (!this.modelValue || this.modelValue.indexOf(entitySubtype) === -1) { |
|||
if (!this.modelValue) { |
|||
this.modelValue = []; |
|||
} |
|||
this.modelValue.push(entitySubtype); |
|||
this.entitySubtypeList.push(entitySubtype); |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
|
|||
chipAdd(event: MatChipInputEvent): void { |
|||
const value = event.value; |
|||
if ((value || '').trim()) { |
|||
this.add(value.trim()); |
|||
} |
|||
this.clear(''); |
|||
} |
|||
|
|||
remove(entitySubtype: string) { |
|||
const index = this.entitySubtypeList.indexOf(entitySubtype); |
|||
if (index >= 0) { |
|||
this.entitySubtypeList.splice(index, 1); |
|||
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
|||
this.modelValue.splice(index, 1); |
|||
if (!this.modelValue.length) { |
|||
this.modelValue = null; |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
} |
|||
|
|||
selected(event: MatAutocompleteSelectedEvent): void { |
|||
this.add(event.option.viewValue); |
|||
this.clear(''); |
|||
} |
|||
|
|||
displayEntitySubtypeFn(entitySubtype?: string): string | undefined { |
|||
return entitySubtype ? entitySubtype : undefined; |
|||
} |
|||
|
|||
fetchEntitySubtypes(searchText?: string): Observable<Array<string>> { |
|||
this.searchText = searchText; |
|||
return this.getEntitySubtypes().pipe( |
|||
map(subTypes => { |
|||
let result = subTypes.filter( subType => { |
|||
return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true; |
|||
}); |
|||
if (!result.length) { |
|||
result = [searchText]; |
|||
} |
|||
return result; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
getEntitySubtypes(): Observable<Array<string>> { |
|||
if (!this.entitySubtypes) { |
|||
let subTypesObservable: Observable<Array<EntitySubtype>>; |
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
subTypesObservable = this.assetService.getAssetTypes(false, true); |
|||
break; |
|||
case EntityType.DEVICE: |
|||
subTypesObservable = this.deviceService.getDeviceTypes(false, true); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
subTypesObservable = this.entityViewService.getEntityViewTypes(false, true); |
|||
break; |
|||
} |
|||
if (subTypesObservable) { |
|||
this.entitySubtypes = subTypesObservable.pipe( |
|||
map(subTypes => subTypes.map(subType => subType.type)), |
|||
publishReplay(1), |
|||
refCount() |
|||
); |
|||
} else { |
|||
return throwError(null); |
|||
} |
|||
} |
|||
return this.entitySubtypes; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.entitySubtypeListFormGroup.get('entitySubtype').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.entitySubtypeInput.nativeElement.value = value; |
|||
this.entitySubtypeListFormGroup.get('entitySubtype').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.entitySubtypeInput.nativeElement.blur(); |
|||
this.entitySubtypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<mat-form-field appearance="standard" [formGroup]="entityTypeListFormGroup" class="mat-block"> |
|||
<mat-chip-list #chipList formControlName="entityTypeList"> |
|||
<mat-chip |
|||
*ngFor="let entityType of entityTypeList" |
|||
[selectable]="!disabled" |
|||
[removable]="!disabled" |
|||
(removed)="remove(entityType)"> |
|||
{{entityType.name}} |
|||
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
|||
</mat-chip> |
|||
<input matInput type="text" placeholder="{{ !disabled ? ((!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder) : '' }}" |
|||
style="max-width: 200px;" |
|||
#entityTypeInput |
|||
(focusin)="onFocus()" |
|||
formControlName="entityType" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
[matAutocomplete]="entityTypeAutocomplete" |
|||
[matChipInputFor]="chipList"> |
|||
</mat-chip-list> |
|||
<mat-autocomplete #entityTypeAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
[displayWith]="displayEntityTypeFn"> |
|||
<mat-option *ngFor="let entityType of filteredEntityTypeList | async" [value]="entityType"> |
|||
<span [innerHTML]="entityType.name | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredEntityTypeList | async)?.length" [value]="null"> |
|||
<span> |
|||
{{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }} |
|||
</span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')"> |
|||
{{ 'entity.entity-type-list-empty' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,262 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterContentInit, |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnInit, |
|||
SkipSelf, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NG_VALUE_ACCESSOR, NgForm, Validators |
|||
} from '@angular/forms'; |
|||
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'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import {BaseData} from '@shared/models/base-data'; |
|||
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'; |
|||
|
|||
interface EntityTypeInfo { |
|||
name: string; |
|||
value: EntityType; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-type-list', |
|||
templateUrl: './entity-type-list.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityTypeListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
|||
|
|||
entityTypeListFormGroup: FormGroup; |
|||
|
|||
modelValue: Array<EntityType> | null; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.requiredValue !== newVal) { |
|||
this.requiredValue = newVal; |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
|
|||
@Input() |
|||
ignoreAuthorityFilter: boolean; |
|||
|
|||
@ViewChild('entityTypeInput', {static: false}) entityTypeInput: ElementRef<HTMLInputElement>; |
|||
@ViewChild('entityTypeAutocomplete', {static: false}) entityTypeAutocomplete: MatAutocomplete; |
|||
@ViewChild('chipList', {static: true}) chipList: MatChipList; |
|||
|
|||
allEntityTypeList: Array<EntityTypeInfo> = []; |
|||
entityTypeList: Array<EntityTypeInfo> = []; |
|||
filteredEntityTypeList: Observable<Array<EntityTypeInfo>>; |
|||
|
|||
placeholder: string; |
|||
secondaryPlaceholder: string; |
|||
|
|||
private searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
private entityService: EntityService, |
|||
private fb: FormBuilder) { |
|||
this.entityTypeListFormGroup = this.fb.group({ |
|||
entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []], |
|||
entityType: [null] |
|||
}); |
|||
} |
|||
|
|||
updateValidators() { |
|||
this.entityTypeListFormGroup.get('entityTypeList').setValidators(this.required ? [Validators.required] : []); |
|||
this.entityTypeListFormGroup.get('entityTypeList').updateValueAndValidity(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
|
|||
this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') |
|||
: this.translate.instant('entity.any-entity'); |
|||
this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); |
|||
|
|||
let entityTypes: Array<EntityType | AliasEntityType>; |
|||
if (this.ignoreAuthorityFilter && this.allowedEntityTypes |
|||
&& this.allowedEntityTypes.length) { |
|||
entityTypes = []; |
|||
this.allowedEntityTypes.forEach((entityTypeValue) => { |
|||
entityTypes.push(entityTypeValue); |
|||
}); |
|||
} else { |
|||
entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes) as Array<EntityType>; |
|||
} |
|||
|
|||
entityTypes.forEach((entityType) => { |
|||
this.allEntityTypeList.push({ |
|||
name: this.translate.instant(entityTypeTranslations.get(entityType).type), |
|||
value: entityType as EntityType |
|||
}); |
|||
}); |
|||
|
|||
this.filteredEntityTypeList = this.entityTypeListFormGroup.get('entityType').valueChanges |
|||
.pipe( |
|||
// startWith<string | BaseData<EntityId>>(''),
|
|||
tap((value) => { |
|||
if (value && typeof value !== 'string') { |
|||
this.add(value); |
|||
} else if (value === null) { |
|||
this.clear(this.entityTypeInput.nativeElement.value); |
|||
} |
|||
}), |
|||
filter((value) => typeof value === 'string'), |
|||
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), |
|||
mergeMap(name => this.fetchEntityTypes(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.entityTypeListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.entityTypeListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<EntityType> | null): void { |
|||
this.searchText = ''; |
|||
if (value != null && value.length > 0) { |
|||
this.modelValue = [...value]; |
|||
this.entityTypeList = []; |
|||
value.forEach((entityType) => { |
|||
this.entityTypeList.push({ |
|||
name: this.translate.instant(entityTypeTranslations.get(entityType).type), |
|||
value: entityType |
|||
}); |
|||
}); |
|||
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
|||
} else { |
|||
this.entityTypeList = []; |
|||
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
|||
this.modelValue = null; |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
add(entityType: EntityTypeInfo): void { |
|||
if (!this.modelValue || this.modelValue.indexOf(entityType.value) === -1) { |
|||
if (!this.modelValue) { |
|||
this.modelValue = []; |
|||
} |
|||
this.modelValue.push(entityType.value); |
|||
this.entityTypeList.push(entityType); |
|||
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
|
|||
remove(entityType: EntityTypeInfo) { |
|||
const index = this.entityTypeList.indexOf(entityType); |
|||
if (index >= 0) { |
|||
this.entityTypeList.splice(index, 1); |
|||
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
|||
this.modelValue.splice(index, 1); |
|||
if (!this.modelValue.length) { |
|||
this.modelValue = null; |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
} |
|||
|
|||
displayEntityTypeFn(entityType?: EntityTypeInfo): string | undefined { |
|||
return entityType ? entityType.name : undefined; |
|||
} |
|||
|
|||
fetchEntityTypes(searchText?: string): Observable<Array<EntityTypeInfo>> { |
|||
this.searchText = searchText; |
|||
let result = this.allEntityTypeList; |
|||
if (searchText && searchText.length) { |
|||
result = this.allEntityTypeList.filter((entityTypeInfo) => entityTypeInfo.name.toLowerCase().includes(searchText.toLowerCase())); |
|||
} |
|||
return of(result); |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.entityTypeListFormGroup.get('entityType').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.entityTypeInput.nativeElement.value = value; |
|||
this.entityTypeListFormGroup.get('entityType').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.entityTypeInput.nativeElement.blur(); |
|||
this.entityTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
///
|
|||
/// 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, Input } from '@angular/core'; |
|||
import { trigger, state, style, transition, animate } from '@angular/animations'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-error', |
|||
template: ` |
|||
<div [@animation]="state" style="margin-top:0.5rem;font-size:.75rem"> |
|||
<mat-error > |
|||
{{message}} |
|||
</mat-error> |
|||
</div> |
|||
`,
|
|||
styles: [` |
|||
:host { |
|||
height: 24px; |
|||
} |
|||
`],
|
|||
animations: [ |
|||
trigger('animation', [ |
|||
state('show', style({ |
|||
opacity: 1, |
|||
})), |
|||
state('hide', style({ |
|||
opacity: 0, |
|||
transform: 'translateY(-1rem)' |
|||
})), |
|||
transition('show => hide', animate('200ms ease-out')), |
|||
transition('* => show', animate('200ms ease-in')) |
|||
|
|||
]), |
|||
] |
|||
}) |
|||
export class TbErrorComponent { |
|||
errorValue: any; |
|||
state: any; |
|||
message; |
|||
|
|||
@Input() |
|||
set error(value) { |
|||
if (value && !this.message) { |
|||
this.message = value; |
|||
this.state = 'hide'; |
|||
setTimeout(() => { |
|||
this.state = 'show'; |
|||
}); |
|||
} else { |
|||
this.errorValue = value; |
|||
this.state = value ? 'show' : 'hide'; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue