Showing
6 changed files
with
277 additions
and
6 deletions
@@ -85,7 +85,7 @@ public class InstallScripts { | @@ -85,7 +85,7 @@ public class InstallScripts { | ||
85 | } | 85 | } |
86 | 86 | ||
87 | public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { | 87 | public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { |
88 | - return Paths.get(getDataDir(), JSON_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); | 88 | + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); |
89 | } | 89 | } |
90 | 90 | ||
91 | public String getDataDir() { | 91 | public String getDataDir() { |
@@ -68,6 +68,12 @@ export class RuleChainService { | @@ -68,6 +68,12 @@ export class RuleChainService { | ||
68 | return this.http.get<RuleChain>(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); | 68 | return this.http.get<RuleChain>(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); |
69 | } | 69 | } |
70 | 70 | ||
71 | + public createDefaultRuleChain(ruleChainName: string, config?: RequestConfig): Observable<RuleChain> { | ||
72 | + return this.http.post<RuleChain>('/api/ruleChain/device/default', { | ||
73 | + name: ruleChainName | ||
74 | + }, defaultHttpOptionsFromConfig(config)); | ||
75 | + } | ||
76 | + | ||
71 | public saveRuleChain(ruleChain: RuleChain, config?: RequestConfig): Observable<RuleChain> { | 77 | public saveRuleChain(ruleChain: RuleChain, config?: RequestConfig): Observable<RuleChain> { |
72 | return this.http.post<RuleChain>('/api/ruleChain', ruleChain, defaultHttpOptionsFromConfig(config)); | 78 | return this.http.post<RuleChain>('/api/ruleChain', ruleChain, defaultHttpOptionsFromConfig(config)); |
73 | } | 79 | } |
@@ -106,6 +106,7 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio | @@ -106,6 +106,7 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio | ||
106 | import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; | 106 | import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; |
107 | import { FilterTextComponent } from './filter/filter-text.component'; | 107 | import { FilterTextComponent } from './filter/filter-text.component'; |
108 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; | 108 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; |
109 | +import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; | ||
109 | 110 | ||
110 | @NgModule({ | 111 | @NgModule({ |
111 | declarations: | 112 | declarations: |
@@ -194,7 +195,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di | @@ -194,7 +195,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di | ||
194 | DeviceProfileDataComponent, | 195 | DeviceProfileDataComponent, |
195 | DeviceProfileComponent, | 196 | DeviceProfileComponent, |
196 | DeviceProfileDialogComponent, | 197 | DeviceProfileDialogComponent, |
197 | - AddDeviceProfileDialogComponent | 198 | + AddDeviceProfileDialogComponent, |
199 | + RuleChainAutocompleteComponent | ||
198 | ], | 200 | ], |
199 | imports: [ | 201 | imports: [ |
200 | CommonModule, | 202 | CommonModule, |
@@ -272,7 +274,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di | @@ -272,7 +274,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di | ||
272 | DeviceProfileDataComponent, | 274 | DeviceProfileDataComponent, |
273 | DeviceProfileComponent, | 275 | DeviceProfileComponent, |
274 | DeviceProfileDialogComponent, | 276 | DeviceProfileDialogComponent, |
275 | - AddDeviceProfileDialogComponent | 277 | + AddDeviceProfileDialogComponent, |
278 | + RuleChainAutocompleteComponent | ||
276 | ], | 279 | ], |
277 | providers: [ | 280 | providers: [ |
278 | WidgetComponentService, | 281 | WidgetComponentService, |
@@ -41,11 +41,10 @@ | @@ -41,11 +41,10 @@ | ||
41 | {{ 'device-profile.name-required' | translate }} | 41 | {{ 'device-profile.name-required' | translate }} |
42 | </mat-error> | 42 | </mat-error> |
43 | </mat-form-field> | 43 | </mat-form-field> |
44 | - <tb-entity-autocomplete | 44 | + <tb-rule-chain-autocomplete |
45 | labelText="device-profile.default-rule-chain" | 45 | labelText="device-profile.default-rule-chain" |
46 | - [entityType]="entityType.RULE_CHAIN" | ||
47 | formControlName="defaultRuleChainId"> | 46 | formControlName="defaultRuleChainId"> |
48 | - </tb-entity-autocomplete> | 47 | + </tb-rule-chain-autocomplete> |
49 | <mat-form-field class="mat-block"> | 48 | <mat-form-field class="mat-block"> |
50 | <mat-label translate>device-profile.type</mat-label> | 49 | <mat-label translate>device-profile.type</mat-label> |
51 | <mat-select formControlName="type" required> | 50 | <mat-select formControlName="type" required> |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2020 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<mat-form-field [formGroup]="selectRuleChainFormGroup" class="mat-block"> | ||
19 | + <input matInput type="text" placeholder="{{ ruleChainLabel | translate }}" | ||
20 | + #entityInput | ||
21 | + formControlName="ruleChainId" | ||
22 | + (focusin)="onFocus()" | ||
23 | + [required]="required" | ||
24 | + [matAutocomplete]="entityAutocomplete"> | ||
25 | + <button *ngIf="selectRuleChainFormGroup.get('ruleChainId').value && !disabled" | ||
26 | + type="button" | ||
27 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
28 | + (click)="clear()"> | ||
29 | + <mat-icon class="material-icons">close</mat-icon> | ||
30 | + </button> | ||
31 | + <mat-autocomplete class="tb-autocomplete" | ||
32 | + #entityAutocomplete="matAutocomplete" | ||
33 | + [displayWith]="displayEntityFn"> | ||
34 | + <mat-option *ngFor="let entity of filteredRuleChains | async" [value]="entity"> | ||
35 | + <span [innerHTML]="entity.name | highlight:searchText"></span> | ||
36 | + </mat-option> | ||
37 | + <mat-option *ngIf="!(filteredRuleChains | async)?.length" [value]="null" class="tb-not-found"> | ||
38 | + <div class="tb-not-found-content" (click)="$event.stopPropagation()"> | ||
39 | + <div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> | ||
40 | + <span translate>device-profile.no-device-profiles-found</span> | ||
41 | + </div> | ||
42 | + <ng-template #searchNotEmpty> | ||
43 | + <span> | ||
44 | + {{ translate.get('rulechain.no-rulechains-matching', | ||
45 | + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} | ||
46 | + </span> | ||
47 | + </ng-template> | ||
48 | + <span> | ||
49 | + <a translate (click)="createDefaultRuleChain($event, searchText)">rulechain.create-new-rulechain</a> | ||
50 | + </span> | ||
51 | + </div> | ||
52 | + </mat-option> | ||
53 | + </mat-autocomplete> | ||
54 | + <mat-error *ngIf="selectRuleChainFormGroup.get('ruleChainId').hasError('required')"> | ||
55 | + {{ 'rulechain.rulechain-required' | translate }} | ||
56 | + </mat-error> | ||
57 | +</mat-form-field> |
1 | +/// | ||
2 | +/// Copyright © 2016-2020 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { Observable } from 'rxjs'; | ||
20 | +import { map, mergeMap, share, tap } from 'rxjs/operators'; | ||
21 | +import { Store } from '@ngrx/store'; | ||
22 | +import { AppState } from '@core/core.state'; | ||
23 | +import { TranslateService } from '@ngx-translate/core'; | ||
24 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
25 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
26 | +import { EntityType } from '@shared/models/entity-type.models'; | ||
27 | +import { BaseData } from '@shared/models/base-data'; | ||
28 | +import { EntityService } from '@core/http/entity.service'; | ||
29 | +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; | ||
30 | +import { RuleChainService } from '@core/http/rule-chain.service'; | ||
31 | +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; | ||
32 | + | ||
33 | +@Component({ | ||
34 | + selector: 'tb-rule-chain-autocomplete', | ||
35 | + templateUrl: './rule-chain-autocomplete.component.html', | ||
36 | + styleUrls: [], | ||
37 | + providers: [{ | ||
38 | + provide: NG_VALUE_ACCESSOR, | ||
39 | + useExisting: forwardRef(() => RuleChainAutocompleteComponent), | ||
40 | + multi: true | ||
41 | + }] | ||
42 | +}) | ||
43 | +export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnInit { | ||
44 | + | ||
45 | + selectRuleChainFormGroup: FormGroup; | ||
46 | + | ||
47 | + ruleChainLabel = 'rulechain.rulechain'; | ||
48 | + | ||
49 | + modelValue: string | null; | ||
50 | + | ||
51 | + @Input() | ||
52 | + labelText: string; | ||
53 | + | ||
54 | + @Input() | ||
55 | + requiredText: string; | ||
56 | + | ||
57 | + private requiredValue: boolean; | ||
58 | + get required(): boolean { | ||
59 | + return this.requiredValue; | ||
60 | + } | ||
61 | + @Input() | ||
62 | + set required(value: boolean) { | ||
63 | + this.requiredValue = coerceBooleanProperty(value); | ||
64 | + } | ||
65 | + | ||
66 | + @Input() | ||
67 | + disabled: boolean; | ||
68 | + | ||
69 | + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; | ||
70 | + | ||
71 | + filteredRuleChains: Observable<Array<BaseData<EntityId>>>; | ||
72 | + | ||
73 | + searchText = ''; | ||
74 | + | ||
75 | + private dirty = false; | ||
76 | + | ||
77 | + private propagateChange = (v: any) => { }; | ||
78 | + | ||
79 | + constructor(private store: Store<AppState>, | ||
80 | + public translate: TranslateService, | ||
81 | + public truncate: TruncatePipe, | ||
82 | + private entityService: EntityService, | ||
83 | + private ruleChainService: RuleChainService, | ||
84 | + private fb: FormBuilder) { | ||
85 | + this.selectRuleChainFormGroup = this.fb.group({ | ||
86 | + ruleChainId: [null] | ||
87 | + }); | ||
88 | + } | ||
89 | + | ||
90 | + registerOnChange(fn: any): void { | ||
91 | + this.propagateChange = fn; | ||
92 | + } | ||
93 | + | ||
94 | + registerOnTouched(fn: any): void { | ||
95 | + } | ||
96 | + | ||
97 | + ngOnInit() { | ||
98 | + this.filteredRuleChains = this.selectRuleChainFormGroup.get('ruleChainId').valueChanges | ||
99 | + .pipe( | ||
100 | + tap(value => { | ||
101 | + let modelValue; | ||
102 | + if (typeof value === 'string' || !value) { | ||
103 | + modelValue = null; | ||
104 | + } else { | ||
105 | + modelValue = value.id.id; | ||
106 | + } | ||
107 | + this.updateView(modelValue); | ||
108 | + if (value === null) { | ||
109 | + this.clear(); | ||
110 | + } | ||
111 | + }), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), | ||
112 | + mergeMap(name => this.fetchRuleChain(name) ), | ||
113 | + share() | ||
114 | + ); | ||
115 | + } | ||
116 | + | ||
117 | + ngAfterViewInit(): void {} | ||
118 | + | ||
119 | + getCurrentEntity(): BaseData<EntityId> | null { | ||
120 | + const currentEntity = this.selectRuleChainFormGroup.get('ruleChainId').value; | ||
121 | + if (currentEntity && typeof currentEntity !== 'string') { | ||
122 | + return currentEntity as BaseData<EntityId>; | ||
123 | + } else { | ||
124 | + return null; | ||
125 | + } | ||
126 | + } | ||
127 | + | ||
128 | + setDisabledState(isDisabled: boolean): void { | ||
129 | + this.disabled = isDisabled; | ||
130 | + if (this.disabled) { | ||
131 | + this.selectRuleChainFormGroup.disable({emitEvent: false}); | ||
132 | + } else { | ||
133 | + this.selectRuleChainFormGroup.enable({emitEvent: false}); | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + textIsNotEmpty(text: string): boolean { | ||
138 | + return (text && text.length > 0); | ||
139 | + } | ||
140 | + | ||
141 | + writeValue(value: string | null): void { | ||
142 | + this.searchText = ''; | ||
143 | + if (value != null) { | ||
144 | + const targetEntityType = EntityType.RULE_CHAIN; | ||
145 | + this.entityService.getEntity(targetEntityType, value, {ignoreLoading: true, ignoreErrors: true}).subscribe( | ||
146 | + (entity) => { | ||
147 | + this.modelValue = entity.id.id; | ||
148 | + this.selectRuleChainFormGroup.get('ruleChainId').patchValue(entity, {emitEvent: false}); | ||
149 | + }, | ||
150 | + () => { | ||
151 | + this.modelValue = null; | ||
152 | + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); | ||
153 | + if (value !== null) { | ||
154 | + this.propagateChange(this.modelValue); | ||
155 | + } | ||
156 | + } | ||
157 | + ); | ||
158 | + } else { | ||
159 | + this.modelValue = null; | ||
160 | + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); | ||
161 | + } | ||
162 | + this.dirty = true; | ||
163 | + } | ||
164 | + | ||
165 | + onFocus() { | ||
166 | + if (this.dirty) { | ||
167 | + this.selectRuleChainFormGroup.get('ruleChainId').updateValueAndValidity({onlySelf: true, emitEvent: true}); | ||
168 | + this.dirty = false; | ||
169 | + } | ||
170 | + } | ||
171 | + | ||
172 | + reset() { | ||
173 | + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: false}); | ||
174 | + } | ||
175 | + | ||
176 | + updateView(value: string | null) { | ||
177 | + if (this.modelValue !== value) { | ||
178 | + this.modelValue = value; | ||
179 | + this.propagateChange(this.modelValue); | ||
180 | + } | ||
181 | + } | ||
182 | + | ||
183 | + displayEntityFn(entity?: BaseData<EntityId>): string | undefined { | ||
184 | + return entity ? entity.name : undefined; | ||
185 | + } | ||
186 | + | ||
187 | + fetchRuleChain(searchText?: string): Observable<Array<BaseData<EntityId>>> { | ||
188 | + this.searchText = searchText; | ||
189 | + return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, | ||
190 | + 50, null, {ignoreLoading: true}); | ||
191 | + } | ||
192 | + | ||
193 | + clear() { | ||
194 | + this.selectRuleChainFormGroup.get('ruleChainId').patchValue('', {emitEvent: true}); | ||
195 | + setTimeout(() => { | ||
196 | + this.entityInput.nativeElement.blur(); | ||
197 | + this.entityInput.nativeElement.focus(); | ||
198 | + }, 0); | ||
199 | + } | ||
200 | + | ||
201 | + createDefaultRuleChain($event: Event, ruleChainName: string) { | ||
202 | + this.ruleChainService.createDefaultRuleChain(ruleChainName).subscribe((ruleChain) => { | ||
203 | + this.updateView(ruleChain.id.id); | ||
204 | + }); | ||
205 | + } | ||
206 | +} |