Browse Source

Devices and Customers pages implementation

pull/1954/head
Igor Kulikov 7 years ago
parent
commit
984e260be3
  1. 5
      ui-ngx/src/app/core/http/device.service.ts
  2. 57
      ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html
  3. 117
      ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts
  4. 38
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts
  5. 72
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  6. 79
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  7. 79
      ui-ngx/src/app/modules/home/pages/customer/customer.component.ts
  8. 36
      ui-ngx/src/app/modules/home/pages/customer/customer.module.ts
  9. 105
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  10. 2
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  11. 217
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  12. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  13. 2
      ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts
  14. 3
      ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts
  15. 4
      ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts
  16. 48
      ui-ngx/src/app/shared/components/entity/entities-table.component.html
  17. 51
      ui-ngx/src/app/shared/components/entity/entities-table.component.ts
  18. 2
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  19. 4
      ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts
  20. 4
      ui-ngx/src/app/shared/models/datasource/entity-datasource.ts
  21. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -102,6 +102,11 @@ export class DeviceService {
return this.http.post<Device>(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public assignDeviceToCustomer(customerId: string, deviceId: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
return this.http.post<Device>(`/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));
}

57
ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #assignToCustomerForm="ngForm" [formGroup]="assignToCustomerFormGroup" (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-autocomplete
formControlName="customerId"
required
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || assignToCustomerForm.invalid
|| !assignToCustomerForm.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>

117
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<EntityId>;
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<AppState>,
@Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData,
private deviceService: DeviceService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AssignToCustomerDialogComponent, boolean>,
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<any>[] = [];
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<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;
}
}
}

38
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 { }

72
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 { }

79
ui-ngx/src/app/modules/home/pages/customer/customer.component.html

@ -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.
-->
<div class="tb-details-buttons">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageUsers')"
[fxShow]="!isEdit && !isPublic">
{{'customer.manage-users' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageAssets')"
[fxShow]="!isEdit">
{{'customer.manage-assets' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageDevices')"
[fxShow]="!isEdit">
{{'customer.manage-devices' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageDashboards')"
[fxShow]="!isEdit">
{{'customer.manage-dashboards' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit && !isPublic">
{{'customer.delete' | translate }}
</button>
<div fxLayout="row">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onCustomerIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>customer.copyId</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<form #entityNgForm="ngForm" [formGroup]="entityForm">
<fieldset [fxShow]="!isPublic" [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>customer.title</mat-label>
<input matInput formControlName="title" required/>
<mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'customer.title-required' | translate }}
</mat-error>
</mat-form-field>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>customer.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
</fieldset>
</form>
</div>

79
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<Customer> {
isPublic = false;
constructor(protected store: Store<AppState>,
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'
}));
}
}

36
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 { }

105
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<EntityTableConfig<Customer>> {
private readonly config: EntityTableConfig<Customer> = new EntityTableConfig<Customer>();
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<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'),
new EntityTableColumn<Customer>('title', 'customer.title'),
new EntityTableColumn<Customer>('email', 'contact.email'),
new EntityTableColumn<Customer>('country', 'contact.country'),
new EntityTableColumn<Customer>('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<Customer> {
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<Customer>): boolean {
switch (action.action) {
case 'manageUsers':
this.manageCustomerUsers(action.event, action.entity);
return true;
}
return false;
}
}

2
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
]
})

217
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<EntityTableConfig<DeviceInfo>> {
@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
private router: Router,
private dialog: MatDialog) {
this.config.entityType = EntityType.CUSTOMER;
this.config.entityType = EntityType.DEVICE;
this.config.entityComponent = DeviceComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE);
this.config.entityResources = entityTypeResources.get(EntityType.DEVICE);
@ -131,7 +127,11 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.columns = this.configureColumns(this.config.componentsData.deviceScope);
this.configureEntityFunctions(this.config.componentsData.deviceScope);
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user';
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config;
})
);
@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
}
configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
const actions: Array<CellActionDescriptor<Device>> = [];
const actions: Array<CellActionDescriptor<DeviceInfo>> = [];
if (deviceScope === 'tenant') {
actions.push(
{
@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
icon: 'share',
isEnabled: (entity) => (!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<GroupActionDescriptor<DeviceInfo>> {
const actions: Array<GroupActionDescriptor<DeviceInfo>> = [];
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<EntityTableConfig<Dev
configureAddActions(deviceScope: string): Array<HeaderActionDescriptor> {
const actions: Array<HeaderActionDescriptor> = [];
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<EntityTableConfig<Dev
// TODO:
}
addDevicesToCustomer($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
makePublic($event: Event, device: Device) {
if ($event) {
$event.stopPropagation();
@ -237,11 +337,24 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
assignToCustomer($event: Event, device: Device) {
assignToCustomer($event: Event, deviceIds: Array<DeviceId>) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialog.open<AssignToCustomerDialogComponent, AssignToCustomerDialogData,
boolean>(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<EntityTableConfig<Dev
content = this.translate.instant('device.unassign-device-text');
}
this.dialogService.confirm(
this.translate.instant(title),
this.translate.instant(content),
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
@ -276,6 +389,34 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
unassignDevicesFromCustomer($event: Event, devices: Array<DeviceInfo>) {
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<any>[] = [];
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<EntityTableConfig<Dev
this.makePublic(action.event, action.entity);
return true;
case 'assignToCustomer':
this.assignToCustomer(action.event, action.entity);
this.assignToCustomer(action.event, [action.entity.id]);
return true;
case 'unassignFromCustomer':
this.unassignFromCustomer(action.event, action.entity);

4
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -20,7 +20,7 @@ import { AdminModule } from './admin/admin.module';
import { HomeLinksModule } from './home-links/home-links.module';
import { ProfileModule } from './profile/profile.module';
import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
// import { CustomerModule } from '@modules/home/pages/customer/customer.module';
import { CustomerModule } from '@modules/home/pages/customer/customer.module';
// import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
import { UserModule } from '@modules/home/pages/user/user.module';
import {DeviceModule} from '@modules/home/pages/device/device.module';
@ -32,7 +32,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module';
ProfileModule,
TenantModule,
DeviceModule,
// CustomerModule,
CustomerModule,
// AuditLogModule,
UserModule
]

2
ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts

@ -46,7 +46,7 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten
private datePipe: DatePipe,
private router: Router) {
this.config.entityType = EntityType.CUSTOMER;
this.config.entityType = EntityType.TENANT;
this.config.entityComponent = TenantComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT);
this.config.entityResources = entityTypeResources.get(EntityType.TENANT);

3
ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts

@ -145,8 +145,7 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
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)
}

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

@ -51,8 +51,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> {
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;
}

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

@ -52,20 +52,30 @@
</button>
<ng-template #addActions>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above"
[matMenuTriggerFor]="addActionsMenu">
<mat-icon>add</mat-icon>
*ngIf="this.entitiesTableConfig.addActionDescriptors.length === 1; else addActionsMenu"
[fxShow]="this.entitiesTableConfig.addActionDescriptors[0].isEnabled()"
(click)="this.entitiesTableConfig.addActionDescriptors[0].onAction($event)"
matTooltip="{{ this.entitiesTableConfig.addActionDescriptors[0].name }}"
matTooltipPosition="above">
<mat-icon>{{this.entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon>
</button>
<mat-menu #addActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors"
[disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled()"
(click)="actionDescriptor.onAction($event)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.name }}</span>
<ng-template #addActionsMenu>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above"
[matMenuTriggerFor]="addActionsMenu">
<mat-icon>add</mat-icon>
</button>
</mat-menu>
<mat-menu #addActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors"
[disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled()"
(click)="actionDescriptor.onAction($event)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.name }}</span>
</button>
</mat-menu>
</ng-template>
</ng-template>
</div>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
@ -140,9 +150,11 @@
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns; trackBy: trackByColumnKey; let col = index">
<mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row, col)"
[ngStyle]="cellStyle(entity, column, row, col)"></mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
@ -155,10 +167,8 @@
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)">
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
<mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style">
{{actionDescriptor.icon}}</mat-icon>
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
[svgIcon]="actionDescriptor.icon"></mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
@ -172,10 +182,8 @@
[disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled(entity)"
(click)="actionDescriptor.onAction($event, entity)">
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
<mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style">
{{actionDescriptor.icon}}</mat-icon>
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
[svgIcon]="actionDescriptor.icon"></mat-icon>
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
</button>
</mat-menu>

51
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<EntityTableColumn<BaseData<HasId>>>;
displayedColumns: string[] = [];
cellContentCache: Array<SafeHtml> = [];
cellStyleCache: Array<any> = [];
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<BaseData<HasId>>(
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<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>, 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<HasId>, column: EntityTableColumn<BaseData<HasId>>, 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<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>>) {
return column.key;
}
}

2
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;

4
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 {

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

@ -36,7 +36,8 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
public currentEntity: T = null;
constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {}
constructor(private fetchFunction: EntitiesFetchFunction<T, P>,
private dataLoadedFunction: () => void) {}
connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
return this.entitiesSubject.asObservable();
@ -59,6 +60,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoadedFunction();
}
);
return result;

1
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",

Loading…
Cancel
Save