import React, { useEffect, useState } from 'react'; import { openapiService } from '@/_services'; import Select from '@/_ui/Select'; import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; import DOMPurify from 'dompurify'; import { ToolTip } from '@/_components'; import CodeHinter from '@/AppBuilder/CodeEditor'; import { withTranslation } from 'react-i18next'; import { isEmpty } from 'lodash'; import PropTypes from 'prop-types'; import SolidIcons from '@/_ui/Icon/SolidIcons'; const operationColorMapping = { get: 'azure', post: 'green', delete: 'red', put: 'yellow', }; const extractSchemaProperties = (schema) => { if (!schema) return {}; if (schema.properties) { return schema.properties; } // Handle allOf - merge all properties if (schema.allOf) { return schema.allOf.reduce((acc, subSchema) => { const props = extractSchemaProperties(subSchema); return { ...acc, ...props }; }, {}); } if (schema.oneOf) { return schema.oneOf.reduce((acc, subSchema) => { const props = extractSchemaProperties(subSchema); return { ...acc, ...props }; }, {}); } // Handle anyOf - similar to oneOf if (schema.anyOf) { return schema.anyOf.reduce((acc, subSchema) => { const props = extractSchemaProperties(subSchema); return { ...acc, ...props }; }, {}); } if (schema.$ref) { console.warn('$ref found in schema, which may need to be resolved:', schema.$ref); return {}; } return {}; }; const ApiEndpointInput = (props) => { const [loadingSpec, setLoadingSpec] = useState(true); const [options, setOptions] = useState(props.options); const [specJson, setSpecJson] = useState(null); const [operationParams, setOperationParams] = useState({}); // Check if specUrl is an object (multiple specs) or string (single spec) const isMultiSpec = typeof props.specUrl === 'object' && !Array.isArray(props.specUrl); const [selectedSpecType, setSelectedSpecType] = useState(isMultiSpec ? Object.keys(props.specUrl)[0] || '' : null); const fetchOpenApiSpec = (specUrlOrType) => { setLoadingSpec(true); const url = isMultiSpec ? props.specUrl[specUrlOrType] : props.specUrl; openapiService .fetchSpecFromUrl(url) .then((response) => response.text()) .then((text) => { const format = url.endsWith('.json') ? 'json' : 'yaml'; openapiService.parseOpenapiSpec(text, format).then((data) => { setSpecJson(data); if (isMultiSpec) { // MODIFIED: Retain existing values instead of clearing them const currentParams = options?.params || { path: {}, query: {}, request: {}, }; // Keep existing values if the operation/path still exists in the new spec let newOperation = options?.operation; let newPath = options?.path; let newSelectedOperation = null; // Validate if the current operation/path exists in the new spec if (newPath && newOperation && data?.paths?.[newPath]?.[newOperation]) { newSelectedOperation = data.paths[newPath][newOperation]; } else { // Only clear if the operation/path doesn't exist in the new spec newOperation = null; newPath = null; } const newOptions = { ...options, operation: newOperation, path: newPath, selectedOperation: newSelectedOperation, params: currentParams, // Retain existing params specType: specUrlOrType, }; setOptions(newOptions); props.optionsChanged(newOptions); } setLoadingSpec(false); }); }); }; const getOperationKey = (operation, path) => { return `${operation}_${path}`; }; const changeOperation = (value) => { const operation = value.split('/', 2)[0]; const path = value.substring(value.indexOf('/')); if (options.operation && options.path) { const currentOperationKey = getOperationKey(options.operation, options.path); setOperationParams((prevState) => ({ ...prevState, [currentOperationKey]: options.params, })); } const newOperationKey = getOperationKey(operation, path); const savedParams = operationParams[newOperationKey] || { path: {}, query: {}, request: {}, }; const newOptions = { ...options, path, operation, selectedOperation: specJson.paths[path][operation], params: savedParams, }; setOptions(newOptions); props.optionsChanged(newOptions); }; const changeParam = (paramType, paramName, value) => { if (value === '') { removeParam(paramType, paramName); } else { let parsedValue = value; if (paramType === 'request') { try { parsedValue = JSON.parse(value); } catch (e) { console.error(`Invalid JSON for request param "${paramName}":`, e); parsedValue = value; } } const newOptions = { ...options, params: { ...options.params, [paramType]: { ...options.params[paramType], [paramName]: parsedValue, }, }, }; setOptions(newOptions); props.optionsChanged(newOptions); } }; const removeParam = (paramType, paramName) => { const newOptions = JSON.parse(JSON.stringify(options)); const newParams = { ...newOptions.params }; const newParamType = { ...newParams[paramType] }; delete newParamType[paramName]; newParams[paramType] = newParamType; newOptions.params = newParams; setOptions(newOptions); props.optionsChanged(newOptions); }; const renderOperationOption = (data) => { const path = data.displayLabel || data.value.substring(data.value.indexOf('/')); const operation = data.operation; const summary = data.summary; const isSelected = data.isSelected; if (path && operation) { return (
{operation.toUpperCase()}
{path}
{summary && !isSelected && ( {summary} )}
); } else { return 'Select an operation'; } }; const categorizeOperations = (operation, path, acc, category) => { const operationData = specJson.paths[path][operation]; const summary = operationData?.summary || ''; // Create searchable label that includes both path and summary const searchableLabel = summary ? `${path} ${summary}` : path; const option = { value: `${operation}${path}`, label: searchableLabel, name: path, operation: operation, summary: summary || null, displayLabel: path, // Keep original path for display }; const existingCategory = acc.find((obj) => obj.label === category); if (existingCategory) { existingCategory.options.push(option); } else { acc.push({ label: category, options: [option], }); } }; const computeOperationSelectionOptions = () => { const paths = specJson?.paths; if (isEmpty(paths)) return []; const pathGroups = Object.keys(paths).reduce((acc, path) => { const operations = Object.keys(paths[path]); const category = path.split('/')[2]; operations.forEach((operation) => categorizeOperations(operation, path, acc, category)); return acc; }, []); return pathGroups; }; const getRequestBodyProperties = () => { if (!options?.selectedOperation?.requestBody?.content) { return {}; } const contentTypes = Object.keys(options.selectedOperation.requestBody.content); if (contentTypes.length === 0) { return {}; } const contentType = contentTypes.includes('application/json') ? 'application/json' : contentTypes[0]; const schema = options.selectedOperation.requestBody.content[contentType]?.schema; return extractSchemaProperties(schema); }; useEffect(() => { const queryParams = { path: props.options?.params?.path ?? {}, query: props.options?.params?.query ?? {}, request: props.options?.params?.request ?? {}, }; setLoadingSpec(true); setOptions({ ...props.options, params: queryParams }); if (!isMultiSpec) { fetchOpenApiSpec(); } }, []); useEffect(() => { if (isMultiSpec && selectedSpecType) { fetchOpenApiSpec(selectedSpecType); } }, [selectedSpecType]); const specTypeOptions = isMultiSpec ? Object.keys(props.specUrl).map((key) => ({ value: key, label: key, })) : []; return (
{/* Render spec type dropdown only for multi-spec */} {isMultiSpec && (
changeOperation(value)} width={'100%'} useMenuPortal={true} customOption={renderOperationOption} styles={queryManagerSelectComponentStyle(props.darkMode, '100%')} useCustomStyles={true} filterOption={(option, inputValue) => { if (!inputValue) return true; const searchValue = inputValue.toLowerCase(); const pathMatch = option.data.displayLabel?.toLowerCase().includes(searchValue); const summaryMatch = option.data.summary?.toLowerCase().includes(searchValue); return pathMatch || summaryMatch; }} /> {options?.selectedOperation && ( )}
{options?.selectedOperation && (
)}
)} ); }; export default withTranslation()(ApiEndpointInput); ApiEndpointInput.propTypes = { options: PropTypes.object, specUrl: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), optionsChanged: PropTypes.func, darkMode: PropTypes.bool, t: PropTypes.func, }; const RenderParameterFields = ({ parameters, type, label, options, changeParam, removeParam, darkMode }) => { let filteredParams; if (type === 'request') { filteredParams = Object.keys(parameters || {}); } else { filteredParams = parameters?.filter((param) => param.in === type); } const paramLabelWithDescription = (param) => { return (
); }; const paramLabelWithoutDescription = (param) => { return ( ); }; const paramType = (param) => { return (
{type === 'query' && param?.schema?.anyOf && param?.schema?.anyOf.map((type, i) => i < param.schema?.anyOf.length - 1 ? type.type.substring(0, 3).toUpperCase() + '|' : type.type.substring(0, 3).toUpperCase() )} {(type === 'path' || (type === 'query' && !param?.schema?.anyOf)) && param?.schema?.type?.substring(0, 3).toUpperCase()} {type === 'request' && parameters[param]?.type?.substring(0, 3).toUpperCase()}
); }; const paramDetails = (param) => { return (
{(type === 'request' && parameters[param]?.description) || param?.description ? paramLabelWithDescription(param) : paramLabelWithoutDescription(param)} {(type === 'request' ? parameters[param]?.required : param.required) && ( * )} {paramType(param)}
); }; const inputField = (param) => { return ( { if (type === 'request') { changeParam(type, param, value); } else { changeParam(type, param.name, value); } }} height={'32px'} /> ); }; const clearButton = (param) => { const handleClear = () => { if (type === 'request') { removeParam(type, param); } else { removeParam(type, param.name); } }; return ( { if (e.key === 'Enter') { handleClear(); } }} tabIndex="0" > ); }; return ( filteredParams?.length > 0 && (
{label}
{filteredParams.map((param) => (
{paramDetails(param)}
{inputField(param)}
{((type === 'request' && options['params'][type][param]) || options['params'][type][param?.name]) && clearButton(param)}
))}
) ); }; RenderParameterFields.propTypes = { parameters: PropTypes.any, type: PropTypes.string, label: PropTypes.string, options: PropTypes.object, changeParam: PropTypes.func, removeParam: PropTypes.func, darkMode: PropTypes.bool, };