Commit e33c9d08cbb0d93eeaefc75a7f80f8190bc3e6c8

Authored by Igor Kulikov
1 parent bd8a104b

Add Assets page

@@ -71,7 +71,8 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen @@ -71,7 +71,8 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
71 this.assignToCustomerText = 'device.assign-device-to-customer-text'; 71 this.assignToCustomerText = 'device.assign-device-to-customer-text';
72 break; 72 break;
73 case EntityType.ASSET: 73 case EntityType.ASSET:
74 - // TODO: 74 + this.assignToCustomerTitle = 'asset.assign-asset-to-customer';
  75 + this.assignToCustomerText = 'asset.assign-asset-to-customer-text';
75 break; 76 break;
76 case EntityType.ENTITY_VIEW: 77 case EntityType.ENTITY_VIEW:
77 // TODO: 78 // TODO:
@@ -70,7 +70,8 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On @@ -70,7 +70,8 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On
70 this.assignToCustomerText = 'device.assign-to-customer-text'; 70 this.assignToCustomerText = 'device.assign-to-customer-text';
71 break; 71 break;
72 case EntityType.ASSET: 72 case EntityType.ASSET:
73 - // TODO: 73 + this.assignToCustomerTitle = 'asset.assign-asset-to-customer';
  74 + this.assignToCustomerText = 'asset.assign-to-customer-text';
