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,7 +19,6 @@ | ||
19 | <section formGroupName="configuration"> | 19 | <section formGroupName="configuration"> |
20 | <fieldset class="fields-group"> | 20 | <fieldset class="fields-group"> |
21 | <legend class="group-title" translate>device-profile.mqtt-device-topic-filters</legend> | 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 | <div fxLayoutGap="8px" fxLayout="column"> | 22 | <div fxLayoutGap="8px" fxLayout="column"> |
24 | <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | 23 | <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> |
25 | <mat-form-field fxFlex> | 24 | <mat-form-field fxFlex> |
@@ -30,8 +29,11 @@ | @@ -30,8 +29,11 @@ | ||
30 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('required')"> | 29 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceTelemetryTopic').hasError('required')"> |
31 | {{ 'device-profile.telemetry-topic-filter-required' | translate}} | 30 | {{ 'device-profile.telemetry-topic-filter-required' | translate}} |
32 | </mat-error> | 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 | </mat-error> | 37 | </mat-error> |
36 | </mat-form-field> | 38 | </mat-form-field> |
37 | <mat-form-field fxFlex> | 39 | <mat-form-field fxFlex> |
@@ -42,8 +44,11 @@ | @@ -42,8 +44,11 @@ | ||
42 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('required')"> | 44 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcRequestTopic').hasError('required')"> |
43 | {{ 'device-profile.rpc-request-topic-filter-required' | translate}} | 45 | {{ 'device-profile.rpc-request-topic-filter-required' | translate}} |
44 | </mat-error> | 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 | </mat-error> | 52 | </mat-error> |
48 | </mat-form-field> | 53 | </mat-form-field> |
49 | </div> | 54 | </div> |
@@ -56,8 +61,11 @@ | @@ -56,8 +61,11 @@ | ||
56 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('required')"> | 61 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceAttributesTopic').hasError('required')"> |
57 | {{ 'device-profile.attributes-topic-filter-required' | translate}} | 62 | {{ 'device-profile.attributes-topic-filter-required' | translate}} |
58 | </mat-error> | 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 | </mat-error> | 69 | </mat-error> |
62 | </mat-form-field> | 70 | </mat-form-field> |
63 | <mat-form-field fxFlex> | 71 | <mat-form-field fxFlex> |
@@ -68,11 +76,17 @@ | @@ -68,11 +76,17 @@ | ||
68 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('required')"> | 76 | <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('configuration.deviceRpcResponseTopic').hasError('required')"> |
69 | {{ 'device-profile.rpc-response-topic-filter-required' | translate}} | 77 | {{ 'device-profile.rpc-response-topic-filter-required' | translate}} |
70 | </mat-error> | 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 | </mat-error> | 84 | </mat-error> |
74 | </mat-form-field> | 85 | </mat-form-field> |
75 | </div> | 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 | </div> | 90 | </div> |
77 | </fieldset> | 91 | </fieldset> |
78 | </section> | 92 | </section> |
@@ -15,15 +15,24 @@ | @@ -15,15 +15,24 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { Component, forwardRef, Input, OnInit } from '@angular/core'; | 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 | import { Store } from '@ngrx/store'; | 27 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@app/core/core.state'; | 28 | import { AppState } from '@app/core/core.state'; |
21 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; | 29 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
22 | import { | 30 | import { |
23 | DeviceProfileTransportConfiguration, | 31 | DeviceProfileTransportConfiguration, |
24 | - DeviceTransportType, MqttDeviceProfileTransportConfiguration | 32 | + DeviceTransportType, |
33 | + MqttDeviceProfileTransportConfiguration | ||
25 | } from '@shared/models/device.models'; | 34 | } from '@shared/models/device.models'; |
26 | -import { isDefinedAndNotNull } from '../../../../../core/utils'; | 35 | +import { isDefinedAndNotNull } from '@core/utils'; |
27 | 36 | ||
28 | @Component({ | 37 | @Component({ |
29 | selector: 'tb-mqtt-device-profile-transport-configuration', | 38 | selector: 'tb-mqtt-device-profile-transport-configuration', |
@@ -41,11 +50,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | @@ -41,11 +50,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | ||
41 | 50 | ||
42 | private requiredValue: boolean; | 51 | private requiredValue: boolean; |
43 | 52 | ||
44 | - private MQTTTopicPattern = new RegExp('^((?![#+]).)*$|^(.*[^#]\/|^)#$|^(.*\/|^)\+(\/.*|$)$'); | ||
45 | - | ||
46 | get required(): boolean { | 53 | get required(): boolean { |
47 | return this.requiredValue; | 54 | return this.requiredValue; |
48 | } | 55 | } |
56 | + | ||
49 | @Input() | 57 | @Input() |
50 | set required(value: boolean) { | 58 | set required(value: boolean) { |
51 | this.requiredValue = coerceBooleanProperty(value); | 59 | this.requiredValue = coerceBooleanProperty(value); |
@@ -70,10 +78,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | @@ -70,10 +78,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | ||
70 | ngOnInit() { | 78 | ngOnInit() { |
71 | this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ | 79 | this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ |
72 | configuration: this.fb.group({ | 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 | this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { | 87 | this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { |
@@ -104,4 +112,34 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | @@ -104,4 +112,34 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control | ||
104 | } | 112 | } |
105 | this.propagateChange(configuration); | 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,7 +792,7 @@ | ||
792 | "no-device-profiles-found": "No device profiles found.", | 792 | "no-device-profiles-found": "No device profiles found.", |
793 | "create-new-device-profile": "Create a new one!", | 793 | "create-new-device-profile": "Create a new one!", |
794 | "mqtt-device-topic-filters": "MQTT device topic filters", | 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 | "telemetry-topic-filter": "Telemetry topic filter", | 796 | "telemetry-topic-filter": "Telemetry topic filter", |
797 | "telemetry-topic-filter-required": "Telemetry topic filter is required.", | 797 | "telemetry-topic-filter-required": "Telemetry topic filter is required.", |
798 | "rpc-request-topic-filter": "RPC request topic filter", | 798 | "rpc-request-topic-filter": "RPC request topic filter", |
@@ -801,7 +801,10 @@ | @@ -801,7 +801,10 @@ | ||
801 | "attributes-topic-filter-required": "Attributes topic filter is required.", | 801 | "attributes-topic-filter-required": "Attributes topic filter is required.", |
802 | "rpc-response-topic-filter": "RPC response topic filter", | 802 | "rpc-response-topic-filter": "RPC response topic filter", |
803 | "rpc-response-topic-filter-required": "RPC response topic filter is required.", | 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 | "dialog": { | 809 | "dialog": { |
807 | "close": "Close dialog" | 810 | "close": "Close dialog" |