From 7e4dc4005c9e68492791b517241625392f36b013 Mon Sep 17 00:00:00 2001 From: Filip Hlavac <50696716+fhlavac@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:20:03 +0100 Subject: [PATCH] Improve routing in RBAC workspaces (#1735) * fix: fix translations * fix: not issue causing actions not being disabled * feat: improve users and user groups routing * chore: update users and user groups tests --- cypress/e2e/users-and-user-groups.cy.ts | 11 +- locales/translation-template.json | 16 ++- package-lock.json | 8 +- package.json | 2 +- src/Messages.js | 9 +- src/Routing.tsx | 26 ++++- src/locales/data.json | 5 +- src/locales/translations.json | 5 +- .../users-and-user-groups.tsx | 106 ------------------ .../user-groups}/UserGroupsTable.tsx | 53 ++++++--- .../user-groups/UserGroupsView.tsx | 26 +++++ .../user-group-detail}/GroupDetailsDrawer.tsx | 4 +- .../GroupDetailsRolesView.tsx | 6 +- .../GroupDetailsServiceAccountsView.tsx | 8 +- .../GroupDetailsUsersView.tsx | 6 +- .../users-and-user-groups.tsx | 70 ++++++++++++ .../users}/UsersTable.tsx | 28 ++--- .../users-and-user-groups/users/UsersView.tsx | 39 +++++++ .../AddUserToGroupModal.tsx} | 19 ++-- .../users/user-detail}/UserDetailsDrawer.tsx | 10 +- .../user-detail}/UserDetailsGroupsView.tsx | 10 +- .../user-detail}/UserDetailsRolesView.tsx | 10 +- .../group/add-group/add-group-wizard.js | 49 ++++---- src/utilities/pathnames.js | 21 +++- 24 files changed, 323 insertions(+), 224 deletions(-) delete mode 100644 src/smart-components/access-management/users-and-user-groups.tsx rename src/smart-components/access-management/{ => users-and-user-groups/user-groups}/UserGroupsTable.tsx (89%) create mode 100644 src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsView.tsx rename src/smart-components/access-management/{ => users-and-user-groups/user-groups/user-group-detail}/GroupDetailsDrawer.tsx (97%) rename src/smart-components/access-management/{ => users-and-user-groups/user-groups/user-group-detail}/GroupDetailsRolesView.tsx (87%) rename src/smart-components/access-management/{ => users-and-user-groups/user-groups/user-group-detail}/GroupDetailsServiceAccountsView.tsx (88%) rename src/smart-components/access-management/{ => users-and-user-groups/user-groups/user-group-detail}/GroupDetailsUsersView.tsx (88%) create mode 100644 src/smart-components/access-management/users-and-user-groups/users-and-user-groups.tsx rename src/smart-components/access-management/{ => users-and-user-groups/users}/UsersTable.tsx (94%) create mode 100644 src/smart-components/access-management/users-and-user-groups/users/UsersView.tsx rename src/smart-components/access-management/{AddUserGroupModal.tsx => users-and-user-groups/users/add-user-to-group/AddUserToGroupModal.tsx} (75%) rename src/smart-components/access-management/{ => users-and-user-groups/users/user-detail}/UserDetailsDrawer.tsx (96%) rename src/smart-components/access-management/{ => users-and-user-groups/users/user-detail}/UserDetailsGroupsView.tsx (84%) rename src/smart-components/access-management/{ => users-and-user-groups/users/user-detail}/UserDetailsRolesView.tsx (84%) diff --git a/cypress/e2e/users-and-user-groups.cy.ts b/cypress/e2e/users-and-user-groups.cy.ts index 2b78c5e7c..62be4e6d0 100644 --- a/cypress/e2e/users-and-user-groups.cy.ts +++ b/cypress/e2e/users-and-user-groups.cy.ts @@ -63,12 +63,11 @@ describe('Users and User Groups page', () => { statusCode: 200, body: mockUserGroups, }).as('getUserGroups'); - - cy.visit('/iam/access-management/users-and-user-groups'); - cy.wait('@getUsers', { timeout: 30000 }); }); it('should display the Users table and correct data', () => { + cy.visit('/iam/access-management/users-and-user-groups'); + cy.wait('@getUsers', { timeout: 30000 }); // Check if the table exists cy.get('[data-ouia-component-id^="iam-users-table"]').should('exist'); @@ -90,12 +89,18 @@ describe('Users and User Groups page', () => { }); it('should display warning modal when removing user', () => { + cy.visit('/iam/access-management/users-and-user-groups'); + cy.wait('@getUsers', { timeout: 30000 }); + cy.get('[data-ouia-component-id^="iam-users-table-table-td-0-6"]').click(); cy.get('[data-ouia-component-id^="OUIA-Generated-DropdownItem-2"]').click(); cy.get('[data-ouia-component-id^="iam-users-table-remove-user-modal"]').should('be.visible'); }); it('should display the User groups table and correct data', () => { + cy.visit('/iam/access-management/users-and-user-groups/user-groups'); + cy.wait('@getUserGroups', { timeout: 30000 }); + // Check if the table exists cy.get('[data-ouia-component-id^="iam-users-table"]').should('exist'); diff --git a/locales/translation-template.json b/locales/translation-template.json index 1cdddab12..9e24949ec 100644 --- a/locales/translation-template.json +++ b/locales/translation-template.json @@ -1738,10 +1738,6 @@ "defaultMessage": "Add", "description": "Add label" }, - "usersAndUserGroupsAddToGroup": { - "defaultMessage": "Remove from user group", - "description": "Remove from user group label" - }, "usersAndUserGroupsAddUserDescription": { "defaultMessage": "Select a user group to add {numUsers} {plural} to. These are all the user groups in your account. To manage user groups, go to user groups.", "description": "Description within add user to user group modal" @@ -1774,6 +1770,10 @@ "defaultMessage": "No description", "description": "No description label" }, + "usersAndUserGroupsRemoveFromGroup": { + "defaultMessage": "Remove from user group", + "description": "Remove from user group label" + }, "usersAndUserGroupsYes": { "defaultMessage": "Yes", "description": "Yes is Org Admin label" @@ -1949,13 +1949,17 @@ "defaultMessage": "Get started with workspaces", "description": "workspaces page section title" }, + "workspacesOverviewSubtitle": { + "defaultMessage": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", + "description": "Securely manage user access and organize assets within your organization using workspaces." + }, "workspacesServiceCardDescription": { "defaultMessage": "Configure workspaces to fit your organizational structure. They can be structured in a heirarchy (parent-child relationships). Permissions assigned to a parent workspace are automatically inherited by its child workspaces, saving you congfiguration time.", "description": "workspaces service card description" }, "workspacesSubtitle": { - "defaultMessage": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", - "description": "Securely manage user access and organize assets within your organization using workspaces." + "defaultMessage": "Workspaces provide a flexible, hierarchical, approach to organizing your assets and streamlining access management. Configure workspaces to fit your organizational structure.", + "description": "Workspaces subtitle" }, "workspacesSuccessAlertTitle": { "defaultMessage": "Your workspace migration is complete and ready to manage!", diff --git a/package-lock.json b/package-lock.json index 9414c46a8..035387ac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@data-driven-forms/react-form-renderer": "^3.23.5", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.5.5", + "@patternfly/react-component-groups": "^5.5.6", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.7.1", "@patternfly/react-icons": "^5.1.1", @@ -4331,9 +4331,9 @@ } }, "node_modules/@patternfly/react-component-groups": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.5.5.tgz", - "integrity": "sha512-Cgp1XxyBWnEDKAQsP+B7A4wlz6Bcp0bjwSMamdOiCR4GALtpBXXGrv6daAomoVCkL9l3zibcAfm/o9d9XBE9Ag==", + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.5.6.tgz", + "integrity": "sha512-UIZSFIEeT6rLeiDOdOS8BcwGngarnw1w3w2A+aEFpdkVFfrUcA45ipShj79kK+lR4LZ3JK0H5nD4ff3MN8QcLA==", "license": "MIT", "dependencies": { "@patternfly/react-core": "^5.4.1", diff --git a/package.json b/package.json index 8f2a776e4..3dd5fc754 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@data-driven-forms/react-form-renderer": "^3.23.5", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.5.5", + "@patternfly/react-component-groups": "^5.5.6", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.7.1", "@patternfly/react-icons": "^5.1.1", diff --git a/src/Messages.js b/src/Messages.js index 9150d8a69..816214748 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -2318,7 +2318,7 @@ export default defineMessages({ defaultMessage: 'Access Management', }, workspacesOverviewSubtitle: { - id: 'workspacesSubtitle', + id: 'workspacesOverviewSubtitle', description: 'Securely manage user access and organize assets within your organization using workspaces.', defaultMessage: 'Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.', @@ -2370,11 +2370,6 @@ export default defineMessages({ description: 'Add label', defaultMessage: 'Add', }, - usersAndUserGroupsAddToGroup: { - id: 'usersAndUserGroupsAddToGroup', - description: 'Add to user group label', - defaultMessage: 'Add to user group', - }, usersAndUserGroupsEditUserGroup: { id: 'usersAndUserGroupsEditUserGroup', description: 'Edit user group label', @@ -2386,7 +2381,7 @@ export default defineMessages({ defaultMessage: 'Delete user group', }, usersAndUserGroupsRemoveFromGroup: { - id: 'usersAndUserGroupsAddToGroup', + id: 'usersAndUserGroupsRemoveFromGroup', description: 'Remove from user group label', defaultMessage: 'Remove from user group', }, diff --git a/src/Routing.tsx b/src/Routing.tsx index 6fa19413f..b036c6907 100644 --- a/src/Routing.tsx +++ b/src/Routing.tsx @@ -44,16 +44,34 @@ const AddGroupServiceAccounts = lazy(() => import('./smart-components/group/serv const RemoveServiceAccountFromGroup = lazy(() => import('./smart-components/group/service-account/remove-group-service-accounts')); const QuickstartsTest = lazy(() => import('./smart-components/quickstarts/quickstarts-test')); -const UsersAndUserGroups = lazy(() => import('./smart-components/access-management/users-and-user-groups')); +const UsersAndUserGroups = lazy(() => import('./smart-components/access-management/users-and-user-groups/users-and-user-groups')); +const UsersView = lazy(() => import('./smart-components/access-management/users-and-user-groups/users/UsersView')); +const UserGroupsView = lazy(() => import('./smart-components/access-management/users-and-user-groups/user-groups/UserGroupsView')); const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommonAuthModel }: Record) => [ { path: pathnames['users-and-user-groups'].path, element: UsersAndUserGroups, childRoutes: [ - isCommonAuthModel && { - path: pathnames['invite-group-users'].path, - element: InviteUsersModalCommonAuth, + { + path: pathnames['users-new'].path, + element: UsersView, + childRoutes: [ + isCommonAuthModel && { + path: pathnames['invite-group-users'].path, + element: InviteUsersModalCommonAuth, + }, + ], + }, + { + path: pathnames['user-groups'].path, + element: UserGroupsView, + childRoutes: [ + { + path: pathnames['create-user-group'].path, + element: AddGroupWizard, + }, + ], }, ], }, diff --git a/src/locales/data.json b/src/locales/data.json index 10404963d..35f2f0fa3 100644 --- a/src/locales/data.json +++ b/src/locales/data.json @@ -435,7 +435,6 @@ "usersAndUserGroups": "Users and User Groups", "usersAndUserGroupsActive": "Active", "usersAndUserGroupsAdd": "Add", - "usersAndUserGroupsAddToGroup": "Remove from user group", "usersAndUserGroupsAddUserDescription": "Select a user group to add {numUsers} {plural} to. These are all the user groups in your account. To manage user groups, go to user groups.", "usersAndUserGroupsCancel": "Cancel", "usersAndUserGroupsDeleteUserGroup": "Delete user group", @@ -444,6 +443,7 @@ "usersAndUserGroupsInactive": "Inactive", "usersAndUserGroupsNo": "No", "usersAndUserGroupsNoDescription": "No description", + "usersAndUserGroupsRemoveFromGroup": "Remove from user group", "usersAndUserGroupsYes": "Yes", "usersDescription": "These are all of the users in your Red Hat organization.", "usersEmptyStateSubtitle": "This filter criteria matches no users.{br}Try changing your filter input.", @@ -488,8 +488,9 @@ "workspacesLearnMore": "Learn more about workspaces", "workspacesOverviewPageSubtitle": "Workspaces let's you group related assets together (such as RHEL hosts). This simplifies management and user access control.", "workspacesOverviewPageTitle": "Get started with workspaces", + "workspacesOverviewSubtitle": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", "workspacesServiceCardDescription": "Configure workspaces to fit your organizational structure. They can be structured in a heirarchy (parent-child relationships). Permissions assigned to a parent workspace are automatically inherited by its child workspaces, saving you congfiguration time.", - "workspacesSubtitle": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", + "workspacesSubtitle": "Workspaces provide a flexible, hierarchical, approach to organizing your assets and streamlining access management. Configure workspaces to fit your organizational structure.", "workspacesSuccessAlertTitle": "Your workspace migration is complete and ready to manage!", "workspacesTitle": "Access Management", "workspacesTooltip": "Add permission to these workspaces.", diff --git a/src/locales/translations.json b/src/locales/translations.json index b2be203fd..34e24eec5 100644 --- a/src/locales/translations.json +++ b/src/locales/translations.json @@ -434,7 +434,6 @@ "usersAndUserGroups": "Users and User Groups", "usersAndUserGroupsActive": "Active", "usersAndUserGroupsAdd": "Add", - "usersAndUserGroupsAddToGroup": "Remove from user group", "usersAndUserGroupsAddUserDescription": "Select a user group to add {numUsers} {plural} to. These are all the user groups in your account. To manage user groups, go to user groups.", "usersAndUserGroupsCancel": "Cancel", "usersAndUserGroupsDeleteUserGroup": "Delete user group", @@ -443,6 +442,7 @@ "usersAndUserGroupsInactive": "Inactive", "usersAndUserGroupsNo": "No", "usersAndUserGroupsNoDescription": "No description", + "usersAndUserGroupsRemoveFromGroup": "Remove from user group", "usersAndUserGroupsYes": "Yes", "usersDescription": "These are all of the users in your Red Hat organization.", "usersEmptyStateSubtitle": "This filter criteria matches no users.{br}Try changing your filter input.", @@ -487,8 +487,9 @@ "workspacesLearnMore": "Learn more about workspaces", "workspacesOverviewPageSubtitle": "Workspaces let's you group related assets together (such as RHEL hosts). This simplifies management and user access control.", "workspacesOverviewPageTitle": "Get started with workspaces", + "workspacesOverviewSubtitle": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", "workspacesServiceCardDescription": "Configure workspaces to fit your organizational structure. They can be structured in a heirarchy (parent-child relationships). Permissions assigned to a parent workspace are automatically inherited by its child workspaces, saving you congfiguration time.", - "workspacesSubtitle": "Securely manage user access and organize assets within your organization using workspaces. Implement granular access controls to streamline permission management and ensure efficient, secure access to resources. View assets and roles organization diagram.", + "workspacesSubtitle": "Workspaces provide a flexible, hierarchical, approach to organizing your assets and streamlining access management. Configure workspaces to fit your organizational structure.", "workspacesSuccessAlertTitle": "Your workspace migration is complete and ready to manage!", "workspacesTitle": "Access Management", "workspacesTooltip": "Add permission to these workspaces.", diff --git a/src/smart-components/access-management/users-and-user-groups.tsx b/src/smart-components/access-management/users-and-user-groups.tsx deleted file mode 100644 index 08ff15542..000000000 --- a/src/smart-components/access-management/users-and-user-groups.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useEffect } from 'react'; -import { useIntl } from 'react-intl'; -import { PageSection, PageSectionVariants, Tab, TabContent, Tabs } from '@patternfly/react-core'; -import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; -import Messages from '../../Messages'; -import UsersTable from './UsersTable'; -import UserGroupsTable from './UserGroupsTable'; -import { useLocation, useNavigate } from 'react-router-dom'; -import AddUserGroupModal from './AddUserGroupModal'; -import { User } from '../../redux/reducers/user-reducer'; -import { Group } from '../../redux/reducers/group-reducer'; -import GroupDetailsDrawer from './GroupDetailsDrawer'; -import { DataViewEventsProvider } from '@patternfly/react-data-view'; -import UserDetailsDrawer from './UserDetailsDrawer'; - -const TAB_NAMES = ['users', 'user-groups']; - -const UsersAndUserGroups: React.FunctionComponent = () => { - const intl = useIntl(); - const [activeTabKey, setActiveTabKey] = React.useState(0); - const [isAddUserGroupModalOpen, setIsAddUserGroupModalOpen] = React.useState(false); - const [selectedUsers, setSelectedUsers] = React.useState([]); - const [focusedUser, setFocusedUser] = React.useState(undefined); - const [focusedGroup, setFocusedGroup] = React.useState(undefined); - const usersRef = React.createRef(); - const groupsRef = React.createRef(); - - const navigate = useNavigate(); - const location = useLocation(); - - const updateURL = (tabKey: string) => { - const params = new URLSearchParams(location.search); - params.set('activeTab', tabKey.toString()); - navigate({ search: params.toString() }); - }; - - const handleTabSelect = (_: React.MouseEvent, key: string | number) => { - const activeTab = Number(key); - setActiveTabKey(activeTab); - updateURL(TAB_NAMES[activeTab]); - }; - - const handleOpenAddUserModal = (selected: User[]) => { - if (selected.length > 0) { - setSelectedUsers(selected); - setIsAddUserGroupModalOpen(true); - } - }; - - useEffect(() => { - const params = new URLSearchParams(location.search); - const tabKey = params.get('activeTab'); - tabKey && setActiveTabKey(Number(TAB_NAMES.findIndex((val) => val === tabKey))); - }, [location.search]); - - return ( - - - - - - - - - - - {activeTabKey === 0 && ( - - - - - - - - )} - {activeTabKey === 1 && ( - - - - - - - - )} - - - ); -}; - -export default UsersAndUserGroups; diff --git a/src/smart-components/access-management/UserGroupsTable.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsTable.tsx similarity index 89% rename from src/smart-components/access-management/UserGroupsTable.tsx rename to src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsTable.tsx index ec383ade1..d0f348d5f 100644 --- a/src/smart-components/access-management/UserGroupsTable.tsx +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsTable.tsx @@ -1,24 +1,25 @@ -import React, { useEffect, useCallback, useMemo, useState, Fragment } from 'react'; +import React, { useEffect, useCallback, useMemo, useState, Fragment, Suspense } from 'react'; +import { formatDistanceToNow } from 'date-fns'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useSelector, useDispatch } from 'react-redux'; +import { Outlet, useSearchParams } from 'react-router-dom'; import { useDataViewSelection, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect'; import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView'; import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar'; import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; import { ButtonVariant, EmptyState, EmptyStateBody, EmptyStateHeader, EmptyStateIcon, Pagination, Tooltip } from '@patternfly/react-core'; -import { ActionsColumn } from '@patternfly/react-table'; -import { mappedProps } from '../../helpers/shared/helpers'; -import { RBACStore } from '../../redux/store'; -import { useSearchParams } from 'react-router-dom'; -import { fetchGroups, removeGroups } from '../../redux/actions/group-actions'; -import { formatDistanceToNow } from 'date-fns'; -import { FormattedMessage, useIntl } from 'react-intl'; -import messages from '../../Messages'; -import { Group } from '../../redux/reducers/group-reducer'; import { DataViewTrObject, DataViewState, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view'; import { SearchIcon } from '@patternfly/react-icons'; +import { ActionsColumn } from '@patternfly/react-table'; import { ResponsiveAction, ResponsiveActions, SkeletonTableBody, SkeletonTableHead, WarningModal } from '@patternfly/react-component-groups'; -import AddGroupWizard from '../group/add-group/add-group-wizard'; +import { mappedProps } from '../../../../helpers/shared/helpers'; +import { RBACStore } from '../../../../redux/store'; +import { fetchGroups, removeGroups } from '../../../../redux/actions/group-actions'; +import { Group } from '../../../../redux/reducers/group-reducer'; +import useAppNavigate from '../../../../hooks/useAppNavigate'; +import pathnames from '../../../../utilities/pathnames'; +import messages from '../../../../Messages'; const COLUMNS: string[] = ['User group name', 'Description', 'Users', 'Service accounts', 'Roles', 'Workspaces', 'Last modified']; @@ -67,11 +68,12 @@ const UserGroupsTable: React.FunctionComponent = ({ focusedGroup, }) => { const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); - const [isAddGroupWizardOpen, setIsAddGroupWizardOpen] = React.useState(false); const [currentGroups, setCurrentGroups] = React.useState([]); const dispatch = useDispatch(); const [activeState, setActiveState] = useState(DataViewState.loading); const intl = useIntl(); + const navigate = useAppNavigate(); + const search = useSearchParams(); const { trigger } = useDataViewEventsContext(); const handleDeleteModalToggle = (groups: Group[]) => { @@ -228,11 +230,6 @@ const UserGroupsTable: React.FunctionComponent = ({ return ( - {isAddGroupWizardOpen && ( -
- -
- )} {isDeleteModalOpen && ( = ({ } actions={ - setIsAddGroupWizardOpen(true)}> + navigate(pathnames['create-user-group'].link)}> {intl.formatMessage(messages.createUserGroup)} = ({ /> + + { + navigate({ pathname: pathnames['user-groups'].link }); + }, + onCancel: () => + navigate({ + pathname: pathnames['user-groups'].link, + search: search.toString(), + }), + enableRoles: false, + pagination: { limit: perPage }, + filters: {}, + postMethod: fetchData, + }, + }} + /> +
); }; diff --git a/src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsView.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsView.tsx new file mode 100644 index 000000000..82661543f --- /dev/null +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/UserGroupsView.tsx @@ -0,0 +1,26 @@ +import { DataViewEventsProvider } from '@patternfly/react-data-view'; +import React from 'react'; +import GroupDetailsDrawer from './user-group-detail/GroupDetailsDrawer'; +import { TabContent } from '@patternfly/react-core'; +import UserGroupsTable from './UserGroupsTable'; +import { Group } from '../../../../redux/reducers/group-reducer'; + +interface UserGroupsViewProps { + groupsRef?: React.Ref; +} + +const UserGroupsView: React.FunctionComponent = ({ groupsRef }) => { + const [focusedGroup, setFocusedGroup] = React.useState(undefined); + + return ( + + + + + + + + ); +}; + +export default UserGroupsView; diff --git a/src/smart-components/access-management/GroupDetailsDrawer.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsDrawer.tsx similarity index 97% rename from src/smart-components/access-management/GroupDetailsDrawer.tsx rename to src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsDrawer.tsx index 2cda0b7ee..83addd70b 100644 --- a/src/smart-components/access-management/GroupDetailsDrawer.tsx +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsDrawer.tsx @@ -16,8 +16,8 @@ import { import React, { useEffect } from 'react'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { useIntl } from 'react-intl'; -import messages from '../../Messages'; -import { Group } from '../../redux/reducers/group-reducer'; +import messages from '../../../../../Messages'; +import { Group } from '../../../../../redux/reducers/group-reducer'; import GroupDetailsRolesView from './GroupDetailsRolesView'; import GroupDetailsServiceAccountsView from './GroupDetailsServiceAccountsView'; import GroupDetailsUsersView from './GroupDetailsUsersView'; diff --git a/src/smart-components/access-management/GroupDetailsRolesView.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsRolesView.tsx similarity index 87% rename from src/smart-components/access-management/GroupDetailsRolesView.tsx rename to src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsRolesView.tsx index 634dfb0f0..a3b772f25 100644 --- a/src/smart-components/access-management/GroupDetailsRolesView.tsx +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsRolesView.tsx @@ -1,10 +1,10 @@ import { DataView, DataViewTable } from '@patternfly/react-data-view'; import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { RBACStore } from '../../redux/store'; -import messages from '../../Messages'; +import { RBACStore } from '../../../../../redux/store'; +import messages from '../../../../../Messages'; import { useIntl } from 'react-intl'; -import { fetchRolesForGroup } from '../../redux/actions/group-actions'; +import { fetchRolesForGroup } from '../../../../../redux/actions/group-actions'; interface GroupRolesViewProps { groupId: string; diff --git a/src/smart-components/access-management/GroupDetailsServiceAccountsView.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsServiceAccountsView.tsx similarity index 88% rename from src/smart-components/access-management/GroupDetailsServiceAccountsView.tsx rename to src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsServiceAccountsView.tsx index 0321fc113..674e283bc 100644 --- a/src/smart-components/access-management/GroupDetailsServiceAccountsView.tsx +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsServiceAccountsView.tsx @@ -1,10 +1,10 @@ import { DataView, DataViewTable } from '@patternfly/react-data-view'; +import { useIntl } from 'react-intl'; import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { RBACStore } from '../../redux/store'; -import messages from '../../Messages'; -import { useIntl } from 'react-intl'; -import { fetchServiceAccountsForGroup } from '../../redux/actions/group-actions'; +import { RBACStore } from '../../../../../redux/store'; +import messages from '../../../../../Messages'; +import { fetchServiceAccountsForGroup } from '../../../../../redux/actions/group-actions'; interface GroupDetailsServiceAccountsViewProps { groupId: string; diff --git a/src/smart-components/access-management/GroupDetailsUsersView.tsx b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsUsersView.tsx similarity index 88% rename from src/smart-components/access-management/GroupDetailsUsersView.tsx rename to src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsUsersView.tsx index c11da0fdc..f2074c910 100644 --- a/src/smart-components/access-management/GroupDetailsUsersView.tsx +++ b/src/smart-components/access-management/users-and-user-groups/user-groups/user-group-detail/GroupDetailsUsersView.tsx @@ -1,10 +1,10 @@ import { DataView, DataViewTable } from '@patternfly/react-data-view'; import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { RBACStore } from '../../redux/store'; -import messages from '../../Messages'; +import { RBACStore } from '../../../../../redux/store'; +import messages from '../../../../../Messages'; import { useIntl } from 'react-intl'; -import { fetchMembersForGroup } from '../../redux/actions/group-actions'; +import { fetchMembersForGroup } from '../../../../../redux/actions/group-actions'; interface GroupDetailsUsersViewProps { groupId: string; diff --git a/src/smart-components/access-management/users-and-user-groups/users-and-user-groups.tsx b/src/smart-components/access-management/users-and-user-groups/users-and-user-groups.tsx new file mode 100644 index 000000000..3fbe115a0 --- /dev/null +++ b/src/smart-components/access-management/users-and-user-groups/users-and-user-groups.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { Outlet, useLocation } from 'react-router-dom'; +import { PageSection, PageSectionVariants, Tab, Tabs } from '@patternfly/react-core'; +import ContentHeader from '@patternfly/react-component-groups/dist/dynamic/ContentHeader'; +import useAppNavigate from '../../../hooks/useAppNavigate'; +import pathnames from '../../../utilities/pathnames'; +import Messages from '../../../Messages'; + +const UsersAndUserGroups: React.FunctionComponent = () => { + const intl = useIntl(); + const usersRef = React.createRef(); + const groupsRef = React.createRef(); + + const navigate = useAppNavigate('/iam/access-management'); + const location = useLocation(); + const activeTabIndex = useMemo(() => Number(location.pathname.endsWith(pathnames['user-groups'].link)), [location.pathname]); + + useEffect(() => { + location.pathname.endsWith(pathnames['users-and-user-groups'].link) && navigate(pathnames['users-new'].link, { replace: true }); + }, [location.pathname, navigate]); + + const handleTabSelect = (_: React.MouseEvent, key: string | number) => { + activeTabIndex !== Number(key) && + navigate((activeTabIndex ? pathnames['users-new'] : pathnames['user-groups']).link, { + replace: true, + }); + }; + + return ( + + + + + + + + + + + + + ); +}; + +export default UsersAndUserGroups; diff --git a/src/smart-components/access-management/UsersTable.tsx b/src/smart-components/access-management/users-and-user-groups/users/UsersTable.tsx similarity index 94% rename from src/smart-components/access-management/UsersTable.tsx rename to src/smart-components/access-management/users-and-user-groups/users/UsersTable.tsx index 857c43243..b5d589104 100644 --- a/src/smart-components/access-management/UsersTable.tsx +++ b/src/smart-components/access-management/users-and-user-groups/users/UsersTable.tsx @@ -1,5 +1,8 @@ import React, { useEffect, useCallback, useState, Fragment, useMemo, Suspense } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useSelector, useDispatch } from 'react-redux'; +import { Outlet, useSearchParams } from 'react-router-dom'; +import { SkeletonTableBody, SkeletonTableHead, WarningModal } from '@patternfly/react-component-groups'; import { useDataViewSelection, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect'; import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction'; @@ -9,19 +12,16 @@ import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataVi import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; import { ButtonVariant, Pagination, EmptyState, EmptyStateHeader, EmptyStateIcon, EmptyStateBody } from '@patternfly/react-core'; import { ActionsColumn } from '@patternfly/react-table'; -import { fetchUsers } from '../../redux/actions/user-actions'; -import { mappedProps } from '../../helpers/shared/helpers'; -import { RBACStore } from '../../redux/store'; -import { User } from '../../redux/reducers/user-reducer'; -import { FormattedMessage, useIntl } from 'react-intl'; -import messages from '../../Messages'; -import { Outlet, useSearchParams } from 'react-router-dom'; -import { SkeletonTableBody, SkeletonTableHead, WarningModal } from '@patternfly/react-component-groups'; -import paths from '../../utilities/pathnames'; -import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; -import useAppNavigate from '../../hooks/useAppNavigate'; import { DataViewState, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view'; import { SearchIcon } from '@patternfly/react-icons'; +import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; +import useAppNavigate from '../../../../hooks/useAppNavigate'; +import { fetchUsers } from '../../../../redux/actions/user-actions'; +import { mappedProps } from '../../../../helpers/shared/helpers'; +import { RBACStore } from '../../../../redux/store'; +import { User } from '../../../../redux/reducers/user-reducer'; +import messages from '../../../../Messages'; +import paths from '../../../../utilities/pathnames'; const COLUMNS: string[] = ['Username', 'Email', 'First name', 'Last name', 'Status', 'Org admin']; @@ -142,7 +142,7 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick, onAddUserClick([user]), }, { @@ -223,11 +223,12 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick, isDisabled={selected.length === 0} ouiaId={`${OUIA_ID}-add-user-button`} > - {intl.formatMessage(messages['usersAndUserGroupsAddToGroup'])} + {intl.formatMessage(messages['addToUserGroup'])} { + console.log('fooo'); appNavigate(paths['invite-group-users'].link); }} > @@ -250,6 +251,7 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick, { appNavigate(paths['users-and-user-groups'].link); }, diff --git a/src/smart-components/access-management/users-and-user-groups/users/UsersView.tsx b/src/smart-components/access-management/users-and-user-groups/users/UsersView.tsx new file mode 100644 index 000000000..06f3e3f31 --- /dev/null +++ b/src/smart-components/access-management/users-and-user-groups/users/UsersView.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { DataViewEventsProvider } from '@patternfly/react-data-view'; +import { TabContent } from '@patternfly/react-core'; +import UserDetailsDrawer from './user-detail/UserDetailsDrawer'; +import UsersTable from './UsersTable'; +import { User } from '../../../../redux/reducers/user-reducer'; +import AddUserToGroupModal from './add-user-to-group/AddUserToGroupModal'; + +interface UsersViewProps { + usersRef?: React.Ref; +} + +const UsersView: React.FunctionComponent = ({ usersRef }) => { + const [focusedUser, setFocusedUser] = React.useState(undefined); + const [selectedUsers, setSelectedUsers] = React.useState([]); + const [isAddUserGroupModalOpen, setIsAddUserGroupModalOpen] = React.useState(false); + + const handleOpenAddUserToGroupModal = (selected: User[]) => { + if (selected.length > 0) { + setSelectedUsers(selected); + setIsAddUserGroupModalOpen(true); + } + }; + + return ( + <> + + + + + + + + + + ); +}; + +export default UsersView; diff --git a/src/smart-components/access-management/AddUserGroupModal.tsx b/src/smart-components/access-management/users-and-user-groups/users/add-user-to-group/AddUserToGroupModal.tsx similarity index 75% rename from src/smart-components/access-management/AddUserGroupModal.tsx rename to src/smart-components/access-management/users-and-user-groups/users/add-user-to-group/AddUserToGroupModal.tsx index 9198e1e5a..06c53e01d 100644 --- a/src/smart-components/access-management/AddUserGroupModal.tsx +++ b/src/smart-components/access-management/users-and-user-groups/users/add-user-to-group/AddUserToGroupModal.tsx @@ -1,18 +1,18 @@ -import { Button, Modal } from '@patternfly/react-core'; import React from 'react'; -import UserGroupsTable from './UserGroupsTable'; -import { useDispatch } from 'react-redux'; -import { addMembersToGroup } from '../../redux/actions/group-actions'; +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; import { FormattedMessage, useIntl } from 'react-intl'; -import messages from '../../Messages'; +import UserGroupsTable from '../../user-groups/UserGroupsTable'; +import { useDispatch } from 'react-redux'; +import { addMembersToGroup } from '../../../../../redux/actions/group-actions'; +import messages from '../../../../../Messages'; -interface AddUserGroupModalProps { +interface AddUserToGroupModalProps { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; selectedUsers: any[]; } -export const AddUserGroupModal: React.FunctionComponent = ({ isOpen, setIsOpen, selectedUsers }) => { +export const AddUserToGroupModal: React.FunctionComponent = ({ isOpen, setIsOpen, selectedUsers }) => { const [selectedGroups, setSelectedGroups] = React.useState([]); const handleUserGroupsChange = (groups: any[]) => setSelectedGroups(groups); const dispatch = useDispatch(); @@ -30,7 +30,8 @@ export const AddUserGroupModal: React.FunctionComponent return ( ); }; -export default AddUserGroupModal; +export default AddUserToGroupModal; diff --git a/src/smart-components/access-management/UserDetailsDrawer.tsx b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsDrawer.tsx similarity index 96% rename from src/smart-components/access-management/UserDetailsDrawer.tsx rename to src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsDrawer.tsx index 419a84136..cbfc9f70c 100644 --- a/src/smart-components/access-management/UserDetailsDrawer.tsx +++ b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsDrawer.tsx @@ -1,3 +1,5 @@ +import React, { useEffect } from 'react'; +import { useIntl } from 'react-intl'; import { Drawer, DrawerActions, @@ -15,14 +17,12 @@ import { TextContent, Title, } from '@patternfly/react-core'; -import React, { useEffect } from 'react'; -import { User } from '../../redux/reducers/user-reducer'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; -import { useIntl } from 'react-intl'; -import messages from '../../Messages'; +import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view'; +import { User } from '../../../../../redux/reducers/user-reducer'; +import messages from '../../../../../Messages'; import UserDetailsGroupsView from './UserDetailsGroupsView'; import UserDetailsRolesView from './UserDetailsRolesView'; -import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view'; interface UserDetailsProps { focusedUser?: User; diff --git a/src/smart-components/access-management/UserDetailsGroupsView.tsx b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsGroupsView.tsx similarity index 84% rename from src/smart-components/access-management/UserDetailsGroupsView.tsx rename to src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsGroupsView.tsx index 5051ad2e6..91e62f19d 100644 --- a/src/smart-components/access-management/UserDetailsGroupsView.tsx +++ b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsGroupsView.tsx @@ -1,11 +1,11 @@ -import { DataView, DataViewTable } from '@patternfly/react-data-view'; import React, { useCallback, useEffect } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { mappedProps } from '../../helpers/shared/helpers'; -import { fetchGroups } from '../../redux/actions/group-actions'; -import { RBACStore } from '../../redux/store'; -import messages from '../../Messages'; +import { DataView, DataViewTable } from '@patternfly/react-data-view'; +import { mappedProps } from '../../../../../helpers/shared/helpers'; +import { fetchGroups } from '../../../../../redux/actions/group-actions'; +import { RBACStore } from '../../../../../redux/store'; +import messages from '../../../../../Messages'; interface UserGroupsViewProps { userId: string; diff --git a/src/smart-components/access-management/UserDetailsRolesView.tsx b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsRolesView.tsx similarity index 84% rename from src/smart-components/access-management/UserDetailsRolesView.tsx rename to src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsRolesView.tsx index 6ca781cd4..be7ae6c9f 100644 --- a/src/smart-components/access-management/UserDetailsRolesView.tsx +++ b/src/smart-components/access-management/users-and-user-groups/users/user-detail/UserDetailsRolesView.tsx @@ -1,11 +1,11 @@ -import { DataView, DataViewTable } from '@patternfly/react-data-view'; import React, { useCallback, useEffect } from 'react'; +import { DataView, DataViewTable } from '@patternfly/react-data-view'; import { useDispatch, useSelector } from 'react-redux'; -import { RBACStore } from '../../redux/store'; -import messages from '../../Messages'; import { useIntl } from 'react-intl'; -import { fetchRoles } from '../../redux/actions/role-actions'; -import { mappedProps } from '../../helpers/shared/helpers'; +import { RBACStore } from '../../../../../redux/store'; +import messages from '../../../../../Messages'; +import { fetchRoles } from '../../../../../redux/actions/role-actions'; +import { mappedProps } from '../../../../../helpers/shared/helpers'; interface UserRolesViewProps { userId: string; diff --git a/src/smart-components/group/add-group/add-group-wizard.js b/src/smart-components/group/add-group/add-group-wizard.js index 4c4f94682..cc400dc33 100644 --- a/src/smart-components/group/add-group/add-group-wizard.js +++ b/src/smart-components/group/add-group/add-group-wizard.js @@ -2,6 +2,7 @@ import React, { useState, createContext, useRef } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import { useIntl } from 'react-intl'; +import { useSearchParams } from 'react-router-dom'; import { Wizard } from '@patternfly/react-core/deprecated'; import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/'; import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer'; @@ -58,15 +59,17 @@ export const onCancel = (emptyCallback, nonEmptyCallback, setGroupData) => (form } }; -const AddGroupWizard = ({ postMethod, pagination, filters, orderBy, enableRoles = true, setIsWizardOpen }) => { +const AddGroupWizard = ({ postMethod, pagination, filters, orderBy }) => { const dispatch = useDispatch(); const intl = useIntl(); const container = useRef(document.createElement('div')); const { isBeta } = useChrome(); + const enableWorkspaces = useFlag('platform.rbac.workspaces'); const enableServiceAccounts = (isBeta() && useFlag('platform.rbac.group-service-accounts')) || (!isBeta() && useFlag('platform.rbac.group-service-accounts.stable')); - const schema = useRef(schemaBuilder(container.current, enableServiceAccounts, enableRoles)); + const schema = useRef(schemaBuilder(container.current, enableServiceAccounts, !enableWorkspaces)); const navigate = useAppNavigate(); + const search = useSearchParams(); const [groupData, setGroupData] = useState({}); const [wizardContextValue, setWizardContextValue] = useState({ success: false, @@ -84,12 +87,17 @@ const AddGroupWizard = ({ postMethod, pagination, filters, orderBy, enableRoles description: intl.formatMessage(messages.addingGroupCanceledDescription), }) ); - setIsWizardOpen - ? setIsWizardOpen(false) - : navigate({ - pathname: paths.groups.link, - search: createQueryParams({ page: 1, per_page: pagination.limit, ...filters }), - }); + navigate( + enableWorkspaces + ? { + pathname: paths['user-groups'].link, + search: search.toString(), + } + : { + pathname: paths.groups.link, + search: createQueryParams({ page: 1, per_page: pagination.limit, ...filters }), + } + ); }; const setWizardError = (error) => setWizardContextValue((prev) => ({ ...prev, error })); @@ -104,7 +112,7 @@ const AddGroupWizard = ({ postMethod, pagination, filters, orderBy, enableRoles name: formData['group-name'], description: formData['group-description'], user_list: formData['users-list'].map((user) => ({ username: user.label })), - roles_list: enableRoles ? formData['roles-list'].map((role) => role.uuid) : [], + roles_list: enableWorkspaces ? [] : formData['roles-list'].map((role) => role.uuid), }; dispatch(addGroup(groupData)).then(({ value }) => { setWizardContextValue((prev) => ({ @@ -126,12 +134,17 @@ const AddGroupWizard = ({ postMethod, pagination, filters, orderBy, enableRoles const onClose = () => { setWizardContextValue((prev) => ({ ...prev, success: false, hideForm: false })); postMethod({ limit: pagination.limit, offset: 0, orderBy, filters: {} }); - setIsWizardOpen - ? setIsWizardOpen(false) - : navigate({ - pathname: paths.groups.link, - search: createQueryParams({ page: 1, per_page: pagination.limit }), - }); + navigate( + enableWorkspaces + ? { + pathname: paths['user-groups'].link, + search: search.toString(), + } + : { + pathname: paths.groups.link, + search: createQueryParams({ page: 1, per_page: pagination.limit, ...filters }), + } + ); }; return ( @@ -149,7 +162,7 @@ const AddGroupWizard = ({ postMethod, pagination, filters, orderBy, enableRoles {intl.formatMessage(messages.discardedInputsWarning)} {wizardContextValue.hideForm ? ( - wizardContextValue.success ? ( + wizardContextValue.success && ( - ) : null + ) ) : (