Browse Source

Implement Assets Profile UI

pull/7342/head
Igor Kulikov 4 years ago
parent
commit
012c8e91de
  1. 16
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  2. 3
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java
  4. 67
      ui-ngx/src/app/core/http/asset-profile.service.ts
  5. 13
      ui-ngx/src/app/core/http/asset.service.ts
  6. 14
      ui-ngx/src/app/core/http/entity.service.ts
  7. 36
      ui-ngx/src/app/core/services/menu.service.ts
  8. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  9. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  10. 45
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  11. 74
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html
  12. 21
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss
  13. 369
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts
  14. 53
      ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html
  15. 100
      ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts
  16. 91
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html
  17. 111
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts
  18. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  19. 74
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts
  20. 27
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html
  21. 38
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts
  22. 35
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts
  23. 189
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts
  24. 12
      ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html
  25. 2
      ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.scss
  26. 5
      ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts
  27. 13
      ui-ngx/src/app/modules/home/pages/asset/asset.component.html
  28. 8
      ui-ngx/src/app/modules/home/pages/asset/asset.component.ts
  29. 11
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  30. 6
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts
  31. 2
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  32. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  33. 51
      ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts
  34. 30
      ui-ngx/src/app/modules/home/pages/profiles/profiles.module.ts
  35. 22
      ui-ngx/src/app/shared/models/asset.models.ts
  36. 1
      ui-ngx/src/app/shared/models/constants.ts
  37. 76
      ui-ngx/src/app/shared/models/edge.models.ts
  38. 24
      ui-ngx/src/app/shared/models/entity-type.models.ts
  39. 26
      ui-ngx/src/app/shared/models/id/asset-profile-id.ts
  40. 1
      ui-ngx/src/app/shared/models/id/public-api.ts
  41. 1
      ui-ngx/src/app/shared/models/vc.models.ts
  42. 51
      ui-ngx/src/assets/locale/locale.constant-en_US.json

16
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -41,18 +41,19 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
@ -65,6 +66,7 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION;
@ -257,6 +259,8 @@ public class AssetController extends BaseController {
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -268,6 +272,9 @@ public class AssetController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink));
}
@ -345,6 +352,8 @@ public class AssetController extends BaseController {
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -359,6 +368,9 @@ public class AssetController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}

