diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index 262a712f542..f6e9d7ac363 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -5711,6 +5711,12 @@ "value": "Response Flow" } ], + "Apis.Details.Policies.PoliciesSection.info": [ + { + "type": 0, + "value": "API level policies will execute before operation level policies" + } + ], "Apis.Details.Policies.PolicyConfigurationEditDrawer.title": [ { "type": 0, diff --git a/portals/publisher/src/main/webapp/site/public/locales/raw.en.json b/portals/publisher/src/main/webapp/site/public/locales/raw.en.json index 4a355f58c1e..4f90455c036 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/raw.en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/raw.en.json @@ -2712,6 +2712,9 @@ "Apis.Details.Policies.PoliciesExpansion.response.flow.title": { "defaultMessage": "Response Flow" }, + "Apis.Details.Policies.PoliciesSection.info": { + "defaultMessage": "API level policies will execute before operation level policies" + }, "Apis.Details.Policies.PolicyConfigurationEditDrawer.title": { "defaultMessage": "Configure {policy}" }, diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyCard.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyCard.tsx index 3624445dc50..c63014f4157 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyCard.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/AttachedPolicyCard.tsx @@ -51,6 +51,7 @@ interface AttachedPolicyCardProps { verb: string; target: string; allPolicies: PolicySpec[] | null; + isAPILevelPolicy: boolean; } /** @@ -66,6 +67,7 @@ const AttachedPolicyCard: FC = ({ verb, target, allPolicies, + isAPILevelPolicy, }) => { const classes = useStyles(); const { api } = useContext(ApiContext); @@ -232,6 +234,7 @@ const AttachedPolicyCard: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> )} 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 ad349e51684..3bdb75f82a1 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 @@ -68,10 +68,20 @@ interface GeneralProps { policySpec: PolicySpec; handleDrawerClose: () => void; isEditMode: boolean; + isAPILevelPolicy: boolean; } const General: FC = ({ - policyObj, setDroppedPolicy, currentFlow, target, verb, apiPolicy, policySpec, handleDrawerClose, isEditMode + policyObj, + setDroppedPolicy, + currentFlow, + target, + verb, + apiPolicy, + policySpec, + handleDrawerClose, + isEditMode, + isAPILevelPolicy, }) => { const intl = useIntl(); const classes = useStyles(); @@ -334,7 +344,7 @@ const General: FC = ({ name={spec.name} type={spec.type.toLowerCase() === 'integer' ? 'number' : 'text'} value={getValue(spec)} - onChange={(e) => onInputChange(e, spec.type)} + onChange={(e: any) => onInputChange(e, spec.type)} fullWidth /> )} @@ -407,7 +417,7 @@ const General: FC = ({ )} ))} - {setDroppedPolicy && ( + {setDroppedPolicy && !isAPILevelPolicy && ( = ({ target, verb, allPolicies, + isAPILevelPolicy, }) => { const reversedPolicyList = [...currentPolicyList].reverse(); const policyListToDisplay = @@ -116,6 +118,7 @@ const AttachedPolicyList: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> ))} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/OperationPolicy.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/OperationPolicy.tsx index b93785a7c89..ff53729cb57 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/OperationPolicy.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/OperationPolicy.tsx @@ -185,6 +185,7 @@ const OperationPolicy: FC = ({ allPolicies={allPolicies} isChoreoConnectEnabled={isChoreoConnectEnabled} policyList={policyList} + isAPILevelPolicy={false} /> 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 7daf97926e0..065b8a1a862 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 @@ -16,32 +16,29 @@ * under the License. */ -import { - Grid, makeStyles, Typography, -} from '@material-ui/core'; +import { makeStyles, Typography } from '@material-ui/core'; import Alert from 'AppComponents/Shared/Alert'; import React, { useState, useEffect, useMemo } from 'react'; import cloneDeep from 'lodash.clonedeep'; import Paper from '@material-ui/core/Paper'; import Box from '@material-ui/core/Box'; import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; -import { HTML5Backend } from 'react-dnd-html5-backend' +import { HTML5Backend } from 'react-dnd-html5-backend'; import { DndProvider } from 'react-dnd'; import { FormattedMessage } from 'react-intl'; -import CONSTS from 'AppData/Constants'; -import { isRestricted } from 'AppData/AuthManager'; import { mapAPIOperations } from 'AppComponents/Apis/Details/Resources/operationUtils'; import API from 'AppData/api'; import { Progress } from 'AppComponents/Shared'; import { arrayMove } from '@dnd-kit/sortable'; -import OperationPolicy from './OperationPolicy'; -import OperationsGroup from './OperationsGroup'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; import PolicyList from './PolicyList'; -import type { ApiPolicy, Policy, PolicySpec } from './Types'; +import type { ApiPolicy, Policy, PolicySpec, ApiLevelPolicy } from './Types'; import GatewaySelector from './GatewaySelector'; import { ApiOperationContextProvider } from './ApiOperationContext'; import { uuidv4 } from './Utils'; import SaveOperationPolicies from './SaveOperationPolicies'; +import PolicyPanel from './components/PolicyPanel'; const Configurations = require('Config'); @@ -54,12 +51,20 @@ const useStyles = makeStyles(() => ({ overflowY: 'scroll', }, paper: { - padding:'2px' + padding: '2px', }, ccTypography: { - paddingLeft:'10px', - marginTop:'20px' - } + paddingLeft: '10px', + marginTop: '20px', + }, + flowTabs: { + '& button': { + minWidth: 50, + }, + }, + flowTab: { + fontSize: 'smaller', + }, })); /** @@ -75,6 +80,7 @@ const Policies: React.FC = () => { const [expandedResource, setExpandedResource] = useState(null); const [isChoreoConnectEnabled, setIsChoreoConnectEnabled] = useState(api.gatewayType === 'wso2/choreo-connect'); const { showMultiVersionPolicies } = Configurations.apis; + const [selectedTab, setSelectedTab] = useState((api.apiPolicies != null) ? 0 : 1); // If Choreo Connect radio button is selected in GatewaySelector, it will pass // value as true to render other UI changes specific to the Choreo Connect. @@ -82,6 +88,29 @@ const Policies: React.FC = () => { setIsChoreoConnectEnabled(isCCEnabled); } + // Tabs + const apiLevelTab = 0; + const operationLevelTab = 1; + + const initApiLevelPolicy: ApiLevelPolicy = { + request: [], + response: [], + fault: [], + } + + const getInitPolicyState = (policyList: any) => { + // Iterating through the policy list of request flow, response flow and fault flow + for (const flow in policyList) { + if (Object.prototype.hasOwnProperty.call(policyList, flow)) { + const policyArray = policyList[flow]; + policyArray.forEach((policyItem: ApiPolicy) => { + // eslint-disable-next-line no-param-reassign + policyItem.uuid = uuidv4(); + }); + } + } + } + /** * Function to get the initial state of all the operation policies from the API object. * We are setting a unique ID for all the operation policies solely for UI specific operations. @@ -94,28 +123,30 @@ const Policies: React.FC = () => { clonedOperations.forEach((operation: any) => { if (operation.operationPolicies) { const { operationPolicies } = operation; - - // Iterating through the policy list of request flow, response flow and fault flow - for (const flow in operationPolicies) { - if (Object.prototype.hasOwnProperty.call(operationPolicies, flow)) { - const policyArray = operationPolicies[flow]; - policyArray.forEach((policyItem: ApiPolicy) => { - // eslint-disable-next-line no-param-reassign - policyItem.uuid = uuidv4(); - }); - } - } + getInitPolicyState(operationPolicies); } }); return clonedOperations; } + const getInitAPILevelPoliciesState = () => { + const clonedAPIPolicies = cloneDeep(api.apiPolicies); + if (api.apiPolicies != null) { + getInitPolicyState(clonedAPIPolicies); + } + return clonedAPIPolicies || initApiLevelPolicy; + }; + const [apiOperations, setApiOperations] = useState(getInitState); + const [apiLevelPolicies, setApiLevelPolicies] = useState(getInitAPILevelPoliciesState); const [openAPISpec, setOpenAPISpec] = useState(null); useEffect(() => { const currentOperations = getInitState(); setApiOperations(currentOperations); + + const currentAPIPolicies = getInitAPILevelPoliciesState(); + setApiLevelPolicies(currentAPIPolicies); }, [api]); /** @@ -139,11 +170,11 @@ const Policies: React.FC = () => { if (showMultiVersionPolicies) { // Get the union of policies depending on the policy display name and version unionByPolicyDisplayName = [...mergedList - .reduce((map, obj) => map.set(obj.name + obj.version, obj), new Map()).values()]; + .reduce((map, obj) => map.set(obj.name + obj.version, obj), new Map()).values()]; } else { // Get the union of policies depending on the policy display name unionByPolicyDisplayName = [...mergedList - .reduce((map, obj) => map.set(obj.name, obj), new Map()).values()]; + .reduce((map, obj) => map.set(obj.name, obj), new Map()).values()]; } unionByPolicyDisplayName.sort( (a: Policy, b: Policy) => a.name.localeCompare(b.name)) @@ -192,19 +223,21 @@ const Policies: React.FC = () => { // Iterating through the policy list of request flow, response flow and fault flow for (const flow in operationPolicies) { if (Object.prototype.hasOwnProperty.call(operationPolicies, flow)) { - operationPolicies[flow] = []; - } } } }); setApiOperations(newApiOperations); + setApiLevelPolicies(initApiLevelPolicy); } useEffect(() => { fetchPolicies(); - }, [isChoreoConnectEnabled]) + if (isChoreoConnectEnabled) { + setSelectedTab(1); + } + }, [isChoreoConnectEnabled]); useEffect(() => { // Update the Swagger spec object when API object gets changed @@ -246,24 +279,45 @@ const Policies: React.FC = () => { updatedOperation: any, target: string, verb: string, currentFlow: string, ) => { const newApiOperations: any = cloneDeep(apiOperations); - let operationInAction = newApiOperations.find((op: any) => - op.target === target && op.verb.toLowerCase() === verb.toLowerCase()); - - const operationFlowPolicy = - operationInAction.operationPolicies[currentFlow].find((p: any) => (p.policyId === updatedOperation.policyId - && p.uuid === updatedOperation.uuid)); + const newApiLevelPolicies: any = cloneDeep(apiLevelPolicies); + + const operationInAction = + selectedTab === operationLevelTab + ? newApiOperations.find( + (op: any) => + op.target === target && + op.verb.toLowerCase() === verb.toLowerCase(), + ) + : null; + + const flowPolicy = ( + selectedTab === apiLevelTab + ? newApiLevelPolicies + : operationInAction.operationPolicies + )[currentFlow].find( + (p: any) => + p.policyId === updatedOperation.policyId && + p.uuid === updatedOperation.uuid, + ); + - if (operationFlowPolicy) { - // Edit operation policy - operationFlowPolicy.parameters = { ...updatedOperation.parameters }; + if (flowPolicy) { + // Edit policy + flowPolicy.parameters = { ...updatedOperation.parameters }; } else { - // Add new operation policy + // Add new policy const uuid = uuidv4(); - operationInAction.operationPolicies[currentFlow].push({ ...updatedOperation, uuid }); + (selectedTab === apiLevelTab ? newApiLevelPolicies : operationInAction + .operationPolicies)[currentFlow].push({ ...updatedOperation, uuid } + ); } // Finally update the state - setApiOperations(newApiOperations); + if (selectedTab === apiLevelTab) { + setApiLevelPolicies(newApiLevelPolicies); + } else { + setApiOperations(newApiOperations); + } } /** @@ -294,20 +348,28 @@ const Policies: React.FC = () => { * @param {string} currentFlow depicts which flow needs to be udpated: request, response or fault */ const deleteApiOperation = (uuid: string, target: string, verb: string, currentFlow: string) => { - const newApiOperations: any = cloneDeep(apiOperations); - const operationInAction = newApiOperations.find((op: any) => - op.target === target && op.verb.toLowerCase() === verb.toLowerCase()); - // Find the location of the element using the following logic - /* - [{a:'1'},{a:'2'},{a:'1'}].map( i => i.a) will output ['1', '2', '1'] - [{a:'1'},{a:'2'},{a:'1'}].map( i => i.a).indexOf('2') will output the location of '2' - */ - const index = operationInAction.operationPolicies[currentFlow].map((p: any) => p.uuid).indexOf(uuid); - // delete the element - operationInAction.operationPolicies[currentFlow].splice(index, 1); - // Finally update the state - setApiOperations(newApiOperations); + if (selectedTab === apiLevelTab) { + const newApiLevelPolicies: any = cloneDeep(apiLevelPolicies); + const index = newApiLevelPolicies[currentFlow].map((p: any) => p.uuid).indexOf(uuid); + newApiLevelPolicies[currentFlow].splice(index, 1); + setApiLevelPolicies(newApiLevelPolicies); + } else { + const newApiOperations: any = cloneDeep(apiOperations); + const operationInAction = newApiOperations.find((op: any) => + op.target === target && op.verb.toLowerCase() === verb.toLowerCase()); + // Find the location of the element using the following logic + /* + [{a:'1'},{a:'2'},{a:'1'}].map( i => i.a) will output ['1', '2', '1'] + [{a:'1'},{a:'2'},{a:'1'}].map( i => i.a).indexOf('2') will output the location of '2' + */ + const index = operationInAction.operationPolicies[currentFlow].map((p: any) => p.uuid).indexOf(uuid); + // delete the element + operationInAction.operationPolicies[currentFlow].splice(index, 1); + + // Finally update the state + setApiOperations(newApiOperations); + } } /** @@ -321,16 +383,35 @@ const Policies: React.FC = () => { const rearrangeApiOperations = ( oldIndex: number, newIndex: number, target: string, verb: string, currentFlow: string, ) => { - const newApiOperations: any = cloneDeep(apiOperations); - let operationInAction = newApiOperations.find((op: any) => - op.target === target && op.verb.toLowerCase() === verb.toLowerCase()); - - const policyArray = operationInAction.operationPolicies[currentFlow]; - operationInAction.operationPolicies[currentFlow] = arrayMove(policyArray, oldIndex, newIndex); - - // Finally update the state - setApiOperations(newApiOperations); - } + if (selectedTab === apiLevelTab) { + const newAPIPolicies: any = cloneDeep(apiLevelPolicies); + const policyArray = newAPIPolicies[currentFlow]; + newAPIPolicies[currentFlow] = arrayMove(policyArray, oldIndex, newIndex); + setApiLevelPolicies(newAPIPolicies); + } else { + const newApiOperations: any = cloneDeep(apiOperations); + const operationInAction = newApiOperations.find((op: any) => + op.target === target && op.verb.toLowerCase() === verb.toLowerCase()); + const policyArray = operationInAction.operationPolicies[currentFlow]; + operationInAction.operationPolicies[currentFlow] = arrayMove(policyArray, oldIndex, newIndex); + setApiOperations(newApiOperations); + } + }; + + const deletePolicyUuid = (operationPolicies: any) => { + // Iterating through the policy list of request flow, response flow and fault flow + for (const flow in operationPolicies) { + if (Object.prototype.hasOwnProperty.call(operationPolicies, flow)) { + const policyArray = operationPolicies[flow]; + policyArray.forEach((policyItem: ApiPolicy) => { + if (policyItem.uuid) { + // eslint-disable-next-line no-param-reassign + delete policyItem.uuid; + } + }); + } + } + }; /** * To update the API object with the attached policies on Save. @@ -338,26 +419,16 @@ const Policies: React.FC = () => { const saveApi = () => { setUpdating(true); const newApiOperations: any = cloneDeep(apiOperations); + const newApiLevelPolicies: any = cloneDeep(apiLevelPolicies); let getewayTypeForPolicies = "wso2/synapse"; const getewayVendorForPolicies = "wso2"; + deletePolicyUuid(newApiLevelPolicies); // Set operation policies to the API object - newApiOperations.forEach((operation: any, index: any, array: any) => { + newApiOperations.forEach((operation: any) => { if (operation.operationPolicies) { const { operationPolicies } = operation; - - // Iterating through the policy list of request flow, response flow and fault flow - for (const flow in operationPolicies) { - if (Object.prototype.hasOwnProperty.call(operationPolicies, flow)) { - const policyArray = operationPolicies[flow]; - policyArray.forEach((policyItem: ApiPolicy) => { - if (policyItem.uuid) { - // eslint-disable-next-line no-param-reassign - delete policyItem.uuid; - } - }); - } - } + deletePolicyUuid(operationPolicies); } }); @@ -366,36 +437,43 @@ const Policies: React.FC = () => { getewayTypeForPolicies = "wso2/choreo-connect"; } - const updatePromise = updateAPI({ - operations: newApiOperations, - gatewayVendor: getewayVendorForPolicies, - gatewayType: getewayTypeForPolicies}); + const updatePromise = updateAPI({ + operations: newApiOperations, + apiPolicies: newApiLevelPolicies, + gatewayVendor: getewayVendorForPolicies, + gatewayType: getewayTypeForPolicies + }); updatePromise .finally(() => { setUpdating(false); }); - } + }; - // handles operations (verbs) for CC policy expansion. - const handleVerbsForCC = (verbObject: any) => { - const array = Object.entries(verbObject).map(([verb]) => { - return verb; - }) - // returns the first element since CC handles resource level policies only. - // therefore returning only the first verb (operation) here for the resource. - return array[0] - } + const handleTabChange = (tab: number) => { + setSelectedTab(tab); + }; /** * To memoize the value passed into ApiOperationContextProvider */ - const providerValue = useMemo(() => ({ - apiOperations, - updateApiOperations, - updateAllApiOperations, - deleteApiOperation, - rearrangeApiOperations, - }), [apiOperations, updateApiOperations, updateAllApiOperations, deleteApiOperation, rearrangeApiOperations]) + const providerValue = useMemo( + () => ({ + apiOperations, + apiLevelPolicies, + updateApiOperations, + updateAllApiOperations, + deleteApiOperation, + rearrangeApiOperations, + }), + [ + apiOperations, + apiLevelPolicies, + updateApiOperations, + updateAllApiOperations, + deleteApiOperation, + rearrangeApiOperations, + ], + ); if (!policies || !openAPISpec || updating) { return @@ -424,46 +502,69 @@ const Policies: React.FC = () => { - {Object.entries(openAPISpec.paths).map(([target, verbObject]: [string, any]) => ( - - - - - {Object.entries(verbObject).map(([verb, operation]) => { - return CONSTS.HTTP_METHODS.includes(verb) ? ( - - - - ) : null; - })} - - - - ))} + + + handleTabChange(tab) + } + indicatorColor='primary' + textColor='primary' + variant='fullWidth' + aria-label='Policies local to API' + className={classes.flowTabs} + > + + API Level Policies + + } + id='api-level-policies-tab' + aria-controls='api-level-policies-tabpanel' + disabled={isChoreoConnectEnabled} + /> + + Operation Level Policies + + } + id='operation-level-policies-tab' + aria-controls='operation-level-policies-tabpanel' + /> + + + + + + diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesExpansion.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesExpansion.tsx index 370334cc54f..d885ca37861 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesExpansion.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesExpansion.tsx @@ -57,6 +57,7 @@ interface PoliciesExpansionProps { allPolicies: PolicySpec[] | null; isChoreoConnectEnabled: boolean; policyList: Policy[]; + isAPILevelPolicy: boolean; } const PoliciesExpansion: FC = ({ @@ -65,6 +66,7 @@ const PoliciesExpansion: FC = ({ allPolicies, isChoreoConnectEnabled, policyList, + isAPILevelPolicy, }) => { // Policies attached for each request, response and fault flow const [requestFlowPolicyList, setRequestFlowPolicyList] = useState([]); @@ -78,6 +80,7 @@ const PoliciesExpansion: FC = ({ const classes = useStyles(); const { apiOperations } = useContext(ApiOperationContext); + const { apiLevelPolicies } = useContext(ApiOperationContext); const { api } = useContext(APIContext); useEffect(() => { @@ -102,15 +105,17 @@ const PoliciesExpansion: FC = ({ useEffect(() => { (async () => { - let operationInAction = apiOperations.find( + + const operationInAction = (!isAPILevelPolicy) ? apiOperations.find( (op: any) => op.target === target && op.verb.toLowerCase() === verb.toLowerCase(), - ); + ) : null; + const apiPolicies = (isAPILevelPolicy) ? apiLevelPolicies : null; // Populate request flow attached policy list const requestFlowList: AttachedPolicy[] = []; - const requestFlow = operationInAction.operationPolicies.request; + const requestFlow = (isAPILevelPolicy) ? apiPolicies.request : operationInAction.operationPolicies.request; for (const requestFlowAttachedPolicy of requestFlow) { const { policyId, policyName, policyVersion, uuid } = requestFlowAttachedPolicy; @@ -153,7 +158,7 @@ const PoliciesExpansion: FC = ({ // Populate response flow attached policy list const responseFlowList: AttachedPolicy[] = []; - const responseFlow = operationInAction.operationPolicies.response; + const responseFlow = isAPILevelPolicy ? apiPolicies.response : operationInAction.operationPolicies.response; for (const responseFlowAttachedPolicy of responseFlow) { const { policyId, policyName, policyVersion, uuid } = responseFlowAttachedPolicy; @@ -197,7 +202,7 @@ const PoliciesExpansion: FC = ({ if (!isChoreoConnectEnabled) { // Populate fault flow attached policy list const faultFlowList: AttachedPolicy[] = []; - const faultFlow = operationInAction.operationPolicies.fault; + const faultFlow = isAPILevelPolicy ? apiPolicies.fault : operationInAction.operationPolicies.fault; for (const faultFlowAttachedPolicy of faultFlow) { const { policyId, policyName, policyVersion, uuid } = faultFlowAttachedPolicy; @@ -214,7 +219,7 @@ const PoliciesExpansion: FC = ({ const policyObj = allPolicies?.find( (policy: PolicySpec) => policy.name === policyName && - policy.version == policyVersion, + policy.version === policyVersion, ); if (policyObj) { faultFlowList.push({ ...policyObj, uniqueKey: uuid }); @@ -239,7 +244,7 @@ const PoliciesExpansion: FC = ({ setFaultFlowPolicyList(faultFlowList); } })(); - }, [apiOperations]); + }, [apiOperations, apiLevelPolicies]); return ( @@ -268,6 +273,7 @@ const PoliciesExpansion: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> @@ -289,6 +295,7 @@ const PoliciesExpansion: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> {!isChoreoConnectEnabled && ( @@ -311,6 +318,7 @@ const PoliciesExpansion: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> )} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesSection.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesSection.tsx new file mode 100644 index 00000000000..ca3960160af --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PoliciesSection.tsx @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023, 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 { Grid, makeStyles } from '@material-ui/core'; +import React, { FC } from 'react'; +import Box from '@material-ui/core/Box'; +import CONSTS from 'AppData/Constants'; +import { isRestricted } from 'AppData/AuthManager'; +import Alert from '@material-ui/lab/Alert'; +import { FormattedMessage } from 'react-intl'; +import OperationPolicy from './OperationPolicy'; +import OperationsGroup from './OperationsGroup'; +import type { Policy, PolicySpec } from './Types'; +import PoliciesExpansion from './PoliciesExpansion'; + +const useStyles = makeStyles(() => ({ + gridItem: { + display: 'flex', + width: '100%', + }, + alert: { + backgroundColor: 'transparent', + marginTop: '-25px', + marginBottom: '-15px', + }, +})); + +interface PolicySectionProps { + openAPISpec: any; + isChoreoConnectEnabled: boolean; + isAPILevelTabSelected: boolean; + allPolicies: PolicySpec[] | null; + policyList: Policy[]; + api: any; + expandedResource: string | null; + setExpandedResource: React.Dispatch>; +} + +/** + * Renders the policy management page. + * @returns {TSX} Policy management page to render. + */ +const PoliciesSection: FC = ({ + openAPISpec, + isChoreoConnectEnabled, + isAPILevelTabSelected, + allPolicies, + policyList, + api, + expandedResource, + setExpandedResource, +}) => { + const classes = useStyles(); + const borderColor = ''; + + return ( + + {isAPILevelTabSelected ? ( + + + + + + + + ) : ( + + {!isChoreoConnectEnabled && ( + + + + + )} + {Object.entries(openAPISpec.paths).map(([target, verbObject]: [string, any]) => ( + + + + {Object.entries(verbObject).map(([verb, operation]) => { + return CONSTS.HTTP_METHODS.includes(verb) ? ( + + + + ) : null; + })} + + + + ))} + + )} + + ); +}; + +export default PoliciesSection; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfigurationEditDrawer.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfigurationEditDrawer.tsx index fb90ad58249..f0aa5a18708 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfigurationEditDrawer.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfigurationEditDrawer.tsx @@ -59,6 +59,7 @@ interface PolicyConfigurationEditDrawerProps { drawerOpen: boolean; setDrawerOpen: React.Dispatch>; allPolicies: PolicySpec[] | null; + isAPILevelPolicy: boolean; } /** @@ -74,10 +75,12 @@ const PolicyConfigurationEditDrawer: FC = ({ allPolicies, drawerOpen, setDrawerOpen, + isAPILevelPolicy, }) => { const classes = useStyles(); const { api } = useContext(ApiContext); const { apiOperations } = useContext(ApiOperationContext); + const { apiLevelPolicies } = useContext(ApiOperationContext); const [policySpec, setPolicySpec] = useState(); useEffect(() => { @@ -102,12 +105,12 @@ const PolicyConfigurationEditDrawer: FC = ({ })(); }, [policyObj]); - const operationInAction = apiOperations.find( + const operationInAction = (!isAPILevelPolicy) ? apiOperations.find( (op: any) => op.target === target && op.verb.toLowerCase() === verb.toLowerCase(), - ); - const operationFlowPolicy = operationInAction.operationPolicies[ + ) : null; + const operationFlowPolicy = ((isAPILevelPolicy) ? apiLevelPolicies : operationInAction.operationPolicies)[ currentFlow ].find((policy: any) => policy.uuid === policyObj?.uniqueKey); @@ -166,6 +169,7 @@ const PolicyConfigurationEditDrawer: FC = ({ apiPolicy={apiPolicy} handleDrawerClose={handleDrawerClose} isEditMode + isAPILevelPolicy={isAPILevelPolicy} /> )} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfiguringDrawer.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfiguringDrawer.tsx index 91db7621d85..ca78c9767e6 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfiguringDrawer.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyConfiguringDrawer.tsx @@ -55,6 +55,7 @@ interface PolicyConfiguringDrawerProps { target: string; verb: string; allPolicies: PolicySpec[] | null; + isAPILevelPolicy: boolean; } /** @@ -69,6 +70,7 @@ const PolicyConfiguringDrawer: FC = ({ target, verb, allPolicies, + isAPILevelPolicy, }) => { const classes = useStyles(); const [drawerOpen, setDrawerOpen] = useState(!!policyObj); @@ -164,6 +166,7 @@ const PolicyConfiguringDrawer: FC = ({ apiPolicy={apiPolicy} handleDrawerClose={handleDrawerClose} isEditMode={false} + isAPILevelPolicy={isAPILevelPolicy} /> diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyDropzone.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyDropzone.tsx index a6298636479..805c3a31160 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyDropzone.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/PolicyDropzone.tsx @@ -71,6 +71,7 @@ interface PolicyDropzoneProps { target: string; verb: string; allPolicies: PolicySpec[] | null; + isAPILevelPolicy: boolean; } /** @@ -87,6 +88,7 @@ const PolicyDropzone: FC = ({ target, verb, allPolicies, + isAPILevelPolicy, }) => { const classes = useStyles(); const [droppedPolicy, setDroppedPolicy] = useState(null); @@ -129,6 +131,7 @@ const PolicyDropzone: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> )} @@ -141,6 +144,7 @@ const PolicyDropzone: FC = ({ target={target} verb={verb} allPolicies={allPolicies} + isAPILevelPolicy={isAPILevelPolicy} /> )} 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 d961ac77a6c..2f802adc12b 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 @@ -86,3 +86,9 @@ export type ApiPolicy = { parameters: any; uuid?: string; }; + +export type ApiLevelPolicy = { + request?: any[]; + response?: any[]; + fault?: any[]; +}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/components/PolicyPanel.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/components/PolicyPanel.tsx new file mode 100644 index 00000000000..3d91221a3f5 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Policies/components/PolicyPanel.tsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023, 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 { Box } from '@material-ui/core'; +import React, { FC } from 'react'; +import PoliciesSection from '../PoliciesSection'; +import type { Policy, PolicySpec } from '../Types'; + +interface PolicyPanelProps { + children?: React.ReactNode; + index: number; + selectedTab: number; + openAPISpec: any; + isChoreoConnectEnabled: boolean; + isAPILevelTabSelected: boolean; + allPolicies: PolicySpec[] | null; + policyList: Policy[]; + api: any; + expandedResource: string | null; + setExpandedResource: React.Dispatch>; +} + +/** + * Tab panel component to render content of a particular tab. + * Renders the policy section under the relevant tab (i.e. API Level or Operation Level). + * @param {JSON} props Input props from parent components. + * @returns {TSX} Tab panel. + */ +const PolicyPanel: FC = ({ + index, + selectedTab, + openAPISpec, + isChoreoConnectEnabled, + isAPILevelTabSelected, + allPolicies, + policyList, + api, + expandedResource, + setExpandedResource, +}) => { + const tabs = ['api-level', 'operation-level']; + const currentTab = tabs[index]; + + return ( + + ); +}; + +export default PolicyPanel; diff --git a/tests/cypress/fixtures/api_artifacts/sampleAddHeader.j2 b/tests/cypress/fixtures/api_artifacts/sampleAddHeader.j2 deleted file mode 100644 index f5fe3c8f586..00000000000 --- a/tests/cypress/fixtures/api_artifacts/sampleAddHeader.j2 +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/cypress/fixtures/api_artifacts/samplePolicyTemplate.j2 b/tests/cypress/fixtures/api_artifacts/samplePolicyTemplate.j2 new file mode 100644 index 00000000000..31cb54dcc61 --- /dev/null +++ b/tests/cypress/fixtures/api_artifacts/samplePolicyTemplate.j2 @@ -0,0 +1 @@ + diff --git a/tests/cypress/integration/publisher/017-api-policies/00-common-policy.spec.js b/tests/cypress/integration/publisher/017-api-policies/00-common-policy.spec.js index 4b0e01221aa..17f9fae173d 100644 --- a/tests/cypress/integration/publisher/017-api-policies/00-common-policy.spec.js +++ b/tests/cypress/integration/publisher/017-api-policies/00-common-policy.spec.js @@ -28,14 +28,14 @@ describe("Common Policies", () => { it("Common Policy", () => { cy.visit(`/publisher/policies`); cy.get('[data-testid="add-new-common-policy"]').click(); - cy.get('#name').type('Add Header sample test'); + cy.get('#name').type('Common Policy Sample'); cy.get('#version').type('1'); - cy.get('input[name="description"]').type('Sample add header policy description'); + cy.get('input[name="description"]').type('Sample common policy description'); cy.get('#fault-select-check-box').uncheck() - //upload the policy file + // Upload policy file cy.get('#upload-policy-file-for-policy').then(function () { - const filepath = `api_artifacts/sampleAddHeader.j2` + const filepath = `api_artifacts/samplePolicyTemplate.j2` cy.get('input[type="file"]').attachFile(filepath) }); @@ -44,21 +44,19 @@ describe("Common Policies", () => { cy.get('[data-testid="add-policy-attribute-display-name-btn"]').type('Header Name'); cy.get('#attribute-require-btn').click(); - //save common policy + // Save Common policy cy.get('[data-testid="policy-create-save-btn"]').click(); cy.wait(2000); - //View Common policy - cy.get('[aria-label="View Add Header sample test"]').click(); - //Download file - cy.get('[data-testid="download-policy-file"]').click(); - const downloadsFolder = Cypress.config('downloadsFolder') - const downloadedFilename = `${downloadsFolder}/swagger.yaml`; + // View Common policy + cy.get('[aria-label="View Common Policy Sample"]').click(); + // Download file + cy.get('[data-testid="download-policy-file"]').click(); cy.get('[data-testid="done-view-policy-file"]').click(); - //Delete Common Policy - cy.get('[aria-label="Delete Add Header sample test"]').click(); + // Delete Common Policy + cy.get('[aria-label="Delete Common Policy Sample"]').click(); cy.contains('Yes').click(); cy.logoutFromPublisher(); diff --git a/tests/cypress/integration/publisher/017-api-policies/01-api-specific-policy.spec.js b/tests/cypress/integration/publisher/017-api-policies/01-api-specific-policy.spec.js index 66ffa02cfd1..bf85357980f 100644 --- a/tests/cypress/integration/publisher/017-api-policies/01-api-specific-policy.spec.js +++ b/tests/cypress/integration/publisher/017-api-policies/01-api-specific-policy.spec.js @@ -26,7 +26,6 @@ describe("Common Policies", () => { cy.loginToPublisher(publisher, password); }) - it("Api Specific Policy", { retries: { runMode: 3, @@ -36,51 +35,38 @@ describe("Common Policies", () => { Utils.addAPI({}).then((apiId) => { apiTestId = apiId; cy.visit(`/publisher/apis/${apiId}/policies`); - //Create API Specific Policy + + // Create API Specific Policy cy.get('[data-testid="add-new-api-specific-policy"]', {timeout: Cypress.config().largeTimeout}).click(); - cy.get('#name').type('Add Header sample test'); + cy.get('#name').type('API Specific Policy Sample'); cy.get('#version').type('1'); - cy.get('input[name="description"]').type('Sample add header policy description'); + cy.get('input[name="description"]').type('Sample API specific policy description'); cy.get('#fault-select-check-box').uncheck() - //upload the policy file + // Upload policy file cy.get('#upload-policy-file-for-policy').then(function () { - const filepath = `api_artifacts/sampleAddHeader.j2` + const filepath = `api_artifacts/samplePolicyTemplate.j2` cy.get('input[type="file"]').attachFile(filepath) }); cy.get('#add-policy-attributes-btn').click(); - cy.get('[data-testid="add-policy-attribute-name-btn"]').type('headerName'); - cy.get('[data-testid="add-policy-attribute-display-name-btn"]').type('Header Name'); + cy.get('[data-testid="add-policy-attribute-name-btn"]').type('sampleAttribute'); + cy.get('[data-testid="add-policy-attribute-display-name-btn"]').type('Sample Attribute'); cy.get('#attribute-require-btn').click(); - //save common policy + + // Save API specific policy cy.get('[data-testid="policy-create-save-btn"]').click(); cy.wait(2000); - //View API Specific Policy - cy.contains('Add Header sample test').trigger('mouseover'); - cy.get('[aria-label="view-AddHeadersampletest"]').click({force:true}); - //Download file + // View API specific policy + cy.contains('API Specific Policy Sample').trigger('mouseover'); + cy.get('[aria-label="view-APISpecificPolicySample"]').click({force:true}); + + // Download file cy.get('[data-testid="download-policy-file"]').click(); - cy.wait(2000); cy.get('[data-testid="done-view-policy-file"]').click(); - //Drag and Drop Policy - const dataTransfer = new DataTransfer(); - cy.contains('Add Header sample test').trigger('dragstart', { - dataTransfer - }); - - cy.contains('Drag and drop policies here').trigger('drop', { - // cy.('[data-testid="drop-policy-zone-request"]').trigger('drop', { - dataTransfer - }); - cy.get('#headerName').type('Testing'); - cy.get('[data-testid="policy-attached-details-save"]').click(); - cy.get('[data-testid="custom-select-save-button"]').scrollIntoView().click(); - cy.visit(`/publisher/apis/${apiId}/scopes`); - cy.visit(`/publisher/apis/${apiId}/policies`); - cy.wait(2000); + cy.logoutFromPublisher(); }); }); diff --git a/tests/cypress/integration/publisher/019-read-only-user/00-verify-that-read-only-user-cannot-create-update-api.spec.js b/tests/cypress/integration/publisher/019-read-only-user/00-verify-that-read-only-user-cannot-create-update-api.spec.js index 5fd768c2883..72102ebd6f8 100644 --- a/tests/cypress/integration/publisher/019-read-only-user/00-verify-that-read-only-user-cannot-create-update-api.spec.js +++ b/tests/cypress/integration/publisher/019-read-only-user/00-verify-that-read-only-user-cannot-create-update-api.spec.js @@ -270,7 +270,7 @@ describe("publisher-019-00 : Verify that read only user cannot create updte api" cy.get('[data-testid="create-policy-form"]').get('[data-testid="displayname"]').type("test name"); cy.get('[data-testid="create-policy-form"]').get('[data-testid="gateway-details-panel"]') .get('[data-testid="file-drop-zone"]').then(function () { - cy.get('input[type="file"]').attachFile('api_artifacts/sampleAddHeader.j2'); + cy.get('input[type="file"]').attachFile('api_artifacts/samplePolicyTemplate.j2'); }); cy.get('[data-testid="create-policy-form"]').get('[data-testid="policy-add-btn-panel"]') .get('[data-testid="policy-create-save-btn"]').should('be.disabled');