diff --git a/portals/admin/src/main/webapp/.eslintrc.js b/portals/admin/src/main/webapp/.eslintrc.js index e9e2c91889c..578fb6018ac 100644 --- a/portals/admin/src/main/webapp/.eslintrc.js +++ b/portals/admin/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/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index 959c1410e0f..b364f2e2cfc 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", @@ -206,12 +233,13 @@ "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.", "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", @@ -220,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", @@ -324,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", @@ -578,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 +759,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 959c1410e0f..b364f2e2cfc 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", @@ -206,12 +233,13 @@ "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.", "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", @@ -220,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", @@ -324,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", @@ -578,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 +759,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/source/src/app/components/AiVendors/AddEditAiVendor.jsx b/portals/admin/src/main/webapp/source/src/app/components/AiVendors/AddEditAiVendor.jsx index 6ac150d0f31..f6c8034ac4f 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,12 +63,15 @@ function reducer(state, newValue) { case 'name': case 'apiVersion': case 'description': + case 'modelList': case 'apiDefinition': return { ...state, [field]: value }; - case 'model': + case 'requestModel': + case 'responseModel': case 'promptTokenCount': case 'completionTokenCount': case 'totalTokenCount': + case 'remainingTokenCount': return { ...state, configurations: { @@ -116,24 +120,40 @@ export default function AddEditAiVendor(props) { configurations: { metadata: [ { - attributeName: 'model', + attributeName: 'requestModel', inputSource: 'payload', attributeIdentifier: '', + required: false, + }, + { + attributeName: 'responseModel', + inputSource: 'payload', + attributeIdentifier: '', + required: true, }, { attributeName: 'promptTokenCount', inputSource: 'payload', attributeIdentifier: '', + required: true, }, { attributeName: 'completionTokenCount', inputSource: 'payload', attributeIdentifier: '', + required: true, }, { attributeName: 'totalTokenCount', inputSource: 'payload', attributeIdentifier: '', + required: true, + }, + { + attributeName: 'remainingTokenCount', + inputSource: 'header', + attributeIdentifier: '', + required: false, }, ], connectorType: '', @@ -141,6 +161,7 @@ export default function AddEditAiVendor(props) { authHeader: '', }, apiDefinition: '', + modelList: [], }); const [state, dispatch] = useReducer(reducer, initialState); @@ -166,8 +187,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) { @@ -230,7 +251,7 @@ export default function AddEditAiVendor(props) { } break; case 'attributeIdentifier': - if (fieldValue.trim() === '') { + if (fieldValue.required && fieldValue.attributeIdentifier.trim() === '') { error = intl.formatMessage({ id: 'AiVendors.AddEditAiVendor.is.empty.error.attributeIdentifier', defaultMessage: 'Attribute identifier is required.', @@ -253,7 +274,7 @@ export default function AddEditAiVendor(props) { const formHasErrors = (validatingActive = false) => { const metadataErrors = state.configurations.metadata.map((meta) => { - return hasErrors('attributeIdentifier', meta.attributeIdentifier, validatingActive) + return hasErrors('attributeIdentifier', meta, validatingActive) || hasErrors('inputSource', meta.inputSource, validatingActive); }); @@ -289,7 +310,9 @@ export default function AddEditAiVendor(props) { const newState = { ...state, configurations: updatedConfigurations, + modelList: JSON.stringify(state.modelList), }; + if (id) { await new API().updateAiVendor(id, { ...newState, apiDefinition: file }); Alert.success(`${state.name} ${intl.formatMessage({ @@ -303,6 +326,7 @@ export default function AddEditAiVendor(props) { defaultMessage: ' - AI/LLM Vendor added successfully.', })}`); } + setSaving(false); history.push('/settings/ai-vendors/'); } catch (e) { @@ -569,7 +593,7 @@ export default function AddEditAiVendor(props) { })} error={hasErrors( 'attributeIdentifier', - metadata.attributeIdentifier, + metadata, validating, )} /> @@ -770,7 +794,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 +805,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.', + })} +
+ )} + /> +
+
+ + + + + + + + + + + + + + + + + + + + {showAddProductionEndpoint && ( + + )} + {productionEndpoints.map((endpoint) => ( + + ))} + + + + + + + + + + + + + {showAddSandboxEndpoint && ( + + )} + {sandboxEndpoints.map((endpoint) => ( + + ))} + + + + + + + + + + + + + ); +} + +export default AIEndpoints; 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 1cfaf0c4da0..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 @@ -34,6 +34,7 @@ import { Alert } from 'AppComponents/Shared'; import API from 'AppData/api'; import EndpointOverview from './EndpointOverview'; +import AIEndpoints from './AIEndpoints'; import { createEndpointConfig, getEndpointTemplateByType } from './endpointUtils'; import { API_SECURITY_KEY_TYPE_PRODUCTION, API_SECURITY_KEY_TYPE_SANDBOX } from '../Configuration/components/APISecurity/components/apiSecurityConstants'; @@ -735,87 +736,100 @@ function Endpoints(props) { defaultMessage='Endpoints' /> -
- - - + {(api.subtypeConfiguration?.subtype === 'AIAPI' && ( + + ))} + {(api.subtypeConfiguration?.subtype !== 'AIAPI') && ( +
+ + + + - - { - endpointValidity.isValid - ?
- : ( - - - {endpointValidity.message} - - - ) - } - - - {api.isRevision || !endpointValidity.isValid - || (settings && settings.portalConfigurationOnlyModeEnabled) - || isRestricted(['apim:api_create'], api) ? ( - + ) : ( + - - ) : ( - + + + + - -
+
+ )}
)} ) 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..c6a95d7eaac --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/EndpointCard.jsx @@ -0,0 +1,679 @@ +/* + * 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, + Paper, +} from '@mui/material'; +import API from 'AppData/api'; +import CircularProgress from '@mui/material/CircularProgress'; +import CONSTS from 'AppData/Constants'; +import { green } from '@mui/material/colors'; +import Alert from 'AppComponents/Shared/Alert'; +import AIEndpointAuth from '../AIEndpointAuth'; +import AdvanceEndpointConfig from '../AdvancedConfig/AdvanceEndpointConfig'; + +const PREFIX = 'EndpointCard'; + +const classes = { + endpointCardWrapper: `${PREFIX}-endpointCardWrapper`, + textField: `${PREFIX}-textField`, + 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%', + }, + + [`& .${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 + }, + }, + } + 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 = ({ + apiObject, + endpoint, + apiKeyParamConfig, + setProductionEndpoints, + setSandboxEndpoints, + showAddEndpoint, + setShowAddEndpoint, +}) => { + const [category, setCategory] = useState(''); + const [isProduction, setProduction] = useState(false); + 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(); + + useEffect(() => { + setProduction(state.deploymentStage === CONSTS.DEPLOYMENT_STAGE.production); + try { + if (state.deploymentStage === CONSTS.DEPLOYMENT_STAGE.production) { + setCategory('production_endpoints'); + setEndpointUrl(state.endpointConfig.production_endpoints?.url); + setAdvancedConfig(state.endpointConfig.production_endpoints?.config ? + state.endpointConfig.production_endpoints.config : {} + ); + } else if (state.deploymentStage === CONSTS.DEPLOYMENT_STAGE.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.deploymentStage === 'PRODUCTION') { + setProductionEndpoints(prev => [...prev, newEndpoint]); + dispatch({ + field: 'reset', + value: { + ...CONSTS.DEFAULT_ENDPOINT, + deploymentStage: CONSTS.DEPLOYMENT_STAGE.production, + } + }); + } else if (newEndpoint.deploymentStage === 'SANDBOX') { + setSandboxEndpoints(prev => [...prev, newEndpoint]); + dispatch({ + field: 'reset', + value: { + ...CONSTS.DEFAULT_ENDPOINT, + deploymentStage: CONSTS.DEPLOYMENT_STAGE.sandbox, + } + }); + } + setEndpointUrl(''); + setAdvancedConfig({}); + + 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); + // 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 + .then((response) => { + const updatedEndpoint = response.body; + + if (updatedEndpoint.deploymentStage === 'PRODUCTION') { + + setProductionEndpoints(prev => + prev.map(endpointObj => ( + endpointObj.id === endpointId + ? { ...endpointObj, ...updatedEndpoint } + : endpointObj + )) + ); + } else if (updatedEndpoint.deploymentStage === '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, deploymentStage) => { + 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); + deleteEndpointPromise + .then(() => { + if (deploymentStage === 'PRODUCTION') { + setProductionEndpoints(prev => prev.filter(endpointObj => endpointObj.id !== endpointId)); + } else if (deploymentStage === '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); + }); + }; + + const saveEndpointSecurityConfig = (endpointSecurityObj, enType) => { + const newEndpointSecurityObj = endpointSecurityObj; + const secretPlaceholder = '******'; + newEndpointSecurityObj.clientSecret = newEndpointSecurityObj.clientSecret + === secretPlaceholder ? '' : newEndpointSecurityObj.clientSecret; + newEndpointSecurityObj.password = newEndpointSecurityObj.password + === secretPlaceholder ? '' : newEndpointSecurityObj.password; + 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) { + // TODO: check apikey textfield error + return true; + } else { + return false; + } + }; + + /** + * 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 === 'production_endpoints') { + dispatch({ + field: 'updateProductionAdvancedConfiguration', + value: { + config: advanceConfigObj, + }, + }); + } else if (category === 'sandbox_endpoints') { + dispatch({ + field: 'updateSandboxAdvancedConfiguration', + value: { + config: advanceConfigObj, + }, + }); + } + handleAdvancedConfigClose(); + }; + + 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, apiObject.id)} + disabled={(isRestricted(['apim:api_create'], apiObject)) || isUpdating} + id={state.id + '-endpoint-test-icon-btn'} + size='large'> + {isUpdating + ? + : ( + + )} + > + + check_circle + + + + )} + + + + )} + > + + settings + + + + + ), + }} + /> + + + + {showAddEndpoint + ? ( + <> + + + + ) : ( + <> + + + + )} + + + + {apiObject.gatewayType !== 'wso2/apk' && ( + + + + + + + + + + + )} + + ); +}; + +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/GeneralEndpointConfigurations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx new file mode 100644 index 00000000000..eab32802f70 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/MultiEndpointComponents/GeneralEndpointConfigurations.jsx @@ -0,0 +1,273 @@ +/* + * 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, 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([]); + } + }, []); + + return ( + + 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']) && ( + + + + + + )} + + + + + + + + + + ); +}; + +export default GeneralEndpointConfigurations; 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 e0aa0df97ec..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 @@ -16,7 +16,7 @@ * under the License. */ -import React, { useState, FC, useContext } from 'react'; +import React, { useState, FC, useContext, useEffect } from 'react'; import { styled } from '@mui/material/styles'; import { Grid, @@ -37,6 +37,8 @@ import { FormattedMessage, useIntl } from 'react-intl'; 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'; @@ -110,6 +112,14 @@ const General: FC = ({ const { updateApiOperations, updateAllApiOperations } = useContext(ApiOperationContext); 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 === 'modelRoundRobin' || policyObj && policyObj.name === 'modelWeightedRoundRobin') { + setManual(true); + } + }, [policyObj]); if (!policyObj) { return @@ -153,6 +163,10 @@ const General: FC = ({ } }); + if (policyObj.name === 'modelRoundRobin' || policyObj.name === 'modelWeightedRoundRobin') { + updateCandidates[policySpec.policyAttributes[0].name] = manualPolicyConfig; + } + // Saving field changes to backend const apiPolicyToSave = {...apiPolicy}; apiPolicyToSave.parameters = updateCandidates; @@ -305,7 +319,7 @@ const General: FC = ({
- {hasAttributes && ( + {(hasAttributes && !isManual) && (
- {policySpec.policyAttributes && policySpec.policyAttributes.map((spec: PolicySpecAttribute) => ( + {(isManual && policyObj.name === 'modelRoundRobin') && ( + + )} + {(isManual && policyObj.name === 'modelWeightedRoundRobin') && ( + + )} + {!isManual && policySpec.policyAttributes && policySpec.policyAttributes.map((spec: PolicySpecAttribute) => ( {/* When the attribute type is string or integer */} @@ -475,7 +501,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 new file mode 100644 index 00000000000..cd8c0dccc37 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelCard.tsx @@ -0,0 +1,143 @@ +/* + * 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 } from 'react'; + +import { FormattedMessage } 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'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; +import Button from '@mui/material/Button'; +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; +} + +const ModelCard: FC = ({ + modelData, + modelList, + endpointList, + isWeightedRoundRobinPolicy, + onUpdate, + onDelete, +}) => { + const { model, endpointId, weight } = modelData; + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + const updatedModel = { ...modelData, [name]: name === "weight" ? parseFloat(value) : value }; + + onUpdate(updatedModel); + } + + return ( + <> + + + + + + + + + + + + + + + {isWeightedRoundRobinPolicy && ( + handleChange(e)} + fullWidth + /> + )} + + + + + + + ); +} + +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..726bf073d1b --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelRoundRobin.tsx @@ -0,0 +1,239 @@ +/* + * 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 RoundRobinConfig { + production: ModelData[]; + sandbox: ModelData[]; + suspendDuration?: number; +} + +interface ModelRoundRobinProps { + setManualPolicyConfig: React.Dispatch>; + manualPolicyConfig: string; +} + +const ModelRoundRobin: 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.deploymentStage === 'PRODUCTION'); + const sandEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'SANDBOX'); + setProductionEndpoints(prodEndpointList); + setSandboxEndpoints(sandEndpointList); + + }).catch((error) => { + console.error(error); + }).finally(() => { + 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(() => { + fetchModelList(); + fetchEndpoints(); + }, []); + + useEffect(() => { + if (manualPolicyConfig !== '') { + setConfig(JSON.parse(manualPolicyConfig.replace(/'/g, '"'))); + } + }, [manualPolicyConfig]); + + useEffect(() => { + setManualPolicyConfig(JSON.stringify(config).replace(/"/g, "'")); + }, [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 ModelRoundRobin; 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..f7075720aa0 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/CustomPolicies/ModelWeightedRoundRobin.tsx @@ -0,0 +1,239 @@ +/* + * 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.deploymentStage === 'PRODUCTION'); + const sandEndpointList = endpoints.filter((endpoint: Endpoint) => endpoint.deploymentStage === 'SANDBOX'); + setProductionEndpoints(prodEndpointList); + setSandboxEndpoints(sandEndpointList); + + }).catch((error) => { + console.error(error); + }).finally(() => { + 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(() => { + fetchModelList(); + fetchEndpoints(); + }, []); + + useEffect(() => { + if (manualPolicyConfig !== '') { + setConfig(JSON.parse(manualPolicyConfig.replace(/'/g, '"'))); + } + }, [manualPolicyConfig]); + + useEffect(() => { + setManualPolicyConfig(JSON.stringify(config).replace(/"/g, "'")); + }, [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/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..0b300ba19c3 --- /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; + deploymentStage: 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/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 a2f0d5104e5..4788abad2f6 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,18 @@ const CONSTS = { proxyProtocol: '', }, }, + DEFAULT_ENDPOINT: { + id: null, + name: '', + deploymentStage: '', + endpointConfig: { + endpoint_type: 'http', + }, + }, + DEPLOYMENT_STAGE: { + production: 'PRODUCTION', + sandbox: 'SANDBOX', + }, GATEWAY_TYPE: { synapse: 'Synapse', choreoConnect: 'ChoreoConnect', 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 aa6a068f984..1e5c792ece5 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 @@ -3589,6 +3589,106 @@ 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 + * @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(), + ); + }); + } + + /** + * 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 = {