Commit 984e260be38ca2c16f37bbfa9ff68aa496d89084
1 parent
3700bcaa
Devices and Customers pages implementation
Showing
21 changed files
with
853 additions
and
77 deletions
@@ -102,6 +102,11 @@ export class DeviceService { | @@ -102,6 +102,11 @@ export class DeviceService { | ||
102 | return this.http.post<Device>(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); | 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 | public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | 110 | public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
106 | return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | 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,6 +21,7 @@ import {DeviceComponent} from '@modules/home/pages/device/device.component'; | ||
21 | import {DeviceRoutingModule} from './device-routing.module'; | 21 | import {DeviceRoutingModule} from './device-routing.module'; |
22 | import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; | 22 | import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; |
23 | import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component'; | 23 | import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component'; |
24 | +import {HomeDialogsModule} from '../../dialogs/home-dialogs.module'; | ||
24 | 25 | ||
25 | @NgModule({ | 26 | @NgModule({ |
26 | entryComponents: [ | 27 | entryComponents: [ |
@@ -36,6 +37,7 @@ import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/devic | @@ -36,6 +37,7 @@ import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/devic | ||
36 | imports: [ | 37 | imports: [ |
37 | CommonModule, | 38 | CommonModule, |
38 | SharedModule, | 39 | SharedModule, |
40 | + HomeDialogsModule, | ||
39 | DeviceRoutingModule | 41 | DeviceRoutingModule |
40 | ] | 42 | ] |
41 | }) | 43 | }) |
@@ -14,35 +14,26 @@ | @@ -14,35 +14,26 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Injectable } from '@angular/core'; | 17 | +import {Injectable} from '@angular/core'; |
18 | 18 | ||
19 | import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router'; | 19 | import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router'; |
20 | - | ||
21 | -import { Tenant } from '@shared/models/tenant.model'; | ||
22 | import { | 20 | import { |
23 | CellActionDescriptor, | 21 | CellActionDescriptor, |
24 | checkBoxCell, | 22 | checkBoxCell, |
25 | DateEntityTableColumn, | 23 | DateEntityTableColumn, |
26 | EntityTableColumn, | 24 | EntityTableColumn, |
27 | - EntityTableConfig, | 25 | + EntityTableConfig, GroupActionDescriptor, |
28 | HeaderActionDescriptor | 26 | HeaderActionDescriptor |
29 | } from '@shared/components/entity/entities-table-config.models'; | 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 | import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models'; | 32 | import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models'; |
42 | import {DeviceComponent} from '@modules/home/pages/device/device.component'; | 33 | import {DeviceComponent} from '@modules/home/pages/device/device.component'; |
43 | -import {Observable, of} from 'rxjs'; | 34 | +import {forkJoin, Observable, of} from 'rxjs'; |
44 | import {select, Store} from '@ngrx/store'; | 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 | import {map, mergeMap, take, tap} from 'rxjs/operators'; | 37 | import {map, mergeMap, take, tap} from 'rxjs/operators'; |
47 | import {AppState} from '@core/core.state'; | 38 | import {AppState} from '@core/core.state'; |
48 | import {DeviceService} from '@app/core/http/device.service'; | 39 | import {DeviceService} from '@app/core/http/device.service'; |
@@ -58,6 +49,11 @@ import { | @@ -58,6 +49,11 @@ import { | ||
58 | DeviceCredentialsDialogData | 49 | DeviceCredentialsDialogData |
59 | } from '@modules/home/pages/device/device-credentials-dialog.component'; | 50 | } from '@modules/home/pages/device/device-credentials-dialog.component'; |
60 | import {DialogService} from '@core/services/dialog.service'; | 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 | @Injectable() | 58 | @Injectable() |
63 | export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { | 59 | export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { |
@@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
76 | private router: Router, | 72 | private router: Router, |
77 | private dialog: MatDialog) { | 73 | private dialog: MatDialog) { |
78 | 74 | ||
79 | - this.config.entityType = EntityType.CUSTOMER; | 75 | + this.config.entityType = EntityType.DEVICE; |
80 | this.config.entityComponent = DeviceComponent; | 76 | this.config.entityComponent = DeviceComponent; |
81 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); | 77 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); |
82 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); | 78 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); |
@@ -131,7 +127,11 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -131,7 +127,11 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
131 | this.config.columns = this.configureColumns(this.config.componentsData.deviceScope); | 127 | this.config.columns = this.configureColumns(this.config.componentsData.deviceScope); |
132 | this.configureEntityFunctions(this.config.componentsData.deviceScope); | 128 | this.configureEntityFunctions(this.config.componentsData.deviceScope); |
133 | this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); | 129 | this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); |
130 | + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); | ||
134 | this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); | 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 | return this.config; | 135 | return this.config; |
136 | }) | 136 | }) |
137 | ); | 137 | ); |
@@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
175 | } | 175 | } |
176 | 176 | ||
177 | configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> { | 177 | configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> { |
178 | - const actions: Array<CellActionDescriptor<Device>> = []; | 178 | + const actions: Array<CellActionDescriptor<DeviceInfo>> = []; |
179 | if (deviceScope === 'tenant') { | 179 | if (deviceScope === 'tenant') { |
180 | actions.push( | 180 | actions.push( |
181 | { | 181 | { |
@@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
183 | icon: 'share', | 183 | icon: 'share', |
184 | isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), | 184 | isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), |
185 | onAction: ($event, entity) => this.makePublic($event, entity) | 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,20 +272,32 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
191 | 272 | ||
192 | configureAddActions(deviceScope: string): Array<HeaderActionDescriptor> { | 273 | configureAddActions(deviceScope: string): Array<HeaderActionDescriptor> { |
193 | const actions: Array<HeaderActionDescriptor> = []; | 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 | return actions; | 301 | return actions; |
209 | } | 302 | } |
210 | 303 | ||
@@ -215,6 +308,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -215,6 +308,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
215 | // TODO: | 308 | // TODO: |
216 | } | 309 | } |
217 | 310 | ||
311 | + addDevicesToCustomer($event: Event) { | ||
312 | + if ($event) { | ||
313 | + $event.stopPropagation(); | ||
314 | + } | ||
315 | + // TODO: | ||
316 | + } | ||
317 | + | ||
218 | makePublic($event: Event, device: Device) { | 318 | makePublic($event: Event, device: Device) { |
219 | if ($event) { | 319 | if ($event) { |
220 | $event.stopPropagation(); | 320 | $event.stopPropagation(); |
@@ -237,11 +337,24 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -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 | if ($event) { | 341 | if ($event) { |
242 | $event.stopPropagation(); | 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 | unassignFromCustomer($event: Event, device: DeviceInfo) { | 360 | unassignFromCustomer($event: Event, device: DeviceInfo) { |
@@ -259,8 +372,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -259,8 +372,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
259 | content = this.translate.instant('device.unassign-device-text'); | 372 | content = this.translate.instant('device.unassign-device-text'); |
260 | } | 373 | } |
261 | this.dialogService.confirm( | 374 | this.dialogService.confirm( |
262 | - this.translate.instant(title), | ||
263 | - this.translate.instant(content), | 375 | + title, |
376 | + content, | ||
264 | this.translate.instant('action.no'), | 377 | this.translate.instant('action.no'), |
265 | this.translate.instant('action.yes'), | 378 | this.translate.instant('action.yes'), |
266 | true | 379 | true |
@@ -276,6 +389,34 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -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 | manageCredentials($event: Event, device: Device) { | 420 | manageCredentials($event: Event, device: Device) { |
280 | if ($event) { | 421 | if ($event) { |
281 | $event.stopPropagation(); | 422 | $event.stopPropagation(); |
@@ -297,7 +438,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -297,7 +438,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
297 | this.makePublic(action.event, action.entity); | 438 | this.makePublic(action.event, action.entity); |
298 | return true; | 439 | return true; |
299 | case 'assignToCustomer': | 440 | case 'assignToCustomer': |
300 | - this.assignToCustomer(action.event, action.entity); | 441 | + this.assignToCustomer(action.event, [action.entity.id]); |
301 | return true; | 442 | return true; |
302 | case 'unassignFromCustomer': | 443 | case 'unassignFromCustomer': |
303 | this.unassignFromCustomer(action.event, action.entity); | 444 | this.unassignFromCustomer(action.event, action.entity); |
@@ -20,7 +20,7 @@ import { AdminModule } from './admin/admin.module'; | @@ -20,7 +20,7 @@ import { AdminModule } from './admin/admin.module'; | ||
20 | import { HomeLinksModule } from './home-links/home-links.module'; | 20 | import { HomeLinksModule } from './home-links/home-links.module'; |
21 | import { ProfileModule } from './profile/profile.module'; | 21 | import { ProfileModule } from './profile/profile.module'; |
22 | import { TenantModule } from '@modules/home/pages/tenant/tenant.module'; | 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 | // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; | 24 | // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; |
25 | import { UserModule } from '@modules/home/pages/user/user.module'; | 25 | import { UserModule } from '@modules/home/pages/user/user.module'; |
26 | import {DeviceModule} from '@modules/home/pages/device/device.module'; | 26 | import {DeviceModule} from '@modules/home/pages/device/device.module'; |
@@ -32,7 +32,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module'; | @@ -32,7 +32,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module'; | ||
32 | ProfileModule, | 32 | ProfileModule, |
33 | TenantModule, | 33 | TenantModule, |
34 | DeviceModule, | 34 | DeviceModule, |
35 | -// CustomerModule, | 35 | + CustomerModule, |
36 | // AuditLogModule, | 36 | // AuditLogModule, |
37 | UserModule | 37 | UserModule |
38 | ] | 38 | ] |
@@ -46,7 +46,7 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten | @@ -46,7 +46,7 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten | ||
46 | private datePipe: DatePipe, | 46 | private datePipe: DatePipe, |
47 | private router: Router) { | 47 | private router: Router) { |
48 | 48 | ||
49 | - this.config.entityType = EntityType.CUSTOMER; | 49 | + this.config.entityType = EntityType.TENANT; |
50 | this.config.entityComponent = TenantComponent; | 50 | this.config.entityComponent = TenantComponent; |
51 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); | 51 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); |
52 | this.config.entityResources = entityTypeResources.get(EntityType.TENANT); | 52 | this.config.entityResources = entityTypeResources.get(EntityType.TENANT); |
@@ -145,8 +145,7 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User> | @@ -145,8 +145,7 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User> | ||
145 | name: this.authority === Authority.TENANT_ADMIN ? | 145 | name: this.authority === Authority.TENANT_ADMIN ? |
146 | this.translate.instant('user.login-as-tenant-admin') : | 146 | this.translate.instant('user.login-as-tenant-admin') : |
147 | this.translate.instant('user.login-as-customer-user'), | 147 | this.translate.instant('user.login-as-customer-user'), |
148 | - icon: 'mdi:login', | ||
149 | - isMdiIcon: true, | 148 | + mdiIcon: 'mdi:login', |
150 | isEnabled: () => true, | 149 | isEnabled: () => true, |
151 | onAction: ($event, entity) => this.loginAsUser($event, entity) | 150 | onAction: ($event, entity) => this.loginAsUser($event, entity) |
152 | } | 151 | } |
@@ -51,8 +51,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> { | @@ -51,8 +51,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> { | ||
51 | name: string; | 51 | name: string; |
52 | nameFunction?: (entity: T) => string; | 52 | nameFunction?: (entity: T) => string; |
53 | icon?: string; | 53 | icon?: string; |
54 | - isMdiIcon?: boolean; | ||
55 | - color?: string; | 54 | + mdiIcon?: string; |
55 | + style?: any; | ||
56 | isEnabled: (entity: T) => boolean; | 56 | isEnabled: (entity: T) => boolean; |
57 | onAction: ($event: MouseEvent, entity: T) => void; | 57 | onAction: ($event: MouseEvent, entity: T) => void; |
58 | } | 58 | } |
@@ -52,20 +52,30 @@ | @@ -52,20 +52,30 @@ | ||
52 | </button> | 52 | </button> |
53 | <ng-template #addActions> | 53 | <ng-template #addActions> |
54 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" | 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 | </button> | 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 | </button> | 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 | </ng-template> | 79 | </ng-template> |
70 | </div> | 80 | </div> |
71 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" | 81 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
@@ -140,9 +150,11 @@ | @@ -140,9 +150,11 @@ | ||
140 | </mat-checkbox> | 150 | </mat-checkbox> |
141 | </mat-cell> | 151 | </mat-cell> |
142 | </ng-container> | 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 | <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell> | 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 | </ng-container> | 158 | </ng-container> |
147 | <ng-container matColumnDef="actions" stickyEnd> | 159 | <ng-container matColumnDef="actions" stickyEnd> |
148 | <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> | 160 | <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> |
@@ -155,10 +167,8 @@ | @@ -155,10 +167,8 @@ | ||
155 | matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" | 167 | matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" |
156 | matTooltipPosition="above" | 168 | matTooltipPosition="above" |
157 | (click)="actionDescriptor.onAction($event, entity)"> | 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 | {{actionDescriptor.icon}}</mat-icon> | 171 | {{actionDescriptor.icon}}</mat-icon> |
160 | - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | ||
161 | - [svgIcon]="actionDescriptor.icon"></mat-icon> | ||
162 | </button> | 172 | </button> |
163 | </div> | 173 | </div> |
164 | <div fxHide fxShow.lt-lg> | 174 | <div fxHide fxShow.lt-lg> |
@@ -172,10 +182,8 @@ | @@ -172,10 +182,8 @@ | ||
172 | [disabled]="isLoading$ | async" | 182 | [disabled]="isLoading$ | async" |
173 | [fxShow]="actionDescriptor.isEnabled(entity)" | 183 | [fxShow]="actionDescriptor.isEnabled(entity)" |
174 | (click)="actionDescriptor.onAction($event, entity)"> | 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 | {{actionDescriptor.icon}}</mat-icon> | 186 | {{actionDescriptor.icon}}</mat-icon> |
177 | - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | ||
178 | - [svgIcon]="actionDescriptor.icon"></mat-icon> | ||
179 | <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> | 187 | <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> |
180 | </button> | 188 | </button> |
181 | </mat-menu> | 189 | </mat-menu> |
@@ -21,7 +21,8 @@ import { | @@ -21,7 +21,8 @@ import { | ||
21 | Input, | 21 | Input, |
22 | OnInit, | 22 | OnInit, |
23 | Type, | 23 | Type, |
24 | - ViewChild | 24 | + ViewChild, |
25 | + ChangeDetectionStrategy | ||
25 | } from '@angular/core'; | 26 | } from '@angular/core'; |
26 | import { PageComponent } from '@shared/components/page.component'; | 27 | import { PageComponent } from '@shared/components/page.component'; |
27 | import { Store } from '@ngrx/store'; | 28 | import { Store } from '@ngrx/store'; |
@@ -51,13 +52,14 @@ import { | @@ -51,13 +52,14 @@ import { | ||
51 | EntityAction | 52 | EntityAction |
52 | } from '@shared/components/entity/entity-component.models'; | 53 | } from '@shared/components/entity/entity-component.models'; |
53 | import { Timewindow } from '@shared/models/time/time.models'; | 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 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | 56 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
56 | 57 | ||
57 | @Component({ | 58 | @Component({ |
58 | selector: 'tb-entities-table', | 59 | selector: 'tb-entities-table', |
59 | templateUrl: './entities-table.component.html', | 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 | export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { | 64 | export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { |
63 | 65 | ||
@@ -73,6 +75,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -73,6 +75,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | ||
73 | columns: Array<EntityTableColumn<BaseData<HasId>>>; | 75 | columns: Array<EntityTableColumn<BaseData<HasId>>>; |
74 | displayedColumns: string[] = []; | 76 | displayedColumns: string[] = []; |
75 | 77 | ||
78 | + cellContentCache: Array<SafeHtml> = []; | ||
79 | + | ||
80 | + cellStyleCache: Array<any> = []; | ||
81 | + | ||
76 | selectionEnabled; | 82 | selectionEnabled; |
77 | 83 | ||
78 | pageLink: PageLink; | 84 | pageLink: PageLink; |
@@ -139,7 +145,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -139,7 +145,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | ||
139 | 145 | ||
140 | this.columns = [...this.entitiesTableConfig.columns]; | 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 | if (this.selectionEnabled) { | 153 | if (this.selectionEnabled) { |
145 | this.displayedColumns.push('select'); | 154 | this.displayedColumns.push('select'); |
@@ -163,7 +172,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -163,7 +172,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | ||
163 | this.pageLink = new PageLink(10, 0, null, sortOrder); | 172 | this.pageLink = new PageLink(10, 0, null, sortOrder); |
164 | } | 173 | } |
165 | this.dataSource = new EntitiesDataSource<BaseData<HasId>>( | 174 | this.dataSource = new EntitiesDataSource<BaseData<HasId>>( |
166 | - this.entitiesTableConfig.entitiesFetchFunction | 175 | + this.entitiesTableConfig.entitiesFetchFunction, |
176 | + () => { | ||
177 | + this.dataLoaded(); | ||
178 | + } | ||
167 | ); | 179 | ); |
168 | if (this.entitiesTableConfig.onLoadAction) { | 180 | if (this.entitiesTableConfig.onLoadAction) { |
169 | this.entitiesTableConfig.onLoadAction(this.route); | 181 | this.entitiesTableConfig.onLoadAction(this.route); |
@@ -221,6 +233,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -221,6 +233,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | ||
221 | this.dataSource.loadEntities(this.pageLink); | 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 | onRowClick($event: Event, entity) { | 241 | onRowClick($event: Event, entity) { |
225 | if ($event) { | 242 | if ($event) { |
226 | $event.stopPropagation(); | 243 | $event.stopPropagation(); |
@@ -347,12 +364,28 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn | @@ -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,7 +105,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit | ||
105 | } | 105 | } |
106 | 106 | ||
107 | ngOnInit() { | 107 | ngOnInit() { |
108 | - this.filteredEntities = this.selectEntityFormGroup.get('dashboard').valueChanges | 108 | + this.filteredEntities = this.selectEntityFormGroup.get('entity').valueChanges |
109 | .pipe( | 109 | .pipe( |
110 | tap(value => { | 110 | tap(value => { |
111 | let modelValue; | 111 | let modelValue; |
@@ -15,6 +15,7 @@ | @@ -15,6 +15,7 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { | 17 | import { |
18 | + ChangeDetectionStrategy, | ||
18 | Component, | 19 | Component, |
19 | ComponentFactoryResolver, | 20 | ComponentFactoryResolver, |
20 | EventEmitter, | 21 | EventEmitter, |
@@ -44,7 +45,8 @@ import { Subscription } from 'rxjs'; | @@ -44,7 +45,8 @@ import { Subscription } from 'rxjs'; | ||
44 | @Component({ | 45 | @Component({ |
45 | selector: 'tb-entity-details-panel', | 46 | selector: 'tb-entity-details-panel', |
46 | templateUrl: './entity-details-panel.component.html', | 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 | export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { | 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,7 +36,8 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = | ||
36 | 36 | ||
37 | public currentEntity: T = null; | 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 | connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { | 42 | connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { |
42 | return this.entitiesSubject.asObservable(); | 43 | return this.entitiesSubject.asObservable(); |
@@ -59,6 +60,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = | @@ -59,6 +60,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = | ||
59 | this.entitiesSubject.next(pageData.data); | 60 | this.entitiesSubject.next(pageData.data); |
60 | this.pageDataSubject.next(pageData); | 61 | this.pageDataSubject.next(pageData); |
61 | result.next(pageData); | 62 | result.next(pageData); |
63 | + this.dataLoadedFunction(); | ||
62 | } | 64 | } |
63 | ); | 65 | ); |
64 | return result; | 66 | return result; |
@@ -395,6 +395,7 @@ | @@ -395,6 +395,7 @@ | ||
395 | "manage-assets": "Manage assets", | 395 | "manage-assets": "Manage assets", |
396 | "manage-devices": "Manage devices", | 396 | "manage-devices": "Manage devices", |
397 | "manage-dashboards": "Manage dashboards", | 397 | "manage-dashboards": "Manage dashboards", |
398 | + "created-time": "Created time", | ||
398 | "title": "Title", | 399 | "title": "Title", |
399 | "title-required": "Title is required.", | 400 | "title-required": "Title is required.", |
400 | "description": "Description", | 401 | "description": "Description", |