- {renderConnectorConfigurations(gatewayConfigurations)}
+ {renderConnectorConfigurations(regularConfigurations)}
+ {mappingConfigurations.length > 0 && (
+
+ }>
+
+
+
+ {renderConnectorConfigurations(mappingConfigurations)}
+
+
+ )}
);
}
@@ -350,4 +398,6 @@ GatewayConfiguration.defaultProps = {
/>,
hasErrors: () => {},
validating: false,
+ planMappingErrors: {},
+ supportedApiTypes: [],
};
diff --git a/portals/admin/src/main/webapp/source/src/app/components/GatewayEnvironments/GatewayPlanMapping.jsx b/portals/admin/src/main/webapp/source/src/app/components/GatewayEnvironments/GatewayPlanMapping.jsx
new file mode 100644
index 00000000000..8d009e53e1b
--- /dev/null
+++ b/portals/admin/src/main/webapp/source/src/app/components/GatewayEnvironments/GatewayPlanMapping.jsx
@@ -0,0 +1,413 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+import API from 'AppData/api';
+import Box from '@mui/material/Box';
+import CircularProgress from '@mui/material/CircularProgress';
+import IconButton from '@mui/material/IconButton';
+import MenuItem from '@mui/material/MenuItem';
+import Stack from '@mui/material/Stack';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableCell from '@mui/material/TableCell';
+import TableHead from '@mui/material/TableHead';
+import TableRow from '@mui/material/TableRow';
+import TableContainer from '@mui/material/TableContainer';
+import TextField from '@mui/material/TextField';
+import FormLabel from '@mui/material/FormLabel';
+import FormHelperText from '@mui/material/FormHelperText';
+import Typography from '@mui/material/Typography';
+import DeleteIcon from '@mui/icons-material/Delete';
+import AddCircleIcon from '@mui/icons-material/AddCircle';
+import ClearIcon from '@mui/icons-material/Clear';
+import Alert from 'AppComponents/Shared/Alert';
+import { FormattedMessage } from 'react-intl';
+
+const DEFAULT_LEFT_COLUMN_LABEL = 'WSO2 Subscription Policy';
+const PLAN_MAPPING_TABLE_MAX_HEIGHT = 400;
+const NON_MAPPABLE_SUBSCRIPTION_POLICIES = [
+ 'Unauthenticated',
+ 'DefaultSubscriptionless',
+ 'AsyncDefaultSubscriptionless',
+];
+const PLAN_MAPPING_API_TYPES = {
+ EVENTCOUNTLIMIT: 'async',
+ AIAPIQUOTALIMIT: 'ai-api',
+};
+
+const resolveSubscriptionPolicyApiType = (policy) => {
+ const quotaType = String(policy?.defaultLimit?.type || '').toUpperCase();
+ return PLAN_MAPPING_API_TYPES[quotaType] || 'rest';
+};
+
+const buildPlanMappingValues = (subscriptionPolicies, supportedApiTypes) => {
+ const supportedApiTypeSet = new Set(supportedApiTypes || []);
+ return (subscriptionPolicies || []).reduce((values, policy) => {
+ const policyName = policy?.policyName || policy?.name || policy?.displayName;
+ if (!policyName || NON_MAPPABLE_SUBSCRIPTION_POLICIES.includes(policyName)) {
+ return values;
+ }
+ const apiType = resolveSubscriptionPolicyApiType(policy);
+ if (!supportedApiTypeSet.has(apiType)) {
+ return values;
+ }
+ return [
+ ...values,
+ {
+ policyName,
+ label: policy.displayName || policyName,
+ apiType,
+ },
+ ];
+ }, []);
+};
+
+/**
+ * Gateway plan mapping configuration.
+ * @param {Object} props component props.
+ * @returns {JSX.Element} rendered gateway plan mapping section.
+ */
+export default function GatewayPlanMapping(props) {
+ const {
+ gatewayConfiguration,
+ supportedApiTypes = [],
+ additionalProperties = {},
+ setAdditionalProperties = () => {},
+ planMappingErrors = {},
+ } = props;
+ const restApi = useMemo(() => new API(), []);
+ const [subscriptionPolicies, setSubscriptionPolicies] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [loadError, setLoadError] = useState('');
+ const [selectedPolicyName, setSelectedPolicyName] = useState('');
+ const [newRemotePlanName, setNewRemotePlanName] = useState('');
+ const [validationError, setValidationError] = useState({});
+
+ useEffect(() => {
+ let isActive = true;
+ setIsLoading(true);
+ setLoadError('');
+ restApi.getSubscritionPolicyList()
+ .then((result) => {
+ if (isActive) {
+ setSubscriptionPolicies(result.body?.list || []);
+ }
+ })
+ .catch((error) => {
+ if (isActive) {
+ setSubscriptionPolicies([]);
+ setLoadError(error?.response?.body?.description || error.message);
+ }
+ })
+ .finally(() => {
+ if (isActive) {
+ setIsLoading(false);
+ }
+ });
+ return () => {
+ isActive = false;
+ };
+ }, [restApi]);
+
+ const leftLabel = DEFAULT_LEFT_COLUMN_LABEL;
+ const rightLabel = gatewayConfiguration?.default || gatewayConfiguration.label || 'Value';
+ const planMappingPropertyPrefix = `${gatewayConfiguration.name}.`;
+ const values = useMemo(() => {
+ return buildPlanMappingValues(subscriptionPolicies, supportedApiTypes);
+ }, [subscriptionPolicies, supportedApiTypes]);
+ const compatibleValues = useMemo(() => {
+ return values.filter((mappingValue) => (
+ mappingValue && typeof mappingValue === 'object' && mappingValue.policyName
+ ));
+ }, [values]);
+
+ const getPlanMappingError = (localPolicyName) => {
+ return planMappingErrors[`${planMappingPropertyPrefix}${localPolicyName}`] || '';
+ };
+ const getPlanMappingPropertyKey = (localPolicyName) => `${planMappingPropertyPrefix}${localPolicyName}`;
+ const mappedValues = compatibleValues.filter((mappingValue) => {
+ const propertyKey = getPlanMappingPropertyKey(mappingValue.policyName);
+ return Boolean((additionalProperties[propertyKey] || '').trim());
+ });
+ const availableValues = compatibleValues.filter((mappingValue) => {
+ const propertyKey = getPlanMappingPropertyKey(mappingValue.policyName);
+ return !(additionalProperties[propertyKey] || '').trim();
+ });
+
+ const clearValues = () => {
+ setSelectedPolicyName('');
+ setNewRemotePlanName('');
+ setValidationError({});
+ };
+ const isAddDisabled = availableValues.length === 0 || !selectedPolicyName || !newRemotePlanName.trim();
+ const validateNewMapping = () => {
+ const nextValidationError = {};
+ if (!selectedPolicyName) {
+ nextValidationError.selectedPolicyName = (
+