From f39d6638273ba5f49bed00e123e0889d3092a066 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 27 Aug 2020 19:32:15 +0300 Subject: [PATCH] UI: Tenant Profiles --- .../app/core/http/tenant-profile.service.ts | 67 +++++++ .../interceptors/global-http-interceptor.ts | 2 +- ui-ngx/src/app/core/services/menu.service.ts | 15 +- .../entity/entities-table.component.ts | 16 +- .../home/components/home-components.module.ts | 10 +- ...tenant-profile-autocomplete.component.html | 46 +++++ .../tenant-profile-autocomplete.component.ts | 171 ++++++++++++++++++ .../profile/tenant-profile.component.html | 68 +++++++ .../profile/tenant-profile.component.scss} | 0 .../profile/tenant-profile.component.ts | 96 ++++++++++ .../modules/home/pages/home-pages.module.ts | 2 + .../tenant-profile-routing.module.ts | 56 ++++++ .../tenant-profile-tabs.component.html | 43 +++++ .../tenant-profile-tabs.component.ts | 38 ++++ .../tenant-profile/tenant-profile.module.ts | 35 ++++ .../tenant-profiles-table-config.resolver.ts | 122 +++++++++++++ .../home/pages/tenant/tenant.component.html | 15 +- .../home/pages/tenant/tenant.component.ts | 10 +- ui-ngx/src/app/shared/models/id/entity-id.ts | 9 + ui-ngx/src/app/shared/models/tenant.model.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 18 +- 21 files changed, 810 insertions(+), 31 deletions(-) create mode 100644 ui-ngx/src/app/core/http/tenant-profile.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html rename ui-ngx/src/app/modules/home/{pages/tenant/tenant.component.scss => components/profile/tenant-profile.component.scss} (100%) create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts diff --git a/ui-ngx/src/app/core/http/tenant-profile.service.ts b/ui-ngx/src/app/core/http/tenant-profile.service.ts new file mode 100644 index 0000000000..f7d0b7ccdf --- /dev/null +++ b/ui-ngx/src/app/core/http/tenant-profile.service.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2020 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 { TenantProfile } from '@shared/models/tenant.model'; +import { EntityInfoData } from '@shared/models/entity.models'; + +@Injectable({ + providedIn: 'root' +}) +export class TenantProfileService { + + constructor( + private http: HttpClient + ) { } + + public getTenantProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveTenantProfile(tenantProfile: TenantProfile, config?: RequestConfig): Observable { + return this.http.post('/api/tenantProfile', tenantProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteTenantProfile(tenantProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/tenantProfile/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultTenantProfile(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/tenantProfile/${tenantProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultTenantProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/tenantProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfo(tenantProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenantProfileInfo/${tenantProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getTenantProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenantProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 0897053bb9..2874ed6cda 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -249,7 +249,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } else { this.activeRequests--; } - if (this.activeRequests === 1) { + if (this.activeRequests === 1 && isLoading) { this.store.dispatch(new ActionLoadStart()); } else if (this.activeRequests === 0) { this.store.dispatch(new ActionLoadFinish()); diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 6032bf24f3..efa72f2951 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -85,6 +85,13 @@ export class MenuService { path: '/tenants', icon: 'supervisor_account' }, + { + name: 'tenant-profile.tenant-profiles', + type: 'link', + path: '/tenantProfiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true + }, { name: 'widget.widget-library', type: 'link', @@ -132,7 +139,13 @@ export class MenuService { name: 'tenant.tenants', icon: 'supervisor_account', path: '/tenants' - } + }, + { + name: 'tenant-profile.tenant-profiles', + icon: 'mdi:alpha-t-box', + isMdiIcon: true, + path: '/tenantProfiles' + }, ] }, { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 6c27a8c9f7..ac20e70128 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -36,9 +36,9 @@ import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; @@ -59,6 +59,7 @@ import { DAY, historyInterval, HistoryWindowType, Timewindow } from '@shared/mod import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; +import { HasUUID } from '../../../../shared/models/id/has-uuid'; @Component({ selector: 'tb-entities-table', @@ -401,16 +402,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn true ).subscribe((result) => { if (result) { - const tasks: Observable[] = []; + const tasks: Observable[] = []; entities.forEach((entity) => { if (this.entitiesTableConfig.deleteEnabled(entity)) { - tasks.push(this.entitiesTableConfig.deleteEntity(entity.id)); + tasks.push(this.entitiesTableConfig.deleteEntity(entity.id).pipe( + map(() => entity.id), + catchError(() => of(null) + ))); } }); forkJoin(tasks).subscribe( - () => { + (ids) => { this.updateData(); - this.entitiesTableConfig.entitiesDeleted(entities.map((e) => e.id)); + this.entitiesTableConfig.entitiesDeleted(ids.filter(id => id !== null)); } ); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index f2b8e1f2d6..787a6f7317 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -84,6 +84,8 @@ import { UserFilterDialogComponent } from '@home/components/filter/user-filter-d import { FilterUserInfoComponent } from './filter/filter-user-info.component'; import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component'; import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component'; +import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component'; +import { TenantProfileComponent } from './profile/tenant-profile.component'; @NgModule({ declarations: @@ -150,7 +152,9 @@ import { FilterPredicateValueComponent } from './filter/filter-predicate-value.c UserFilterDialogComponent, FilterUserInfoComponent, FilterUserInfoDialogComponent, - FilterPredicateValueComponent + FilterPredicateValueComponent, + TenantProfileAutocompleteComponent, + TenantProfileComponent ], imports: [ CommonModule, @@ -206,7 +210,9 @@ import { FilterPredicateValueComponent } from './filter/filter-predicate-value.c FiltersDialogComponent, FilterSelectComponent, FiltersEditComponent, - UserFilterDialogComponent + UserFilterDialogComponent, + TenantProfileAutocompleteComponent, + TenantProfileComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html new file mode 100644 index 0000000000..0f892cc3be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -0,0 +1,46 @@ + + + + + + + + + + + {{ translate.get('tenant-profile.no-tenant-profiles-matching', {entity: searchText}) | async }} + + + + + {{ 'tenant-profile.tenant-profile-required' | translate }} + + diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts new file mode 100644 index 0000000000..99ee16c5d3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts @@ -0,0 +1,171 @@ +/// +/// Copyright © 2016-2020 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, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, startWith, 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 { TenantProfileId } from '@shared/models/id/tenant-profile-id'; +import { EntityInfoData } from '@shared/models/entity.models'; +import { TenantProfileService } from '@core/http/tenant-profile.service'; +import { entityIdEquals } from '../../../../shared/models/id/entity-id'; + +@Component({ + selector: 'tb-tenant-profile-autocomplete', + templateUrl: './tenant-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TenantProfileAutocompleteComponent), + multi: true + }] +}) +export class TenantProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectTenantProfileFormGroup: FormGroup; + + modelValue: TenantProfileId | null; + + @Input() + selectDefaultProfile = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('tenantProfileInput', {static: true}) tenantProfileInput: ElementRef; + + filteredTenantProfiles: Observable>; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private tenantProfileService: TenantProfileService, + private fb: FormBuilder) { + this.selectTenantProfileFormGroup = this.fb.group({ + tenantProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTenantProfiles = this.selectTenantProfileFormGroup.get('tenantProfile').valueChanges + .pipe( + tap((value: EntityInfoData | string) => { + let modelValue: TenantProfileId | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = new TenantProfileId(value.id.id); + } + this.updateView(modelValue); + }), + startWith(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTenantProfiles(name) ) + ); + } + + selectDefaultTenantProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.tenantProfileService.getDefaultTenantProfileInfo().subscribe( + (profile) => { + if (profile) { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: TenantProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new TenantProfileId(profile.id.id); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true}); + } + ); + } else { + this.modelValue = null; + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: true}); + this.selectDefaultTenantProfileIfNeeded(); + } + } + + updateView(value: TenantProfileId | null) { + if (!entityIdEquals(this.modelValue, value)) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTenantProfileFn(profile?: EntityInfoData): string | undefined { + return profile ? profile.name : undefined; + } + + fetchTenantProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.tenantProfileService.getTenantProfileInfos(pageLink, {ignoreLoading: true}).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + clear() { + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.tenantProfileInput.nativeElement.blur(); + this.tenantProfileInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html new file mode 100644 index 0000000000..f36ae2d37c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -0,0 +1,68 @@ + +
+ + +
+ +
+
+
+
+
+ + tenant-profile.name + + + {{ 'tenant-profile.name-required' | translate }} + + +
+ +
{{ 'tenant.isolated-tb-core' | translate }}
+
{{'tenant.isolated-tb-core-details' | translate}}
+
+ +
{{ 'tenant.isolated-tb-rule-engine' | translate }}
+
{{'tenant.isolated-tb-rule-engine-details' | translate}}
+
+
+ + tenant-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss rename to ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts new file mode 100644 index 0000000000..8a0fabe971 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TenantProfile } from '@app/shared/models/tenant.model'; +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'; + +@Component({ + selector: 'tb-tenant-profile', + templateUrl: './tenant-profile.component.html', + styleUrls: ['./tenant-profile.component.scss'] +}) +export class TenantProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: TenantProfile, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: TenantProfile): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], + isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + description: [entity ? entity.description : '', []], + } + ); + } + + updateForm(entity: TenantProfile) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); + this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({description: entity.description}); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + if (!this.isAdd) { + this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); + this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); + } + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + onTenantProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('tenant-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index a6578ed38e..5c8503eb2b 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -29,6 +29,7 @@ import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.mo import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; +import { TenantProfileModule } from './tenant-profile/tenant-profile.module'; import { MODULES_MAP } from '@shared/public-api'; import { modulesMap } from '../../common/modules-map'; @@ -37,6 +38,7 @@ import { modulesMap } from '../../common/modules-map'; AdminModule, HomeLinksModule, ProfileModule, + TenantProfileModule, TenantModule, DeviceModule, AssetModule, diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts new file mode 100644 index 0000000000..c10efb76f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { TenantProfilesTableConfigResolver } from './tenant-profiles-table-config.resolver'; + +const routes: Routes = [ + { + path: 'tenantProfiles', + data: { + breadcrumb: { + label: 'tenant-profile.tenant-profiles', + icon: 'mdi:alpha-t-box' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.SYS_ADMIN], + title: 'tenant-profile.tenant-profiles' + }, + resolve: { + entitiesTableConfig: TenantProfilesTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + TenantProfilesTableConfigResolver + ] +}) +export class TenantProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html new file mode 100644 index 0000000000..40a9f55aa5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts new file mode 100644 index 0000000000..cab08974f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2020 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 { TenantProfile } from '@shared/models/tenant.model'; + +@Component({ + selector: 'tb-tenant-profile-tabs', + templateUrl: './tenant-profile-tabs.component.html', + styleUrls: [] +}) +export class TenantProfileTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts new file mode 100644 index 0000000000..66d55d37ed --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2020 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 { TenantProfileRoutingModule } from './tenant-profile-routing.module'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; + +@NgModule({ + declarations: [ + TenantProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + TenantProfileRoutingModule + ] +}) +export class TenantProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..a56fbf60da --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2020 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 } from '@angular/router'; +import { TenantProfile } from '@shared/models/tenant.model'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} 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 { TenantProfileService } from '@core/http/tenant-profile.service'; +import { TenantProfileComponent } from '../../components/profile/tenant-profile.component'; +import { TenantProfileTabsComponent } from './tenant-profile-tabs.component'; +import { DialogService } from '@core/services/dialog.service'; + +@Injectable() +export class TenantProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private tenantProfileService: TenantProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService) { + + this.config.entityType = EntityType.TENANT_PROFILE; + this.config.entityComponent = TenantProfileComponent; + this.config.entityTabsComponent = TenantProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.TENANT_PROFILE); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'tenant-profile.name', '40%'), + new EntityTableColumn('description', 'tenant-profile.description', '60%'), + new EntityTableColumn('isDefault', 'tenant-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('tenant-profile.set-default'), + icon: 'flag', + isEnabled: (tenantProfile) => !tenantProfile.default, + onAction: ($event, entity) => this.setDefaultTenantProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = tenantProfile => this.translate.instant('tenant-profile.delete-tenant-profile-title', + { tenantProfileName: tenantProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('tenant-profile.delete-tenant-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant-profile.delete-tenant-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('tenant-profile.delete-tenant-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.tenantProfileService.getTenantProfiles(pageLink); + this.config.loadEntity = id => this.tenantProfileService.getTenantProfile(id.id); + this.config.saveEntity = tenantProfile => this.tenantProfileService.saveTenantProfile(tenantProfile); + this.config.deleteEntity = id => this.tenantProfileService.deleteTenantProfile(id.id); + this.config.onEntityAction = action => this.onTenantProfileAction(action); + this.config.deleteEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + this.config.entitySelectionEnabled = (tenantProfile) => tenantProfile && !tenantProfile.default; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('tenant-profile.tenant-profiles'); + + return this.config; + } + + setDefaultTenantProfile($event: Event, tenantProfile: TenantProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('tenant-profile.set-default-tenant-profile-title', {tenantProfileName: tenantProfile.name}), + this.translate.instant('tenant-profile.set-default-tenant-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.tenantProfileService.setDefaultTenantProfile(tenantProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onTenantProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultTenantProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html index 3f22741130..1b37efd149 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -49,6 +49,11 @@ {{ 'tenant.title-required' | translate }} + +
tenant.description @@ -56,16 +61,6 @@
- diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts index 19fe1f342d..566086939b 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts @@ -27,7 +27,7 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod @Component({ selector: 'tb-tenant', templateUrl: './tenant.component.html', - styleUrls: ['./tenant.component.scss'] + styleUrls: [] }) export class TenantComponent extends ContactBasedComponent { @@ -52,8 +52,6 @@ export class TenantComponent extends ContactBasedComponent { { title: [entity ? entity.title : '', [Validators.required]], tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], - // isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], - // isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] @@ -66,8 +64,6 @@ export class TenantComponent extends ContactBasedComponent { updateEntityForm(entity: Tenant) { this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); - // this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); - // this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); } @@ -75,10 +71,6 @@ export class TenantComponent extends ContactBasedComponent { if (this.entityForm) { if (this.isEditValue) { this.entityForm.enable({emitEvent: false}); - /* if (!this.isAdd) { - this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); - this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); - } */ } else { this.entityForm.disable({emitEvent: false}); } diff --git a/ui-ngx/src/app/shared/models/id/entity-id.ts b/ui-ngx/src/app/shared/models/id/entity-id.ts index 6ead753066..1596af127b 100644 --- a/ui-ngx/src/app/shared/models/id/entity-id.ts +++ b/ui-ngx/src/app/shared/models/id/entity-id.ts @@ -16,7 +16,16 @@ import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { isDefinedAndNotNull } from '@core/utils'; export interface EntityId extends HasUUID { entityType: EntityType | AliasEntityType; } + +export function entityIdEquals(entityId1: EntityId, entityId2: EntityId): boolean { + if (isDefinedAndNotNull(entityId1) && isDefinedAndNotNull(entityId2)) { + return entityId1.id === entityId2.id && entityId1.entityType === entityId2.entityType; + } else { + return entityId1 === entityId2; + } +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 7eaedd7c41..a0602515d3 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -26,7 +26,7 @@ export interface TenantProfileData { export interface TenantProfile extends BaseData { name: string; description: string; - isDefault: boolean; + default: boolean; isolatedTbCore: boolean; isolatedTbRuleEngine: boolean; profileData: TenantProfileData; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 19d079395d..746356452c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1683,7 +1683,23 @@ "tenant-profile-details": "Tenant profile details", "no-tenant-profiles-text": "No tenant profiles found", "search": "Search tenant profiles", - "selected-tenant-profiles": "{ count, plural, 1 {1 tenant profile} other {# tenant profiles} } selected" + "selected-tenant-profiles": "{ count, plural, 1 {1 tenant profile} other {# tenant profiles} } selected", + "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", + "tenant-profile-required": "Tenant profile is required", + "idCopiedMessage": "Tenant profile Id has been copied to clipboard", + "set-default": "Make tenant profile default", + "delete": "Delete tenant profile", + "copyId": "Copy tenant profile Id", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "default": "Default", + "delete-tenant-profile-title": "Are you sure you want to delete the tenant profile '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.", + "delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?", + "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.", + "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' root?", + "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified." }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",