3
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -67,9 +67,6 @@ public class AssetProfileController extends BaseController {
private final TbAssetProfileService tbAssetProfileService;
@Autowired
private TimeseriesService timeseriesService;
@ApiOperation(value = "Get Asset Profile (getAssetProfileById)",
notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " +
"The server checks that the asset profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH,

2
application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java

@ -44,7 +44,7 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem
TenantId tenantId = assetProfile.getTenantId();
try {
AssetProfile savedAssetProfile = checkNotNull(assetProfileService.saveAssetProfile(assetProfile));
autoCommit(user, assetProfile.getId());
autoCommit(user, savedAssetProfile.getId());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAssetProfile.getId(),
actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);

67
ui-ngx/src/app/core/http/asset-profile.service.ts

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { AssetProfile, AssetProfileInfo } from '@shared/models/asset.models';
@Injectable({
providedIn: 'root'
})
export class AssetProfileService {
constructor(
private http: HttpClient
) {
}
public getAssetProfiles(pageLink: PageLink, config?: RequestConfig): Observable<PageData<AssetProfile>> {
return this.http.get<PageData<AssetProfile>>(`/api/assetProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
public getAssetProfile(assetProfileId: string, config?: RequestConfig): Observable<AssetProfile> {
return this.http.get<AssetProfile>(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config));
}
public saveAssetProfile(assetProfile: AssetProfile, config?: RequestConfig): Observable<AssetProfile> {
return this.http.post<AssetProfile>('/api/assetProfile', assetProfile, defaultHttpOptionsFromConfig(config));
}
public deleteAssetProfile(assetProfileId: string, config?: RequestConfig) {
return this.http.delete(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config));
}
public setDefaultAssetProfile(assetProfileId: string, config?: RequestConfig): Observable<AssetProfile> {
return this.http.post<AssetProfile>(`/api/assetProfile/${assetProfileId}/default`, defaultHttpOptionsFromConfig(config));
}
public getDefaultAssetProfileInfo(config?: RequestConfig): Observable<AssetProfileInfo> {
return this.http.get<AssetProfileInfo>('/api/assetProfileInfo/default', defaultHttpOptionsFromConfig(config));
}
public getAssetProfileInfo(assetProfileId: string, config?: RequestConfig): Observable<AssetProfileInfo> {
return this.http.get<AssetProfileInfo>(`/api/assetProfileInfo/${assetProfileId}`, defaultHttpOptionsFromConfig(config));
}
public getAssetProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<AssetProfileInfo>> {
return this.http.get<PageData<AssetProfileInfo>>(`/api/assetProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
}

13
ui-ngx/src/app/core/http/asset.service.ts

@ -38,12 +38,25 @@ export class AssetService {
defaultHttpOptionsFromConfig(config));
}
public getTenantAssetInfosByAssetProfileId(pageLink: PageLink, assetProfileId: string = '',
config?: RequestConfig): Observable<PageData<AssetInfo>> {
return this.http.get<PageData<AssetInfo>>(`/api/tenant/assetInfos${pageLink.toQuery()}&assetProfileId=${assetProfileId}`,
defaultHttpOptionsFromConfig(config));
}
public getCustomerAssetInfos(customerId: string, pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<AssetInfo>> {
return this.http.get<PageData<AssetInfo>>(`/api/customer/${customerId}/assetInfos${pageLink.toQuery()}&type=${type}`,
defaultHttpOptionsFromConfig(config));
}
public getCustomerAssetInfosByAssetProfileId(customerId: string, pageLink: PageLink, assetProfileId: string = '',
config?: RequestConfig): Observable<PageData<AssetInfo>> {
return this.http.get<PageData<AssetInfo>>
(`/api/customer/${customerId}/assetInfos${pageLink.toQuery()}&assetProfileId=${assetProfileId}`,
defaultHttpOptionsFromConfig(config));
}
public getAsset(assetId: string, config?: RequestConfig): Observable<Asset> {
return this.http.get<Asset>(`/api/asset/${assetId}`, defaultHttpOptionsFromConfig(config));
}

14
ui-ngx/src/app/core/http/entity.service.ts

@ -87,6 +87,7 @@ import { RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.mode
import { WidgetService } from '@core/http/widget.service';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { QueueService } from '@core/http/queue.service';
import { AssetProfileService } from '@core/http/asset-profile.service';
@Injectable({
providedIn: 'root'
@ -110,6 +111,7 @@ export class EntityService {
private otaPackageService: OtaPackageService,
private widgetService: WidgetService,
private deviceProfileService: DeviceProfileService,
private assetProfileService: AssetProfileService,
private utils: UtilsService,
private queueService: QueueService
) { }
@ -237,6 +239,11 @@ export class EntityService {
(id) => this.deviceProfileService.getDeviceProfileInfo(id, config),
entityIds);
break;
case EntityType.ASSET_PROFILE:
observable = this.getEntitiesByIdsObservable(
(id) => this.assetProfileService.getAssetProfileInfo(id, config),
entityIds);
break;
}
return observable;
}
@ -382,6 +389,10 @@ export class EntityService {
pageLink.sortOrder.property = 'name';
entitiesObservable = this.deviceProfileService.getDeviceProfileInfos(pageLink, null, config);
break;
case EntityType.ASSET_PROFILE:
pageLink.sortOrder.property = 'name';
entitiesObservable = this.assetProfileService.getAssetProfileInfos(pageLink, config);
break;
}
return entitiesObservable;
}
@ -1409,6 +1420,9 @@ export class EntityService {
case EdgeEventType.DEVICE_PROFILE:
entityObservable = this.deviceProfileService.getDeviceProfile(entityId);
break;
case EdgeEventType.ASSET_PROFILE:
entityObservable = this.assetProfileService.getAssetProfile(entityId);
break;
case EdgeEventType.RELATION:
entityObservable = of(entity.body);
break;

36
ui-ngx/src/app/core/services/menu.service.ts

@ -293,11 +293,29 @@ export class MenuService {
},
{
id: guid(),
name: 'device-profile.device-profiles',
type: 'link',
path: '/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
name: 'profiles.profiles',
type: 'toggle',
path: '/profiles',
height: '80px',
icon: 'badge',
pages: [
{
id: guid(),
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
},
{
id: guid(),
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true
}
]
},
{
id: guid(),
@ -450,6 +468,12 @@ export class MenuService {
name: 'asset.assets',
icon: 'domain',
path: '/assets'
},
{
name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true,
path: '/profiles/assetProfiles'
}
]
},
@ -465,7 +489,7 @@ export class MenuService {
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/deviceProfiles'
path: '/profiles/deviceProfiles'
},
{
name: 'ota-update.ota-updates',

6
ui-ngx/src/app/modules/common/modules-map.ts

@ -290,6 +290,9 @@ import * as DashboardImageDialogComponent from '@home/components/dashboard-page/
import * as WidgetContainerComponent from '@home/components/widget/widget-container.component';
import * as TenantProfileQueuesComponent from '@home/components/profile/queue/tenant-profile-queues.component';
import * as QueueFormComponent from '@home/components/queue/queue-form.component';
import * as AssetProfileComponent from '@home/components/profile/asset-profile.component';
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
import { IModulesMap } from '@modules/common/modules-map.models';
@ -537,6 +540,9 @@ class ModulesMap implements IModulesMap {
MqttDeviceProfileTransportConfigurationComponent,
'@home/components/profile/device/coap-device-profile-transport-configuration.component':
CoapDeviceProfileTransportConfigurationComponent,
'@home/components/profile/asset-profile.component': AssetProfileComponent,
'@home/components/profile/asset-profile-dialog.component': AssetProfileDialogComponent,
'@home/components/profile/asset-profile-autocomplete.component': AssetProfileAutocompleteComponent,
'@home/components/profile/alarm/device-profile-alarms.component': DeviceProfileAlarmsComponent,
'@home/components/profile/alarm/device-profile-alarm.component': DeviceProfileAlarmComponent,
'@home/components/profile/alarm/create-alarm-rules.component': CreateAlarmRulesComponent,

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -170,6 +170,9 @@ import { RateLimitsListComponent } from '@home/components/profile/tenant/rate-li
import { RateLimitsComponent } from '@home/components/profile/tenant/rate-limits/rate-limits.component';
import { RateLimitsTextComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-text.component';
import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component';
import { AssetProfileComponent } from '@home/components/profile/asset-profile.component';
import { AssetProfileDialogComponent } from '@home/components/profile/asset-profile-dialog.component';
import { AssetProfileAutocompleteComponent } from '@home/components/profile/asset-profile-autocomplete.component';
@NgModule({
declarations:
@ -266,6 +269,9 @@ import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenan
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
AssetProfileComponent,
AssetProfileDialogComponent,
AssetProfileAutocompleteComponent,
RuleChainAutocompleteComponent,
AlarmScheduleInfoComponent,
DeviceProfileProvisionConfigurationComponent,
@ -402,6 +408,9 @@ import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenan
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
AssetProfileComponent,
AssetProfileDialogComponent,
AssetProfileAutocompleteComponent,
AlarmScheduleInfoComponent,
AlarmScheduleComponent,
AlarmDynamicValue,

45
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -72,6 +72,8 @@ import { DeviceService } from '@core/http/device.service';
import { AssetService } from '@core/http/asset.service';
import { EdgeService } from '@core/http/edge.service';
import { RuleNode } from '@shared/models/rule-node.models';
import { AssetProfileService } from '@core/http/asset-profile.service';
import { AssetProfile } from '@shared/models/asset.models';
// @dynamic
@Injectable()
@ -85,6 +87,7 @@ export class ImportExportService {
private dashboardUtils: DashboardUtilsService,
private widgetService: WidgetService,
private deviceProfileService: DeviceProfileService,
private assetProfileService: AssetProfileService,
private tenantProfileService: TenantProfileService,
private entityService: EntityService,
private ruleChainService: RuleChainService,
@ -532,6 +535,37 @@ export class ImportExportService {
);
}
public exportAssetProfile(assetProfileId: string) {
this.assetProfileService.getAssetProfile(assetProfileId).subscribe(
(assetProfile) => {
let name = assetProfile.name;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareProfileExport(assetProfile), name);
},
(e) => {
this.handleExportError(e, 'asset-profile.export-failed-error');
}
);
}
public importAssetProfile(): Observable<AssetProfile> {
return this.openImportDialog('asset-profile.import', 'asset-profile.asset-profile-file').pipe(
mergeMap((assetProfile: AssetProfile) => {
if (!this.validateImportedAssetProfile(assetProfile)) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('asset-profile.invalid-asset-profile-file-error'),
type: 'error'}));
throw new Error('Invalid asset profile file');
} else {
return this.assetProfileService.saveAssetProfile(assetProfile);
}
}),
catchError((err) => {
return of(null);
})
);
}
public exportTenantProfile(tenantProfileId: string) {
this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe(
(tenantProfile) => {
@ -628,6 +662,13 @@ export class ImportExportService {
return true;
}
private validateImportedAssetProfile(assetProfile: AssetProfile): boolean {
if (isUndefined(assetProfile.name)) {
return false;
}
return true;
}
private validateImportedTenantProfile(tenantProfile: TenantProfile): boolean {
return isDefined(tenantProfile.name)
&& isDefined(tenantProfile.profileData)
@ -892,7 +933,9 @@ export class ImportExportService {
}
filename += '.' + fileType.extension;
const blob = new Blob([data], {type: fileType.mimeType});
// @ts-ignore
if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
// @ts-ignore
this.window.navigator.msSaveOrOpenBlob(blob, filename);
} else {
const e = this.document.createEvent('MouseEvents');
@ -913,7 +956,7 @@ export class ImportExportService {
return dashboard;
}
private prepareProfileExport<T extends DeviceProfile|TenantProfile>(profile: T): T {
private prepareProfileExport<T extends DeviceProfile|AssetProfile|TenantProfile>(profile: T): T {
profile = this.prepareExport(profile);
profile.default = false;
return profile;

74
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html

@ -0,0 +1,74 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="selectAssetProfileFormGroup" class="mat-block">
<input matInput type="text" placeholder="{{ 'asset-profile.asset-profile' | translate }}"
#assetProfileInput
formControlName="assetProfile"
(focusin)="onFocus()"
[required]="required"
(keydown)="assetProfileEnter($event)"
(keypress)="assetProfileEnter($event)"
[matAutocomplete]="assetProfileAutocomplete"
[fxShow]="!showDetailsPageLink || !disabled || !selectAssetProfileFormGroup.get('assetProfile').value">
<a *ngIf="showDetailsPageLink && selectAssetProfileFormGroup.get('assetProfile').value && disabled" aria-label="Open asset profile" [routerLink]=assetProfileURL>
{{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }}
</a>
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled && editProfileEnabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Edit"
matTooltip="{{ 'asset-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editAssetProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
(closed)="onPanelClosed()"
#assetProfileAutocomplete="matAutocomplete"
[displayWith]="displayAssetProfileFn">
<mat-option *ngFor="let assetProfile of filteredAssetProfiles | async" [value]="assetProfile">
<span [innerHTML]="assetProfile.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredAssetProfiles | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty">
<span translate>asset-profile.no-asset-profiles-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('asset-profile.no-asset-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span>
<a translate (click)="createAssetProfile($event, searchText)">asset-profile.create-new-asset-profile</a>
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectAssetProfileFormGroup.get('assetProfile').hasError('required')">
{{ 'asset-profile.asset-profile-required' | translate }}
</mat-error>
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint>
</mat-form-field>

21
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host{
.mat-icon-button a {
border-bottom: none;
color: inherit;
}
}

369
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts

@ -0,0 +1,369 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
NgZone,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { entityIdEquals } from '@shared/models/id/entity-id';
import { TruncatePipe } from '@shared//pipe/truncate.pipe';
import { ENTER } from '@angular/cdk/keycodes';
import { MatDialog } from '@angular/material/dialog';
import { MatAutocomplete } from '@angular/material/autocomplete';
import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
import { AssetProfileId } from '@shared/models/id/asset-profile-id';
import { AssetProfile, AssetProfileInfo } from '@shared/models/asset.models';
import { AssetProfileService } from '@core/http/asset-profile.service';
import { AssetProfileDialogComponent, AssetProfileDialogData } from './asset-profile-dialog.component';
@Component({
selector: 'tb-asset-profile-autocomplete',
templateUrl: './asset-profile-autocomplete.component.html',
styleUrls: ['./asset-profile-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AssetProfileAutocompleteComponent),
multi: true
}]
})
export class AssetProfileAutocompleteComponent implements ControlValueAccessor, OnInit {
selectAssetProfileFormGroup: FormGroup;
modelValue: AssetProfileId | null;
@Input()
selectDefaultProfile = false;
@Input()
selectFirstProfile = false;
@Input()
displayAllOnEmpty = false;
@Input()
editProfileEnabled = true;
@Input()
addNewProfile = true;
@Input()
showDetailsPageLink = false;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@Input()
hint: string;
@Output()
assetProfileUpdated = new EventEmitter<AssetProfileId>();
@Output()
assetProfileChanged = new EventEmitter<AssetProfileInfo>();
@ViewChild('assetProfileInput', {static: true}) assetProfileInput: ElementRef;
@ViewChild('assetProfileAutocomplete', {static: true}) assetProfileAutocomplete: MatAutocomplete;
filteredAssetProfiles: Observable<Array<AssetProfileInfo>>;
searchText = '';
assetProfileURL: string;
private dirty = false;
private ignoreClosedPanel = false;
private allAssetProfile: AssetProfileInfo = {
name: this.translate.instant('asset-profile.all-asset-profiles'),
id: null
};
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
public truncate: TruncatePipe,
private assetProfileService: AssetProfileService,
private fb: FormBuilder,
private zone: NgZone,
private dialog: MatDialog) {
this.selectAssetProfileFormGroup = this.fb.group({
assetProfile: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredAssetProfiles = this.selectAssetProfileFormGroup.get('assetProfile').valueChanges
.pipe(
tap((value: AssetProfileInfo | string) => {
let modelValue: AssetProfileInfo | null;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value;
}
if (!this.displayAllOnEmpty || modelValue) {
this.updateView(modelValue);
}
}),
map(value => {
if (value) {
if (typeof value === 'string') {
return value;
} else {
if (this.displayAllOnEmpty && value === this.allAssetProfile) {
return '';
} else {
return value.name;
}
}
} else {
return '';
}
}),
debounceTime(150),
distinctUntilChanged(),
switchMap(name => this.fetchAssetProfiles(name)),
share()
);
}
selectDefaultAssetProfileIfNeeded(): void {
if (this.selectDefaultProfile && !this.modelValue) {
this.assetProfileService.getDefaultAssetProfileInfo().subscribe(
(profile) => {
if (profile) {
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: false});
this.updateView(profile);
} else {
this.selectFirstAssetProfileIfNeeded();
}
}
);
} else {
this.selectFirstAssetProfileIfNeeded();
}
}
selectFirstAssetProfileIfNeeded(): void {
if (this.selectFirstProfile && !this.modelValue) {
const pageLink = new PageLink(1, 0, null, {
property: 'createdTime',
direction: Direction.DESC
});
this.assetProfileService.getAssetProfileInfos(pageLink, {ignoreLoading: true}).subscribe(
(pageData => {
const data = pageData.data;
if (data.length) {
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(data[0], {emitEvent: false});
this.updateView(data[0]);
}
})
);
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectAssetProfileFormGroup.disable({emitEvent: false});
} else {
this.selectAssetProfileFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AssetProfileId | null): void {
this.searchText = '';
if (value != null) {
this.assetProfileService.getAssetProfileInfo(value.id).subscribe(
(profile) => {
this.modelValue = new AssetProfileId(profile.id.id);
this.assetProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: false});
this.assetProfileChanged.emit(profile);
}
);
} else if (this.displayAllOnEmpty) {
this.modelValue = null;
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(this.allAssetProfile, {emitEvent: false});
} else {
this.modelValue = null;
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(null, {emitEvent: false});
this.selectDefaultAssetProfileIfNeeded();
}
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.selectAssetProfileFormGroup.get('assetProfile').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
onPanelClosed() {
if (this.ignoreClosedPanel) {
this.ignoreClosedPanel = false;
} else {
if (this.displayAllOnEmpty && !this.selectAssetProfileFormGroup.get('assetProfile').value) {
this.zone.run(() => {
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(this.allAssetProfile, {emitEvent: true});
}, 0);
}
}
}
updateView(assetProfile: AssetProfileInfo | null) {
const idValue = assetProfile && assetProfile.id ? new AssetProfileId(assetProfile.id.id) : null;
if (!entityIdEquals(this.modelValue, idValue)) {
this.modelValue = idValue;
this.propagateChange(this.modelValue);
this.assetProfileChanged.emit(assetProfile);
}
}
displayAssetProfileFn(profile?: AssetProfileInfo): string | undefined {
return profile ? profile.name : undefined;
}
fetchAssetProfiles(searchText?: string): Observable<Array<AssetProfileInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(10, 0, searchText, {
property: 'name',
direction: Direction.ASC
});
return this.assetProfileService.getAssetProfileInfos(pageLink, {ignoreLoading: true}).pipe(
catchError(() => of(emptyPageData<AssetProfileInfo>())),
map(pageData => {
let data = pageData.data;
if (this.displayAllOnEmpty) {
data = [this.allAssetProfile, ...data];
}
return data;
})
);
}
clear() {
this.ignoreClosedPanel = true;
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.assetProfileInput.nativeElement.blur();
this.assetProfileInput.nativeElement.focus();
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
assetProfileEnter($event: KeyboardEvent) {
if (this.editProfileEnabled && $event.keyCode === ENTER) {
$event.preventDefault();
if (!this.modelValue) {
this.createAssetProfile($event, this.searchText);
}
}
}
createAssetProfile($event: Event, profileName: string) {
$event.preventDefault();
const assetProfile: AssetProfile = {
name: profileName
} as AssetProfile;
if (this.addNewProfile) {
this.openAssetProfileDialog(assetProfile, true);
}
}
editAssetProfile($event: Event) {
$event.preventDefault();
this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe(
(assetProfile) => {
this.openAssetProfileDialog(assetProfile, false);
}
);
}
openAssetProfileDialog(assetProfile: AssetProfile, isAdd: boolean) {
this.dialog.open<AssetProfileDialogComponent, AssetProfileDialogData,
AssetProfile>(AssetProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
assetProfile
}
}).afterClosed().subscribe(
(savedAssetProfile) => {
if (!savedAssetProfile) {
setTimeout(() => {
this.assetProfileInput.nativeElement.blur();
this.assetProfileInput.nativeElement.focus();
}, 0);
} else {
this.assetProfileService.getAssetProfileInfo(savedAssetProfile.id.id).subscribe(
(profile) => {
this.modelValue = new AssetProfileId(profile.id.id);
this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: true});
if (isAdd) {
this.propagateChange(this.modelValue);
} else {
this.assetProfileUpdated.next(savedAssetProfile.id);
}
this.assetProfileChanged.emit(profile);
}
);
}
}
);
}
}

53
ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form (ngSubmit)="save()" style="width: 600px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'asset-profile.add' : 'asset-profile.edit' ) | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-asset-profile
#assetProfileComponent
[standalone]="true"
[entity]="assetProfile"
[isEdit]="true">
</tb-asset-profile>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || assetProfileComponent.entityForm?.invalid || !assetProfileComponent.entityForm?.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
</div>
</form>

100
ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts

@ -0,0 +1,100 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
ComponentFactoryResolver,
Inject,
Injector,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { AssetProfile } from '@shared/models/asset.models';
import { AssetProfileComponent } from '@home/components/profile/asset-profile.component';
import { AssetProfileService } from '@core/http/asset-profile.service';
export interface AssetProfileDialogData {
assetProfile: AssetProfile;
isAdd: boolean;
}
@Component({
selector: 'tb-asset-profile-dialog',
templateUrl: './asset-profile-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AssetProfileDialogComponent}],
styleUrls: []
})
export class AssetProfileDialogComponent extends
DialogComponent<AssetProfileDialogComponent, AssetProfile> implements ErrorStateMatcher, AfterViewInit {
isAdd: boolean;
assetProfile: AssetProfile;
submitted = false;
@ViewChild('assetProfileComponent', {static: true}) assetProfileComponent: AssetProfileComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogData,
public dialogRef: MatDialogRef<AssetProfileDialogComponent, AssetProfile>,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private assetProfileService: AssetProfileService) {
super(store, router, dialogRef);
this.isAdd = this.data.isAdd;
this.assetProfile = this.data.assetProfile;
}
ngAfterViewInit(): void {
if (this.isAdd) {
setTimeout(() => {
this.assetProfileComponent.entityForm.markAsDirty();
}, 0);
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.assetProfileComponent.entityForm.valid) {
this.assetProfile = {...this.assetProfile, ...this.assetProfileComponent.entityFormValue()};
this.assetProfileService.saveAssetProfile(this.assetProfile).subscribe(
(assetProfile) => {
this.dialogRef.close(assetProfile);
}
);
}
}
}

91
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html

@ -0,0 +1,91 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[fxShow]="!isEdit">
{{'asset-profile.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[fxShow]="!isEdit && !entity?.default">
{{'asset-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'asset-profile.delete' | translate }}
</button>
<div fxLayout="row" fxLayout.xs="column">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onAssetProfileIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>asset-profile.copyId</span>
</button>
</div>
</div>
<div [ngClass]="{'mat-padding': !standalone}" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit" style="min-width: 0;">
<mat-form-field class="mat-block">
<mat-label translate>asset-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="entityForm.get('name').hasError('required')">
{{ 'asset-profile.name-required' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('name').hasError('maxlength')">
{{ 'asset-profile.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-rule-chain-autocomplete
labelText="asset-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
placeholder="{{'asset-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<div tb-hint>{{'asset-profile.mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete>
<tb-queue-autocomplete
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-image-input fxFlex
label="{{'asset-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>asset-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</div>

111
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts

@ -0,0 +1,111 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@app/core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { EntityComponent } from '../entity/entity.component';
import { EntityType } from '@shared/models/entity-type.models';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { ServiceType } from '@shared/models/queue.models';
import { EntityId } from '@shared/models/id/entity-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { AssetProfile } from '@shared/models/asset.models';
@Component({
selector: 'tb-asset-profile',
templateUrl: './asset-profile.component.html',
styleUrls: []
})
export class AssetProfileComponent extends EntityComponent<AssetProfile> {
@Input()
standalone = false;
entityType = EntityType;
serviceType = ServiceType.TB_RULE_ENGINE;
assetProfileId: EntityId;
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: AssetProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetProfile>,
protected fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: AssetProfile): FormGroup {
this.assetProfileId = entity?.id ? entity.id : null;
const form = this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]],
image: [entity ? entity.image : null],
defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []],
defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []],
defaultQueueName: [entity ? entity.defaultQueueName : null, []],
description: [entity ? entity.description : '', []],
}
);
return form;
}
updateForm(entity: AssetProfile) {
this.assetProfileId = entity.id;
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({image: entity.image}, {emitEvent: false});
this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false});
this.entityForm.patchValue({description: entity.description}, {emitEvent: false});
}
prepareFormValue(formValue: any): any {
if (formValue.defaultRuleChainId) {
formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId);
}
if (formValue.defaultDashboardId) {
formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId);
}
return super.prepareFormValue(formValue);
}
onAssetProfileIdCopied(event) {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('asset-profile.idCopiedMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}

2
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -25,7 +25,7 @@
(keypress)="deviceProfileEnter($event)"
[matAutocomplete]="deviceProfileAutocomplete"
[fxShow]="!showDetailsPageLink || !disabled || !selectDeviceProfileFormGroup.get('deviceProfile').value">
<a *ngIf="showDetailsPageLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled" aria-label="Open tenant profile" [routerLink]=deviceProfileURL>
<a *ngIf="showDetailsPageLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled" aria-label="Open device profile" [routerLink]=deviceProfileURL>
{{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }}
</a>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled"

74
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts

@ -0,0 +1,74 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { AssetProfilesTableConfigResolver } from './asset-profiles-table-config.resolver';
export const assetProfilesRoutes: Routes = [
{
path: 'assetProfiles',
data: {
breadcrumb: {
label: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'asset-profile.asset-profiles'
},
resolve: {
entitiesTableConfig: AssetProfilesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'mdi:alpha-a-box'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'asset-profile.asset-profiles'
},
resolve: {
entitiesTableConfig: AssetProfilesTableConfigResolver
}
}
]
}
];
@NgModule({
providers: [
AssetProfilesTableConfigResolver
]
})
export class AssetProfileRoutingModule { }

27
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

38
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { AssetProfile } from '@shared/models/asset.models';
@Component({
selector: 'tb-asset-profile-tabs',
templateUrl: './asset-profile-tabs.component.html',
styleUrls: []
})
export class AssetProfileTabsComponent extends EntityTabsComponent<AssetProfile> {
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
super.ngOnInit();
}
}

35
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts

@ -0,0 +1,35 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AssetProfileTabsComponent } from './asset-profile-tabs.component';
import { AssetProfileRoutingModule } from './asset-profile-routing.module';
@NgModule({
declarations: [
AssetProfileTabsComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
AssetProfileRoutingModule
]
})
export class AssetProfileModule { }

