Skip to content
Open
7 changes: 7 additions & 0 deletions portals/admin/src/main/webapp/site/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,13 @@
"GatewayEnvironments.AddEditVhost.httpsPort": "HTTPS Port",
"GatewayEnvironments.AddEditVhost.wsPort": "WS Port",
"GatewayEnvironments.AddEditVhost.wssPort": "WSS Port",
"GatewayEnvironments.GatewayConfiguration.advancedSettings": "Advanced Settings",
"GatewayEnvironments.PlanMapping.apiType.ai": "AI APIs",
"GatewayEnvironments.PlanMapping.apiType.async": "Async APIs",
"GatewayEnvironments.PlanMapping.apiType.other": "Other APIs",
"GatewayEnvironments.PlanMapping.apiType.rest": "REST APIs",
"GatewayEnvironments.PlanMapping.helper": "Map each local subscription plan to the corresponding gateway plan identifier.",
"GatewayEnvironments.PlanMapping.noCompatibleLocalPlans": "No local subscription plans match the supported API types of this gateway.",
"Gateways.AddEditGateway.back.to.gateways": "Back to Gateways",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"Gateways.AddEditGateway.loading.gateway.details": "Loading gateway details...",
"Gateways.AddEditGateway.loading.platform.gateway.details": "Loading platform gateway details...",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
import { FormattedMessage } from 'react-intl';
import InputLabel from '@mui/material/InputLabel';
import FormHelperText from '@mui/material/FormHelperText';
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 CustomGatewayInputField from 'AppComponents/GatewayEnvironments/CustomGatewayInputField';
import GatewayPlanMapping from './GatewayPlanMapping';

const StyledSpan = styled('span')(({ theme }) => ({ color: theme.palette.error.dark }));
const PLAN_MAPPING_PROPERTY_PREFIX = 'plan_mapping.';

