diff --git a/library/src/components/TryItOut.tsx b/library/src/components/TryItOut.tsx new file mode 100644 index 000000000..7f2865b98 --- /dev/null +++ b/library/src/components/TryItOut.tsx @@ -0,0 +1,699 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import Form from '@rjsf/antd'; +import validator from '@rjsf/validator-ajv8'; + +interface TryItOutFormProps { + schemaPayload: any; + channelName: string; + backendUrl: string; +} + +interface FormState { + formData: any; + rawText: string; + sent: boolean; + error: string | null; + isLoading: boolean; + showRaw: boolean; + sendToRealBroker: boolean; + endpointUrl: string; + showUrlInput: boolean; + responseData: any | null; +} + +export const TryItOutForm: React.FC = ({ + backendUrl, + channelName, + schemaPayload, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [state, setState] = useState({ + formData: {}, + rawText: '{}', + sent: false, + error: null, + isLoading: false, + showRaw: false, + sendToRealBroker: false, + endpointUrl: `${window.origin}/${backendUrl}`, + showUrlInput: false, + responseData: null, + }); + + const normalizeSchema = useCallback((schema: any): any => { + if (!schema || typeof schema !== 'object') return schema; + + const clean = { ...schema }; + + const fieldsToRemove = [ + 'x-parser-schema-id', + 'anySchema', + 'cannotBeDefined', + 'maximum', + 'minimum', + 'oneOf', + 'readOnly', + 'writeOnly', + 'description', + ]; + + fieldsToRemove.forEach((field) => delete clean[field]); + + if (clean.properties) { + clean.properties = Object.entries(clean.properties).reduce( + (acc, [key, prop]) => { + if (prop === false || (prop as any)?.cannotBeDefined === false) + return acc; + return { ...acc, [key]: normalizeSchema(prop) }; + }, + {}, + ); + } + + if (clean.dependencies) { + clean.dependencies = Object.entries(clean.dependencies).reduce( + (acc, [key, dep]) => ({ ...acc, [key]: normalizeSchema(dep) }), + {}, + ); + } + + ['if', 'then', 'else'].forEach((field) => { + if (clean[field]) clean[field] = normalizeSchema(clean[field]); + }); + + if (Array.isArray(clean.oneOf)) { + clean.oneOf = clean.oneOf.map(normalizeSchema); + } + + return clean; + }, []); + + const sanitizeSchema = useCallback((schema: any) => { + if (schema === true) return {}; + if (typeof schema !== 'object' || schema === null) return schema; + + const cleaned = { ...schema }; + + if (cleaned.properties) { + cleaned.properties = Object.entries(cleaned.properties).reduce( + (acc, [key, prop]) => ({ ...acc, [key]: sanitizeSchema(prop) }), + {}, + ); + } + + const problematicFields = [ + 'additionalProperties', + 'dependencies', + 'patternProperties', + 'definitions', + ]; + + problematicFields.forEach((field) => { + if (cleaned[field] === true) { + cleaned[field] = {}; + } + }); + + return cleaned; + }, []); + + const cleanedSchema = useMemo( + () => sanitizeSchema(normalizeSchema(schemaPayload)), + [schemaPayload, normalizeSchema, sanitizeSchema], + ); + + const handleSubmit = useCallback( + async (data: any) => { + setState((prev) => ({ + ...prev, + responseData: null, + isLoading: true, + error: null, + })); + + try { + const payload = { + channelName, + message: data, + options: { + sendToRealBroker: state.sendToRealBroker, + timestamp: new Date().toISOString(), + }, + }; + + const response = await fetch(state.endpointUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const responseJson = await response.json(); + + if (!response.ok) { + throw new Error( + responseJson.message || `HTTP error! status: ${response.status}`, + ); + } + + setState((prev) => ({ + ...prev, + responseData: responseJson, + sent: true, + isLoading: false, + })); + + setTimeout(() => setState((prev) => ({ ...prev, sent: false })), 3000); + } catch (e: any) { + setState((prev) => ({ + ...prev, + error: e.message, + isLoading: false, + })); + } + }, + [channelName, state.sendToRealBroker, state.endpointUrl], + ); + + const handleRawSend = useCallback(() => { + try { + const parsed = JSON.parse(state.rawText); + handleSubmit(parsed); + } catch (e: any) { + setState((prev) => ({ ...prev, error: 'Invalid JSON: ' + e.message })); + } + }, [state.rawText, handleSubmit]); + + const updateState = useCallback((updates: Partial) => { + setState((prev) => ({ ...prev, ...updates })); + }, []); + + const EndpointIcon = () => ( + + + + + ); + + const LoadingSpinner = () => ( + + + + ); + + if (!isOpen) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+
+ + +
+ +
+ +
+
+ + {state.showUrlInput && ( +
+ + updateState({ endpointUrl: e.target.value })} + style={{ + width: '100%', + padding: '10px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px', + boxSizing: 'border-box', + }} + placeholder="Enter API endpoint URL" + /> +
+ )} + + {state.showRaw ? ( +
+