74 break; 75 break;
75 case EntityType.ENTITY_VIEW: 76 case EntityType.ENTITY_VIEW:
76 // TODO: 77 // TODO:
  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 {AssetsTableConfigResolver} from './assets-table-config.resolver';
  23 +
  24 +const routes: Routes = [
  25 + {
  26 + path: 'assets',
  27 + component: EntitiesTableComponent,
  28 + data: {
  29 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  30 + title: 'asset.assets',
  31 + assetsType: 'tenant',
  32 + breadcrumb: {
  33 + label: 'asset.assets',
  34 + icon: 'domain'
  35 + }
  36 + },
  37 + resolve: {
  38 + entitiesTableConfig: AssetsTableConfigResolver
  39 + }
  40 + }
  41 +];
  42 +
  43 +@NgModule({
  44 + imports: [RouterModule.forChild(routes)],
  45 + exports: [RouterModule],
  46 + providers: [
  47 + AssetsTableConfigResolver
  48 + ]
  49 +})
  50 +export class AssetRoutingModule { }
  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 +<tb-entity-subtype-select
  19 + [showLabel]="true"
  20 + [entityType]="entityType.ASSET"
  21 + [ngModel]="entitiesTableConfig.componentsData.assetType"
  22 + (ngModelChange)="assetTypeChanged($event)">
  23 +</tb-entity-subtype-select>
  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 +:host {
  17 + flex: 1;
  18 + display: flex;
  19 + justify-content: flex-start;
  20 +}
  21 +
  22 +:host ::ng-deep {
  23 + tb-entity-subtype-select {
  24 + mat-form-field {
  25 + font-size: 16px;
  26 +
  27 + .mat-form-field-wrapper {
  28 + padding-bottom: 0;
  29 + }
  30 +
  31 + .mat-form-field-underline {
  32 + bottom: 0;
  33 + }
  34 + }
  35 + }
  36 +}
  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 {EntityTableHeaderComponent} from '@shared/components/entity/entity-table-header.component';
  21 +import {EntityType} from '@shared/models/entity-type.models';
  22 +import {AssetInfo} from '@shared/models/asset.models';
  23 +
  24 +@Component({
  25 + selector: 'tb-asset-table-header',
  26 + templateUrl: './asset-table-header.component.html',
  27 + styleUrls: ['./asset-table-header.component.scss']
  28 +})
  29 +export class AssetTableHeaderComponent extends EntityTableHeaderComponent<AssetInfo> {
  30 +
  31 + entityType = EntityType;
  32 +
  33 + constructor(protected store: Store<AppState>) {
  34 + super(store);
  35 + }
  36 +
  37 + assetTypeChanged(assetType: string) {
  38 + this.entitiesTableConfig.componentsData.assetType = assetType;
  39 + this.entitiesTableConfig.table.updateData();
  40 + }
  41 +
  42 +}
  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, 'makePublic')"
  22 + [fxShow]="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer(entity) && !entity?.customerIsPublic">
  23 + {{'asset.make-public' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'assignToCustomer')"
  28 + [fxShow]="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer(entity)">
  29 + {{'asset.assign-to-customer' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'unassignFromCustomer')"
  34 + [fxShow]="!isEdit && (assetScope === 'customer' || assetScope === 'tenant') && isAssignedToCustomer(entity)">
  35 + {{ (entity?.customerIsPublic ? 'asset.make-private' : 'asset.unassign-from-customer') | translate }}
  36 + </button>
  37 + <button mat-raised-button color="primary"
  38 + [disabled]="(isLoading$ | async)"
  39 + (click)="onEntityAction($event, 'delete')"
  40 + [fxShow]="!hideDelete() && !isEdit">
  41 + {{'asset.delete' | translate }}
  42 + </button>
  43 + <div fxLayout="row">
  44 + <button mat-raised-button
  45 + ngxClipboard
  46 + (cbOnSuccess)="onAssetIdCopied($event)"
  47 + [cbContent]="entity?.id?.id"
  48 + [fxShow]="!isEdit">
  49 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  50 + <span translate>asset.copyId</span>
  51 + </button>
  52 + </div>
  53 +</div>
  54 +<div class="mat-padding" fxLayout="column">
  55 + <mat-form-field class="mat-block"
  56 + [fxShow]="!isEdit && isAssignedToCustomer(entity)
  57 + && !entity?.customerIsPublic && assetScope === 'tenant'">
  58 + <mat-label translate>asset.assignedToCustomer</mat-label>
  59 + <input matInput disabled [ngModel]="entity?.customerTitle">
  60 + </mat-form-field>
  61 + <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
  62 + [fxShow]="!isEdit && entity?.customerIsPublic && (assetScope === 'customer' || assetScope === 'tenant')">
  63 + {{ 'asset.asset-public' | translate }}
  64 + </div>
  65 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  66 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  67 + <mat-form-field class="mat-block">
  68 + <mat-label translate>asset.name</mat-label>
  69 + <input matInput formControlName="name" required>
  70 + <mat-error *ngIf="entityForm.get('name').hasError('required')">
  71 + {{ 'asset.name-required' | translate }}
  72 + </mat-error>
  73 + </mat-form-field>
  74 + <tb-entity-subtype-autocomplete
  75 + formControlName="type"
  76 + [required]="true"
  77 + [entityType]="entityType.ASSET"
  78 + >
  79 + </tb-entity-subtype-autocomplete>
  80 + <div formGroupName="additionalInfo">
  81 + <mat-form-field class="mat-block">
  82 + <mat-label translate>asset.description</mat-label>
  83 + <textarea matInput formControlName="description" rows="2"></textarea>
  84 + </mat-form-field>
  85 + </div>
  86 + </fieldset>
  87 + </form>
  88 +</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 +:host {
  18 +
  19 +}
  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 {EntityComponent} from '@shared/components/entity/entity.component';
  21 +import {FormBuilder, FormGroup, Validators} from '@angular/forms';
  22 +import {EntityType} from '@shared/models/entity-type.models';
  23 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  24 +import {ActionNotificationShow} from '@core/notification/notification.actions';
  25 +import {TranslateService} from '@ngx-translate/core';
  26 +import {AssetInfo} from '@app/shared/models/asset.models';
  27 +
  28 +@Component({
  29 + selector: 'tb-asset',
  30 + templateUrl: './asset.component.html',
  31 + styleUrls: ['./asset.component.scss']
  32 +})
  33 +export class AssetComponent extends EntityComponent<AssetInfo> {
  34 +
  35 + entityType = EntityType;
  36 +
  37 + assetScope: 'tenant' | 'customer' | 'customer_user';
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + protected translate: TranslateService,
  41 + public fb: FormBuilder) {
  42 + super(store);
  43 + }
  44 +
  45 + ngOnInit() {
  46 + this.assetScope = this.entitiesTableConfig.componentsData.assetScope;
  47 + super.ngOnInit();
  48 + }
  49 +
  50 + hideDelete() {
  51 + if (this.entitiesTableConfig) {
  52 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  53 + } else {
  54 + return false;
  55 + }
  56 + }
  57 +
  58 + isAssignedToCustomer(entity: AssetInfo): boolean {
  59 + return entity && entity.customerId && entity.customerId.id !== NULL_UUID;
  60 + }
  61 +
  62 + buildForm(entity: AssetInfo): FormGroup {
  63 + return this.fb.group(
  64 + {
  65 + name: [entity ? entity.name : '', [Validators.required]],
  66 + type: [entity ? entity.type : null, [Validators.required]],
  67 + additionalInfo: this.fb.group(
  68 + {
  69 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  70 + }
  71 + )
  72 + }
  73 + );
  74 + }
  75 +
  76 + updateForm(entity: AssetInfo) {
  77 + this.entityForm.patchValue({name: entity.name});
  78 + this.entityForm.patchValue({type: entity.type});
  79 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  80 + }
  81 +
  82 +
  83 + onAssetIdCopied($event) {
  84 + this.store.dispatch(new ActionNotificationShow(
  85 + {
  86 + message: this.translate.instant('asset.idCopiedMessage'),
  87 + type: 'success',
  88 + duration: 750,
  89 + verticalPosition: 'bottom',
  90 + horizontalPosition: 'right'
  91 + }));
  92 + }
  93 +}
  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 {HomeDialogsModule} from '../../dialogs/home-dialogs.module';
  21 +import {AssetComponent} from './asset.component';
  22 +import {AssetTableHeaderComponent} from './asset-table-header.component';
  23 +import {AssetRoutingModule} from './asset-routing.module';
  24 +
  25 +@NgModule({
  26 + entryComponents: [
  27 + AssetComponent,
  28 + AssetTableHeaderComponent
  29 + ],
  30 + declarations: [
  31 + AssetComponent,
  32 + AssetTableHeaderComponent
  33 + ],
  34 + imports: [
  35 + CommonModule,
  36 + SharedModule,
  37 + HomeDialogsModule,
  38 + AssetRoutingModule
  39 + ]
  40 +})
  41 +export class AssetModule { }
  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 {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router';
  20 +import {
  21 + CellActionDescriptor,
  22 + checkBoxCell,
  23 + DateEntityTableColumn,
  24 + EntityTableColumn,
  25 + EntityTableConfig,
  26 + GroupActionDescriptor,
  27 + HeaderActionDescriptor
  28 +} from '@shared/components/entity/entities-table-config.models';
  29 +import {TranslateService} from '@ngx-translate/core';
  30 +import {DatePipe} from '@angular/common';
  31 +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models';
  32 +import {EntityAction} from '@shared/components/entity/entity-component.models';
  33 +import {forkJoin, Observable, of} from 'rxjs';
  34 +import {select, Store} from '@ngrx/store';
  35 +import {selectAuthUser} from '@core/auth/auth.selectors';
  36 +import {map, mergeMap, take, tap} from 'rxjs/operators';
  37 +import {AppState} from '@core/core.state';
  38 +import {Authority} from '@app/shared/models/authority.enum';
  39 +import {CustomerService} from '@core/http/customer.service';
  40 +import {Customer} from '@app/shared/models/customer.model';
  41 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  42 +import {BroadcastService} from '@core/services/broadcast.service';
  43 +import {MatDialog} from '@angular/material';
  44 +import {DialogService} from '@core/services/dialog.service';
  45 +import {
  46 + AssignToCustomerDialogComponent,
  47 + AssignToCustomerDialogData
  48 +} from '@modules/home/dialogs/assign-to-customer-dialog.component';
  49 +import {
  50 + AddEntitiesToCustomerDialogComponent,
  51 + AddEntitiesToCustomerDialogData
  52 +} from '../../dialogs/add-entities-to-customer-dialog.component';
  53 +import {Asset, AssetInfo} from '@app/shared/models/asset.models';
  54 +import {AssetService} from '@app/core/http/asset.service';
  55 +import {AssetComponent} from '@modules/home/pages/asset/asset.component';
  56 +import {AssetTableHeaderComponent} from '@modules/home/pages/asset/asset-table-header.component';
  57 +import {AssetId} from '@app/shared/models/id/asset-id';
  58 +
  59 +@Injectable()
  60 +export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<AssetInfo>> {
  61 +
  62 + private readonly config: EntityTableConfig<AssetInfo> = new EntityTableConfig<AssetInfo>();
  63 +
  64 + private customerId: string;
  65 +
  66 + constructor(private store: Store<AppState>,
  67 + private broadcast: BroadcastService,
  68 + private assetService: AssetService,
  69 + private customerService: CustomerService,
  70 + private dialogService: DialogService,
  71 + private translate: TranslateService,
  72 + private datePipe: DatePipe,
  73 + private router: Router,
  74 + private dialog: MatDialog) {
  75 +
  76 + this.config.entityType = EntityType.ASSET;
  77 + this.config.entityComponent = AssetComponent;
  78 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.ASSET);
  79 + this.config.entityResources = entityTypeResources.get(EntityType.ASSET);
  80 +
  81 + this.config.deleteEntityTitle = asset => this.translate.instant('asset.delete-asset-title', { assetName: asset.name });
  82 + this.config.deleteEntityContent = () => this.translate.instant('asset.delete-asset-text');
  83 + this.config.deleteEntitiesTitle = count => this.translate.instant('asset.delete-assets-title', {count});
  84 + this.config.deleteEntitiesContent = () => this.translate.instant('asset.delete-assets-text');
  85 +
  86 + this.config.loadEntity = id => this.assetService.getAssetInfo(id.id);
  87 + this.config.saveEntity = asset => {
  88 + return this.assetService.saveAsset(asset).pipe(
  89 + tap(() => {
  90 + this.broadcast.broadcast('assetSaved');
  91 + }),
  92 + mergeMap((savedAsset) => this.assetService.getAssetInfo(savedAsset.id.id)
  93 + ));
  94 + };
  95 + this.config.onEntityAction = action => this.onAssetAction(action);
  96 +
  97 + this.config.headerComponent = AssetTableHeaderComponent;
  98 +
  99 + }
  100 +
  101 + resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<AssetInfo>> {
  102 + const routeParams = route.params;
  103 + this.config.componentsData = {
  104 + assetScope: route.data.assetsType,
  105 + assetType: ''
  106 + };
  107 + this.customerId = routeParams.customerId;
  108 + return this.store.pipe(select(selectAuthUser), take(1)).pipe(
  109 + tap((authUser) => {
  110 + if (authUser.authority === Authority.CUSTOMER_USER) {
  111 + this.config.componentsData.assetScope = 'customer_user';
  112 + this.customerId = authUser.customerId;
  113 + }
  114 + }),
  115 + mergeMap(() =>
  116 + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer)
  117 + ),
  118 + map((parentCustomer) => {
  119 + if (parentCustomer) {
  120 + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) {
  121 + this.config.tableTitle = this.translate.instant('customer.public-assets');
  122 + } else {
  123 + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('asset.assets');
  124 + }
  125 + } else {
  126 + this.config.tableTitle = this.translate.instant('asset.assets');
  127 + }
  128 + this.config.columns = this.configureColumns(this.config.componentsData.assetScope);
  129 + this.configureEntityFunctions(this.config.componentsData.assetScope);
  130 + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope);
  131 + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope);
  132 + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope);
  133 + this.config.addEnabled = this.config.componentsData.assetScope !== 'customer_user';
  134 + this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant';
  135 + this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant';
  136 + return this.config;
  137 + })
  138 + );
  139 + }
  140 +
  141 + configureColumns(assetScope: string): Array<EntityTableColumn<AssetInfo>> {
  142 + const columns: Array<EntityTableColumn<AssetInfo>> = [
  143 + new DateEntityTableColumn<AssetInfo>('createdTime', 'asset.created-time', this.datePipe, '150px'),
  144 + new EntityTableColumn<AssetInfo>('name', 'asset.name'),
  145 + new EntityTableColumn<AssetInfo>('type', 'asset.asset-type'),
  146 + ];
  147 + if (assetScope === 'tenant') {
  148 + columns.push(
  149 + new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer'),
  150 + new EntityTableColumn<AssetInfo>('customerIsPublic', 'asset.public', '60px',
  151 + entity => {
  152 + return checkBoxCell(entity.customerIsPublic);
  153 + }, () => ({}), false),
  154 + );
  155 + }
  156 + return columns;
  157 + }
  158 +
  159 + configureEntityFunctions(assetScope: string): void {
  160 + if (assetScope === 'tenant') {
  161 + this.config.entitiesFetchFunction = pageLink =>
  162 + this.assetService.getTenantAssetInfos(pageLink, this.config.componentsData.assetType);
  163 + this.config.deleteEntity = id => this.assetService.deleteAsset(id.id);
  164 + } else {
  165 + this.config.entitiesFetchFunction = pageLink =>
  166 + this.assetService.getCustomerAssetInfos(this.customerId, pageLink, this.config.componentsData.assetType);
  167 + this.config.deleteEntity = id => this.assetService.unassignAssetFromCustomer(id.id);
  168 + }
  169 + }
  170 +
  171 + configureCellActions(assetScope: string): Array<CellActionDescriptor<AssetInfo>> {
  172 + const actions: Array<CellActionDescriptor<AssetInfo>> = [];
  173 + if (assetScope === 'tenant') {
  174 + actions.push(
  175 + {
  176 + name: this.translate.instant('asset.make-public'),
  177 + icon: 'share',
  178 + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
  179 + onAction: ($event, entity) => this.makePublic($event, entity)
  180 + },
  181 + {
  182 + name: this.translate.instant('asset.assign-to-customer'),
  183 + icon: 'assignment_ind',
  184 + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
  185 + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id])
  186 + },
  187 + {
  188 + name: this.translate.instant('asset.unassign-from-customer'),
  189 + icon: 'assignment_return',
  190 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic),
  191 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  192 + },
  193 + {
  194 + name: this.translate.instant('asset.make-private'),
  195 + icon: 'reply',
  196 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic),
  197 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  198 + }
  199 + );
  200 + }
  201 + if (assetScope === 'customer') {
  202 + actions.push(
  203 + {
  204 + name: this.translate.instant('asset.unassign-from-customer'),
  205 + icon: 'assignment_return',
  206 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic),
  207 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  208 + },
  209 + {
  210 + name: this.translate.instant('asset.make-private'),
  211 + icon: 'reply',
  212 + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic),
  213 + onAction: ($event, entity) => this.unassignFromCustomer($event, entity)
  214 + }
  215 + );
  216 + }
  217 + return actions;
  218 + }
  219 +
  220 + configureGroupActions(assetScope: string): Array<GroupActionDescriptor<AssetInfo>> {
  221 + const actions: Array<GroupActionDescriptor<AssetInfo>> = [];
  222 + if (assetScope === 'tenant') {
  223 + actions.push(
  224 + {
  225 + name: this.translate.instant('asset.assign-assets'),
  226 + icon: 'assignment_ind',
  227 + isEnabled: true,
  228 + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id))
  229 + }
  230 + );
  231 + }
  232 + if (assetScope === 'customer') {
  233 + actions.push(
  234 + {
  235 + name: this.translate.instant('asset.unassign-assets'),
  236 + icon: 'assignment_return',
  237 + isEnabled: true,
  238 + onAction: ($event, entities) => this.unassignAssetsFromCustomer($event, entities)
  239 + }
  240 + );
  241 + }
  242 + return actions;
  243 + }
  244 +
  245 + configureAddActions(assetScope: string): Array<HeaderActionDescriptor> {
  246 + const actions: Array<HeaderActionDescriptor> = [];
  247 + if (assetScope === 'tenant') {
  248 + actions.push(
  249 + {
  250 + name: this.translate.instant('asset.add-asset-text'),
  251 + icon: 'insert_drive_file',
  252 + isEnabled: () => true,
  253 + onAction: ($event) => this.config.table.addEntity($event)
  254 + },
  255 + {
  256 + name: this.translate.instant('asset.import'),
  257 + icon: 'file_upload',
  258 + isEnabled: () => true,
  259 + onAction: ($event) => this.importAssets($event)
  260 + }
  261 + );
  262 + }
  263 + if (assetScope === 'customer') {
  264 + actions.push(
  265 + {
  266 + name: this.translate.instant('asset.assign-new-asset'),
  267 + icon: 'add',
  268 + isEnabled: () => true,
  269 + onAction: ($event) => this.addAssetsToCustomer($event)
  270 + }
  271 + );
  272 + }
  273 + return actions;
  274 + }
  275 +
  276 + importAssets($event: Event) {
  277 + if ($event) {
  278 + $event.stopPropagation();
  279 + }
  280 + // TODO:
  281 + }
  282 +
  283 + addAssetsToCustomer($event: Event) {
  284 + if ($event) {
  285 + $event.stopPropagation();
  286 + }
  287 + this.dialog.open<AddEntitiesToCustomerDialogComponent, AddEntitiesToCustomerDialogData,
  288 + boolean>(AddEntitiesToCustomerDialogComponent, {
  289 + disableClose: true,
  290 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  291 + data: {
  292 + customerId: this.customerId,
  293 + entityType: EntityType.ASSET
  294 + }
  295 + }).afterClosed()
  296 + .subscribe((res) => {
  297 + if (res) {
  298 + this.config.table.updateData();
  299 + }
  300 + });
  301 + }
  302 +
  303 + makePublic($event: Event, asset: Asset) {
  304 + if ($event) {
  305 + $event.stopPropagation();
  306 + }
  307 + this.dialogService.confirm(
  308 + this.translate.instant('asset.make-public-asset-title', {assetName: asset.name}),
  309 + this.translate.instant('asset.make-public-asset-text'),
  310 + this.translate.instant('action.no'),
  311 + this.translate.instant('action.yes'),
  312 + true
  313 + ).subscribe((res) => {
  314 + if (res) {
  315 + this.assetService.makeAssetPublic(asset.id.id).subscribe(
  316 + () => {
  317 + this.config.table.updateData();
  318 + }
  319 + );
  320 + }
  321 + }
  322 + );
  323 + }
  324 +
  325 + assignToCustomer($event: Event, assetIds: Array<AssetId>) {
  326 + if ($event) {
  327 + $event.stopPropagation();
  328 + }
  329 + this.dialog.open<AssignToCustomerDialogComponent, AssignToCustomerDialogData,
  330 + boolean>(AssignToCustomerDialogComponent, {
  331 + disableClose: true,
  332 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  333 + data: {
  334 + entityIds: assetIds,
  335 + entityType: EntityType.ASSET
  336 + }
  337 + }).afterClosed()
  338 + .subscribe((res) => {
  339 + if (res) {
  340 + this.config.table.updateData();
  341 + }
  342 + });
  343 + }
  344 +
  345 + unassignFromCustomer($event: Event, asset: AssetInfo) {
  346 + if ($event) {
  347 + $event.stopPropagation();
  348 + }
  349 + const isPublic = asset.customerIsPublic;
  350 + let title;
  351 + let content;
  352 + if (isPublic) {
  353 + title = this.translate.instant('asset.make-private-asset-title', {assetName: asset.name});
  354 + content = this.translate.instant('asset.make-private-asset-text');
  355 + } else {
  356 + title = this.translate.instant('asset.unassign-asset-title', {assetName: asset.name});
  357 + content = this.translate.instant('asset.unassign-asset-text');
  358 + }
  359 + this.dialogService.confirm(
  360 + title,
  361 + content,
  362 + this.translate.instant('action.no'),
  363 + this.translate.instant('action.yes'),
  364 + true
  365 + ).subscribe((res) => {
  366 + if (res) {
  367 + this.assetService.unassignAssetFromCustomer(asset.id.id).subscribe(
  368 + () => {
  369 + this.config.table.updateData();
  370 + }
  371 + );
  372 + }
  373 + }
  374 + );
  375 + }
  376 +
  377 + unassignAssetsFromCustomer($event: Event, assets: Array<AssetInfo>) {
  378 + if ($event) {
  379 + $event.stopPropagation();
  380 + }
  381 + this.dialogService.confirm(
  382 + this.translate.instant('asset.unassign-assets-title', {count: assets.length}),
  383 + this.translate.instant('asset.unassign-assets-text'),
  384 + this.translate.instant('action.no'),
  385 + this.translate.instant('action.yes'),
  386 + true
  387 + ).subscribe((res) => {
  388 + if (res) {
  389 + const tasks: Observable<any>[] = [];
  390 + assets.forEach(
  391 + (asset) => {
  392 + tasks.push(this.assetService.unassignAssetFromCustomer(asset.id.id));
  393 + }
  394 + );
  395 + forkJoin(tasks).subscribe(
  396 + () => {
  397 + this.config.table.updateData();
  398 + }
  399 + );
  400 + }
  401 + }
  402 + );
  403 + }
  404 +
  405 + onAssetAction(action: EntityAction<AssetInfo>): boolean {
  406 + switch (action.action) {
  407 + case 'makePublic':
  408 + this.makePublic(action.event, action.entity);
  409 + return true;
  410 + case 'assignToCustomer':
  411 + this.assignToCustomer(action.event, [action.entity.id]);
  412 + return true;
  413 + case 'unassignFromCustomer':
  414 + this.unassignFromCustomer(action.event, action.entity);
  415 + return true;
  416 + }
  417 + return false;
  418 + }
  419 +
  420 +}
