diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index eabbb495a4..cd74753f31 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -85,7 +85,7 @@ public class InstallScripts { } public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { - return Paths.get(getDataDir(), JSON_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); } public String getDataDir() { diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index 2ac567ac30..e7ab664ae9 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -68,6 +68,12 @@ export class RuleChainService { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } + public createDefaultRuleChain(ruleChainName: string, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/device/default', { + name: ruleChainName + }, defaultHttpOptionsFromConfig(config)); + } + public saveRuleChain(ruleChain: RuleChain, config?: RequestConfig): Observable { return this.http.post('/api/ruleChain', ruleChain, defaultHttpOptionsFromConfig(config)); } 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 0f8b4a88cf..d583369016 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 @@ -106,6 +106,7 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; import { FilterTextComponent } from './filter/filter-text.component'; import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; +import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; @NgModule({ declarations: @@ -194,7 +195,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di DeviceProfileDataComponent, DeviceProfileComponent, DeviceProfileDialogComponent, - AddDeviceProfileDialogComponent + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent ], imports: [ CommonModule, @@ -272,7 +274,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di DeviceProfileDataComponent, DeviceProfileComponent, DeviceProfileDialogComponent, - AddDeviceProfileDialogComponent + AddDeviceProfileDialogComponent, + RuleChainAutocompleteComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 5e1c4adfd6..d7f119a3b7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -41,11 +41,10 @@ {{ 'device-profile.name-required' | translate }} - - + device-profile.type diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html new file mode 100644 index 0000000000..9f6dd96294 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -0,0 +1,57 @@ + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('rulechain.no-rulechains-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + rulechain.create-new-rulechain + +
+
+
+ + {{ 'rulechain.rulechain-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts new file mode 100644 index 0000000000..708707e7ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -0,0 +1,206 @@ +/// +/// 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 { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +@Component({ + selector: 'tb-rule-chain-autocomplete', + templateUrl: './rule-chain-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RuleChainAutocompleteComponent), + multi: true + }] +}) +export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectRuleChainFormGroup: FormGroup; + + ruleChainLabel = 'rulechain.rulechain'; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; + + filteredRuleChains: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private ruleChainService: RuleChainService, + private fb: FormBuilder) { + this.selectRuleChainFormGroup = this.fb.group({ + ruleChainId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredRuleChains = this.selectRuleChainFormGroup.get('ruleChainId').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchRuleChain(name) ), + share() + ); + } + + ngAfterViewInit(): void {} + + getCurrentEntity(): BaseData | null { + const currentEntity = this.selectRuleChainFormGroup.get('ruleChainId').value; + if (currentEntity && typeof currentEntity !== 'string') { + return currentEntity as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectRuleChainFormGroup.disable({emitEvent: false}); + } else { + this.selectRuleChainFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + const targetEntityType = EntityType.RULE_CHAIN; + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectRuleChainFormGroup.get('ruleChainId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayEntityFn(entity?: BaseData): string | undefined { + return entity ? entity.name : undefined; + } + + fetchRuleChain(searchText?: string): Observable>> { + this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, + 50, null, {ignoreLoading: true}); + } + + clear() { + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.entityInput.nativeElement.blur(); + this.entityInput.nativeElement.focus(); + }, 0); + } + + createDefaultRuleChain($event: Event, ruleChainName: string) { + this.ruleChainService.createDefaultRuleChain(ruleChainName).subscribe((ruleChain) => { + this.updateView(ruleChain.id.id); + }); + } +}