- Business Plans for Shared Organizations
+ Business Plans
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/ShareAPI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/ShareAPI.jsx
new file mode 100644
index 00000000000..8f598b4963e
--- /dev/null
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/ShareAPI.jsx
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) 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 { Link } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import Grid from '@mui/material/Grid';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+import { FormattedMessage, injectIntl, useIntl } from 'react-intl';
+import { CircularProgress } from '@mui/material';
+import Alert from 'AppComponents/Shared/Alert';
+import API from 'AppData/api';
+import { withAPI, useAPI } from 'AppComponents/Apis/Details/components/ApiContext';
+import CONSTS from 'AppData/Constants';
+import { isRestricted } from 'AppData/AuthManager';
+import OrganizationSubscriptionPoliciesManage from './OrganizationSubscriptionPoliciesManage';
+import SharedOrganizations from './SharedOrganizations';
+
+
+const PREFIX = 'ShareAPI';
+
+const classes = {
+ FormControl: `${PREFIX}-FormControl`,
+ FormControlOdd: `${PREFIX}-FormControlOdd`,
+ FormLabel: `${PREFIX}-FormLabel`,
+ buttonWrapper: `${PREFIX}-buttonWrapper`,
+ root: `${PREFIX}-root`,
+ group: `${PREFIX}-group`,
+ helpButton: `${PREFIX}-helpButton`,
+ helpIcon: `${PREFIX}-helpIcon`,
+ htmlTooltip: `${PREFIX}-htmlTooltip`,
+ buttonSection: `${PREFIX}-buttonSection`,
+ emptyBox: `${PREFIX}-emptyBox`
+}
+
+
+const Root = styled('div')((
+ {
+ theme
+ }
+) => ({
+ [`& .${classes.buttonSection}`]: {
+ marginTop: theme.spacing(2),
+ },
+
+ [`& .${classes.emptyBox}`]: {
+ marginTop: theme.spacing(2),
+ }
+}));
+
+/**
+ * ShareAPI component
+ *
+ * @class ShareAPI
+ * @extends {Component}
+ * @param {Object} props - The properties passed to the component.
+ * @param {Function} props.updateAPI - Function to update the API.
+ */
+function ShareAPI(props) {
+
+ const [api] = useAPI();
+ const intl = useIntl();
+ const { updateAPI } = props;
+ const restApi = new API();
+ const [tenants, setTenants] = useState(null);
+ const [updateInProgress, setUpdateInProgress] = useState(false);
+ const [organizationPolicies, setOrganizationPolicies] = useState([]);
+ const [organizations, setOrganizations] = useState({});
+ const [visibleOrganizations, setVisibleOrganizations] = useState(api.visibleOrganizations);
+ const [selectionMode, setSelectionMode] = useState("none");
+
+ /**
+ * Save API sharing information (visible organizations and organization policies)
+ */
+ function saveAPI() {
+ setUpdateInProgress(true);
+
+ let updatedVisibleOrganizations = [];
+ let updatedOrganizationPolicies = [...organizationPolicies];
+
+ if (selectionMode === "all") {
+ updatedVisibleOrganizations = ["all"];
+ updatedOrganizationPolicies = updatedOrganizationPolicies.filter(policy => policy.organizationID === "all");
+ } else if (selectionMode === "none") {
+ updatedVisibleOrganizations = ["none"];
+ updatedOrganizationPolicies = [];
+ } else if (selectionMode === "select") {
+ updatedVisibleOrganizations = visibleOrganizations;
+ updatedOrganizationPolicies = updatedOrganizationPolicies.filter(policy =>
+ visibleOrganizations.includes(policy.organizationID)
+ );
+ }
+
+ const newApi = {
+ visibleOrganizations : updatedVisibleOrganizations,
+ ...(api.apiType !== API.CONSTS.APIProduct && { organizationPolicies : updatedOrganizationPolicies }),
+ };
+ updateAPI(newApi)
+ .then(() => {
+ Alert.info(intl.formatMessage({
+ id: 'Apis.Details.ShareAPI.update.success',
+ defaultMessage: 'API sharing configurations updated successfully',
+ }));
+ })
+ .catch((error) => {
+ if (error.response) {
+ Alert.error(error.response.body.description);
+ } else {
+ Alert.error(intl.formatMessage({
+ id: 'Apis.Details.ShareAPI.update.error',
+ defaultMessage: 'Error occurred while updating API sharing configurations',
+ }));
+ }
+ }).finally(() => {
+ setUpdateInProgress(false);
+ });
+ }
+
+ useEffect(() => {
+ restApi.getTenantsByState(CONSTS.TENANT_STATE_ACTIVE)
+ .then((result) => {
+ setTenants(result.body.count);
+ });
+ restApi.organizations()
+ .then((result) => {
+ setOrganizations(result.body);
+ })
+ setOrganizationPolicies(api.organizationPolicies ? [...api.organizationPolicies] : []);
+ setVisibleOrganizations([...api.visibleOrganizations]);
+
+ if (visibleOrganizations.includes("none")) {
+ setSelectionMode("none");
+ } else if (visibleOrganizations.includes("all")) {
+ setSelectionMode("all");
+ } else {
+ setSelectionMode("select");
+ }
+ }, []);
+
+ const handleShareAPISave = () => {
+ saveAPI();
+ };
+
+ if (typeof tenants !== 'number') {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+ (
+
+
+
+
+ {(api.gatewayVendor === 'wso2') &&
+ (
+ <>
+ {organizations?.list?.length > 0 && selectionMode !== "none" &&
+
+ }
+ >
+ )}
+ {(api.gatewayVendor === 'wso2') && (
+
+
+
+
+
+
+
+
+ )}
+
+ )
+ );
+}
+
+
+ShareAPI.propTypes = {
+ api: PropTypes.shape({
+ id: PropTypes.string,
+ }).isRequired,
+ intl: PropTypes.shape({
+ formatMessage: PropTypes.func,
+ }).isRequired,
+};
+
+export default injectIntl(withAPI(ShareAPI));
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/SharedOrganizations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/SharedOrganizations.jsx
new file mode 100644
index 00000000000..0cd9d43d463
--- /dev/null
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/ShareAPI/SharedOrganizations.jsx
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) 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 from 'react';
+import { styled } from '@mui/material/styles';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import Autocomplete from '@mui/material/Autocomplete';
+import CheckBoxIcon from '@mui/icons-material/CheckBox';
+import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
+import HelpOutline from '@mui/icons-material/HelpOutline';
+import {
+ RadioGroup,
+ FormControlLabel,
+ FormLabel,
+ Radio,
+ TextField,
+ Checkbox,
+ Tooltip,
+ Box,
+ Paper,
+} from "@mui/material";
+
+const PREFIX = 'SharedOrganizations';
+
+const classes = {
+ tooltip: `${PREFIX}-tooltip`,
+ listItemText: `${PREFIX}-listItemText`,
+ sharedOrganizationsPaper: `${PREFIX}-sharedOrganizationsPaper`
+};
+
+const StyledBox = styled(Box)(({ theme }) => ({
+ [`& .${classes.tooltip}`]: {
+ top: theme.spacing(1),
+ marginLeft: theme.spacing(0.5),
+ },
+
+ [`& .${classes.listItemText}`]: {
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+
+ [`& .${classes.sharedOrganizationsPaper}`]: {
+ padding: theme.spacing(2),
+ }
+}));
+
+const icon = ;
+const checkedIcon = ;
+
+/**
+ * Render the organizations drop down.
+ * @param {JSON} props props passed from it's parents.
+ * @returns {JSX} Render the organizations drop down.
+ */
+function SharedOrganizations(props) {
+ const { organizations, visibleOrganizations, setVisibleOrganizations, selectionMode, setSelectionMode } = props;
+
+ if (organizations && !organizations.list) {
+ return null;
+ } else if (organizations && organizations.list) {
+ const optionsList = organizations.list;
+ const handleRadioChange = (event) => {
+ const { value } = event.target;
+ setSelectionMode(value);
+ };
+ const handleDropdownChange = (event, newValue) => {
+ setVisibleOrganizations(newValue.map((org) => org.organizationId));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ label='Do not share with any organization' />
+
+
+
+
+ >
+ )}
+ aria-label='Shared Organizations'
+ placement='right-end'
+ interactive
+ className={classes.tooltip}
+ >
+
+
+
+
+ } label='Share with all organizations' />
+
+
+
+
+ >
+ )}
+ aria-label='Shared Organizations'
+ placement='right-end'
+ interactive
+ className={classes.tooltip}
+ >
+
+
+
+ }
+ label='Share with only selected organizations' />
+
+ {selectionMode === "select" && (
+ option.displayName}
+ isOptionEqualToValue={(option, value) => option.organizationId === value.organizationId}
+ value={organizations.list.filter((org) =>
+ visibleOrganizations.includes(org.organizationId)
+ )}
+ onChange={handleDropdownChange}
+ renderOption={(optionProps, option, { selected }) => (
+
+
+ {option.displayName}
+
+ )}
+ renderInput={(params) => (
+
+ )}
+ />
+ )}
+
+
+ );
+ }
+}
+
+SharedOrganizations.defaultProps = {
+ organizations: [],
+ configDispatcher: PropTypes.func.isRequired,
+};
+
+export default SharedOrganizations;
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Subscriptions/Subscriptions.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Subscriptions/Subscriptions.jsx
index e245f52e66a..d54deb4026f 100644
--- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Subscriptions/Subscriptions.jsx
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Subscriptions/Subscriptions.jsx
@@ -37,7 +37,6 @@ import { useAppContext } from 'AppComponents/Shared/AppContext';
import { isRestricted } from 'AppData/AuthManager';
import SubscriptionsTable from './SubscriptionsTable';
import SubscriptionPoliciesManage from './SubscriptionPoliciesManage';
-import OrganizationSubscriptionPoliciesManage from './OrganizationSubscriptionPoliciesManage';
import SubscriptionAvailability from './SubscriptionAvailability';
const PREFIX = 'Subscriptions';
@@ -85,9 +84,6 @@ function Subscriptions(props) {
const { settings } = useAppContext();
const isSubValidationDisabled = api.policies && api.policies.length === 1
&& api.policies[0].includes(CONSTS.DEFAULT_SUBSCRIPTIONLESS_PLAN);
- const [organizationPolicies, setOrganizationPolicies] = useState([]);
- const [organizations, setOrganizations] = useState({});
- const [visibleOrganizations, setVisibleOrganizations] = useState(api.visibleOrganizations);
/**
* Save subscription information (policies, subscriptionAvailability, subscriptionAvailableTenants)
@@ -97,7 +93,6 @@ function Subscriptions(props) {
const { subscriptionAvailability } = availability;
const newApi = {
policies,
- ...(settings?.orgAccessControlEnabled && api.apiType !== API.CONSTS.APIProduct && { organizationPolicies }),
subscriptionAvailability,
subscriptionAvailableTenants: tenantList,
};
@@ -131,14 +126,6 @@ function Subscriptions(props) {
.then((result) => {
setSubscriptions(result.body.count);
});
- if (settings && settings.orgAccessControlEnabled && api.apiType !== API.CONSTS.APIProduct) {
- restApi.organizations()
- .then((result) => {
- setOrganizations(result.body.list);
- })
- setOrganizationPolicies(api.organizationPolicies ? [...api.organizationPolicies] : []);
- setVisibleOrganizations([...api.visibleOrganizations]);
- }
setPolices([...api.policies]);
setOriginalPolicies([...api.policies]);
}, []);
@@ -175,23 +162,12 @@ function Subscriptions(props) {
(
{(api.gatewayVendor === 'wso2') &&
(
- <>
-
- {organizations?.length > 0 &&
-
- }
- >
+
)}
{isSubValidationDisabled && (
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx
index 71e31d8dc58..27be170e2e7 100644
--- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx
@@ -42,6 +42,7 @@ import API from 'AppData/api';
import MUIAlert from 'AppComponents/Shared/MuiAlert';
import DeleteApiButton from './DeleteApiButton';
import CreateNewVersionButton from './CreateNewVersionButton';
+import ShareButton from './ShareButton';
const PREFIX = 'APIDetailsTopMenu';
const classes = {
@@ -466,6 +467,12 @@ const APIDetailsTopMenu = (props) => {
)}
{/* Page error banner */}
{/* end of Page error banner */}
+ {api.apiType !== API.CONSTS.APIProduct && isVisibleInStore
+ ? <>
+
+ > : null
+ }
{api.isRevision || (settings && settings.portalConfigurationOnlyModeEnabled)
? null :
<>
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/ShareButton.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/ShareButton.jsx
new file mode 100644
index 00000000000..614a0c7da42
--- /dev/null
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/ShareButton.jsx
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) 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 from 'react';
+import { styled } from '@mui/material/styles';
+import IosShareIcon from '@mui/icons-material/IosShare';
+import PropTypes from 'prop-types';
+import { Link, withRouter } from 'react-router-dom';
+import Typography from '@mui/material/Typography';
+import { FormattedMessage } from 'react-intl';
+
+import { resourceMethod, resourcePath, ScopeValidation } from 'AppData/ScopeValidation';
+import VerticalDivider from 'AppComponents/Shared/VerticalDivider';
+
+const PREFIX = 'ShareButton';
+
+const classes = {
+ root: `${PREFIX}-root`,
+ backLink: `${PREFIX}-backLink`,
+ backIcon: `${PREFIX}-backIcon`,
+ backText: `${PREFIX}-backText`,
+ shareAPIWrapper: `${PREFIX}-shareAPIWrapper`,
+ shareAPI: `${PREFIX}-shareAPI`,
+ linkText: `${PREFIX}-linkText`,
+};
+
+const Root = styled('div')(({ theme }) => ({
+ [`& .${classes.root}`]: {
+ background: theme.palette.background.paper,
+ borderBottom: 'solid 1px ' + theme.palette.grey.A200,
+ display: 'flex',
+ alignItems: 'center',
+ },
+ [`& .${classes.backLink}`]: {
+ alignItems: 'center',
+ textDecoration: 'none',
+ display: 'flex',
+ },
+ [`& .${classes.backIcon}`]: {
+ color: theme.palette.primary.main,
+ fontSize: 56,
+ cursor: 'pointer',
+ },
+ [`& .${classes.backText}`]: {
+ color: theme.palette.primary.main,
+ cursor: 'pointer',
+ fontFamily: theme.typography.fontFamily,
+ },
+ [`& .${classes.shareAPIWrapper}`]: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ },
+ [`& .${classes.shareAPI}`]: {
+ display: 'flex',
+ flexDirection: 'column',
+ textAlign: 'center',
+ justifyContent: 'center',
+ cursor: 'pointer',
+ color: theme.custom.shareButtonColor || 'inherit',
+ },
+ [`& .${classes.linkText}`]: {
+ fontSize: theme.typography.fontSize,
+ },
+}));
+
+/**
+ *
+ * Function to create a 'shareAPI' button
+ *
+ * @param {any} props props
+ * @returns {*} React ShareAPI function component
+ * @constructor
+ */
+function ShareButton(props) {
+ const { api } = props;
+ return (
+
+ {/* allowing share API based on scopes */}
+
+
+
+
+ );
+}
+
+ShareButton.propTypes = {
+ api: PropTypes.shape({
+ id: PropTypes.string,
+ }).isRequired,
+ history: PropTypes.shape({ push: PropTypes.func }).isRequired,
+};
+
+export default withRouter(ShareButton);
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/index.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/index.jsx
index 83089f24f45..abdb89b027d 100644
--- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/index.jsx
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/index.jsx
@@ -78,6 +78,7 @@ import Policies from './Policies/Policies';
import ExternalStores from './ExternalStores/ExternalStores';
import { APIProvider } from './components/ApiContext';
import CreateNewVersion from './NewVersion/NewVersion';
+import ShareAPI from './ShareAPI/ShareAPI';
import TryOutConsole from './TryOut/TryOutConsole';
import Compliance from './APICompliance/Compliance';
@@ -1065,6 +1066,8 @@ class Details extends Component {
path={Details.subPaths.PROPERTIES_PRODUCT}
component={() => }
/>
+ } />
} />