// Styled wrapper to mimic TextField's outlined style
const StyledFormControl = styled(FormControl)(({ theme }) => ({
Expand Down Expand Up @@ -167,7 +173,10 @@

// Clear any properties in additionalProperties that are not in the current valid set
Object.keys(additionalProperties).forEach((propName) => {
if (!currentValidProperties.includes(propName)) {
if (
!currentValidProperties.includes(propName)
&& !propName.startsWith(PLAN_MAPPING_PROPERTY_PREFIX)
) {
setAdditionalProperties(propName, undefined);
}
});
Expand Down Expand Up @@ -207,7 +216,15 @@
});
}
}
if (gatewayConfiguration.type === 'input') {
if (gatewayConfiguration.type === 'mapping') {
return (
<GatewayPlanMapping
gatewayConfiguration={gatewayConfiguration}
additionalProperties={additionalProperties}
setAdditionalProperties={setAdditionalProperties}
/>
);
} else if (gatewayConfiguration.type === 'input') {
if (gatewayConfiguration.mask) {
return (
<FormControl variant='outlined' fullWidth disabled={disabled}>
Expand Down Expand Up @@ -333,9 +350,25 @@
});
};

const regularConfigurations = gatewayConfigurations.filter((config) => config.type !== 'mapping');

Check warning on line 353 in portals/admin/src/main/webapp/source/src/app/components/GatewayEnvironments/GatewayConfiguration.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'gatewayConfigurations.filter' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=wso2_apim-apps&issues=AZ3S2IODMYklAafZIQbT&open=AZ3S2IODMYklAafZIQbT&pullRequest=1338
const mappingConfigurations = gatewayConfigurations.filter((config) => config.type === 'mapping');

Check warning on line 354 in portals/admin/src/main/webapp/source/src/app/components/GatewayEnvironments/GatewayConfiguration.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'gatewayConfigurations.filter' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=wso2_apim-apps&issues=AZ3S2IODMYklAafZIQbU&open=AZ3S2IODMYklAafZIQbU&pullRequest=1338

return (
<div>
{renderConnectorConfigurations(gatewayConfigurations)}
{renderConnectorConfigurations(regularConfigurations)}
{mappingConfigurations.length > 0 && (
<Accordion sx={{ mt: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<FormattedMessage
id='GatewayEnvironments.GatewayConfiguration.advancedSettings'
defaultMessage='Advanced Settings'
/>
</AccordionSummary>
<AccordionDetails>
{renderConnectorConfigurations(mappingConfigurations)}
</AccordionDetails>
</Accordion>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
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 TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
import FormLabel from '@mui/material/FormLabel';
import FormHelperText from '@mui/material/FormHelperText';
import Tooltip from '@mui/material/Tooltip';
import HelpOutline from '@mui/icons-material/HelpOutline';
import Typography from '@mui/material/Typography';
import { FormattedMessage } from 'react-intl';

const PLAN_MAPPING_PROPERTY_PREFIX = 'plan_mapping.';

/**
* Gateway plan mapping configuration.
* @param {Object} props component props.
* @returns {JSX.Element} rendered gateway plan mapping section.
*/
export default function GatewayPlanMapping(props) {
const {
gatewayConfiguration,
additionalProperties = {},
setAdditionalProperties = () => {},
} = props;

const leftLabel = gatewayConfiguration?.labels?.left || 'Key';
const rightLabel = gatewayConfiguration?.labels?.right || gatewayConfiguration.label || 'Value';
const values = Array.isArray(gatewayConfiguration.values) ? gatewayConfiguration.values : [];
const groupedValues = values.reduce((groups, mappingValue) => {
if (!mappingValue || typeof mappingValue !== 'object' || !mappingValue.id) {
return groups;
}
const apiType = mappingValue.apiType || 'other';
return {
...groups,
[apiType]: [...(groups[apiType] || []), mappingValue],
};
}, {});
const groupedValuesForDisplay = {
...groupedValues,
...(!groupedValues.async && groupedValues.rest ? { async: groupedValues.rest } : {}),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const apiTypeOrder = ['rest', 'async', 'ai-api', 'other'];
const apiTypeLabels = {
rest: (
<FormattedMessage
id='GatewayEnvironments.PlanMapping.apiType.rest'
defaultMessage='REST APIs'
/>
),
async: (
<FormattedMessage
id='GatewayEnvironments.PlanMapping.apiType.async'
defaultMessage='Async APIs'
/>
),
'ai-api': (
<FormattedMessage
id='GatewayEnvironments.PlanMapping.apiType.ai'
defaultMessage='AI APIs'
/>
),
other: (
<FormattedMessage
id='GatewayEnvironments.PlanMapping.apiType.other'
defaultMessage='Other APIs'
/>
),
};
const orderedApiTypes = [
...apiTypeOrder.filter((apiType) => groupedValuesForDisplay[apiType]?.length > 0),
...Object.keys(groupedValuesForDisplay).filter((apiType) => !apiTypeOrder.includes(apiType)),
];

const getPlanMappingValue = (localPolicyId) => {
return additionalProperties[`${PLAN_MAPPING_PROPERTY_PREFIX}${localPolicyId}`] || '';
};
const tableStyles = {
'& .MuiTableCell-head': {
fontWeight: 500,
color: '#8A94A6',
fontSize: '0.8rem',
borderBottom: '1px solid #EEF1F6',
px: 2,
py: 1,
},
'& .MuiTableCell-body': {
fontSize: '0.75rem',
borderBottom: '1px solid #EEF1F6',
color: '#2F3441',
px: 2,
py: 1.5,
verticalAlign: 'middle',
},
'& .MuiTableRow-root:last-of-type .MuiTableCell-body': {
borderBottom: 'none',
},
};

return (
<Box mt={1}>
<Stack direction='row' spacing={1} alignItems='center' mb={0.75}>
{gatewayConfiguration.label && (
<FormLabel component='legend'>{gatewayConfiguration.label}</FormLabel>
)}
{gatewayConfiguration.tooltip && (
<Tooltip title={gatewayConfiguration.tooltip} placement='right-end' interactive>
<HelpOutline fontSize='small' color='action' />
</Tooltip>
)}
</Stack>
<Typography variant='body2' color='text.secondary' sx={{ mb: 2 }}>
<FormattedMessage
id='GatewayEnvironments.PlanMapping.helper'
defaultMessage='Map each local subscription plan to the corresponding gateway plan identifier.'
/>
</Typography>
{orderedApiTypes.length === 0 ? (
<FormHelperText>
<FormattedMessage
id='GatewayEnvironments.PlanMapping.noCompatibleLocalPlans'
defaultMessage='No local subscription plans match the supported API types of this gateway.'
/>
</FormHelperText>
) : orderedApiTypes.map((apiType) => (
<Box
key={apiType}
mb={2}
sx={{
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: 'hidden',
backgroundColor: 'background.paper',
}}
>
<Box
display='flex'
alignItems='center'
px={2}
py={1.25}
sx={{ backgroundColor: 'action.hover' }}
>
<Typography variant='subtitle2'>
{apiTypeLabels[apiType] || apiType}
</Typography>
</Box>
<TableContainer>
<Table size='small' sx={tableStyles}>
<TableHead>
<TableRow>
<TableCell>{leftLabel}</TableCell>
<TableCell>{rightLabel}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{groupedValuesForDisplay[apiType].map((mappingValue) => (
<TableRow key={`${apiType}.${mappingValue.id}`}>
<TableCell component='th' scope='row'>
<Typography variant='body2' fontWeight={500}>
{mappingValue.label || mappingValue.id}
</Typography>
</TableCell>
<TableCell>
<TextField
id={`${gatewayConfiguration.name}.${mappingValue.id}`}
margin='dense'
name={mappingValue.id}
fullWidth
variant='outlined'
value={getPlanMappingValue(mappingValue.id)}
onChange={(event) => setAdditionalProperties(
`${PLAN_MAPPING_PROPERTY_PREFIX}${mappingValue.id}`,
event.target.value || undefined,
)}
placeholder={mappingValue.label || mappingValue.id}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
))}
</Box>
);
}

GatewayPlanMapping.propTypes = {
gatewayConfiguration: PropTypes.shape({
name: PropTypes.string,
label: PropTypes.string,
tooltip: PropTypes.string,
labels: PropTypes.shape({
left: PropTypes.string,
right: PropTypes.string,
}),
values: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
id: PropTypes.string,
label: PropTypes.string,
apiType: PropTypes.string,
}),
])),
}).isRequired,
additionalProperties: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
])),
setAdditionalProperties: PropTypes.func,
};

GatewayPlanMapping.defaultProps = {
additionalProperties: {},
setAdditionalProperties: () => {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"Apis.Details.APIKeys.ApiKeyAssociation.associateSuccess.message": "API key has been successfully associated with the application.",
"Apis.Details.APIKeys.ApiKeyAssociation.associateSuccess.title": "Association Successful",
"Apis.Details.APIKeys.ApiKeyAssociation.button.associate": "Associate",
"Apis.Details.APIKeys.ApiKeyAssociation.button.associating": "Associating...",
"Apis.Details.APIKeys.ApiKeyAssociation.button.cancel": "Cancel",
"Apis.Details.APIKeys.ApiKeyAssociation.button.ok": "OK",
"Apis.Details.APIKeys.ApiKeyAssociation.button.removeAssociation": "Remove Association",
Expand All @@ -51,6 +52,7 @@
"Apis.Details.APIKeys.ApiKeyGenerate.alert.regenerateFailed": "Failed to regenerate API Key. Please try again.",
"Apis.Details.APIKeys.ApiKeyGenerate.button.close": "Close",
"Apis.Details.APIKeys.ApiKeyGenerate.button.regenerate": "Regenerate",
"Apis.Details.APIKeys.ApiKeyGenerate.button.regenerating": "Regenerating...",
"Apis.Details.APIKeys.ApiKeyGenerate.copyAlert.title": "Please Copy the API Key",
"Apis.Details.APIKeys.ApiKeyGenerate.keyLabel": "API Key",
"Apis.Details.APIKeys.ApiKeyGenerate.regenerateKey.alert.message": "Please copy this regenerated API Key value as it will be displayed only for the current browser session. (The API Key will not be visible in the UI after the page is refreshed.)",
Expand Down Expand Up @@ -80,7 +82,9 @@
"Apis.Details.APIKeys.ApiKeyListing.button.generating": "Generating...",
"Apis.Details.APIKeys.ApiKeyListing.button.ok": "OK",
"Apis.Details.APIKeys.ApiKeyListing.button.removeAssociation": "Remove Association",
"Apis.Details.APIKeys.ApiKeyListing.button.removingAssociation": "Removing...",
"Apis.Details.APIKeys.ApiKeyListing.button.revoke": "Revoke",
"Apis.Details.APIKeys.ApiKeyListing.button.revoking": "Revoking...",
"Apis.Details.APIKeys.ApiKeyListing.column.actions": "Actions",
"Apis.Details.APIKeys.ApiKeyListing.column.apiKey": "API Key",
"Apis.Details.APIKeys.ApiKeyListing.column.application": "Application",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
Expand Down Expand Up @@ -225,8 +226,16 @@ export default function ApiKeyAssociation(apiUUID, refreshApiKeys, subscribedApp
onClick={handleAssociateKey}
variant='contained'
disabled={!selectedAppForAssociation || isAssociating}
startIcon={isAssociating ? <CircularProgress size={16} color='inherit' /> : null}
>
<FormattedMessage id='Apis.Details.APIKeys.ApiKeyAssociation.button.associate' defaultMessage='Associate' />
{isAssociating ? (
<FormattedMessage
id='Apis.Details.APIKeys.ApiKeyAssociation.button.associating'
defaultMessage='Associating...'
/>
) : (
<FormattedMessage id='Apis.Details.APIKeys.ApiKeyAssociation.button.associate' defaultMessage='Associate' />
)}
</Button>
</DialogActions>
</Dialog>
Expand Down Expand Up @@ -391,6 +400,9 @@ export default function ApiKeyAssociation(apiUUID, refreshApiKeys, subscribedApp
);

return {
isAssociating,
isDissociating,
selectedKeyForDissociate,
// Handlers
handleOpenAssociationModal,
handleRemoveAssociation,
Expand Down
Loading
Loading