189
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts

@ -0,0 +1,189 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { Resolve, Router } from '@angular/router';
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig,
HeaderActionDescriptor
} from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { AssetProfile } from '@shared/models/asset.models';
import { AssetProfileService } from '@core/http/asset-profile.service';
import { AssetProfileComponent } from '@home/components/profile/asset-profile.component';
import { AssetProfileTabsComponent } from './asset-profile-tabs.component';
@Injectable()
export class AssetProfilesTableConfigResolver implements Resolve<EntityTableConfig<AssetProfile>> {
private readonly config: EntityTableConfig<AssetProfile> = new EntityTableConfig<AssetProfile>();
constructor(private assetProfileService: AssetProfileService,
private importExport: ImportExportService,
private homeDialogs: HomeDialogsService,
private translate: TranslateService,
private datePipe: DatePipe,
private dialogService: DialogService,
private router: Router,
private dialog: MatDialog) {
this.config.entityType = EntityType.ASSET_PROFILE;
this.config.entityComponent = AssetProfileComponent;
this.config.entityTabsComponent = AssetProfileTabsComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.ASSET_PROFILE);
this.config.entityResources = entityTypeResources.get(EntityType.ASSET_PROFILE);
this.config.hideDetailsTabsOnEdit = false;
this.config.columns.push(
new DateEntityTableColumn<AssetProfile>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<AssetProfile>('name', 'asset-profile.name', '50%'),
new EntityTableColumn<AssetProfile>('description', 'asset-profile.description', '50%'),
new EntityTableColumn<AssetProfile>('isDefault', 'asset-profile.default', '60px',
entity => {
return checkBoxCell(entity.default);
})
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('asset-profile.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportAssetProfile($event, entity)
},
{
name: this.translate.instant('asset-profile.set-default'),
icon: 'flag',
isEnabled: (assetProfile) => !assetProfile.default,
onAction: ($event, entity) => this.setDefaultAssetProfile($event, entity)
}
);
this.config.deleteEntityTitle = assetProfile => this.translate.instant('asset-profile.delete-asset-profile-title',
{ assetProfileName: assetProfile.name });
this.config.deleteEntityContent = () => this.translate.instant('asset-profile.delete-asset-profile-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('asset-profile.delete-asset-profiles-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('asset-profile.delete-asset-profiles-text');
this.config.entitiesFetchFunction = pageLink => this.assetProfileService.getAssetProfiles(pageLink);
this.config.loadEntity = id => this.assetProfileService.getAssetProfile(id.id);
this.config.saveEntity = assetProfile => this.assetProfileService.saveAssetProfile(assetProfile);
this.config.deleteEntity = id => this.assetProfileService.deleteAssetProfile(id.id);
this.config.onEntityAction = action => this.onAssetProfileAction(action);
this.config.deleteEnabled = (assetProfile) => assetProfile && !assetProfile.default;
this.config.entitySelectionEnabled = (assetProfile) => assetProfile && !assetProfile.default;
this.config.addActionDescriptors = this.configureAddActions();
}
resolve(): EntityTableConfig<AssetProfile> {
this.config.tableTitle = this.translate.instant('asset-profile.asset-profiles');
return this.config;
}
configureAddActions(): Array<HeaderActionDescriptor> {
const actions: Array<HeaderActionDescriptor> = [];
actions.push(
{
name: this.translate.instant('asset-profile.create-asset-profile'),
icon: 'insert_drive_file',
isEnabled: () => true,
onAction: ($event) => this.config.getTable().addEntity($event)
},
{
name: this.translate.instant('asset-profile.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: ($event) => this.importAssetProfile($event)
}
);
return actions;
}
setDefaultAssetProfile($event: Event, assetProfile: AssetProfile) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('asset-profile.set-default-asset-profile-title', {assetProfileName: assetProfile.name}),
this.translate.instant('asset-profile.set-default-asset-profile-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((res) => {
if (res) {
this.assetProfileService.setDefaultAssetProfile(assetProfile.id.id).subscribe(
() => {
this.config.updateData();
}
);
}
}
);
}
private openAssetProfile($event: Event, assetProfile: AssetProfile) {
if ($event) {
$event.stopPropagation();
}
const url = this.router.createUrlTree(['profiles', 'assetProfiles', assetProfile.id.id]);
this.router.navigateByUrl(url);
}
importAssetProfile($event: Event) {
this.importExport.importAssetProfile().subscribe(
(assetProfile) => {
if (assetProfile) {
this.config.updateData();
}
}
);
}
exportAssetProfile($event: Event, assetProfile: AssetProfile) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportAssetProfile(assetProfile.id.id);
}
onAssetProfileAction(action: EntityAction<AssetProfile>): boolean {
switch (action.action) {
case 'open':
this.openAssetProfile(action.event, action.entity);
return true;
case 'setDefault':
this.setDefaultAssetProfile(action.event, action.entity);
return true;
case 'export':
this.exportAssetProfile(action.event, action.entity);
return true;
}
return false;
}
}

