From 61316d1e0810e5f83b46a09427795d8fd6464288 Mon Sep 17 00:00:00 2001 From: Yu Zhao Date: Thu, 1 Jun 2023 15:52:50 -0400 Subject: [PATCH 01/42] Modified the user page to add activate user or deactivate user feature --- src/Messages.js | 20 +++ src/helpers/user/user-helper.js | 50 +++++++- src/redux/action-types.js | 1 + src/redux/actions/user-actions.js | 31 +++++ .../group/add-group/users-list.js | 116 +++++++++++------- 5 files changed, 172 insertions(+), 46 deletions(-) diff --git a/src/Messages.js b/src/Messages.js index adf95b454..e085b6c6e 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -212,6 +212,26 @@ export default defineMessages({ description: 'Edit group error notification description', defaultMessage: 'The group was not updated successfuly.', }, + editUserSuccessTitle: { + id: 'editUserSuccessTitle', + description: 'Edit user success notification title', + defaultMessage: 'Success updating user', + }, + editUserSuccessDescription: { + id: 'editUserSuccessDescription', + description: 'Edit user success notification description', + defaultMessage: 'The user was updated successfully.', + }, + editUserErrorTitle: { + id: 'editUserErrorTitle', + description: 'Edit user error notification title', + defaultMessage: 'Failed updating user', + }, + editUserErrorDescription: { + id: 'editUserErrorDescription', + description: 'Edit user error notification description', + defaultMessage: 'The user was not updated successfuly.', + }, removeGroupSuccess: { id: 'removeGroupSuccess', description: 'Remove group success notification title', diff --git a/src/helpers/user/user-helper.js b/src/helpers/user/user-helper.js index 065ad9ed7..ed4c40d1b 100644 --- a/src/helpers/user/user-helper.js +++ b/src/helpers/user/user-helper.js @@ -8,6 +8,50 @@ const principalStatusApiMap = { Inactive: 'disabled', All: 'all', }; + +export const baseUrl = 'https://keycloak-user-service-fips-test.apps.fips-key.2vn8.p1.openshiftapps.com'; + +const fetchUsersApi = async (limit, offset, matchCriteria, username, sortOrder, email, mappedStatus) => { + let requestOpts = { + method: 'GET', + referrerPolicy: 'no-referrer', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + const result = await fetch(`${baseUrl}/users?offset=${offset}&limit=${limit}`, requestOpts) + .then((res) => res.json()) + .then((res) => { + return { data: res?.users, meta: res?.meta }; + }) + .catch((error) => { + return error; + }); + return result; +}; + +export async function updateUser(user) { + //TODO: this need to be replace with our api + // await principalApi.updateUser(user.uuid, user); + + let requestOpts = { + method: 'PUT', + referrerPolicy: 'no-referrer', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + try { + const response = await fetch(`${baseUrl}/user/${user.id}/activate/${user.enabled}`, requestOpts) + } catch(err) { + alert(err); + } + +} + export async function fetchUsers({ limit, offset = 0, orderBy, filters = {}, inModal, matchCriteria = 'partial' }) { const { username, email, status = [] } = filters; const sortOrder = orderBy === '-username' ? 'desc' : 'asc'; @@ -17,12 +61,14 @@ export async function fetchUsers({ limit, offset = 0, orderBy, filters = {}, inM : status.length === 2 ? principalStatusApiMap.All : principalStatusApiMap[status[0]] || principalStatusApiMap.All; - const response = await principalApi.listPrincipals(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); + // const response = await principalApi.listPrincipals(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); + const response = await fetchUsersApi(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); const isPaginationValid = isOffsetValid(offset, response?.meta?.count); offset = isPaginationValid ? offset : getLastPageOffset(response.meta.count, limit); const { data, meta } = isPaginationValid ? response - : await principalApi.listPrincipals(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); + : // : await principalApi.listPrincipals(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); + await fetchUsersApi(limit, offset, matchCriteria, username, sortOrder, email, mappedStatus); return { data, meta: { diff --git a/src/redux/action-types.js b/src/redux/action-types.js index c07b5bc0a..5ca6800c1 100644 --- a/src/redux/action-types.js +++ b/src/redux/action-types.js @@ -8,6 +8,7 @@ export const UPDATE_GROUP = 'UPDATE_GROUP'; export const REMOVE_GROUPS = 'REMOVE_GROUPS'; export const UPDATE_GROUPS_FILTERS = 'UPDATE_GROUPS_FILTERS'; +export const UPDATE_USER = 'UPDATE_USER' export const FETCH_USERS = 'FETCH_USERS'; export const UPDATE_USERS_FILTERS = 'UPDATE_USERS_FILTERS'; diff --git a/src/redux/actions/user-actions.js b/src/redux/actions/user-actions.js index b48724c72..20fe0290f 100644 --- a/src/redux/actions/user-actions.js +++ b/src/redux/actions/user-actions.js @@ -1,5 +1,36 @@ import * as ActionTypes from '../action-types'; import * as UserHelper from '../../helpers/user/user-helper'; +import { createIntl, createIntlCache } from 'react-intl'; +import messages from '../../Messages'; +import providerMessages from '../../locales/data.json'; +import { locale } from '../../AppEntry'; + +export const updateUser = (userData) => { + const cache = createIntlCache(); + const intl = createIntl({ locale, messages: providerMessages }, cache); + return { + type: ActionTypes.UPDATE_USER, + payload: UserHelper.updateUser(userData), + meta: { + notifications: { + fulfilled: { + variant: 'success', + title: intl.formatMessage(messages.editUserSuccessTitle), + dismissDelay: 8000, + dismissable: true, + description: intl.formatMessage(messages.editUserSuccessDescription), + }, + rejected: { + variant: 'danger', + title: intl.formatMessage(messages.editUserErrorTitle), + dismissDelay: 8000, + dismissable: true, + description: intl.formatMessage(messages.editUserErrorDescription), + }, + }, + }, + }; +}; export const fetchUsers = (apiProps) => ({ type: ActionTypes.FETCH_USERS, diff --git a/src/smart-components/group/add-group/users-list.js b/src/smart-components/group/add-group/users-list.js index 70e068112..104327297 100644 --- a/src/smart-components/group/add-group/users-list.js +++ b/src/smart-components/group/add-group/users-list.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; import { Link, useHistory } from 'react-router-dom'; import { mappedProps } from '../../../helpers/shared/helpers'; import { TableToolbarView } from '../../../presentational-components/shared/table-toolbar-view'; -import { fetchUsers, updateUsersFilters } from '../../../redux/actions/user-actions'; -import { Label } from '@patternfly/react-core'; +import { fetchUsers, updateUsersFilters, updateUser } from '../../../redux/actions/user-actions'; +import { Label, Switch } from '@patternfly/react-core'; import { sortable, nowrap } from '@patternfly/react-table'; import UsersRow from '../../../presentational-components/shared/UsersRow'; import { @@ -21,47 +21,6 @@ import { useIntl } from 'react-intl'; import messages from '../../../Messages'; import PermissionsContext from '../../../utilities/permissions-context'; -const createRows = (userLinks, data, checkedRows = [], intl) => - data - ? data.reduce( - (acc, { username, is_active: isActive, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin }) => [ - ...acc, - { - uuid: username, - cells: [ - isOrgAdmin ? ( - - - {intl.formatMessage(messages.yes)} - - ) : ( - - - {intl.formatMessage(messages.no)} - - ), - { title: userLinks ? {username.toString()} : username.toString() }, - email, - firstName, - lastName, - { - title: ( - - ), - props: { - 'data-is-active': isActive, - }, - }, - ], - selected: Boolean(checkedRows?.find?.(({ uuid }) => uuid === username)), - }, - ], - [] - ) - : []; - const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props }) => { const intl = useIntl(); const history = useHistory(); @@ -76,6 +35,75 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props redirected: !inModal && users.pagination.redirected, })); + const toggleUserActivationStatus = (checked, _event, user) => { + //TODO: Call api to toggle user activation status + const pagination = inModal ? defaultSettings : syncDefaultPaginationWithUrl(history, defaultPagination); + const newFilters = inModal ? { status: filters.status } : syncDefaultFiltersWithUrl(history, ['username', 'email', 'status'], filters); + const newUserData = { ...user, enabled: checked }; + dispatch(updateUser(newUserData)).then((res) => { + if (res?.action?.type === 'UPDATE_USER_FULFILLED') { + setFilters(newFilters); + fetchData({ ...mappedProps({ ...pagination, filters: newFilters }), inModal }); + } + }); + }; + + const createRows = (userLinks, data, checkedRows = []) => + data + ? data.reduce( + (acc, { id, username, enabled: enabled, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin }) => [ + ...acc, + { + uuid: username, + cells: [ + isOrgAdmin ? ( + + + {intl.formatMessage(messages.yes)} + + ) : ( + + + {intl.formatMessage(messages.no)} + + ), + { title: userLinks ? {username.toString()} : username.toString() }, + email, + firstName, + lastName, + { + title: ( + { + toggleUserActivationStatus(checked, _event, { + id, + username, + enabled: enabled, + email, + first_name: firstName, + last_name: lastName, + is_org_admin: isOrgAdmin, + }); + }} + /> + ), + props: { + 'data-is-active': enabled, + }, + }, + ], + selected: Boolean(checkedRows?.find?.(({ uuid }) => uuid === username)), + }, + ], + [] + ) + : []; + + // const users = useSelector(({ userReducer: { users } }) => users?.data?.map?.((data) => ({ ...data, uuid: data.username }))); const users = useSelector(({ userReducer: { users } }) => users?.data?.map?.((data) => ({ ...data, uuid: data.username }))); const pagination = useSelector( ({ @@ -108,7 +136,7 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props [dispatch] ); - const rows = createRows(userLinks, users, selectedUsers, intl); + const rows = createRows(userLinks, users, selectedUsers); const columns = [ { title: intl.formatMessage(messages.orgAdministrator), key: 'org-admin', transforms: [nowrap] }, { title: intl.formatMessage(messages.username), key: 'username', transforms: [sortable] }, From 52ebfdaf88dd449ecbb8b405cc2af068dd78bc92 Mon Sep 17 00:00:00 2001 From: Yu Zhao Date: Thu, 1 Jun 2023 17:51:25 -0400 Subject: [PATCH 02/42] use is_active to active or deactive user --- .gitignore | 4 ++++ src/helpers/user/user-helper.js | 2 +- src/smart-components/group/add-group/users-list.js | 10 +++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index a4c61dd04..7a3413475 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules # production dist +build #test coverage statistics coverage @@ -30,3 +31,6 @@ yarn.lock coverage # webpack cache .cache + +local-build.sh +frontend-build-history.sh diff --git a/src/helpers/user/user-helper.js b/src/helpers/user/user-helper.js index ed4c40d1b..167dac762 100644 --- a/src/helpers/user/user-helper.js +++ b/src/helpers/user/user-helper.js @@ -45,7 +45,7 @@ export async function updateUser(user) { }; try { - const response = await fetch(`${baseUrl}/user/${user.id}/activate/${user.enabled}`, requestOpts) + const response = await fetch(`${baseUrl}/user/${user.id}/activate/${user.is_active}`, requestOpts) } catch(err) { alert(err); } diff --git a/src/smart-components/group/add-group/users-list.js b/src/smart-components/group/add-group/users-list.js index 104327297..ce667b0dc 100644 --- a/src/smart-components/group/add-group/users-list.js +++ b/src/smart-components/group/add-group/users-list.js @@ -39,7 +39,7 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props //TODO: Call api to toggle user activation status const pagination = inModal ? defaultSettings : syncDefaultPaginationWithUrl(history, defaultPagination); const newFilters = inModal ? { status: filters.status } : syncDefaultFiltersWithUrl(history, ['username', 'email', 'status'], filters); - const newUserData = { ...user, enabled: checked }; + const newUserData = { ...user, is_active: checked }; dispatch(updateUser(newUserData)).then((res) => { if (res?.action?.type === 'UPDATE_USER_FULFILLED') { setFilters(newFilters); @@ -51,7 +51,7 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props const createRows = (userLinks, data, checkedRows = []) => data ? data.reduce( - (acc, { id, username, enabled: enabled, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin }) => [ + (acc, { id, username, is_active: is_active, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin }) => [ ...acc, { uuid: username, @@ -77,12 +77,12 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props key="status" label={intl.formatMessage(messages.active)} labelOff={intl.formatMessage(messages.inactive)} - isChecked={enabled} + isChecked={is_active} onChange={(checked, _event) => { toggleUserActivationStatus(checked, _event, { id, username, - enabled: enabled, + is_active: is_active, email, first_name: firstName, last_name: lastName, @@ -92,7 +92,7 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props /> ), props: { - 'data-is-active': enabled, + 'data-is-active': is_active, }, }, ], From d000e8d0e096f97458c4dfc9df13cd3aca475e85 Mon Sep 17 00:00:00 2001 From: Yu Zhao Date: Tue, 6 Jun 2023 14:19:55 -0400 Subject: [PATCH 03/42] Added the invite users modal without api call --- src/Messages.js | 45 +++++++ .../group/add-group/users-list.js | 45 ++++++- .../user/invite-users/invite-users.js | 122 ++++++++++++++++++ src/utilities/pathnames.js | 4 + 4 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 src/smart-components/user/invite-users/invite-users.js diff --git a/src/Messages.js b/src/Messages.js index e085b6c6e..872ce5d99 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -1,6 +1,51 @@ import { defineMessages } from 'react-intl'; export default defineMessages({ + inviteUsers: { + id: 'inviteUsers', + description: 'Invite users', + defaultMessage: 'Invite users', + }, + inviteUsersTitle: { + id: 'inviteUsersTitle', + description: 'Invite users modal title', + defaultMessage: 'Invite New Users', + }, + inviteUsersDescription: { + id: 'inviteUsersDescription', + description: 'Invite users modal description', + defaultMessage: 'Invite users to create a Red Hat login with your organization. Your e-mail address will be included in the invite as a point of contact.', + }, + inviteUsersFormIsAdminFieldTitle: { + id: 'inviteUsersFormIsAdminFieldTitle', + description: 'Invite users form is admin field title', + defaultMessage: 'Organization Administrators', + }, + inviteUsersFormIsAdminFieldDescription: { + id: 'inviteUsersFormIsAdminFieldDescription', + description: 'Invite users form is admin 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.', + }, + inviteUsersFormEmailsFieldTitle: { + id: 'inviteUsersFormEmailsFieldTitle', + description: 'Invite users form emails field title', + defaultMessage: 'Enter the e-mail addresses of the users you would like to invite', + }, + inviteUsersFormEmailsFieldDescription: { + id: 'inviteUsersFormEmailsFieldDescription', + description: 'Invite users form emails field description', + defaultMessage: 'Enter up to 50 email addresses separated by commans or returns.', + }, + inviteUsersCancelled: { + id: 'inviteUsersCancelled', + description: 'Invite users cancelled notification description', + defaultMessage: 'Invite users process was canceled by the user.', + }, + inviteUsersButton: { + id: 'inviteUsersButton', + description: 'Invite users button text', + defaultMessage: 'Invite new users', + }, notApplicable: { id: 'notApplicable', description: 'Not applicable text for resource definitions', diff --git a/src/smart-components/group/add-group/users-list.js b/src/smart-components/group/add-group/users-list.js index ce667b0dc..91ca7dc6d 100644 --- a/src/smart-components/group/add-group/users-list.js +++ b/src/smart-components/group/add-group/users-list.js @@ -1,11 +1,11 @@ import React, { useEffect, Fragment, useState, useContext, useRef, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; -import { Link, useHistory } from 'react-router-dom'; +import { Link, useHistory, Route, Switch } from 'react-router-dom'; import { mappedProps } from '../../../helpers/shared/helpers'; import { TableToolbarView } from '../../../presentational-components/shared/table-toolbar-view'; import { fetchUsers, updateUsersFilters, updateUser } from '../../../redux/actions/user-actions'; -import { Label, Switch } from '@patternfly/react-core'; +import { Button, Label, Switch as PF4Switch } from '@patternfly/react-core'; import { sortable, nowrap } from '@patternfly/react-table'; import UsersRow from '../../../presentational-components/shared/UsersRow'; import { @@ -20,12 +20,17 @@ import { CheckIcon, CloseIcon } from '@patternfly/react-icons'; import { useIntl } from 'react-intl'; import messages from '../../../Messages'; import PermissionsContext from '../../../utilities/permissions-context'; +import InviteUsers from '../../user/invite-users/invite-users'; +import { useScreenSize, isSmallScreen } from '@redhat-cloud-services/frontend-components/useScreenSize'; +import paths from '../../../utilities/pathnames'; const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props }) => { const intl = useIntl(); const history = useHistory(); const dispatch = useDispatch(); - const { orgAdmin } = useContext(PermissionsContext); + const [selectedRows, setSelectedRows] = useState([]); + const { orgAdmin, userAccessAdministrator } = useContext(PermissionsContext); + const screenSize = useScreenSize(); // use for text filter to focus const innerRef = useRef(null); const defaultPagination = useSelector(({ userReducer: { users } }) => ({ @@ -35,6 +40,35 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props redirected: !inModal && users.pagination.redirected, })); + const routes = () => ( + + + + + + ); + + const toolbarButtons = () => + orgAdmin || userAccessAdministrator + ? [ + + + , + ...(isSmallScreen(screenSize) + ? [ + { + label: intl.formatMessage(messages.inviteUsers), + onClick: () => { + history.push(paths['invite-users'].path); + }, + }, + ] + : []), + ] + : []; + const toggleUserActivationStatus = (checked, _event, user) => { //TODO: Call api to toggle user activation status const pagination = inModal ? defaultSettings : syncDefaultPaginationWithUrl(history, defaultPagination); @@ -73,8 +107,9 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, inModal, props lastName, { title: ( - { const orderBy = `${direction === 'desc' ? '-' : ''}${columns[index].key}`; diff --git a/src/smart-components/user/invite-users/invite-users.js b/src/smart-components/user/invite-users/invite-users.js new file mode 100644 index 000000000..72bb131c9 --- /dev/null +++ b/src/smart-components/user/invite-users/invite-users.js @@ -0,0 +1,122 @@ +import React, { useContext, useState, Fragment, useEffect } from 'react'; +import { useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; +import { Modal, Button, ModalVariant, ExpandableSection, Form, FormGroup, TextArea, Checkbox } from '@patternfly/react-core'; +import { useDispatch } from 'react-redux'; +import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/'; +import PermissionsContext from '../../../utilities/permissions-context'; +import { WarningModal } from '../../common/warningModal'; +import messages from '../../../Messages'; + +const InviteUsers = () => { + const dispatch = useDispatch(); + const intl = useIntl(); + const { push } = useHistory(); + + const [isCheckboxLabelExpanded, setIsCheckboxLabelExpanded] = useState(false); + const [areNewUsersAdmins, setAreNewUsersAdmins] = useState(false); + const [rawEmails, setRawEmails] = useState(''); + const [userEmailList, setUserEmailList] = useState([]); + const [cancelWarningVisible, setCancelWarningVisible] = useState(false); + const { orgAdmin, userAccessAdministrator } = useContext(PermissionsContext); + const isAdmin = orgAdmin || userAccessAdministrator; + + const onSubmit = () => { + //TODO: call api to invite users. + // push({ pathname: `/users` }); + console.log(userEmailList); + }; + + const onCancel = () => (userEmailList?.length > 0 && setCancelWarningVisible(true)) || redirectToUsers(); + + const onCheckboxLabelToggle = (isExpanded) => { + setIsCheckboxLabelExpanded(isExpanded); + }; + + const extractEmails = (rawEmails) => { + const regex = /([a-zA-Z0-9._+-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi; + let emails = rawEmails.match(regex) || []; + setUserEmailList(emails); + }; + + const handleRawEmailsChange = (value) => { + setRawEmails(value); + }; + + const redirectToUsers = () => { + dispatch( + addNotification({ + variant: 'warning', + title: intl.formatMessage(messages.inviteUsers), + dismissDelay: 8000, + description: intl.formatMessage(messages.inviteUsersCancelled), + }) + ); + push({ pathname: `/users` }); + }; + + useEffect(() => { + extractEmails(rawEmails); + }, [rawEmails]); + + return ( + + setCancelWarningVisible(false)} + onConfirmCancel={redirectToUsers} + /> + + {intl.formatMessage(messages.inviteUsersButton)} + , + , + ]} + > +
+ +