@@ -22,6 +22,7 @@ import {Authority} from '@shared/models/authority.enum'; @@ -22,6 +22,7 @@ import {Authority} from '@shared/models/authority.enum';
22 import {UsersTableConfigResolver} from '../user/users-table-config.resolver'; 22 import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
23 import {CustomersTableConfigResolver} from './customers-table-config.resolver'; 23 import {CustomersTableConfigResolver} from './customers-table-config.resolver';
24 import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver'; 24 import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
  25 +import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver';
25 26
26 const routes: Routes = [ 27 const routes: Routes = [
27 { 28 {
@@ -74,6 +75,22 @@ const routes: Routes = [ @@ -74,6 +75,22 @@ const routes: Routes = [
74 resolve: { 75 resolve: {
75 entitiesTableConfig: DevicesTableConfigResolver 76 entitiesTableConfig: DevicesTableConfigResolver
76 } 77 }
  78 + },
  79 + {
  80 + path: ':customerId/assets',
  81 + component: EntitiesTableComponent,
  82 + data: {
  83 + auth: [Authority.TENANT_ADMIN],
  84 + title: 'customer.assets',
  85 + assetsType: 'customer',
  86 + breadcrumb: {
  87 + label: 'customer.assets',
  88 + icon: 'domain'
  89 + }
  90 + },
  91 + resolve: {
  92 + entitiesTableConfig: AssetsTableConfigResolver
  93 + }
77 } 94 }
78 ] 95 ]
79 } 96 }
@@ -141,8 +141,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev @@ -141,8 +141,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
141 ); 141 );
142 } 142 }
143 143
144 - configureColumns(deviceScope: string): Array<EntityTableColumn<Device | DeviceInfo>> {  
145 - const columns: Array<EntityTableColumn<Device | DeviceInfo>> = [ 144 + configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
  145 + const columns: Array<EntityTableColumn<DeviceInfo>> = [
146 new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'), 146 new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'),
147 new EntityTableColumn<DeviceInfo>('name', 'device.name'), 147 new EntityTableColumn<DeviceInfo>('name', 'device.name'),
148 new EntityTableColumn<DeviceInfo>('type', 'device.device-type'), 148 new EntityTableColumn<DeviceInfo>('type', 'device.device-type'),
@@ -24,6 +24,7 @@ import { CustomerModule } from '@modules/home/pages/customer/customer.module'; @@ -24,6 +24,7 @@ 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';
  27 +import {AssetModule} from '@modules/home/pages/asset/asset.module';
27 28
28 @NgModule({ 29 @NgModule({
29 exports: [ 30 exports: [
@@ -32,6 +33,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module'; @@ -32,6 +33,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module';
32 ProfileModule, 33 ProfileModule,
33 TenantModule, 34 TenantModule,
34 DeviceModule, 35 DeviceModule,
  36 + AssetModule,
35 CustomerModule, 37 CustomerModule,
36 // AuditLogModule, 38 // AuditLogModule,
37 UserModule 39 UserModule
@@ -60,7 +60,9 @@ export const HelpLinks = { @@ -60,7 +60,9 @@ export const HelpLinks = {
60 tenants: helpBaseUrl + '/docs/user-guide/ui/tenants', 60 tenants: helpBaseUrl + '/docs/user-guide/ui/tenants',
61 customers: helpBaseUrl + '/docs/user-guide/customers', 61 customers: helpBaseUrl + '/docs/user-guide/customers',
62 users: helpBaseUrl + '/docs/user-guide/ui/users', 62 users: helpBaseUrl + '/docs/user-guide/ui/users',
63 - devices: helpBaseUrl + '/docs/user-guide/ui/devices' 63 + devices: helpBaseUrl + '/docs/user-guide/ui/devices',
  64 + assets: helpBaseUrl + '/docs/user-guide/ui/assets',
  65 + entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views'
64 } 66 }
65 }; 67 };
66 68
@@ -108,6 +108,34 @@ export const entityTypeTranslations = new Map<EntityType, EntityTypeTranslation> @@ -108,6 +108,34 @@ export const entityTypeTranslations = new Map<EntityType, EntityTypeTranslation>
108 search: 'device.search', 108 search: 'device.search',
109 selectedEntities: 'device.selected-devices' 109 selectedEntities: 'device.selected-devices'
110 } 110 }
  111 + ],
  112 + [
  113 + EntityType.ASSET,
  114 + {
  115 + type: 'entity.type-asset',
  116 + typePlural: 'entity.type-assets',
  117 + list: 'entity.list-of-assets',
  118 + nameStartsWith: 'entity.asset-name-starts-with',
  119 + details: 'asset.asset-details',
  120 + add: 'asset.add',
  121 + noEntities: 'asset.no-assets-text',
  122 + search: 'asset.search',
  123 + selectedEntities: 'asset.selected-assets'
  124 + }
  125 + ],
  126 + [
  127 + EntityType.ENTITY_VIEW,
  128 + {
  129 + type: 'entity.type-entity-view',
  130 + typePlural: 'entity.type-entity-views',
  131 + list: 'entity.list-of-entity-views',
  132 + nameStartsWith: 'entity.entity-view-name-starts-with',
  133 + details: 'entity-view.entity-view-details',
  134 + add: 'entity-view.add',
  135 + noEntities: 'entity-view.no-entity-views-text',
  136 + search: 'entity-view.search',
  137 + selectedEntities: 'entity-view.selected-entity-views'
  138 + }
111 ] 139 ]
112 ] 140 ]
113 ); 141 );
@@ -137,6 +165,18 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource>( @@ -137,6 +165,18 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource>(
137 { 165 {
138 helpLinkId: 'devices' 166 helpLinkId: 'devices'
139 } 167 }
  168 + ],
  169 + [
  170 + EntityType.ASSET,
  171 + {
  172 + helpLinkId: 'assets'
  173 + }
  174 + ],
  175 + [
  176 + EntityType.ENTITY_VIEW,
  177 + {
  178 + helpLinkId: 'entityViews'
  179 + }
140 ] 180 ]
141 ] 181 ]
142 ); 182 );
@@ -228,6 +228,7 @@ @@ -228,6 +228,7 @@
228 "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", 228 "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
229 "asset-type-list-empty": "No asset types selected.", 229 "asset-type-list-empty": "No asset types selected.",
230 "asset-types": "Asset types", 230 "asset-types": "Asset types",
  231 + "created-time": "Created time",
