From 984e260be38ca2c16f37bbfa9ff68aa496d89084 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 15 Aug 2019 14:48:14 +0300 Subject: [PATCH] Devices and Customers pages implementation --- ui-ngx/src/app/core/http/device.service.ts | 5 + .../assign-to-customer-dialog.component.html | 57 +++++ .../assign-to-customer-dialog.component.ts | 117 ++++++++++ .../home/dialogs/home-dialogs.module.ts | 38 +++ .../pages/customer/customer-routing.module.ts | 72 ++++++ .../pages/customer/customer.component.html | 79 +++++++ .../home/pages/customer/customer.component.ts | 79 +++++++ .../home/pages/customer/customer.module.ts | 36 +++ .../customers-table-config.resolver.ts | 105 +++++++++ .../home/pages/device/device.module.ts | 2 + .../device/devices-table-config.resolver.ts | 217 +++++++++++++++--- .../modules/home/pages/home-pages.module.ts | 4 +- .../tenant/tenants-table-config.resolver.ts | 2 +- .../pages/user/users-table-config.resolver.ts | 3 +- .../entity/entities-table-config.models.ts | 4 +- .../entity/entities-table.component.html | 48 ++-- .../entity/entities-table.component.ts | 51 +++- .../entity/entity-autocomplete.component.ts | 2 +- .../entity/entity-details-panel.component.ts | 4 +- .../models/datasource/entity-datasource.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 1 + 21 files changed, 853 insertions(+), 77 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/customer/customer.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/customer/customer.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/customer/customer.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index f970a90475..a45f89077b 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -102,6 +102,11 @@ export class DeviceService { return this.http.post(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); } + public assignDeviceToCustomer(customerId: string, deviceId: string, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.post(`/api/customer/${customerId}/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); } diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html new file mode 100644 index 0000000000..3d9f36c62d --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

{{ assignToCustomerTitle | translate }}

+ + +
+ + +
+
+
+ {{ assignToCustomerText | translate }} + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts new file mode 100644 index 0000000000..ea40fea272 --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts @@ -0,0 +1,117 @@ +/// +/// 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 AssignToCustomerDialogData { + entityIds: Array; + entityType: EntityType; +} + +@Component({ + selector: 'tb-assign-to-customer-dialog', + templateUrl: './assign-to-customer-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}], + styleUrls: [] +}) +export class AssignToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { + + assignToCustomerFormGroup: FormGroup; + + submitted = false; + + entityType = EntityType; + + assignToCustomerTitle: string; + assignToCustomerText: string; + + constructor(protected store: Store, + @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData, + private deviceService: DeviceService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.assignToCustomerFormGroup = this.fb.group({ + customerId: [null, [Validators.required]] + }); + switch (this.data.entityType) { + case EntityType.DEVICE: + this.assignToCustomerTitle = 'device.assign-device-to-customer'; + this.assignToCustomerText = 'device.assign-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 customerId: string = this.assignToCustomerFormGroup.get('customerId').value; + const tasks: Observable[] = []; + this.data.entityIds.forEach( + (entityId) => { + tasks.push(this.getAssignToCustomerTask(customerId, entityId.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getAssignToCustomerTask(customerId: string, entityId: string): Observable { + 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; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts new file mode 100644 index 0000000000..de8d55263d --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts @@ -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. +/// + +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'; + +@NgModule({ + entryComponents: [ + AssignToCustomerDialogComponent + ], + declarations: + [ + AssignToCustomerDialogComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + AssignToCustomerDialogComponent + ] +}) +export class HomeDialogsModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts new file mode 100644 index 0000000000..e2a8c44359 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -0,0 +1,72 @@ +/// +/// 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 {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component'; +import {Authority} from '@shared/models/authority.enum'; +import {UsersTableConfigResolver} from '../user/users-table-config.resolver'; +import {CustomersTableConfigResolver} from './customers-table-config.resolver'; + +const routes: Routes = [ + { + path: 'customers', + data: { + breadcrumb: { + label: 'customer.customers', + icon: 'supervisor_account' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.customers' + }, + resolve: { + entitiesTableConfig: CustomersTableConfigResolver + } + }, + { + path: ':customerId/users', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'user.customer-users', + breadcrumb: { + label: 'user.customer-users', + icon: 'account_circle' + } + }, + resolve: { + entitiesTableConfig: UsersTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + CustomersTableConfigResolver + ] +}) +export class CustomerRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html new file mode 100644 index 0000000000..8637388038 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -0,0 +1,79 @@ + +
+ + + + + +
+ +
+
+
+
+
+ + customer.title + + + {{ 'customer.title-required' | translate }} + + +
+ + customer.description + + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts new file mode 100644 index 0000000000..3a7d4b55cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts @@ -0,0 +1,79 @@ +/// +/// 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 } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Customer } from '@shared/models/customer.model'; +import { ContactBasedComponent } from '@shared/components/entity/contact-based.component'; +import {Tenant} from '@app/shared/models/tenant.model'; +import {ActionNotificationShow} from '@app/core/notification/notification.actions'; +import {TranslateService} from '@ngx-translate/core'; + +@Component({ + selector: 'tb-customer', + templateUrl: './customer.component.html' +}) +export class CustomerComponent extends ContactBasedComponent { + + isPublic = false; + + constructor(protected store: Store, + protected translate: TranslateService, + protected fb: FormBuilder) { + super(store, fb); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildEntityForm(entity: Customer): FormGroup { + return this.fb.group( + { + title: [entity ? entity.title : '', [Validators.required]], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + } + ) + } + ); + } + + updateEntityForm(entity: Customer) { + this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; + this.entityForm.patchValue({title: entity.title}); + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + } + + onCustomerIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('customer.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts new file mode 100644 index 0000000000..c0b5ff743f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.module.ts @@ -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. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import {CustomerComponent} from '@modules/home/pages/customer/customer.component'; +import {CustomerRoutingModule} from './customer-routing.module'; + +@NgModule({ + entryComponents: [ + CustomerComponent + ], + declarations: [ + CustomerComponent + ], + imports: [ + CommonModule, + SharedModule, + CustomerRoutingModule + ] +}) +export class CustomerModule { } diff --git a/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts new file mode 100644 index 0000000000..3eb876d0a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { Resolve, Router } from '@angular/router'; + +import { Tenant } from '@shared/models/tenant.model'; +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@shared/components/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { + EntityType, + entityTypeResources, + entityTypeTranslations +} from '@shared/models/entity-type.models'; +import { EntityAction } from '@shared/components/entity/entity-component.models'; +import {Customer} from '@app/shared/models/customer.model'; +import {CustomerService} from '@app/core/http/customer.service'; +import {CustomerComponent} from '@modules/home/pages/customer/customer.component'; + +@Injectable() +export class CustomersTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private customerService: CustomerService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router) { + + this.config.entityType = EntityType.CUSTOMER; + this.config.entityComponent = CustomerComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.CUSTOMER); + this.config.entityResources = entityTypeResources.get(EntityType.CUSTOMER); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'customer.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'customer.title'), + new EntityTableColumn('email', 'contact.email'), + new EntityTableColumn('country', 'contact.country'), + new EntityTableColumn('city', 'contact.city') + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('customer.manage-customer-users'), + icon: 'account_circle', + isEnabled: (customer) => !customer.additionalInfo || !customer.additionalInfo.isPublic, + onAction: ($event, entity) => this.manageCustomerUsers($event, entity) + } + ); + + this.config.deleteEntityTitle = customer => this.translate.instant('customer.delete-customer-title', { customerTitle: customer.title }); + this.config.deleteEntityContent = () => this.translate.instant('customer.delete-customer-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('customer.delete-customers-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('customer.delete-customers-text'); + + this.config.entitiesFetchFunction = pageLink => this.customerService.getCustomers(pageLink); + this.config.loadEntity = id => this.customerService.getCustomer(id.id); + this.config.saveEntity = customer => this.customerService.saveCustomer(customer); + this.config.deleteEntity = id => this.customerService.deleteCustomer(id.id); + this.config.onEntityAction = action => this.onCustomerAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('customer.customers'); + + return this.config; + } + + manageCustomerUsers($event: Event, customer: Customer) { + if ($event) { + $event.stopPropagation(); + } + this.router.navigateByUrl(`customers/${customer.id.id}/users`); + } + + onCustomerAction(action: EntityAction): boolean { + switch (action.action) { + case 'manageUsers': + this.manageCustomerUsers(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index 50bcbb5b56..2644e2b26a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -21,6 +21,7 @@ import {DeviceComponent} from '@modules/home/pages/device/device.component'; import {DeviceRoutingModule} from './device-routing.module'; import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component'; +import {HomeDialogsModule} from '../../dialogs/home-dialogs.module'; @NgModule({ entryComponents: [ @@ -36,6 +37,7 @@ import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/devic imports: [ CommonModule, SharedModule, + HomeDialogsModule, DeviceRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 9cc9ecb078..a57777f318 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -14,35 +14,26 @@ /// limitations under the License. /// -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router'; - -import { Tenant } from '@shared/models/tenant.model'; import { CellActionDescriptor, checkBoxCell, DateEntityTableColumn, EntityTableColumn, - EntityTableConfig, + EntityTableConfig, GroupActionDescriptor, HeaderActionDescriptor } from '@shared/components/entity/entities-table-config.models'; -import { TenantService } from '@core/http/tenant.service'; -import { TranslateService } from '@ngx-translate/core'; -import { DatePipe } from '@angular/common'; -import { - EntityType, - entityTypeResources, - entityTypeTranslations -} from '@shared/models/entity-type.models'; -import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; -import { EntityAction } from '@shared/components/entity/entity-component.models'; -import { User } from '@shared/models/user.model'; +import {TranslateService} from '@ngx-translate/core'; +import {DatePipe} from '@angular/common'; +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models'; +import {EntityAction} from '@shared/components/entity/entity-component.models'; import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models'; import {DeviceComponent} from '@modules/home/pages/device/device.component'; -import {Observable, of} from 'rxjs'; +import {forkJoin, Observable, of} from 'rxjs'; import {select, Store} from '@ngrx/store'; -import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors'; +import {selectAuthUser} from '@core/auth/auth.selectors'; import {map, mergeMap, take, tap} from 'rxjs/operators'; import {AppState} from '@core/core.state'; import {DeviceService} from '@app/core/http/device.service'; @@ -58,6 +49,11 @@ import { DeviceCredentialsDialogData } from '@modules/home/pages/device/device-credentials-dialog.component'; import {DialogService} from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import {DeviceId} from '@app/shared/models/id/device-id'; @Injectable() export class DevicesTableConfigResolver implements Resolve> { @@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve this.config.componentsData.deviceScope === 'tenant'; return this.config; }) ); @@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve> { - const actions: Array> = []; + const actions: Array> = []; if (deviceScope === 'tenant') { actions.push( { @@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve (!entity.customerId || entity.customerId.id === NULL_UUID), onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('device.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('device.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.manage-credentials'), + icon: 'security', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('device.manage-credentials'), + icon: 'security', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + if (deviceScope === 'customer_user') { + actions.push( + { + name: this.translate.instant('device.view-credentials'), + icon: 'security', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.manageCredentials($event, entity) + } + ); + } + return actions; + } + + configureGroupActions(deviceScope: string): Array> { + const actions: Array> = []; + if (deviceScope === 'tenant') { + actions.push( + { + name: this.translate.instant('device.assign-devices'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.unassign-devices'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignDevicesFromCustomer($event, entities) } ); } @@ -191,20 +272,32 @@ export class DevicesTableConfigResolver implements Resolve { const actions: Array = []; - actions.push( - { - name: this.translate.instant('device.add-device-text'), - icon: 'insert_drive_file', - isEnabled: () => true, - onAction: ($event) => this.config.table.addEntity($event) - }, - { - name: this.translate.instant('device.import'), - icon: 'file_upload', - isEnabled: () => true, - onAction: ($event) => this.importDevices($event) - } - ); + if (deviceScope === 'tenant') { + actions.push( + { + name: this.translate.instant('device.add-device-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('device.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importDevices($event) + } + ); + } + if (deviceScope === 'customer') { + actions.push( + { + name: this.translate.instant('device.assign-new-device'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addDevicesToCustomer($event) + } + ); + } return actions; } @@ -215,6 +308,13 @@ export class DevicesTableConfigResolver implements Resolve) { if ($event) { $event.stopPropagation(); } - // TODO: + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: deviceIds, + entityType: EntityType.DEVICE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); } unassignFromCustomer($event: Event, device: DeviceInfo) { @@ -259,8 +372,8 @@ export class DevicesTableConfigResolver implements Resolve) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device.unassign-devices-title', {count: devices.length}), + this.translate.instant('device.unassign-devices-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + devices.forEach( + (device) => { + tasks.push(this.deviceService.unassignDeviceFromCustomer(device.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + manageCredentials($event: Event, device: Device) { if ($event) { $event.stopPropagation(); @@ -297,7 +438,7 @@ export class DevicesTableConfigResolver implements Resolve name: this.authority === Authority.TENANT_ADMIN ? this.translate.instant('user.login-as-tenant-admin') : this.translate.instant('user.login-as-customer-user'), - icon: 'mdi:login', - isMdiIcon: true, + mdiIcon: 'mdi:login', isEnabled: () => true, onAction: ($event, entity) => this.loginAsUser($event, entity) } diff --git a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts index c140636c0c..f61dd04fb6 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts @@ -51,8 +51,8 @@ export interface CellActionDescriptor> { name: string; nameFunction?: (entity: T) => string; icon?: string; - isMdiIcon?: boolean; - color?: string; + mdiIcon?: string; + style?: any; isEnabled: (entity: T) => boolean; onAction: ($event: MouseEvent, entity: T) => void; } diff --git a/ui-ngx/src/app/shared/components/entity/entities-table.component.html b/ui-ngx/src/app/shared/components/entity/entities-table.component.html index 9ddc5ed91e..89238df962 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/shared/components/entity/entities-table.component.html @@ -52,20 +52,30 @@ - - - + + + +
@@ -172,10 +182,8 @@ [disabled]="isLoading$ | async" [fxShow]="actionDescriptor.isEnabled(entity)" (click)="actionDescriptor.onAction($event, entity)"> - + {{actionDescriptor.icon}} - {{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }} diff --git a/ui-ngx/src/app/shared/components/entity/entities-table.component.ts b/ui-ngx/src/app/shared/components/entity/entities-table.component.ts index a415d54ee8..da9dc3331e 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entities-table.component.ts @@ -21,7 +21,8 @@ import { Input, OnInit, Type, - ViewChild + ViewChild, + ChangeDetectionStrategy } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; @@ -51,13 +52,14 @@ import { EntityAction } from '@shared/components/entity/entity-component.models'; import { Timewindow } from '@shared/models/time/time.models'; -import { DomSanitizer } from '@angular/platform-browser'; +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; @Component({ selector: 'tb-entities-table', templateUrl: './entities-table.component.html', - styleUrls: ['./entities-table.component.scss'] + styleUrls: ['./entities-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { @@ -73,6 +75,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn columns: Array>>; displayedColumns: string[] = []; + cellContentCache: Array = []; + + cellStyleCache: Array = []; + selectionEnabled; pageLink: PageLink; @@ -139,7 +145,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.columns = [...this.entitiesTableConfig.columns]; - this.selectionEnabled = this.entitiesTableConfig.selectionEnabled; + const enabledGroupActionDescriptors = + this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled); + + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled && enabledGroupActionDescriptors.length; if (this.selectionEnabled) { this.displayedColumns.push('select'); @@ -163,7 +172,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.pageLink = new PageLink(10, 0, null, sortOrder); } this.dataSource = new EntitiesDataSource>( - this.entitiesTableConfig.entitiesFetchFunction + this.entitiesTableConfig.entitiesFetchFunction, + () => { + this.dataLoaded(); + } ); if (this.entitiesTableConfig.onLoadAction) { this.entitiesTableConfig.onLoadAction(this.route); @@ -221,6 +233,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.dataSource.loadEntities(this.pageLink); } + private dataLoaded() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + } + onRowClick($event: Event, entity) { if ($event) { $event.stopPropagation(); @@ -347,12 +364,28 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } } - cellContent(entity: BaseData, column: EntityTableColumn>) { - return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); + cellContent(entity: BaseData, column: EntityTableColumn>, row: number, col: number) { + const index = row * this.columns.length + col; + let res = this.cellContentCache[index]; + if (!res) { + res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); + this.cellContentCache[index] = res; + } + return res; + } + + cellStyle(entity: BaseData, column: EntityTableColumn>, row: number, col: number) { + const index = row * this.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + res = {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; + this.cellStyleCache[index] = res; + } + return res; } - cellStyle(entity: BaseData, column: EntityTableColumn>) { - return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; + trackByColumnKey(index, column: EntityTableColumn>) { + return column.key; } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 982e929071..be51e544aa 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -105,7 +105,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } ngOnInit() { - this.filteredEntities = this.selectEntityFormGroup.get('dashboard').valueChanges + this.filteredEntities = this.selectEntityFormGroup.get('entity').valueChanges .pipe( tap(value => { let modelValue; diff --git a/ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts index 582b8b389f..e61daaac2e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectionStrategy, Component, ComponentFactoryResolver, EventEmitter, @@ -44,7 +45,8 @@ import { Subscription } from 'rxjs'; @Component({ selector: 'tb-entity-details-panel', templateUrl: './entity-details-panel.component.html', - styleUrls: ['./entity-details-panel.component.scss'] + styleUrls: ['./entity-details-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/shared/models/datasource/entity-datasource.ts b/ui-ngx/src/app/shared/models/datasource/entity-datasource.ts index f8bb0e2426..ecfdc755f6 100644 --- a/ui-ngx/src/app/shared/models/datasource/entity-datasource.ts +++ b/ui-ngx/src/app/shared/models/datasource/entity-datasource.ts @@ -36,7 +36,8 @@ export class EntitiesDataSource, P extends PageLink = public currentEntity: T = null; - constructor(private fetchFunction: EntitiesFetchFunction) {} + constructor(private fetchFunction: EntitiesFetchFunction, + private dataLoadedFunction: () => void) {} connect(collectionViewer: CollectionViewer): Observable> { return this.entitiesSubject.asObservable(); @@ -59,6 +60,7 @@ export class EntitiesDataSource, P extends PageLink = this.entitiesSubject.next(pageData.data); this.pageDataSubject.next(pageData); result.next(pageData); + this.dataLoadedFunction(); } ); return result; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 34fe3394dc..63ce8dbe30 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -395,6 +395,7 @@ "manage-assets": "Manage assets", "manage-devices": "Manage devices", "manage-dashboards": "Manage dashboards", + "created-time": "Created time", "title": "Title", "title-required": "Title is required.", "description": "Description",