12
ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<tb-entity-subtype-select
[showLabel]="true"
[entityType]="entityType.ASSET"
[ngModel]="entitiesTableConfig.componentsData.assetType"
(ngModelChange)="assetTypeChanged($event)">
</tb-entity-subtype-select>
<tb-asset-profile-autocomplete
[ngModel]="entitiesTableConfig.componentsData.assetProfileId"
(ngModelChange)="assetProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-asset-profile-autocomplete>

2
ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.scss

@ -23,7 +23,7 @@
}
:host ::ng-deep {
tb-entity-subtype-select {
tb-asset-profile-autocomplete {
width: 100%;
mat-form-field {

5
ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.ts

@ -20,6 +20,7 @@ import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { EntityType } from '@shared/models/entity-type.models';
import { AssetInfo } from '@shared/models/asset.models';
import { AssetProfileId } from '@shared/models/id/asset-profile-id';
@Component({
selector: 'tb-asset-table-header',
@ -34,8 +35,8 @@ export class AssetTableHeaderComponent extends EntityTableHeaderComponent<AssetI
super(store);
}
assetTypeChanged(assetType: string) {
this.entitiesTableConfig.componentsData.assetType = assetType;
assetProfileChanged(assetProfileId: AssetProfileId) {
this.entitiesTableConfig.componentsData.assetProfileId = assetProfileId;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}

13
ui-ngx/src/app/modules/home/pages/asset/asset.component.html

@ -86,12 +86,13 @@
{{ 'asset.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-subtype-autocomplete
formControlName="type"
[required]="true"
[entityType]="entityType.ASSET"
>
</tb-entity-subtype-autocomplete>
<tb-asset-profile-autocomplete
[selectDefaultProfile]="isAdd"
required
formControlName="assetProfileId"
[showDetailsPageLink]="true"
(assetProfileUpdated)="onAssetProfileUpdated()">
</tb-asset-profile-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>asset.label</mat-label>
<input matInput formControlName="label">

8
ui-ngx/src/app/modules/home/pages/asset/asset.component.ts

@ -67,7 +67,7 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
return this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]],
type: [entity ? entity.type : null, [Validators.required, Validators.maxLength(255)]],
assetProfileId: [entity ? entity.assetProfileId : null, [Validators.required]],
label: [entity ? entity.label : '', Validators.maxLength(255)],
additionalInfo: this.fb.group(
{
@ -80,7 +80,7 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
updateForm(entity: AssetInfo) {
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({type: entity.type});
this.entityForm.patchValue({assetProfileId: entity.assetProfileId});
this.entityForm.patchValue({label: entity.label});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
}
@ -96,4 +96,8 @@ export class AssetComponent extends EntityComponent<AssetInfo> {
horizontalPosition: 'right'
}));
}
onAssetProfileUpdated() {
this.entitiesTableConfig.updateData(false);
}
}

11
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -115,6 +115,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
const routeParams = route.params;
this.config.componentsData = {
assetScope: route.data.assetsType,
assetProfileId: null,
assetType: '',
edgeId: routeParams.edgeId
};
@ -164,8 +165,8 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
const columns: Array<EntityTableColumn<AssetInfo>> = [
new DateEntityTableColumn<AssetInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<AssetInfo>('name', 'asset.name', '25%'),
new EntityTableColumn<AssetInfo>('type', 'asset.asset-type', '25%'),
new EntityTableColumn<DeviceInfo>('label', 'asset.label', '25%'),
new EntityTableColumn<AssetInfo>('assetProfileName', 'asset-profile.asset-profile', '25%'),
new EntityTableColumn<AssetInfo>('label', 'asset.label', '25%'),
];
if (assetScope === 'tenant') {
columns.push(
@ -182,14 +183,16 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
configureEntityFunctions(assetScope: string): void {
if (assetScope === 'tenant') {
this.config.entitiesFetchFunction = pageLink =>
this.assetService.getTenantAssetInfos(pageLink, this.config.componentsData.assetType);
this.assetService.getTenantAssetInfosByAssetProfileId(pageLink, this.config.componentsData.assetProfileId !== null ?
this.config.componentsData.assetProfileId.id : '');
this.config.deleteEntity = id => this.assetService.deleteAsset(id.id);
} else if (assetScope === 'edge' || assetScope === 'edge_customer_user') {
this.config.entitiesFetchFunction = pageLink =>
this.assetService.getEdgeAssets(this.config.componentsData.edgeId, pageLink, this.config.componentsData.assetType);
} else {
this.config.entitiesFetchFunction = pageLink =>
this.assetService.getCustomerAssetInfos(this.customerId, pageLink, this.config.componentsData.assetType);
this.assetService.getCustomerAssetInfosByAssetProfileId(this.customerId, pageLink,
this.config.componentsData.assetProfileId !== null ? this.config.componentsData.assetProfileId.id : '');
this.config.deleteEntity = id => this.assetService.unassignAssetFromCustomer(id.id);
}
}

6
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts

@ -15,7 +15,7 @@
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
@ -25,7 +25,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [
export const deviceProfilesRoutes: Routes = [
{
path: 'deviceProfiles',
data: {
@ -67,8 +67,6 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
DeviceProfilesTableConfigResolver
]

2
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -185,7 +185,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
if ($event) {
$event.stopPropagation();
}
const url = this.router.createUrlTree(['deviceProfiles', deviceProfile.id.id]);
const url = this.router.createUrlTree(['profiles', 'deviceProfiles', deviceProfile.id.id]);
this.router.navigateByUrl(url);
}

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

@ -38,6 +38,8 @@ import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module';
import { EdgeModule } from '@home/pages/edge/edge.module';
import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
import { VcModule } from '@home/pages/vc/vc.module';
import { AssetProfileModule } from '@home/pages/asset-profile/asset-profile.module';
import { ProfilesModule } from '@home/pages/profiles/profiles.module';
@NgModule({
exports: [
@ -48,6 +50,8 @@ import { VcModule } from '@home/pages/vc/vc.module';
TenantProfileModule,
TenantModule,
DeviceProfileModule,
AssetProfileModule,
ProfilesModule,
DeviceModule,
AssetModule,
EdgeModule,

51
ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts

@ -0,0 +1,51 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core';
import { deviceProfilesRoutes } from '@home/pages/device-profile/device-profile-routing.module';
import { assetProfilesRoutes } from '@home/pages/asset-profile/asset-profile-routing.module';
const routes: Routes = [
{
path: 'profiles',
data: {
auth: [Authority.TENANT_ADMIN],
breadcrumb: {
label: 'profiles.profiles',
icon: 'badge'
}
},
children: [
{
path: '',
data: {
auth: [Authority.TENANT_ADMIN],
redirectTo: '/profiles/deviceProfiles'
}
},
...deviceProfilesRoutes,
...assetProfilesRoutes
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfilesRoutingModule { }

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

@ -0,0 +1,30 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { ProfilesRoutingModule } from './profiles-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
ProfilesRoutingModule
]
})
export class ProfilesModule { }

22
ui-ngx/src/app/shared/models/asset.models.ts

@ -19,6 +19,26 @@ import { AssetId } from './id/asset-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
import { AssetProfileId } from '@shared/models/id/asset-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { EntityInfoData } from '@shared/models/entity.models';
export interface AssetProfile extends BaseData<AssetProfileId>, ExportableEntity<AssetProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
default?: boolean;
image?: string;
defaultRuleChainId?: RuleChainId;
defaultDashboardId?: DashboardId;
defaultQueueName?: string;
}
export interface AssetProfileInfo extends EntityInfoData {
image?: string;
defaultDashboardId?: DashboardId;
}
export interface Asset extends BaseData<AssetId>, ExportableEntity<AssetId> {
tenantId?: TenantId;
@ -26,12 +46,14 @@ export interface Asset extends BaseData<AssetId>, ExportableEntity<AssetId> {
name: string;
type: string;
label: string;
assetProfileId?: AssetProfileId;
additionalInfo?: any;
}
export interface AssetInfo extends Asset {
customerTitle: string;
customerIsPublic: boolean;
assetProfileName: string;
}
export interface AssetSearchQuery extends EntitySearchQuery {

1
ui-ngx/src/app/shared/models/constants.ts

@ -133,6 +133,7 @@ export const HelpLinks = {
users: helpBaseUrl + '/docs/user-guide/ui/users',
devices: helpBaseUrl + '/docs/user-guide/ui/devices',
deviceProfiles: helpBaseUrl + '/docs/user-guide/ui/device-profiles',
assetProfiles: helpBaseUrl + '/docs/user-guide/ui/asset-profiles',
edges: helpBaseUrl + '/docs/user-guide/ui/edges',
assets: helpBaseUrl + '/docs/user-guide/ui/assets',
entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',

76
ui-ngx/src/app/shared/models/edge.models.ts

@ -45,49 +45,50 @@ export interface EdgeSearchQuery extends EntitySearchQuery {
}
export enum EdgeEventType {
DASHBOARD = "DASHBOARD",
ASSET = "ASSET",
DEVICE = "DEVICE",
DEVICE_PROFILE = "DEVICE_PROFILE",
ENTITY_VIEW = "ENTITY_VIEW",
ALARM = "ALARM",
RULE_CHAIN = "RULE_CHAIN",
RULE_CHAIN_METADATA = "RULE_CHAIN_METADATA",
EDGE = "EDGE",
USER = "USER",
CUSTOMER = "CUSTOMER",
RELATION = "RELATION",
TENANT = "TENANT",
WIDGETS_BUNDLE = "WIDGETS_BUNDLE",
WIDGET_TYPE = "WIDGET_TYPE",
ADMIN_SETTINGS = "ADMIN_SETTINGS"
DASHBOARD = 'DASHBOARD',
ASSET = 'ASSET',
DEVICE = 'DEVICE',
DEVICE_PROFILE = 'DEVICE_PROFILE',
ASSET_PROFILE = 'ASSET_PROFILE',
ENTITY_VIEW = 'ENTITY_VIEW',
ALARM = 'ALARM',
RULE_CHAIN = 'RULE_CHAIN',
RULE_CHAIN_METADATA = 'RULE_CHAIN_METADATA',
EDGE = 'EDGE',
USER = 'USER',
CUSTOMER = 'CUSTOMER',
RELATION = 'RELATION',
TENANT = 'TENANT',
WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
WIDGET_TYPE = 'WIDGET_TYPE',
ADMIN_SETTINGS = 'ADMIN_SETTINGS'
}
export enum EdgeEventActionType {
ADDED = "ADDED",
DELETED = "DELETED",
UPDATED = "UPDATED",
POST_ATTRIBUTES = "POST_ATTRIBUTES",
ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED",
ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED",
TIMESERIES_UPDATED = "TIMESERIES_UPDATED",
CREDENTIALS_UPDATED = "CREDENTIALS_UPDATED",
ASSIGNED_TO_CUSTOMER = "ASSIGNED_TO_CUSTOMER",
UNASSIGNED_FROM_CUSTOMER = "UNASSIGNED_FROM_CUSTOMER",
RELATION_ADD_OR_UPDATE = "RELATION_ADD_OR_UPDATE",
RELATION_DELETED = "RELATION_DELETED",
RPC_CALL = "RPC_CALL",
ALARM_ACK = "ALARM_ACK",
ALARM_CLEAR = "ALARM_CLEAR",
ASSIGNED_TO_EDGE = "ASSIGNED_TO_EDGE",
UNASSIGNED_FROM_EDGE = "UNASSIGNED_FROM_EDGE",
CREDENTIALS_REQUEST = "CREDENTIALS_REQUEST",
ENTITY_MERGE_REQUEST = "ENTITY_MERGE_REQUEST"
ADDED = 'ADDED',
DELETED = 'DELETED',
UPDATED = 'UPDATED',
POST_ATTRIBUTES = 'POST_ATTRIBUTES',
ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED',
ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED',
TIMESERIES_UPDATED = 'TIMESERIES_UPDATED',
CREDENTIALS_UPDATED = 'CREDENTIALS_UPDATED',
ASSIGNED_TO_CUSTOMER = 'ASSIGNED_TO_CUSTOMER',
UNASSIGNED_FROM_CUSTOMER = 'UNASSIGNED_FROM_CUSTOMER',
RELATION_ADD_OR_UPDATE = 'RELATION_ADD_OR_UPDATE',
RELATION_DELETED = 'RELATION_DELETED',
RPC_CALL = 'RPC_CALL',
ALARM_ACK = 'ALARM_ACK',
ALARM_CLEAR = 'ALARM_CLEAR',
ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE',
UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE',
CREDENTIALS_REQUEST = 'CREDENTIALS_REQUEST',
ENTITY_MERGE_REQUEST = 'ENTITY_MERGE_REQUEST'
}
export enum EdgeEventStatus {
DEPLOYED = "DEPLOYED",
PENDING = "PENDING"
DEPLOYED = 'DEPLOYED',
PENDING = 'PENDING'
}
export const edgeEventTypeTranslations = new Map<EdgeEventType, string>(
@ -96,6 +97,7 @@ export const edgeEventTypeTranslations = new Map<EdgeEventType, string>(
[EdgeEventType.ASSET, 'edge-event.type-asset'],
[EdgeEventType.DEVICE, 'edge-event.type-device'],
[EdgeEventType.DEVICE_PROFILE, 'edge-event.type-device-profile'],
[EdgeEventType.ASSET_PROFILE, 'edge-event.type-asset-profile'],
[EdgeEventType.ENTITY_VIEW, 'edge-event.type-entity-view'],
[EdgeEventType.ALARM, 'edge-event.type-alarm'],
[EdgeEventType.RULE_CHAIN, 'edge-event.type-rule-chain'],

24
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -26,6 +26,7 @@ export enum EntityType {
ASSET = 'ASSET',
DEVICE = 'DEVICE',
DEVICE_PROFILE = 'DEVICE_PROFILE',
ASSET_PROFILE = 'ASSET_PROFILE',
ALARM = 'ALARM',
RULE_CHAIN = 'RULE_CHAIN',
RULE_NODE = 'RULE_NODE',
@ -150,6 +151,20 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
selectedEntities: 'device-profile.selected-device-profiles'
}
],
[
EntityType.ASSET_PROFILE,
{
type: 'entity.type-asset-profile',
typePlural: 'entity.type-asset-profiles',
list: 'entity.list-of-asset-profiles',
nameStartsWith: 'entity.asset-profile-name-starts-with',
details: 'asset-profile.asset-profile-details',
add: 'asset-profile.add',
noEntities: 'asset-profile.no-asset-profiles-text',
search: 'asset-profile.search',
selectedEntities: 'asset-profile.selected-asset-profiles'
}
],
[
EntityType.ASSET,
{
@ -361,6 +376,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
helpLinkId: 'deviceProfiles'
}
],
[
EntityType.ASSET_PROFILE,
{
helpLinkId: 'assetProfiles'
}
],
[
EntityType.ASSET,
{
@ -426,7 +447,8 @@ export const baseDetailsPageByEntityType = new Map<EntityType, string>([
[EntityType.DASHBOARD, '/dashboards'],
[EntityType.ASSET, '/assets'],
[EntityType.DEVICE, '/devices'],
[EntityType.DEVICE_PROFILE, '/deviceProfiles'],
[EntityType.DEVICE_PROFILE, '/profiles/deviceProfiles'],
[EntityType.ASSET_PROFILE, '/profiles/assetProfiles'],
[EntityType.RULE_CHAIN, '/ruleChains'],
[EntityType.EDGE, '/edgeInstances'],
[EntityType.ENTITY_VIEW, '/entityViews'],

26
ui-ngx/src/app/shared/models/id/asset-profile-id.ts

@ -0,0 +1,26 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityId } from './entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class AssetProfileId implements EntityId {
entityType = EntityType.ASSET_PROFILE;
id: string;
constructor(id: string) {
this.id = id;
}
}

1
ui-ngx/src/app/shared/models/id/public-api.ts

@ -36,3 +36,4 @@ export * from './user-id';
export * from './widget-type-id';
export * from './widgets-bundle-id';
export * from './edge-id';
export * from './asset-id';

1
ui-ngx/src/app/shared/models/vc.models.ts

@ -28,6 +28,7 @@ export const exportableEntityTypes: Array<EntityType> = [
EntityType.DASHBOARD,
EntityType.CUSTOMER,
EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE,
EntityType.RULE_CHAIN,
EntityType.WIDGETS_BUNDLE
];

51
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1231,6 +1231,49 @@
"unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?",
"unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge."
},
"asset-profile": {
"asset-profile": "Asset profile",
"asset-profiles": "Asset profiles",
"all-asset-profiles": "All",
"add": "Add asset profile",
"edit": "Edit asset profile",
"asset-profile-details": "Asset profile details",
"no-asset-profiles-text": "No asset profiles found",
"search": "Search asset profiles",
"selected-asset-profiles": "{ count, plural, 1 {1 asset profile} other {# asset profiles} } selected",
"no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.",
"asset-profile-required": "Asset profile is required",
"idCopiedMessage": "Asset profile Id has been copied to clipboard",
"set-default": "Make asset profile default",
"delete": "Delete asset profile",
"copyId": "Copy asset profile Id",
"name-max-length": "Name should be less than 256",
"new-device-profile-name": "Asset profile name",
"new-device-profile-name-required": "Asset profile name is required.",
"name": "Name",
"name-required": "Name is required.",
"image": "Asset profile image",
"description": "Description",
"default": "Default",
"default-rule-chain": "Default rule chain",
"mobile-dashboard": "Mobile dashboard",
"mobile-dashboard-hint": "Used by mobile application as a asset details dashboard",
"select-queue-hint": "Select from a drop-down list.",
"delete-asset-profile-title": "Are you sure you want to delete the asset profile '{{assetProfileName}}'?",
"delete-asset-profile-text": "Be careful, after the confirmation the asset profile and all related data will become unrecoverable.",
"delete-asset-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 asset profile} other {# asset profiles} }?",
"delete-asset-profiles-text": "Be careful, after the confirmation all selected asset profiles will be removed and all related data will become unrecoverable.",
"set-default-asset-profile-title": "Are you sure you want to make the asset profile '{{assetProfileName}}' default?",
"set-default-asset-profile-text": "After the confirmation the asset profile will be marked as default and will be used for new assets with no profile specified.",
"no-asset-profiles-found": "No asset profiles found.",
"create-new-asset-profile": "Create a new one!",
"create-asset-profile": "Create new asset profile",
"import": "Import asset profile",
"export": "Export asset profile",
"export-failed-error": "Unable to export asset profile: {{error}}",
"asset-profile-file": "Asset profile file",
"invalid-asset-profile-file-error": "Unable to import asset profile: Invalid asset profile data structure."
},
"device-profile": {
"device-profile": "Device profile",
"device-profiles": "Device profiles",
@ -1722,6 +1765,7 @@
"type-asset": "Asset",
"type-device": "Device",
"type-device-profile": "Device Profile",
"type-asset-profile": "Asset Profile",
"type-entity-view": "Entity View",
"type-alarm": "Alarm",
"type-rule-chain": "Rule Chain",
@ -1808,6 +1852,10 @@
"type-device-profiles": "Device profiles",
"list-of-device-profiles": "{ count, plural, 1 {One device profile} other {List of # device profiles} }",
"device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'",
"type-asset-profile": "Asset profile",
"type-asset-profiles": "Asset profiles",
"list-of-asset-profiles": "{ count, plural, 1 {One asset profile} other {List of # asset profiles} }",
"asset-profile-name-starts-with": "Asset profiles whose names start with '{{prefix}}'",
"type-asset": "Asset",
"type-assets": "Assets",
"list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }",
@ -2660,6 +2708,9 @@
"tokenCopiedSuccessMessage": "JWT token has been copied to clipboard",
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
},
"profiles": {
"profiles": "Profiles"
},
"security": {
"security": "Security",
"2fa": {

Loading…
Cancel
Save