231 "name": "Name", 232 "name": "Name",
232 "name-required": "Name is required.", 233 "name-required": "Name is required.",
233 "description": "Description", 234 "description": "Description",
@@ -264,7 +265,9 @@ @@ -264,7 +265,9 @@
264 "asset-required": "Asset is required", 265 "asset-required": "Asset is required",
265 "name-starts-with": "Asset name starts with", 266 "name-starts-with": "Asset name starts with",
266 "import": "Import assets", 267 "import": "Import assets",
267 - "asset-file": "Asset file" 268 + "asset-file": "Asset file",
  269 + "search": "Search assets",
  270 + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected"
268 }, 271 },
269 "attribute": { 272 "attribute": {
270 "attributes": "Attributes", 273 "attributes": "Attributes",
@@ -867,6 +870,7 @@ @@ -867,6 +870,7 @@
867 "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.", 870 "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.",
868 "entity-view-type-list-empty": "No entity view types selected.", 871 "entity-view-type-list-empty": "No entity view types selected.",
869 "entity-view-types": "Entity View types", 872 "entity-view-types": "Entity View types",
  873 + "created-time": "Created time",
870 "name": "Name", 874 "name": "Name",
871 "name-required": "Name is required.", 875 "name-required": "Name is required.",
872 "description": "Description", 876 "description": "Description",
@@ -900,7 +904,9 @@ @@ -900,7 +904,9 @@
900 "make-public-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' public?", 904 "make-public-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' public?",
901 "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", 905 "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.",
902 "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", 906 "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?",
903 - "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others." 907 + "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others.",
  908 + "search": "Search entity views",
  909 + "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } selected"
904 }, 910 },
905 "event": { 911 "event": {
906 "event-type": "Event type", 912 "event-type": "Event type",