diff --git a/portals/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index c4f0357e87d..a6084ee0a40 100644 --- a/portals/admin/src/main/webapp/site/public/locales/en.json +++ b/portals/admin/src/main/webapp/site/public/locales/en.json @@ -348,9 +348,48 @@ "Application.Name": "Application Name", "Application.Owner": "Application Owner", "Application.organization": "Application Organization", - "Applications.Listing.ApplicationTableHead.actions": "Actions", - "Applications.Listing.ApplicationTableHead.name": "Name", - "Applications.Listing.ApplicationTableHead.owner": "Owner", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.more.than.label": "more than {to}", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.range.label": "{from}-{to} of {count}", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.show.label": "Show", + "ApplicationSettings.ChangeAppOwner.applications.list.title": "Change Application Owner", + "ApplicationSettings.ChangeAppOwner.applications.search": "Search", + "ApplicationSettings.ChangeAppOwner.applications.searching": "Searching", + "ApplicationSettings.ChangeAppOwner.clear.search": "Clear Search", + "ApplicationSettings.ChangeAppOwner.column.actions": "Actions", + "ApplicationSettings.ChangeAppOwner.column.name": "Name", + "ApplicationSettings.ChangeAppOwner.column.owner": "Owner", + "ApplicationSettings.ChangeAppOwner.empty.message": "No Data to Display", + "ApplicationSettings.ChangeAppOwner.search.placeholder": "Search Application by Name/Owner", + "ApplicationSettings.ListApplications.change.app.owner.tab.title": "Owner", + "ApplicationSettings.ListApplications.change.app.owner.title": "Change Application Owner", + "ApplicationSettings.ListApplications.change.app.settings.title": "Change Application Settings", + "ApplicationSettings.ListApplications.learn.more.link": "Learn More…", + "ApplicationSettings.ListApplications.opaque.token.warning": "You have one or more legacy applications that are using opaque access tokens. Support for opaque tokens has been deprecated. Please upgrade these applications to use JWT-based access tokens.", + "ApplicationSettings.ListApplications.upgrade.legacy.app.tab.title": "Legacy Applications", + "ApplicationSettings.UpgradeToJWTDialog.app.upgrade.error": "Error while upgrading application {appName} to JWT", + "ApplicationSettings.UpgradeToJWTDialog.app.upgrade.successful": "Application {appName} upgraded to JWT successfully", + "ApplicationSettings.UpgradeToJWTDialog.button.cancel": "Cancel", + "ApplicationSettings.UpgradeToJWTDialog.button.open": "Upgrade to JWT", + "ApplicationSettings.UpgradeToJWTDialog.button.upgrade": "Upgrade", + "ApplicationSettings.UpgradeToJWTDialog.dialog.body1": "You are about to upgrade the application {appName} to use JWT-based access tokens.", + "ApplicationSettings.UpgradeToJWTDialog.dialog.body2": "This change will permanently switch the format of the newly generated access tokens from opaque to JWT.", + "ApplicationSettings.UpgradeToJWTDialog.dialog.confirmation": "Do you want to proceed with the upgrade?", + "ApplicationSettings.UpgradeToJWTDialog.dialog.important": "Important", + "ApplicationSettings.UpgradeToJWTDialog.dialog.title": "Upgrade Application to JWT", + "ApplicationSettings.UpgradeToJWTDialog.dialog.warning1": "This action cannot be undone", + "ApplicationSettings.UpgradeToJWTDialog.dialog.warning2": "Existing opaque tokens will still be supported", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.more.than.label": "more than {to}", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.range.label": "{from}-{to} of {count}", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.show.label": "Show", + "ApplicationSettings.UpgradeTokenType.applications.search": "Search", + "ApplicationSettings.UpgradeTokenType.applications.searching": "Searching", + "ApplicationSettings.UpgradeTokenType.clear.search": "Clear Search", + "ApplicationSettings.UpgradeTokenType.column.actions": "Actions", + "ApplicationSettings.UpgradeTokenType.column.createdTime": "Created Date", + "ApplicationSettings.UpgradeTokenType.column.name": "Name", + "ApplicationSettings.UpgradeTokenType.column.owner": "Owner", + "ApplicationSettings.UpgradeTokenType.empty.message": "No Data to Display", + "ApplicationSettings.UpgradeTokenType.search.placeholder": "Search Application by Name/Owner", "Applications.Listing.Listing.applications.edit.error.already.exist": "{owner} already has an application with name: {name}", "Applications.Listing.Listing.applications.edit.error.default": "Something went wrong when validating user", "Applications.Listing.Listing.applications.edit.error.owner.invalid": "{owner} is not a valid Subscriber", @@ -358,16 +397,6 @@ "Applications.Listing.Listing.applications.edit.error.unknown": "Something went wrong when updating owner", "Applications.Listing.Listing.applications.edit.owner.label": "Owner", "Applications.Listing.Listing.applications.edit.save.btn": "Save", - "Applications.Listing.Listing.applications.list.rows.more.than.label": "more than {to}", - "Applications.Listing.Listing.applications.list.rows.range.label": "{from}-{to} of {count}", - "Applications.Listing.Listing.applications.list.rows.show.label": "Show", - "Applications.Listing.Listing.applications.list.title": "Change Application Owner", - "Applications.Listing.Listing.applications.search": "Search", - "Applications.Listing.Listing.applications.searching": "Searching", - "Applications.Listing.Listing.clear.search": "Clear Search", - "Applications.Listing.Listing.empty.message": "No Data to Display", - "Applications.Listing.Listing.search.placeholder": "Search Application by Name/Owner", - "Applications.Listing.Listing.title": "Change Application Owner", "Applications.Listing.apis.list.rows.more.than.label": "more than {to}", "Applications.Listing.apis.list.rows.range.label": "{from}-{to} of {count}", "Applications.Listing.apis.list.rows.show.label": "Show", @@ -396,7 +425,7 @@ "Base.RouteMenuMapping.application.reg": "Application Registration", "Base.RouteMenuMapping.application.throttling.policies": "Application Policies", "Base.RouteMenuMapping.application.update": "Application Update", - "Base.RouteMenuMapping.applications": "Change Application Owner", + "Base.RouteMenuMapping.applications": "Change Application Settings", "Base.RouteMenuMapping.blacklisted.items": "Deny Policies", "Base.RouteMenuMapping.compliance": "Compliance", "Base.RouteMenuMapping.custom.throttling.policies": "Custom Policies", diff --git a/portals/admin/src/main/webapp/site/public/locales/fr.json b/portals/admin/src/main/webapp/site/public/locales/fr.json index 429f64423fb..b84e35c4116 100644 --- a/portals/admin/src/main/webapp/site/public/locales/fr.json +++ b/portals/admin/src/main/webapp/site/public/locales/fr.json @@ -348,9 +348,48 @@ "Application.Name": "Application Name", "Application.Owner": "Application Owner", "Application.organization": "Application Organization", - "Applications.Listing.ApplicationTableHead.actions": "Actions", - "Applications.Listing.ApplicationTableHead.name": "Name", - "Applications.Listing.ApplicationTableHead.owner": "Owner", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.more.than.label": "more than {to}", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.range.label": "{from}-{to} of {count}", + "ApplicationSettings.ChangeAppOwner.applications.list.rows.show.label": "Show", + "ApplicationSettings.ChangeAppOwner.applications.list.title": "Change Application Owner", + "ApplicationSettings.ChangeAppOwner.applications.search": "Search", + "ApplicationSettings.ChangeAppOwner.applications.searching": "Searching", + "ApplicationSettings.ChangeAppOwner.clear.search": "Clear Search", + "ApplicationSettings.ChangeAppOwner.column.actions": "Actions", + "ApplicationSettings.ChangeAppOwner.column.name": "Name", + "ApplicationSettings.ChangeAppOwner.column.owner": "Owner", + "ApplicationSettings.ChangeAppOwner.empty.message": "No Data to Display", + "ApplicationSettings.ChangeAppOwner.search.placeholder": "Search Application by Name/Owner", + "ApplicationSettings.ListApplications.change.app.owner.tab.title": "Owner", + "ApplicationSettings.ListApplications.change.app.owner.title": "Change Application Owner", + "ApplicationSettings.ListApplications.change.app.settings.title": "Change Application Settings", + "ApplicationSettings.ListApplications.learn.more.link": "Learn More…", + "ApplicationSettings.ListApplications.opaque.token.warning": "You have one or more legacy applications that are using opaque access tokens. Support for opaque tokens has been deprecated. Please upgrade these applications to use JWT-based access tokens.", + "ApplicationSettings.ListApplications.upgrade.legacy.app.tab.title": "Legacy Applications", + "ApplicationSettings.UpgradeToJWTDialog.app.upgrade.error": "Error while upgrading application {appName} to JWT", + "ApplicationSettings.UpgradeToJWTDialog.app.upgrade.successful": "Application {appName} upgraded to JWT successfully", + "ApplicationSettings.UpgradeToJWTDialog.button.cancel": "Cancel", + "ApplicationSettings.UpgradeToJWTDialog.button.open": "Upgrade to JWT", + "ApplicationSettings.UpgradeToJWTDialog.button.upgrade": "Upgrade", + "ApplicationSettings.UpgradeToJWTDialog.dialog.body1": "You are about to upgrade the application {appName} to use JWT-based access tokens.", + "ApplicationSettings.UpgradeToJWTDialog.dialog.body2": "This change will permanently switch the format of the newly generated access tokens from opaque to JWT.", + "ApplicationSettings.UpgradeToJWTDialog.dialog.confirmation": "Do you want to proceed with the upgrade?", + "ApplicationSettings.UpgradeToJWTDialog.dialog.important": "Important", + "ApplicationSettings.UpgradeToJWTDialog.dialog.title": "Upgrade Application to JWT", + "ApplicationSettings.UpgradeToJWTDialog.dialog.warning1": "This action cannot be undone", + "ApplicationSettings.UpgradeToJWTDialog.dialog.warning2": "Existing opaque tokens will still be supported", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.more.than.label": "more than {to}", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.range.label": "{from}-{to} of {count}", + "ApplicationSettings.UpgradeTokenType.applications.list.rows.show.label": "Show", + "ApplicationSettings.UpgradeTokenType.applications.search": "Search", + "ApplicationSettings.UpgradeTokenType.applications.searching": "Searching", + "ApplicationSettings.UpgradeTokenType.clear.search": "Clear Search", + "ApplicationSettings.UpgradeTokenType.column.actions": "Actions", + "ApplicationSettings.UpgradeTokenType.column.createdTime": "Created Date", + "ApplicationSettings.UpgradeTokenType.column.name": "Name", + "ApplicationSettings.UpgradeTokenType.column.owner": "Owner", + "ApplicationSettings.UpgradeTokenType.empty.message": "No Data to Display", + "ApplicationSettings.UpgradeTokenType.search.placeholder": "Search Application by Name/Owner", "Applications.Listing.Listing.applications.edit.error.already.exist": "{owner} already has an application with name: {name}", "Applications.Listing.Listing.applications.edit.error.default": "Something went wrong when validating user", "Applications.Listing.Listing.applications.edit.error.owner.invalid": "{owner} is not a valid Subscriber", @@ -358,16 +397,6 @@ "Applications.Listing.Listing.applications.edit.error.unknown": "Something went wrong when updating owner", "Applications.Listing.Listing.applications.edit.owner.label": "Owner", "Applications.Listing.Listing.applications.edit.save.btn": "Save", - "Applications.Listing.Listing.applications.list.rows.more.than.label": "more than {to}", - "Applications.Listing.Listing.applications.list.rows.range.label": "{from}-{to} of {count}", - "Applications.Listing.Listing.applications.list.rows.show.label": "Show", - "Applications.Listing.Listing.applications.list.title": "Change Application Owner", - "Applications.Listing.Listing.applications.search": "Search", - "Applications.Listing.Listing.applications.searching": "Searching", - "Applications.Listing.Listing.clear.search": "Clear Search", - "Applications.Listing.Listing.empty.message": "No Data to Display", - "Applications.Listing.Listing.search.placeholder": "Search Application by Name/Owner", - "Applications.Listing.Listing.title": "Change Application Owner", "Applications.Listing.apis.list.rows.more.than.label": "more than {to}", "Applications.Listing.apis.list.rows.range.label": "{from}-{to} of {count}", "Applications.Listing.apis.list.rows.show.label": "Show", @@ -396,7 +425,7 @@ "Base.RouteMenuMapping.application.reg": "Application Registration", "Base.RouteMenuMapping.application.throttling.policies": "Application Policies", "Base.RouteMenuMapping.application.update": "Application Update", - "Base.RouteMenuMapping.applications": "Change Application Owner", + "Base.RouteMenuMapping.applications": "Change Application Settings", "Base.RouteMenuMapping.blacklisted.items": "Deny Policies", "Base.RouteMenuMapping.compliance": "Compliance", "Base.RouteMenuMapping.custom.throttling.policies": "Custom Policies", diff --git a/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ContentBase.jsx b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ContentBase.jsx index 0b11a8ca786..af88737607b 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ContentBase.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/ContentBase.jsx @@ -19,6 +19,7 @@ import React from 'react'; import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import Toolbar from '@mui/material/Toolbar'; +import Alert from '@mui/material/Alert'; import Typography from '@mui/material/Typography'; import Paper from '@mui/material/Paper'; import Box from '@mui/material/Box'; @@ -43,7 +44,7 @@ const Root = styled('div')({ */ function ContentBase(props) { const { - title, pageDescription, children, help, width, pageStyle, PaperProps, paperLess, + title, pageDescription, children, help, width, pageStyle, PaperProps, paperLess, warning, } = props; let size = 8;// default half/medium if ([width, pageStyle].includes('small')) { @@ -82,6 +83,17 @@ function ContentBase(props) { + {warning && ( + + + + + {warning} + + + + + )} {pageStyle === 'paperLess' || paperLess ? children : ( @@ -102,11 +114,13 @@ ContentBase.defaultProps = { pageStyle: 'half', paperLess: false, pageDescription: null, + warning: null, }; ContentBase.propTypes = { help: PropTypes.element.isRequired, title: PropTypes.string.isRequired, pageDescription: PropTypes.string, + warning: PropTypes.node, children: PropTypes.element.isRequired, width: PropTypes.oneOf(['medium', 'full', 'small']), pageStyle: PropTypes.oneOf(['half', 'full', 'small']), // @deprecated diff --git a/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/TabbedContentBase.jsx b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/TabbedContentBase.jsx new file mode 100644 index 00000000000..169b5b5dd3f --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/AdminPages/Addons/TabbedContentBase.jsx @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. 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 PropTypes from 'prop-types'; +import Grid from '@mui/material/Grid'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Box from '@mui/material/Box'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; + +/** + * This is a wrapper component around ContentBase used for displaying a tabbed structure. + * @param {object} props - The component props. + * @returns {JSX.Element} The rendered tabbed content component. + */ +function TabbedContentBase(props) { + const { + title, + help, + pageDescription, + tabs, + warning, + } = props; + + const [value, setValue] = useState(0); + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( + + + + + + {tabs.map((tab, index) => ( + + ))} + + + + + {tabs[value]?.content} + + + + + + ); +} + +TabbedContentBase.propTypes = { + title: PropTypes.string.isRequired, + help: PropTypes.element.isRequired, + pageDescription: PropTypes.string, + tabs: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.node.isRequired, + content: PropTypes.element.isRequired, + }), + ).isRequired, + warning: PropTypes.node, +}; + +TabbedContentBase.defaultProps = { + pageDescription: null, + warning: null, +}; + +export default TabbedContentBase; diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ApplicationTableHead.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ApplicationTableHead.jsx index 3e6336f5ef3..bd9a77a11fb 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ApplicationTableHead.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ApplicationTableHead.jsx @@ -22,7 +22,6 @@ import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import TableSortLabel from '@mui/material/TableSortLabel'; -import { FormattedMessage } from 'react-intl'; /** * @inheritdoc @@ -33,39 +32,8 @@ const applicationTableHead = (props) => { const createSortHandler = (property) => (event) => { props.onRequestSort(event, property); }; - const columnData = [ - { - id: 'name', - numeric: false, - disablePadding: true, - label: (), - sorting: true, - }, - { - id: 'owner', - numeric: false, - disablePadding: false, - label: (), - sorting: true, - }, - { - id: 'actions', - numeric: false, - disablePadding: false, - label: (), - sorting: false, - }, - ]; - const { order, orderBy } = props; + + const { order, orderBy, columnData } = props; return ( @@ -98,5 +66,14 @@ applicationTableHead.propTypes = { onRequestSort: PropTypes.func.isRequired, order: PropTypes.string.isRequired, orderBy: PropTypes.string.isRequired, + columnData: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + numeric: PropTypes.bool, + disablePadding: PropTypes.bool, + label: PropTypes.node.isRequired, + sorting: PropTypes.bool.isRequired, + }), + ).isRequired, }; export default applicationTableHead; diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/AppsTableContent.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/AppsTableContent.jsx index 67e27df5e67..7a6b9dad89c 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/AppsTableContent.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/AppsTableContent.jsx @@ -21,7 +21,6 @@ import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; import ResourceNotFound from 'AppComponents/Base/Errors/ResourceNotFound'; -import EditApplication from 'AppComponents/ApplicationSettings/EditApplication'; import PropTypes from 'prop-types'; /** @@ -63,13 +62,17 @@ class AppsTableContent extends Component { */ render() { const { - apps, editComponentProps, apiCall, + apps, columns, editComponentProps, apiCall, filterTokenTypes, } = this.props; const { notFound } = this.state; if (notFound) { return ; } + const filteredApps = filterTokenTypes + ? apps.filter((app) => filterTokenTypes.includes(app.tokenType)) + : apps; + const rowHeight = 48; return ( - {apps && apps.map((app) => { + {filteredApps && filteredApps.map((app) => { return ( - - {app.name} - - - {app.owner} - - - - + {columns.map((column) => ( + + + {column.render(app, { apiCall, editComponentProps })} + + + ))} ); })} @@ -111,8 +119,20 @@ class AppsTableContent extends Component { ); } } + +AppsTableContent.defaultProps = { + filterTokenTypes: null, +}; + AppsTableContent.propTypes = { toggleDeleteConfirmation: PropTypes.func.isRequired, apps: PropTypes.instanceOf(Map).isRequired, + columns: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + render: PropTypes.func.isRequired, + }), + ).isRequired, + filterTokenTypes: PropTypes.arrayOf(PropTypes.string), }; export default (AppsTableContent); diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ChangeAppOwner.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ChangeAppOwner.jsx new file mode 100644 index 00000000000..ac738db3981 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ChangeAppOwner.jsx @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. 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 PropTypes from 'prop-types'; +import { useIntl, FormattedMessage } from 'react-intl'; +import EditApplication from 'AppComponents/ApplicationSettings/EditApplication'; +import AppsTableContent from 'AppComponents/ApplicationSettings/AppsTableContent'; +import ApplicationTableHead from 'AppComponents/ApplicationSettings/ApplicationTableHead'; +import EditIcon from '@mui/icons-material/Edit'; +import Table from '@mui/material/Table'; +import TableFooter from '@mui/material/TableFooter'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import HighlightOffRoundedIcon from '@mui/icons-material/HighlightOffRounded'; +import Tooltip from '@mui/material/Tooltip'; +import TextField from '@mui/material/TextField'; +import SearchIcon from '@mui/icons-material/Search'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; + +/** + * Renders a searchable, paginated table of applications with the ability to change application owners. + * + * @param {object} props - Component properties including application data, loading state, search and + * pagination handlers. + * @returns {JSX.Element} The rendered application table with search, pagination, and edit functionality. + */ +export default function ChangeAppOwner(props) { + const intl = useIntl(); + const { + loading, + applicationList, + page, + rowsPerPage, + totalApps, + searchQuery, + handleChangePage, + handleChangeRowsPerPage, + setQuery, + clearSearch, + filterApps, + apiCall, + } = props; + + const columnData = [ + { + id: 'name', + numeric: false, + disablePadding: true, + label: (), + sorting: true, + }, + { + id: 'owner', + numeric: false, + disablePadding: false, + label: (), + sorting: true, + }, + { + id: 'actions', + numeric: false, + disablePadding: false, + label: (), + sorting: false, + }, + ]; + + const columns = [ + { + id: 'name', + render: (app) => app.name, + }, + { + id: 'owner', + render: (app) => app.owner, + }, + { + id: 'actions', + render: (app, columnProps) => ( + + ), + }, + ]; + + return ( + <> + + + + + + + + + ({ + '& .search-input': { + fontSize: theme.typography.fontSize, + }, + })} + InputProps={{ + disableUnderline: true, + className: 'search-input', + }} + value={searchQuery} + onChange={setQuery} + /> + { searchQuery.length > 0 + && ( + + + + + + )} + + + + {loading ? ( + + ) : ( + + )} + + + + + + + + {applicationList && applicationList.length > 0 + && ( + + + , + title: intl.formatMessage({ + id: 'ApplicationSettings.ChangeAppOwner.applications.list.title', + defaultMessage: 'Change Application Owner', + }), + applicationList, + }} + EditComponent={EditApplication} + apiCall={apiCall} + columns={columns} + /> + + + { + if (count !== -1) { + return intl.formatMessage({ + id: 'ApplicationSettings.ChangeAppOwner.applications.' + + 'list.rows.range.label', + defaultMessage: '{from}-{to} of {count}', + }, + { + from, to, count, + }); + } + return intl.formatMessage({ + id: 'ApplicationSettings.ChangeAppOwner.applications.' + + 'list.rows.more.than.label', + defaultMessage: 'more than {to}', + }, + { to }); + }} + labelRowsPerPage={intl.formatMessage({ + id: 'ApplicationSettings.ChangeAppOwner.applications.list.rows.show.label', + defaultMessage: 'Show', + })} + page={page} + backIconButtonProps={{ + 'aria-label': 'Previous Page', + }} + nextIconButtonProps={{ + 'aria-label': 'Next Page', + }} + onPageChange={handleChangePage} + onRowsPerPageChange={handleChangeRowsPerPage} + /> + + + + )} + {applicationList && applicationList.length === 0 && !loading && ( + + + + + + + + )} + > + ); +} + +ChangeAppOwner.propTypes = { + loading: PropTypes.bool.isRequired, + applicationList: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + owner: PropTypes.string.isRequired, + })), + page: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + totalApps: PropTypes.number.isRequired, + searchQuery: PropTypes.string.isRequired, + handleChangePage: PropTypes.func.isRequired, + handleChangeRowsPerPage: PropTypes.func.isRequired, + setQuery: PropTypes.func.isRequired, + clearSearch: PropTypes.func.isRequired, + filterApps: PropTypes.func.isRequired, + apiCall: PropTypes.func.isRequired, +}; + +ChangeAppOwner.defaultProps = { + applicationList: [], +}; diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/EditApplication.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/EditApplication.jsx index 297dab4b853..4c628eaf405 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/EditApplication.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/EditApplication.jsx @@ -117,8 +117,11 @@ function Edit(props) { }; const formSaveCallback = () => { + const body = { + owner, + }; return validateOwner().then(() => { - return restApi.updateApplicationOwner(dataRow.applicationId, owner) + return restApi.updateApplicationSettings(dataRow.applicationId, body) .then(() => { return ( intl.formatMessage({ diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ListApplications.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ListApplications.jsx index d7bd6c0fef1..3d53790a759 100644 --- a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ListApplications.jsx +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/ListApplications.jsx @@ -17,33 +17,28 @@ */ import React, { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import API from 'AppData/api'; -import { useIntl, FormattedMessage } from 'react-intl'; -import EditApplication from 'AppComponents/ApplicationSettings/EditApplication'; -import AppsTableContent from 'AppComponents/ApplicationSettings/AppsTableContent'; -import ApplicationTableHead from 'AppComponents/ApplicationSettings/ApplicationTableHead'; -import EditIcon from '@mui/icons-material/Edit'; -import Table from '@mui/material/Table'; -import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; -import TableFooter from '@mui/material/TableFooter'; -import TablePagination from '@mui/material/TablePagination'; -import TableRow from '@mui/material/TableRow'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; import Box from '@mui/material/Box'; -import Grid from '@mui/material/Grid'; -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import HighlightOffRoundedIcon from '@mui/icons-material/HighlightOffRounded'; -import Tooltip from '@mui/material/Tooltip'; -import TextField from '@mui/material/TextField'; -import SearchIcon from '@mui/icons-material/Search'; -import Alert from '@mui/material/Alert'; -import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import ContentBase from 'AppComponents/AdminPages/Addons/ContentBase'; +import TabbedContentBase from 'AppComponents/AdminPages/Addons/TabbedContentBase'; +import ChangeAppOwner from 'AppComponents/ApplicationSettings/ChangeAppOwner'; +import UpgradeTokenType from 'AppComponents/ApplicationSettings/UpgradeTokenType'; +import Configurations from 'Config'; +/** + * Renders the application management view with support for: + * - Displaying a paginated and searchable list of applications. + * - Filtering applications by name or owner. + * - Changing the application owner. + * - Upgrading legacy applications from opaque tokens to JWT-based tokens. + * + * @returns {JSX.Element} The rendered component with tabs for changing owner and upgrading tokens. + */ export default function ListApplications() { const intl = useIntl(); - const [loading, setLoading] = useState(false); const [applicationList, setApplicationList] = useState(null); const [totalApps, setTotalApps] = useState(0); @@ -52,9 +47,13 @@ export default function ListApplications() { const [searchQuery, setSearchQuery] = useState(''); /** - * API call to get application list - * @returns {Promise}. - */ + * Fetches a paginated list of applications from the API. + * + * @param {number} pageNo - The current page number (0-indexed). + * @param {string} [user=searchQuery] - Optional filter by application owner. + * @param {string} [name=searchQuery] - Optional filter by application name. + * @returns {Promise} A promise resolving to the list of applications. + */ function apiCall(pageNo, user = searchQuery, name = searchQuery) { setLoading(true); const restApi = new API(); @@ -88,6 +87,12 @@ export default function ListApplications() { }); }, [rowsPerPage]); + /** + * Handles page change in the paginated table. + * + * @param {React.MouseEvent} event - The page change event. + * @param {number} pageNo - The new page number. + */ function handleChangePage(event, pageNo) { setPage(pageNo); apiCall(pageNo).then((result) => { @@ -95,6 +100,11 @@ export default function ListApplications() { }); } + /** + * Handles change in rows per page for the paginated table. + * + * @param {React.ChangeEvent} event - The change event. + */ function handleChangeRowsPerPage(event) { const nextRowsPerPage = event.target.value; const rowsPerPageRatio = rowsPerPage / nextRowsPerPage; @@ -106,6 +116,9 @@ export default function ListApplications() { }); } + /** + * Clears the search input and reloads the full application list. + */ function clearSearch() { setPage(0); setSearchQuery(''); @@ -114,6 +127,12 @@ export default function ListApplications() { }); } + /** + * Handles search input change. + * Clears search if input is empty, otherwise updates search query state. + * + * @param {React.ChangeEvent} event - The change event from the search input. + */ function setQuery(event) { const newQuery = event.target.value; if (newQuery === '') { @@ -123,6 +142,11 @@ export default function ListApplications() { } } + /** + * Filters applications based on the current search query. + * + * @param {React.FormEvent} e - Form submission event. + */ function filterApps(e) { e.preventDefault(); setPage(0); @@ -130,165 +154,103 @@ export default function ListApplications() { setApplicationList(result); }); } + const childProps = { + loading, + applicationList, + totalApps, + page, + rowsPerPage, + searchQuery, + apiCall, + handleChangePage, + handleChangeRowsPerPage, + clearSearch, + setQuery, + filterApps, + }; - return ( - { + // Skip apps that already use JWT + if (app.tokenType === 'JWT') { + return false; + } + + const keyManagers = Array.isArray(app.keyManagers) ? app.keyManagers : []; + + // Case 1: Has key managers matching the specific name and allowed type + const hasAllowedKM = keyManagers.some((km) => km.name === 'Resident Key Manager' + && ALLOWED_KEY_MANAGERS.includes(km.type)); + + // Case 2: No key managers + const noKeyManagers = keyManagers.length === 0; + + return hasAllowedKM || noKeyManagers; + }); + const warning = ( + <> + {intl.formatMessage({ + defaultMessage: 'You have one or more legacy applications that are using opaque access tokens.' + + ' Support for opaque tokens has been deprecated. Please upgrade these ' + + ' applications to use JWT-based access tokens.', + id: 'ApplicationSettings.ListApplications.opaque.token.warning', })} - > - - - - - - - - - ({ - '& .search-input': { - fontSize: theme.typography.fontSize, - }, - })} - InputProps={{ - disableUnderline: true, - className: 'search-input', - }} - value={searchQuery} - onChange={setQuery} - /> - { searchQuery.length > 0 - && ( - - - - - - )} - - - - {loading ? ( - - ) : ( - - )} + {intl.formatMessage({ + defaultMessage: 'Learn More…', + id: 'ApplicationSettings.ListApplications.learn.more.link', + })} + + > + ); - - - - - - - {applicationList && applicationList.length > 0 - && ( - - - , - title: intl.formatMessage({ - id: 'Applications.Listing.Listing.applications.list.title', - defaultMessage: 'Change Application Owner', - }), - applicationList, - }} - EditComponent={EditApplication} - apiCall={apiCall} - /> - - - { - if (count !== -1) { - return intl.formatMessage({ - id: 'Applications.Listing.Listing.applications.list.rows.range.label', - defaultMessage: '{from}-{to} of {count}', - }, - { - from, to, count, - }); - } - return intl.formatMessage({ - id: 'Applications.Listing.Listing.applications.list.rows.more.than.label', - defaultMessage: 'more than {to}', - }, - { to }); - }} - labelRowsPerPage={intl.formatMessage({ - id: 'Applications.Listing.Listing.applications.list.rows.show.label', - defaultMessage: 'Show', - })} - page={page} - backIconButtonProps={{ - 'aria-label': 'Previous Page', - }} - nextIconButtonProps={{ - 'aria-label': 'Next Page', - }} - onPageChange={handleChangePage} - onRowsPerPageChange={handleChangeRowsPerPage} - /> - - - - )} - {applicationList && applicationList.length === 0 && !loading && ( - - - - - - + const tabs = [ + { + label: intl.formatMessage({ + defaultMessage: 'Owner', + id: 'ApplicationSettings.ListApplications.change.app.owner.tab.title', + }), + content: , + }, + { + label: ( + + + {intl.formatMessage({ + defaultMessage: 'Legacy Applications', + id: 'ApplicationSettings.ListApplications.upgrade.legacy.app.tab.title', + })} - )} - + ), + content: , + }, + ]; + + return ( + upgradableApps?.length ? ( + + ) : ( + + + + ) ); } diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeToJWTDialog.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeToJWTDialog.jsx new file mode 100644 index 00000000000..e6cbc1d5e73 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeToJWTDialog.jsx @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. 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 PropTypes from 'prop-types'; +import API from 'AppData/api'; +import Alert from 'AppComponents/Shared/Alert'; +import { Alert as MuiAlert } from '@mui/material'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Typography from '@mui/material/Typography'; +import { useIntl } from 'react-intl'; + +const UpgradeToJWTDialog = (props) => { + const intl = useIntl(); + const restApi = new API(); + const { + updateList, app, + } = props; + const [open, setOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleUpgrade = async () => { + setSubmitting(true); + const body = { + tokenType: 'JWT', + }; + try { + await restApi.updateApplicationSettings(app.applicationId, body); + Alert.success(intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.app.upgrade.successful', + defaultMessage: 'Application {appName} upgraded to JWT successfully', + }, { + appName: app.name, + })); + handleClose(); + updateList(); + } catch { + const upgradeError = intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.app.upgrade.error', + defaultMessage: 'Error while upgrading application {appName} to JWT', + }, { + appName: app.name, + }); + Alert.error(upgradeError); + } finally { + setSubmitting(false); + } + }; + + return ( + <> + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.button.open', + defaultMessage: 'Upgrade to JWT', + })} + + + { + if (submitting && (reason === 'backdropClick' || reason === 'escapeKeyDown')) { + return; + } + handleClose(); + }} + disableEscapeKeyDown={submitting} + > + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.title', + defaultMessage: 'Upgrade Application to JWT', + })} + + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.body1', + defaultMessage: 'You are about to upgrade the application {appName} ' + + 'to use JWT-based access tokens.', + }, { appName: {app.name} })} + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.body2', + defaultMessage: 'This change will permanently switch the format of the newly generated' + + ' access tokens from opaque to JWT.', + })} + + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.important', + defaultMessage: 'Important', + })} + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.warning1', + defaultMessage: 'This action cannot be undone', + })} + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.warning2', + defaultMessage: 'Existing opaque tokens will still be supported', + })} + + + + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.dialog.confirmation', + defaultMessage: 'Do you want to proceed with the upgrade?', + })} + + + + + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.button.cancel', + defaultMessage: 'Cancel', + })} + + + {intl.formatMessage({ + id: 'ApplicationSettings.UpgradeToJWTDialog.button.upgrade', + defaultMessage: 'Upgrade', + })} + + + + > + ); +}; + +UpgradeToJWTDialog.propTypes = { + updateList: PropTypes.func.isRequired, + app: PropTypes.shape({ + applicationId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, +}; + +export default UpgradeToJWTDialog; diff --git a/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeTokenType.jsx b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeTokenType.jsx new file mode 100644 index 00000000000..c799454f793 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/ApplicationSettings/UpgradeTokenType.jsx @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. 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 PropTypes from 'prop-types'; +import { useIntl, FormattedMessage } from 'react-intl'; +import AppsTableContent from 'AppComponents/ApplicationSettings/AppsTableContent'; +import ApplicationTableHead from 'AppComponents/ApplicationSettings/ApplicationTableHead'; +import Table from '@mui/material/Table'; +import TableFooter from '@mui/material/TableFooter'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import HighlightOffRoundedIcon from '@mui/icons-material/HighlightOffRounded'; +import Tooltip from '@mui/material/Tooltip'; +import TextField from '@mui/material/TextField'; +import SearchIcon from '@mui/icons-material/Search'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; +import UpgradeToJWTDialog from 'AppComponents/ApplicationSettings/UpgradeToJWTDialog'; + +/** + * Renders a searchable table of applications that can be upgraded to JWT tokens. + * Includes pagination, search, and action buttons for upgrading each application. + * + * @param {object} props - Component props including application data, pagination, search, and API handlers. + * @returns {JSX.Element} The table UI with upgrade functionality. + */ +export default function UpgradeTokenType(props) { + const intl = useIntl(); + const { + loading, + applicationList, + page, + rowsPerPage, + totalApps, + searchQuery, + handleChangePage, + handleChangeRowsPerPage, + setQuery, + clearSearch, + filterApps, + apiCall, + } = props; + + const columnData = [ + { + id: 'name', + numeric: false, + disablePadding: true, + label: (), + sorting: true, + }, + { + id: 'owner', + numeric: false, + disablePadding: false, + label: (), + sorting: true, + }, + { + id: 'createdTime', + numeric: false, + disablePadding: false, + label: (), + sorting: true, + }, + { + id: 'actions', + numeric: false, + disablePadding: false, + label: (), + sorting: false, + }, + ]; + + const columns = [ + { + id: 'name', + render: (app) => app.name, + }, + { + id: 'owner', + render: (app) => app.owner, + }, + { + id: 'createdTime', + render: (app) => new Date(app.createdTime).toLocaleDateString('en-CA'), + }, + { + id: 'actions', + render: (app, columnProps) => ( + + ), + }, + ]; + + return ( + <> + + + + + + + + + ({ + '& .search-input': { + fontSize: theme.typography.fontSize, + }, + })} + InputProps={{ + disableUnderline: true, + className: 'search-input', + }} + value={searchQuery} + onChange={setQuery} + /> + { searchQuery.length > 0 + && ( + + + + + + )} + + + + {loading ? ( + + ) : ( + + )} + + + + + + + + {applicationList && applicationList.length > 0 + && ( + + + + + + { + if (count !== -1) { + return intl.formatMessage({ + id: 'ApplicationSettings.UpgradeTokenType.applications.' + + 'list.rows.range.label', + defaultMessage: '{from}-{to} of {count}', + }, + { + from, to, count, + }); + } + return intl.formatMessage({ + id: 'ApplicationSettings.UpgradeTokenType.' + + 'applications.list.rows.more.than.label', + defaultMessage: 'more than {to}', + }, + { to }); + }} + labelRowsPerPage={intl.formatMessage({ + id: 'ApplicationSettings.UpgradeTokenType.applications.' + + 'list.rows.show.label', + defaultMessage: 'Show', + })} + page={page} + backIconButtonProps={{ + 'aria-label': 'Previous Page', + }} + nextIconButtonProps={{ + 'aria-label': 'Next Page', + }} + onPageChange={handleChangePage} + onRowsPerPageChange={handleChangeRowsPerPage} + /> + + + + )} + {applicationList && applicationList.length === 0 && !loading && ( + + + + + + + + )} + > + ); +} + +UpgradeTokenType.propTypes = { + loading: PropTypes.bool.isRequired, + applicationList: PropTypes.arrayOf(PropTypes.shape({ + applicationId: PropTypes.string, + name: PropTypes.string, + owner: PropTypes.string, + createdTime: PropTypes.string, + tokenType: PropTypes.string, + })).isRequired, + page: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + totalApps: PropTypes.number.isRequired, + searchQuery: PropTypes.string.isRequired, + handleChangePage: PropTypes.func.isRequired, + handleChangeRowsPerPage: PropTypes.func.isRequired, + setQuery: PropTypes.func.isRequired, + clearSearch: PropTypes.func.isRequired, + filterApps: PropTypes.func.isRequired, + apiCall: PropTypes.func.isRequired, +}; 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 4727cafe9b3..2a730fbd82f 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 @@ -488,10 +488,10 @@ const RouteMenuMapping = (intl) => [ }), children: [ { - id: 'Change Application Owner', + id: 'Change Application Settings', displayText: intl.formatMessage({ id: 'Base.RouteMenuMapping.applications', - defaultMessage: 'Change Application Owner', + defaultMessage: 'Change Application Settings', }), path: '/settings/applications', component: ListApplications, 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 cc42156ba9a..bf3266f31f0 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 @@ -518,6 +518,22 @@ class API extends Resource { }); } + /** + * Update application + */ + updateApplicationSettings(id, body) { + return this.client.then((client) => { + const payload = { + applicationId: id, + }; + return client.apis['Application'].updateApplicationSettings( + payload, + { requestBody: body }, + this._requestMetaData(), + ); + }); + } + /** * Update an application's owner */ diff --git a/tests/cypress/integration/admin/11-change-the-owner-of-application.spec.js b/tests/cypress/integration/admin/11-change-the-owner-of-application.spec.js index 0fa95965e36..9d8177f6822 100644 --- a/tests/cypress/integration/admin/11-change-the-owner-of-application.spec.js +++ b/tests/cypress/integration/admin/11-change-the-owner-of-application.spec.js @@ -36,10 +36,10 @@ describe("Change the owner of application", () => { //login to admin portal cy.loginToAdmin(carbonUsername, carbonPassword); - cy.get('[data-testid="Change Application Owner-child-link"]').click({ force: true }); + cy.get('[data-testid="Change Application Settings-child-link"]').click({ force: true }); cy.get("#itest-application-list-table").within(() => { cy.contains("tr", appName).within(() => { - cy.get("td > span").click({ force: true }); + cy.get("td > div > span").click({ force: true }); }); });