From fd358e0fb343febf3c419a5ae2b4c496dcfc971c Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Fri, 8 Nov 2024 17:05:33 +0100 Subject: [PATCH] feat(invite-users): add invite users modal via data driven forms --- package-lock.json | 114 +- package.json | 2 +- src/Messages.js | 76 + src/Routing.tsx | 16 +- src/helpers/user/user-helper.js | 4 + .../shared/table-composable-toolbar-view.tsx | 2 +- .../access-management/UsersTable.tsx | 53 +- .../common/expandable-checkbox.tsx | 75 + .../{form-renderer.js => form-renderer.tsx} | 8 +- .../invite-users-modal-common-auth.tsx | 156 + .../user/users-list-not-selectable.tsx | 166 +- src/smart-components/user/users.tsx | 5 +- .../frontend-components/useChrome/index.js | 2 + .../shared/__snapshots__/toolbar.test.js.snap | 1001 ++-- src/test/role/__snapshots__/role.test.js.snap | 4951 +++++------------ .../role/__snapshots__/roles.test.js.snap | 1275 +---- .../inventory-groups-role.test.js.snap | 6 + .../group/__snapshots__/groups.test.js.snap | 171 +- .../add-group-members.test.js.snap | 358 +- .../__snapshots__/group-members.test.js.snap | 1433 +---- .../add-group-service-accounts.test.js.snap | 167 +- .../group-service-accounts.test.js.snap | 182 +- .../CommonBundleView.test.js.snap | 267 +- src/utilities/pathnames.js | 7 +- 24 files changed, 3345 insertions(+), 7152 deletions(-) create mode 100644 src/smart-components/common/expandable-checkbox.tsx rename src/smart-components/common/{form-renderer.js => form-renderer.tsx} (77%) create mode 100644 src/smart-components/user/invite-users/invite-users-modal-common-auth.tsx diff --git a/package-lock.json b/package-lock.json index 50e46283d..23115dc80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@data-driven-forms/react-form-renderer": "^3.22.4", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.4.0-prerelease.2", + "@patternfly/react-component-groups": "^5.5.4", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.2.0", "@patternfly/react-icons": "^5.1.1", @@ -4030,12 +4030,13 @@ } }, "node_modules/@patternfly/react-component-groups": { - "version": "5.4.0-prerelease.2", - "license": "MIT", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.5.4.tgz", + "integrity": "sha512-3qf0CU3vtHGGb38LkfCfZAfrm1zXReFVjpI5E0WryLOpWl+NamuYbKfyY7j1SCj8Zq4kqCTLdXj+je3WBs+GGg==", "dependencies": { - "@patternfly/react-core": "^5.3.3", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-table": "^5.3.3", + "@patternfly/react-core": "^5.4.1", + "@patternfly/react-icons": "^5.4.0", + "@patternfly/react-table": "^5.4.1", "clsx": "^2.1.1", "react-jss": "^10.10.0" }, @@ -4052,15 +4053,16 @@ } }, "node_modules/@patternfly/react-core": { - "version": "5.3.4", - "license": "MIT", - "dependencies": { - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-styles": "^5.3.1", - "@patternfly/react-tokens": "^5.3.1", - "focus-trap": "7.5.2", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.8.tgz", + "integrity": "sha512-4KRsQsH39VmTiFPLdN34QqNZg6gKrTamJxKtWEPO1VKA0TpoRMwpFEGk9BDyxipxYST6WzXznAaLCidGkCDlWw==", + "dependencies": { + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "@patternfly/react-tokens": "^5.4.1", + "focus-trap": "7.6.0", "react-dropzone": "^14.2.3", - "tslib": "^2.5.0" + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^17 || ^18", @@ -4084,21 +4086,6 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-data-view/node_modules/@patternfly/react-component-groups": { - "version": "5.3.0-prerelease.2", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.3.3", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-table": "^5.3.3", - "clsx": "^2.1.1", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, "node_modules/@patternfly/react-data-view/node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -4107,27 +4094,30 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "5.3.2", - "license": "MIT", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.4.2.tgz", + "integrity": "sha512-CMQ5oHYzW6TPVTs2jpNJmP2vGCAKR/YeTPwHGO9dLkAUej1IcIxtCCWK2Fdo2UJsnBjuZihasyw2b6ehvbUm9Q==", "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" } }, "node_modules/@patternfly/react-styles": { - "version": "5.3.1", - "license": "MIT" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.4.1.tgz", + "integrity": "sha512-XA8PXksD8uiA3RTwxdUwJXOCf+V6sVd+2HKapWAdRLvtSV+Sdk7NgCvalb4IAQncsddLopjPQD8gAHA298+N8w==" }, "node_modules/@patternfly/react-table": { - "version": "5.3.4", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.3.4", - "@patternfly/react-icons": "^5.3.2", - "@patternfly/react-styles": "^5.3.1", - "@patternfly/react-tokens": "^5.3.1", - "lodash": "^4.17.19", - "tslib": "^2.5.0" + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-5.4.9.tgz", + "integrity": "sha512-fSbBZRihVCAaUOKRFzzqYhBrTSI/VGU6O9I0a21T+bXwHz071OsefBdE/ZQiJhqHpJTC+WAZWM76/1CEEnrBFw==", + "dependencies": { + "@patternfly/react-core": "^5.4.8", + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "@patternfly/react-tokens": "^5.4.1", + "lodash": "^4.17.21", + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^17 || ^18", @@ -4135,8 +4125,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "5.3.1", - "license": "MIT" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.4.1.tgz", + "integrity": "sha512-eygdHE7Krta1mijAv/E8RHiKIgysD0eeNTo8EXUYC8/M4e5K6sqpr2p6rQBF8QiRMN8FnbXvZT3K2OQ28pYt9Q==" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -5207,28 +5198,6 @@ "react-router-dom": "^5.0.0 || ^6.0.0" } }, - "node_modules/@redhat-cloud-services/frontend-components/node_modules/@patternfly/react-component-groups": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^5.1.1", - "@patternfly/react-icons": "^5.1.1", - "@patternfly/react-table": "^5.1.1", - "clsx": "^2.0.0", - "react-jss": "^10.10.0" - }, - "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@redhat-cloud-services/frontend-components/node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@redhat-cloud-services/host-inventory-client": { "version": "1.5.0", "license": "Apache-2.0", @@ -12255,8 +12224,9 @@ "peer": true }, "node_modules/focus-trap": { - "version": "7.5.2", - "license": "MIT", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", "dependencies": { "tabbable": "^6.2.0" } @@ -24850,7 +24820,8 @@ }, "node_modules/tabbable": { "version": "6.2.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tapable": { "version": "2.2.1", @@ -25652,8 +25623,9 @@ "license": "ISC" }, "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tunnel-agent": { "version": "0.6.0", diff --git a/package.json b/package.json index 0a4051105..0432e5f77 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@data-driven-forms/react-form-renderer": "^3.22.4", "@formatjs/cli": "6.2.2", "@patternfly/quickstarts": "^5.1.0", - "@patternfly/react-component-groups": "^5.4.0-prerelease.2", + "@patternfly/react-component-groups": "^5.5.4", "@patternfly/react-core": "^5.1.1", "@patternfly/react-data-view": "^5.2.0", "@patternfly/react-icons": "^5.1.1", diff --git a/src/Messages.js b/src/Messages.js index 229171b79..aa9b03c15 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -28,6 +28,67 @@ export default defineMessages({ defaultMessage: 'The organization administrator role is the highest permission level with full access to content and features. This is the only role that can manage users.', }, + inviteUsersFormManageSupportCasesFieldTitle: { + id: 'inviteUsersFormManageSupportCasesFieldTitle', + description: 'Invite users form manage support cases field title', + defaultMessage: 'Organization administrators', + }, + inviteUsersFormManageSupportCasesFieldDescription: { + id: 'inviteUsersFormManageSupportCasesFieldDescription', + description: 'Invite users form manage support cases field description', + defaultMessage: + 'The organization administrator role is the highest permission level with full access to content and features. This is the only role that can manage users.', + }, + inviteUsersFormDownloadSoftwareUpdatesFieldTitle: { + id: 'inviteUsersFormDownloadSoftwareUpdatesFieldTitle', + description: 'Invite users form download software and updates field title', + defaultMessage: 'Download software and updates', + }, + inviteUsersFormDownloadSoftwareUpdatesFieldDescription: { + id: 'inviteUsersFormDownloadSoftwareUpdatesFieldDescription', + description: 'Invite users form download software and updates field description', + defaultMessage: 'User can download software and updates from the Red Hat Customer Portal.', + }, + inviteUsersFormManageSubscriptionsFieldTitle: { + id: 'inviteUsersFormManageSubscriptionsFieldTitle', + description: 'Invite users form manage subscriptions field title', + defaultMessage: 'Manage your subscriptions', + }, + inviteUsersFormManageSubscriptionsFieldDescription: { + id: 'inviteUsersFormManageSubscriptionsFieldDescription', + description: 'Invite users form manage subscriptions field description', + defaultMessage: 'Grants user access to subscription management via Red Hat Subscription Management in the Red Hat Customer Portal.', + }, + inviteUsersFormManageSubscriptionsViewEditUsersOnlyTitle: { + id: 'inviteUsersFormManageSubscriptionsViewEditUsersOnlyTitle', + description: 'Invite users form manage subscriptions field View edit Users only title', + defaultMessage: 'View/Edit user’s only', + }, + inviteUsersFormManageSubscriptionsViewEditUsersOnlyDescription: { + id: 'inviteUsersFormManageSubscriptionsViewEditUsersOnlyDescription', + description: 'Invite users form manage subscriptions field View edit Users only description', + defaultMessage: 'User can view and edit only the systems that they have registered in the account.', + }, + inviteUsersFormManageSubscriptionsViewAllTitle: { + id: 'inviteUsersFormManageSubscriptionsViewAllTitle', + description: 'Invite users form manage subscriptions field view all option title', + defaultMessage: 'User can view and edit only the systems that they have registered in the account.', + }, + inviteUsersFormManageSubscriptionsViewAllDescription: { + id: 'inviteUsersFormManageSubscriptionsViewAllDescription', + description: 'Invite users form manage subscriptions field view all option description', + defaultMessage: 'User can view (but not edit) all systems and Subscription Management Applications in the account.', + }, + inviteUsersFormManageSubscriptionsViewEditAllTitle: { + id: 'inviteUsersFormManageSubscriptionsViewEditAllTitle', + description: 'Invite users form manage subscriptions field View/Edit all option title', + defaultMessage: 'View/Edit all', + }, + inviteUsersFormManageSubscriptionsViewEditAllDescription: { + id: 'inviteUsersFormManageSubscriptionsViewEditAllDescription', + description: 'Invite users form manage subscriptions field View/Edit all option description', + defaultMessage: 'User can view and edit all systems and Subscription Management Applications in the account.', + }, inviteUsersFormEmailsFieldTitle: { id: 'inviteUsersFormEmailsFieldTitle', description: 'Invite users form emails field title', @@ -38,6 +99,21 @@ export default defineMessages({ description: 'Invite users form emails field description', defaultMessage: 'Enter up to 50 e-mail addresses separated by commas or returns.', }, + inviteUsersFormEmailsFieldError: { + id: 'inviteUsersFormEmailsFieldError', + description: 'Invite users form emails field error message is one email address is not valid.', + defaultMessage: 'Some of the email addresses you provided are not valid', + }, + inviteUsersMessageTitle: { + id: 'inviteUsersMessageTitle', + description: 'Message to be sent to each email', + defaultMessage: 'Send a message with the invite', + }, + inviteUsersCustomerPortalPermissions: { + id: 'inviteUsersCustomerPortalPermissions', + description: 'Customer portal permissions title', + defaultMessage: 'Customer Portal access permissions', + }, inviteUsersCancelled: { id: 'inviteUsersCancelled', description: 'Invite users cancelled notification description', diff --git a/src/Routing.tsx b/src/Routing.tsx index 9ceadb129..2355d21ba 100644 --- a/src/Routing.tsx +++ b/src/Routing.tsx @@ -16,6 +16,7 @@ const Users = lazy(() => import('./smart-components/user/users')); const UserDetail = lazy(() => import('./smart-components/user/user')); const AddUserToGroup = lazy(() => import('./smart-components/user/add-user-to-group/add-user-to-group')); const InviteUsersModal = lazy(() => import('./smart-components/user/invite-users/invite-users-modal')); +const InviteUsersModalCommonAuth = lazy(() => import('./smart-components/user/invite-users/invite-users-modal-common-auth')); const Roles = lazy(() => import('./smart-components/role/roles')); const Role = lazy(() => import('./smart-components/role/role')); @@ -43,10 +44,16 @@ const QuickstartsTest = lazy(() => import('./smart-components/quickstarts/quicks const UsersAndUserGroups = lazy(() => import('./smart-components/access-management/users-and-user-groups')); -const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag }: Record) => [ +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.overview.path, @@ -74,9 +81,9 @@ const getRoutes = ({ enableServiceAccounts, isITLess, isWorkspacesFlag }: Record path: pathnames.users.path, element: Users, childRoutes: [ - isITLess && { + (isITLess || isCommonAuthModel) && { path: pathnames['invite-users'].path, - element: InviteUsersModal, + element: isCommonAuthModel ? InviteUsersModalCommonAuth : InviteUsersModal, }, ], }, @@ -262,6 +269,7 @@ const Routing = () => { const location = useLocation(); const { updateDocumentTitle, isBeta } = useChrome(); const isITLess = useFlag('platform.rbac.itless'); + const isCommonAuthModel = useFlag('platform.rbac.common-auth-model'); const enableServiceAccounts = (isBeta() && useFlag('platform.rbac.group-service-accounts')) || (!isBeta() && useFlag('platform.rbac.group-service-accounts.stable')); const isWorkspacesFlag = useFlag('platform.rbac.workspaces'); @@ -282,7 +290,7 @@ const Routing = () => { } }, [location.pathname, updateDocumentTitle]); - const routes = getRoutes({ enableServiceAccounts, isITLess, isWorkspacesFlag }); + const routes = getRoutes({ enableServiceAccounts, isITLess, isWorkspacesFlag, isCommonAuthModel }); const renderedRoutes = useMemo(() => renderRoutes(routes as never), [routes]); return ( }> diff --git a/src/helpers/user/user-helper.js b/src/helpers/user/user-helper.js index e81716d24..958bb2dca 100644 --- a/src/helpers/user/user-helper.js +++ b/src/helpers/user/user-helper.js @@ -2,6 +2,10 @@ import { getLastPageOffset, isOffsetValid } from '../shared/pagination'; import { getPrincipalApi } from '../shared/user-login'; import { isInt, isStage, isITLessProd } from '../../itLessConfig'; +export const MANAGE_SUBSCRIPTIONS_VIEW_EDIT_USER = 'view-edit-user'; +export const MANAGE_SUBSCRIPTIONS_VIEW_ALL = 'view-all'; +export const MANAGE_SUBSCRIPTIONS_VIEW_EDIT_ALL = 'view-edit-all'; + const principalApi = getPrincipalApi(); const principalStatusApiMap = { diff --git a/src/presentational-components/shared/table-composable-toolbar-view.tsx b/src/presentational-components/shared/table-composable-toolbar-view.tsx index 2cb2d7666..2b3cb6a39 100644 --- a/src/presentational-components/shared/table-composable-toolbar-view.tsx +++ b/src/presentational-components/shared/table-composable-toolbar-view.tsx @@ -54,7 +54,7 @@ interface MainTableProps { setFilterValue: (value: FilterProps) => void; pagination: { limit?: number; offset?: number; count?: number; noBottom?: boolean }; fetchData: (config: FetchDataProps) => void; - toolbarButtons?: () => React.ReactNode[]; + toolbarButtons?: () => (React.JSX.Element | React.ReactNode)[]; filterPlaceholder?: string; filters: Array<{ value: string | number | Array; diff --git a/src/smart-components/access-management/UsersTable.tsx b/src/smart-components/access-management/UsersTable.tsx index 6fd7c80ca..9bb29e406 100644 --- a/src/smart-components/access-management/UsersTable.tsx +++ b/src/smart-components/access-management/UsersTable.tsx @@ -1,11 +1,13 @@ -import React, { useEffect, useCallback, useState, Fragment, useMemo } from 'react'; +import React, { useEffect, useCallback, useState, Fragment, useMemo, Suspense } from 'react'; import { useSelector, useDispatch } from 'react-redux'; 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'; +import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions'; 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 { Button, Pagination, ButtonVariant } from '@patternfly/react-core'; +import { Pagination, ButtonVariant } from '@patternfly/react-core'; import { ActionsColumn } from '@patternfly/react-table'; import { fetchUsers } from '../../redux/actions/user-actions'; import { mappedProps } from '../../helpers/shared/helpers'; @@ -13,9 +15,12 @@ import { RBACStore } from '../../redux/store'; import { User } from '../../redux/reducers/user-reducer'; import { useIntl } from 'react-intl'; import messages from '../../Messages'; -import { useSearchParams } from 'react-router-dom'; +import { Outlet, useSearchParams } from 'react-router-dom'; import { WarningModal } from '@patternfly/react-component-groups'; import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view'; +import paths from '../../utilities/pathnames'; +import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; +import useAppNavigate from '../../hooks/useAppNavigate'; const COLUMNS: string[] = ['Username', 'Email', 'First name', 'Last name', 'Status', 'Org admin']; @@ -35,11 +40,13 @@ interface UsersTableProps { } const UsersTable: React.FunctionComponent = ({ onAddUserClick, focusedUser }) => { + const { getBundle, getApp } = useChrome(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [currentUser, setCurrentUser] = useState(); const dispatch = useDispatch(); const intl = useIntl(); const { trigger } = useDataViewEventsContext(); + const appNavigate = useAppNavigate(`/${getBundle()}/${getApp()}`); const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent, user: User) => { setCurrentUser(user); @@ -176,21 +183,41 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick, } pagination={React.cloneElement(paginationComponent, { isCompact: true })} actions={ - + + { + onAddUserClick(selected); + }} + variant="primary" + isDisabled={selected.length === 0} + ouiaId={`${OUIA_ID}-add-user-button`} + > + {intl.formatMessage(messages['usersAndUserGroupsAddToGroup'])} + + { + appNavigate(paths['invite-group-users'].link); + }} + > + {intl.formatMessage(messages.inviteUsers)} + + } /> + + { + appNavigate(paths['users-and-user-groups'].link); + }, + }} + /> + ); }; diff --git a/src/smart-components/common/expandable-checkbox.tsx b/src/smart-components/common/expandable-checkbox.tsx new file mode 100644 index 000000000..b620c21e5 --- /dev/null +++ b/src/smart-components/common/expandable-checkbox.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useFormApi } from '@data-driven-forms/react-form-renderer'; +import useFieldApi, { UseFieldApiConfig } from '@data-driven-forms/react-form-renderer/use-field-api'; +import { ExpandableSection, Checkbox, FormGroup, Radio, Tooltip } from '@patternfly/react-core'; +import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/outlined-question-circle-icon'; + +export type ExpandableCheckboxItemType = { + title: string; + description: string; + name: string; + options?: { title: string; description: string; name: string }[]; +}; + +const ExpandableCheckbox: React.FC = (props) => { + const formOptions = useFormApi(); + const { items, input, ...rest } = useFieldApi(props); + console.log(formOptions, rest, formOptions.getState().values, 'this is formOptions!'); + return ( + + {items?.map((item: ExpandableCheckboxItemType, key: number) => ( + { + const newVal = value && item.options ? item.options?.[0]?.name : value; + input.onChange({ + ...formOptions.getState().values[input.name], + [item.name]: newVal, + }); + }} + label={ + + {item.description} + {item.options && ( + + {item.options?.map((option, key) => ( + { + input.onChange({ + ...formOptions.getState().values[input.name], + [item.name]: option.name, + }); + }} + label={ + + {option.title}{' '} + {option.description && ( + + + + )} + + } + id={`${item.name}-option-${key}`} + /> + ))} + + )} + + } + id={`${item.name}-checkbox`} + name={item.name} + /> + ))} + + ); +}; + +// const FieldListenerWrapper: React.FC = (props) => {() => }; + +export default ExpandableCheckbox; diff --git a/src/smart-components/common/form-renderer.js b/src/smart-components/common/form-renderer.tsx similarity index 77% rename from src/smart-components/common/form-renderer.js rename to src/smart-components/common/form-renderer.tsx index fd9125e1c..36a369d52 100644 --- a/src/smart-components/common/form-renderer.js +++ b/src/smart-components/common/form-renderer.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import FormButtons from './FormButtons'; import FormTemplate from '@data-driven-forms/pf4-component-mapper/form-template'; import TextField from '@data-driven-forms/pf4-component-mapper/text-field'; import Textarea from '@data-driven-forms/pf4-component-mapper/textarea'; import ReactFormRender from '@data-driven-forms/react-form-renderer/form-renderer'; import componentTypes from '@data-driven-forms/react-form-renderer/component-types'; +import FormTemplateCommonProps from '@data-driven-forms/common/form-template'; -const FormRenderer = ({ formTemplateProps, ...props }) => ( +const FormRenderer: React.FC<{ formTemplateProps?: FormTemplateCommonProps } & FormTemplateCommonProps> = ({ formTemplateProps, ...props }) => ( ( /> ); -FormRenderer.propTypes = { - formTemplateProps: PropTypes.object, -}; - export default FormRenderer; diff --git a/src/smart-components/user/invite-users/invite-users-modal-common-auth.tsx b/src/smart-components/user/invite-users/invite-users-modal-common-auth.tsx new file mode 100644 index 000000000..320beffbc --- /dev/null +++ b/src/smart-components/user/invite-users/invite-users-modal-common-auth.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import FormRenderer from '../../common/form-renderer'; +import ModalFormTemplate from '../../common/ModalFormTemplate'; +import { useIntl } from 'react-intl'; +import messages from '../../../Messages'; +import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer'; +import componentMapper from '@data-driven-forms/pf4-component-mapper/component-mapper'; +import AccordionCheckbox from '../../common/expandable-checkbox'; +import { addUsers } from '../../../redux/actions/user-actions'; +import { useDispatch } from 'react-redux'; +import { useFlag } from '@unleash/proxy-client-react'; +import { + MANAGE_SUBSCRIPTIONS_VIEW_ALL, + MANAGE_SUBSCRIPTIONS_VIEW_EDIT_ALL, + MANAGE_SUBSCRIPTIONS_VIEW_EDIT_USER, +} from '../../../helpers/user/user-helper'; +import { useOutletContext } from 'react-router-dom'; + +const ExpandableCheckboxComponent = 'expandableCheckbox'; +const EMAIL_REGEXP = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +type SubmitValues = { + 'email-addresses': string; + 'invite-message'?: string; + 'customer-portal-permissions'?: { + 'is-org-admin'?: boolean; + 'manage-support-cases'?: boolean; + 'download-software-updates'?: boolean; + 'manage-subscriptions'?: + | typeof MANAGE_SUBSCRIPTIONS_VIEW_EDIT_USER + | typeof MANAGE_SUBSCRIPTIONS_VIEW_ALL + | typeof MANAGE_SUBSCRIPTIONS_VIEW_EDIT_ALL; + }; +}; + +const InviteUsers = () => { + const { fetchData } = useOutletContext<{ fetchData: (isSubmit: boolean) => void }>(); + const isCommonAuth = useFlag('platform.rbac.common-auth-model'); + const dispatch = useDispatch(); + const onCancel = () => { + fetchData(false); + }; + const onSubmit = (values: SubmitValues) => { + const action = addUsers( + { + emails: values['email-addresses']?.split(/[\s,]+/), + message: values['invite-message'], + isAdmin: values['customer-portal-permissions']?.['is-org-admin'], + manageSupportCases: values['customer-portal-permissions']?.['manage-support-cases'], + downloadUpdates: values['customer-portal-permissions']?.['download-software-updates'], + mangeSubscriptions: values['customer-portal-permissions']?.['manage-subscriptions'], + }, + isCommonAuth + ); + action.payload.then(() => fetchData(true)); + dispatch(action); + }; + const intl = useIntl(); + const schema = React.useMemo( + () => ({ + description: intl.formatMessage(messages.inviteUsersDescription), + fields: [ + { + component: componentTypes.TEXTAREA, + label: intl.formatMessage(messages.inviteUsersFormEmailsFieldTitle), + name: 'email-addresses', + placeholder: intl.formatMessage(messages.inviteUsersFormEmailsFieldDescription), + rows: 5, + isRequired: true, + validate: [ + { + type: validatorTypes.REQUIRED, + }, + (value: string) => + value.split(/[\s,]+/).every((email: string) => EMAIL_REGEXP.test(email)) + ? undefined + : intl.formatMessage(messages.inviteUsersFormEmailsFieldError), + ], + }, + { + component: componentTypes.TEXTAREA, + label: intl.formatMessage(messages.inviteUsersMessageTitle), + name: 'invite-message', + rows: 3, + }, + { + component: ExpandableCheckboxComponent, + items: [ + { + name: 'is-org-admin', + title: intl.formatMessage(messages.inviteUsersFormIsAdminFieldTitle), + description: intl.formatMessage(messages.inviteUsersFormIsAdminFieldDescription), + }, + { + name: 'manage-support-cases', + title: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsFieldTitle), + description: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsFieldDescription), + }, + { + name: 'download-software-updates', + title: intl.formatMessage(messages.inviteUsersFormDownloadSoftwareUpdatesFieldTitle), + description: intl.formatMessage(messages.inviteUsersFormDownloadSoftwareUpdatesFieldDescription), + }, + { + name: 'manage-subscriptions', + title: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsFieldTitle), + description: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsFieldDescription), + options: [ + { + name: MANAGE_SUBSCRIPTIONS_VIEW_EDIT_USER, + title: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewEditUsersOnlyTitle), + description: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewEditUsersOnlyDescription), + }, + { + name: MANAGE_SUBSCRIPTIONS_VIEW_ALL, + title: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewAllTitle), + description: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewAllDescription), + }, + { + name: MANAGE_SUBSCRIPTIONS_VIEW_EDIT_ALL, + title: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewEditAllTitle), + description: intl.formatMessage(messages.inviteUsersFormManageSubscriptionsViewEditAllDescription), + }, + ], + }, + ], + name: 'customer-portal-permissions', + }, + ], + }), + [] + ); + return ( + ( + + )} + /> + ); +}; + +export default InviteUsers; diff --git a/src/smart-components/user/users-list-not-selectable.tsx b/src/smart-components/user/users-list-not-selectable.tsx index 82851980e..760552296 100644 --- a/src/smart-components/user/users-list-not-selectable.tsx +++ b/src/smart-components/user/users-list-not-selectable.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useState, useContext, useRef, useCallback } from 'react'; +import React, { useEffect, useState, useContext, useRef, useCallback, Suspense } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { mappedProps } from '../../helpers/shared/helpers'; import { TableComposableToolbarView } from '../../presentational-components/shared/table-composable-toolbar-view'; import { fetchUsers, updateUsersFilters } from '../../redux/actions/user-actions'; import UsersRow from '../../presentational-components/shared/UsersRow'; +import paths from '../../utilities/pathnames'; import { defaultSettings, defaultAdminSettings, @@ -19,6 +20,11 @@ import PermissionsContext from '../../utilities/permissions-context'; import { createRows } from './user-table-helpers'; import { ISortBy } from '@patternfly/react-table'; import { UserFilters } from '../../redux/reducers/user-reducer'; +import AppLink from '../../presentational-components/shared/AppLink'; +import { Button } from '@patternfly/react-core'; +import { useFlag } from '@unleash/proxy-client-react'; +import useAppNavigate from '../../hooks/useAppNavigate'; +import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; interface UsersListNotSelectableI { userLinks: boolean; @@ -32,6 +38,9 @@ const UsersListNotSelectable = ({ userLinks, usesMetaInURL, props }: UsersListNo const location = useLocation(); const dispatch = useDispatch(); const { orgAdmin } = useContext(PermissionsContext); + const isCommonAuthModel = useFlag('platform.rbac.common-auth-model'); + const { getBundle, getApp } = useChrome(); + const appNavigate = useAppNavigate(`/${getBundle()}/${getApp()}`); // use for text filter to focus const innerRef = useRef(null); @@ -111,73 +120,96 @@ const UsersListNotSelectable = ({ userLinks, usesMetaInURL, props }: UsersListNo setFilters({ username: '', ...payload }); }; + const toolbarButtons = () => [ + + + , + ]; + return ( - { - const orderBy = `${direction === 'desc' ? '-' : ''}${columns[index].key}`; - setSortByState({ index, direction }); - fetchData({ ...pagination, filters, usesMetaInURL, orderBy }); - }} - ouiaId="users-table" - fetchData={(config) => { - const status = Object.prototype.hasOwnProperty.call(config, 'status') ? config.status : filters.status; - const { username, email, count, limit, offset, orderBy } = config; - - Promise.resolve(fetchData({ ...mappedProps({ count, limit, offset, orderBy, filters: { username, email, status } }), usesMetaInURL })).then( - () => { - if (innerRef !== null && innerRef.current !== null) { - innerRef.current.focus(); + + [] as React.ReactNode[]} + isSelectable={false} + isCompact={false} + borders={false} + columns={columns} + rows={createRows(userLinks, users, intl)} + sortBy={sortByState} + onSort={(e, index, direction) => { + const orderBy = `${direction === 'desc' ? '-' : ''}${columns[index].key}`; + setSortByState({ index, direction }); + fetchData({ ...pagination, filters, usesMetaInURL, orderBy }); + }} + ouiaId="users-table" + fetchData={(config) => { + const status = Object.prototype.hasOwnProperty.call(config, 'status') ? config.status : filters.status; + const { username, email, count, limit, offset, orderBy } = config; + + Promise.resolve(fetchData({ ...mappedProps({ count, limit, offset, orderBy, filters: { username, email, status } }), usesMetaInURL })).then( + () => { + if (innerRef !== null && innerRef.current !== null) { + innerRef.current.focus(); + } } - } - ); - applyPaginationToUrl(location, navigate, limit || 0, offset || 0); - usesMetaInURL && applyFiltersToUrl(location, navigate, { username, email, status }); - }} - emptyFilters={{ username: '', email: '', status: [] }} - setFilterValue={({ username, email, status }) => { - updateFilters({ - username: typeof username === 'undefined' ? filters.username : username, - email: typeof email === 'undefined' ? filters.email : email, - status: typeof status === 'undefined' || status === filters.status ? filters.status : status, - }); - }} - isLoading={isLoading} - pagination={pagination} - rowWrapper={UsersRow} - title={{ singular: intl.formatMessage(messages.user), plural: intl.formatMessage(messages.users).toLowerCase() }} - filters={[ - { - key: 'username', - value: typeof filters?.username === 'object' || typeof filters?.username === 'undefined' ? '' : filters.username, - placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.username).toLowerCase() }), - innerRef, - }, - { - key: 'email', - value: filters.email || '', - placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.email).toLowerCase() }), - innerRef, - }, - { - key: 'status', - value: filters.status || [], - label: intl.formatMessage(messages.status), - type: 'checkbox', - items: [ - { label: intl.formatMessage(messages.active), value: 'Active' }, - { label: intl.formatMessage(messages.inactive), value: 'Inactive' }, - ], - }, - ]} - tableId="users-list" - {...props} - /> + ); + applyPaginationToUrl(location, navigate, limit || 0, offset || 0); + usesMetaInURL && applyFiltersToUrl(location, navigate, { username, email, status }); + }} + emptyFilters={{ username: '', email: '', status: [] }} + setFilterValue={({ username, email, status }) => { + updateFilters({ + username: typeof username === 'undefined' ? filters.username : username, + email: typeof email === 'undefined' ? filters.email : email, + status: typeof status === 'undefined' || status === filters.status ? filters.status : status, + }); + }} + isLoading={isLoading} + pagination={pagination} + rowWrapper={UsersRow} + title={{ singular: intl.formatMessage(messages.user), plural: intl.formatMessage(messages.users).toLowerCase() }} + filters={[ + { + key: 'username', + value: typeof filters?.username === 'object' || typeof filters?.username === 'undefined' ? '' : filters.username, + placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.username).toLowerCase() }), + innerRef, + }, + { + key: 'email', + value: filters.email || '', + placeholder: intl.formatMessage(messages.filterByKey, { key: intl.formatMessage(messages.email).toLowerCase() }), + innerRef, + }, + { + key: 'status', + value: filters.status || [], + label: intl.formatMessage(messages.status), + type: 'checkbox', + items: [ + { label: intl.formatMessage(messages.active), value: 'Active' }, + { label: intl.formatMessage(messages.inactive), value: 'Inactive' }, + ], + }, + ]} + tableId="users-list" + {...props} + /> + + { + appNavigate(paths['users'].link); + if (isSubmit) { + fetchData({ ...pagination, filters, usesMetaInURL }); + } + }, + }} + /> + + ); }; diff --git a/src/smart-components/user/users.tsx b/src/smart-components/user/users.tsx index 694769a21..916d96a19 100644 --- a/src/smart-components/user/users.tsx +++ b/src/smart-components/user/users.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from 'react'; +import React, { useEffect, useContext, Suspense } from 'react'; import { Stack, StackItem } from '@patternfly/react-core'; import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'; import { useIntl } from 'react-intl'; @@ -17,6 +17,7 @@ const Users = () => { const activeUserPermissions = useContext(PermissionsContext); const { appNavClick } = useChrome(); const isITLess = useFlag('platform.rbac.itless'); + const isCommonAuthModel = useFlag('platform.rbac.common-auth-model'); const description = ; @@ -30,7 +31,7 @@ const Users = () => { isSelectable: !isITLess ? false : activeUserPermissions.userAccessAdministrator || activeUserPermissions.orgAdmin, isCompact: false, }, - usesMetaInURL: isITLess ? !location.pathname.includes(paths['invite-users'].link) : true, + usesMetaInURL: isITLess || isCommonAuthModel ? !location.pathname.includes(paths['invite-users'].link) : true, }; return ( diff --git a/src/test/__mocks__/@redhat-cloud-services/frontend-components/useChrome/index.js b/src/test/__mocks__/@redhat-cloud-services/frontend-components/useChrome/index.js index 669441c3c..2d1a42495 100644 --- a/src/test/__mocks__/@redhat-cloud-services/frontend-components/useChrome/index.js +++ b/src/test/__mocks__/@redhat-cloud-services/frontend-components/useChrome/index.js @@ -5,6 +5,8 @@ const useChrome = () => ({ isProd: () => true, getEnvironment: () => undefined, auth: { getUser: () => undefined }, + getBundle: () => 'iam', + getApp: () => 'user-access', }); module.exports = useChrome; diff --git a/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap b/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap index 9396dff12..13f5bd94e 100644 --- a/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap +++ b/src/test/presentional-components/shared/__snapshots__/toolbar.test.js.snap @@ -24,7 +24,9 @@ exports[` checkedRows is loading - false 1`] = ` - + +