From 75018b61573ccb0f7dc2c63221e336a2f3646618 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Sun, 26 Jan 2025 23:39:48 +0530 Subject: [PATCH 01/15] Add admin portal changes for model listing under vendor --- .../main/webapp/site/public/locales/en.json | 1 + .../main/webapp/site/public/locales/fr.json | 1 + .../components/AiVendors/AddEditAiVendor.jsx | 92 ++++++++++++++++++- .../main/webapp/source/src/app/data/api.js | 2 + 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/portals/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index 4879ec485de..817d26c0aa6 100644 --- a/portals/admin/src/main/webapp/site/public/locales/en.json +++ b/portals/admin/src/main/webapp/site/public/locales/en.json @@ -206,6 +206,7 @@ "AiVendors.AddEditAiVendor.form.add": "Add", "AiVendors.AddEditAiVendor.form.cancel": "Cancel", "AiVendors.AddEditAiVendor.form.connectorType": "Connector Type", + "AiVendors.AddEditAiVendor.form.connectorType.help": "Connector Type for AI/LLM Vendor", "AiVendors.AddEditAiVendor.form.description": "Description", "AiVendors.AddEditAiVendor.form.description.help": "Description of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.displayName.help": "API Version of the AI/LLM Vendor.", diff --git a/portals/admin/src/main/webapp/site/public/locales/fr.json b/portals/admin/src/main/webapp/site/public/locales/fr.json index 4879ec485de..817d26c0aa6 100644 --- a/portals/admin/src/main/webapp/site/public/locales/fr.json +++ b/portals/admin/src/main/webapp/site/public/locales/fr.json @@ -206,6 +206,7 @@ "AiVendors.AddEditAiVendor.form.add": "Add", "AiVendors.AddEditAiVendor.form.cancel": "Cancel", "AiVendors.AddEditAiVendor.form.connectorType": "Connector Type", + "AiVendors.AddEditAiVendor.form.connectorType.help": "Connector Type for AI/LLM Vendor", "AiVendors.AddEditAiVendor.form.description": "Description", "AiVendors.AddEditAiVendor.form.description.help": "Description of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.displayName.help": "API Version of the AI/LLM Vendor.", diff --git a/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx b/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx index 6ac150d0f31..9b193f037bf 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx @@ -33,6 +33,7 @@ import Grid from '@mui/material/Grid'; import Select from '@mui/material/Select'; import { styled } from '@mui/material/styles'; import TextField from '@mui/material/TextField'; +import { MuiChipsInput } from 'mui-chips-input'; import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; import AIAPIDefinition from './AIAPIDefinition'; @@ -62,6 +63,7 @@ function reducer(state, newValue) { case 'name': case 'apiVersion': case 'description': + case 'modelList': case 'apiDefinition': return { ...state, [field]: value }; case 'model': @@ -141,6 +143,7 @@ export default function AddEditAiVendor(props) { authHeader: '', }, apiDefinition: '', + modelList: [], }); const [state, dispatch] = useReducer(reducer, initialState); @@ -166,8 +169,8 @@ export default function AddEditAiVendor(props) { description: aiVendorBody.description || '', configurations: JSON.parse(aiVendorBody.configurations), apiDefinition: aiVendorBody.apiDefinition || '', + modelList: aiVendorBody.modelList || [], }; - if (newState.configurations.authQueryParameter) { setAuthSource('authQueryParameter'); } else if (newState.configurations.authHeader) { @@ -289,7 +292,9 @@ export default function AddEditAiVendor(props) { const newState = { ...state, configurations: updatedConfigurations, + modelList: state.modelList.join(','), }; + if (id) { await new API().updateAiVendor(id, { ...newState, apiDefinition: file }); Alert.success(`${state.name} ${intl.formatMessage({ @@ -297,12 +302,34 @@ export default function AddEditAiVendor(props) { defaultMessage: ' - AI/LLM Vendor edited successfully.', })}`); } else { + // const aiVendorBody = new FormData(); + // Object.entries(newState).forEach(([key, value]) => { + // if (key === 'modelList' && Array.isArray(value)) { + // value.forEach((model) => { + // console.log('key', key); + // console.log('model', model); + // aiVendorBody.append(key, model); + // }); + // } else { + // aiVendorBody.append(key, value); + // } + // }); + // if (file) { + // aiVendorBody.append('apiDefinition', file); + // } + // console.log('aiVendorBody', aiVendorBody); + // await new API().addAiVendor(aiVendorBody); + // // await new API().addAiVendor({ ...newState, apiDefinition: file }); await new API().addAiVendor({ ...newState, apiDefinition: file }); Alert.success(`${state.name} ${intl.formatMessage({ id: 'AiVendor.add.success.msg', defaultMessage: ' - AI/LLM Vendor added successfully.', })}`); } + + // const modelListAsArray = state.modelList.split(','); + // dispatch({ field: 'modelList', value: modelListAsArray }); + setSaving(false); history.push('/settings/ai-vendors/'); } catch (e) { @@ -770,7 +797,7 @@ export default function AddEditAiVendor(props) { state.configurations.connectorType, validating, ) || intl.formatMessage({ - id: 'AiVendors.AddEditAiVendor.form.name.help', + id: 'AiVendors.AddEditAiVendor.form.connectorType.help', defaultMessage: 'Connector Type for AI/LLM Vendor', })} /> @@ -781,6 +808,67 @@ export default function AddEditAiVendor(props) { + + + + + + + + + + + { + state.modelList.push(model); + }} + onDeleteChip={(model) => { + const filteredModelList = state.modelList.filter( + (modelItem) => modelItem !== model, + ); + dispatch({ field: 'modelList', value: filteredModelList }); + }} + placeholder={intl.formatMessage({ + id: 'AiVendors.AddEditAiVendor.modelList.placeholder', + defaultMessage: 'Type Model name and press Enter', + })} + data-testid='ai-vendor-llm-model-list' + helperText={( +
+ {intl.formatMessage({ + id: 'AiVendors.AddEditAiVendor.modelList.help', + defaultMessage: 'Type available models and ' + + 'press enter/return to add them.', + })} +
+ )} + /> +
+
+ + + + + + ) : ( + - - ) : ( - + + + + - - + + )} )} ) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx new file mode 100644 index 00000000000..7497ba01429 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +// import { FormattedMessage } from 'react-intl'; +import EndpointTextField from './EndpointTextField'; +// import GenericEndpoint from '../GenericEndpoint'; + +const EndpointSection = () => { + return ( +
+ + {/* + )} + className={classes. + defaultEndpointWrapper} + endpointURL={getEndpoints + ( + 'sandbox_endpoints' + )} + type='' + index={0} + category='sandbox_endpoints' + editEndpoint={editEndpoint} + esCategory='sandbox' + setAdvancedConfigOpen= + {toggleAdvanceConfig} + setESConfigOpen= + {toggleEndpointSecurityConfig} + apiId={api.id} + /> + {endpointCategory.sandbox && + api.subtypeConfiguration?.subtype === 'AIAPI' && + (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && + ()} */} +
+ ); +}; + +export default EndpointSection; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx new file mode 100644 index 00000000000..589b5622cf7 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { styled } from '@mui/material/styles'; +import { + // Grid, + Tooltip, + InputAdornment, + IconButton, Icon, +} from '@mui/material'; +// import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import TextField from '@mui/material/TextField'; +// import Autocomplete from 'AppComponents/Shared/Autocomplete'; +// import { isRestricted } from 'AppData/AuthManager'; + +const PREFIX = 'EndpointTextField'; + +const classes = { + endpointInputWrapper: `${PREFIX}-endpointInputWrapper`, + textField: `${PREFIX}-textField`, + input: `${PREFIX}-input`, + iconButton: `${PREFIX}-iconButton` +}; + +const Root = styled('div')(( + { + theme + } +) => ({ + [`& .${classes.endpointInputWrapper}`]: { + width: '100%', + display: 'flex', + justifyContent: 'space-between', + }, + + [`& .${classes.textField}`]: { + width: '100%', + }, + + [`& .${classes.input}`]: { + marginLeft: theme.spacing(1), + flex: 1, + }, + + [`& .${classes.iconButton}`]: { + padding: theme.spacing(1), + } +})); + +const EndpointTextField = () => { + const intl = useIntl(); + + return ( + + + <> + setAdvancedConfigOpen(index, type, category)} + // disabled={(isRestricted(['apim:api_create'], api))} + size='large'> + + )} + > + + settings + + + + {/* setESConfigOpen(type, esCategory)} + // disabled={(isRestricted(['apim:api_create'], api))} + size='large'> + + )} + > + + security + + + */} + + + ), + }} + /> + + ); +}; + +export default EndpointTextField; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/api.js b/portals/publisher/src/main/webapp/source/src/app/data/api.js index 9a7fe20b335..5d940ae92d8 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/api.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/api.js @@ -3580,6 +3580,109 @@ class API extends Resource { }); } + /** + * Get all endpoints of the API + * @param {String} apiId UUID of the API + * @param {number} limit Limit of the endpoints list which needs to be retrieved + * @param {number} offset Offset of the endpoints list which needs to be retrieved + * @returns {Promise} Promise containing the list of endpoints of the API + */ + static getApiEndpoints(apiId, limit = null, offset = 0) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['API Endpoints'].getApiEndpoints( + { + apiId: apiId, + limit, + offset, + }, + this._requestMetaData(), + ); + }); + } + + /** + * Add an endpoint to the API + * @param {String} apiId UUID of the API + * @param {Object} endpointBody Endpoint object to be added + * @returns {Promise} Promise containing the added endpoint object + */ + static addApiEndpoint(apiId, endpointBody) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['API Endpoints'].addApiEndpoint( + { + apiId: apiId, + }, + { + requestBody: endpointBody, + }, + this._requestMetaData(), + ); + }); + } + + // /** + // * Retrieve an endpoint of the API + // * @param {String} apiId UUID of the API + // * @param {String} endpointId UUID of the endpoint + // * @returns {Promise} Promise containing the requested endpoint + // */ + // static getApiEndpoint(apiId, endpointId) { + // const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + // return restApiClient.then(client => { + // return client.apis['API Endpoints'].getApiEndpoint( + // { + // apiId: apiId, + // endpointId: endpointId, + // }, + // this._requestMetaData(), + // ); + // }); + // } + + /** + * Update an endpoint of the API + * @param {String} apiId UUID of the API + * @param {String} endpointId UUID of the endpoint + * @param {Object} endpointBody Updated endpoint object + * @returns {Promise} Promise containing the updated endpoint + */ + static updateApiEndpoint(apiId, endpointId, endpointBody) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['API Endpoints'].updateApiEndpoint( + { + apiId: apiId, + endpointId: endpointId, + }, + { + requestBody: endpointBody, + }, + this._requestMetaData(), + ); + }); + } + + /** + * Delete an endpoint of the API + * @param {String} apiId UUID of the API + * @param {String} endpointId UUID of the endpoint + * @returns {Promise} Promise containing the deleted endpoint + */ + static deleteApiEndpoint(apiId, endpointId) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['API Endpoints'].deleteApiEndpoint( + { + apiId: apiId, + endpointId: endpointId, + }, + this._requestMetaData(), + ); + }); + } + } API.CONSTS = { From 0c7d82d003a9cf294eb4d78d0ca3fe72efce8ec1 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Wed, 5 Feb 2025 03:05:37 +0530 Subject: [PATCH 03/15] Update endpoints page to support multiple endpoints for AI APIs --- .../Apis/Details/Endpoints/AIEndpointAuth.jsx | 135 ++-- .../Apis/Details/Endpoints/AIEndpoints.jsx | 171 +++--- .../Details/Endpoints/EndpointOverview.jsx | 29 +- .../Apis/Details/Endpoints/Endpoints.jsx | 8 - .../MultiEndpointComponents/EndpointCard.jsx | 580 ++++++++++++++++++ .../EndpointSection.jsx | 65 -- .../EndpointTextField.jsx | 81 +-- .../GeneralEndpointConfigurations.jsx | 29 + .../webapp/source/src/app/data/Constants.js | 11 + 9 files changed, 778 insertions(+), 331 deletions(-) create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx delete mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx index 36e31c26844..6f9cb91d958 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx @@ -20,18 +20,23 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { isRestricted } from 'AppData/AuthManager'; import { FormattedMessage, useIntl } from 'react-intl'; -import { Icon, TextField, InputAdornment, IconButton } from '@mui/material'; +import { Icon, TextField, InputAdornment, IconButton, Grid } from '@mui/material'; import CONSTS from 'AppData/Constants'; +/** + * AI Endpoint Auth component + * @param {*} props properties + * @returns {JSX} AI Endpoint Auth component + */ export default function AIEndpointAuth(props) { - const { api, saveEndpointSecurityConfig, apiKeyParamConfig, isProduction } = props; + const { api, endpoint, apiKeyParamConfig, isProduction, saveEndpointSecurityConfig } = props; const intl = useIntl(); const [apiKeyIdentifier] = useState(apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParam); const [apiKeyIdentifierType] = useState(apiKeyParamConfig.authHeader ? 'HEADER' : 'QUERY_PARAMETER'); const [apiKeyValue, setApiKeyValue] = useState( - api.endpointConfig?.endpoint_security?.[isProduction ? 'production' : 'sandbox']?.apiKeyValue === '' + endpoint.endpointConfig?.endpoint_security?.[isProduction ? 'production' : 'sandbox']?.apiKeyValue === '' ? '********' : null ); @@ -43,7 +48,7 @@ export default function AIEndpointAuth(props) { useEffect(() => { - let newApiKeyValue = api.endpointConfig?.endpoint_security?.[isProduction ? + let newApiKeyValue = endpoint.endpointConfig?.endpoint_security?.[isProduction ? 'production' : 'sandbox']?.apiKeyValue === '' ? '' : null; if ((llmProviderName === 'MistralAI' || llmProviderName === 'OpenAI') && @@ -98,65 +103,69 @@ export default function AIEndpointAuth(props) { return ( <> - - ) : ( - - )} - id={'api-key-id-' + (isProduction ? '-production' : '-sandbox')} - sx={{ width: '49%', mr: 2 }} - value={apiKeyIdentifier} - placeholder={apiKeyIdentifier} - variant='outlined' - margin='normal' - required - /> - } - id={'api-key-value' + (isProduction ? '-production' : '-sandbox')} - sx={{ width: '49%' }} - value={apiKeyValue} - placeholder={intl.formatMessage({ - id: 'Apis.Details.Endpoints.Security.api.key.value.placeholder', - defaultMessage: 'Enter API Key', - })} - onChange={handleApiKeyChange} - onBlur={handleApiKeyBlur} - error={!apiKeyValue} - helperText={!apiKeyValue ? ( - - ) : ''} - variant='outlined' - margin='normal' - required - type={showApiKey ? 'text' : 'password'} - InputProps={{ - endAdornment: ( - - - - {showApiKey ? 'visibility' : 'visibility_off'} - - - - ), - }} - /> + + + ) : ( + + )} + id={'api-key-id-' + endpoint.id} + value={apiKeyIdentifier} + placeholder={apiKeyIdentifier} + sx={{ width: '100%', minHeight: '80px' }} + variant='outlined' + margin='normal' + required + /> + + + } + id={'api-key-value' + endpoint.id} + value={apiKeyValue} + placeholder={intl.formatMessage({ + id: 'Apis.Details.Endpoints.Security.api.key.value.placeholder', + defaultMessage: 'Enter API Key', + })} + sx={{ width: '100%', minHeight: '80px' }} + onChange={handleApiKeyChange} + onBlur={handleApiKeyBlur} + error={!apiKeyValue} + helperText={!apiKeyValue ? ( + + ) : ''} + variant='outlined' + margin='normal' + required + type={showApiKey ? 'text' : 'password'} + InputProps={{ + endAdornment: ( + + + + {showApiKey ? 'visibility' : 'visibility_off'} + + + + ), + }} + /> + ); } diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx index 1ef7c9015ec..3d10710cf83 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx @@ -27,7 +27,7 @@ import { FormattedMessage, useIntl } from 'react-intl'; import { Progress } from 'AppComponents/Shared'; import API from 'AppData/api'; import Alert from 'AppComponents/Shared/Alert'; -import EndpointSection from './MultiEndpointComponents/EndpointSection'; +import EndpointCard from './MultiEndpointComponents/EndpointCard'; const PREFIX = 'AIEndpoints'; @@ -153,12 +153,14 @@ const Root = styled('div')(( const AIEndpoints = ({ - api + api, + apiKeyParamConfig, + editEndpoint, }) => { const [productionEndpoints, setProductionEndpoints] = useState([]); const [sandboxEndpoints, setSandboxEndpoints] = useState([]); const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); + // const [saving, setSaving] = useState(false); const intl = useIntl(); @@ -190,99 +192,33 @@ const AIEndpoints = ({ fetchEndpoints(); }, []); - const addEndpoint = (endpointBody) => { - setSaving(true); - const addEndpointPromise = API.addEndpoint(api.id, endpointBody); - addEndpointPromise - .then((response) => { - const newEndpoint = response.body; - - if (newEndpoint.environment === 'PRODUCTION') { - setProductionEndpoints(prev => [...prev, newEndpoint]); - } else if (newEndpoint.environment === 'SANDBOX') { - setSandboxEndpoints(prev => [...prev, newEndpoint]); - } - - Alert.success(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.add.success', - defaultMessage: 'Endpoint added successfully!', - })); - }).catch((error) => { - console.error(error); - Alert.error(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.add.error', - defaultMessage: 'Something went wrong while adding the endpoint', - })); - }).finally(() => { - setSaving(false); - }); - }; + // const addEndpoint = (endpointBody) => { + // setSaving(true); + // const addEndpointPromise = API.addEndpoint(api.id, endpointBody); + // addEndpointPromise + // .then((response) => { + // const newEndpoint = response.body; + + // if (newEndpoint.environment === 'PRODUCTION') { + // setProductionEndpoints(prev => [...prev, newEndpoint]); + // } else if (newEndpoint.environment === 'SANDBOX') { + // setSandboxEndpoints(prev => [...prev, newEndpoint]); + // } - const updateEndpoint = (endpointId, endpointBody) => { - setSaving(true); - const updateEndpointPromise = API.updateEndpoint(api.id, endpointId, endpointBody); - updateEndpointPromise - .then((response) => { - const updatedEndpoint = response.body; - - if (updatedEndpoint.environment === 'PRODUCTION') { - setProductionEndpoints(prev => - prev.map(endpoint => ( - endpoint.id === endpointId - ? { ...endpoint, ...updatedEndpoint } - : endpoint - )) - ); - } else if (updatedEndpoint.environment === 'SANDBOX') { - setSandboxEndpoints(prev => - prev.map(endpoint => ( - endpoint.id === endpointId - ? { ...endpoint, ...updatedEndpoint } - : endpoint - )) - ); - } - - Alert.success(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.update.success', - defaultMessage: 'Endpoint updated successfully!', - })); - }).catch((error) => { - console.error(error); - Alert.error(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.update.error', - defaultMessage: 'Something went wrong while updating the endpoint', - })); - }).finally(() => { - setSaving(false); - }); - }; - - const deleteEndpoint = (endpointId, environment) => { - setSaving(true); - const deleteEndpointPromise = API.deleteEndpoint(api.id, endpointId); - deleteEndpointPromise - .then(() => { - if (environment === 'PRODUCTION') { - setProductionEndpoints(prev => prev.filter(endpoint => endpoint.id !== endpointId)); - } else if (environment === 'SANDBOX') { - setSandboxEndpoints(prev => prev.filter(endpoint => endpoint.id !== endpointId)); - } - - Alert.success(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.delete.success', - defaultMessage: 'Endpoint deleted successfully!', - })); - }).catch((error) => { - console.error(error); - Alert.error(intl.formatMessage({ - id: 'Apis.Details.Endpoints.endpoints.delete.error', - defaultMessage: 'Something went wrong while deleting the endpoint', - })); - }).finally(() => { - setSaving(false); - }); - }; + // Alert.success(intl.formatMessage({ + // id: 'Apis.Details.Endpoints.endpoints.add.success', + // defaultMessage: 'Endpoint added successfully!', + // })); + // }).catch((error) => { + // console.error(error); + // Alert.error(intl.formatMessage({ + // id: 'Apis.Details.Endpoints.endpoints.add.error', + // defaultMessage: 'Something went wrong while adding the endpoint', + // })); + // }).finally(() => { + // setSaving(false); + // }); + // }; if (loading) { return ; @@ -290,7 +226,7 @@ const AIEndpoints = ({ return ( - + @@ -299,7 +235,12 @@ const AIEndpoints = ({ defaultMessage='Primary Endpoints' /> - + {console.log('productionEndpoints', productionEndpoints)} + {console.log('sandboxEndpoints', sandboxEndpoints)} + {/* */} @@ -310,7 +251,18 @@ const AIEndpoints = ({ defaultMessage='Production Endpoints' /> - + {productionEndpoints.map((endpoint) => ( + + ))} @@ -321,8 +273,31 @@ const AIEndpoints = ({ defaultMessage='Sandbox Endpoints' /> + {sandboxEndpoints.map((endpoint) => ( + + ))} + + + + + {/* */} + ); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/EndpointOverview.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/EndpointOverview.jsx index 7e3da732a4d..4d6fc957f14 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/EndpointOverview.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/EndpointOverview.jsx @@ -54,9 +54,6 @@ import EndpointSecurity from './GeneralConfiguration/EndpointSecurity'; import Credentials from './AWSLambda/Credentials.jsx'; import ServiceEndpoint from './ServiceEndpoint'; import CustomBackend from './CustomBackend'; -import { API_SECURITY_KEY_TYPE_PRODUCTION } from '../Configuration/components/APISecurity/components/apiSecurityConstants'; -import { API_SECURITY_KEY_TYPE_SANDBOX } from '../Configuration/components/APISecurity/components/apiSecurityConstants'; -import AIEndpointAuth from './AIEndpointAuth'; const PREFIX = 'EndpointOverview'; @@ -382,10 +379,6 @@ function EndpointOverview(props) { return ''; }; - const updateEndpointConfig = (type) => { - - } - const handleOnChangeEndpointCategoryChange = (category) => { let endpointConfigCopy = cloneDeep(endpointConfig); if (category === 'prod') { @@ -1075,7 +1068,6 @@ function EndpointOverview(props) { ) : ( - <> - {api.subtypeConfiguration?.subtype === 'AIAPI' && // eslint-disable-line - (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && // eslint-disable-line - ()} )} {endpointType.key === 'prototyped' ?
@@ -1233,7 +1216,7 @@ function EndpointOverview(props) { ) : ( - <> - {endpointCategory.sandbox && // eslint-disable-line - api.subtypeConfiguration?.subtype === 'AIAPI' && // eslint-disable-line - (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && // eslint-disable-line - ()} // eslint-disable-line )}
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx index 2d2624b3194..97741ac9ecd 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx @@ -744,14 +744,6 @@ function Endpoints(props) { onChangeAPI={apiDispatcher} endpointsDispatcher={apiDispatcher} saveAndRedirect={saveAndRedirect} - sandBoxBackendList={sandBoxBackendList} - setSandBoxBackendList={setSandBoxBackendList} - productionBackendList={productionBackendList} - setProductionBackendList={setProductionBackendList} - isValidSequenceBackend={isValidSequenceBackend} - setIsValidSequenceBackend={setIsValidSequenceBackend} - isCustomBackendSelected={isCustomBackendSelected} - setIsCustomBackendSelected={setIsCustomBackendSelected} apiKeyParamConfig={apiKeyParamConfig} /> ))} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx new file mode 100644 index 00000000000..77238ce6579 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx @@ -0,0 +1,580 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useReducer, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Grid'; +import { isRestricted } from 'AppData/AuthManager'; +import { + Icon, + IconButton, + InputAdornment, + TextField, + Tooltip, + Chip, + Button, + Dialog, + DialogTitle, + DialogContent, + Typography, +} from '@mui/material'; +import API from 'AppData/api'; +import CircularProgress from '@mui/material/CircularProgress'; +// import EndpointTextField from './EndpointTextField'; +import CONSTS from 'AppData/Constants'; +import { green } from '@mui/material/colors'; +import Alert from 'AppComponents/Shared/Alert'; +import AIEndpointAuth from '../AIEndpointAuth'; +// import GenericEndpoint from '../GenericEndpoint'; + +const PREFIX = 'EndpointCard'; + +const classes = { + endpointCardWrapper: `${PREFIX}-endpointCardWrapper`, + textField: `${PREFIX}-textField`, + urlTextField: `${PREFIX}-urlTextField`, + btn: `${PREFIX}-btn`, + iconButton: `${PREFIX}-iconButton`, + iconButtonValid: `${PREFIX}-iconButtonValid`, + endpointValidChip: `${PREFIX}-endpointValidChip`, + endpointInvalidChip: `${PREFIX}-endpointInvalidChip`, + endpointErrorChip: `${PREFIX}-endpointErrorChip`, +}; + +const Root = styled(Grid)(({ theme }) => ({ + [`& .${classes.endpointCardWrapper}`]: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + + [`& .${classes.textField}`]: { + width: '100%', + minHeight: '80px', + }, + + [`& .${classes.btn}`]: { + marginRight: theme.spacing(0.5), + }, + + [`& .${classes.iconButton}`]: { + padding: theme.spacing(1), + }, + + [`& .${classes.iconButtonValid}`]: { + padding: theme.spacing(1), + color: green[500], + }, + + [`& .${classes.endpointValidChip}`]: { + color: 'green', + border: '1px solid green', + }, + + [`& .${classes.endpointInvalidChip}`]: { + color: '#ffd53a', + border: '1px solid #ffd53a', + }, + + [`& .${classes.endpointErrorChip}`]: { + color: 'red', + border: '1px solid red', + }, +})); + +/** + * Reducer to manage endpoint state + * @param {JSON} state state + * @param {JSON} param1 field and value + * @returns {Promise} promised State + */ +function endpointReducer(state, { field, value }) { + switch (field) { + case 'name': + return { ...state, [field]: value }; + case 'updateProductionEndpointUrl': + return { + ...state, + endpointConfig: { + ...state.endpointConfig, + production_endpoints: { url: value }, + }, + } + case 'updateSandboxEndpointUrl': + return { + ...state, + endpointConfig: { + ...state.endpointConfig, + sandbox_endpoints: { url: value }, + }, + } + case 'updateEndpointSecurity': + return { + ...state, + endpointConfig: { + ...state.endpointConfig, + endpoint_security: { + ...state.endpointConfig.endpoint_security, + ...value + }, + }, + } + default: + return state; + } +} + +const EndpointCard = ({ + api, + endpoint, + name, + apiKeyParamConfig, + category, + setProductionEndpoints, + setSandboxEndpoints, +}) => { + const [isEndpointValid, setIsEndpointValid] = useState(); + const [statusCode, setStatusCode] = useState(''); + const [isUpdating, setUpdating] = useState(false); + const [isErrorCode, setIsErrorCode] = useState(false); + const [endpointUrl, setEndpointUrl] = useState(''); + const [advancedConfigOpen, setAdvancedConfigOpen] = useState(false); + const [isEndpointUpdating, setEndpointUpdating] = useState(false); + const [isEndpointDeleting, setEndpointDeleting] = useState(false); + const [state, dispatch] = useReducer(endpointReducer, endpoint || CONSTS.DEFAULT_ENDPOINT); + const iff = (condition, then, otherwise) => (condition ? then : otherwise); + const intl = useIntl(); + + useEffect(() => { + try { + if (state.environment === CONSTS.ENVIRONMENTS.production) { + setEndpointUrl(state.endpointConfig.production_endpoints?.url); + } else if (state.environment === CONSTS.ENVIRONMENTS.sandbox) { + setEndpointUrl(state.endpointConfig.sandbox_endpoints?.url); + } + } catch (error) { + console.error('Failed to extract endpoint URL from the endpoint object', error); + } + }, [state]); + + const updateEndpoint = (endpointId, endpointBody) => { + setEndpointUpdating(true); + const updateEndpointPromise = API.updateEndpoint(api.id, endpointId, endpointBody); + updateEndpointPromise + .then((response) => { + const updatedEndpoint = response.body; + + if (updatedEndpoint.environment === 'PRODUCTION') { + setProductionEndpoints(prev => + prev.map(endpointObj => ( + endpointObj.id === endpointId + ? { ...endpointObj, ...updatedEndpoint } + : endpointObj + )) + ); + } else if (updatedEndpoint.environment === 'SANDBOX') { + setSandboxEndpoints(prev => + prev.map(endpointObj => ( + endpointObj.id === endpointId + ? { ...endpointObj, ...updatedEndpoint } + : endpointObj + )) + ); + } + + Alert.success(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.update.success', + defaultMessage: 'Endpoint updated successfully!', + })); + }).catch((error) => { + console.error(error); + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.update.error', + defaultMessage: 'Something went wrong while updating the endpoint', + })); + }).finally(() => { + setEndpointUpdating(false); + }); + }; + + const deleteEndpoint = (endpointId, environment) => { + setEndpointDeleting(true); + const deleteEndpointPromise = API.deleteEndpoint(api.id, endpointId); + deleteEndpointPromise + .then(() => { + if (environment === 'PRODUCTION') { + setProductionEndpoints(prev => prev.filter(endpointObj => endpointObj.id !== endpointId)); + } else if (environment === 'SANDBOX') { + setSandboxEndpoints(prev => prev.filter(endpointObj => endpointObj.id !== endpointId)); + } + + Alert.success(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.delete.success', + defaultMessage: 'Endpoint deleted successfully!', + })); + }).catch((error) => { + console.error(error); + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.delete.error', + defaultMessage: 'Something went wrong while deleting the endpoint', + })); + }).finally(() => { + setEndpointDeleting(false); + }); + }; + + // /** + // * Method to get the advance configuration from the selected endpoint. + // * + // * @param {number} index The selected endpoint index + // * @param {string} epType The type of the endpoint. (loadbalance/ failover) + // * @param {string} category The endpoint category (Production/ sandbox) + // * @return {object} The advance config object of the endpoint. + // * */ + // const getAdvanceConfig = (index, epType, category) => { + // const endpointTypeProperty = getEndpointTypeProperty(epType, category); + // let advanceConfig = {}; + // if (index > 0) { + // if (epConfig.endpoint_type === 'failover') { + // advanceConfig = epConfig[endpointTypeProperty][index - 1].config; + // } else { + // advanceConfig = epConfig[endpointTypeProperty][index].config; + // } + // } else { + // const endpointInfo = epConfig[endpointTypeProperty]; + // if (Array.isArray(endpointInfo)) { + // advanceConfig = endpointInfo[0].config; + // } else { + // advanceConfig = endpointInfo.config; + // } + // } + // return advanceConfig; + // }; + + // /** + // * Method to open/ close the advance configuration dialog. This method also sets some information about the + // * seleted endpoint type/ category and index. + // * + // * @param {number} index The index of the selected endpoint. + // * @param {string} type The endpoint type + // * @param {string} category The endpoint category. + // * */ + // const toggleAdvanceConfig = (index, type, category) => { + // const advanceEPConfig = getAdvanceConfig(index, type, category); + // setAdvancedConfigOptions(() => { + // return ({ + // open: !advanceConfigOptions.open, + // index, + // type, + // category, + // config: advanceEPConfig === undefined ? {} : advanceEPConfig, + // }); + // }); + // }; + + const saveEndpointSecurityConfig = (endpointSecurityObj, enType) => { + const { type } = endpointSecurityObj; + let newEndpointSecurityObj = endpointSecurityObj; + const secretPlaceholder = '******'; + newEndpointSecurityObj.clientSecret = newEndpointSecurityObj.clientSecret + === secretPlaceholder ? '' : newEndpointSecurityObj.clientSecret; + newEndpointSecurityObj.password = newEndpointSecurityObj.password + === secretPlaceholder ? '' : newEndpointSecurityObj.password; + if (type === 'NONE') { + newEndpointSecurityObj = { ...CONSTS.DEFAULT_ENDPOINT_SECURITY, type }; + } else { + newEndpointSecurityObj.enabled = true; + } + + dispatch({ + field: 'updateEndpointSecurity', + value: { + [enType]: newEndpointSecurityObj, + }, + }); + }; + + /** + * Method to test the endpoint. + * @param {String} endpointURL Endpoint URL + * @param {String} apiID API ID + */ + function testEndpoint(endpointURL, apiID) { + setUpdating(true); + const restApi = new API(); + restApi.testEndpoint(endpointURL, apiID) + .then((result) => { + if (result.body.error !== null) { + setStatusCode(result.body.error); + setIsErrorCode(true); + } else { + setStatusCode(result.body.statusCode + ' ' + result.body.statusMessage); + setIsErrorCode(false); + } + if (result.body.statusCode >= 200 && result.body.statusCode < 300) { + setIsEndpointValid(true); + setIsErrorCode(false); + } else { + setIsEndpointValid(false); + } + }).finally(() => { + setUpdating(false); + }); + } + + const handleEndpointBlur = () => { + if (category === 'production_endpoints') { + dispatch({ field: 'updateProductionEndpointUrl', value: endpointUrl.trim() }); + } else if (category === 'sandbox_endpoints') { + dispatch({ field: 'updateSandboxEndpointUrl', value: endpointUrl.trim() }); + } + }; + + /** + * Method to check whether the endpoint has errors. + * @returns {boolean} Whether the endpoint has errors + */ + const endpointHasErrors = () => { + if (!state.name || !endpointUrl) { + return true; + } else { + return false; + } + }; + + const handleAdvancedConfigOpen = () => { + setAdvancedConfigOpen(true); + }; + + const handleAdvancedConfigClose = () => { + setAdvancedConfigOpen(false); + }; + + return ( + + {/* */} + + + dispatch({ field: 'name', value: e.target.value })} + variant='outlined' + margin='normal' + error={!state.name} + helperText={!state.name + ? ( + + ) : '' + } + required + /> + + + setEndpointUrl(e.target.value)} + onBlur={handleEndpointBlur} + variant='outlined' + margin='normal' + error={!endpointUrl} + helperText={!endpointUrl + ? ( + + ) : '' + } + required + InputProps={{ + endAdornment: ( + + {statusCode && ( + + )} + testEndpoint(endpointUrl, api.id)} + disabled={(isRestricted(['apim:api_create'], api)) || isUpdating} + id={state.id + '-endpoint-test-icon-btn'} + size='large'> + {isUpdating + ? + : ( + + )} + > + + check_circle + + + + )} + + + + )} + > + + settings + + + + + ), + }} + /> + + + + + + + + + + + + + + + {/* {endpointSecurityConfig.category === 'production' ? ( + + ) : ( + + )} */} + + + + ); +}; + +export default EndpointCard; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx deleted file mode 100644 index 7497ba01429..00000000000 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointSection.jsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -// import { FormattedMessage } from 'react-intl'; -import EndpointTextField from './EndpointTextField'; -// import GenericEndpoint from '../GenericEndpoint'; - -const EndpointSection = () => { - return ( -
- - {/* - )} - className={classes. - defaultEndpointWrapper} - endpointURL={getEndpoints - ( - 'sandbox_endpoints' - )} - type='' - index={0} - category='sandbox_endpoints' - editEndpoint={editEndpoint} - esCategory='sandbox' - setAdvancedConfigOpen= - {toggleAdvanceConfig} - setESConfigOpen= - {toggleEndpointSecurityConfig} - apiId={api.id} - /> - {endpointCategory.sandbox && - api.subtypeConfiguration?.subtype === 'AIAPI' && - (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && - ()} */} -
- ); -}; - -export default EndpointSection; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx index 589b5622cf7..92086b58313 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx @@ -18,15 +18,16 @@ import React from 'react'; import { styled } from '@mui/material/styles'; -import { - // Grid, - Tooltip, - InputAdornment, - IconButton, Icon, -} from '@mui/material'; +// import { +// // Grid, +// Tooltip, +// InputAdornment, +// IconButton, Icon, +// } from '@mui/material'; // import PropTypes from 'prop-types'; -import { FormattedMessage, useIntl } from 'react-intl'; -import TextField from '@mui/material/TextField'; +// import { FormattedMessage, useIntl } from 'react-intl'; +// import TextField from '@mui/material/TextField'; +import GeneralEndpointConfigurations from './GeneralEndpointConfigurations'; // import Autocomplete from 'AppComponents/Shared/Autocomplete'; // import { isRestricted } from 'AppData/AuthManager'; @@ -65,71 +66,11 @@ const Root = styled('div')(( })); const EndpointTextField = () => { - const intl = useIntl(); + // const intl = useIntl(); return ( - - <> - setAdvancedConfigOpen(index, type, category)} - // disabled={(isRestricted(['apim:api_create'], api))} - size='large'> - - )} - > - - settings - - - - {/* setESConfigOpen(type, esCategory)} - // disabled={(isRestricted(['apim:api_create'], api))} - size='large'> - - )} - > - - security - - - */} - - - ), - }} - /> + ); }; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx new file mode 100644 index 00000000000..d56578fc8ad --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +const GeneralEndpointConfigurations = () => { + return ( +
+

GeneralEndpointConfigurations

+
+ ); +}; + +export default GeneralEndpointConfigurations; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js index a2f0d5104e5..f2796450e38 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js @@ -64,6 +64,17 @@ const CONSTS = { proxyProtocol: '', }, }, + DEFAULT_ENDPOINT: { + id: null, + name: '', + endpointType: 'REST', + environment: '', + endpointConfig: {}, + }, + ENVIRONMENTS: { + production: 'PRODUCTION', + sandbox: 'SANDBOX', + }, GATEWAY_TYPE: { synapse: 'Synapse', choreoConnect: 'ChoreoConnect', From 92b5f11fdd44d73ebc8467a7717ee423f93decf2 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Wed, 5 Feb 2025 19:31:30 +0530 Subject: [PATCH 04/15] Add UI changes for multi-endpoint support --- .../Apis/Details/Endpoints/AIEndpointAuth.jsx | 7 +- .../Apis/Details/Endpoints/AIEndpoints.jsx | 455 +++--------------- .../Apis/Details/Endpoints/Endpoints.jsx | 2 +- .../MultiEndpointComponents/EndpointCard.jsx | 389 +++++++++------ 4 files changed, 313 insertions(+), 540 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx index 6f9cb91d958..ddf51719e90 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx @@ -120,7 +120,8 @@ export default function AIEndpointAuth(props) { id={'api-key-id-' + endpoint.id} value={apiKeyIdentifier} placeholder={apiKeyIdentifier} - sx={{ width: '100%', minHeight: '80px' }} + helperText=' ' + sx={{ width: '100%' }} variant='outlined' margin='normal' required @@ -139,7 +140,7 @@ export default function AIEndpointAuth(props) { id: 'Apis.Details.Endpoints.Security.api.key.value.placeholder', defaultMessage: 'Enter API Key', })} - sx={{ width: '100%', minHeight: '80px' }} + sx={{ width: '100%' }} onChange={handleApiKeyChange} onBlur={handleApiKeyBlur} error={!apiKeyValue} @@ -148,7 +149,7 @@ export default function AIEndpointAuth(props) { id='Apis.Details.Endpoints.Security.no.api.key.value.error' defaultMessage='API Key should not be empty' /> - ) : ''} + ) : ' '} variant='outlined' margin='normal' required diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx index 3d10710cf83..d2225c78046 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx @@ -22,11 +22,16 @@ import { Grid, Paper, Typography, + // Box, + Button, } from '@mui/material'; import { FormattedMessage, useIntl } from 'react-intl'; import { Progress } from 'AppComponents/Shared'; import API from 'AppData/api'; +import { isRestricted } from 'AppData/AuthManager'; import Alert from 'AppComponents/Shared/Alert'; +import AddCircle from '@mui/icons-material/AddCircle'; +import CONSTS from 'AppData/Constants'; import EndpointCard from './MultiEndpointComponents/EndpointCard'; const PREFIX = 'AIEndpoints'; @@ -153,20 +158,19 @@ const Root = styled('div')(( const AIEndpoints = ({ - api, + apiObject, apiKeyParamConfig, - editEndpoint, }) => { const [productionEndpoints, setProductionEndpoints] = useState([]); const [sandboxEndpoints, setSandboxEndpoints] = useState([]); + const [showAddEndpoint, setShowAddEndpoint] = useState(false); const [loading, setLoading] = useState(true); - // const [saving, setSaving] = useState(false); const intl = useIntl(); const fetchEndpoints = () => { setLoading(true); - const endpointsPromise = API.getApiEndpoints(api.id); + const endpointsPromise = API.getApiEndpoints(apiObject.id); endpointsPromise .then((response) => { const endpoints = response.body.list; @@ -192,33 +196,16 @@ const AIEndpoints = ({ fetchEndpoints(); }, []); - // const addEndpoint = (endpointBody) => { - // setSaving(true); - // const addEndpointPromise = API.addEndpoint(api.id, endpointBody); - // addEndpointPromise - // .then((response) => { - // const newEndpoint = response.body; - - // if (newEndpoint.environment === 'PRODUCTION') { - // setProductionEndpoints(prev => [...prev, newEndpoint]); - // } else if (newEndpoint.environment === 'SANDBOX') { - // setSandboxEndpoints(prev => [...prev, newEndpoint]); - // } - - // Alert.success(intl.formatMessage({ - // id: 'Apis.Details.Endpoints.endpoints.add.success', - // defaultMessage: 'Endpoint added successfully!', - // })); - // }).catch((error) => { - // console.error(error); - // Alert.error(intl.formatMessage({ - // id: 'Apis.Details.Endpoints.endpoints.add.error', - // defaultMessage: 'Something went wrong while adding the endpoint', - // })); - // }).finally(() => { - // setSaving(false); - // }); - // }; + const toggleAddEndpoint = () => { + setShowAddEndpoint(!showAddEndpoint); + }; + + const getDefaultEndpoint = (environment) => { + return { + ...CONSTS.DEFAULT_ENDPOINT, + environment, + } + }; if (loading) { return ; @@ -235,32 +222,53 @@ const AIEndpoints = ({ defaultMessage='Primary Endpoints' /> - {console.log('productionEndpoints', productionEndpoints)} - {console.log('sandboxEndpoints', sandboxEndpoints)} - {/* */} - - + + + + + + + + {showAddEndpoint && ( + - + )} {productionEndpoints.map((endpoint) => ( ))} @@ -273,16 +281,25 @@ const AIEndpoints = ({ defaultMessage='Sandbox Endpoints' /> + {showAddEndpoint && ( + + )} {sandboxEndpoints.map((endpoint) => ( ))} @@ -301,340 +318,6 @@ const AIEndpoints = ({ ); - - // const { endpointConfig } = api; - // const [endpointSecurityInfo, setEndpointSecurityInfo] = useState(null); - // const [advanceConfigOptions, setAdvancedConfigOptions] = useState({ - // open: false, - // index: 0, - // type: '', - // category: '', - // config: undefined, - // }); - // const [endpointSecurityConfig, setEndpointSecurityConfig] = useState({ - // open: false, - // type: '', - // category: '', - // config: undefined, - // }); - - // /** - // * Method to modify the endpoint represented by the given parameters. - // * - // * If url is null, remove the endpoint from the endpoint config. - // * - // * @param {number} index The index of the endpoint in the listing. - // * @param {string} category The endpoint category. (production/ sand box) - // * @param {string} url The new endpoint url. - // * */ - // const editEndpoint = (index, category, url) => { - // let modifiedEndpoint = null; - // // Make a copy of the endpoint config. - // const endpointConfigCopy = cloneDeep(epConfig); - // /* - // * If the index > 0, it means that the endpoint is load balance or fail over. - // * Otherwise it is the default endpoint. (index = 0) - // * */ - // if (index > 0) { - // const endpointTypeProperty = getEndpointTypeProperty(endpointConfigCopy.endpoint_type, category); - // modifiedEndpoint = endpointConfigCopy[endpointTypeProperty]; - // /* - // * In failover case, the failover endpoints are a separate object. But in endpoint listing, since we - // * consider all the endpoints as a single list, to get the real index of the failover endpoint we use - // * index - 1. - // * */ - // if (endpointConfigCopy.endpoint_type === 'failover') { - // modifiedEndpoint[index - 1].url = url.trim(); - // } else { - // modifiedEndpoint[index].url = url.trim(); - // } - // endpointConfigCopy[endpointTypeProperty] = modifiedEndpoint; - // } else if (url !== '') { - // modifiedEndpoint = endpointConfigCopy[category]; - - // /* - // * In this case, we are editing the default endpoint. - // * If the endpoint type is load balance, the production_endpoints or the sandbox_endpoint object is an - // * array. Otherwise, in failover mode, the default endpoint is an object. - // * - // * So, we check whether the endpoints is an array or an object. - // * - // * If This is the first time a user creating an endpoint endpoint config object does not have - // * production_endpoints or sandbox_endpoints object. - // * Therefore create new object and add to the endpoint config. - // * */ - // if (!modifiedEndpoint) { - // modifiedEndpoint = getEndpointTemplate(endpointConfigCopy.endpoint_type); - // modifiedEndpoint.url = url.trim(); - // } else if (Array.isArray(modifiedEndpoint)) { - // if (url === '') { - // modifiedEndpoint.splice(0, 1); - // } else { - // modifiedEndpoint[0].url = url.trim(); - // } - // } else { - // modifiedEndpoint.url = url.trim(); - // } - // endpointConfigCopy[category] = modifiedEndpoint; - // } else { - // /* - // * If the url is empty, delete the respective endpoint object. - // * */ - // delete endpointConfigCopy[category]; - // } - // endpointsDispatcher({ action: category, value: modifiedEndpoint }); - - // }; - - // /** - // * Method to get the advance configuration from the selected endpoint. - // * - // * @param {number} index The selected endpoint index - // * @param {string} epType The type of the endpoint. (loadbalance/ failover) - // * @param {string} category The endpoint category (Production/ sandbox) - // * @return {object} The advance config object of the endpoint. - // * */ - // const getAdvanceConfig = (index, epType, category) => { - // const endpointTypeProperty = getEndpointTypeProperty(epType, category); - // let advanceConfig = {}; - // if (index > 0) { - // if (epConfig.endpoint_type === 'failover') { - // advanceConfig = epConfig[endpointTypeProperty][index - 1].config; - // } else { - // advanceConfig = epConfig[endpointTypeProperty][index].config; - // } - // } else { - // const endpointInfo = epConfig[endpointTypeProperty]; - // if (Array.isArray(endpointInfo)) { - // advanceConfig = endpointInfo[0].config; - // } else { - // advanceConfig = endpointInfo.config; - // } - // } - // return advanceConfig; - // }; - - // /** - // * Method to open/ close the advance configuration dialog. This method also sets some information about the - // * seleted endpoint type/ category and index. - // * - // * @param {number} index The index of the selected endpoint. - // * @param {string} type The endpoint type - // * @param {string} category The endpoint category. - // * */ - // const toggleAdvanceConfig = (index, type, category) => { - // const advanceEPConfig = getAdvanceConfig(index, type, category); - // setAdvancedConfigOptions(() => { - // return ({ - // open: !advanceConfigOptions.open, - // index, - // type, - // category, - // config: advanceEPConfig === undefined ? {} : advanceEPConfig, - // }); - // }); - // }; - - // const saveEndpointSecurityConfig = (endpointSecurityObj, enType) => { - // const { type } = endpointSecurityObj; - // let newEndpointSecurityObj = endpointSecurityObj; - // const secretPlaceholder = '******'; - // newEndpointSecurityObj.clientSecret = newEndpointSecurityObj.clientSecret - // === secretPlaceholder ? '' : newEndpointSecurityObj.clientSecret; - // newEndpointSecurityObj.password = newEndpointSecurityObj.password - // === secretPlaceholder ? '' : newEndpointSecurityObj.password; - // if (type === 'NONE') { - // newEndpointSecurityObj = { ...CONSTS.DEFAULT_ENDPOINT_SECURITY, type }; - // } else { - // newEndpointSecurityObj.enabled = true; - // } - // endpointsDispatcher({ - // action: 'endpointSecurity', - // value: { - // ...endpointSecurityInfo, - // [enType]: newEndpointSecurityObj, - // }, - // }); - // setEndpointSecurityConfig({ open: false }); - // }; - - // /** - // * Method to save the advance configurations. - // * - // * @param {object} advanceConfig The advance configuration object. - // * */ - // const saveAdvanceConfig = (advanceConfig) => { - // const config = cloneDeep(epConfig); - // const endpointConfigProperty = getEndpointTypeProperty( - // advanceConfigOptions.type, advanceConfigOptions.category, - // ); - // const selectedEndpoints = config[endpointConfigProperty]; - // if (Array.isArray(selectedEndpoints)) { - // if (advanceConfigOptions.type === 'failover') { - // selectedEndpoints[advanceConfigOptions.index - 1].config = advanceConfig; - // } else { - // selectedEndpoints[advanceConfigOptions.index].config = advanceConfig; - // } - // } else { - // selectedEndpoints.config = advanceConfig; - // } - // setAdvancedConfigOptions({ open: false }); - // endpointsDispatcher({ - // action: 'set_advance_config', - // value: { ...config, [endpointConfigProperty]: selectedEndpoints }, - // }); - // }; - - // /** - // * Method to close the advance configuration dialog box. - // * */ - // const closeAdvanceConfig = () => { - // setAdvancedConfigOptions({ open: false }); - // }; - - // return ( - // - // - //

Hello

- // - // - // <> - // - // - // - // - // ) : ( - // - // )} - // className={classes.defaultEndpointWrapper} - // endpointURL={getEndpoints - // ( - // 'production_endpoints' - // )} - // type='' - // index={0} - // category='production_endpoints' - // editEndpoint={editEndpoint} - // setAdvancedConfigOpen={toggleAdvanceConfig} - // esCategory='production' - // setESConfigOpen={toggleEndpointSecurityConfig} - // ap - // iId={api.id} - // /> - // {api.subtypeConfiguration?.subtype === 'AIAPI' && // eslint-disable-line - // (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && - // ()} - // {/*
- // - // - // )} - // className={classes. - // defaultEndpointWrapper} - // endpointURL={getEndpoints - // ( - // 'sandbox_endpoints' - // )} - // type='' - // index={0} - // category='sandbox_endpoints' - // editEndpoint={editEndpoint} - // esCategory='sandbox' - // setAdvancedConfigOpen= - // {toggleAdvanceConfig} - // setESConfigOpen= - // {toggleEndpointSecurityConfig} - // apiId={api.id} - // /> - // {endpointCategory.sandbox && // eslint-disable-line - // (apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParameter) && // eslint-disable-line - // ()} - //
*/} - // - //
- //
- // - // - // - // - // - // - //
- // {api.gatewayType !== 'wso2/apk' && ( - // - // - // - // - // - // - // - // - // - // - // )} - //
- // ); } export default AIEndpoints; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx index 97741ac9ecd..6883c70c87f 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/Endpoints.jsx @@ -740,7 +740,7 @@ function Endpoints(props) { ({ [`& .${classes.textField}`]: { width: '100%', - minHeight: '80px', }, [`& .${classes.btn}`]: { @@ -135,28 +132,55 @@ function endpointReducer(state, { field, value }) { }, }, } + case 'updateProductionAdvancedConfiguration': + return { + ...state, + endpointConfig: { + ...state.endpointConfig, + production_endpoints: { + ...state.endpointConfig.production_endpoints, + ...value + }, + }, + } + case 'updateSandboxAdvancedConfiguration': + return { + ...state, + endpointConfig: { + ...state.endpointConfig, + sandbox_endpoints: { + ...state.endpointConfig.sandbox_endpoints, + ...value + }, + }, + } + case 'reset': + return value; default: return state; } } const EndpointCard = ({ - api, + apiObject, endpoint, - name, apiKeyParamConfig, - category, setProductionEndpoints, setSandboxEndpoints, + showAddEndpoint, + setShowAddEndpoint, }) => { + const [category, setCategory] = useState(''); const [isEndpointValid, setIsEndpointValid] = useState(); const [statusCode, setStatusCode] = useState(''); const [isUpdating, setUpdating] = useState(false); const [isErrorCode, setIsErrorCode] = useState(false); const [endpointUrl, setEndpointUrl] = useState(''); const [advancedConfigOpen, setAdvancedConfigOpen] = useState(false); + const [isEndpointSaving, setEndpointSaving] = useState(false); const [isEndpointUpdating, setEndpointUpdating] = useState(false); const [isEndpointDeleting, setEndpointDeleting] = useState(false); + const [advanceConfig, setAdvancedConfig] = useState({}); const [state, dispatch] = useReducer(endpointReducer, endpoint || CONSTS.DEFAULT_ENDPOINT); const iff = (condition, then, otherwise) => (condition ? then : otherwise); const intl = useIntl(); @@ -164,23 +188,76 @@ const EndpointCard = ({ useEffect(() => { try { if (state.environment === CONSTS.ENVIRONMENTS.production) { + setCategory('production_endpoints'); setEndpointUrl(state.endpointConfig.production_endpoints?.url); + setAdvancedConfig(state.endpointConfig.production_endpoints?.config ? + state.endpointConfig.production_endpoints.config : {} + ); } else if (state.environment === CONSTS.ENVIRONMENTS.sandbox) { + setCategory('sandbox_endpoints'); setEndpointUrl(state.endpointConfig.sandbox_endpoints?.url); + setAdvancedConfig(state.endpointConfig.sandbox_endpoints?.config ? + state.endpointConfig.sandbox_endpoints.config : {} + ); } } catch (error) { console.error('Failed to extract endpoint URL from the endpoint object', error); } }, [state]); + const addEndpoint = (endpointBody) => { + setEndpointSaving(true); + const addEndpointPromise = API.addApiEndpoint(apiObject.id, endpointBody); + addEndpointPromise + .then((response) => { + const newEndpoint = response.body; + + if (newEndpoint.environment === 'PRODUCTION') { + setProductionEndpoints(prev => [...prev, newEndpoint]); + dispatch({ + field: 'reset', + value: { + ...CONSTS.DEFAULT_ENDPOINT, + environment: CONSTS.ENVIRONMENTS.production, + } + }); + } else if (newEndpoint.environment === 'SANDBOX') { + setSandboxEndpoints(prev => [...prev, newEndpoint]); + dispatch({ + field: 'reset', + value: { + ...CONSTS.DEFAULT_ENDPOINT, + environment: CONSTS.ENVIRONMENTS.sandbox, + } + }); + } + + Alert.success(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.add.success', + defaultMessage: 'Endpoint added successfully!', + })); + }).catch((error) => { + console.error(error); + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.endpoints.add.error', + defaultMessage: 'Something went wrong while adding the endpoint', + })); + }).finally(() => { + setEndpointSaving(false); + }); + }; + const updateEndpoint = (endpointId, endpointBody) => { setEndpointUpdating(true); - const updateEndpointPromise = API.updateEndpoint(api.id, endpointId, endpointBody); + // If primary default or sandbox default is edited, perform an API save and then send the endpoint update call + + const updateEndpointPromise = API.updateApiEndpoint(apiObject.id, endpointId, endpointBody); updateEndpointPromise .then((response) => { const updatedEndpoint = response.body; if (updatedEndpoint.environment === 'PRODUCTION') { + setProductionEndpoints(prev => prev.map(endpointObj => ( endpointObj.id === endpointId @@ -215,7 +292,9 @@ const EndpointCard = ({ const deleteEndpoint = (endpointId, environment) => { setEndpointDeleting(true); - const deleteEndpointPromise = API.deleteEndpoint(api.id, endpointId); + // if primary endpoint, show alert saying that this endpoint is treated as a primary endpoint + // and hence cannot be deleted. + const deleteEndpointPromise = API.deleteApiEndpoint(apiObject.id, endpointId); deleteEndpointPromise .then(() => { if (environment === 'PRODUCTION') { @@ -239,55 +318,6 @@ const EndpointCard = ({ }); }; - // /** - // * Method to get the advance configuration from the selected endpoint. - // * - // * @param {number} index The selected endpoint index - // * @param {string} epType The type of the endpoint. (loadbalance/ failover) - // * @param {string} category The endpoint category (Production/ sandbox) - // * @return {object} The advance config object of the endpoint. - // * */ - // const getAdvanceConfig = (index, epType, category) => { - // const endpointTypeProperty = getEndpointTypeProperty(epType, category); - // let advanceConfig = {}; - // if (index > 0) { - // if (epConfig.endpoint_type === 'failover') { - // advanceConfig = epConfig[endpointTypeProperty][index - 1].config; - // } else { - // advanceConfig = epConfig[endpointTypeProperty][index].config; - // } - // } else { - // const endpointInfo = epConfig[endpointTypeProperty]; - // if (Array.isArray(endpointInfo)) { - // advanceConfig = endpointInfo[0].config; - // } else { - // advanceConfig = endpointInfo.config; - // } - // } - // return advanceConfig; - // }; - - // /** - // * Method to open/ close the advance configuration dialog. This method also sets some information about the - // * seleted endpoint type/ category and index. - // * - // * @param {number} index The index of the selected endpoint. - // * @param {string} type The endpoint type - // * @param {string} category The endpoint category. - // * */ - // const toggleAdvanceConfig = (index, type, category) => { - // const advanceEPConfig = getAdvanceConfig(index, type, category); - // setAdvancedConfigOptions(() => { - // return ({ - // open: !advanceConfigOptions.open, - // index, - // type, - // category, - // config: advanceEPConfig === undefined ? {} : advanceEPConfig, - // }); - // }); - // }; - const saveEndpointSecurityConfig = (endpointSecurityObj, enType) => { const { type } = endpointSecurityObj; let newEndpointSecurityObj = endpointSecurityObj; @@ -358,21 +388,51 @@ const EndpointCard = ({ } }; + /** + * Method to open the advanced configuration dialog box. + */ const handleAdvancedConfigOpen = () => { setAdvancedConfigOpen(true); }; + /** + * Method to close the advanced configurations dialog box. + */ const handleAdvancedConfigClose = () => { setAdvancedConfigOpen(false); }; + /** + * Method to save the advance configurations. + * + * @param {object} advanceConfigObj The advance configuration object + * */ + const saveAdvanceConfig = (advanceConfigObj) => { + setAdvancedConfig(advanceConfigObj); + if (category === CONSTS.ENVIRONMENTS.production) { + dispatch({ + field: 'updateProductionAdvancedConfiguration', + value: { + config: advanceConfigObj, + }, + }); + } else if (category === CONSTS.ENVIRONMENTS.sandbox) { + dispatch({ + field: 'updateSandboxAdvancedConfiguration', + value: { + config: advanceConfigObj, + }, + }); + } + handleAdvancedConfigClose(); + }; + return ( - - {/* */} - + + - ) : '' + ) : ' ' } required /> setEndpointUrl(e.target.value)} onBlur={handleEndpointBlur} variant='outlined' @@ -411,7 +471,7 @@ const EndpointCard = ({ id='Apis.Details.Endpoints.endpoint.url.helper.text' defaultMessage='Endpoint URL should not be empty' /> - ) : '' + ) : ' ' } required InputProps={{ @@ -431,8 +491,8 @@ const EndpointCard = ({ testEndpoint(endpointUrl, api.id)} - disabled={(isRestricted(['apim:api_create'], api)) || isUpdating} + onClick={() => testEndpoint(endpointUrl, apiObject.id)} + disabled={(isRestricted(['apim:api_create'], apiObject)) || isUpdating} id={state.id + '-endpoint-test-icon-btn'} size='large'> {isUpdating @@ -459,7 +519,7 @@ const EndpointCard = ({ className={classes.iconButton} aria-label='Settings' onClick={handleAdvancedConfigOpen} - disabled={(isRestricted(['apim:api_create'], api))} + disabled={(isRestricted(['apim:api_create'], apiObject))} id={state.id + '-endpoint-configuration-icon-btn'} size='large'> - - + - : - - } - - + - : - - } - + )} - - - - - - - - {/* {endpointSecurityConfig.category === 'production' ? ( - - ) : ( - + + + + + + + - )} */} - - + + + )} ); }; From fb94e3e76956fdfd8e1492929cedc45b290bf835 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Thu, 6 Feb 2025 18:07:15 +0530 Subject: [PATCH 05/15] Update endpoints UI for AI APIs --- .../Apis/Details/Endpoints/AIEndpointAuth.jsx | 43 +-- .../Apis/Details/Endpoints/AIEndpoints.jsx | 254 ++++++++++++++++-- .../MultiEndpointComponents/EndpointCard.jsx | 33 ++- .../main/webapp/source/src/app/data/api.js | 19 -- 4 files changed, 279 insertions(+), 70 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx index ddf51719e90..6b8fdf7d8f8 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx @@ -35,11 +35,7 @@ export default function AIEndpointAuth(props) { const [apiKeyIdentifier] = useState(apiKeyParamConfig.authHeader || apiKeyParamConfig.authQueryParam); const [apiKeyIdentifierType] = useState(apiKeyParamConfig.authHeader ? 'HEADER' : 'QUERY_PARAMETER'); - const [apiKeyValue, setApiKeyValue] = useState( - endpoint.endpointConfig?.endpoint_security?.[isProduction ? 'production' : 'sandbox']?.apiKeyValue === '' - ? '********' - : null - ); + const [apiKeyValue, setApiKeyValue] = useState(null); const [isHeaderParameter] = useState(!!apiKeyParamConfig.authHeader); const [showApiKey, setShowApiKey] = useState(false); @@ -47,24 +43,31 @@ export default function AIEndpointAuth(props) { const llmProviderName = subtypeConfig ? subtypeConfig.llmProviderName : null; useEffect(() => { + setApiKeyValue( + endpoint.endpointConfig?.endpoint_security?.[isProduction ? 'production' : 'sandbox']?.apiKeyValue === '' + ? '********' + : null + ); + }, []); - let newApiKeyValue = endpoint.endpointConfig?.endpoint_security?.[isProduction ? - 'production' : 'sandbox']?.apiKeyValue === '' ? '' : null; + // useEffect(() => { + // let newApiKeyValue = endpoint.endpointConfig?.endpoint_security?.[isProduction ? + // 'production' : 'sandbox']?.apiKeyValue === '' ? '' : null; - if ((llmProviderName === 'MistralAI' || llmProviderName === 'OpenAI') && - newApiKeyValue != null && newApiKeyValue !== '') { - newApiKeyValue = `Bearer ${newApiKeyValue}`; - } + // if ((llmProviderName === 'MistralAI' || llmProviderName === 'OpenAI') && + // newApiKeyValue != null && newApiKeyValue !== '') { + // newApiKeyValue = `Bearer ${newApiKeyValue}`; + // } - saveEndpointSecurityConfig({ - ...CONSTS.DEFAULT_ENDPOINT_SECURITY, - type: 'apikey', - apiKeyIdentifier, - apiKeyIdentifierType, - apiKeyValue: newApiKeyValue, - enabled: true, - }, isProduction ? 'production' : 'sandbox'); - }, []); + // saveEndpointSecurityConfig({ + // ...CONSTS.DEFAULT_ENDPOINT_SECURITY, + // type: 'apikey', + // apiKeyIdentifier, + // apiKeyIdentifierType, + // apiKeyValue: newApiKeyValue, + // enabled: true, + // }, isProduction ? 'production' : 'sandbox'); + // }, []); const handleApiKeyChange = (event) => { let apiKeyVal = event.target.value; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx index d2225c78046..1a63dcb1074 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx @@ -16,7 +16,7 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { styled } from '@mui/material/styles'; import { Grid, @@ -26,9 +26,14 @@ import { Button, } from '@mui/material'; import { FormattedMessage, useIntl } from 'react-intl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; import { Progress } from 'AppComponents/Shared'; import API from 'AppData/api'; import { isRestricted } from 'AppData/AuthManager'; +import { APIContext } from 'AppComponents/Apis/Details/components/ApiContext'; import Alert from 'AppComponents/Shared/Alert'; import AddCircle from '@mui/icons-material/AddCircle'; import CONSTS from 'AppData/Constants'; @@ -51,7 +56,8 @@ const classes = { configDialogHeader: `${PREFIX}-configDialogHeader`, addLabel: `${PREFIX}-addLabel`, buttonIcon: `${PREFIX}-buttonIcon`, - button: `${PREFIX}-button` + button: `${PREFIX}-button`, + btn: `${PREFIX}-btn`, }; const Root = styled('div')(( @@ -136,6 +142,10 @@ const Root = styled('div')(( textTransform: 'none', }, + [`& .${classes.btn}`]: { + marginRight: theme.spacing(0.5), + }, + [`& .${classes.endpointInputWrapper}`]: { width: '100%', display: 'flex', @@ -163,10 +173,15 @@ const AIEndpoints = ({ }) => { const [productionEndpoints, setProductionEndpoints] = useState([]); const [sandboxEndpoints, setSandboxEndpoints] = useState([]); - const [showAddEndpoint, setShowAddEndpoint] = useState(false); + const [showAddProductionEndpoint, setShowAddProductionEndpoint] = useState(false); + const [showAddSandboxEndpoint, setShowAddSandboxEndpoint] = useState(false); + const [primaryProductionEndpoint, setPrimaryProductionEndpoint] = useState(null); + const [primarySandboxEndpoint, setPrimarySandboxEndpoint] = useState(null); const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); const intl = useIntl(); + const { updateAPI } = useContext(APIContext); const fetchEndpoints = () => { setLoading(true); @@ -196,8 +211,33 @@ const AIEndpoints = ({ fetchEndpoints(); }, []); - const toggleAddEndpoint = () => { - setShowAddEndpoint(!showAddEndpoint); + /** + * Set primary endpoints + */ + useEffect(() => { + const { primaryProductionEndpointId, primarySandboxEndpointId } = apiObject; + if (primaryProductionEndpointId) { + setPrimaryProductionEndpoint( + productionEndpoints.find( + (endpoint) => endpoint.id === primaryProductionEndpointId, + ), + ); + } + if (primarySandboxEndpointId) { + setPrimarySandboxEndpoint( + sandboxEndpoints.find( + (endpoint) => endpoint.id === primarySandboxEndpointId, + ), + ); + } + }, [productionEndpoints, sandboxEndpoints]); + + const toggleAddProductionEndpoint = () => { + setShowAddProductionEndpoint(!showAddProductionEndpoint); + }; + + const toggleAddSandboxEndpoint = () => { + setShowAddSandboxEndpoint(!showAddSandboxEndpoint); }; const getDefaultEndpoint = (environment) => { @@ -207,6 +247,69 @@ const AIEndpoints = ({ } }; + const handlePrimaryEndpointChange = (environment, event) => { + if (environment === CONSTS.ENVIRONMENTS.production) { + setPrimaryProductionEndpoint(event.target.value); + } else if (environment === CONSTS.ENVIRONMENTS.sandbox) { + setPrimarySandboxEndpoint(event.target.value); + } + }; + + const savePrimaryEndpoints = () => { + setSaving(true); + // Verify if production and/or sandbox primary endpoints are selected + if (!primaryProductionEndpoint && !primarySandboxEndpoint) { + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.primary.endpoints.save.error', + defaultMessage: 'Please select at least one primary endpoint', + })); + setSaving(false); + return; + } + + const updatePromise = updateAPI({ + primaryProductionEndpointId: primaryProductionEndpoint?.id, + primarySandboxEndpointId: primarySandboxEndpoint?.id, + }); + updatePromise + .catch((error) => { + console.error(error); + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.primary.endpoints.save.error', + defaultMessage: 'Error occurred while saving primary endpoints', + })); + } + ).finally(() => { + setSaving(false); + }); + }; + + const resetPrimaryEndpoints = () => { + const { primaryProductionEndpointId, primarySandboxEndpointId } = apiObject; + + // Reset primary production endpoint + if (primaryProductionEndpointId) { + setPrimaryProductionEndpoint( + productionEndpoints.find( + (endpoint) => endpoint.id === primaryProductionEndpointId, + ), + ); + } else { + setPrimaryProductionEndpoint(null); + } + + // Reset primary sandbox endpoint + if (primarySandboxEndpointId) { + setPrimarySandboxEndpoint( + sandboxEndpoints.find( + (endpoint) => endpoint.id === primarySandboxEndpointId, + ), + ); + } else { + setPrimarySandboxEndpoint(null); + } + } + if (loading) { return ; } @@ -222,6 +325,97 @@ const AIEndpoints = ({ defaultMessage='Primary Endpoints' /> + + + + + + + + + + + + + + + + + + + + + + @@ -235,12 +429,15 @@ const AIEndpoints = ({ + + + {showAddSandboxEndpoint && ( )} {sandboxEndpoints.map((endpoint) => ( diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx index 104fe46c747..a38dece3e82 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx @@ -171,6 +171,7 @@ const EndpointCard = ({ setShowAddEndpoint, }) => { const [category, setCategory] = useState(''); + const [isProduction, setProduction] = useState(false); const [isEndpointValid, setIsEndpointValid] = useState(); const [statusCode, setStatusCode] = useState(''); const [isUpdating, setUpdating] = useState(false); @@ -186,6 +187,7 @@ const EndpointCard = ({ const intl = useIntl(); useEffect(() => { + setProduction(state.environment === CONSTS.ENVIRONMENTS.production); try { if (state.environment === CONSTS.ENVIRONMENTS.production) { setCategory('production_endpoints'); @@ -231,6 +233,8 @@ const EndpointCard = ({ } }); } + setEndpointUrl(''); + setAdvancedConfig({}); Alert.success(intl.formatMessage({ id: 'Apis.Details.Endpoints.endpoints.add.success', @@ -249,7 +253,10 @@ const EndpointCard = ({ const updateEndpoint = (endpointId, endpointBody) => { setEndpointUpdating(true); - // If primary default or sandbox default is edited, perform an API save and then send the endpoint update call + // TODO + // If primary default or sandbox default is edited && primaryID is not in apiDTO, + // perform an API save and then send the endpoint update call OR block update and ask + // to set primary endpoints first. const updateEndpointPromise = API.updateApiEndpoint(apiObject.id, endpointId, endpointBody); updateEndpointPromise @@ -292,6 +299,7 @@ const EndpointCard = ({ const deleteEndpoint = (endpointId, environment) => { setEndpointDeleting(true); + // TODO // if primary endpoint, show alert saying that this endpoint is treated as a primary endpoint // and hence cannot be deleted. const deleteEndpointPromise = API.deleteApiEndpoint(apiObject.id, endpointId); @@ -319,18 +327,13 @@ const EndpointCard = ({ }; const saveEndpointSecurityConfig = (endpointSecurityObj, enType) => { - const { type } = endpointSecurityObj; - let newEndpointSecurityObj = endpointSecurityObj; + const newEndpointSecurityObj = endpointSecurityObj; const secretPlaceholder = '******'; newEndpointSecurityObj.clientSecret = newEndpointSecurityObj.clientSecret === secretPlaceholder ? '' : newEndpointSecurityObj.clientSecret; newEndpointSecurityObj.password = newEndpointSecurityObj.password === secretPlaceholder ? '' : newEndpointSecurityObj.password; - if (type === 'NONE') { - newEndpointSecurityObj = { ...CONSTS.DEFAULT_ENDPOINT_SECURITY, type }; - } else { - newEndpointSecurityObj.enabled = true; - } + newEndpointSecurityObj.enabled = true; dispatch({ field: 'updateEndpointSecurity', @@ -382,6 +385,7 @@ const EndpointCard = ({ */ const endpointHasErrors = () => { if (!state.name || !endpointUrl) { + // TODO: check apikey textfield error return true; } else { return false; @@ -409,14 +413,14 @@ const EndpointCard = ({ * */ const saveAdvanceConfig = (advanceConfigObj) => { setAdvancedConfig(advanceConfigObj); - if (category === CONSTS.ENVIRONMENTS.production) { + if (category === 'production_endpoints') { dispatch({ field: 'updateProductionAdvancedConfiguration', value: { config: advanceConfigObj, }, }); - } else if (category === CONSTS.ENVIRONMENTS.sandbox) { + } else if (category === 'sandbox_endpoints') { dispatch({ field: 'updateSandboxAdvancedConfiguration', value: { @@ -459,7 +463,6 @@ const EndpointCard = ({ id={state.id + '-url'} className={classes.textField} value={endpointUrl} - // placeholder={!endpointUrl ? 'https://ai.com/production' : ''} onChange={(e) => setEndpointUrl(e.target.value)} onBlur={handleEndpointBlur} variant='outlined' @@ -544,9 +547,9 @@ const EndpointCard = ({ @@ -557,6 +560,7 @@ const EndpointCard = ({ variant='contained' color='primary' type='submit' + size='small' onClick={() => addEndpoint(state)} className={classes.btn} disable={ endpointHasErrors() || isRestricted(['apim:api_create'], apiObject) } @@ -573,6 +577,7 @@ const EndpointCard = ({ + + + ); +} + +export default ModelCard; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx new file mode 100644 index 00000000000..e38b44440db --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Button from '@mui/material/Button'; +import API from 'AppData/api'; +import { Progress } from 'AppComponents/Shared'; +import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; +import { Endpoint, ModelData } from './Types'; +import ModelCard from './ModelCard'; + +interface RoundRobinConfig { + production: ModelData[]; + sandbox: ModelData[]; + suspendDuration?: number; +} + +const ModelRoundRobin: FC = () => { + const [apiFromContext] = useAPI(); + const [config, setConfig] = useState({ + production: [ + { model: "gpt-4", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3", weight: 0.5 }, + { model: "gpt-35-turbo", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3" }, // No weight initially + ], + sandbox: [ + // { model: "gpt-4", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3", weight: 0.5 }, + // { model: "gpt-35-turbo", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3" }, // No weight initially + ], + suspendDuration: 60, + // production: [], + // sandbox: [], + // suspendDuration: 0, + }); + const [modelList, setModelList] = useState([]); + const [productionEndpoints, setProductionEndpoints] = useState([]); + const [sandboxEndpoints, setSandboxEndpoints] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchEndpoints = () => { + setLoading(true); + const endpointsPromise = API.getApiEndpoints(apiFromContext.id); + endpointsPromise + .then((response) => { + const endpoints = response.body.list; + + // Filter endpoints based on endpoint type + const prodEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.environment === 'PRODUCTION'); + const sandEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.environment === 'SANDBOX'); + setProductionEndpoints(prodEndpointList); + setSandboxEndpoints(sandEndpointList); + + }).catch((error) => { + console.error(error); + }).finally(() => { + setLoading(false); + }); + } + + useEffect(() => { + setModelList(['gpt-35-turbo', 'gpt-4', 'gpt-4o', 'gpt-4o-mini']); + fetchEndpoints(); + }, []); + + const handleAddModel = (env: 'production' | 'sandbox') => { + const newModel: ModelData = { + model: '', + endpointId: '', + }; + + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: [...prevConfig[env], newModel], + })); + } + + const handleUpdate = (env: 'production' | 'sandbox', index: number, updatedModel: ModelData) => { + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: prevConfig[env].map((item, i) => (i === index ? updatedModel : item)), + })); + } + + const handleDelete = (env: 'production' | 'sandbox', index: number) => { + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: prevConfig[env].filter((item, i) => i !== index), + })); + } + + if (loading) { + return ; + } + + return ( + <> + + + } + aria-controls='production-content' + id='production-header' + > + + + + + + + {config.production.map((model, index) => ( + handleUpdate('production', index, updatedModel)} + onDelete={() => handleDelete('production', index)} + /> + ))} + + + + } + aria-controls='sandbox-content' + id='sandbox-header' + > + + + + + + {config.sandbox.map((model, index) => ( + handleUpdate('sandbox', index, updatedModel)} + onDelete={() => handleDelete('sandbox', index)} + /> + ))} + + + + setConfig({ ...config, suspendDuration: e.target.value })} + // onChange={(e: any) => onInputChange(e, spec.type)} + fullWidth + /> + + + ); +} + +export default ModelRoundRobin; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/Types.d.ts b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/Types.d.ts new file mode 100644 index 00000000000..e8441f34442 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/Types.d.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type Endpoint = { + id: string; + name: string; + endpointType: string; + environment: string; + endpointConfig: any; +} + +export type ModelData = { + model: string; + endpointId: string; + weight?: number; +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx index 43e60d6138f..09f713aa662 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx @@ -414,6 +414,22 @@ const GeneralDetails: FC = ({ label='SOAPTOREST' data-testid='soaptorest-flow' /> + + } + label='AI' + data-testid='ai-flow' + /> {supportedApiTypesError From 60e56af71ee6931b48f083cdbeefef5e849a560d Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Fri, 7 Feb 2025 11:36:50 +0530 Subject: [PATCH 07/15] Add model round robin policy --- .../Policies/AttachedPolicyForm/General.tsx | 18 +- .../Policies/CustomPolicies/ModelCard.tsx | 168 +++++++++--------- .../CustomPolicies/ModelRoundRobin.tsx | 70 ++++---- 3 files changed, 140 insertions(+), 116 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx index d68c0de228b..5de542631e2 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx @@ -112,9 +112,10 @@ const General: FC = ({ policySpec.policyAttributes.forEach(attr => { initState[attr.name] = null }); const [state, setState] = useState(initState); const [isManual, setManual] = useState(false); + const [manualPolicyConfig, setManualPolicyConfig] = useState(''); useEffect(() => { - if (policyObj && policyObj.name === 'AModelRoundRobin') { + if (policyObj && policyObj.name === 'ModelRoundRobin') { setManual(true); } }, [policyObj]); @@ -161,6 +162,10 @@ const General: FC = ({ } }); + if (policyObj.name === 'ModelRoundRobin') { + updateCandidates[policySpec.policyAttributes[0].name] = manualPolicyConfig; + } + // Saving field changes to backend const apiPolicyToSave = {...apiPolicy}; apiPolicyToSave.parameters = updateCandidates; @@ -313,7 +318,7 @@ const General: FC = ({
- {hasAttributes && ( + {(hasAttributes && !isManual) && (
- {(isManual && policyObj.name === 'AModelRoundRobin') && ( - + {(isManual && policyObj.name === 'ModelRoundRobin') && ( + )} {!isManual && policySpec.policyAttributes && policySpec.policyAttributes.map((spec: PolicySpecAttribute) => ( @@ -486,7 +494,7 @@ const General: FC = ({ type='submit' color='primary' data-testid='policy-attached-details-save' - disabled={ isSaveDisabled() || formHasErrors() || saving} + disabled={!isManual && (isSaveDisabled() || formHasErrors() || saving)} > {saving ? <> diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx index 6d38e768ceb..2c030914d5d 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx @@ -19,6 +19,7 @@ import React, { FC, useState, useEffect } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; +import Grid from '@mui/material/Grid'; import TextField from '@mui/material/TextField'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; @@ -58,7 +59,7 @@ const ModelCard: FC = ({ onUpdate(updatedModel); } - const handleIsWeightedChange = () => { + const handleIsWeightedChange = (event: React.ChangeEvent) => { setUseWeight(!useWeight); if (useWeight) { @@ -71,90 +72,95 @@ const ModelCard: FC = ({ return ( <> - - - - + + + + + + + + + + + + + + handleIsWeightedChange(e)} + sx={{ margin: '5px 0' }} + /> + } + label='Is Weighted?' + /> + {useWeight && ( + handleChange(e)} + fullWidth /> - - - - - - - - - - } - label='Is Weighted?' - /> - {useWeight && ( - handleChange(e)} - fullWidth - /> - )} - + + ); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx index e38b44440db..6c7c93d3636 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx @@ -26,6 +26,7 @@ import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import Button from '@mui/material/Button'; +import AddCircle from '@mui/icons-material/AddCircle'; import API from 'AppData/api'; import { Progress } from 'AppComponents/Shared'; import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; @@ -38,21 +39,20 @@ interface RoundRobinConfig { suspendDuration?: number; } -const ModelRoundRobin: FC = () => { +interface ModelRoundRobinProps { + setManualPolicyConfig: React.Dispatch>; + manualPolicyConfig: string; +} + +const ModelRoundRobin: FC = ({ + setManualPolicyConfig, + manualPolicyConfig, +}) => { const [apiFromContext] = useAPI(); const [config, setConfig] = useState({ - production: [ - { model: "gpt-4", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3", weight: 0.5 }, - { model: "gpt-35-turbo", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3" }, // No weight initially - ], - sandbox: [ - // { model: "gpt-4", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3", weight: 0.5 }, - // { model: "gpt-35-turbo", endpointId: "8e3bd85a-e21b-4dd5-a1d2-88a539c45dc3" }, // No weight initially - ], - suspendDuration: 60, - // production: [], - // sandbox: [], - // suspendDuration: 0, + production: [], + sandbox: [], + suspendDuration: 0, }); const [modelList, setModelList] = useState([]); const [productionEndpoints, setProductionEndpoints] = useState([]); @@ -84,6 +84,16 @@ const ModelRoundRobin: FC = () => { fetchEndpoints(); }, []); + useEffect(() => { + if (manualPolicyConfig !== '') { + setConfig(JSON.parse(manualPolicyConfig)); + } + }, [manualPolicyConfig]); + + useEffect(() => { + setManualPolicyConfig(JSON.stringify(config)); + }, [config]); + const handleAddModel = (env: 'production' | 'sandbox') => { const newModel: ModelData = { model: '', @@ -132,15 +142,16 @@ const ModelRoundRobin: FC = () => { {config.production.map((model, index) => ( @@ -169,35 +180,35 @@ const ModelRoundRobin: FC = () => { - {config.sandbox.map((model, index) => ( - handleUpdate('sandbox', index, updatedModel)} - onDelete={() => handleDelete('sandbox', index)} - /> - ))} + {config.sandbox.map((model, index) => ( + handleUpdate('sandbox', index, updatedModel)} + onDelete={() => handleDelete('sandbox', index)} + /> + ))} { type='number' value={config.suspendDuration} onChange={(e: any) => setConfig({ ...config, suspendDuration: e.target.value })} - // onChange={(e: any) => onInputChange(e, spec.type)} fullWidth />
From d0660c3093e49d72ef3c9faec49714218d8d9941 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Fri, 7 Feb 2025 13:32:36 +0530 Subject: [PATCH 08/15] Add certificate section for AI API endpoints page --- .../Apis/Details/Endpoints/AIEndpoints.jsx | 32 ++- .../GeneralEndpointConfigurations.jsx | 254 +++++++++++++++++- 2 files changed, 272 insertions(+), 14 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx index e6c9133a4bd..9d0c22ebe5b 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx @@ -37,6 +37,7 @@ import Alert from 'AppComponents/Shared/Alert'; import AddCircle from '@mui/icons-material/AddCircle'; import CONSTS from 'AppData/Constants'; import EndpointCard from './MultiEndpointComponents/EndpointCard'; +import GeneralEndpointConfigurations from './MultiEndpointComponents/GeneralEndpointConfigurations'; const PREFIX = 'AIEndpoints'; @@ -176,6 +177,7 @@ const AIEndpoints = ({ const [showAddSandboxEndpoint, setShowAddSandboxEndpoint] = useState(false); const [primaryProductionEndpoint, setPrimaryProductionEndpoint] = useState(null); const [primarySandboxEndpoint, setPrimarySandboxEndpoint] = useState(null); + const [endpointList, setEndpointList] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -195,6 +197,16 @@ const AIEndpoints = ({ setProductionEndpoints(prodEndpointList); setSandboxEndpoints(sandEndpointList); + // Loop through endpoints and add endpointList + const tempEndpointUrlList = []; + for (const prodEndpoint of prodEndpointList) { + tempEndpointUrlList.push(prodEndpoint.endpointConfig?.production_endpoints); + } + for (const sandEndpoint of sandEndpointList) { + tempEndpointUrlList.push(sandEndpoint.endpointConfig?.sandbox_endpoints); + } + setEndpointList(tempEndpointUrlList); + }).catch((error) => { console.error(error); Alert.error(intl.formatMessage({ @@ -522,19 +534,21 @@ const AIEndpoints = ({ - - + + + + - - {/* */} + ); } -export default AIEndpoints; \ No newline at end of file +export default AIEndpoints; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx index d56578fc8ad..eab32802f70 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx @@ -16,13 +16,257 @@ * under the License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { styled } from '@mui/material/styles'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Grid, + Typography, + Box, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { isRestricted } from 'AppData/AuthManager'; +import { FormattedMessage, useIntl } from 'react-intl'; +import API from 'AppData/api'; +import Alert from 'AppComponents/Shared/Alert'; +import Certificates from '../GeneralConfiguration/Certificates'; + +const PREFIX = 'GeneralConfiguration'; + +const classes = { + configHeaderContainer: `${PREFIX}-configHeaderContainer`, + generalConfigContent: `${PREFIX}-generalConfigContent`, + secondaryHeading: `${PREFIX}-secondaryHeading`, + heading: `${PREFIX}-heading`, + endpointConfigSection: `${PREFIX}-endpointConfigSection`, + generalConfigPanel: `${PREFIX}-generalConfigPanel`, + securityHeading: `${PREFIX}-securityHeading`, + sandboxEndpointSwitch: `${PREFIX}-sandboxEndpointSwitch` +}; + +const Root = styled('div')(( + { + theme + } +) => ({ + [`& .${classes.configHeaderContainer}`]: { + display: 'flex', + justifyContent: 'space-between', + }, + + [`& .${classes.generalConfigContent}`]: { + boxShadow: 'inset -1px 2px 3px 0px #c3c3c3', + }, + + [`& .${classes.secondaryHeading}`]: { + fontSize: theme.typography.pxToRem(15), + color: theme.palette.text.secondary, + display: 'flex', + }, + + [`& .${classes.heading}`]: { + fontSize: theme.typography.pxToRem(15), + flexBasis: '33.33%', + flexShrink: 0, + fontWeight: '900', + }, + + [`& .${classes.endpointConfigSection}`]: { + padding: '10px', + }, + + [`& .${classes.generalConfigPanel}`]: { + width: '100%', + }, + + [`& .${classes.securityHeading}`]: { + fontWeight: 600, + }, + + [`& .${classes.sandboxEndpointSwitch}`]: { + marginLeft: theme.spacing(2), + } +})); + +const GeneralEndpointConfigurations = ({ + endpointList, +}) => { + const [isConfigExpanded, setConfigExpand] = useState(false); + const [endpointCertificates, setEndpointCertificates] = useState([]); + const [aliasList, setAliasList] = useState([]); + + const intl = useIntl(); + + /** + * Method to save the certificate. + * @param {*} certificate certificate + * @param {*} endpoint endpoint + * @param {*} alias alas + * @returns {Promise} promise + */ + const saveCertificate = (certificate, endpoint, alias) => { + return API.addCertificate(certificate, endpoint, alias) + .then((resp) => { + if (resp.status === 201) { + Alert.info(intl.formatMessage({ + id: 'Apis.Details.Endpoints.GeneralConfiguration.Certificates.certificate.add.success', + defaultMessage: 'Certificate added successfully', + })); + const tmpCertificates = [...endpointCertificates]; + tmpCertificates.push({ + alias: resp.obj.alias, + endpoint: resp.obj.endpoint, + }); + setEndpointCertificates(tmpCertificates); + } + }) + .catch((err) => { + console.error(err.message); + if (err.message === 'Conflict') { + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.GeneralConfiguration.Certificates.certificate.alias.exist', + defaultMessage: 'Adding Certificate Failed. Certificate Alias Exists.', + })); + } else if (err.response && err.response.body && err.response.body.description) { + Alert.error(err.response.body.description); + } else { + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.GeneralConfiguration.Certificates.certificate.error', + defaultMessage: 'Something went wrong while adding the certificate.', + })); + } + return Promise.reject(err); + }); + }; + /** + * Method to delete the selected certificate. + * + * @param {string} alias The alias of the certificate to be deleted. + * */ + const deleteCertificate = (alias) => { + return API.deleteEndpointCertificate(alias) + .then((resp) => { + setEndpointCertificates(() => { + if (resp.status === 200) { + return endpointCertificates.filter((cert) => { + return cert.alias !== alias; + }); + } else { + return -1; + } + }); + Alert.info(intl.formatMessage({ + id: 'Apis.Details.Endpoints.GeneralConfiguration.Certificates.certificate.delete.success', + defaultMessage: 'Certificate Deleted Successfully', + })); + }) + .catch((err) => { + console.log(err); + Alert.error(intl.formatMessage({ + id: 'Apis.Details.Endpoints.GeneralConfiguration.Certificates.certificate.delete.error', + defaultMessage: 'Error Deleting Certificate', + })); + return Promise.reject(err); + }); + }; + + // Get the certificates from backend. + useEffect(() => { + if (!isRestricted(['apim:ep_certificates_view', 'apim:api_view'])) { + const endpointCertificatesList = []; + const aliases = []; + + let endpoints = endpointList; + const filteredEndpoints = []; + const epLookup = []; + for (const ep of endpoints) { + if (ep) { + if (!epLookup.includes(ep.url)) { + filteredEndpoints.push(ep); + epLookup.push(ep.url); + } + } + } + endpoints = filteredEndpoints; + + for (const ep of endpoints) { + if (ep && ep.url) { + const params = {}; + params.endpoint = ep.url; + API.getEndpointCertificates(params) + .then((response) => { + const { certificates } = response.obj; + for (const cert of certificates) { + endpointCertificatesList.push(cert); + aliases.push(cert.alias); + } + }) + .catch((err) => { + console.error(err); + }); + } + } + setEndpointCertificates(endpointCertificatesList); + setAliasList(aliases); + } else { + setEndpointCertificates([]); + } + }, []); -const GeneralEndpointConfigurations = () => { return ( -
-

GeneralEndpointConfigurations

-
+ + setConfigExpand(!isConfigExpanded)} + className={classes.generalConfigPanel} + disabled={isRestricted(['apim:ep_certificates_view', 'apim:api_view'])} + > + } + id='panel1bh-header' + className={classes.configHeaderContainer} + > + + + : + {' '} + {endpointCertificates.length} + {isRestricted(['apim:ep_certificates_view', 'apim:api_view']) && ( + + + + + + )} + + + + + + + + + ); }; From a1130c7b3c541ebf6d9650badf9d74e572ab5762 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Sun, 9 Feb 2025 16:30:49 +0530 Subject: [PATCH 09/15] Minor modifications --- .../components/AiVendors/AddEditAiVendor.jsx | 2 +- .../MultiEndpointComponents/EndpointCard.jsx | 409 +++++++++--------- .../EndpointTextField.jsx | 78 ---- .../CustomPolicies/ModelRoundRobin.tsx | 2 +- 4 files changed, 208 insertions(+), 283 deletions(-) delete mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointTextField.jsx diff --git a/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx b/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx index e200d3672ca..73e349f7087 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx @@ -292,7 +292,7 @@ export default function AddEditAiVendor(props) { const newState = { ...state, configurations: updatedConfigurations, - modelList: state.modelList.join(','), + modelList: JSON.stringify(state.modelList), }; if (id) { diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx index a38dece3e82..5f86795eeb1 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx @@ -33,6 +33,7 @@ import { DialogTitle, DialogContent, Typography, + Paper, } from '@mui/material'; import API from 'AppData/api'; import CircularProgress from '@mui/material/CircularProgress'; @@ -433,218 +434,220 @@ const EndpointCard = ({ return ( - - - dispatch({ field: 'name', value: e.target.value })} - variant='outlined' - margin='normal' - error={!state.name} - helperText={!state.name - ? ( - - ) : ' ' - } - required + + + + dispatch({ field: 'name', value: e.target.value })} + variant='outlined' + margin='normal' + error={!state.name} + helperText={!state.name + ? ( + + ) : ' ' + } + required + /> + + + setEndpointUrl(e.target.value)} + onBlur={handleEndpointBlur} + variant='outlined' + margin='normal' + error={!endpointUrl} + helperText={!endpointUrl + ? ( + + ) : ' ' + } + required + InputProps={{ + endAdornment: ( + + {statusCode && ( + + )} + testEndpoint(endpointUrl, apiObject.id)} + disabled={(isRestricted(['apim:api_create'], apiObject)) || isUpdating} + id={state.id + '-endpoint-test-icon-btn'} + size='large'> + {isUpdating + ? + : ( + + )} + > + + check_circle + + + + )} + + + + )} + > + + settings + + + + + ), + }} + /> + + - - - setEndpointUrl(e.target.value)} - onBlur={handleEndpointBlur} - variant='outlined' - margin='normal' - error={!endpointUrl} - helperText={!endpointUrl + + {showAddEndpoint ? ( - - ) : ' ' - } - required - InputProps={{ - endAdornment: ( - - {statusCode && ( - - )} - testEndpoint(endpointUrl, apiObject.id)} - disabled={(isRestricted(['apim:api_create'], apiObject)) || isUpdating} - id={state.id + '-endpoint-test-icon-btn'} - size='large'> - {isUpdating - ? - : ( - - )} - > - - check_circle - - - - )} - - - + + + + ) : ( + <> + - - - ) : ( - <> - - + - - )} + } + + + )} + - + {apiObject.gatewayType !== 'wso2/apk' && ( ({ - [`& .${classes.endpointInputWrapper}`]: { - width: '100%', - display: 'flex', - justifyContent: 'space-between', - }, - - [`& .${classes.textField}`]: { - width: '100%', - }, - - [`& .${classes.input}`]: { - marginLeft: theme.spacing(1), - flex: 1, - }, - - [`& .${classes.iconButton}`]: { - padding: theme.spacing(1), - } -})); - -const EndpointTextField = () => { - // const intl = useIntl(); - - return ( - - - - ); -}; - -export default EndpointTextField; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx index 6c7c93d3636..565ed26bd72 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx @@ -223,4 +223,4 @@ const ModelRoundRobin: FC = ({ ); } -export default ModelRoundRobin; \ No newline at end of file +export default ModelRoundRobin; From c0bf6ac8a684084ac71e4cb908f7f918a3e67f4e Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Mon, 10 Feb 2025 16:05:56 +0530 Subject: [PATCH 10/15] Add intra-vendor model routing policies --- .../main/webapp/source/src/app/data/api.js | 8 +- .../Policies/AttachedPolicyForm/General.tsx | 13 +- .../ModelWeightedRoundRobin.tsx | 226 ++++++++++++++++++ .../Policies/PolicyForm/GeneralDetails.tsx | 16 -- .../webapp/source/src/app/data/Constants.js | 1 - 5 files changed, 242 insertions(+), 22 deletions(-) create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx diff --git a/portals/admin/src/main/webapp/source/src/app/data/api.js b/portals/admin/src/main/webapp/source/src/app/data/api.js index 3892ca9fb10..3e0a18f2be0 100644 --- a/portals/admin/src/main/webapp/source/src/app/data/api.js +++ b/portals/admin/src/main/webapp/source/src/app/data/api.js @@ -1204,7 +1204,10 @@ class API extends Resource { }; return client.apis['LLMProviders'].addLLMProvider( payload, - { requestBody: aiVendorBody }, + { requestBody: { + ...aiVendorBody, + modelList: JSON.stringify(aiVendorBody.modelList) + }}, this._requestMetaData(), ); }); @@ -1223,7 +1226,8 @@ class API extends Resource { payload, { requestBody: { ...aiVendorBody, - llmProviderId: aiVendorId + llmProviderId: aiVendorId, + modelList: JSON.stringify(aiVendorBody.modelList) }}, this._requestMetaData(), ); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx index 5de542631e2..2ed9ad2b5ee 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx @@ -38,6 +38,7 @@ import { Progress } from 'AppComponents/Shared'; import { PolicySpec, ApiPolicy, AttachedPolicy, Policy, PolicySpecAttribute } from '../Types'; import ApiOperationContext from "../ApiOperationContext"; import ModelRoundRobin from '../CustomPolicies/ModelRoundRobin'; +import ModelWeightedRoundRobin from '../CustomPolicies/ModelWeightedRoundRobin'; const PREFIX = 'General'; @@ -115,7 +116,7 @@ const General: FC = ({ const [manualPolicyConfig, setManualPolicyConfig] = useState(''); useEffect(() => { - if (policyObj && policyObj.name === 'ModelRoundRobin') { + if (policyObj && policyObj.name === 'modelRoundRobin' || policyObj && policyObj.name === 'modelWeightedRoundRobin') { setManual(true); } }, [policyObj]); @@ -162,7 +163,7 @@ const General: FC = ({ } }); - if (policyObj.name === 'ModelRoundRobin') { + if (policyObj.name === 'modelRoundRobin') { updateCandidates[policySpec.policyAttributes[0].name] = manualPolicyConfig; } @@ -354,12 +355,18 @@ const General: FC = ({ - {(isManual && policyObj.name === 'ModelRoundRobin') && ( + {(isManual && policyObj.name === 'modelRoundRobin') && ( )} + {(isManual && policyObj.name === 'modelWeightedRoundRobin') && ( + + )} {!isManual && policySpec.policyAttributes && policySpec.policyAttributes.map((spec: PolicySpecAttribute) => ( diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx new file mode 100644 index 00000000000..cea6df62c7e --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Button from '@mui/material/Button'; +import AddCircle from '@mui/icons-material/AddCircle'; +import API from 'AppData/api'; +import { Progress } from 'AppComponents/Shared'; +import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; +import { Endpoint, ModelData } from './Types'; +import ModelCard from './ModelCard'; + +interface WeightedRoundRobinConfig { + production: ModelData[]; + sandbox: ModelData[]; + suspendDuration?: number; +} + +interface ModelWeightedRoundRobinProps { + setManualPolicyConfig: React.Dispatch>; + manualPolicyConfig: string; +} + +const ModelWeightedRoundRobin: FC = ({ + setManualPolicyConfig, + manualPolicyConfig, +}) => { + const [apiFromContext] = useAPI(); + const [config, setConfig] = useState({ + production: [], + sandbox: [], + suspendDuration: 0, + }); + const [modelList, setModelList] = useState([]); + const [productionEndpoints, setProductionEndpoints] = useState([]); + const [sandboxEndpoints, setSandboxEndpoints] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchEndpoints = () => { + setLoading(true); + const endpointsPromise = API.getApiEndpoints(apiFromContext.id); + endpointsPromise + .then((response) => { + const endpoints = response.body.list; + + // Filter endpoints based on endpoint type + const prodEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.environment === 'PRODUCTION'); + const sandEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.environment === 'SANDBOX'); + setProductionEndpoints(prodEndpointList); + setSandboxEndpoints(sandEndpointList); + + }).catch((error) => { + console.error(error); + }).finally(() => { + setLoading(false); + }); + } + + useEffect(() => { + setModelList(['gpt-35-turbo', 'gpt-4', 'gpt-4o', 'gpt-4o-mini']); + fetchEndpoints(); + }, []); + + useEffect(() => { + if (manualPolicyConfig !== '') { + setConfig(JSON.parse(manualPolicyConfig)); + } + }, [manualPolicyConfig]); + + useEffect(() => { + setManualPolicyConfig(JSON.stringify(config)); + }, [config]); + + const handleAddModel = (env: 'production' | 'sandbox') => { + const newModel: ModelData = { + model: '', + endpointId: '', + }; + + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: [...prevConfig[env], newModel], + })); + } + + const handleUpdate = (env: 'production' | 'sandbox', index: number, updatedModel: ModelData) => { + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: prevConfig[env].map((item, i) => (i === index ? updatedModel : item)), + })); + } + + const handleDelete = (env: 'production' | 'sandbox', index: number) => { + setConfig((prevConfig) => ({ + ...prevConfig, + [env]: prevConfig[env].filter((item, i) => i !== index), + })); + } + + if (loading) { + return ; + } + + return ( + <> + + + } + aria-controls='production-content' + id='production-header' + > + + + + + + + {config.production.map((model, index) => ( + handleUpdate('production', index, updatedModel)} + onDelete={() => handleDelete('production', index)} + /> + ))} + + + + } + aria-controls='sandbox-content' + id='sandbox-header' + > + + + + + + + {config.sandbox.map((model, index) => ( + handleUpdate('sandbox', index, updatedModel)} + onDelete={() => handleDelete('sandbox', index)} + /> + ))} + + + setConfig({ ...config, suspendDuration: e.target.value })} + fullWidth + /> + + + ); +} + +export default ModelWeightedRoundRobin; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx index 09f713aa662..43e60d6138f 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx @@ -414,22 +414,6 @@ const GeneralDetails: FC = ({ label='SOAPTOREST' data-testid='soaptorest-flow' /> - - } - label='AI' - data-testid='ai-flow' - /> {supportedApiTypesError diff --git a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js index f2796450e38..9339855971c 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js @@ -67,7 +67,6 @@ const CONSTS = { DEFAULT_ENDPOINT: { id: null, name: '', - endpointType: 'REST', environment: '', endpointConfig: {}, }, From 0a7b037e1652cf5000b5767a2ce8c073bcbe7cf4 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Tue, 11 Feb 2025 12:48:57 +0530 Subject: [PATCH 11/15] Fix model list retrieval --- .../main/webapp/site/public/locales/en.json | 50 +++++++++++++--- .../main/webapp/site/public/locales/fr.json | 50 +++++++++++++--- .../main/webapp/site/public/locales/en.json | 8 ++- .../publisher/src/main/webapp/.eslintrc.js | 1 + .../main/webapp/site/public/locales/en.json | 59 +++++++++++++++++-- .../Policies/CustomPolicies/ModelCard.tsx | 40 +++---------- .../CustomPolicies/ModelRoundRobin.tsx | 19 +++++- .../ModelWeightedRoundRobin.tsx | 19 +++++- .../main/webapp/source/src/app/data/api.js | 16 +++++ 9 files changed, 199 insertions(+), 63 deletions(-) diff --git a/portals/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index 817d26c0aa6..f1a103b1fc9 100644 --- a/portals/admin/src/main/webapp/site/public/locales/en.json +++ b/portals/admin/src/main/webapp/site/public/locales/en.json @@ -167,6 +167,33 @@ "AdminPages.KeyManagers.List.empty.content.keymanagers": "It is possible to register an OAuth Provider.", "AdminPages.KeyManagers.Usages.dialog.close.btn": "Close", "AdminPages.KeyManagers.Usages.dialog.title": "Key Manager Usages -", + "AdminPages.Labels.AddEdit.form.add.successful": "Label added successfully", + "AdminPages.Labels.AddEdit.form.description": "Description", + "AdminPages.Labels.AddEdit.form.description.helper.text": "Description of the Label", + "AdminPages.Labels.AddEdit.form.edit.successful": "Label edited successfully", + "AdminPages.Labels.AddEdit.form.error.description.too.long": "Label description is too long", + "AdminPages.Labels.AddEdit.form.error.name.empty": "Name is Empty", + "AdminPages.Labels.AddEdit.form.error.name.has.spaces": "Name contains spaces", + "AdminPages.Labels.AddEdit.form.error.name.has.special.chars": "Name field contains special characters", + "AdminPages.Labels.AddEdit.form.error.name.too.long": "Label name is too long", + "AdminPages.Labels.AddEdit.form.name": "Name", + "AdminPages.Labels.AddEdit.form.name.helper.text": "Name of the Label", + "AdminPages.Labels.AddEdit.form.save.btn": "Save", + "AdminPages.Labels.Delete.form.delete.btn": "Delete", + "AdminPages.Labels.Delete.form.delete.content": "Are you sure you want to delete this Label?", + "AdminPages.Labels.Delete.form.delete.successful": "Label deleted successfully", + "AdminPages.Labels.Delete.form.delete.title": "Delete Label?", + "AdminPages.Labels.List.addButtonProps.title": "Add Label", + "AdminPages.Labels.List.addButtonProps.triggerButtonText": "Add Label", + "AdminPages.Labels.List.empty.content.labels": "Labels help you organize and group your artifacts, such as APIs, in a simple and flexible way. You can define labels to tag your artifacts based on usecases, categories, domains, or any criteria you choose.", + "AdminPages.Labels.List.empty.title.labels": "Labels", + "AdminPages.Labels.List.search.default": "Search by Label name", + "AdminPages.Labels.List.title.labels": "Labels", + "AdminPages.Labels.Usages.dialog.close.btn": "Close", + "AdminPages.Labels.Usages.dialog.title": "Labels Usages -", + "AdminPages.Labels.table.header.label.description": "Description", + "AdminPages.Labels.table.header.label.name": "Label Name", + "AdminPages.Labels.table.header.label.usage": "Usage", "AdminPages.Organization.AddEdit.form.error.description.too.long": "Organization description is too long", "AdminPages.Organizations.AddEdit.form.add.successful": "Organizations added successfully", "AdminPages.Organizations.AddEdit.form.description": "Description", @@ -183,7 +210,7 @@ "AdminPages.Organizations.Delete.form.delete.title": "Delete Organization?", "AdminPages.Organizations.List.addButtonProps.title": "Register Organization", "AdminPages.Organizations.List.addButtonProps.triggerButtonText": "Register Organization", - "AdminPages.Organizations.List.empty.content.organization": "You can register organizations here to map the organizations that are created in an External Identity Provider. You should belong to an organization to access this feature.", + "AdminPages.Organizations.List.empty.content.organization": "Manage your organizations by registering new organizations or updating existing entries.", "AdminPages.Organizations.List.empty.title.organization": "Organizations", "AdminPages.Organizations.List.search.default": "Search by Organization Name", "AdminPages.Organizations.List.title.organizations": "Organizations", @@ -212,7 +239,7 @@ "AiVendors.AddEditAiVendor.form.displayName.help": "API Version of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.has.errors": "One or more fields contain errors.", "AiVendors.AddEditAiVendor.form.name": "Name", - "AiVendors.AddEditAiVendor.form.name.help": "Connector Type for AI/LLM Vendor", + "AiVendors.AddEditAiVendor.form.name.help": "Name of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.update.btn": "Update", "AiVendors.AddEditAiVendor.general.details": "General Details", "AiVendors.AddEditAiVendor.general.details.description": "Provide name and description of the AI/LLM Vendor", @@ -221,6 +248,10 @@ "AiVendors.AddEditAiVendor.is.empty.error.attributeIdentifier": "Attribute identifier is required.", "AiVendors.AddEditAiVendor.is.empty.error.connectorType": "Connector type is required.", "AiVendors.AddEditAiVendor.is.empty.error.inputSource": "Input source is required.", + "AiVendors.AddEditAiVendor.modelList": "Model List", + "AiVendors.AddEditAiVendor.modelList.description": "List down AI/LLM Vendor supported model list", + "AiVendors.AddEditAiVendor.modelList.help": "Type available models and press enter/return to add them.", + "AiVendors.AddEditAiVendor.modelList.placeholder": "Type Model name and press Enter", "AiVendors.AddEditAiVendor.title.edit": "AI/LLM Vendor - Edit", "AiVendors.AddEditAiVendor.title.new": "AI/LLM Vendor - Create new", "AiVendors.AiAPIDefinition.browse.files.to.upload": "Browse File to Upload", @@ -325,8 +356,9 @@ "Base.RouteMenuMapping.keymanagers": "Key Managers", "Base.RouteMenuMapping.keymanagers.items.Adding": "Add Key Manager", "Base.RouteMenuMapping.keymanagers.items.Editing": "Edit Key Manager", - "Base.RouteMenuMapping.overview": "Overview", + "Base.RouteMenuMapping.labels": "Labels", "Base.RouteMenuMapping.organizations": "Organizations", + "Base.RouteMenuMapping.overview": "Overview", "Base.RouteMenuMapping.role.permissions": "Scope Assignments", "Base.RouteMenuMapping.ruleset.catalog": "Ruleset Catalog", "Base.RouteMenuMapping.settings": "Settings", @@ -579,11 +611,6 @@ "Governance.Rulesets.AddEdit.general.details.description": "Provide name and description of the ruleset.", "Governance.Rulesets.AddEdit.title.edit": "Edit Ruleset - {name}", "Governance.Rulesets.AddEdit.title.new": "Create New Ruleset", - "Governance.Rulesets.Create.genai": "Create with GenAI", - "Governance.Rulesets.Create.genai.desc": "Use AI to help you create a ruleset", - "Governance.Rulesets.Create.options": "Choose Creation Method", - "Governance.Rulesets.Create.scratch": "Create from Scratch", - "Governance.Rulesets.Create.scratch.desc": "Create a ruleset manually", "Governance.Rulesets.List.add.new.ruleset": "Create Ruleset", "Governance.Rulesets.List.addRuleset.title": "Create Ruleset", "Governance.Rulesets.List.addRuleset.triggerButtonText": "Create Ruleset", @@ -731,6 +758,13 @@ "Keymanager.Local.Claim": "Local Claim", "Keymanager.Local.Claim.remove": "Remove", "Keymanager.Remote.Claim": "Remote Claim", + "Labels.AddEditLabel.api.no.usages": "No API usages for this Label specifically.", + "Labels.AddEditLabel.usages": "Label Usage", + "Labels.ListLabelUsages.API.usages.count.multiple": "{count} APIs are using this label specifically", + "Labels.ListLabelUsages.API.usages.count.one": "1 API is using this Label specifically.", + "Labels.ListLabelUsages.permission.denied.content": "You dont have enough permission to view Label Usages. Please contact the site administrator.", + "Labels.ListLabelUsages.permission.denied.title": "Permission Denied", + "Labels.ListLabelsAPIUsages.error": "Unable to get Label API usage details", "LoginDenied.logout": "Logout", "LoginDenied.message": "The server could not verify that you are authorized to access the requested resource.", "LoginDenied.retry": "Retry", diff --git a/portals/admin/src/main/webapp/site/public/locales/fr.json b/portals/admin/src/main/webapp/site/public/locales/fr.json index 817d26c0aa6..f1a103b1fc9 100644 --- a/portals/admin/src/main/webapp/site/public/locales/fr.json +++ b/portals/admin/src/main/webapp/site/public/locales/fr.json @@ -167,6 +167,33 @@ "AdminPages.KeyManagers.List.empty.content.keymanagers": "It is possible to register an OAuth Provider.", "AdminPages.KeyManagers.Usages.dialog.close.btn": "Close", "AdminPages.KeyManagers.Usages.dialog.title": "Key Manager Usages -", + "AdminPages.Labels.AddEdit.form.add.successful": "Label added successfully", + "AdminPages.Labels.AddEdit.form.description": "Description", + "AdminPages.Labels.AddEdit.form.description.helper.text": "Description of the Label", + "AdminPages.Labels.AddEdit.form.edit.successful": "Label edited successfully", + "AdminPages.Labels.AddEdit.form.error.description.too.long": "Label description is too long", + "AdminPages.Labels.AddEdit.form.error.name.empty": "Name is Empty", + "AdminPages.Labels.AddEdit.form.error.name.has.spaces": "Name contains spaces", + "AdminPages.Labels.AddEdit.form.error.name.has.special.chars": "Name field contains special characters", + "AdminPages.Labels.AddEdit.form.error.name.too.long": "Label name is too long", + "AdminPages.Labels.AddEdit.form.name": "Name", + "AdminPages.Labels.AddEdit.form.name.helper.text": "Name of the Label", + "AdminPages.Labels.AddEdit.form.save.btn": "Save", + "AdminPages.Labels.Delete.form.delete.btn": "Delete", + "AdminPages.Labels.Delete.form.delete.content": "Are you sure you want to delete this Label?", + "AdminPages.Labels.Delete.form.delete.successful": "Label deleted successfully", + "AdminPages.Labels.Delete.form.delete.title": "Delete Label?", + "AdminPages.Labels.List.addButtonProps.title": "Add Label", + "AdminPages.Labels.List.addButtonProps.triggerButtonText": "Add Label", + "AdminPages.Labels.List.empty.content.labels": "Labels help you organize and group your artifacts, such as APIs, in a simple and flexible way. You can define labels to tag your artifacts based on usecases, categories, domains, or any criteria you choose.", + "AdminPages.Labels.List.empty.title.labels": "Labels", + "AdminPages.Labels.List.search.default": "Search by Label name", + "AdminPages.Labels.List.title.labels": "Labels", + "AdminPages.Labels.Usages.dialog.close.btn": "Close", + "AdminPages.Labels.Usages.dialog.title": "Labels Usages -", + "AdminPages.Labels.table.header.label.description": "Description", + "AdminPages.Labels.table.header.label.name": "Label Name", + "AdminPages.Labels.table.header.label.usage": "Usage", "AdminPages.Organization.AddEdit.form.error.description.too.long": "Organization description is too long", "AdminPages.Organizations.AddEdit.form.add.successful": "Organizations added successfully", "AdminPages.Organizations.AddEdit.form.description": "Description", @@ -183,7 +210,7 @@ "AdminPages.Organizations.Delete.form.delete.title": "Delete Organization?", "AdminPages.Organizations.List.addButtonProps.title": "Register Organization", "AdminPages.Organizations.List.addButtonProps.triggerButtonText": "Register Organization", - "AdminPages.Organizations.List.empty.content.organization": "You can register organizations here to map the organizations that are created in an External Identity Provider. You should belong to an organization to access this feature.", + "AdminPages.Organizations.List.empty.content.organization": "Manage your organizations by registering new organizations or updating existing entries.", "AdminPages.Organizations.List.empty.title.organization": "Organizations", "AdminPages.Organizations.List.search.default": "Search by Organization Name", "AdminPages.Organizations.List.title.organizations": "Organizations", @@ -212,7 +239,7 @@ "AiVendors.AddEditAiVendor.form.displayName.help": "API Version of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.has.errors": "One or more fields contain errors.", "AiVendors.AddEditAiVendor.form.name": "Name", - "AiVendors.AddEditAiVendor.form.name.help": "Connector Type for AI/LLM Vendor", + "AiVendors.AddEditAiVendor.form.name.help": "Name of the AI/LLM Vendor.", "AiVendors.AddEditAiVendor.form.update.btn": "Update", "AiVendors.AddEditAiVendor.general.details": "General Details", "AiVendors.AddEditAiVendor.general.details.description": "Provide name and description of the AI/LLM Vendor", @@ -221,6 +248,10 @@ "AiVendors.AddEditAiVendor.is.empty.error.attributeIdentifier": "Attribute identifier is required.", "AiVendors.AddEditAiVendor.is.empty.error.connectorType": "Connector type is required.", "AiVendors.AddEditAiVendor.is.empty.error.inputSource": "Input source is required.", + "AiVendors.AddEditAiVendor.modelList": "Model List", + "AiVendors.AddEditAiVendor.modelList.description": "List down AI/LLM Vendor supported model list", + "AiVendors.AddEditAiVendor.modelList.help": "Type available models and press enter/return to add them.", + "AiVendors.AddEditAiVendor.modelList.placeholder": "Type Model name and press Enter", "AiVendors.AddEditAiVendor.title.edit": "AI/LLM Vendor - Edit", "AiVendors.AddEditAiVendor.title.new": "AI/LLM Vendor - Create new", "AiVendors.AiAPIDefinition.browse.files.to.upload": "Browse File to Upload", @@ -325,8 +356,9 @@ "Base.RouteMenuMapping.keymanagers": "Key Managers", "Base.RouteMenuMapping.keymanagers.items.Adding": "Add Key Manager", "Base.RouteMenuMapping.keymanagers.items.Editing": "Edit Key Manager", - "Base.RouteMenuMapping.overview": "Overview", + "Base.RouteMenuMapping.labels": "Labels", "Base.RouteMenuMapping.organizations": "Organizations", + "Base.RouteMenuMapping.overview": "Overview", "Base.RouteMenuMapping.role.permissions": "Scope Assignments", "Base.RouteMenuMapping.ruleset.catalog": "Ruleset Catalog", "Base.RouteMenuMapping.settings": "Settings", @@ -579,11 +611,6 @@ "Governance.Rulesets.AddEdit.general.details.description": "Provide name and description of the ruleset.", "Governance.Rulesets.AddEdit.title.edit": "Edit Ruleset - {name}", "Governance.Rulesets.AddEdit.title.new": "Create New Ruleset", - "Governance.Rulesets.Create.genai": "Create with GenAI", - "Governance.Rulesets.Create.genai.desc": "Use AI to help you create a ruleset", - "Governance.Rulesets.Create.options": "Choose Creation Method", - "Governance.Rulesets.Create.scratch": "Create from Scratch", - "Governance.Rulesets.Create.scratch.desc": "Create a ruleset manually", "Governance.Rulesets.List.add.new.ruleset": "Create Ruleset", "Governance.Rulesets.List.addRuleset.title": "Create Ruleset", "Governance.Rulesets.List.addRuleset.triggerButtonText": "Create Ruleset", @@ -731,6 +758,13 @@ "Keymanager.Local.Claim": "Local Claim", "Keymanager.Local.Claim.remove": "Remove", "Keymanager.Remote.Claim": "Remote Claim", + "Labels.AddEditLabel.api.no.usages": "No API usages for this Label specifically.", + "Labels.AddEditLabel.usages": "Label Usage", + "Labels.ListLabelUsages.API.usages.count.multiple": "{count} APIs are using this label specifically", + "Labels.ListLabelUsages.API.usages.count.one": "1 API is using this Label specifically.", + "Labels.ListLabelUsages.permission.denied.content": "You dont have enough permission to view Label Usages. Please contact the site administrator.", + "Labels.ListLabelUsages.permission.denied.title": "Permission Denied", + "Labels.ListLabelsAPIUsages.error": "Unable to get Label API usage details", "LoginDenied.logout": "Logout", "LoginDenied.message": "The server could not verify that you are authorized to access the requested resource.", "LoginDenied.retry": "Retry", diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index e498d55323a..77561095a62 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -292,9 +292,11 @@ "Apis.Details.Overview.tags.title": "Tags", "Apis.Details.PubTopic.copied": "Copied", "Apis.Details.PubTopic.copy.to.clipboard": "Copy to clipboard", - "Apis.Details.Resources.components.Operation.disable.security.when.used.in.api.products": "Security enabled", + "Apis.Details.Resources.components.Operation.scopes": "Scopes", + "Apis.Details.Resources.components.Operation.security": "Security", + "Apis.Details.Resources.components.Operation.security.disabled": "Disabled", + "Apis.Details.Resources.components.Operation.security.enabled": "Enabled", "Apis.Details.Resources.components.Operation.security.operation": "Security", - "Apis.Details.Resources.components.enabled.security": "No security", "Apis.Details.Sdk.download.btn": "Download", "Apis.Details.Sdk.no.sdks": "No SDKs", "Apis.Details.Sdk.no.sdks.content": "No SDKs available for this API", @@ -624,6 +626,8 @@ "Shared.AppsAndKeys.ApplicationCreateForm.application.name": "Application Name", "Shared.AppsAndKeys.ApplicationCreateForm.assign.api.request": "Assign API request quota per access token. Allocated quota will be shared among all the subscribed APIs of the application.", "Shared.AppsAndKeys.ApplicationCreateForm.describe.length.error.suffix": "characters remaining", + "Shared.AppsAndKeys.ApplicationCreateForm.enable.share.app.with.org": "Share with the organization", + "Shared.AppsAndKeys.ApplicationCreateForm.enable.share.app.with.org.label": "Share application with the organization", "Shared.AppsAndKeys.ApplicationCreateForm.enter.a.name": "Enter a name to identify the Application. You will be able to pick this application when subscribing to APIs", "Shared.AppsAndKeys.ApplicationCreateForm.my.mobile.application": "My Application", "Shared.AppsAndKeys.ApplicationCreateForm.my.mobile.application.placeholder": "My Mobile Application", diff --git a/portals/publisher/src/main/webapp/.eslintrc.js b/portals/publisher/src/main/webapp/.eslintrc.js index 21825e49878..7395b65a52f 100644 --- a/portals/publisher/src/main/webapp/.eslintrc.js +++ b/portals/publisher/src/main/webapp/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { jsx: true, modules: true, }, + requireConfigFile: false, babelOptions: { presets: ['@babel/preset-react', '@babel/preset-typescript'], }, diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index 9358b5863c5..37d6684a93e 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -133,14 +133,22 @@ "Apis.Create.GraphQL.ApiCreateGraphQL.created.error": "Something went wrong while adding the API", "Apis.Create.GraphQL.ApiCreateGraphQL.created.success": "{name} API created successfully", "Apis.Create.GraphQL.ApiCreateGraphQL.error.policies.not.available": "Throttling policies not available. Contact your administrator", - "Apis.Create.GraphQL.ApiCreateGraphQL.heading": "Create an API using a GraphQL SDL definition", + "Apis.Create.GraphQL.ApiCreateGraphQL.heading": "Create a GraphQL API", "Apis.Create.GraphQL.ApiCreateGraphQL.next": "Next", - "Apis.Create.GraphQL.ApiCreateGraphQL.sub.heading": "Create an API by importing an existing GraphQL SDL definition.", + "Apis.Create.GraphQL.ApiCreateGraphQL.sub.heading": "Create a GraphQL API by importing a SDL definition using a file, SDL hosted URL, or by using a GraphQL endpoint.", "Apis.Create.GraphQL.ApiCreateGraphQL.wizard.one": "Provide GraphQL", "Apis.Create.GraphQL.ApiCreateGraphQL.wizard.two": "Create API", "Apis.Create.GraphQL.Steps.ProvideGraphQL.Input.file.dropzone": "Drag & Drop files here {break} or {break} Browse files{break}({accept})", "Apis.Create.GraphQL.Steps.ProvideGraphQL.Input.file.upload": "Browse File to Upload", - "Apis.Create.GraphQL.Steps.ProvideGraphQL.Input.type": "Provide GraphQL File", + "Apis.Create.GraphQL.Steps.ProvideGraphQL.Input.type": "Input Type", + "Apis.Create.GraphQL.create.api.endpoint.label": "GraphQL Endpoint", + "Apis.Create.GraphQL.create.api.endpoint.placeholder": "Enter GraphQL Endpoint", + "Apis.Create.GraphQL.create.api.form.endpoint.label": "GraphQL Endpoint", + "Apis.Create.GraphQL.create.api.form.file.label": "GraphQL File/Archive", + "Apis.Create.GraphQL.create.api.form.url.label": "GraphQL SDL URL", + "Apis.Create.GraphQL.create.api.url.helper.text": "Click away to validate the URL", + "Apis.Create.GraphQL.create.api.url.label": "GraphQL SDL URL", + "Apis.Create.GraphQL.create.api.url.placeholder": "Enter GraphQL SDL URL", "Apis.Create.OpenAPI.ApiCreateOpenAPI.back": "Back", "Apis.Create.OpenAPI.ApiCreateOpenAPI.cancel": "Cancel", "Apis.Create.OpenAPI.ApiCreateOpenAPI.create": "Create", @@ -397,6 +405,7 @@ "Apis.Details.Configurartion.components.QueryAnalysis.cancle.btn": "Cancel", "Apis.Details.Configurartion.components.QueryAnalysis.edit": "Edit Complexity Values", "Apis.Details.Configurartion.components.QueryAnalysis.update.complexity": "update complexity", + "Apis.Details.Configuration\n .Configuration.Design.APIProduct.sub.heading": "Configure basic API Product meta information", "Apis.Details.Configuration.ApiKeyHeader.helper.text": "ApiKey header name cannot contain spaces or special characters", "Apis.Details.Configuration.AuthHeader.helper.text": "Authorization header name cannot contain spaces or special characters", "Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.prod": "Backend Rate Limiting", @@ -417,9 +426,10 @@ "Apis.Details.Configuration.Components.validate.role.error": "Error when validating role: {role}", "Apis.Details.Configuration.Configuration.ApiKeyHeader.tooltip": "The header name that is used to send the api key information. \"ApiKey\" is the default header.", "Apis.Details.Configuration.Configuration.AuthHeader.tooltip": "The header name that is used to send the authorization information. \"Authorization\" is the default header.", - "Apis.Details.Configuration.Configuration.Design.APIProduct.sub.heading": "Configure basic API Product meta information", + "Apis.Details.Configuration.Configuration.Design.no.labels": "No Labels Attached", "Apis.Details.Configuration.Configuration.Design.sub.heading": "Configure basic API meta information", "Apis.Details.Configuration.Configuration.Design.topic.header": "Design Configurations", + "Apis.Details.Configuration.Configuration.Design.topic.label": "Labels", "Apis.Details.Configuration.Configuration.Endpoints.edit.api.endpoints": "Edit API Endpoints", "Apis.Details.Configuration.Configuration.apiKey.header.label": "ApiKey Header", "Apis.Details.Configuration.Configuration.auth.header.label": "Authorization Header", @@ -548,6 +558,8 @@ "Apis.Details.Configuration.components.SchemaValidation.description": "Enabling JSON schema validation will cause to build the payload in every request and response. This will impact the round trip time of an API request!", "Apis.Details.Configuration.components.SchemaValidation.description.question": "Do you want to enable schema validation?", "Apis.Details.Configuration.components.SchemaValidation.title": "Caution!", + "Apis.Details.Configuration.components.Shared.Organizations.label": "Share API with Organizations", + "Apis.Details.Configuration.components.Shared.organizations.dropdown.tooltip": "Allow to share API with other organizations. There has to be pre-defined organizations in the environment in order to share the API.", "Apis.Details.Configuration.components.Social.giturl": "GitHub URL", "Apis.Details.Configuration.components.Social.giturl.help": "This GitHub URL will be available in the API overview page in developer portal", "Apis.Details.Configuration.components.Social.slack": "Slack URL", @@ -696,6 +708,16 @@ "Apis.Details.Endpoints.\n GenericEndpoint.config.endpoint": "Endpoint configurations", "Apis.Details.Endpoints.\n GenericEndpoint.security.endpoint": "Endpoint security", "Apis.Details.Endpoints..EndpointOverview.change.type.proceed": "Proceed", + "Apis.Details.Endpoints.AIEndpoints.add.new.endpoint": "Add New Endpoint", + "Apis.Details.Endpoints.AIEndpoints.general.config.header": "General Endpoint Configurations", + "Apis.Details.Endpoints.AIEndpoints.primary.endpoints.label": "Primary Endpoints", + "Apis.Details.Endpoints.AIEndpoints.primary.production.endpoint.label": "Primary Production Endpoint", + "Apis.Details.Endpoints.AIEndpoints.primary.sandbox.endpoint.label": "Primary Sandbox Endpoint", + "Apis.Details.Endpoints.AIEndpoints.production.endpoints.label": "Production Endpoints", + "Apis.Details.Endpoints.AIEndpoints.reset.primary.endpoints": "Reset", + "Apis.Details.Endpoints.AIEndpoints.sandbox.endpoints.label": "Sandbox Endpoints", + "Apis.Details.Endpoints.AIEndpoints.save.primary.endpoints": "Save", + "Apis.Details.Endpoints.AIEndpoints.saving.primary.endpoints": "Saving", "Apis.Details.Endpoints.API.Definition.fetch.error": "Error occurred while fetching API definition", "Apis.Details.Endpoints.AdvancedConfig.AdvanceEndpointConfig.action": "Action", "Apis.Details.Endpoints.AdvancedConfig.AdvanceEndpointConfig.cancel.button": "Close", @@ -917,6 +939,25 @@ "Apis.Details.Endpoints.UploadCustomBackend.click.or.drop.to.upload.file": "Click or drag the sequence backend file to upload.", "Apis.Details.Endpoints.UploadCustomBackend.config.save.button": "Save", "Apis.Details.Endpoints.UploadCustomBackend.invalid.file": "Invalid file type", + "Apis.Details.Endpoints.endpoint.add": "Add", + "Apis.Details.Endpoints.endpoint.advanced.configuration": "Advanced Configurations", + "Apis.Details.Endpoints.endpoint.cancel": "Cancel", + "Apis.Details.Endpoints.endpoint.check.status": "Check endpoint status", + "Apis.Details.Endpoints.endpoint.configurations.tooltip": "Endpoint configurations", + "Apis.Details.Endpoints.endpoint.delete": "Delete", + "Apis.Details.Endpoints.endpoint.deleting": "Deleting", + "Apis.Details.Endpoints.endpoint.name.helper.text": "Endpoint name should not be empty", + "Apis.Details.Endpoints.endpoint.update": "Update", + "Apis.Details.Endpoints.endpoint.updating": "Updating", + "Apis.Details.Endpoints.endpoint.url.helper.text": "Endpoint URL should not be empty", + "Apis.Details.Endpoints.endpoints.add.error": "Something went wrong while adding the endpoint", + "Apis.Details.Endpoints.endpoints.add.success": "Endpoint added successfully!", + "Apis.Details.Endpoints.endpoints.delete.error": "Something went wrong while deleting the endpoint", + "Apis.Details.Endpoints.endpoints.delete.success": "Endpoint deleted successfully!", + "Apis.Details.Endpoints.endpoints.fetch.error": "Something went wrong while fetching endpoints", + "Apis.Details.Endpoints.endpoints.update.error": "Something went wrong while updating the endpoint", + "Apis.Details.Endpoints.endpoints.update.success": "Endpoint updated successfully!", + "Apis.Details.Endpoints.primary.endpoints.save.error": "Error occurred while saving primary endpoints", "Apis.Details.Environments.\n Environments.select.table": "Select Revision", "Apis.Details.Environments.\n Environments.gateway.deployed.revision": "Deployed Revision", "Apis.Details.Environments.Environments\n .select.vhost": "Select Access URL", @@ -1214,6 +1255,12 @@ "Apis.Details.Policies.Components.TabPanel.Components.Common.Policy.List": "Common Policies", "Apis.Details.Policies.CreatePolicy.create.new.policy": "Create New Policy", "Apis.Details.Policies.CreatePolicy.create.new.policy.link": "Want to create a common policy that will be visible to all APIs instead?", + "Apis.Details.Policies.Custom.Policies.Modelcard.delete": "Delete", + "Apis.Details.Policies.Custom.Policies.model.add": "Add Model", + "Apis.Details.Policies.CustomPolicies.ModelRoundRobin.accordion.production": "Production", + "Apis.Details.Policies.CustomPolicies.ModelRoundRobin.accordion.sandbox": "Sandbox", + "Apis.Details.Policies.CustomPolicies.ModelRoundRobin.select.endpoint": "Endpoint", + "Apis.Details.Policies.CustomPolicies.ModelRoundRobin.select.model": "Model", "Apis.Details.Policies.DeletePolicy.cancel": "Cancel", "Apis.Details.Policies.DeletePolicy.confirm": "Delete", "Apis.Details.Policies.DeletePolicy.delete.confirm": "Confirm Delete", @@ -1697,8 +1744,8 @@ "Apis.Listing.SampleAPI.SampleAPI.create.new": "Let’s get started !", "Apis.Listing.SampleAPI.SampleAPI.create.new.description": "Choose your option to create an API", "Apis.Listing.SampleAPI.SampleAPI.graphql.api": "GraphQL", - "Apis.Listing.SampleAPI.SampleAPI.graphql.import.sdl.content": "Use an existing definition", - "Apis.Listing.SampleAPI.SampleAPI.graphql.import.sdl.title": "Import GraphQL SDL", + "Apis.Listing.SampleAPI.SampleAPI.graphql.import.sdl.content": "Use an existing schema or endpoint", + "Apis.Listing.SampleAPI.SampleAPI.graphql.import.sdl.title": "Create GraphQL API", "Apis.Listing.SampleAPI.SampleAPI.no.apis.deployed": "No APIs have been deployed yet", "Apis.Listing.SampleAPI.SampleAPI.rest.api": "REST API", "Apis.Listing.SampleAPI.SampleAPI.rest.api.import.open.content": "Import OAS 3 or Swagger 2.0 definition", diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx index 2c030914d5d..cd8c0dccc37 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx @@ -16,9 +16,9 @@ * under the License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import Grid from '@mui/material/Grid'; import TextField from '@mui/material/TextField'; import InputLabel from '@mui/material/InputLabel'; @@ -26,13 +26,14 @@ import MenuItem from '@mui/material/MenuItem'; import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; import Button from '@mui/material/Button'; -import { Checkbox, FormControlLabel, Paper } from '@mui/material'; +import { Paper } from '@mui/material'; import { Endpoint, ModelData } from './Types'; interface ModelCardProps { modelData: ModelData; modelList: string[]; endpointList: Endpoint[]; + isWeightedRoundRobinPolicy: boolean; onUpdate: (updatedModel: ModelData) => void; onDelete: () => void; } @@ -41,35 +42,19 @@ const ModelCard: FC = ({ modelData, modelList, endpointList, + isWeightedRoundRobinPolicy, onUpdate, onDelete, }) => { const { model, endpointId, weight } = modelData; - const [useWeight, setUseWeight] = useState(weight !== undefined); const handleChange = (event: React.ChangeEvent) => { const { name, value } = event.target; const updatedModel = { ...modelData, [name]: name === "weight" ? parseFloat(value) : value }; - // Remove weight if it is not used - if (!useWeight && name === "weight") { - delete updatedModel.weight; - } - onUpdate(updatedModel); } - const handleIsWeightedChange = (event: React.ChangeEvent) => { - setUseWeight(!useWeight); - - if (useWeight) { - const { weight, ...updatedModel } = modelData; - onUpdate(updatedModel); - } else { - onUpdate({ ...modelData, weight: 0.5 }); - } - } - return ( <> @@ -120,20 +105,9 @@ const ModelCard: FC = ({ ))} - handleIsWeightedChange(e)} - sx={{ margin: '5px 0' }} - /> - } - label='Is Weighted?' - /> - {useWeight && ( + {isWeightedRoundRobinPolicy && ( = ({ setLoading(false); }); } + + const fetchModelList = () => { + const modelListPromise = API.getLLMProviderModelList(JSON.parse(apiFromContext.subtypeConfiguration.configuration).llmProviderId); + modelListPromise + .then((response) => { + setModelList(response.body); + }).catch((error) => { + console.error(error); + }); + } useEffect(() => { - setModelList(['gpt-35-turbo', 'gpt-4', 'gpt-4o', 'gpt-4o-mini']); + fetchModelList(); fetchEndpoints(); }, []); @@ -160,6 +170,7 @@ const ModelRoundRobin: FC = ({ modelData={model} modelList={modelList} endpointList={productionEndpoints} + isWeightedRoundRobinPolicy={false} onUpdate={(updatedModel) => handleUpdate('production', index, updatedModel)} onDelete={() => handleDelete('production', index)} /> @@ -181,15 +192,16 @@ const ModelRoundRobin: FC = ({ {config.sandbox.map((model, index) => ( @@ -198,6 +210,7 @@ const ModelRoundRobin: FC = ({ modelData={model} modelList={modelList} endpointList={sandboxEndpoints} + isWeightedRoundRobinPolicy={false} onUpdate={(updatedModel) => handleUpdate('sandbox', index, updatedModel)} onDelete={() => handleDelete('sandbox', index)} /> diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx index cea6df62c7e..caaa1182cc5 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx @@ -79,8 +79,18 @@ const ModelWeightedRoundRobin: FC = ({ }); } + const fetchModelList = () => { + const modelListPromise = API.getLLMProviderModelList(JSON.parse(apiFromContext.subtypeConfiguration.configuration).llmProviderId); + modelListPromise + .then((response) => { + setModelList(response.body); + }).catch((error) => { + console.error(error); + }); + } + useEffect(() => { - setModelList(['gpt-35-turbo', 'gpt-4', 'gpt-4o', 'gpt-4o-mini']); + fetchModelList(); fetchEndpoints(); }, []); @@ -160,6 +170,7 @@ const ModelWeightedRoundRobin: FC = ({ modelData={model} modelList={modelList} endpointList={productionEndpoints} + isWeightedRoundRobinPolicy={true} onUpdate={(updatedModel) => handleUpdate('production', index, updatedModel)} onDelete={() => handleDelete('production', index)} /> @@ -181,15 +192,16 @@ const ModelWeightedRoundRobin: FC = ({ {config.sandbox.map((model, index) => ( @@ -198,6 +210,7 @@ const ModelWeightedRoundRobin: FC = ({ modelData={model} modelList={modelList} endpointList={sandboxEndpoints} + isWeightedRoundRobinPolicy={true} onUpdate={(updatedModel) => handleUpdate('sandbox', index, updatedModel)} onDelete={() => handleDelete('sandbox', index)} /> diff --git a/portals/publisher/src/main/webapp/source/src/app/data/api.js b/portals/publisher/src/main/webapp/source/src/app/data/api.js index 9c8ea91f6d8..ddbb8f9a3a9 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/api.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/api.js @@ -3580,6 +3580,22 @@ class API extends Resource { }); } + /** + * Get the LLM provider model list + * + * @param {String} llmProviderId LLM Provider ID + * @returns {Promise} Promise containing the list of LLM provider models + */ + static getLLMProviderModelList(llmProviderId) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['LLMProvider'].getLLMProviderModels( + { llmProviderId }, + this._requestMetaData(), + ) + }); + } + /** * Get all endpoints of the API * @param {String} apiId UUID of the API From 878f158e7d64a8be1b0c8267c119328a11c881b9 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Tue, 11 Feb 2025 13:48:02 +0530 Subject: [PATCH 12/15] Update round robin and weighted round robin policy configuring logic --- .../Apis/Details/Policies/AttachedPolicyForm/General.tsx | 2 +- .../Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx | 4 ++-- .../Policies/CustomPolicies/ModelWeightedRoundRobin.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx index 2ed9ad2b5ee..b6b3f5abbb2 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyForm/General.tsx @@ -163,7 +163,7 @@ const General: FC = ({ } }); - if (policyObj.name === 'modelRoundRobin') { + if (policyObj.name === 'modelRoundRobin' || policyObj.name === 'modelWeightedRoundRobin') { updateCandidates[policySpec.policyAttributes[0].name] = manualPolicyConfig; } diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx index 7b5ab2e7456..b8930f4a9f2 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx @@ -96,12 +96,12 @@ const ModelRoundRobin: FC = ({ useEffect(() => { if (manualPolicyConfig !== '') { - setConfig(JSON.parse(manualPolicyConfig)); + setConfig(JSON.parse(manualPolicyConfig.replace(/'/g, '"'))); } }, [manualPolicyConfig]); useEffect(() => { - setManualPolicyConfig(JSON.stringify(config)); + setManualPolicyConfig(JSON.stringify(config).replace(/"/g, "'")); }, [config]); const handleAddModel = (env: 'production' | 'sandbox') => { diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx index caaa1182cc5..a870bb1d69d 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx @@ -96,12 +96,12 @@ const ModelWeightedRoundRobin: FC = ({ useEffect(() => { if (manualPolicyConfig !== '') { - setConfig(JSON.parse(manualPolicyConfig)); + setConfig(JSON.parse(manualPolicyConfig.replace(/'/g, '"'))); } }, [manualPolicyConfig]); useEffect(() => { - setManualPolicyConfig(JSON.stringify(config)); + setManualPolicyConfig(JSON.stringify(config).replace(/"/g, "'")); }, [config]); const handleAddModel = (env: 'production' | 'sandbox') => { From dc36cefb10be0c7d7514ae352528221b2a0219f7 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Wed, 12 Feb 2025 15:36:25 +0530 Subject: [PATCH 13/15] Add filtering logic to only show AI policies within AI APIs --- .../Apis/Details/Policies/Policies.tsx | 26 +++++++++++++++---- .../Policies/PolicyForm/GeneralDetails.tsx | 26 ++++++++++++------- .../Apis/Details/Policies/Types.d.ts | 4 +-- .../components/Shared/PoliciesUI/Types.d.ts | 4 +-- .../webapp/source/src/app/data/Constants.js | 4 ++- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Policies.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Policies.tsx index 63711c3d11b..7da252e5d7e 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Policies.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Policies.tsx @@ -217,11 +217,27 @@ const Policies: React.FC = () => { let filteredCommonPoliciesByAPITypeList = []; if (api.type === "HTTP" || api.type === "SOAP" || api.type === "SOAPTOREST") { - // Get HTTP supported policies - filteredApiPoliciesByAPITypeList = filteredApiPolicyByGatewayTypeList.filter( - (policy: Policy) => policy.supportedApiTypes.includes(api.type)); - filteredCommonPoliciesByAPITypeList = filteredCommonPolicyByGatewayTypeList.filter( - (policy: Policy) => policy.supportedApiTypes.includes(api.type)); + // Get API policies based on the API type + filteredApiPoliciesByAPITypeList = filteredApiPolicyByGatewayTypeList.filter((policy: Policy) => { + return policy.supportedApiTypes.some((item: any) => { + if (typeof item === 'string') { + return item === api.type; + } else if (typeof item === 'object') { + return item.apiType === api.type && item.subType === api.subtypeConfiguration?.subtype; + } + }); + }); + + // Get common policies based on the API type + filteredCommonPoliciesByAPITypeList = filteredCommonPolicyByGatewayTypeList.filter((policy: Policy) => { + return policy.supportedApiTypes.some((item: any) => { + if (typeof item === 'string') { + return item === api.type; + } else if (typeof item === 'object') { + return item.apiType === api.type && item.subType === api.subtypeConfiguration?.subtype; + } + }); + }); } setApiPolicies(filteredApiPoliciesByAPITypeList); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx index 43e60d6138f..c3b2bbe89d4 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyForm/GeneralDetails.tsx @@ -55,7 +55,7 @@ interface GeneralDetailsProps { version: string | null; description: string; applicableFlows: string[]; - supportedApiTypes: string[]; + supportedApiTypes: string[] | Map[]; dispatch?: React.Dispatch; isViewMode: boolean; } @@ -374,9 +374,11 @@ const GeneralDetails: FC = ({ typeof item === 'string') && + supportedApiTypes.includes('HTTP') + } id='http-select-check-box' onChange={handleApiTypeChange} /> @@ -389,9 +391,11 @@ const GeneralDetails: FC = ({ typeof item === 'string') && + supportedApiTypes.includes('SOAP') + } id='soap-select-check-box' onChange={handleApiTypeChange} /> @@ -404,9 +408,11 @@ const GeneralDetails: FC = ({ typeof item === 'string') && + supportedApiTypes.includes('SOAPTOREST') + } id='soaptorest-select-check-box' onChange={handleApiTypeChange} /> diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Types.d.ts b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Types.d.ts index 2f802adc12b..56edb1a1a74 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Types.d.ts +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/Types.d.ts @@ -23,7 +23,7 @@ export type Policy = { displayName: string; applicableFlows: string[]; supportedGateways: string[]; - supportedApiTypes: string[]; + supportedApiTypes: string[] | Map[]; isAPISpecific: boolean; supportedGateways: string[]; }; @@ -60,7 +60,7 @@ export type PolicySpec = { description: string; applicableFlows: string[]; supportedGateways: string[]; - supportedApiTypes: string[]; + supportedApiTypes: string[] | Map[]; policyAttributes: PolicySpecAttribute[]; isAPISpecific?: boolean; md5?: string; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Shared/PoliciesUI/Types.d.ts b/portals/publisher/src/main/webapp/source/src/app/components/Shared/PoliciesUI/Types.d.ts index 91bfd0ee553..062e0ed4df7 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Shared/PoliciesUI/Types.d.ts +++ b/portals/publisher/src/main/webapp/source/src/app/components/Shared/PoliciesUI/Types.d.ts @@ -23,7 +23,7 @@ export type Policy = { displayName: string; applicableFlows: string[]; supportedGateways: string[]; - supportedApiTypes: string[]; + supportedApiTypes: string[] | Map[]; isAPISpecific: boolean; supportedGateways: string[]; }; @@ -60,7 +60,7 @@ export type PolicySpec = { description: string; applicableFlows: string[]; supportedGateways: string[]; - supportedApiTypes: string[]; + supportedApiTypes: string[] | Map[]; policyAttributes: PolicySpecAttribute[]; isAPISpecific?: boolean; md5?: string; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js index 9339855971c..826510e63f1 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/Constants.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/Constants.js @@ -68,7 +68,9 @@ const CONSTS = { id: null, name: '', environment: '', - endpointConfig: {}, + endpointConfig: { + endpoint_type: 'http', + }, }, ENVIRONMENTS: { production: 'PRODUCTION', From 412c69403db6fe32df7c99b8021ab8c5596c21c1 Mon Sep 17 00:00:00 2001 From: Ashera Silva Date: Thu, 13 Feb 2025 15:28:08 +0530 Subject: [PATCH 14/15] Rename environment as deploymentStage --- .../Apis/Details/Endpoints/AIEndpoints.jsx | 34 ++++++----- .../MultiEndpointComponents/EndpointCard.jsx | 60 +++++++++---------- .../CustomPolicies/ModelRoundRobin.tsx | 4 +- .../ModelWeightedRoundRobin.tsx | 4 +- .../Policies/CustomPolicies/Types.d.ts | 2 +- .../webapp/source/src/app/data/Constants.js | 4 +- 6 files changed, 56 insertions(+), 52 deletions(-) diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx index 9d0c22ebe5b..ff24e7b479d 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints.jsx @@ -190,10 +190,10 @@ const AIEndpoints = ({ endpointsPromise .then((response) => { const endpoints = response.body.list; - + // Filter endpoints based on endpoint type - const prodEndpointList = endpoints.filter((endpoint) => endpoint.environment === 'PRODUCTION'); - const sandEndpointList = endpoints.filter((endpoint) => endpoint.environment === 'SANDBOX'); + const prodEndpointList = endpoints.filter((endpoint) => endpoint.deploymentStage === 'PRODUCTION'); + const sandEndpointList = endpoints.filter((endpoint) => endpoint.deploymentStage === 'SANDBOX'); setProductionEndpoints(prodEndpointList); setSandboxEndpoints(sandEndpointList); @@ -246,22 +246,22 @@ const AIEndpoints = ({ const toggleAddProductionEndpoint = () => { setShowAddProductionEndpoint(!showAddProductionEndpoint); }; - + const toggleAddSandboxEndpoint = () => { setShowAddSandboxEndpoint(!showAddSandboxEndpoint); }; - const getDefaultEndpoint = (environment) => { + const getDefaultEndpoint = (deploymentStage) => { return { ...CONSTS.DEFAULT_ENDPOINT, - environment, + deploymentStage, } }; - const handlePrimaryEndpointChange = (environment, event) => { - if (environment === CONSTS.ENVIRONMENTS.production) { + const handlePrimaryEndpointChange = (deploymentStage, event) => { + if (deploymentStage === CONSTS.DEPLOYMENT_STAGE.production) { setPrimaryProductionEndpoint(event.target.value); - } else if (environment === CONSTS.ENVIRONMENTS.sandbox) { + } else if (deploymentStage === CONSTS.DEPLOYMENT_STAGE.sandbox) { setPrimarySandboxEndpoint(event.target.value); } }; @@ -338,7 +338,7 @@ const AIEndpoints = ({ - + handlePrimaryEndpointChange(CONSTS.ENVIRONMENTS.production, e)} + onChange={ + (e) => handlePrimaryEndpointChange(CONSTS.DEPLOYMENT_STAGE.production, e) + } > None @@ -374,7 +376,9 @@ const AIEndpoints = ({ id='primary-sandbox-endpoint' value={primarySandboxEndpoint || ''} label='Primary Sandbox Endpoint' - onChange={(e) => handlePrimaryEndpointChange(CONSTS.ENVIRONMENTS.sandbox, e)} + onChange={ + (e) => handlePrimaryEndpointChange(CONSTS.DEPLOYMENT_STAGE.sandbox, e) + } > None @@ -386,7 +390,7 @@ const AIEndpoints = ({ - +