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 deleted file mode 100644 index 6b8fdf7d8f8..00000000000 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpointAuth.jsx +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2024, 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, useState } from 'react'; -import PropTypes from 'prop-types'; -import { isRestricted } from 'AppData/AuthManager'; -import { FormattedMessage, useIntl } from 'react-intl'; -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, 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(null); - const [isHeaderParameter] = useState(!!apiKeyParamConfig.authHeader); - const [showApiKey, setShowApiKey] = useState(false); - - const subtypeConfig = api.subtypeConfiguration && JSON.parse(api.subtypeConfiguration.configuration); - const llmProviderName = subtypeConfig ? subtypeConfig.llmProviderName : null; - - useEffect(() => { - setApiKeyValue( - 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}`; - // } - - // saveEndpointSecurityConfig({ - // ...CONSTS.DEFAULT_ENDPOINT_SECURITY, - // type: 'apikey', - // apiKeyIdentifier, - // apiKeyIdentifierType, - // apiKeyValue: newApiKeyValue, - // enabled: true, - // }, isProduction ? 'production' : 'sandbox'); - // }, []); - - const handleApiKeyChange = (event) => { - let apiKeyVal = event.target.value; - if (apiKeyVal === '********') { - apiKeyVal = ''; - } else if (apiKeyVal === '') { - apiKeyVal = null; - } else if (apiKeyVal.includes('********')) { - apiKeyVal = apiKeyVal.replace('********', ''); - } - setApiKeyValue(apiKeyVal); - }; - - const handleApiKeyBlur = () => { - let updatedApiKeyValue = apiKeyValue; - if ((llmProviderName === 'MistralAI' || llmProviderName === 'OpenAI') && - apiKeyValue !== null && apiKeyValue !== '') { - updatedApiKeyValue = `Bearer ${updatedApiKeyValue}`; - } - - saveEndpointSecurityConfig({ - ...CONSTS.DEFAULT_ENDPOINT_SECURITY, - type: 'apikey', - apiKeyIdentifier, - apiKeyIdentifierType, - apiKeyValue: updatedApiKeyValue, - enabled: true, - }, isProduction ? 'production' : 'sandbox'); - }; - - const handleToggleApiKeyVisibility = () => { - if (apiKeyValue !== '********') { - setShowApiKey((prev) => !prev); - } - }; - - return ( - <> - - - ) : ( - - )} - id={'api-key-id-' + endpoint.id} - value={apiKeyIdentifier} - placeholder={apiKeyIdentifier} - helperText=' ' - sx={{ width: '100%' }} - 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%' }} - onChange={handleApiKeyChange} - onBlur={handleApiKeyBlur} - error={!apiKeyValue} - helperText={!apiKeyValue ? ( - - ) : ' '} - variant='outlined' - margin='normal' - required - type={showApiKey ? 'text' : 'password'} - InputProps={{ - endAdornment: ( - - - - {showApiKey ? 'visibility' : 'visibility_off'} - - - - ), - }} - /> - - - ); -} - -AIEndpointAuth.propTypes = { - api: PropTypes.shape({}).isRequired, - saveEndpointSecurityConfig: PropTypes.func.isRequired, - isProduction: PropTypes.bool.isRequired, -}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints/AIEndpoints.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints/AIEndpoints.jsx index bca8cd56875..b72aac5dbcd 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints/AIEndpoints.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/AIEndpoints/AIEndpoints.jsx @@ -238,18 +238,27 @@ const AIEndpoints = ({ defaultMessage='Production Endpoints' /> - {productionEndpoints.map((endpoint) => ( - - ))} + {productionEndpoints.length > 0 ? ( + productionEndpoints.map((endpoint) => ( + + )) + ) : ( + + + + )} @@ -260,18 +269,27 @@ const AIEndpoints = ({ defaultMessage='Sandbox Endpoints' /> - {sandboxEndpoints.map((endpoint) => ( - - ))} + {sandboxEndpoints.length > 0 ? ( + sandboxEndpoints.map((endpoint) => ( + + )) + ) : ( + + + + )} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/index.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/index.jsx index cf46a2cd93a..855ee511836 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/index.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/index.jsx @@ -35,19 +35,19 @@ const Endpoint = () => { path={'/' + urlPrefix + '/:api_uuid/endpoints/'} component={() => } /> + {!isRestricted(['apim:api_manage']) && ( + } + /> + )} {!isRestricted(['apim:api_view', 'apim:api_manage']) && ( - <> - } - /> - } - /> - + } + /> )} 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 102b4a7d887..e937c59a3fa 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 @@ -25,8 +25,8 @@ 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 Button from '@mui/material/Button'; -import { Paper } from '@mui/material'; +import { Paper, IconButton } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; import { Endpoint, ModelData } from './Types'; interface ModelCardProps { @@ -57,9 +57,9 @@ const ModelCard: FC = ({ return ( <> - + - + = ({ - + = ({ {isWeightApplicable && ( - handleChange(e)} - fullWidth - /> + + handleChange(e)} + fullWidth + /> + )} - - {onDelete && ( - - )} - + + + + )} ); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelFailover.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelFailover.tsx index 65e3a81157e..20dc41888c0 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelFailover.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelFailover.tsx @@ -24,7 +24,6 @@ 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'; @@ -32,6 +31,11 @@ import { Progress } from 'AppComponents/Shared'; import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; import { Endpoint, ModelData } from './Types'; import ModelCard from './ModelCard'; +import { styled } from '@mui/material/styles'; +import Alert from '@mui/material/Alert'; +import { Link } from 'react-router-dom'; +import Switch from '@mui/material/Switch'; +import FormControlLabel from '@mui/material/FormControlLabel'; interface ModelConfig { targetModel: ModelData; @@ -50,6 +54,24 @@ interface ModelFailoverProps { manualPolicyConfig: string; } +const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ + minHeight: 48, + maxHeight: 48, + '&.Mui-expanded': { + minHeight: 48, + maxHeight: 48, + }, + '& .MuiAccordionSummary-content': { + margin: 0, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + '&.Mui-expanded': { + margin: 0, + } + } +})); + const ModelFailover: FC = ({ setManualPolicyConfig, manualPolicyConfig, @@ -77,6 +99,8 @@ const ModelFailover: FC = ({ const [productionEndpoints, setProductionEndpoints] = useState([]); const [sandboxEndpoints, setSandboxEndpoints] = useState([]); const [loading, setLoading] = useState(false); + const [productionEnabled, setProductionEnabled] = useState(false); + const [sandboxEnabled, setSandboxEnabled] = useState(false); const fetchEndpoints = () => { setLoading(true); @@ -84,10 +108,37 @@ const ModelFailover: FC = ({ endpointsPromise .then((response) => { const endpoints = response.body.list; + const defaultEndpoints = []; + + if (apiFromContext.endpointConfig?.production_endpoints) { + defaultEndpoints.push({ + id: `${apiFromContext.id}--PRODUCTION`, + name: 'Default Production Endpoint', + deploymentStage: 'PRODUCTION', + endpointConfig: { + production_endpoints: apiFromContext.endpointConfig.production_endpoints, + endpoint_security: apiFromContext.endpointConfig.endpoint_security + } + }); + } + + if (apiFromContext.endpointConfig?.sandbox_endpoints) { + defaultEndpoints.push({ + id: `${apiFromContext.id}--SANDBOX`, + name: 'Default Sandbox Endpoint', + deploymentStage: 'SANDBOX', + endpointConfig: { + sandbox_endpoints: apiFromContext.endpointConfig.sandbox_endpoints, + endpoint_security: apiFromContext.endpointConfig.endpoint_security + } + }); + } + + const allEndpoints = [...defaultEndpoints, ...endpoints]; // Filter endpoints based on endpoint type - const prodEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'PRODUCTION'); - const sandEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'SANDBOX'); + const prodEndpointList = allEndpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'PRODUCTION'); + const sandEndpointList = allEndpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'SANDBOX'); setProductionEndpoints(prodEndpointList); setSandboxEndpoints(sandEndpointList); @@ -115,7 +166,17 @@ const ModelFailover: FC = ({ useEffect(() => { if (manualPolicyConfig !== '') { - setConfig(JSON.parse(manualPolicyConfig.replace(/'/g, '"'))); + const parsedConfig = JSON.parse(manualPolicyConfig.replace(/'/g, '"')); + setConfig(parsedConfig); + + // Set toggle states based on whether there's any configuration + const hasProductionConfig = parsedConfig.production.targetModel.model !== '' + || parsedConfig.production.fallbackModels.length > 0; + const hasSandboxConfig = parsedConfig.sandbox.targetModel.model !== '' + || parsedConfig.sandbox.fallbackModels.length > 0; + + setProductionEnabled(hasProductionConfig); + setSandboxEnabled(hasSandboxConfig); } }, [manualPolicyConfig]); @@ -168,6 +229,57 @@ const ModelFailover: FC = ({ })); } + const isAddModelDisabled = (env: 'production' | 'sandbox') => { + if (modelList.length === 0) { + return true; + } + return env === 'production' ? productionEndpoints.length === 0 : sandboxEndpoints.length === 0; + }; + + const getEndpointsUrl = () => { + return `/apis/${apiFromContext.id}/endpoints`; + }; + + const handleProductionToggle = (event: React.ChangeEvent) => { + setProductionEnabled(event.target.checked); + if (!event.target.checked) { + setConfig(prev => ({ + ...prev, + production: { + targetModel: { + model: '', + endpointId: '', + }, + fallbackModels: [], + }, + })); + } + }; + + const handleSandboxToggle = (event: React.ChangeEvent) => { + setSandboxEnabled(event.target.checked); + if (!event.target.checked) { + setConfig(prev => ({ + ...prev, + sandbox: { + targetModel: { + model: '', + endpointId: '', + }, + fallbackModels: [], + }, + })); + } + }; + + const handleAccordionChange = (env: 'production' | 'sandbox') => (event: React.SyntheticEvent, expanded: boolean) => { + if (env === 'production') { + handleProductionToggle({ target: { checked: expanded } } as React.ChangeEvent); + } else { + handleSandboxToggle({ target: { checked: expanded } } as React.ChangeEvent); + } + }; + if (loading) { return ; } @@ -175,20 +287,61 @@ const ModelFailover: FC = ({ return ( <> - - } + + - - + + + + } + label="" /> - - + + {modelList.length === 0 && ( + + + + )} + {productionEndpoints.length === 0 && ( + + + configure endpoints + + ), + }} + /> + + )} + + + = ({ isWeightApplicable={false} onUpdate={(updatedTargetModel) => handleTargetModelUpdate('production', 0, updatedTargetModel)} /> + + +