Commit 984e260be38ca2c16f37bbfa9ff68aa496d89084

Authored by Igor Kulikov
1 parent 3700bcaa

Devices and Customers pages implementation

... ... @@ -102,6 +102,11 @@ export class DeviceService {
102 102 return this.http.post<Device>(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors));
103 103 }
104 104
  105 + public assignDeviceToCustomer(customerId: string, deviceId: string,
  106 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
  107 + return this.http.post<Device>(`/api/customer/${customerId}/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors));
  108 + }
  109 +
105 110 public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
106 111 return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
107 112 }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #assignToCustomerForm="ngForm" [formGroup]="assignToCustomerFormGroup" (ngSubmit)="assign()">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ assignToCustomerTitle | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <span>{{ assignToCustomerText | translate }}</span>
  34 + <tb-entity-autocomplete
  35 + formControlName="customerId"
  36 + required
  37 + [entityType]="entityType.CUSTOMER">
  38 + </tb-entity-autocomplete>
  39 + </fieldset>
  40 + </div>
  41 + <div mat-dialog-actions fxLayout="row">
  42 + <span fxFlex></span>
  43 + <button mat-button mat-raised-button color="primary"
  44 + type="submit"
  45 + [disabled]="(isLoading$ | async) || assignToCustomerForm.invalid
  46 + || !assignToCustomerForm.dirty">
  47 + {{ 'action.assign' | translate }}
  48 + </button>
  49 + <button mat-button color="primary"
  50 + style="margin-right: 20px;"
  51 + type="button"
  52 + [disabled]="(isLoading$ | async)"
  53 + (click)="cancel()" cdkFocusInitial>
  54 + {{ 'action.cancel' | translate }}
  55 + </button>
  56 + </div>
  57 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {Component, Inject, OnInit, SkipSelf} from '@angular/core';
  18 +import {ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material';
  19 +import {PageComponent} from '@shared/components/page.component';
  20 +import {Store} from '@ngrx/store';
  21 +import {AppState} from '@core/core.state';
  22 +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms';
  23 +import {DeviceService} from '@core/http/device.service';
  24 +import {EntityId} from '@shared/models/id/entity-id';
  25 +import {EntityType} from '@shared/models/entity-type.models';
  26 +import {forkJoin, Observable} from 'rxjs';
  27 +
  28 +export interface AssignToCustomerDialogData {
  29 + entityIds: Array<EntityId>;
  30 + entityType: EntityType;
  31 +}
  32 +
  33 +@Component({
  34 + selector: 'tb-assign-to-customer-dialog',
  35 + templateUrl: './assign-to-customer-dialog.component.html',
  36 + providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}],
  37 + styleUrls: []
  38 +})
  39 +export class AssignToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  40 +
  41 + assignToCustomerFormGroup: FormGroup;
  42 +
  43 + submitted = false;
  44 +
  45 + entityType = EntityType;
  46 +
  47 + assignToCustomerTitle: string;
  48 + assignToCustomerText: string;
  49 +
  50 + constructor(protected store: Store<AppState>,
  51 + @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData,
  52 + private deviceService: DeviceService,
  53 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  54 + public dialogRef: MatDialogRef<AssignToCustomerDialogComponent, boolean>,
  55 + public fb: FormBuilder) {
  56 + super(store);
  57 + }
  58 +
  59 + ngOnInit(): void {
  60 + this.assignToCustomerFormGroup = this.fb.group({
  61 + customerId: [null, [Validators.required]]
  62 + });
  63 + switch (this.data.entityType) {
  64 + case EntityType.DEVICE:
  65 + this.assignToCustomerTitle = 'device.assign-device-to-customer';
  66 + this.assignToCustomerText = 'device.assign-to-customer-text';
  67 + break;
  68 + case EntityType.ASSET:
  69 + // TODO:
  70 + break;
  71 + case EntityType.ENTITY_VIEW:
  72 + // TODO:
  73 + break;
  74 + }
  75 + }
  76 +
  77 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  78 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  79 + const customErrorState = !!(control && control.invalid && this.submitted);
  80 + return originalErrorState || customErrorState;
  81 + }
  82 +
  83 + cancel(): void {
  84 + this.dialogRef.close(false);
  85 + }
  86 +
  87 + assign(): void {
  88 + this.submitted = true;
  89 + const customerId: string = this.assignToCustomerFormGroup.get('customerId').value;
  90 + const tasks: Observable<any>[] = [];
  91 + this.data.entityIds.forEach(
  92 + (entityId) => {
  93 + tasks.push(this.getAssignToCustomerTask(customerId, entityId.id));
  94 + }
  95 + );
  96 + forkJoin(tasks).subscribe(
  97 + () => {
  98 + this.dialogRef.close(true);
  99 + }
  100 + );
  101 + }
  102 +
  103 + private getAssignToCustomerTask(customerId: string, entityId: string): Observable<any> {
  104 + switch (this.data.entityType) {
  105 + case EntityType.DEVICE:
  106 + return this.deviceService.assignDeviceToCustomer(customerId, entityId);
  107 + break;
  108 + case EntityType.ASSET:
  109 + // TODO:
  110 + break;
  111 + case EntityType.ENTITY_VIEW:
  112 + // TODO:
  113 + break;
  114 + }
  115 + }
  116 +
  117 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@app/shared/shared.module';
  20 +import {AssignToCustomerDialogComponent} from '@modules/home/dialogs/assign-to-customer-dialog.component';
  21 +
  22 +@NgModule({
  23 + entryComponents: [
  24 + AssignToCustomerDialogComponent
  25 + ],
  26 + declarations:
  27 + [
  28 + AssignToCustomerDialogComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule
  33 + ],
  34 + exports: [
  35 + AssignToCustomerDialogComponent
  36 + ]
  37 +})
  38 +export class HomeDialogsModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {NgModule} from '@angular/core';
  18 +import {RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
  21 +import {Authority} from '@shared/models/authority.enum';
  22 +import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
  23 +import {CustomersTableConfigResolver} from './customers-table-config.resolver';
  24 +
  25 +const routes: Routes = [
  26 + {
  27 + path: 'customers',
  28 + data: {
  29 + breadcrumb: {
  30 + label: 'customer.customers',
  31 + icon: 'supervisor_account'
  32 + }
  33 + },
  34 + children: [
  35 + {
  36 + path: '',
  37 + component: EntitiesTableComponent,
  38 + data: {
  39 + auth: [Authority.TENANT_ADMIN],
  40 + title: 'customer.customers'
  41 + },
  42 + resolve: {
  43 + entitiesTableConfig: CustomersTableConfigResolver
  44 + }
  45 + },
  46 + {
  47 + path: ':customerId/users',
  48 + component: EntitiesTableComponent,
  49 + data: {
  50 + auth: [Authority.TENANT_ADMIN],
  51 + title: 'user.customer-users',
  52 + breadcrumb: {
  53 + label: 'user.customer-users',
  54 + icon: 'account_circle'
  55 + }
  56 + },
  57 + resolve: {
  58 + entitiesTableConfig: UsersTableConfigResolver
  59 + }
  60 + }
  61 + ]
  62 + }
  63 +];
  64 +
  65 +@NgModule({
  66 + imports: [RouterModule.forChild(routes)],
  67 + exports: [RouterModule],
  68 + providers: [
  69 + CustomersTableConfigResolver
  70 + ]
  71 +})
  72 +export class CustomerRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'manageUsers')"
  22 + [fxShow]="!isEdit && !isPublic">
  23 + {{'customer.manage-users' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'manageAssets')"
  28 + [fxShow]="!isEdit">
  29 + {{'customer.manage-assets' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'manageDevices')"
  34 + [fxShow]="!isEdit">
  35 + {{'customer.manage-devices' | translate }}
  36 + </button>
  37 + <button mat-raised-button color="primary"
  38 + [disabled]="(isLoading$ | async)"
  39 + (click)="onEntityAction($event, 'manageDashboards')"
  40 + [fxShow]="!isEdit">
  41 + {{'customer.manage-dashboards' | translate }}
  42 + </button>
  43 + <button mat-raised-button color="primary"
  44 + [disabled]="(isLoading$ | async)"
  45 + (click)="onEntityAction($event, 'delete')"
  46 + [fxShow]="!hideDelete() && !isEdit && !isPublic">
  47 + {{'customer.delete' | translate }}
  48 + </button>
  49 + <div fxLayout="row">
  50 + <button mat-raised-button
  51 + ngxClipboard
  52 + (cbOnSuccess)="onCustomerIdCopied($event)"
  53 + [cbContent]="entity?.id?.id"
  54 + [fxShow]="!isEdit">
  55 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  56 + <span translate>customer.copyId</span>
  57 + </button>
  58 + </div>
  59 +</div>
  60 +<div class="mat-padding" fxLayout="column">
  61 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  62 + <fieldset [fxShow]="!isPublic" [disabled]="(isLoading$ | async) || !isEdit">
  63 + <mat-form-field class="mat-block">
  64 + <mat-label translate>customer.title</mat-label>
  65 + <input matInput formControlName="title" required/>
  66 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  67 + {{ 'customer.title-required' | translate }}
  68 + </mat-error>
  69 + </mat-form-field>
  70 + <div formGroupName="additionalInfo" fxLayout="column">
  71 + <mat-form-field class="mat-block">
  72 + <mat-label translate>customer.description</mat-label>
  73 + <textarea matInput formControlName="description" rows="2"></textarea>
  74 + </mat-form-field>
  75 + </div>
  76 + <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
  77 + </fieldset>
  78 + </form>
  79 +</div>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  21 +import { Customer } from '@shared/models/customer.model';
  22 +import { ContactBasedComponent } from '@shared/components/entity/contact-based.component';
  23 +import {Tenant} from '@app/shared/models/tenant.model';
  24 +import {ActionNotificationShow} from '@app/core/notification/notification.actions';
  25 +import {TranslateService} from '@ngx-translate/core';
  26 +
  27 +@Component({
  28 + selector: 'tb-customer',
  29 + templateUrl: './customer.component.html'
  30 +})
  31 +export class CustomerComponent extends ContactBasedComponent<Customer> {
  32 +
  33 + isPublic = false;
  34 +
  35 + constructor(protected store: Store<AppState>,
  36 + protected translate: TranslateService,
  37 + protected fb: FormBuilder) {
  38 + super(store, fb);
  39 + }
  40 +
  41 + hideDelete() {
  42 + if (this.entitiesTableConfig) {
  43 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  44 + } else {
  45 + return false;
  46 + }
  47 + }
  48 +
  49 + buildEntityForm(entity: Customer): FormGroup {
  50 + return this.fb.group(
  51 + {
  52 + title: [entity ? entity.title : '', [Validators.required]],
  53 + additionalInfo: this.fb.group(
  54 + {
  55 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
  56 + }
  57 + )
  58 + }
  59 + );
  60 + }
  61 +
  62 + updateEntityForm(entity: Customer) {
  63 + this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
  64 + this.entityForm.patchValue({title: entity.title});
  65 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  66 + }
  67 +
  68 + onCustomerIdCopied(event) {
  69 + this.store.dispatch(new ActionNotificationShow(
  70 + {
  71 + message: this.translate.instant('customer.idCopiedMessage'),
  72 + type: 'success',
  73 + duration: 750,
  74 + verticalPosition: 'bottom',
  75 + horizontalPosition: 'right'
  76 + }));
  77 + }
  78 +
  79 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import {CustomerComponent} from '@modules/home/pages/customer/customer.component';
  21 +import {CustomerRoutingModule} from './customer-routing.module';
  22 +
  23 +@NgModule({
  24 + entryComponents: [
  25 + CustomerComponent
  26 + ],
  27 + declarations: [
  28 + CustomerComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule,
  33 + CustomerRoutingModule
  34 + ]
  35 +})
  36 +export class CustomerModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +
  19 +import { Resolve, Router } from '@angular/router';
  20 +
  21 +import { Tenant } from '@shared/models/tenant.model';
  22 +import {
  23 + DateEntityTableColumn,
  24 + EntityTableColumn,
  25 + EntityTableConfig
  26 +} from '@shared/components/entity/entities-table-config.models';
  27 +import { TranslateService } from '@ngx-translate/core';
  28 +import { DatePipe } from '@angular/common';
  29 +import {
  30 + EntityType,
  31 + entityTypeResources,
  32 + entityTypeTranslations
  33 +} from '@shared/models/entity-type.models';
  34 +import { EntityAction } from '@shared/components/entity/entity-component.models';
  35 +import {Customer} from '@app/shared/models/customer.model';
  36 +import {CustomerService} from '@app/core/http/customer.service';
  37 +import {CustomerComponent} from '@modules/home/pages/customer/customer.component';
  38 +
  39 +@Injectable()
  40 +export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<Customer>> {
  41 +
  42 + private readonly config: EntityTableConfig<Customer> = new EntityTableConfig<Customer>();
  43 +
  44 + constructor(private customerService: CustomerService,
  45 + private translate: TranslateService,
  46 + private datePipe: DatePipe,
  47 + private router: Router) {
  48 +
  49 + this.config.entityType = EntityType.CUSTOMER;
  50 + this.config.entityComponent = CustomerComponent;
  51 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.CUSTOMER);
  52 + this.config.entityResources = entityTypeResources.get(EntityType.CUSTOMER);
  53 +
  54 + this.config.columns.push(
  55 + new DateEntityTableColumn<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'),
  56 + new EntityTableColumn<Customer>('title', 'customer.title'),
  57 + new EntityTableColumn<Customer>('email', 'contact.email'),
  58 + new EntityTableColumn<Customer>('country', 'contact.country'),
  59 + new EntityTableColumn<Customer>('city', 'contact.city')
  60 + );
  61 +
  62 + this.config.cellActionDescriptors.push(
  63 + {
  64 + name: this.translate.instant('customer.manage-customer-users'),
  65 + icon: 'account_circle',
  66 + isEnabled: (customer) => !customer.additionalInfo || !customer.additionalInfo.isPublic,
  67 + onAction: ($event, entity) => this.manageCustomerUsers($event, entity)
  68 + }
  69 + );
  70 +
  71 + this.config.deleteEntityTitle = customer => this.translate.instant('customer.delete-customer-title', { customerTitle: customer.title });
  72 + this.config.deleteEntityContent = () => this.translate.instant('customer.delete-customer-text');
  73 + this.config.deleteEntitiesTitle = count => this.translate.instant('customer.delete-customers-title', {count});
  74 + this.config.deleteEntitiesContent = () => this.translate.instant('customer.delete-customers-text');
  75 +
  76 + this.config.entitiesFetchFunction = pageLink => this.customerService.getCustomers(pageLink);
  77 + this.config.loadEntity = id => this.customerService.getCustomer(id.id);
  78 + this.config.saveEntity = customer => this.customerService.saveCustomer(customer);
  79 + this.config.deleteEntity = id => this.customerService.deleteCustomer(id.id);
  80 + this.config.onEntityAction = action => this.onCustomerAction(action);
  81 + }
  82 +
  83 + resolve(): EntityTableConfig<Customer> {
  84 + this.config.tableTitle = this.translate.instant('customer.customers');
  85 +
  86 + return this.config;
  87 + }
  88 +
  89 + manageCustomerUsers($event: Event, customer: Customer) {
  90 + if ($event) {
  91 + $event.stopPropagation();
  92 + }
  93 + this.router.navigateByUrl(`customers/${customer.id.id}/users`);
  94 + }
  95 +
  96 + onCustomerAction(action: EntityAction<Customer>): boolean {
  97 + switch (action.action) {
  98 + case 'manageUsers':
  99 + this.manageCustomerUsers(action.event, action.entity);
  100 + return true;
  101 + }
  102 + return false;
  103 + }
  104 +
  105 +}
... ...
... ... @@ -21,6 +21,7 @@ import {DeviceComponent} from '@modules/home/pages/device/device.component';
21 21 import {DeviceRoutingModule} from './device-routing.module';
22 22 import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component';
23 23 import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component';
  24 +import {HomeDialogsModule} from '../../dialogs/home-dialogs.module';
24 25
25 26 @NgModule({
26 27 entryComponents: [
... ... @@ -36,6 +37,7 @@ import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/devic
36 37 imports: [
37 38 CommonModule,
38 39 SharedModule,
  40 + HomeDialogsModule,
39 41 DeviceRoutingModule
40 42 ]
41 43 })
... ...
... ... @@ -14,35 +14,26 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Injectable } from '@angular/core';
  17 +import {Injectable} from '@angular/core';
18 18
19 19 import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router';
20   -
21   -import { Tenant } from '@shared/models/tenant.model';
22 20 import {
23 21 CellActionDescriptor,
24 22 checkBoxCell,
25 23 DateEntityTableColumn,
26 24 EntityTableColumn,
27   - EntityTableConfig,
  25 + EntityTableConfig, GroupActionDescriptor,
28 26 HeaderActionDescriptor
29 27 } from '@shared/components/entity/entities-table-config.models';
30   -import { TenantService } from '@core/http/tenant.service';
31   -import { TranslateService } from '@ngx-translate/core';
32   -import { DatePipe } from '@angular/common';
33   -import {
34   - EntityType,
35   - entityTypeResources,
36   - entityTypeTranslations
37   -} from '@shared/models/entity-type.models';
38   -import { TenantComponent } from '@modules/home/pages/tenant/tenant.component';
39   -import { EntityAction } from '@shared/components/entity/entity-component.models';
40   -import { User } from '@shared/models/user.model';
  28 +import {TranslateService} from '@ngx-translate/core';
  29 +import {DatePipe} from '@angular/common';
  30 +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models';
  31 +import {EntityAction} from '@shared/components/entity/entity-component.models';
41 32 import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models';
42 33 import {DeviceComponent} from '@modules/home/pages/device/device.component';
43   -import {Observable, of} from 'rxjs';
  34 +import {forkJoin, Observable, of} from 'rxjs';
44 35 import {select, Store} from '@ngrx/store';
45   -import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors';
  36 +import {selectAuthUser} from '@core/auth/auth.selectors';
46 37 import {map, mergeMap, take, tap} from 'rxjs/operators';
47 38 import {AppState} from '@core/core.state';
48 39 import {DeviceService} from '@app/core/http/device.service';
... ... @@ -58,6 +49,11 @@ import {
58 49 DeviceCredentialsDialogData
59 50 } from '@modules/home/pages/device/device-credentials-dialog.component';
60 51 import {DialogService} from '@core/services/dialog.service';
  52 +import {
  53 + AssignToCustomerDialogComponent,
  54 + AssignToCustomerDialogData
  55 +} from '@modules/home/dialogs/assign-to-customer-dialog.component';
  56 +import {DeviceId} from '@app/shared/models/id/device-id';
61 57
62 58 @Injectable()
63 59 export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
... ... @@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
76 72 private router: Router,
77 73 private dialog: MatDialog) {
78 74
79   - this.config.entityType = EntityType.CUSTOMER;
  75 + this.config.entityType = EntityType.DEVICE;
80 76 this.config.entityComponent = DeviceComponent;
81 77 this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE);
82 78 this.config.entityResources = entityTypeResources.get(EntityType.DEVICE);
... ... @@ -131,7 +127,11 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
131 127 this.config.columns = this.configureColumns(this.config.componentsData.deviceScope);
132 128 this.configureEntityFunctions(this.config.componentsData.deviceScope);
133 129 this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
  130 + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
134 131 this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
  132 + this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user';
  133 + this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
  134 + this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
135 135 return this.config;
136 136 })
137 137 );
... ... @@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
175 175 }
176 176
177 177 configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
178   - const actions: Array<CellActionDescriptor<Device>> = [];
  178 + const actions: Array<CellActionDescriptor<DeviceInfo>> = [];
179 179 if (deviceScope === 'tenant') {
180 180 actions.push(
181 181 {
... ... @@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
183 183 icon: 'share',
184 184 isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
185 185 onAction: ($event, entity) => this.makePublic($event, entity)
  186 + },
  187 + {
  188 + name: this.translate.instant('device.assign-to-customer'),
  189 + icon: 'assignment_ind',
  190 + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
  191 + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id])
  192 + },
  193 + {
  194 + name: this.translate.instant('device.unassign-from-customer'),
  195 + icon: 'assignment_return',
  196 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic),
  197 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  198 + },
  199 + {
  200 + name: this.translate.instant('device.make-private'),
  201 + icon: 'reply',
  202 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic),
  203 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  204 + },
  205 + {
  206 + name: this.translate.instant('device.manage-credentials'),
  207 + icon: 'security',
  208 + isEnabled: (entity) => true,
  209 + onAction: ($event, entity) => this.manageCredentials($event, entity)
  210 + }
  211 + );
  212 + }
  213 + if (deviceScope === 'customer') {
  214 + actions.push(
  215 + {
  216 + name: this.translate.instant('device.unassign-from-customer'),
  217 + icon: 'assignment_return',
  218 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic),
  219 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  220 + },
  221 + {
  222 + name: this.translate.instant('device.make-private'),
  223 + icon: 'reply',
  224 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic),
  225 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  226 + },
  227 + {
  228 + name: this.translate.instant('device.manage-credentials'),
  229 + icon: 'security',
  230 + isEnabled: (entity) => true,
  231 + onAction: ($event, entity) => this.manageCredentials($event, entity)
  232 + }
  233 + );
  234 + }
  235 + if (deviceScope === 'customer_user') {
  236 + actions.push(
  237 + {
  238 + name: this.translate.instant('device.view-credentials'),
  239 + icon: 'security',
  240 + isEnabled: (entity) => true,
  241 + onAction: ($event, entity) => this.manageCredentials($event, entity)
  242 + }
  243 + );
  244 + }
  245 + return actions;
  246 + }
  247 +
  248 + configureGroupActions(deviceScope: string): Array<GroupActionDescriptor<DeviceInfo>> {
  249 + const actions: Array<GroupActionDescriptor<DeviceInfo>> = [];
  250 + if (deviceScope === 'tenant') {
  251 + actions.push(
  252 + {
  253 + name: this.translate.instant('device.assign-devices'),
  254 + icon: 'assignment_ind',
  255 + isEnabled: true,
  256 + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id))
  257 + }
  258 + );
  259 + }
  260 + if (deviceScope === 'customer') {
  261 + actions.push(
  262 + {
  263 + name: this.translate.instant('device.unassign-devices'),
  264 + icon: 'assignment_return',
  265 + isEnabled: true,
  266 + onAction: ($event, entities) => this.unassignDevicesFromCustomer($event, entities)
186 267 }
187 268 );
188 269 }
... ... @@ -191,20 +272,32 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
191 272
192 273 configureAddActions(deviceScope: string): Array<HeaderActionDescriptor> {
193 274 const actions: Array<HeaderActionDescriptor> = [];
194   - actions.push(
195   - {
196   - name: this.translate.instant('device.add-device-text'),
197   - icon: 'insert_drive_file',
198   - isEnabled: () => true,
199   - onAction: ($event) => this.config.table.addEntity($event)
200   - },
201   - {
202   - name: this.translate.instant('device.import'),
203   - icon: 'file_upload',
204   - isEnabled: () => true,
205   - onAction: ($event) => this.importDevices($event)
206   - }
207   - );
  275 + if (deviceScope === 'tenant') {
  276 + actions.push(
  277 + {
  278 + name: this.translate.instant('device.add-device-text'),
  279 + icon: 'insert_drive_file',
  280 + isEnabled: () => true,
  281 + onAction: ($event) => this.config.table.addEntity($event)
  282 + },
  283 + {
  284 + name: this.translate.instant('device.import'),
  285 + icon: 'file_upload',
  286 + isEnabled: () => true,
  287 + onAction: ($event) => this.importDevices($event)
  288 + }
  289 + );
  290 + }
  291 + if (deviceScope === 'customer') {
  292 + actions.push(
  293 + {
  294 + name: this.translate.instant('device.assign-new-device'),
  295 + icon: 'add',
  296 + isEnabled: () => true,
  297 + onAction: ($event) => this.addDevicesToCustomer($event)
  298 + }
  299 + );
  300 + }
208 301 return actions;
209 302 }
210 303
... ... @@ -215,6 +308,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
215 308 // TODO:
216 309 }
217 310
  311 + addDevicesToCustomer($event: Event) {
  312 + if ($event) {
  313 + $event.stopPropagation();
  314 + }
  315 + // TODO:
  316 + }
  317 +
218 318 makePublic($event: Event, device: Device) {
219 319 if ($event) {
220 320 $event.stopPropagation();
... ... @@ -237,11 +337,24 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
237 337 );
238 338 }
239 339
240   - assignToCustomer($event: Event, device: Device) {
  340 + assignToCustomer($event: Event, deviceIds: Array<DeviceId>) {
241 341 if ($event) {
242 342 $event.stopPropagation();
243 343 }
244   - // TODO:
  344 + this.dialog.open<AssignToCustomerDialogComponent, AssignToCustomerDialogData,
  345 + boolean>(AssignToCustomerDialogComponent, {
  346 + disableClose: true,
  347 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  348 + data: {
  349 + entityIds: deviceIds,
  350 + entityType: EntityType.DEVICE
  351 + }
  352 + }).afterClosed()
  353 + .subscribe((res) => {
  354 + if (res) {
  355 + this.config.table.updateData();
  356 + }
  357 + });
245 358 }
246 359
247 360 unassignFromCustomer($event: Event, device: DeviceInfo) {
... ... @@ -259,8 +372,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
259 372 content = this.translate.instant('device.unassign-device-text');
260 373 }
261 374 this.dialogService.confirm(
262   - this.translate.instant(title),
263   - this.translate.instant(content),
  375 + title,
  376 + content,
264 377 this.translate.instant('action.no'),
265 378 this.translate.instant('action.yes'),
266 379 true
... ... @@ -276,6 +389,34 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
276 389 );
277 390 }
278 391
  392 + unassignDevicesFromCustomer($event: Event, devices: Array<DeviceInfo>) {
  393 + if ($event) {
  394 + $event.stopPropagation();
  395 + }
  396 + this.dialogService.confirm(
  397 + this.translate.instant('device.unassign-devices-title', {count: devices.length}),
  398 + this.translate.instant('device.unassign-devices-text'),
  399 + this.translate.instant('action.no'),
  400 + this.translate.instant('action.yes'),
  401 + true
  402 + ).subscribe((res) => {
  403 + if (res) {
  404 + const tasks: Observable<any>[] = [];
  405 + devices.forEach(
  406 + (device) => {
  407 + tasks.push(this.deviceService.unassignDeviceFromCustomer(device.id.id));
  408 + }
  409 + );
  410 + forkJoin(tasks).subscribe(
  411 + () => {
  412 + this.config.table.updateData();
  413 + }
  414 + );
  415 + }
  416 + }
  417 + );
  418 + }
  419 +
279 420 manageCredentials($event: Event, device: Device) {
280 421 if ($event) {
281 422 $event.stopPropagation();
... ... @@ -297,7 +438,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
297 438 this.makePublic(action.event, action.entity);
298 439 return true;
299 440 case 'assignToCustomer':
300   - this.assignToCustomer(action.event, action.entity);
  441 + this.assignToCustomer(action.event, [action.entity.id]);
301 442 return true;
302 443 case 'unassignFromCustomer':
303 444 this.unassignFromCustomer(action.event, action.entity);
... ...
... ... @@ -20,7 +20,7 @@ import { AdminModule } from './admin/admin.module';
20 20 import { HomeLinksModule } from './home-links/home-links.module';
21 21 import { ProfileModule } from './profile/profile.module';
22 22 import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
23   -// import { CustomerModule } from '@modules/home/pages/customer/customer.module';
  23 +import { CustomerModule } from '@modules/home/pages/customer/customer.module';
24 24 // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
25 25 import { UserModule } from '@modules/home/pages/user/user.module';
26 26 import {DeviceModule} from '@modules/home/pages/device/device.module';
... ... @@ -32,7 +32,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module';
32 32 ProfileModule,
33 33 TenantModule,
34 34 DeviceModule,
35   -// CustomerModule,
  35 + CustomerModule,
36 36 // AuditLogModule,
37 37 UserModule
38 38 ]
... ...
... ... @@ -46,7 +46,7 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten
46 46 private datePipe: DatePipe,
47 47 private router: Router) {
48 48
49   - this.config.entityType = EntityType.CUSTOMER;
  49 + this.config.entityType = EntityType.TENANT;
50 50 this.config.entityComponent = TenantComponent;
51 51 this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT);
52 52 this.config.entityResources = entityTypeResources.get(EntityType.TENANT);
... ...
... ... @@ -145,8 +145,7 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
145 145 name: this.authority === Authority.TENANT_ADMIN ?
146 146 this.translate.instant('user.login-as-tenant-admin') :
147 147 this.translate.instant('user.login-as-customer-user'),
148   - icon: 'mdi:login',
149   - isMdiIcon: true,
  148 + mdiIcon: 'mdi:login',
150 149 isEnabled: () => true,
151 150 onAction: ($event, entity) => this.loginAsUser($event, entity)
152 151 }
... ...
... ... @@ -51,8 +51,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> {
51 51 name: string;
52 52 nameFunction?: (entity: T) => string;
53 53 icon?: string;
54   - isMdiIcon?: boolean;
55   - color?: string;
  54 + mdiIcon?: string;
  55 + style?: any;
56 56 isEnabled: (entity: T) => boolean;
57 57 onAction: ($event: MouseEvent, entity: T) => void;
58 58 }
... ...
... ... @@ -52,20 +52,30 @@
52 52 </button>
53 53 <ng-template #addActions>
54 54 <button mat-button mat-icon-button [disabled]="isLoading$ | async"
55   - matTooltip="{{ translations.add | translate }}"
56   - matTooltipPosition="above"
57   - [matMenuTriggerFor]="addActionsMenu">
58   - <mat-icon>add</mat-icon>
  55 + *ngIf="this.entitiesTableConfig.addActionDescriptors.length === 1; else addActionsMenu"
  56 + [fxShow]="this.entitiesTableConfig.addActionDescriptors[0].isEnabled()"
  57 + (click)="this.entitiesTableConfig.addActionDescriptors[0].onAction($event)"
  58 + matTooltip="{{ this.entitiesTableConfig.addActionDescriptors[0].name }}"
  59 + matTooltipPosition="above">
  60 + <mat-icon>{{this.entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon>
59 61 </button>
60   - <mat-menu #addActionsMenu="matMenu" xPosition="before">
61   - <button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors"
62   - [disabled]="isLoading$ | async"
63   - [fxShow]="actionDescriptor.isEnabled()"
64   - (click)="actionDescriptor.onAction($event)">
65   - <mat-icon>{{actionDescriptor.icon}}</mat-icon>
66   - <span>{{ actionDescriptor.name }}</span>
  62 + <ng-template #addActionsMenu>
  63 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  64 + matTooltip="{{ translations.add | translate }}"
  65 + matTooltipPosition="above"
  66 + [matMenuTriggerFor]="addActionsMenu">
  67 + <mat-icon>add</mat-icon>
67 68 </button>
68   - </mat-menu>
  69 + <mat-menu #addActionsMenu="matMenu" xPosition="before">
  70 + <button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors"
  71 + [disabled]="isLoading$ | async"
  72 + [fxShow]="actionDescriptor.isEnabled()"
  73 + (click)="actionDescriptor.onAction($event)">
  74 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  75 + <span>{{ actionDescriptor.name }}</span>
  76 + </button>
  77 + </mat-menu>
  78 + </ng-template>
69 79 </ng-template>
70 80 </div>
71 81 <button mat-button mat-icon-button [disabled]="isLoading$ | async"
... ... @@ -140,9 +150,11 @@
140 150 </mat-checkbox>
141 151 </mat-cell>
142 152 </ng-container>
143   - <ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
  153 + <ng-container [matColumnDef]="column.key" *ngFor="let column of columns; trackBy: trackByColumnKey; let col = index">
144 154 <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
145   - <mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
  155 + <mat-cell *matCellDef="let entity; let row = index"
  156 + [innerHTML]="cellContent(entity, column, row, col)"
  157 + [ngStyle]="cellStyle(entity, column, row, col)"></mat-cell>
146 158 </ng-container>
147 159 <ng-container matColumnDef="actions" stickyEnd>
148 160 <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
... ... @@ -155,10 +167,8 @@
155 167 matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
156 168 matTooltipPosition="above"
157 169 (click)="actionDescriptor.onAction($event, entity)">
158   - <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
  170 + <mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style">
159 171 {{actionDescriptor.icon}}</mat-icon>
160   - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
161   - [svgIcon]="actionDescriptor.icon"></mat-icon>
162 172 </button>
163 173 </div>
164 174 <div fxHide fxShow.lt-lg>
... ... @@ -172,10 +182,8 @@
172 182 [disabled]="isLoading$ | async"
173 183 [fxShow]="actionDescriptor.isEnabled(entity)"
174 184 (click)="actionDescriptor.onAction($event, entity)">
175   - <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
  185 + <mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style">
176 186 {{actionDescriptor.icon}}</mat-icon>
177   - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
178   - [svgIcon]="actionDescriptor.icon"></mat-icon>
179 187 <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
180 188 </button>
181 189 </mat-menu>
... ...
... ... @@ -21,7 +21,8 @@ import {
21 21 Input,
22 22 OnInit,
23 23 Type,
24   - ViewChild
  24 + ViewChild,
  25 + ChangeDetectionStrategy
25 26 } from '@angular/core';
26 27 import { PageComponent } from '@shared/components/page.component';
27 28 import { Store } from '@ngrx/store';
... ... @@ -51,13 +52,14 @@ import {
51 52 EntityAction
52 53 } from '@shared/components/entity/entity-component.models';
53 54 import { Timewindow } from '@shared/models/time/time.models';
54   -import { DomSanitizer } from '@angular/platform-browser';
  55 +import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
55 56 import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
56 57
57 58 @Component({
58 59 selector: 'tb-entities-table',
59 60 templateUrl: './entities-table.component.html',
60   - styleUrls: ['./entities-table.component.scss']
  61 + styleUrls: ['./entities-table.component.scss'],
  62 + changeDetection: ChangeDetectionStrategy.OnPush
61 63 })
62 64 export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit {
63 65
... ... @@ -73,6 +75,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
73 75 columns: Array<EntityTableColumn<BaseData<HasId>>>;
74 76 displayedColumns: string[] = [];
75 77
  78 + cellContentCache: Array<SafeHtml> = [];
  79 +
  80 + cellStyleCache: Array<any> = [];
  81 +
76 82 selectionEnabled;
77 83
78 84 pageLink: PageLink;
... ... @@ -139,7 +145,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
139 145
140 146 this.columns = [...this.entitiesTableConfig.columns];
141 147
142   - this.selectionEnabled = this.entitiesTableConfig.selectionEnabled;
  148 + const enabledGroupActionDescriptors =
  149 + this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled);
  150 +
  151 + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled && enabledGroupActionDescriptors.length;
143 152
144 153 if (this.selectionEnabled) {
145 154 this.displayedColumns.push('select');
... ... @@ -163,7 +172,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
163 172 this.pageLink = new PageLink(10, 0, null, sortOrder);
164 173 }
165 174 this.dataSource = new EntitiesDataSource<BaseData<HasId>>(
166   - this.entitiesTableConfig.entitiesFetchFunction
  175 + this.entitiesTableConfig.entitiesFetchFunction,
  176 + () => {
  177 + this.dataLoaded();
  178 + }
167 179 );
168 180 if (this.entitiesTableConfig.onLoadAction) {
169 181 this.entitiesTableConfig.onLoadAction(this.route);
... ... @@ -221,6 +233,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
221 233 this.dataSource.loadEntities(this.pageLink);
222 234 }
223 235
  236 + private dataLoaded() {
  237 + this.cellContentCache.length = 0;
  238 + this.cellStyleCache.length = 0;
  239 + }
  240 +
224 241 onRowClick($event: Event, entity) {
225 242 if ($event) {
226 243 $event.stopPropagation();
... ... @@ -347,12 +364,28 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
347 364 }
348 365 }
349 366
350   - cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
351   - return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
  367 + cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>, row: number, col: number) {
  368 + const index = row * this.columns.length + col;
  369 + let res = this.cellContentCache[index];
  370 + if (!res) {
  371 + res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
  372 + this.cellContentCache[index] = res;
  373 + }
  374 + return res;
  375 + }
  376 +
  377 + cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>, row: number, col: number) {
  378 + const index = row * this.columns.length + col;
  379 + let res = this.cellStyleCache[index];
  380 + if (!res) {
  381 + res = {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
  382 + this.cellStyleCache[index] = res;
  383 + }
  384 + return res;
352 385 }
353 386
354   - cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
355   - return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
  387 + trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>>) {
  388 + return column.key;
356 389 }
357 390
358 391 }
... ...
... ... @@ -105,7 +105,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
105 105 }
106 106
107 107 ngOnInit() {
108   - this.filteredEntities = this.selectEntityFormGroup.get('dashboard').valueChanges
  108 + this.filteredEntities = this.selectEntityFormGroup.get('entity').valueChanges
109 109 .pipe(
110 110 tap(value => {
111 111 let modelValue;
... ...
... ... @@ -15,6 +15,7 @@
15 15 ///
16 16
17 17 import {
  18 + ChangeDetectionStrategy,
18 19 Component,
19 20 ComponentFactoryResolver,
20 21 EventEmitter,
... ... @@ -44,7 +45,8 @@ import { Subscription } from 'rxjs';
44 45 @Component({
45 46 selector: 'tb-entity-details-panel',
46 47 templateUrl: './entity-details-panel.component.html',
47   - styleUrls: ['./entity-details-panel.component.scss']
  48 + styleUrls: ['./entity-details-panel.component.scss'],
  49 + changeDetection: ChangeDetectionStrategy.OnPush
48 50 })
49 51 export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy {
50 52
... ...
... ... @@ -36,7 +36,8 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
36 36
37 37 public currentEntity: T = null;
38 38
39   - constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {}
  39 + constructor(private fetchFunction: EntitiesFetchFunction<T, P>,
  40 + private dataLoadedFunction: () => void) {}
40 41
41 42 connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
42 43 return this.entitiesSubject.asObservable();
... ... @@ -59,6 +60,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
59 60 this.entitiesSubject.next(pageData.data);
60 61 this.pageDataSubject.next(pageData);
61 62 result.next(pageData);
  63 + this.dataLoadedFunction();
62 64 }
63 65 );
64 66 return result;
... ...
... ... @@ -395,6 +395,7 @@
395 395 "manage-assets": "Manage assets",
396 396 "manage-devices": "Manage devices",
397 397 "manage-dashboards": "Manage dashboards",
  398 + "created-time": "Created time",
398 399 "title": "Title",
399 400 "title-required": "Title is required.",
400 401 "description": "Description",
... ...