Commit ddf8a3569e7157083c60fafa77f6eb3747cdd238
1 parent
f3a5cb31
Refactoring validation mqtt topic filter; Add help hint
Showing
4 changed files
with
79 additions
and
20 deletions
... | ... | @@ -19,7 +19,6 @@ |
19 | 19 | <section formGroupName="configuration"> |
20 | 20 | <fieldset class="fields-group"> |
21 | 21 | <legend class="group-title" translate>device-profile.mqtt-device-topic-filters</legend> |
22 | - <h6 class="mat-body" innerHTML="{{ 'device-profile.support-level-wildcards' | translate }}"></h6> | |
23 | 22 | <div fxLayoutGap="8px" fxLayout="column"> |
24 | 23 | <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> |
25 | 24 | <mat-form-field fxFlex> |
... | ... | @@ -30,8 +29,11 @@ |
30 | 29 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('required')"> |
31 | 30 | {{ 'device-profile.telemetry-topic-filter-required' | translate}} |
32 | 31 | </mat-error> |
33 | - <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('pattern')"> | |
34 | - {{ 'device-profile.not-valid-pattern-topic-filter' | translate}} | |
32 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('invalidSingleTopicCharacter')"> | |
33 | + {{ 'device-profile.not-valid-single-character' | translate}} | |
34 | + </mat-error> | |
35 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('invalidMultiTopicCharacter')"> | |
36 | + {{ 'device-profile.not-valid-multi-character' | translate}} | |
35 | 37 | </mat-error> |
36 | 38 | </mat-form-field> |
37 | 39 | <mat-form-field fxFlex> |
... | ... | @@ -42,8 +44,11 @@ |
42 | 44 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('required')"> |
43 | 45 | {{ 'device-profile.rpc-request-topic-filter-required' | translate}} |
44 | 46 | </mat-error> |
45 | - <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('pattern')"> | |
46 | - {{ 'device-profile.not-valid-pattern-topic-filter' | translate}} | |
47 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('invalidSingleTopicCharacter')"> | |
48 | + {{ 'device-profile.not-valid-single-character' | translate}} | |
49 | + </mat-error> | |
50 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('invalidMultiTopicCharacter')"> | |
51 | + {{ 'device-profile.not-valid-multi-character' | translate}} | |
47 | 52 | </mat-error> |
48 | 53 | </mat-form-field> |
49 | 54 | </div> |
... | ... | @@ -56,8 +61,11 @@ |
56 | 61 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('required')"> |
57 | 62 | {{ 'device-profile.attributes-topic-filter-required' | translate}} |
58 | 63 | </mat-error> |
59 | - <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('pattern')"> | |
60 | - {{ 'device-profile.not-valid-pattern-topic-filter' | translate}} | |
64 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('invalidSingleTopicCharacter')"> | |
65 | + {{ 'device-profile.not-valid-single-character' | translate}} | |
66 | + </mat-error> | |
67 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('invalidMultiTopicCharacter')"> | |
68 | + {{ 'device-profile.not-valid-multi-character' | translate}} | |
61 | 69 | </mat-error> |
62 | 70 | </mat-form-field> |
63 | 71 | <mat-form-field fxFlex> |
... | ... | @@ -68,11 +76,17 @@ |
68 | 76 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('required')"> |
69 | 77 | {{ 'device-profile.rpc-response-topic-filter-required' | translate}} |
70 | 78 | </mat-error> |
71 | - <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('pattern')"> | |
72 | - {{ 'device-profile.not-valid-pattern-topic-filter' | translate}} | |
79 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('invalidSingleTopicCharacter')"> | |
80 | + {{ 'device-profile.not-valid-single-character' | translate}} | |
81 | + </mat-error> | |
82 | + <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('invalidMultiTopicCharacter')"> | |
83 | + {{ 'device-profile.not-valid-multi-character' | translate}} | |
73 | 84 | </mat-error> |
74 | 85 | </mat-form-field> |
75 | 86 | </div> |
87 | + <div class="tb-hint" innerHTML="{{ 'device-profile.support-level-wildcards' | translate }}"></div> | |
88 | + <div class="tb-hint" innerHTML="{{ 'device-profile.single-level-wildcards-hint' | translate }}"></div> | |
89 | + <div class="tb-hint" innerHTML="{{ 'device-profile.multi-level-wildcards-hint' | translate }}"></div> | |
76 | 90 | </div> |
77 | 91 | </fieldset> |
78 | 92 | </section> | ... | ... |
... | ... | @@ -15,15 +15,24 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
18 | -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormBuilder, | |
21 | + FormControl, | |
22 | + FormGroup, | |
23 | + NG_VALUE_ACCESSOR, | |
24 | + ValidatorFn, | |
25 | + Validators | |
26 | +} from '@angular/forms'; | |
19 | 27 | import { Store } from '@ngrx/store'; |
20 | 28 | import { AppState } from '@app/core/core.state'; |
21 | 29 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
22 | 30 | import { |
23 | 31 | DeviceProfileTransportConfiguration, |
24 | - DeviceTransportType, MqttDeviceProfileTransportConfiguration | |
32 | + DeviceTransportType, | |
33 | + MqttDeviceProfileTransportConfiguration | |
25 | 34 | } from '@shared/models/device.models'; |
26 | -import { isDefinedAndNotNull } from '../../../../../core/utils'; | |
35 | +import { isDefinedAndNotNull } from '@core/utils'; | |
27 | 36 | |
28 | 37 | @Component({ |
29 | 38 | selector: 'tb-mqtt-device-profile-transport-configuration', |
... | ... | @@ -41,11 +50,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control |
41 | 50 | |
42 | 51 | private requiredValue: boolean; |
43 | 52 | |
44 | - private MQTTTopicPattern = new RegExp('^((?![#+]).)*$|^(.*[^#]\/|^)#$|^(.*\/|^)\+(\/.*|$)$'); | |
45 | - | |
46 | 53 | get required(): boolean { |
47 | 54 | return this.requiredValue; |
48 | 55 | } |
56 | + | |
49 | 57 | @Input() |
50 | 58 | set required(value: boolean) { |
51 | 59 | this.requiredValue = coerceBooleanProperty(value); |
... | ... | @@ -70,10 +78,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control |
70 | 78 | ngOnInit() { |
71 | 79 | this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ |
72 | 80 | configuration: this.fb.group({ |
73 | - deviceAttributesTopic: [null, [Validators.required, Validators.pattern(this.MQTTTopicPattern)]], | |
74 | - deviceTelemetryTopic: [null, [Validators.required, Validators.pattern(this.MQTTTopicPattern)]], | |
75 | - deviceRpcRequestTopic: [null, [Validators.required, Validators.pattern(this.MQTTTopicPattern)]], | |
76 | - deviceRpcResponseTopic: [null, [Validators.required, Validators.pattern(this.MQTTTopicPattern)]] | |
81 | + deviceAttributesTopic: [null, [Validators.required, this.validationMQTTTopic()]], | |
82 | + deviceTelemetryTopic: [null, [Validators.required, this.validationMQTTTopic()]], | |
83 | + deviceRpcRequestTopic: [null, [Validators.required, this.validationMQTTTopic()]], | |
84 | + deviceRpcResponseTopic: [null, [Validators.required, this.validationMQTTTopic()]] | |
77 | 85 | }) |
78 | 86 | }); |
79 | 87 | this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { |
... | ... | @@ -104,4 +112,34 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control |
104 | 112 | } |
105 | 113 | this.propagateChange(configuration); |
106 | 114 | } |
115 | + | |
116 | + private validationMQTTTopic(): ValidatorFn { | |
117 | + return (c: FormControl) => { | |
118 | + const newTopic = c.value; | |
119 | + const wildcardSymbols = /[#+]/g; | |
120 | + let findSymbol = wildcardSymbols.exec(newTopic); | |
121 | + while (findSymbol) { | |
122 | + const index = findSymbol.index; | |
123 | + const currentSymbol = findSymbol[0]; | |
124 | + const prevSymbol = index > 0 ? newTopic[index - 1] : null; | |
125 | + const nextSymbol = index < (newTopic.length - 1) ? newTopic[index + 1] : null; | |
126 | + if (currentSymbol === '#' && (index !== (newTopic.length - 1) || (prevSymbol !== null && prevSymbol !== '/'))) { | |
127 | + return { | |
128 | + invalidMultiTopicCharacter: { | |
129 | + valid: false | |
130 | + } | |
131 | + }; | |
132 | + } | |
133 | + if (currentSymbol === '+' && ((prevSymbol !== null && prevSymbol !== '/') || (nextSymbol !== null && nextSymbol !== '/'))) { | |
134 | + return { | |
135 | + invalidSingleTopicCharacter: { | |
136 | + valid: false | |
137 | + } | |
138 | + }; | |
139 | + } | |
140 | + findSymbol = wildcardSymbols.exec(newTopic); | |
141 | + } | |
142 | + return null; | |
143 | + }; | |
144 | + } | |
107 | 145 | } | ... | ... |
... | ... | @@ -792,7 +792,7 @@ |
792 | 792 | "no-device-profiles-found": "No device profiles found.", |
793 | 793 | "create-new-device-profile": "Create a new one!", |
794 | 794 | "mqtt-device-topic-filters": "MQTT device topic filters", |
795 | - "support-level-wildcards": "Support single <code>(+)</code> and multi <code>(#)</code> level wildcards", | |
795 | + "support-level-wildcards": "Single <code>[+]</code> and multi-level <code>[#]</code> wildcards supported.", | |
796 | 796 | "telemetry-topic-filter": "Telemetry topic filter", |
797 | 797 | "telemetry-topic-filter-required": "Telemetry topic filter is required.", |
798 | 798 | "rpc-request-topic-filter": "RPC request topic filter", |
... | ... | @@ -801,7 +801,10 @@ |
801 | 801 | "attributes-topic-filter-required": "Attributes topic filter is required.", |
802 | 802 | "rpc-response-topic-filter": "RPC response topic filter", |
803 | 803 | "rpc-response-topic-filter-required": "RPC response topic filter is required.", |
804 | - "not-valid-pattern-topic-filter": "Not valid pattern topic filter" | |
804 | + "not-valid-single-character": "Invalid use of a single-level wildcard character", | |
805 | + "not-valid-multi-character": "Invalid use of a multi-level wildcard character", | |
806 | + "single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.", | |
807 | + "multi-level-wildcards-hint": "<code>[#]</code> can replace the topic filter itself and must be the last symbol of the topic. Ex.: <b>#</b> or <b>v1/devices/me/#</b>." | |
805 | 808 | }, |
806 | 809 | "dialog": { |
807 | 810 | "close": "Close dialog" | ... | ... |