/** * Copyright (c) 2021-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AlertLevels, TestableComponentInterface } from "@thiva/core/models"; import { addAlert } from "@thiva/core/store"; import { useTrigger } from "@thiva/forms"; import { GenericIconProps, Heading, LinkButton, PrimaryButton, Steps, useWizardAlert } from "@thiva/react-components"; import { AxiosError, AxiosResponse } from "axios"; import intersection from "lodash-es/intersection"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { Dispatch } from "redux"; import { Button, Grid, Icon, Modal } from "semantic-ui-react"; import { AddGroupUsers } from "./group-assign-users"; import { AppConstants, history } from "@thiva/admin.core.v1"; import { EventPublisher } from "@thiva/admin.core.v1/utils"; import { createGroup } from "@thiva/admin.groups.v1/api"; import { CreateGroupSummary } from "@thiva/admin.groups.v1/components/wizard"; import { getGroupsWizardStepIcons } from "@thiva/admin.groups.v1/configs"; import { CreateGroupInterface, CreateGroupMemberInterface } from "@thiva/admin.groups.v1/models"; import { updateRole } from "@thiva/admin.roles.v2/api"; import { PatchRoleDataInterface } from "@thiva/admin.roles.v2/models"; import { CONSUMER_USERSTORE, PRIMARY_USERSTORE } from "@thiva/admin.userstores.v1/constants"; import { commonConfig } from "../../../configs"; /** * Interface which captures create group props. */ interface CreateGroupProps extends TestableComponentInterface { closeWizard: () => void; updateList: () => void; initStep?: number; requiredSteps?: WizardStepsFormTypes[] | string[]; showStepper?: boolean; submitStep?: WizardStepsFormTypes | string; } /** * Enum for wizard steps form types. * @readonly */ enum WizardStepsFormTypes { BASIC_DETAILS = "BasicDetails", SUMMARY = "summary" } /** * Interface to capture current wizard state */ interface WizardStateInterface { [ key: string ]: any; } /** * Interface for wizard step. */ interface WizardStepInterface { content: ReactElement; icon: GenericIconProps | any; name: string; title: string; } /** * Component to handle addition of a new group to the system. * * @param props - props related to the create group wizard Member */ export const CreateGroupWizard: FunctionComponent = (props: CreateGroupProps): ReactElement => { const { closeWizard, initStep, requiredSteps, showStepper, submitStep, [ "data-testid" ]: testId } = props; const { t } = useTranslation(); const dispatch: Dispatch = useDispatch(); const [ currentStep, setCurrentWizardStep ] = useState(initStep); const [ partiallyCompletedStep, setPartiallyCompletedStep ] = useState(undefined); const [ wizardState, setWizardState ] = useState(undefined); const [ wizardSteps, setWizardSteps ] = useState(undefined); const [ selectedUserStore, setSelectedUserStore ] = useState( commonConfig?.primaryUserstoreOnly ? PRIMARY_USERSTORE : CONSUMER_USERSTORE); const [ isSubmitting, setIsSubmitting ] = useState(false); const [ submitGeneralSettings, setSubmitGeneralSettings ] = useTrigger(); const [ finishSubmit, setFinishSubmit ] = useTrigger(); const [ isEnded, setEnded ] = useState(false); const [ isWizardActionDisabled, setIsWizardActionDisabled ] = useState(true); const [ alert, setAlert, alertComponent ] = useWizardAlert(); const eventPublisher: EventPublisher = EventPublisher.getInstance(); useEffect(() => { setWizardSteps(filterSteps([ WizardStepsFormTypes.BASIC_DETAILS, WizardStepsFormTypes.SUMMARY ])); }, []); /** * Sets the current wizard step to the previous on every `partiallyCompletedStep` * value change , and resets the partially completed step value. */ useEffect(() => { if (partiallyCompletedStep === undefined) { return; } setCurrentWizardStep(currentStep - 1); setPartiallyCompletedStep(undefined); }, [ partiallyCompletedStep ]); useEffect(() => { if(!isEnded) { return; } if (wizardState && wizardState[ WizardStepsFormTypes.BASIC_DETAILS ]) { addGroup(wizardState[ WizardStepsFormTypes.BASIC_DETAILS ]); } }, [ wizardState && wizardState[ WizardStepsFormTypes.BASIC_DETAILS ] ]); /** * Method to handle create group action when create group wizard finish action is triggered. * * @param groupDetails - basic data required to create group. */ const addGroup = (groupDetails: any): void => { let groupName: string = ""; groupDetails?.domain !== "primary" ? groupName = groupDetails?.BasicDetails.basic ? groupDetails?.BasicDetails?.basic?.domain + "/" + groupDetails?.BasicDetails?.basic?.groupName : groupDetails?.domain + "/" + groupDetails?.groupName : groupName = groupDetails?.BasicDetails?.basic ? groupDetails?.BasicDetails?.groupName : groupDetails?.groupName; const members: CreateGroupMemberInterface[] = []; const users: any = groupDetails?.BasicDetails?.users; if (users?.length > 0) { users?.forEach((user: any) => { members?.push({ display: user.userName, value: user.id }); }); } const groupData: CreateGroupInterface = { "displayName": groupName, "members" : members, "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:Group" ] }; setIsSubmitting(true); /** * Create Group API Call. */ createGroup(groupData).then((response: AxiosResponse) => { if (response.status === 201) { const createdGroup: any = response.data; const rolesList: string[] = []; if (groupDetails?.RoleList?.roles) { groupDetails?.RoleList?.roles.forEach((role: any) => { rolesList?.push(role.id); }); } const roleData: PatchRoleDataInterface = { "Operations": [ { "op": "add", "value": { "groups": [ { "display": createdGroup.displayName, "value": createdGroup.id } ] } } ], "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] }; if (rolesList && rolesList.length > 0) { for (const roleId of rolesList) { updateRole(roleId, roleData) .catch((error: AxiosError) => { if (!error.response || error.response.status === 401) { setAlert({ description: t("console:manage.features.groups.notifications." + "createPermission." + "error.description"), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createPermission." + "error.message") }); } else if (error.response && error.response.data.detail) { setAlert({ description: t("console:manage.features.groups.notifications." + "createPermission." + "error.description", { description: error.response.data.detail }), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createPermission." + "error.message") }); } else { setAlert({ description: t("console:manage.features.groups.notifications." + "createPermission." + "genericError.description"), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createPermission." + "genericError." + "message") }); } }); } } dispatch( addAlert({ description: t("console:manage.features.groups.notifications.createGroup.success." + "description"), level: AlertLevels.SUCCESS, message: t("console:manage.features.groups.notifications.createGroup.success." + "message") }) ); } closeWizard(); history.push(AppConstants.getPaths().get("GROUP_EDIT").replace(":id", response.data.id)); }).catch((error: AxiosError) => { if (!error.response || error.response.status === 401) { closeWizard(); dispatch( addAlert({ description: t("console:manage.features.groups.notifications.createGroup.error.description"), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createGroup.error.message") }) ); } else if (error.response && error.response.data.detail) { closeWizard(); dispatch( addAlert({ description: t("console:manage.features.groups.notifications.createGroup.error.description", { description: error.response.data.detail }), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createGroup.error.message") }) ); } else { closeWizard(); dispatch(addAlert({ description: t("console:manage.features.groups.notifications.createGroup.genericError.description"), level: AlertLevels.ERROR, message: t("console:manage.features.groups.notifications.createGroup.genericError.message") })); } }).finally(() => { setIsSubmitting(false); }) ; }; /** * Method to handle the create group wizard finish action. * */ const handleGroupWizardFinish = (values: any) => { eventPublisher.publish("manage-groups-finish-create-new-group"); addGroup(values); }; /** * Generates a summary of the wizard. * */ const generateWizardSummary = () => { if (!wizardState) { return; } return wizardState; }; /** * Handles wizard step submit. * * @param values - Forms values to be stored in state. * @param formType - Type of the form. */ const handleWizardSubmit = (values: any, formType: WizardStepsFormTypes) => { if (WizardStepsFormTypes.BASIC_DETAILS === formType) { setSelectedUserStore(values.basic.domain); } if (submitStep !== formType && !isEnded) { setCurrentWizardStep(currentStep + 1); } setWizardState({ ...wizardState, [ formType ]: values }); // If the submit step is not default, and submit step is the current step, submit the form. if (submitStep !== WizardStepsFormTypes.SUMMARY && submitStep === formType) { handleGroupWizardFinish({ ...wizardState, [ formType ]: values }); return; } }; /** * Function to change the current wizard step to next. */ const changeStepToNext = (): void => { switch(currentStep) { case 0: setSubmitGeneralSettings(); break; case 1: setFinishSubmit(); break; } }; const navigateToPrevious = () => { setPartiallyCompletedStep(currentStep); }; const handleFinishFlow = () => { setEnded(true); setSubmitGeneralSettings(); }; /** * Filters the steps evaluating the requested steps. * */ const filterSteps = (steps: WizardStepsFormTypes[]): WizardStepInterface[] => { const getStepContent = (stepsToFilter: WizardStepsFormTypes[] | string[]) => { const filteredSteps: any[] = []; stepsToFilter.forEach((step: WizardStepsFormTypes) => { if (step === WizardStepsFormTypes.BASIC_DETAILS) { filteredSteps.push(getBasicDetailsWizardStep()); } else if (step === WizardStepsFormTypes.SUMMARY) { filteredSteps.push(getSummaryWizardStep()); } }); return filteredSteps; }; if (!requiredSteps) { return getStepContent(steps); } return getStepContent(intersection(steps, requiredSteps)); }; /** * Get the wizard basic step. * */ const getBasicDetailsWizardStep = (): WizardStepInterface => { return { content: ( <> handleWizardSubmit(values, WizardStepsFormTypes.BASIC_DETAILS) } onUserFetchRequestFinish={ () => setIsWizardActionDisabled(false) } /> ), icon: getGroupsWizardStepIcons().general, name: WizardStepsFormTypes.BASIC_DETAILS, title: t("roles:addRoleWizard.wizardSteps.0") }; }; /** * Get the summary wizard step. * */ const getSummaryWizardStep = (): WizardStepInterface => { return { content: ( handleGroupWizardFinish(wizardState) } summary={ generateWizardSummary() } /> ), icon: getGroupsWizardStepIcons().summary, name: WizardStepsFormTypes.SUMMARY, title: t("roles:addRoleWizard.wizardSteps.3") }; }; /** * Resolves the step content. * * @returns step content. */ const resolveStepContent = (): ReactElement => { if (!wizardSteps) { return null; } switch (wizardSteps[currentStep]?.name) { case WizardStepsFormTypes.BASIC_DETAILS: return getBasicDetailsWizardStep()?.content; case WizardStepsFormTypes.SUMMARY: return getSummaryWizardStep()?.content; } }; return ( { t("roles:addRoleWizard.heading", { type: "Group" }) } { wizardState && wizardState[ WizardStepsFormTypes.BASIC_DETAILS ]?.groupName ? " - " + wizardState[ WizardStepsFormTypes.BASIC_DETAILS ]?.groupName :"" } Create new group and add users to the group. { showStepper && ( { wizardSteps?.map((step: WizardStepInterface, index: number) => ( )) } ) } { alert && alertComponent } { resolveStepContent() } closeWizard() } data-testid={ `${ testId }-cancel-button` } > { t("common:cancel") } { currentStep < wizardSteps?.length - 1 && ( { t("roles:addRoleWizard.buttons.next") } ) } { (wizardSteps?.length > 1 && currentStep === 0) && ( ) } { currentStep === wizardSteps?.length - 1 && ( { t("roles:addRoleWizard.buttons.finish") } ) } { (wizardSteps?.length > 1 && currentStep > 0) && ( { t("roles:addRoleWizard.buttons.previous") } ) } ); }; /** * Default props for Create group wizard component. * NOTE : Current step is set to 0 in order to start from * beginning of the wizard. */ CreateGroupWizard.defaultProps = { initStep: 0, requiredSteps: [ WizardStepsFormTypes.BASIC_DETAILS, WizardStepsFormTypes.SUMMARY ], showStepper: false, submitStep: WizardStepsFormTypes.SUMMARY };