From 57bf68994ecddec67e7e9f98075ed53ea2e41370 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Wed, 8 Nov 2023 14:21:09 -0500 Subject: [PATCH] Add activate modal and user settings for stage users --- src/components/BulkSelectorUsersPrep.tsx | 3 + .../Form/PrincipalAliasMultiTextBox.tsx | 14 +- src/components/UserSettings.tsx | 13 +- .../UsersSections/UsersAccountSettings.tsx | 7 + src/components/modals/ActivateStageUsers.tsx | 252 ++++++++++++++++++ src/components/modals/AddUser.tsx | 34 ++- src/components/modals/DeleteUsers.tsx | 81 +++--- ...edUsersTable.tsx => UsersDisplayTable.tsx} | 26 +- src/components/tables/UsersTable.tsx | 38 +-- src/hooks/useUserSettingsData.tsx | 23 +- src/pages/ActiveUsers/ActiveUsers.tsx | 6 +- src/pages/ActiveUsers/ActiveUsersTabs.tsx | 4 +- .../PreservedUsers/PreservedUsersTabs.tsx | 4 +- src/pages/StageUsers/StageUsers.tsx | 225 ++++++++++++---- src/pages/StageUsers/StageUsersTabs.tsx | 6 +- src/services/rpc.ts | 151 +++++++++-- src/store/Identity/stageUsers-slice.ts | 3 +- 17 files changed, 714 insertions(+), 176 deletions(-) create mode 100644 src/components/modals/ActivateStageUsers.tsx rename src/components/tables/{DeletedUsersTable.tsx => UsersDisplayTable.tsx} (82%) diff --git a/src/components/BulkSelectorUsersPrep.tsx b/src/components/BulkSelectorUsersPrep.tsx index e38f0221..e084eb22 100644 --- a/src/components/BulkSelectorUsersPrep.tsx +++ b/src/components/BulkSelectorUsersPrep.tsx @@ -204,6 +204,9 @@ const BulkSelectorPrep = (props: PropsToBulkSelectorPrep) => { isSelecting = true, selectableUsersList: User[] ) => { + if (selectableUsersList.length === 0) { + return; + } props.usersData.changeSelectedUserNames( isSelecting ? selectableUsersList.map((r) => r.uid) : [] ); diff --git a/src/components/Form/PrincipalAliasMultiTextBox.tsx b/src/components/Form/PrincipalAliasMultiTextBox.tsx index be38d6ad..6fbd6a30 100644 --- a/src/components/Form/PrincipalAliasMultiTextBox.tsx +++ b/src/components/Form/PrincipalAliasMultiTextBox.tsx @@ -11,6 +11,8 @@ import { ErrorResult, useAddPrincipalAliasMutation, useRemovePrincipalAliasMutation, + useAddStagePrincipalAliasMutation, + useRemoveStagePrincipalAliasMutation, } from "src/services/rpc"; // Layouts import SecondaryButton from "../layouts/SecondaryButton"; @@ -25,6 +27,7 @@ interface PrincipalAliasMultiTextBoxProps { ipaObject: Record; metadata: Metadata; onRefresh: () => void; + from: "active-users" | "stage-users" | "preserved-users"; } const PrincipalAliasMultiTextBox = (props: PrincipalAliasMultiTextBoxProps) => { @@ -32,9 +35,14 @@ const PrincipalAliasMultiTextBox = (props: PrincipalAliasMultiTextBoxProps) => { const alerts = useAlerts(); // RTK hooks - const [addPrincipalAlias] = useAddPrincipalAliasMutation(); - const [removePrincipalAlias] = useRemovePrincipalAliasMutation(); - + let [addPrincipalAlias] = useAddPrincipalAliasMutation(); + if (props.from === "stage-users") { + [addPrincipalAlias] = useAddStagePrincipalAliasMutation(); + } + let [removePrincipalAlias] = useRemovePrincipalAliasMutation(); + if (props.from === "stage-users") { + [removePrincipalAlias] = useRemoveStagePrincipalAliasMutation(); + } // 'krbprincipalname' value from ipaObject const krbprincipalname = props.ipaObject["krbprincipalname"] as string[]; diff --git a/src/components/UserSettings.tsx b/src/components/UserSettings.tsx index 5068cd3f..b5ffc1f1 100644 --- a/src/components/UserSettings.tsx +++ b/src/components/UserSettings.tsx @@ -38,7 +38,11 @@ import UsersMailingAddress from "src/components/UsersSections/UsersMailingAddres import UsersEmployeeInfo from "src/components/UsersSections/UsersEmployeeInfo"; import UsersAttributesSMB from "src/components/UsersSections/UsersAttributesSMB"; // RPC -import { ErrorResult, useSaveUserMutation } from "src/services/rpc"; +import { + ErrorResult, + useSaveUserMutation, + useSaveStageUserMutation, +} from "src/services/rpc"; // Hooks import useAlerts from "src/hooks/useAlerts"; @@ -67,8 +71,11 @@ const UserSettings = (props: PropsToUserSettings) => { // Alerts to show in the UI const alerts = useAlerts(); - // RTK hook: save user - const [saveUser] = useSaveUserMutation(); + // RTK hook: save user (acive/preserved and stage) + let [saveUser] = useSaveUserMutation(); + if (props.from === "stage-users") { + [saveUser] = useSaveStageUserMutation(); + } // Kebab const [isKebabOpen, setIsKebabOpen] = useState(false); diff --git a/src/components/UsersSections/UsersAccountSettings.tsx b/src/components/UsersSections/UsersAccountSettings.tsx index 012d21f6..c602fc4a 100644 --- a/src/components/UsersSections/UsersAccountSettings.tsx +++ b/src/components/UsersSections/UsersAccountSettings.tsx @@ -43,6 +43,12 @@ interface PropsToUsersAccountSettings { from: "active-users" | "stage-users" | "preserved-users"; } +// Generic data to pass to the Textbox adder +interface ElementData { + id: string | number; + element: string; +} + const UsersAccountSettings = (props: PropsToUsersAccountSettings) => { // TODO: Handle the `has_password` variable (boolean) by another Ipa component const [password] = useState(""); @@ -185,6 +191,7 @@ const UsersAccountSettings = (props: PropsToUsersAccountSettings) => { ipaObject={ipaObject} metadata={props.metadata} onRefresh={props.onRefresh} + from={props.from} /> void; +} + +export interface PropsToActivateUsers { + show: boolean; + handleModalToggle: () => void; + selectedUsersData: SelectedUsersData; + onRefresh?: () => void; + onOpenDeleteModal?: () => void; + onCloseDeleteModal?: () => void; +} + +const ActivateStageUsers = (props: PropsToActivateUsers) => { + // Set dispatch (Redux) + const dispatch = useAppDispatch(); + + // Alerts + const alerts = useAlerts(); + + // Define 'executeUserStageCommand' to activate user data to IPA server + const [executeUserActivateCommand] = useBatchMutCommandMutation(); + + const [noMembersChecked, setNoMembers] = useState(false); + + // List of fields + const fields = [ + { + id: "question-text", + pfComponent: ( + + + Are you sure you want to activate the selected stage users? + + + ), + }, + { + id: "activate-users-table", + pfComponent: ( + + ), + }, + { + id: "no-members", + pfComponent: ( + { + setNoMembers(!noMembersChecked); + }} + id="no-members-checkbox" + name="no-members" + /> + ), + }, + ]; + + // Close modal + const closeModal = () => { + props.handleModalToggle(); + }; + + // Handle API error data + const [isModalErrorOpen, setIsModalErrorOpen] = useState(false); + const [errorTitle, setErrorTitle] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const closeAndCleanErrorParameters = () => { + setIsModalErrorOpen(false); + setErrorTitle(""); + setErrorMessage(""); + }; + + const onCloseErrorModal = () => { + closeAndCleanErrorParameters(); + }; + + const errorModalActions = [ + , + ]; + + const handleAPIError = (error: FetchBaseQueryError | SerializedError) => { + if ("code" in error) { + setErrorTitle("IPA error " + error.code + ": " + error.name); + if (error.message !== undefined) { + setErrorMessage(error.message); + } + } else if ("data" in error) { + const errorData = error.data as ErrorData; + const errorCode = errorData.code as string; + const errorName = errorData.name as string; + const errorMessage = errorData.error as string; + + setErrorTitle("IPA error " + errorCode + ": " + errorName); + setErrorMessage(errorMessage); + } + setIsModalErrorOpen(true); + }; + + // Stage user + const activateUsers = () => { + // Prepare users params + const uidsToActivatePayload: Command[] = []; + + props.selectedUsersData.selectedUsers.map((uid) => { + const payloadItem = { + method: "stageuser_activate", + params: [uid, { no_members: noMembersChecked }], + } as Command; + uidsToActivatePayload.push(payloadItem); + }); + + // [API call] activate elements + executeUserActivateCommand(uidsToActivatePayload).then((response) => { + if ("data" in response) { + const data = response.data as BatchRPCResponse; + const result = data.result; + const error = data.error as FetchBaseQueryError | SerializedError; + + if (result) { + if ("error" in result.results[0] && result.results[0].error) { + const errorData = { + code: result.results[0].error_code, + name: result.results[0].error_name, + error: result.results[0].error, + } as ErrorData; + + const error = { + status: "CUSTOM_ERROR", + data: errorData, + } as FetchBaseQueryError; + + // Handle error + handleAPIError(error); + } else { + // Update data from Redux + props.selectedUsersData.selectedUsers.map((user) => { + dispatch(removeStageUser(user[0])); + }); + + // Reset selected values + props.selectedUsersData.updateSelectedUsers([]); + + // Refresh data + if (props.onRefresh !== undefined) { + props.onRefresh(); + } + + // Show alert: success + alerts.addAlert( + "activate-users-success", + "Users activated", + "success" + ); + + closeModal(); + } + } else if (error) { + // Handle error + handleAPIError(error); + } + } + }); + }; + + // Set the Modal and Action buttons for 'Stage' option + const modalStageActions: JSX.Element[] = [ + , + , + ]; + + const modalActivate: JSX.Element = ( + + ); + + // Render 'ActivateStageUsers' + return ( + <> + + {modalActivate} + {isModalErrorOpen && ( + + )} + + ); +}; + +export default ActivateStageUsers; diff --git a/src/components/modals/AddUser.tsx b/src/components/modals/AddUser.tsx index ae85ad46..7cc128c7 100644 --- a/src/components/modals/AddUser.tsx +++ b/src/components/modals/AddUser.tsx @@ -357,7 +357,7 @@ const AddUser = (props: PropsToAddUser) => { const [verifyPasswordHidden, setVerifyPasswordHidden] = React.useState(true); // List of fields - const fields = [ + let fields = [ { id: "user-login", name: "User login", @@ -452,7 +452,12 @@ const AddUser = (props: PropsToAddUser) => { ref={userClassRef} /> ), - labelIcon: , + labelIcon: + props.from !== "stage-users" ? ( + + ) : ( +
+ ), }, { id: "no-private-group", @@ -528,6 +533,14 @@ const AddUser = (props: PropsToAddUser) => { }, ]; + // For stage users we need to clean up the fields + if (props.from === "stage-users") { + const new_fields = fields.filter( + (el) => el.id !== "no-private-group" && el.id !== "gid-form" + ); + fields = new_fields; + } + // Helper method to reset validation values const resetValidations = () => { resetUserLoginError(); @@ -578,20 +591,27 @@ const AddUser = (props: PropsToAddUser) => { const newUserData = { givenname: firstName, - noprivate: isNoPrivateGroupChecked, sn: lastName, userclass: userClass !== "" ? userClass : undefined, - gidnumber: gidSelected, userpassword: newPassword, version: apiVersion, }; + // Define payload data + let method = "user_add"; + if (props.from === "stage-users") { + method = "stageuser_add"; + } else { + // Non-stage users use noprivate + newUserData["noprivate"] = isNoPrivateGroupChecked; + // Add gidNumber for non-stage users + newUserData["gidnumber"] = gidSelected; + } // Prepare the command data const newUserCommandData = [usLogin, newUserData]; - // Define payload data const newUserPayload: Command = { - method: "user_add", + method: method, params: newUserCommandData, }; @@ -770,7 +790,7 @@ const AddUser = (props: PropsToAddUser) => { variantType="small" modalPosition="top" offPosition="76px" - title="Add user" + title={props.from === "stage-users" ? "Add stage user" : "Add user"} formId="users-add-user-modal" fields={fields} show={props.show} diff --git a/src/components/modals/DeleteUsers.tsx b/src/components/modals/DeleteUsers.tsx index b4883cb1..7f6e0c91 100644 --- a/src/components/modals/DeleteUsers.tsx +++ b/src/components/modals/DeleteUsers.tsx @@ -10,7 +10,7 @@ import { // Layouts import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; // Tables -import DeletedUsersTable from "src/components/tables/DeletedUsersTable"; +import UsersDisplayTable from "src/components/tables/UsersDisplayTable"; // Redux import { useAppDispatch } from "src/store/hooks"; import { removeUser as removeActiveUser } from "src/store/Identity/activeUsers-slice"; @@ -103,8 +103,8 @@ const DeleteUsers = (props: PropsToDeleteUsers) => { { id: "deleted-users-table", pfComponent: ( - ), @@ -194,14 +194,20 @@ const DeleteUsers = (props: PropsToDeleteUsers) => { }; // Delete user - const deleteUsersNew = (uidsToDelete: string[]) => { + const deleteUsers = (uidsToDelete: string[]) => { // Prepare users params const uidsToDeletePayload: Command[] = []; - - const deletionParams = { preserve: !isDeleteChecked }; + let deletionParams = {}; + if (props.from !== "stage-users") { + deletionParams = { preserve: !isDeleteChecked }; + } uidsToDelete.map((uid) => { + let method = "user_del"; + if (props.from === "stage-users") { + method = "stageuser_del"; + } const payloadItem = { - method: "user_del", + method: method, params: [[uid], deletionParams], } as Command; uidsToDeletePayload.push(payloadItem); @@ -275,38 +281,14 @@ const DeleteUsers = (props: PropsToDeleteUsers) => { }); }; - // Delete users (for components not adapted to communication layer) - // NOTE: This function will dissapear when all user types will be adapted to C.L. - const deleteUsers = () => { - deleteUsersFromRedux(); - - props.selectedUsersData.updateSelectedUsers([]); - if ( - props.from === "active-users" && - props.buttonsData.updateIsDeleteButtonDisabled !== undefined - ) { - props.buttonsData.updateIsDeleteButtonDisabled(true); - } - props.buttonsData.updateIsDeletion(true); - closeModal(); - }; - - // [Temporal solution] Defines which function is used to delete users - const setDeleteFunction = () => { - if (props.from === "active-users") { - return deleteUsersNew(props.selectedUsersData.selectedUsers); - } else { - // 'stage' and 'preserved users' - return deleteUsers(); - } - }; - // Set the Modal and Action buttons for 'Delete' option const modalActionsDelete: JSX.Element[] = [ , ]; + let title = "Remove Active Users"; + if (props.from === "stage-users") { + title = "Remove Stage Users"; + // Drop last field (radio buttons with option to perserve) + fields.splice(-1); + } else if (props.from === "preserved-users") { + title = "Remove Preserved Users"; + // Drop last field (radio buttons with option to perserve) + fields.splice(-1); + } + const modalDelete: JSX.Element = ( { /> ); - // Preserve users - // TODO: Remove this to adapt the general solution - // to all user pages when the C.L. is fully implemented - const setPreserveFunction = () => { - if (props.from === "active-users") { - return deleteUsersNew(props.selectedUsersData.selectedUsers); - } else { - // User pages not adapted to communication layer - return alert("This functionality will be provided soon!"); - } - }; - // Set the Modal and Action buttons for 'Preserve' option const modalActionsPreserve: JSX.Element[] = [