@@ -26,6 +26,34 @@ interface SendMessageProps {
2626 messageData ?: Partial < MessageFormData > | null ;
2727}
2828
29+ // JSON formatting utility with comprehensive error handling
30+ const formatJsonString = ( input : string ) : { formatted : string ; error : string | null } => {
31+ if ( ! input || input . trim ( ) === '' ) {
32+ return { formatted : input , error : null } ;
33+ }
34+
35+ try {
36+ const parsed = JSON . parse ( input ) ;
37+ const formatted = JSON . stringify ( parsed , null , 2 ) ;
38+ return { formatted, error : null } ;
39+ } catch ( e ) {
40+ const errorMessage = e instanceof Error ? e . message : 'Invalid JSON format' ;
41+ return { formatted : input , error : errorMessage } ;
42+ }
43+ } ;
44+
45+ // JSON validation utility for optional validation
46+ const validateJsonField = ( value : string , fieldName : string , validateJson : boolean ) : boolean | string => {
47+ if ( ! validateJson || ! value || value . trim ( ) === '' ) return true ;
48+
49+ try {
50+ JSON . parse ( value ) ;
51+ return true ;
52+ } catch ( e ) {
53+ return `Invalid JSON in ${ fieldName } field: ${ e instanceof Error ? e . message : 'Parse error' } ` ;
54+ }
55+ } ;
56+
2957const SendMessage : React . FC < SendMessageProps > = ( {
3058 closeSidebar,
3159 messageData = null ,
@@ -38,6 +66,13 @@ const SendMessage: React.FC<SendMessageProps> = ({
3866 use : SerdeUsage . SERIALIZE ,
3967 } ) ;
4068 const sendMessage = useSendMessage ( { clusterName, topicName } ) ;
69+
70+ // Formatting state management
71+ const [ formatKey , setFormatKey ] = React . useState < boolean > ( false ) ;
72+ const [ formatValue , setFormatValue ] = React . useState < boolean > ( false ) ;
73+ const [ formatHeaders , setFormatHeaders ] = React . useState < boolean > ( false ) ;
74+ const [ validateJson , setValidateJson ] = React . useState < boolean > ( false ) ;
75+
4176 const defaultValues = React . useMemo ( ( ) => getDefaultValues ( serdes ) , [ serdes ] ) ;
4277 const partitionOptions = React . useMemo (
4378 ( ) => getPartitionOptions ( topic ?. partitions || [ ] ) ,
@@ -59,11 +94,40 @@ const SendMessage: React.FC<SendMessageProps> = ({
5994 formState : { isSubmitting } ,
6095 control,
6196 setValue,
97+ watch,
6298 } = useForm < MessageFormData > ( {
6399 mode : 'onChange' ,
64100 defaultValues : formDefaults ,
65101 } ) ;
66102
103+ // Format toggle handler with error handling and user feedback
104+ const handleFormatToggle = React . useCallback ( ( field : 'key' | 'content' | 'headers' ) => {
105+ const currentValue = watch ( field ) || '' ;
106+ const { formatted, error } = formatJsonString ( currentValue ) ;
107+
108+ if ( error ) {
109+ showAlert ( 'error' , {
110+ id : `format-error-${ field } ` ,
111+ title : 'Format Error' ,
112+ message : `Cannot format ${ field } : ${ error } ` ,
113+ } ) ;
114+ } else {
115+ setValue ( field , formatted ) ;
116+ // Update formatting state
117+ switch ( field ) {
118+ case 'key' :
119+ setFormatKey ( true ) ;
120+ break ;
121+ case 'content' :
122+ setFormatValue ( true ) ;
123+ break ;
124+ case 'headers' :
125+ setFormatHeaders ( true ) ;
126+ break ;
127+ }
128+ }
129+ } , [ watch , setValue ] ) ;
130+
67131 const submit = async ( {
68132 keySerde,
69133 valueSerde,
@@ -75,9 +139,21 @@ const SendMessage: React.FC<SendMessageProps> = ({
75139 } : MessageFormData ) => {
76140 let errors : string [ ] = [ ] ;
77141
142+ // JSON validation if enabled
143+ if ( validateJson ) {
144+ const keyValidation = validateJsonField ( key || '' , 'key' , validateJson ) ;
145+ const contentValidation = validateJsonField ( content || '' , 'content' , validateJson ) ;
146+ const headersValidation = validateJsonField ( headers || '' , 'headers' , validateJson ) ;
147+
148+ if ( typeof keyValidation === 'string' ) errors . push ( keyValidation ) ;
149+ if ( typeof contentValidation === 'string' ) errors . push ( contentValidation ) ;
150+ if ( typeof headersValidation === 'string' ) errors . push ( headersValidation ) ;
151+ }
152+
153+ // Existing schema validation
78154 if ( keySerde ) {
79155 const selectedKeySerde = serdes . key ?. find ( ( k ) => k . name === keySerde ) ;
80- errors = validateBySchema ( key , selectedKeySerde ?. schema , 'key' ) ;
156+ errors = [ ... errors , ... validateBySchema ( key , selectedKeySerde ?. schema , 'key' ) ] ;
81157 }
82158
83159 if ( valueSerde ) {
@@ -111,6 +187,7 @@ const SendMessage: React.FC<SendMessageProps> = ({
111187 } ) ;
112188 return ;
113189 }
190+
114191 try {
115192 await sendMessage . mutateAsync ( {
116193 key : key || null ,
@@ -190,6 +267,14 @@ const SendMessage: React.FC<SendMessageProps> = ({
190267 />
191268 </ S . FlexItem >
192269 </ S . Flex >
270+ < S . ValidationSection >
271+ < Switch
272+ name = "validateJson"
273+ onChange = { setValidateJson }
274+ checked = { validateJson }
275+ />
276+ < InputLabel > Validate JSON before submission</ InputLabel >
277+ </ S . ValidationSection >
193278 < div >
194279 < Controller
195280 control = { control }
@@ -201,58 +286,136 @@ const SendMessage: React.FC<SendMessageProps> = ({
201286 < InputLabel > Keep contents</ InputLabel >
202287 </ div >
203288 </ S . Columns >
289+
204290 < S . Columns >
205- < div >
206- < InputLabel > Key</ InputLabel >
291+ < S . FieldGroup >
292+ < S . FieldHeader >
293+ < InputLabel > Key</ InputLabel >
294+ < S . FormatButton
295+ buttonSize = "S"
296+ buttonType = { formatKey ? "primary" : "secondary" }
297+ onClick = { ( ) => handleFormatToggle ( 'key' ) }
298+ aria-label = "Format JSON for key field"
299+ type = "button"
300+ disabled = { isSubmitting }
301+ >
302+ Format JSON
303+ </ S . FormatButton >
304+ </ S . FieldHeader >
207305 < Controller
208306 control = { control }
209307 name = "key"
210308 render = { ( { field : { name, onChange, value } } ) => (
211309 < Editor
212310 readOnly = { isSubmitting }
213311 name = { name }
214- onChange = { onChange }
312+ onChange = { ( newValue ) => {
313+ onChange ( newValue ) ;
314+ // Reset format state when user manually edits
315+ if ( formatKey && newValue !== value ) {
316+ setFormatKey ( false ) ;
317+ }
318+ } }
215319 value = { value }
216320 height = "40px"
321+ mode = { formatKey ? "json5" : undefined }
322+ setOptions = { {
323+ showLineNumbers : formatKey ,
324+ tabSize : 2 ,
325+ useWorker : false
326+ } }
217327 />
218328 ) }
219329 />
220- </ div >
221- < div >
222- < InputLabel > Value</ InputLabel >
330+ </ S . FieldGroup >
331+
332+ < S . FieldGroup >
333+ < S . FieldHeader >
334+ < InputLabel > Value</ InputLabel >
335+ < S . FormatButton
336+ buttonSize = "S"
337+ buttonType = { formatValue ? "primary" : "secondary" }
338+ onClick = { ( ) => handleFormatToggle ( 'content' ) }
339+ aria-label = "Format JSON for value field"
340+ type = "button"
341+ disabled = { isSubmitting }
342+ >
343+ Format JSON
344+ </ S . FormatButton >
345+ </ S . FieldHeader >
223346 < Controller
224347 control = { control }
225348 name = "content"
226349 render = { ( { field : { name, onChange, value } } ) => (
227350 < Editor
228351 readOnly = { isSubmitting }
229352 name = { name }
230- onChange = { onChange }
353+ onChange = { ( newValue ) => {
354+ onChange ( newValue ) ;
355+ // Reset format state when user manually edits
356+ if ( formatValue && newValue !== value ) {
357+ setFormatValue ( false ) ;
358+ }
359+ } }
231360 value = { value }
232361 height = "280px"
362+ mode = { formatValue ? "json5" : undefined }
363+ setOptions = { {
364+ showLineNumbers : formatValue ,
365+ tabSize : 2 ,
366+ useWorker : false
367+ } }
233368 />
234369 ) }
235370 />
236- </ div >
371+ </ S . FieldGroup >
237372 </ S . Columns >
373+
238374 < S . Columns >
239- < div >
240- < InputLabel > Headers</ InputLabel >
241- < Controller
242- control = { control }
243- name = "headers"
244- render = { ( { field : { name, onChange, value } } ) => (
245- < Editor
246- readOnly = { isSubmitting }
247- name = { name }
248- onChange = { onChange }
249- value = { value || '{}' }
250- height = "40px"
251- />
252- ) }
253- />
254- </ div >
375+ < S . FieldGroup >
376+ < S . FieldHeader >
377+ < InputLabel > Headers</ InputLabel >
378+ < S . FormatButton
379+ buttonSize = "S"
380+ buttonType = { formatHeaders ? "primary" : "secondary" }
381+ onClick = { ( ) => handleFormatToggle ( 'headers' ) }
382+ aria-label = "Format JSON for headers field"
383+ type = "button"
384+ disabled = { isSubmitting }
385+ >
386+ Format JSON
387+ </ S . FormatButton >
388+ </ S . FieldHeader >
389+ < S . ResizableEditorWrapper >
390+ < Controller
391+ control = { control }
392+ name = "headers"
393+ render = { ( { field : { name, onChange, value } } ) => (
394+ < Editor
395+ readOnly = { isSubmitting }
396+ name = { name }
397+ onChange = { ( newValue ) => {
398+ onChange ( newValue ) ;
399+ // Reset format state when user manually edits
400+ if ( formatHeaders && newValue !== value ) {
401+ setFormatHeaders ( false ) ;
402+ }
403+ } }
404+ value = { value || '{}' }
405+ height = "40px"
406+ mode = { formatHeaders ? "json5" : undefined }
407+ setOptions = { {
408+ showLineNumbers : formatHeaders ,
409+ tabSize : 2 ,
410+ useWorker : false
411+ } }
412+ />
413+ ) }
414+ />
415+ </ S . ResizableEditorWrapper >
416+ </ S . FieldGroup >
255417 </ S . Columns >
418+
256419 < Button
257420 buttonSize = "M"
258421 buttonType = "primary"
@@ -266,4 +429,4 @@ const SendMessage: React.FC<SendMessageProps> = ({
266429 ) ;
267430} ;
268431
269- export default SendMessage ;
432+ export default SendMessage ;
0 commit comments