From f82bdfa975e0ee1f2af7eaaaaf5c599547105fdb Mon Sep 17 00:00:00 2001 From: Nisan Abeywickrama <29643986+nisan-abeywickrama@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:41:56 +0530 Subject: [PATCH] Add graphql api creation by SDL url and introspection --- .../Apis/Create/Components/DefaultAPIForm.jsx | 4 + .../Apis/Create/GraphQL/ApiCreateGraphQL.jsx | 19 +- .../Create/GraphQL/Steps/ProvideGraphQL.jsx | 383 ++++++++++++++---- .../APIDefinition/ImportDefinition.jsx | 19 +- .../Listing/Landing/Menus/GraphqlAPIMenu.jsx | 4 +- .../main/webapp/source/src/app/data/api.js | 47 ++- 6 files changed, 385 insertions(+), 91 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx index 3c59a2d6756..7b790250d68 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx @@ -160,6 +160,7 @@ export default function DefaultAPIForm(props) { const { onChange, onValidate, api, isAPIProduct, multiGateway, isWebSocket, children, appendChildrenBeforeEndpoint, hideEndpoint, + readOnlyAPIEndpoint, } = props; const [validity, setValidity] = useState({}); @@ -594,6 +595,7 @@ export default function DefaultAPIForm(props) { { }, api: {}, // Uncontrolled component isWebSocket: false, + readOnlyAPIEndpoint: null, }; DefaultAPIForm.propTypes = { api: PropTypes.shape({}), @@ -764,4 +767,5 @@ DefaultAPIForm.propTypes = { isWebSocket: PropTypes.shape({}), onChange: PropTypes.func.isRequired, onValidate: PropTypes.func, + readOnlyAPIEndpoint: PropTypes.string, }; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/ApiCreateGraphQL.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/ApiCreateGraphQL.jsx index bbde17fb9b4..13ddb518435 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/ApiCreateGraphQL.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/ApiCreateGraphQL.jsx @@ -81,11 +81,12 @@ export default function ApiCreateGraphQL(props) { case 'version': case 'gatewayType': case 'endpoint': + return { ...currentState, [action]: value }; case 'context': case 'isFormValid': return { ...currentState, [action]: value }; case 'inputType': - return { ...currentState, [action]: value, inputValue: value === 'url' ? '' : null }; + return { ...currentState, [action]: value, inputValue: value === ('url' || 'endpoint') ? '' : null }; case 'graphQLInfo': return { ...currentState, [action]: value }; case 'preSetAPI': @@ -145,6 +146,7 @@ export default function ApiCreateGraphQL(props) { endpoint, gatewayType, implementationType, + inputType, inputValue, graphQLInfo: { operations }, } = apiInputs; @@ -166,7 +168,6 @@ export default function ApiCreateGraphQL(props) { policies, operations, }; - const uploadMethod = 'file'; if (endpoint) { additionalProperties.endpointConfig = { endpoint_type: 'http', @@ -182,10 +183,14 @@ export default function ApiCreateGraphQL(props) { const apiData = { additionalProperties: JSON.stringify(additionalProperties), implementationType, - [uploadMethod]: uploadMethod, - file: inputValue, }; + if (inputType === 'file') { + apiData.file = inputValue; + } else if (inputType === 'url' || inputType === 'endpoint') { + apiData.schema = apiInputs.graphQLInfo.graphQLSchema.schemaDefinition; + } + newApi .importGraphQL(apiData) .then((response) => { @@ -222,13 +227,14 @@ export default function ApiCreateGraphQL(props) { @@ -272,6 +278,7 @@ export default function ApiCreateGraphQL(props) { multiGateway={multiGateway} api={apiInputs} isAPIProduct={false} + readOnlyAPIEndpoint={apiInputs.inputType==='endpoint' ? apiInputs.endpoint : null} /> )} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx index cfb959a4243..ab67899fb35 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx @@ -15,13 +15,13 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import Grid from '@mui/material/Grid'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import CircularProgress from '@mui/material/CircularProgress'; import Button from '@mui/material/Button'; import List from '@mui/material/List'; @@ -36,6 +36,10 @@ import ListItemText from '@mui/material/ListItemText'; import API from 'AppData/api'; import DropZoneLocal, { humanFileSize } from 'AppComponents/Shared/DropZoneLocal'; import Banner from 'AppComponents/Shared/Banner'; +import { debounce, FormControlLabel, InputAdornment, Radio, RadioGroup, TextField } from '@mui/material'; +import APIValidation from 'AppData/APIValidation'; +import CheckIcon from '@mui/icons-material/Check'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; const PREFIX = 'ProvideGraphQL'; @@ -56,15 +60,18 @@ const Root = styled('div')(( /** * Sub component of API Create using GraphQL UI, This is handling the taking input of GraphQL file or URL from the user - * In the create API using OpenAPI wizard first step out of 2 steps + * In the create API using GraphQL wizard first step out of 2 steps * @export * @param {*} props * @returns {React.Component} @inheritdoc */ export default function ProvideGraphQL(props) { const { apiInputs, inputsDispatcher, onValidate } = props; - const { inputValue } = apiInputs; - + const { inputType, inputValue } = apiInputs; + const isURLInput = inputType === ProvideGraphQL.INPUT_TYPES.URL || inputType === ''; + const isFileInput = inputType === ProvideGraphQL.INPUT_TYPES.FILE; + const isEndpointInput = inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT; + const intl = useIntl(); // If valid value is `null`,that means valid, else an error object will be there const [isValid, setValidity] = useState({ file: null }); const [isValidating, setIsValidating] = useState(false); @@ -106,31 +113,161 @@ export default function ProvideGraphQL(props) { }); } + function reset() { + inputsDispatcher({ action: 'importingContent', value: null }); + inputsDispatcher({ action: 'inputValue', value: null }); + inputsDispatcher({ action: 'isFormValid', value: false }); + inputsDispatcher({ action: 'endpoint', value: '' }); + } + + const isInvalidURL = Boolean(isValid.url); + let urlStateEndAdornment = null; + if (isValidating) { + urlStateEndAdornment = ( + + + + ); + } else if (isValid.url !== undefined) { + if (isInvalidURL) { + urlStateEndAdornment = ( + + + + ); + } else { + urlStateEndAdornment = ( + + + + ); + } + } + + const debouncedValidateURLOrEndpoint = useCallback( + debounce((newURL) => { + const handleResponse = (response) => { + const { + body: { graphQLInfo }, + } = response; + const isValidURL = response.body.isValid; + if (isValidURL) { + inputsDispatcher({ action: 'graphQLInfo', value: graphQLInfo }); + setValidity({ isValidURL, file: null }); + } else { + setValidity({ isValidURL, file: { message: 'GraphQL content validation failed!' } }); + } + onValidate(isValidURL); + setIsValidating(false); + }; + + const handleError = (error) => { + setValidity({ url: { message: error.message } }); + onValidate(false); + setIsValidating(false); + console.error(error); + } + + const validationFunction = inputType === ProvideGraphQL.INPUT_TYPES.URL + ? () => API.validateGraphQLByUrl(newURL) + : () => API.validateGraphQLByEndpoint(newURL, { useIntrospection: true }); + + validationFunction() + .then(handleResponse) + .catch(handleError); + + if(inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT){ + inputsDispatcher({ action: 'endpoint', value: newURL }); + } + }, 750), + [inputType], + ); + + function validateURL(value) { + const state = APIValidation.url.required().validate(value).error; + if (state === null) { + setIsValidating(true); + debouncedValidateURLOrEndpoint(apiInputs.inputValue); + } else { + setValidity({ ...isValid, url: state }); + onValidate(false); + } + } + useEffect(() => { if (inputValue) { - onDrop([inputValue]); + if (inputType === ProvideGraphQL.INPUT_TYPES.FILE) { + onDrop([inputValue]); + } else if (inputType === ProvideGraphQL.INPUT_TYPES.URL || + inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT) { + validateURL(inputValue); + } } - }, [inputValue]); + }, [inputType, inputValue]); + + useEffect(() => { + reset(); + }, [inputType]); + const accept = '.graphql,text/plain'; return ( - {!apiInputs.inputValue && ( - - - - <> - * - {' '} - - - - - - )} + + + + <> + * + {' '} + + + + inputsDispatcher({ + action: 'inputType', + value: event.target.value + })} + > + } + label={intl.formatMessage({ + id: 'Apis.Create.GraphQL.create.api.form.file.label', + defaultMessage: 'GraphQL File/Archive', + })} + aria-label='GraphQL File/Archive' + id='graphql-file-select-radio' + /> + } + label={intl.formatMessage({ + id: 'Apis.Create.GraphQL.create.api.form.url.label', + defaultMessage: 'GraphQL SDL URL', + })} + id='graphql-url-select-radio' + /> + } + label={intl.formatMessage({ + id: 'Apis.Create.GraphQL.create.api.form.endpoint.label', + defaultMessage: 'GraphQL Endpoint', + })} + aria-label='GraphQL Endpoint' + id='graphql-endpoint-select-radio' + /> + + + {isValid.file && ( @@ -145,64 +282,146 @@ export default function ProvideGraphQL(props) { )} - {apiInputs.inputValue ? ( - - - - - - - - - - { - inputsDispatcher({ action: 'inputValue', value: null }); - inputsDispatcher({ action: 'isFormValid', value: false }); - }} - data-testid='btn-delete-imported-file' - size='large'> - - - - - - ) : ( - - {isValidating ? () - : ([ - , accept }} - />, - , - ] + + { + inputsDispatcher({ action: 'inputValue', value: null }); + inputsDispatcher({ action: 'isFormValid', value: false }); + }} + data-testid='btn-delete-imported-file' + size='large'> + + + + + + ) : ( + + {isValidating ? () + : ([ + , accept }} + />, + , + ] + )} + + )} + + )} + {isURLInput && ( + inputsDispatcher({ action: 'inputValue', value })} + value={apiInputs.inputValue} + InputLabelProps={{ + shrink: true, + }} + InputProps={{ + onBlur: ({ target: { value } }) => { + validateURL(value); + }, + endAdornment: urlStateEndAdornment, + }} + // 'Give the URL of GraphQL endpoint' + helperText={(isValid.url && isValid.url.message) + || ( + )} - + error={isInvalidURL} + data-testid='graphql-add-form-url' + /> + )} + {isEndpointInput && ( + inputsDispatcher({ action: 'inputValue', value })} + value={apiInputs.inputValue} + InputLabelProps={{ + shrink: true, + }} + InputProps={{ + onBlur: ({ target: { value } }) => { + validateURL(value); + }, + endAdornment: urlStateEndAdornment, + }} + helperText={(isValid.url && isValid.url.message) + || ( + + )} + error={isInvalidURL} + data-testid='graphql-add-form-endpoint' + /> )} @@ -213,10 +432,18 @@ export default function ProvideGraphQL(props) { ProvideGraphQL.defaultProps = { onValidate: () => {}, }; + +ProvideGraphQL.INPUT_TYPES = { + URL: 'url', + ENDPOINT: 'endpoint', + FILE: 'file', +}; + ProvideGraphQL.propTypes = { apiInputs: PropTypes.shape({ type: PropTypes.string, inputType: PropTypes.string, + inputValue: PropTypes.string }).isRequired, inputsDispatcher: PropTypes.func.isRequired, onValidate: PropTypes.func, diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/ImportDefinition.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/ImportDefinition.jsx index 656342506dc..be92a4e94bc 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/ImportDefinition.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/ImportDefinition.jsx @@ -253,10 +253,20 @@ export default function ImportDefinition(props) { function updateGraphQLSchema() { const { inputValue, + inputType, } = apiInputs; - const promisedValidation = API.validateGraphQLFile(inputValue); - promisedValidation + let validationPromise; + + if (inputType === ProvideGraphQL.INPUT_TYPES.FILE) { + validationPromise = API.validateGraphQLFile(inputValue); + } else if (inputType === ProvideGraphQL.INPUT_TYPES.URL) { + validationPromise = API.validateGraphQLByUrl(inputValue); + } else if (inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT) { + validationPromise = API.validateGraphQLByEndpoint(inputValue, { useIntrospection: true }); + } + + validationPromise .then((response) => { const { isValid, graphQLInfo } = response.obj; if (isValid === true) { @@ -446,18 +456,19 @@ export default function ImportDefinition(props) { defaultMessage='Cancel' /> - + }