diff --git a/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx b/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx
index a17c8856969..058f9fec2ee 100644
--- a/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx
+++ b/portals/admin/src/main/webapp/source/src/app/components/Base/RouteMenuMapping.jsx
@@ -41,6 +41,7 @@ import TenantConfSave from 'AppComponents/AdvancedSettings/TenantConfSave';
import GamesIcon from '@mui/icons-material/Games';
import CategoryIcon from '@mui/icons-material/Category';
+import BookmarksIcon from '@mui/icons-material/Bookmarks';
import PolicyIcon from '@mui/icons-material/Policy';
import BlockIcon from '@mui/icons-material/Block';
import AssignmentIcon from '@mui/icons-material/Assignment';
@@ -61,6 +62,7 @@ import VpnKeyIcon from '@mui/icons-material/VpnKey';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import ListApis from '../APISettings/ListApis';
import UsageReport from '../APISettings/UsageReport';
+import ListLabels from '../Labels/ListLabels';
const RouteMenuMapping = (intl) => [
{
@@ -263,6 +265,16 @@ const RouteMenuMapping = (intl) => [
},
],
},
+ {
+ id: 'Labels',
+ displayText: intl.formatMessage({
+ id: 'Base.RouteMenuMapping.labels',
+ defaultMessage: 'Labels',
+ }),
+ path: '/settings/labels',
+ component: ListLabels,
+ icon: ,
+ },
{
id: 'Tasks',
displayText: intl.formatMessage({
diff --git a/portals/admin/src/main/webapp/source/src/app/components/Labels/AddEditLabel.jsx b/portals/admin/src/main/webapp/source/src/app/components/Labels/AddEditLabel.jsx
new file mode 100644
index 00000000000..693ef2517a8
--- /dev/null
+++ b/portals/admin/src/main/webapp/source/src/app/components/Labels/AddEditLabel.jsx
@@ -0,0 +1,268 @@
+/*
+ * 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, { useReducer, useEffect, useState } from 'react';
+import { styled } from '@mui/material/styles';
+import API from 'AppData/api';
+import PropTypes from 'prop-types';
+import TextField from '@mui/material/TextField';
+import { FormattedMessage, useIntl } from 'react-intl';
+import FormDialogBase from 'AppComponents/AdminPages/Addons/FormDialogBase';
+import Alert from 'AppComponents/Shared/Alert';
+
+const StyledSpan = styled('span')(({ theme }) => ({ color: theme.palette.error.dark }));
+
+/**
+ * Reducer
+ * @param {JSON} state State
+ * @returns {Promise}.
+ */
+function reducer(state, { field, value }) {
+ switch (field) {
+ case 'name':
+ case 'description':
+ return { ...state, [field]: value };
+ case 'editDetails':
+ return value;
+ default:
+ return state;
+ }
+}
+
+/**
+ * Render a pop-up dialog to add/edit a Label
+ * @param {JSON} props .
+ * @returns {JSX}.
+ */
+function AddEdit(props) {
+ const {
+ updateList, dataRow, icon, triggerButtonText, title,
+ } = props;
+ const intl = useIntl();
+ const [initialState, setInitialState] = useState({
+ description: '',
+ });
+ const [editMode, setIsEditMode] = useState(false);
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const { name, description } = state;
+
+ const onChange = (e) => {
+ dispatch({ field: e.target.name, value: e.target.value });
+ };
+
+ useEffect(() => {
+ setInitialState({
+ description: '',
+ });
+ }, []);
+
+ const hasErrors = (fieldName, value) => {
+ let error;
+ switch (fieldName) {
+ case 'name':
+ if (value === undefined) {
+ error = false;
+ break;
+ }
+ if (value === '') {
+ error = intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.error.name.empty',
+ defaultMessage: 'Name is Empty',
+ });
+ } else if (value.length > 255) {
+ error = intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.error.name.too.long',
+ defaultMessage: 'Label name is too long',
+ });
+ } else if (/\s/.test(value)) {
+ error = intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.error.name.has.spaces',
+ defaultMessage: 'Name contains spaces',
+ });
+ } else if (/[!@#$%^&*(),?"{}[\]|<>\t\n]/i.test(value)) {
+ error = intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.error.name.has.special.chars',
+ defaultMessage: 'Name field contains special characters',
+ });
+ } else {
+ error = false;
+ }
+ break;
+ case 'description':
+ if (value && value.length > 1024) {
+ error = intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.error.description.too.long',
+ defaultMessage: 'Label description is too long',
+ });
+ }
+ break;
+ default:
+ break;
+ }
+ return error;
+ };
+ const getAllFormErrors = () => {
+ let errorText = '';
+ let NameErrors;
+ let DescriptionErrors;
+ if (name === undefined) {
+ dispatch({ field: 'name', value: '' });
+ NameErrors = hasErrors('name', '');
+ } else {
+ NameErrors = hasErrors('name', name);
+ }
+ if (NameErrors) {
+ errorText += NameErrors + '\n';
+ }
+ if (description !== undefined) {
+ DescriptionErrors = hasErrors('description', description);
+ }
+ if (DescriptionErrors) {
+ errorText += DescriptionErrors + '\n';
+ }
+ return errorText;
+ };
+ const formSaveCallback = () => {
+ const formErrors = getAllFormErrors();
+ if (formErrors !== '') {
+ Alert.error(formErrors);
+ return false;
+ }
+ const restApi = new API();
+ let promiseAPICall;
+ if (dataRow) {
+ // assign the update promise to the promiseAPICall
+ promiseAPICall = restApi.updateLabel(dataRow.id, name, description);
+ } else {
+ // assign the create promise to the promiseAPICall
+ promiseAPICall = restApi.createLabel(name, description);
+ }
+
+ return promiseAPICall
+ .then(() => {
+ if (dataRow) {
+ return (
+ intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.edit.successful',
+ defaultMessage: 'Label edited successfully',
+ })
+ );
+ } else {
+ return (
+ intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.add.successful',
+ defaultMessage: 'Label added successfully',
+ })
+ );
+ }
+ })
+ .catch((error) => {
+ const { response } = error;
+ if (response.body) {
+ throw response.body.description;
+ }
+ })
+ .finally(() => {
+ updateList();
+ });
+ };
+ const dialogOpenCallback = () => {
+ if (dataRow) {
+ const { name: originalName, description: originalDescription } = dataRow;
+ setIsEditMode(true);
+ dispatch({ field: 'editDetails', value: { name: originalName, description: originalDescription } });
+ }
+ };
+ return (
+
+
+
+ *
+
+ )}
+ fullWidth
+ error={hasErrors('name', name)}
+ helperText={hasErrors('name', name)
+ || intl.formatMessage({
+ id: 'AdminPages.Labels.AddEdit.form.name.helper.text',
+ defaultMessage: 'Name of the Label',
+ })}
+ variant='outlined'
+ disabled={editMode}
+ />
+
+
+ );
+}
+
+AddEdit.defaultProps = {
+ icon: null,
+ dataRow: null,
+};
+
+AddEdit.propTypes = {
+ updateList: PropTypes.func.isRequired,
+ dataRow: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ }),
+ icon: PropTypes.element,
+ triggerButtonText: PropTypes.oneOfType([
+ PropTypes.element.isRequired,
+ PropTypes.string.isRequired,
+ ]).isRequired,
+ title: PropTypes.shape({}).isRequired,
+};
+
+export default AddEdit;
diff --git a/portals/admin/src/main/webapp/source/src/app/components/Labels/DeleteLabel.jsx b/portals/admin/src/main/webapp/source/src/app/components/Labels/DeleteLabel.jsx
new file mode 100644
index 00000000000..7e17eaf3fea
--- /dev/null
+++ b/portals/admin/src/main/webapp/source/src/app/components/Labels/DeleteLabel.jsx
@@ -0,0 +1,99 @@
+/*
+ * 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 from 'react';
+import API from 'AppData/api';
+import PropTypes from 'prop-types';
+import { FormattedMessage, useIntl } from 'react-intl';
+import DialogContentText from '@mui/material/DialogContentText';
+import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
+import FormDialogBase from 'AppComponents/AdminPages/Addons/FormDialogBase';
+import Alert from 'AppComponents/Shared/Alert';
+
+/**
+ * Render delete dialog box.
+ * @param {JSON} props component props.
+ * @returns {JSX} Loading animation.
+ */
+function Delete({ updateList, dataRow }) {
+ const { id, noOfApis } = dataRow;
+ const intl = useIntl();
+ const getValidationErrors = () => {
+ let errorText = '';
+ if (noOfApis > 0) {
+ errorText += 'Unable to delete the Label. It is attached to API(s)';
+ }
+ return errorText;
+ };
+
+ const formSaveCallback = () => {
+ const validationErrors = getValidationErrors();
+ if (validationErrors !== '') {
+ Alert.error(validationErrors);
+ return false;
+ }
+
+ const restApi = new API();
+ return restApi
+ .deleteLabel(id)
+ .then(() => {
+ return (
+
+ );
+ })
+ .catch((error) => {
+ throw error.response.body.description;
+ })
+ .finally(() => {
+ updateList();
+ });
+ };
+
+ return (
+ }
+ formSaveCallback={formSaveCallback}
+ >
+
+
+
+
+ );
+}
+Delete.propTypes = {
+ updateList: PropTypes.number.isRequired,
+ dataRow: PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ noOfApis: PropTypes.number,
+ }).isRequired,
+};
+export default Delete;
diff --git a/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabelUsages.jsx b/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabelUsages.jsx
new file mode 100644
index 00000000000..396845edb4f
--- /dev/null
+++ b/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabelUsages.jsx
@@ -0,0 +1,319 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import { FormattedMessage, useIntl } from 'react-intl';
+import Typography from '@mui/material/Typography';
+import Grid from '@mui/material/Grid';
+import { styled, Alert as MUIAlert } from '@mui/material';
+import MUIDataTable from 'mui-datatables';
+import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase';
+import InlineProgress from 'AppComponents/AdminPages/Addons/InlineProgress';
+import Alert from 'AppComponents/Shared/Alert';
+import Paper from '@mui/material/Paper';
+import API from 'AppData/api';
+import WarningBase from 'AppComponents/AdminPages/Addons/WarningBase';
+import Box from '@mui/material/Box';
+
+const styles = {
+ searchInput: (theme) => ({
+ fontSize: theme.typography.fontSize,
+ }),
+ block: {
+ display: 'block',
+ },
+ contentWrapper: (theme) => ({
+ margin: theme.spacing(2),
+ }),
+ approveButton: (theme) => ({
+ textDecoration: 'none',
+ backgroundColor: theme.palette.success.light,
+ }),
+ rejectButton: (theme) => ({
+ textDecoration: 'none',
+ backgroundColor: theme.palette.error.light,
+ }),
+ pageTitle: {
+ minHeight: 43,
+ backgroundColor: '#f6f6f6',
+ },
+};
+
+const StyledDiv = styled('div')({});
+
+/**
+ * Render a list
+ * @param {JSON} props props passed from parent
+ * @returns {JSX} Header AppBar components.
+ */
+function ListLabelUsages(props) {
+ const intl = useIntl();
+ const restApi = new API();
+ const [data, setData] = useState(null);
+ const [hasListPermission, setHasListPermission] = useState(true);
+ const [errorMessage, setError] = useState(null);
+ const { id } = props;
+
+ /**
+ * API call to get Detected Data
+ * @returns {Promise}.
+ */
+ async function apiCall() {
+ return restApi
+ .getLabelApiUsages(id)
+ .then((result) => {
+ return result.body;
+ })
+ .catch((error) => {
+ const { status } = error;
+ if (status === 401) {
+ setHasListPermission(false);
+ } else {
+ Alert.error(intl.formatMessage({
+ id: 'Labels.ListLabelsAPIUsages.error',
+ defaultMessage: 'Unable to get Label API usage details',
+ }));
+ throw (error);
+ }
+ });
+ }
+
+ const fetchData = async () => {
+ const apiUsageData = await apiCall();
+ if (apiUsageData) {
+ setData(apiUsageData.apis);
+ } else {
+ setError('Unable to fetch data');
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const pageProps = {
+
+ pageStyle: 'half',
+ title: intl.formatMessage({
+ id: 'Labels.AddEditLabel.usages',
+ defaultMessage: 'Label Usage',
+ }),
+ };
+
+ const columApiProps = [
+ {
+ name: 'name',
+ label: intl.formatMessage({
+ id: 'Api.Name',
+ defaultMessage: 'API Name',
+ }),
+ options: {
+ sort: false,
+ filter: true,
+ },
+ },
+ {
+ name: 'version',
+ label: intl.formatMessage({
+ id: 'Api.Version',
+ defaultMessage: 'Version',
+ }),
+ options: {
+ sort: false,
+ filter: true,
+ },
+ },
+ {
+ name: 'provider',
+ label: intl.formatMessage({
+ id: 'Api.Provider',
+ defaultMessage: 'Provider',
+ }),
+ options: {
+ sort: false,
+ filter: true,
+ },
+ },
+ ];
+
+ const noDataMessage = (
+
+ );
+
+ const columnsApis = [
+ ...columApiProps,
+ ];
+
+ const options = {
+ selectableRows: 'none',
+ filter: false,
+ search: true,
+ print: false,
+ download: false,
+ viewColumns: false,
+ customToolbar: false,
+ responsive: 'stacked',
+ textLabels: {
+ toolbar: {
+ search: intl.formatMessage({
+ id: 'Mui.data.table.search.icon.label',
+ defaultMessage: 'Search',
+ }),
+ },
+ body: {
+ noMatch: intl.formatMessage({
+ id: 'Mui.data.table.search.no.records.found',
+ defaultMessage: 'Sorry, no matching records found',
+ }),
+ },
+ pagination: {
+ rowsPerPage: intl.formatMessage({
+ id: 'Mui.data.table.pagination.rows.per.page',
+ defaultMessage: 'Rows per page:',
+ }),
+ displayRows: intl.formatMessage({
+ id: 'Mui.data.table.pagination.display.rows',
+ defaultMessage: 'of',
+ }),
+ },
+ },
+ };
+
+ if (!hasListPermission) {
+ return (
+
+ )}
+ content={(
+
+ )}
+ />
+ );
+ }
+ if (!errorMessage && !data) {
+ return (
+
+
+
+
+ );
+ }
+ if (errorMessage) {
+ return (
+
+ {errorMessage}
+
+
+ );
+ }
+ return (
+ <>
+
+
+
+ {data.count > 0
+ ? (
+ <>
+
+
+
+ {data.count === 1
+ ? intl.formatMessage({
+ id: 'Labels.ListLabelUsages.API.usages'
+ + '.count.one',
+ defaultMessage: '1 API is using this Label'
+ + ' specifically.',
+ })
+ : intl.formatMessage({
+ id: 'Labels.ListLabelUsages.API.usages'
+ + '.count.multiple',
+ defaultMessage: '{count} APIs are using this label'
+ + ' specifically',
+ },
+ { count: data.count })}
+
+
+
+
+
+
+ {data && data.list.length > 0 && (
+
+ )}
+ {data && data.list.length === 0 && (
+
+
+ {noDataMessage}
+
+
+ )}
+
+
+ >
+ )
+ : (
+
+
+
+
+
+
+
+ )}
+
+
+
+ >
+ );
+}
+
+ListLabelUsages.propTypes = {
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ dialogOpen: PropTypes.bool.isRequired,
+ closeDialog: PropTypes.func.isRequired,
+};
+
+export default ListLabelUsages;
diff --git a/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabels.jsx b/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabels.jsx
new file mode 100644
index 00000000000..bf68d327b98
--- /dev/null
+++ b/portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabels.jsx
@@ -0,0 +1,226 @@
+/*
+ * 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, { useState } from 'react';
+import API from 'AppData/api';
+import { useIntl, FormattedMessage } from 'react-intl';
+import Typography from '@mui/material/Typography';
+import ListBase from 'AppComponents/AdminPages/Addons/ListBase';
+import Delete from 'AppComponents/Labels/DeleteLabel';
+import AddEdit from 'AppComponents/Labels/AddEditLabel';
+import EditIcon from '@mui/icons-material/Edit';
+import {
+ Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton,
+} from '@mui/material';
+import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
+import ListLabelUsages from './ListLabelUsages';
+
+/**
+ * API call to get Label list
+ * @returns {Promise}.
+ */
+function apiCall() {
+ const restApi = new API();
+ return restApi
+ .labelsListGet()
+ .then((result) => {
+ return result.body.list;
+ })
+ .catch((error) => {
+ throw error;
+ });
+}
+const TruncatedNameCell = ({ children }) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+/**
+ * Render a list
+ * @returns {JSX} Header AppBar components.
+ */
+export default function ListLabels() {
+ const intl = useIntl();
+ const [selectedArtifactId, setSelectedArtifactId] = useState(null);
+ const [selectedLabelName, setSelectedLabelName] = useState(null);
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ const openDialog = (artifactId, LabelName) => {
+ setSelectedArtifactId(artifactId);
+ setSelectedLabelName(LabelName);
+ setDialogOpen(true);
+ };
+
+ const closeDialog = () => {
+ setSelectedArtifactId(null);
+ setDialogOpen(false);
+ };
+
+ const columProps = [
+ { name: 'id', options: { display: false } },
+ {
+ name: 'name',
+ label: intl.formatMessage({
+ id: 'AdminPages.Labels.table.header.label.name',
+ defaultMessage: 'Label Name',
+ }),
+ options: {
+ filter: true,
+ sort: true,
+ customBodyRender: (data) => {
+ return {data};
+ },
+ },
+ },
+ {
+ name: 'description',
+ label: intl.formatMessage({
+ id: 'AdminPages.Labels.table.header.label.description',
+ defaultMessage: 'Description',
+ }),
+ options: {
+ filter: true,
+ sort: false,
+ },
+ },
+ {
+ name: 'usage',
+ label: intl.formatMessage({
+ id: 'AdminPages.Labels.table.header.label.usage',
+ defaultMessage: 'Usage',
+ }),
+ options: {
+ customBodyRender: (value, tableMeta) => {
+ if (typeof tableMeta.rowData === 'object') {
+ const artifactId = tableMeta.rowData[0];
+ const LabelName = tableMeta.rowData[1];
+ return (
+ openDialog(artifactId, LabelName)}
+ >
+
+
+ );
+ } else {
+ return
;
+ }
+ },
+ },
+ },
+ ];
+ const addButtonProps = {
+ triggerButtonText: intl.formatMessage({
+ id: 'AdminPages.Labels.List.addButtonProps.triggerButtonText',
+ defaultMessage: 'Add Label',
+ }),
+ /* This title is what as the title of the popup dialog box */
+ title: intl.formatMessage({
+ id: 'AdminPages.Labels.List.addButtonProps.title',
+ defaultMessage: 'Add Label',
+ }),
+ };
+ const searchProps = {
+ searchPlaceholder: intl.formatMessage({
+ id: 'AdminPages.Labels.List.search.default',
+ defaultMessage: 'Search by Label name',
+ }),
+ active: true,
+ };
+ const pageProps = {
+ pageStyle: 'half',
+ title: intl.formatMessage({
+ id: 'AdminPages.Labels.List.title.labels',
+ defaultMessage: 'Labels',
+ }),
+ };
+
+ const emptyBoxProps = {
+ content: (
+
+
+
+ ),
+ title: (
+
+
+
+ ),
+ };
+
+ return (
+ <>
+
+ ,
+ title: 'Edit Label',
+ }}
+ DeleteComponent={Delete}
+ />
+ >
+ );
+}
diff --git a/portals/admin/src/main/webapp/source/src/app/data/api.js b/portals/admin/src/main/webapp/source/src/app/data/api.js
index e7c2d04c0bf..1c6cc221fd7 100644
--- a/portals/admin/src/main/webapp/source/src/app/data/api.js
+++ b/portals/admin/src/main/webapp/source/src/app/data/api.js
@@ -284,6 +284,82 @@ class API extends Resource {
});
}
+ /**
+ * Get list of labels
+ */
+ labelsListGet() {
+ return this.client.then((client) => {
+ return client.apis['Labels (Collection)'].getAllLabels(
+ this._requestMetaData(),
+ );
+ });
+ }
+
+ /**
+ * Update an Labels
+ */
+ updateLabel(id, name, description) {
+ return this.client.then((client) => {
+ const data = {
+ name: name,
+ description: description,
+ };
+ return client.apis[
+ 'Label (Individual)'
+ ].updateLabel(
+ { labelId: id },
+ { requestBody: data },
+ this._requestMetaData(),
+ );
+ });
+ }
+
+ /**
+ * Delete an Labels
+ */
+ deleteLabel(id) {
+ return this.client.then((client) => {
+ return client.apis[
+ 'Label (Individual)'
+ ].deleteLabel(
+ { labelId: id },
+ this._requestMetaData(),
+ );
+ });
+ }
+
+ /**
+ * Add an Labels
+ */
+ createLabel(name, description) {
+ return this.client.then((client) => {
+ const data = {
+ name: name,
+ description: description,
+ };
+ const payload = {
+ 'Content-Type': 'application/json',
+ };
+ return client.apis['Label (Individual)'].createLabel(
+ payload,
+ { requestBody: data },
+ this._requestMetaData(),
+ );
+ });
+ }
+
+ /**
+ * Get Label api usages
+ */
+ getLabelApiUsages(labelId) {
+ return this.client.then((client) => {
+ return client.apis['Label (Individual)'].getLabelUsage(
+ { labelId: labelId },
+ this._requestMetaData(),
+ );
+ });
+ }
+
/**
* Get Application Throttling Policies
*/
diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/DesignConfigurations.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/DesignConfigurations.jsx
index 7c33c21d1fe..e844024dcbf 100644
--- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/DesignConfigurations.jsx
+++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/DesignConfigurations.jsx
@@ -29,12 +29,26 @@ import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import { Link } from 'react-router-dom';
import Button from '@mui/material/Button';
-import Container from '@mui/material/Container';
import Box from '@mui/material/Box';
+import {
+ Checkbox,
+ Chip,
+ IconButton,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Stack,
+ TextField,
+ Tooltip
+} from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress';
import CONSTS from 'AppData/Constants';
import Alert from 'AppComponents/Shared/Alert';
+import AddIcon from '@mui/icons-material/Add';
import { APIContext } from 'AppComponents/Apis/Details/components/ApiContext';
import UpdateWithoutDetails from 'AppComponents/Apis/Details/Configuration/components/UpdateWithoutDetails';
@@ -307,6 +321,7 @@ function configReducer(state, configAction) {
* @returns
*/
export default function DesignConfigurations() {
+ const [anchorEl, setAnchorEl] = useState(null);
const { api, updateAPI } = useContext(APIContext);
const { data: settings } = usePublisherSettings();
const [isUpdating, setIsUpdating] = useState(false);
@@ -316,6 +331,12 @@ export default function DesignConfigurations() {
const [errorInExternalEndpoints, setErrorInExternalEndpoints] = useState(false);
const [apiConfig, configDispatcher] = useReducer(configReducer, copyAPIConfig(api));
+ const [loading, setLoading] = useState(true);
+ const [labels, setLabels] = useState({});
+ const [searchQuery, setSearchQuery] = useState('');
+ const [seaerchResult, setSeaerchResult] = useState({});
+ const [updatedLabels, setUpdatedLabels] = useState([]);
+ const [unselectedLabels, setUnselectedLabels] = useState([]);
const [descriptionType, setDescriptionType] = useState('');
const [overview, setOverview] = useState('');
const [overviewDocument, setOverviewDocument] = useState(null);
@@ -329,6 +350,14 @@ export default function DesignConfigurations() {
return (/([~!@#;%^&*+=|\\<>"'/,])/.test(tag)) || (tag.length > 30);
});
const intl = useIntl();
+
+ const handleOpenList = (event) => setAnchorEl(event.currentTarget);
+ const handleCloseList = () => {
+ setSearchQuery('');
+ setSeaerchResult({});
+ setAnchorEl(null);
+ }
+
const handleChange = (event) => {
const type = event.target.value;
if (type === CONSTS.DESCRIPTION_TYPES.DESCRIPTION) {
@@ -438,8 +467,38 @@ export default function DesignConfigurations() {
}));
}
});
+ // const apiClient = new API();
+ API.labels().then((response) => setLabels(response.body));
+ restApi.getAPILabels(api.id).then((response) => {
+ setUpdatedLabels(response.body.list.map((label) => label.name));
+ }).finally(() => {
+ setLoading(false);
+ });
}, []);
+ useEffect(() => {
+ setUnselectedLabels(labels?.list?.filter(label => !updatedLabels.includes(label.name))
+ .map((label) => label.name).sort());
+ }, [labels, updatedLabels]);
+
+ const attachLabel = (name) => {
+ const apiClient = new API();
+ apiClient.attachLabels(api.id,
+ labels.list?.filter(label => [name].includes(label.name)))
+ .then((response) => {
+ setUpdatedLabels(response.body.list.map((label) => label.name))
+ });
+ }
+
+ const detachLabel = (name) => {
+ const apiClient = new API();
+ apiClient.detachLabels(api.id,
+ labels.list?.filter(label => [name].includes(label.name)))
+ .then((response) => {
+ setUpdatedLabels(response.body.list.map((label) => label.name))
+ });
+ }
+
/**
*
* Handle the configuration view save button action
@@ -519,41 +578,176 @@ export default function DesignConfigurations() {
setIsOpen(false);
};
const restricted = isRestricted(['apim:api_publish', 'apim:api_create'], api
- || isUpdating || api.isRevision || invalidTagsExist
- || (apiConfig.visibility === 'RESTRICTED'
- && apiConfig.visibleRoles.length === 0));
+ || isUpdating || api.isRevision || invalidTagsExist
+ || (apiConfig.visibility === 'RESTRICTED'
+ && apiConfig.visibleRoles.length === 0));
+
+ const LabelMenu = () => {
+ if (seaerchResult && seaerchResult.list && searchQuery !== '') {
+ if (seaerchResult.list.length !== 0) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+
+ return (
+
+
+
+ {updatedLabels && updatedLabels.length !== 0 ? (
+ updatedLabels.map((label) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ {unselectedLabels && unselectedLabels.length !== 0 ? (
+ unselectedLabels.map((label) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+ );
+ };
return (
(
-
-
-
-
-
-
- {api.apiType === API.CONSTS.APIProduct
- ? (
-
-
-
- )
- : (
-
-
-
- )}
-
+
+
+
+
+
+
+
+
+
+ {api.apiType === API.CONSTS.APIProduct
+ ? (
+
+
+
+ )
+ : (
+
+
+
+ )}
+
+
+
+
@@ -618,16 +812,19 @@ export default function DesignConfigurations() {
/>
)}
- { settings && !settings.portalConfigurationOnlyModeEnabled && (
+ {settings && !settings.portalConfigurationOnlyModeEnabled && (
-
+
)}