From d407e2470144fee8a8153f99770eae55a8a29e4e Mon Sep 17 00:00:00 2001 From: dakshina Date: Tue, 21 Jan 2025 02:11:32 +0530 Subject: [PATCH] Implement Labels Feature UIs --- .../app/components/Base/RouteMenuMapping.jsx | 12 + .../app/components/Labels/AddEditLabel.jsx | 268 ++++++++++++++ .../src/app/components/Labels/DeleteLabel.jsx | 99 ++++++ .../app/components/Labels/ListLabelUsages.jsx | 319 +++++++++++++++++ .../src/app/components/Labels/ListLabels.jsx | 226 ++++++++++++ .../main/webapp/source/src/app/data/api.js | 76 ++++ .../Configuration/DesignConfigurations.jsx | 330 +++++++++++++++--- .../components/APICategories.jsx | 4 + .../Details/components/APIDetailsTopMenu.jsx | 9 +- .../main/webapp/source/src/app/data/api.js | 103 ++++++ 10 files changed, 1399 insertions(+), 47 deletions(-) create mode 100644 portals/admin/src/main/webapp/source/src/app/components/Labels/AddEditLabel.jsx create mode 100644 portals/admin/src/main/webapp/source/src/app/components/Labels/DeleteLabel.jsx create mode 100644 portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabelUsages.jsx create mode 100644 portals/admin/src/main/webapp/source/src/app/components/Labels/ListLabels.jsx 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 ( + <> + + + + {' '} + {selectedLabelName} + + + + + + + + + , + 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 ( + + {seaerchResult.list + .filter(label => updatedLabels.includes(label.name)) + .concat(seaerchResult.list.filter(label => !updatedLabels.includes(label.name))) + .map((label) => ( + updatedLabels.includes(label.name) + ? detachLabel(label.name) + : attachLabel(label.name)} + > + + + + + + ))} + + ); + } else { + return ( + + + + + ); + } + } + + return ( + + + + + + {updatedLabels && updatedLabels.length !== 0 ? ( + updatedLabels.map((label) => ( + detachLabel(label)}> + + + + + + )) + ) : ( + + + + + )} + + + + + + {unselectedLabels && unselectedLabels.length !== 0 ? ( + unselectedLabels.map((label) => ( + attachLabel(label)}> + + + + + + )) + ) : ( + + + + + )} + + + ); + }; return ( ( - - - - - e.stopPropagation()} + id='label-menu' + sx={{ + maxHeight: '450px', + width: '350px', + wordWrap: 'break-word' + }}> + + + + + + + { + setSearchQuery(e.target.value); + if (e.target.value === '') { + setSeaerchResult({}); + } else { + setSeaerchResult({ + list: labels.list.filter(label => label.name.includes(e.target.value)) + }); + } + }} + onKeyDown={(e) => e.stopPropagation()} + id='label-search-textfield' + placeholder='Search...' + size='small' /> - - - {api.apiType === API.CONSTS.APIProduct - ? ( - - - - ) - : ( - - - - )} - + + + + + + + + + + {api.apiType === API.CONSTS.APIProduct + ? ( + + + + ) + : ( + + + + )} + + + +
@@ -618,16 +812,19 @@ export default function DesignConfigurations() { /> )} - { settings && !settings.portalConfigurationOnlyModeEnabled && ( + {settings && !settings.portalConfigurationOnlyModeEnabled && ( - + )}