Browse Source

Customers page

pull/1954/head
Igor Kulikov 7 years ago
parent
commit
680cd1d2c1
  1. 4
      ui-ngx/src/app/core/http/device.service.ts
  2. 76
      ui-ngx/src/app/core/http/entity.service.ts
  3. 58
      ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html
  4. 118
      ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts
  5. 10
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts
  6. 17
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  7. 57
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  8. 19
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  9. 9
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  10. 1
      ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts
  11. 1
      ui-ngx/src/app/shared/components/entity/entities-table.component.html
  12. 1
      ui-ngx/src/app/shared/components/entity/entities-table.component.ts
  13. 23
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  14. 48
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  15. 202
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  16. 9
      ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts
  17. 16
      ui-ngx/src/app/shared/models/datasource/entity-datasource.ts
  18. 9
      ui-ngx/src/app/shared/shared.module.ts

4
ui-ngx/src/app/core/http/device.service.ts

@ -52,6 +52,10 @@ export class DeviceService {
return this.http.get<Device>(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDevices(deviceIds: Array<string>, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Device>> {
return this.http.get<Array<Device>>(`/api/devices?deviceIds=${deviceIds.join(',')}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDeviceInfo(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DeviceInfo> {
return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}

76
ui-ngx/src/app/core/http/entity.service.ts

@ -15,7 +15,7 @@
///
import {Injectable} from '@angular/core';
import {Observable, throwError, of, empty, EMPTY} from 'rxjs/index';
import {Observable, throwError, of, empty, EMPTY, forkJoin} from 'rxjs/index';
import {HttpClient} from '@angular/common/http';
import {PageLink} from '@shared/models/page/page-link';
import {EntityType} from '@shared/models/entity-type.models';
@ -86,7 +86,6 @@ export class EntityService {
}
return observable;
}
public getEntity(entityType: EntityType, entityId: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<BaseData<EntityId>> {
const entityObservable = this.getEntityObservable(entityType, entityId, ignoreErrors, ignoreLoading);
@ -97,6 +96,79 @@ export class EntityService {
}
}
private getEntitiesByIdsObservable(fetchEntityFunction: (entityId: string) => Observable<BaseData<EntityId>>,
entityIds: Array<string>): Observable<Array<BaseData<EntityId>>> {
const tasks: Observable<BaseData<EntityId>>[] = [];
entityIds.forEach((entityId) => {
tasks.push(fetchEntityFunction(entityId));
});
return forkJoin(tasks).pipe(
map((entities) => {
if (entities) {
entities.sort((entity1, entity2) => {
const index1 = entityIds.indexOf(entity1.id.id);
const index2 = entityIds.indexOf(entity2.id.id);
return index1 - index2;
});
return entities;
} else {
return [];
}
})
);
}
private getEntitiesObservable(entityType: EntityType, entityIds: Array<string>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<BaseData<EntityId>>> {
let observable: Observable<Array<BaseData<EntityId>>>;
switch (entityType) {
case EntityType.DEVICE:
observable = this.deviceService.getDevices(entityIds, ignoreErrors, ignoreLoading);
break;
case EntityType.ASSET:
// TODO:
break;
case EntityType.ENTITY_VIEW:
// TODO:
break;
case EntityType.TENANT:
observable = this.getEntitiesByIdsObservable(
(id) => this.tenantService.getTenant(id, ignoreErrors, ignoreLoading),
entityIds);
break;
case EntityType.CUSTOMER:
observable = this.getEntitiesByIdsObservable(
(id) => this.customerService.getCustomer(id, ignoreErrors, ignoreLoading),
entityIds);
break;
case EntityType.DASHBOARD:
observable = this.getEntitiesByIdsObservable(
(id) => this.dashboardService.getDashboardInfo(id, ignoreErrors, ignoreLoading),
entityIds);
break;
case EntityType.USER:
observable = this.getEntitiesByIdsObservable(
(id) => this.userService.getUser(id, ignoreErrors, ignoreLoading),
entityIds);
break;
case EntityType.ALARM:
console.error('Get Alarm Entity is not implemented!');
break;
}
return observable;
}
public getEntities(entityType: EntityType, entityIds: Array<string>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<BaseData<EntityId>>> {
const entitiesObservable = this.getEntitiesObservable(entityType, entityIds, ignoreErrors, ignoreLoading);
if (entitiesObservable) {
return entitiesObservable;
} else {
return throwError(null);
}
}
private getSingleTenantByPageLinkObservable(pageLink: PageLink,
ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<Tenant>> {

58
ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html

@ -0,0 +1,58 @@
<!--
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 #addEntitiesToCustomerForm="ngForm" [formGroup]="addEntitiesToCustomerFormGroup" (ngSubmit)="assign()">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ assignToCustomerTitle | 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 mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<span>{{ assignToCustomerText | translate }}</span>
<tb-entity-list
formControlName="entityIds"
required
[entityType]="entityType"
>
</tb-entity-list>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || addEntitiesToCustomerForm.invalid
|| !addEntitiesToCustomerForm.dirty">
{{ 'action.assign' | 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>

118
ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts

@ -0,0 +1,118 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Component, Inject, OnInit, SkipSelf} from '@angular/core';
import {ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material';
import {PageComponent} from '@shared/components/page.component';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms';
import {DeviceService} from '@core/http/device.service';
import {EntityId} from '@shared/models/id/entity-id';
import {EntityType} from '@shared/models/entity-type.models';
import {forkJoin, Observable} from 'rxjs';
export interface AddEntitiesToCustomerDialogData {
customerId: string;
entityType: EntityType;
}
@Component({
selector: 'tb-add-entities-to-customer-dialog',
templateUrl: './add-entities-to-customer-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent}],
styleUrls: []
})
export class AddEntitiesToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
addEntitiesToCustomerFormGroup: FormGroup;
submitted = false;
entityType: EntityType;
assignToCustomerTitle: string;
assignToCustomerText: string;
constructor(protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData,
private deviceService: DeviceService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddEntitiesToCustomerDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
this.entityType = data.entityType;
}
ngOnInit(): void {
this.addEntitiesToCustomerFormGroup = this.fb.group({
entityIds: [null, [Validators.required]]
});
switch (this.data.entityType) {
case EntityType.DEVICE:
this.assignToCustomerTitle = 'device.assign-device-to-customer';
this.assignToCustomerText = 'device.assign-device-to-customer-text';
break;
case EntityType.ASSET:
// TODO:
break;
case EntityType.ENTITY_VIEW:
// TODO:
break;
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(false);
}
assign(): void {
this.submitted = true;
const entityIds: Array<string> = this.addEntitiesToCustomerFormGroup.get('entityIds').value;
const tasks: Observable<any>[] = [];
entityIds.forEach(
(entityId) => {
tasks.push(this.getAssignToCustomerTask(this.data.customerId, entityId));
}
);
forkJoin(tasks).subscribe(
() => {
this.dialogRef.close(true);
}
);
}
private getAssignToCustomerTask(customerId: string, entityId: string): Observable<any> {
switch (this.data.entityType) {
case EntityType.DEVICE:
return this.deviceService.assignDeviceToCustomer(customerId, entityId);
break;
case EntityType.ASSET:
// TODO:
break;
case EntityType.ENTITY_VIEW:
// TODO:
break;
}
}
}

10
ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts

@ -18,21 +18,25 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import {AssignToCustomerDialogComponent} from '@modules/home/dialogs/assign-to-customer-dialog.component';
import {AddEntitiesToCustomerDialogComponent} from '@modules/home/dialogs/add-entities-to-customer-dialog.component';
@NgModule({
entryComponents: [
AssignToCustomerDialogComponent
AssignToCustomerDialogComponent,
AddEntitiesToCustomerDialogComponent
],
declarations:
[
AssignToCustomerDialogComponent
AssignToCustomerDialogComponent,
AddEntitiesToCustomerDialogComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
AssignToCustomerDialogComponent
AssignToCustomerDialogComponent,
AddEntitiesToCustomerDialogComponent
]
})
export class HomeDialogsModule { }

17
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -21,6 +21,7 @@ import {EntitiesTableComponent} from '@shared/components/entity/entities-table.c
import {Authority} from '@shared/models/authority.enum';
import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
import {CustomersTableConfigResolver} from './customers-table-config.resolver';
import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
const routes: Routes = [
{
@ -57,6 +58,22 @@ const routes: Routes = [
resolve: {
entitiesTableConfig: UsersTableConfigResolver
}
},
{
path: ':customerId/devices',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.devices',
devicesType: 'customer',
breadcrumb: {
label: 'customer.devices',
icon: 'devices_other'
}
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
}

57
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -65,6 +65,39 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
icon: 'account_circle',
isEnabled: (customer) => !customer.additionalInfo || !customer.additionalInfo.isPublic,
onAction: ($event, entity) => this.manageCustomerUsers($event, entity)
},
{
name: this.translate.instant('customer.manage-customer-assets'),
nameFunction: (customer) => {
return customer.additionalInfo && customer.additionalInfo.isPublic
? this.translate.instant('customer.manage-public-assets')
: this.translate.instant('customer.manage-customer-assets');
},
icon: 'domain',
isEnabled: (customer) => true,
onAction: ($event, entity) => this.manageCustomerAssets($event, entity)
},
{
name: this.translate.instant('customer.manage-customer-devices'),
nameFunction: (customer) => {
return customer.additionalInfo && customer.additionalInfo.isPublic
? this.translate.instant('customer.manage-public-devices')
: this.translate.instant('customer.manage-customer-devices');
},
icon: 'devices_other',
isEnabled: (customer) => true,
onAction: ($event, entity) => this.manageCustomerDevices($event, entity)
},
{
name: this.translate.instant('customer.manage-customer-dashboards'),
nameFunction: (customer) => {
return customer.additionalInfo && customer.additionalInfo.isPublic
? this.translate.instant('customer.manage-public-dashboards')
: this.translate.instant('customer.manage-customer-dashboards');
},
icon: 'dashboard',
isEnabled: (customer) => true,
onAction: ($event, entity) => this.manageCustomerDashboards($event, entity)
}
);
@ -78,6 +111,9 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
this.config.saveEntity = customer => this.customerService.saveCustomer(customer);
this.config.deleteEntity = id => this.customerService.deleteCustomer(id.id);
this.config.onEntityAction = action => this.onCustomerAction(action);
this.config.deleteEnabled = (customer) => customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
this.config.entitySelectionEnabled = (customer) => customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
this.config.detailsReadonly = (customer) => customer && customer.additionalInfo && customer.additionalInfo.isPublic;
}
resolve(): EntityTableConfig<Customer> {
@ -93,6 +129,27 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
this.router.navigateByUrl(`customers/${customer.id.id}/users`);
}
manageCustomerAssets($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`customers/${customer.id.id}/assets`);
}
manageCustomerDevices($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`customers/${customer.id.id}/devices`);
}
manageCustomerDashboards($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`customers/${customer.id.id}/dashboards`);
}
onCustomerAction(action: EntityAction<Customer>): boolean {
switch (action.action) {
case 'manageUsers':

19
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -54,6 +54,10 @@ import {
AssignToCustomerDialogData
} from '@modules/home/dialogs/assign-to-customer-dialog.component';
import {DeviceId} from '@app/shared/models/id/device-id';
import {
AddEntitiesToCustomerDialogComponent,
AddEntitiesToCustomerDialogData
} from '../../dialogs/add-entities-to-customer-dialog.component';
@Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
@ -312,7 +316,20 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialog.open<AddEntitiesToCustomerDialogComponent, AddEntitiesToCustomerDialogData,
boolean>(AddEntitiesToCustomerDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
customerId: this.customerId,
entityType: EntityType.DEVICE
}
}).afterClosed()
.subscribe((res) => {
if (res) {
this.config.table.updateData();
}
});
}
makePublic($event: Event, device: Device) {

9
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -29,6 +29,7 @@ import {AppState} from '@app/core/core.state';
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
import {Authority} from '@shared/models/authority.enum';
import {TranslateService} from '@ngx-translate/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
@Component({
selector: 'tb-dashboard-autocomplete',
@ -64,8 +65,14 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
@Input()
customerId: string;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
required: boolean;
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;

1
ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts

@ -126,6 +126,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
headerComponent: Type<EntityTableHeaderComponent<T>>;
addEntity: CreateEntityOperation<T> = null;
detailsReadonly: EntityBooleanFunction<T> = () => false;
entitySelectionEnabled: EntityBooleanFunction<T> = () => true;
deleteEnabled: EntityBooleanFunction<T> = () => true;
deleteEntityTitle: EntityStringFunction<T> = () => '';
deleteEntityContent: EntityStringFunction<T> = () => '';

1
ui-ngx/src/app/shared/components/entity/entities-table.component.html

@ -145,6 +145,7 @@
</mat-header-cell>
<mat-cell *matCellDef="let entity">
<mat-checkbox (click)="$event.stopPropagation()"
[fxShow]="entitiesTableConfig.entitySelectionEnabled(entity)"
(change)="$event ? dataSource.selection.toggle(entity) : null"
[checked]="dataSource.selection.isSelected(entity)">
</mat-checkbox>

1
ui-ngx/src/app/shared/components/entity/entities-table.component.ts

@ -173,6 +173,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
}
this.dataSource = new EntitiesDataSource<BaseData<HasId>>(
this.entitiesTableConfig.entitiesFetchFunction,
this.entitiesTableConfig.entitySelectionEnabled,
() => {
this.dataLoaded();
}

23
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -17,7 +17,7 @@
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable} from 'rxjs';
import {map, mergeMap, startWith, tap} from 'rxjs/operators';
import {map, mergeMap, startWith, tap, share} from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {TranslateService} from '@ngx-translate/core';
@ -25,6 +25,7 @@ import {AliasEntityType, EntityType} 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 {coerceBooleanProperty} from '@angular/cdk/coercion';
@Component({
selector: 'tb-entity-autocomplete',
@ -70,8 +71,14 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
excludeEntityIds: Array<string>;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
required: boolean;
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ -115,10 +122,14 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
modelValue = value.id.id;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
startWith<string | BaseData<EntityId>>(''),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchEntities(name) )
mergeMap(name => this.fetchEntities(name) ),
share()
);
}
@ -216,12 +227,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
);
} else {
this.modelValue = null;
this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true});
this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
}
}
reset() {
this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true});
this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
}
updateView(value: string | null) {
@ -264,7 +275,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
}
clear() {
this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true});
this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
setTimeout(() => {
this.entityInput.nativeElement.blur();
this.entityInput.nativeElement.focus();

48
ui-ngx/src/app/shared/components/entity/entity-list.component.html

@ -0,0 +1,48 @@
<!--
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 [formGroup]="entityListFormGroup" class="mat-block">
<mat-chip-list #chipList>
<mat-chip
*ngFor="let entity of entities"
[selectable]="!disabled"
[removable]="!disabled"
(removed)="remove(entity)">
{{entity.name}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ 'entity.entity-list' | translate }}"
#entityInput
formControlName="entity"
[matAutocomplete]="entityAutocomplete"
[matChipInputFor]="chipList">
</mat-chip-list>
<mat-autocomplete #entityAutocomplete="matAutocomplete"
[displayWith]="displayEntityFn">
<mat-option *ngFor="let entity of filteredEntities | async" [value]="entity">
<span [innerHTML]="entity.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span>
{{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error>
{{ 'entity.entity-list-empty' | translate }}
</mat-error>
</mat-form-field>

202
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

@ -0,0 +1,202 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NG_VALUE_ACCESSOR, NgForm
} from '@angular/forms';
import {Observable} 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} 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';
@Component({
selector: 'tb-entity-list',
templateUrl: './entity-list.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityListComponent),
multi: true
}
]
})
export class EntityListComponent implements ControlValueAccessor, OnInit, AfterViewInit {
entityListFormGroup: FormGroup;
modelValue: Array<string> | null;
entityTypeValue: EntityType;
@Input()
set entityType(entityType: EntityType) {
if (this.entityTypeValue !== entityType) {
this.entityTypeValue = entityType;
this.reset();
}
}
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ViewChild('entityInput', {static: false}) entityInput: ElementRef<HTMLInputElement>;
@ViewChild('entityAutocomplete', {static: false}) matAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: false}) chipList: MatChipList;
entities: Array<BaseData<EntityId>> = [];
filteredEntities: Observable<Array<BaseData<EntityId>>>;
private searchText = '';
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private entityService: EntityService,
private fb: FormBuilder) {
this.entityListFormGroup = this.fb.group({
entity: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredEntities = this.entityListFormGroup.get('entity').valueChanges
.pipe(
startWith<string | BaseData<EntityId>>(''),
tap((value) => {
if (value && typeof value !== 'string') {
this.add(value);
} else if (value === null) {
this.clear(this.entityInput.nativeElement.value);
}
}),
filter((value) => typeof value === 'string'),
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchEntities(name) ),
share()
);
}
ngAfterViewInit(): void {}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: Array<string> | null): void {
this.searchText = '';
if (value != null) {
this.modelValue = value;
this.entityService.getEntities(this.entityTypeValue, value).subscribe(
(entities) => {
this.entities = entities;
}
);
} else {
this.entities = [];
this.modelValue = null;
}
}
reset() {
this.entities = [];
this.modelValue = null;
this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true});
this.propagateChange(this.modelValue);
}
add(entity: BaseData<EntityId>): void {
if (!this.modelValue || this.modelValue.indexOf(entity.id.id) === -1) {
if (!this.modelValue) {
this.modelValue = [];
}
this.modelValue.push(entity.id.id);
this.entities.push(entity);
if (this.required) {
this.chipList.errorState = false;
}
}
this.propagateChange(this.modelValue);
this.clear();
}
remove(entity: BaseData<EntityId>) {
const index = this.entities.indexOf(entity);
if (index >= 0) {
this.entities.splice(index, 1);
this.modelValue.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
if (this.required) {
this.chipList.errorState = true;
}
}
this.propagateChange(this.modelValue);
this.clear();
}
}
displayEntityFn(entity?: BaseData<EntityId>): string | undefined {
return entity ? entity.name : undefined;
}
fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
this.searchText = searchText;
return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
50, '', false, true).pipe(
map((data) => data ? data : []));
}
clear(value: string = '') {
this.entityInput.nativeElement.value = value;
this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.entityInput.nativeElement.blur();
this.entityInput.nativeElement.focus();
}, 0);
}
}

9
ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts

@ -32,6 +32,7 @@ import {TranslateService} from '@ngx-translate/core';
import {DeviceService} from '@core/http/device.service';
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
import {BroadcastService} from '@app/core/services/broadcast.service';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
@Component({
selector: 'tb-entity-subtype-autocomplete',
@ -52,8 +53,14 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
@Input()
entityType: EntityType;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
required: boolean;
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;

16
ui-ngx/src/app/shared/models/datasource/entity-datasource.ts

@ -22,6 +22,7 @@ import { BaseData, HasId } from '@shared/models/base-data';
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { catchError, map, take, tap } from 'rxjs/operators';
import { SelectionModel } from '@angular/cdk/collections';
import {EntityBooleanFunction} from '@shared/components/entity/entities-table-config.models';
export type EntitiesFetchFunction<T extends BaseData<HasId>, P extends PageLink> = (pageLink: P) => Observable<PageData<T>>;
@ -37,6 +38,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
public currentEntity: T = null;
constructor(private fetchFunction: EntitiesFetchFunction<T, P>,
private selectionEnabledFunction: EntityBooleanFunction<T>,
private dataLoadedFunction: () => void) {}
connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
@ -69,7 +71,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe(
map((entities) => numSelected === entities.length)
map((entities) => numSelected === this.selectableEntitiesCount(entities))
);
}
@ -103,13 +105,21 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
this.entitiesSubject.pipe(
tap((entities) => {
const numSelected = this.selection.selected.length;
if (numSelected === entities.length) {
if (numSelected === this.selectableEntitiesCount(entities)) {
this.selection.clear();
} else {
entities.forEach(row => this.selection.select(row));
entities.forEach(row => {
if (this.selectionEnabledFunction(row)) {
this.selection.select(row);
}
});
}
}),
take(1)
).subscribe();
}
private selectableEntitiesCount(entities: Array<T>): number {
return entities.filter((entity) => this.selectionEnabledFunction(entity)).length;
}
}

9
ui-ngx/src/app/shared/shared.module.ts

@ -47,7 +47,9 @@ import {
MatDatepickerModule,
MatSliderModule,
MatExpansionModule,
MatStepperModule, MatAutocompleteModule
MatStepperModule,
MatAutocompleteModule,
MatChipsModule
} from '@angular/material';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { FlexLayoutModule } from '@angular/flex-layout';
@ -81,6 +83,7 @@ import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autoc
import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component';
import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component';
import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component';
import {EntityListComponent} from '@shared/components/entity/entity-list.component';
@NgModule({
providers: [
@ -124,6 +127,7 @@ import {EntityAutocompleteComponent} from './components/entity/entity-autocomple
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,
EntityAutocompleteComponent,
EntityListComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -162,6 +166,7 @@ import {EntityAutocompleteComponent} from './components/entity/entity-autocomple
MatExpansionModule,
MatStepperModule,
MatAutocompleteModule,
MatChipsModule,
ClipboardModule,
FlexLayoutModule.withConfig({addFlexToParent: false}),
FormsModule,
@ -192,6 +197,7 @@ import {EntityAutocompleteComponent} from './components/entity/entity-autocomple
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,
EntityAutocompleteComponent,
EntityListComponent,
// ValueInputComponent,
MatButtonModule,
MatCheckboxModule,
@ -222,6 +228,7 @@ import {EntityAutocompleteComponent} from './components/entity/entity-autocomple
MatExpansionModule,
MatStepperModule,
MatAutocompleteModule,
MatChipsModule,
ClipboardModule,
FlexLayoutModule,
FormsModule,

Loading…
Cancel
Save