Skip to content

Commit 41691aa

Browse files
authored
feat: Add option to block saving Cloud Config parameter if validation fails (#3225)
1 parent 3421b39 commit 41691aa

9 files changed

Lines changed: 525 additions & 47 deletions

File tree

src/components/Dropdown/Dropdown.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
border: 1px solid $blue;
2828
background: white;
2929
overflow: auto;
30+
overscroll-behavior: none;
3031

3132
button {
3233
@include buttonReset;

src/components/MultiSelect/MultiSelect.react.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export default class MultiSelect extends React.Component {
5454
}
5555

5656
toggle() {
57+
if (this.props.disabled) {
58+
return;
59+
}
5760
this.setPosition();
5861
this.setState({ open: !this.state.open });
5962
}
@@ -184,4 +187,5 @@ MultiSelect.propTypes = {
184187
dense: PropTypes.bool.describe('Mini variant - less height'),
185188
chips: PropTypes.bool.describe('Display chip for every selected item'),
186189
formatSelection: PropTypes.func.describe('Custom function to format the display text. Receives array of selected labels.'),
190+
disabled: PropTypes.bool.describe('When true, prevents the dropdown from opening.'),
187191
};

src/components/MultiSelect/MultiSelect.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
border: 1px solid $blue;
2828
background: white;
2929
overflow: auto;
30+
overscroll-behavior: none;
3031

3132
> *:last-child .option {
3233
border-bottom: none;

src/components/NonPrintableHighlighter/NonPrintableHighlighter.react.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ export default class NonPrintableHighlighter extends React.Component {
654654
>
655655
<span className={styles.regexIcon}></span>
656656
<span className={styles.regexText}>
657-
<span className={invalidCount > 0 ? styles.regexLabelInvalid : undefined}>
657+
<span className={invalidCount > 0 ? styles.regexSummaryInvalid : undefined}>
658658
{isJson
659659
? `${regexResult.results.filter(r => r.isValid).length}/${regexResult.results.length} valid regex pattern${regexResult.results.length > 1 ? 's' : ''}`
660660
: (regexResult.results[0].isValid ? 'Valid regex pattern' : 'Invalid regex pattern')

src/components/NonPrintableHighlighter/NonPrintableHighlighter.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,8 @@
223223
font-weight: 600;
224224
color: #d32f2f;
225225
}
226+
227+
.regexSummaryInvalid {
228+
color: #d32f2f;
229+
font-weight: 600;
230+
}

src/dashboard/Data/Config/Config.react.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ class Config extends TableView {
5757
removeEntryArrayValue: [],
5858
serverHistoryLimit: undefined,
5959
currentParamHistory: null,
60+
detectNonPrintable: false,
61+
detectRegex: false,
62+
nonPrintableBlockSave: [],
63+
nonPrintableShowOnlyFor: [],
64+
detectNonAlphanumeric: false,
65+
nonAlphanumericBlockSave: [],
66+
nonAlphanumericShowOnlyFor: [],
67+
regexBlockSave: [],
68+
regexShowOnlyFor: [],
6069
};
6170
this.noteTimeout = null;
6271
this.serverStorage = null;
@@ -98,12 +107,38 @@ class Config extends TableView {
98107
this.context.applicationId
99108
);
100109
if (settings) {
110+
const updates = {
111+
detectNonPrintable: settings.detectNonPrintable !== undefined ? !!settings.detectNonPrintable : true,
112+
detectNonAlphanumeric: settings.detectNonAlphanumeric !== undefined ? !!settings.detectNonAlphanumeric : true,
113+
detectRegex: settings.detectRegex !== undefined ? !!settings.detectRegex : true,
114+
};
101115
if (settings.historyLimit !== undefined) {
102-
this.setState({ serverHistoryLimit: settings.historyLimit });
116+
updates.serverHistoryLimit = settings.historyLimit;
117+
}
118+
if (Array.isArray(settings.nonPrintableBlockSave)) {
119+
updates.nonPrintableBlockSave = settings.nonPrintableBlockSave;
120+
}
121+
if (Array.isArray(settings.nonPrintableShowOnlyFor)) {
122+
updates.nonPrintableShowOnlyFor = settings.nonPrintableShowOnlyFor;
123+
}
124+
if (Array.isArray(settings.nonAlphanumericBlockSave)) {
125+
updates.nonAlphanumericBlockSave = settings.nonAlphanumericBlockSave;
103126
}
127+
if (Array.isArray(settings.nonAlphanumericShowOnlyFor)) {
128+
updates.nonAlphanumericShowOnlyFor = settings.nonAlphanumericShowOnlyFor;
129+
}
130+
if (Array.isArray(settings.regexBlockSave)) {
131+
updates.regexBlockSave = settings.regexBlockSave;
132+
}
133+
if (Array.isArray(settings.regexShowOnlyFor)) {
134+
updates.regexShowOnlyFor = settings.regexShowOnlyFor;
135+
}
136+
this.setState(updates);
137+
} else {
138+
this.setState({ detectNonPrintable: true, detectNonAlphanumeric: true, detectRegex: true });
104139
}
105140
} catch {
106-
// Fall back to existing context value
141+
this.setState({ detectNonPrintable: true, detectNonAlphanumeric: true, detectRegex: true });
107142
}
108143
}
109144

@@ -193,6 +228,15 @@ class Config extends TableView {
193228
parseServerVersion={this.context.serverInfo?.parseServerVersion}
194229
loading={this.state.loading}
195230
configHistory={this.state.currentParamHistory}
231+
detectNonPrintable={this.state.detectNonPrintable}
232+
detectRegex={this.state.detectRegex}
233+
nonPrintableBlockSave={this.state.nonPrintableBlockSave}
234+
nonPrintableShowOnlyFor={this.state.nonPrintableShowOnlyFor}
235+
detectNonAlphanumeric={this.state.detectNonAlphanumeric}
236+
nonAlphanumericBlockSave={this.state.nonAlphanumericBlockSave}
237+
nonAlphanumericShowOnlyFor={this.state.nonAlphanumericShowOnlyFor}
238+
regexBlockSave={this.state.regexBlockSave}
239+
regexShowOnlyFor={this.state.regexShowOnlyFor}
196240
/>
197241
);
198242
} else if (this.state.showDeleteParameterDialog) {

src/dashboard/Data/Config/ConfigDialog.react.js

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import FileInput from 'components/FileInput/FileInput.react';
1212
import GeoPointInput from 'components/GeoPointInput/GeoPointInput.react';
1313
import Label from 'components/Label/Label.react';
1414
import Modal from 'components/Modal/Modal.react';
15-
import NonPrintableHighlighter from 'components/NonPrintableHighlighter/NonPrintableHighlighter.react';
15+
import NonPrintableHighlighter, { hasNonPrintableChars, getNonPrintableCharsFromJson, hasNonAlphanumericChars, getNonAlphanumericCharsFromJson, getRegexValidation, getRegexValidationFromJson } from 'components/NonPrintableHighlighter/NonPrintableHighlighter.react';
1616
import Option from 'components/Dropdown/Option.react';
1717
import Parse from 'parse';
1818
import React from 'react';
@@ -28,7 +28,6 @@ import LoaderContainer from 'components/LoaderContainer/LoaderContainer.react';
2828
import ServerConfigStorage from 'lib/ServerConfigStorage';
2929
import { CurrentApp } from 'context/currentApp';
3030

31-
const CONFIG_KEY = 'config.settings';
3231
const FORMATTING_CONFIG_KEY = 'config.formatting.syntax';
3332

3433
const PARAM_TYPES = ['Boolean', 'String', 'Number', 'Date', 'Object', 'Array', 'GeoPoint', 'File'];
@@ -51,7 +50,7 @@ const EDITORS = {
5150
<Toggle type={Toggle.Types.TRUE_FALSE} value={!!value} onChange={onChange} />
5251
),
5352
String: (value, onChange, wordWrap, syntaxColors, options = {}) => (
54-
<NonPrintableHighlighter value={value} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={true} detectRegex={!!options.detectRegex}>
53+
<NonPrintableHighlighter value={value} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={!!options.detectNonAlphanumeric} detectRegex={!!options.detectRegex}>
5554
<TextInput multiline={true} value={value || ''} onChange={onChange} />
5655
</NonPrintableHighlighter>
5756
),
@@ -60,7 +59,7 @@ const EDITORS = {
6059
),
6160
Date: (value, onChange) => <DateTimeInput fixed={true} value={value} onChange={onChange} />,
6261
Object: (value, onChange, wordWrap, syntaxColors, options = {}) => (
63-
<NonPrintableHighlighter value={value} isJson={true} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={true} detectRegex={!!options.detectRegex}>
62+
<NonPrintableHighlighter value={value} isJson={true} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={!!options.detectNonAlphanumeric} detectRegex={!!options.detectRegex}>
6463
<JsonEditor
6564
value={value || ''}
6665
onChange={onChange}
@@ -71,7 +70,7 @@ const EDITORS = {
7170
</NonPrintableHighlighter>
7271
),
7372
Array: (value, onChange, wordWrap, syntaxColors, options = {}) => (
74-
<NonPrintableHighlighter value={value} isJson={true} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={true} detectRegex={!!options.detectRegex}>
73+
<NonPrintableHighlighter value={value} isJson={true} detectNonPrintable={!!options.detectNonPrintable} detectNonAlphanumeric={!!options.detectNonAlphanumeric} detectRegex={!!options.detectRegex}>
7574
<JsonEditor
7675
value={value || ''}
7776
onChange={onChange}
@@ -129,8 +128,6 @@ export default class ConfigDialog extends React.Component {
129128
wordWrap: false,
130129
error: null,
131130
syntaxColors: null,
132-
detectNonPrintable: true,
133-
detectRegex: true,
134131
};
135132
if (props.param.length > 0) {
136133
let initialValue = props.value;
@@ -147,15 +144,12 @@ export default class ConfigDialog extends React.Component {
147144
wordWrap: false,
148145
error: initialError,
149146
syntaxColors: null,
150-
detectNonPrintable: true,
151-
detectRegex: true,
152147
};
153148
}
154149
}
155150

156151
componentDidMount() {
157152
this.loadSyntaxColors();
158-
this.loadValueAnalysisSettings();
159153
}
160154

161155
async loadSyntaxColors() {
@@ -175,24 +169,21 @@ export default class ConfigDialog extends React.Component {
175169
}
176170
}
177171

178-
async loadValueAnalysisSettings() {
179-
try {
180-
const serverStorage = new ServerConfigStorage(this.context);
181-
if (serverStorage.isServerConfigEnabled()) {
182-
const settings = await serverStorage.getConfig(
183-
CONFIG_KEY,
184-
this.context.applicationId
185-
);
186-
if (settings?.detectNonPrintable !== undefined) {
187-
this.setState({ detectNonPrintable: !!settings.detectNonPrintable });
188-
}
189-
if (settings?.detectRegex !== undefined) {
190-
this.setState({ detectRegex: !!settings.detectRegex });
191-
}
192-
}
193-
} catch {
194-
// Silently fail - keep defaults (true)
172+
getEffectiveDetectionFlags() {
173+
const isExistingParam = this.props.param && this.props.param.length > 0;
174+
let detectNonPrintable = this.props.detectNonPrintable;
175+
if (detectNonPrintable && isExistingParam && this.props.nonPrintableShowOnlyFor.length > 0) {
176+
detectNonPrintable = this.props.nonPrintableShowOnlyFor.includes(this.props.param);
177+
}
178+
let detectNonAlphanumeric = this.props.detectNonAlphanumeric;
179+
if (detectNonAlphanumeric && isExistingParam && this.props.nonAlphanumericShowOnlyFor.length > 0) {
180+
detectNonAlphanumeric = this.props.nonAlphanumericShowOnlyFor.includes(this.props.param);
195181
}
182+
let detectRegex = this.props.detectRegex;
183+
if (detectRegex && isExistingParam && this.props.regexShowOnlyFor.length > 0) {
184+
detectRegex = this.props.regexShowOnlyFor.includes(this.props.param);
185+
}
186+
return { detectNonPrintable, detectNonAlphanumeric, detectRegex };
196187
}
197188

198189
valid() {
@@ -201,32 +192,41 @@ export default class ConfigDialog extends React.Component {
201192
}
202193
switch (this.state.type) {
203194
case 'String':
204-
return !!this.state.value;
195+
if (!this.state.value) {
196+
return false;
197+
}
198+
break;
205199
case 'Number':
206-
return !isNaN(parseFloat(this.state.value));
200+
if (isNaN(parseFloat(this.state.value))) {
201+
return false;
202+
}
203+
break;
207204
case 'Date':
208-
return !isNaN(new Date(this.state.value));
205+
if (isNaN(new Date(this.state.value))) {
206+
return false;
207+
}
208+
break;
209209
case 'Object':
210210
try {
211211
const obj = JSON.parse(this.state.value);
212-
if (obj && typeof obj === 'object') {
213-
return true;
212+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
213+
return false;
214214
}
215-
return false;
216215
} catch {
217216
return false;
218217
}
218+
break;
219219
case 'Array':
220220
try {
221221
const obj = JSON.parse(this.state.value);
222-
if (obj && Array.isArray(obj)) {
223-
return true;
222+
if (!obj || !Array.isArray(obj)) {
223+
return false;
224224
}
225-
return false;
226225
} catch {
227226
return false;
228227
}
229-
case 'GeoPoint':
228+
break;
229+
case 'GeoPoint': {
230230
const val = this.state.value;
231231
if (!val || typeof val !== 'object') {
232232
return false;
@@ -242,14 +242,81 @@ export default class ConfigDialog extends React.Component {
242242
) {
243243
return false;
244244
}
245-
return true;
246-
case 'File':
245+
break;
246+
}
247+
case 'File': {
247248
const fileVal = this.state.value;
248-
if (fileVal && fileVal.url()) {
249-
return true;
249+
if (!fileVal || !fileVal.url()) {
250+
return false;
250251
}
251-
return false;
252+
break;
253+
}
254+
}
255+
256+
// Compute effective detection flags (respecting show-only-for settings)
257+
const { detectNonPrintable, detectNonAlphanumeric, detectRegex } = this.getEffectiveDetectionFlags();
258+
259+
// Block save if non-printable characters detected for this param
260+
if (
261+
detectNonPrintable &&
262+
this.props.param.length > 0 &&
263+
this.props.nonPrintableBlockSave.includes(this.props.param)
264+
) {
265+
const value = this.state.value;
266+
if (value && typeof value === 'string') {
267+
if (this.state.type === 'Object' || this.state.type === 'Array') {
268+
if (getNonPrintableCharsFromJson(value).totalCount > 0) {
269+
return false;
270+
}
271+
} else if (this.state.type === 'String') {
272+
if (hasNonPrintableChars(value)) {
273+
return false;
274+
}
275+
}
276+
}
277+
}
278+
279+
// Block save if non-alphanumeric characters detected for this param
280+
if (
281+
detectNonAlphanumeric &&
282+
this.props.param.length > 0 &&
283+
this.props.nonAlphanumericBlockSave.includes(this.props.param)
284+
) {
285+
const value = this.state.value;
286+
if (value && typeof value === 'string') {
287+
if (this.state.type === 'Object' || this.state.type === 'Array') {
288+
if (getNonAlphanumericCharsFromJson(value).totalCount > 0) {
289+
return false;
290+
}
291+
} else if (this.state.type === 'String') {
292+
if (hasNonAlphanumericChars(value)) {
293+
return false;
294+
}
295+
}
296+
}
252297
}
298+
299+
// Block save if regex validation fails for this param
300+
if (
301+
detectRegex &&
302+
this.props.param.length > 0 &&
303+
this.props.regexBlockSave.includes(this.props.param)
304+
) {
305+
const value = this.state.value;
306+
if (value && typeof value === 'string') {
307+
if (this.state.type === 'Object' || this.state.type === 'Array') {
308+
const result = getRegexValidationFromJson(value);
309+
if (result.results.some(r => !r.isValid)) {
310+
return false;
311+
}
312+
} else if (this.state.type === 'String') {
313+
if (!getRegexValidation(value).isValid) {
314+
return false;
315+
}
316+
}
317+
}
318+
}
319+
253320
return true;
254321
}
255322

@@ -346,6 +413,13 @@ export default class ConfigDialog extends React.Component {
346413
this.setState({ selectedIndex: index, value });
347414
};
348415

416+
// Determine effective detection flags based on show-only-for settings
417+
const {
418+
detectNonPrintable: effectiveDetectNonPrintable,
419+
detectNonAlphanumeric: effectiveDetectNonAlphanumeric,
420+
detectRegex: effectiveDetectRegex,
421+
} = this.getEffectiveDetectionFlags();
422+
349423
const dialogContent = (
350424
<div>
351425
<Field
@@ -372,7 +446,7 @@ export default class ConfigDialog extends React.Component {
372446
value => this.setState({ value, error: null }),
373447
this.state.wordWrap,
374448
this.state.syntaxColors,
375-
{ detectNonPrintable: this.state.detectNonPrintable, detectRegex: this.state.detectRegex }
449+
{ detectNonPrintable: effectiveDetectNonPrintable, detectNonAlphanumeric: effectiveDetectNonAlphanumeric, detectRegex: effectiveDetectRegex }
376450
)}
377451
/>
378452

0 commit comments

Comments
 (0)