From e30833e901ebed215da524bd0def73f79b02f9d5 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 11 Oct 2024 07:52:14 +0530 Subject: [PATCH 1/8] Bug fixes related to conosle settings in primary organizations for managed deployments --- .../add-admin-user-basic.tsx | 513 ++++++++++ .../add-external-admin-wizard.tsx | 894 ++++++++++++++++++ .../administrators-list.tsx | 2 +- .../console-administrators.tsx | 4 +- .../console-roles-edit.scss | 5 + .../console-roles-edit/console-roles-edit.tsx | 88 +- .../components/edit-role/edit-role-users.tsx | 74 +- features/admin.roles.v2/models/roles.ts | 7 + 8 files changed, 1523 insertions(+), 64 deletions(-) create mode 100644 features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx create mode 100644 features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx diff --git a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx new file mode 100644 index 00000000000..e09dad53f60 --- /dev/null +++ b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx @@ -0,0 +1,513 @@ +/** + * Copyright (c) 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 { FeatureAccessConfigInterface } from "@wso2is/access-control"; +import { InternalAdminFormDataInterface } from "@wso2is/admin.administrators.v1/models"; +import { isAdminUser, isCollaboratorUser } from "@wso2is/admin.administrators.v1/utils/administrators"; +import { useApplicationList } from "@wso2is/admin.applications.v1/api/application"; +import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; +import { + AppState, + SharedUserStoreUtils, + UIConstants, + UserBasicInterface, + getUsersList +} from "@wso2is/admin.core.v1"; +import { EventPublisher } from "@wso2is/admin.core.v1/utils"; +import { administratorConfig } from "@wso2is/admin.extensions.v1/configs/administrator"; +import { SCIMConfigs } from "@wso2is/admin.extensions.v1/configs/scim"; +import { getRolesList } from "@wso2is/admin.roles.v2/api"; +import { AdminAccountTypes } from "@wso2is/admin.users.v1/constants/user-management-constants"; +import { UserInviteInterface, UserListInterface } from "@wso2is/admin.users.v1/models/user"; +import { UserManagementUtils } from "@wso2is/admin.users.v1/utils"; +import { getUserNameWithoutDomain } from "@wso2is/core/helpers"; +import { IdentifiableComponentInterface, RolesInterface } from "@wso2is/core/models"; +import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; +import { + ContentLoader, + DocumentationLink, + Heading, + Hint, + TransferComponent, + TransferList, + TransferListItem, + useDocumentation +} from "@wso2is/react-components"; +import { FormValidation } from "@wso2is/validation"; +import { AxiosResponse } from "axios"; +import debounce, { DebouncedFunc } from "lodash-es/debounce"; +import isEmpty from "lodash-es/isEmpty"; +import kebabCase from "lodash-es/kebabCase"; +import React, { FormEvent, ReactElement, useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { Divider, Dropdown, DropdownDivider, DropdownItem, DropdownMenu, DropdownProps, Grid, Header } from "semantic-ui-react"; + +/** + * Proptypes for the add admin user basic component. + */ +interface AddAdminUserBasicProps extends IdentifiableComponentInterface { + triggerSubmit: boolean; + onSubmit: (values: any) => void; + administratorType: string; + setFinishButtonDisabled: (values: boolean) => void; +} + +/** + * Add admin user basic component. + * + * @returns add admin modal component. + */ +export const AddAdminUserBasic: React.FunctionComponent = ( + props: AddAdminUserBasicProps): ReactElement => { + + const { + administratorType, + triggerSubmit, + onSubmit, + setFinishButtonDisabled, + [ "data-componentid"]: componentId + } = props; + + const [ userRoleOptions, setUserRoleList ] = useState([]); + const [ rolesList, setRolesList ] = useState([]); + const [ usersList, setUsersList ] = useState([]); + const [ checkedAssignedListItems, setCheckedAssignedListItems ] = useState([]); + const [ isUserListRequestLoading, setUserListRequestLoading ] = useState(false); + const [ isUserRoleOptionsRequestLoading, setUserRoleOptionsRequestLoading ] = useState(false); + const [ listItemLimit, setListItemLimit ] = useState(0); + const [ userListMetaContent, setUserListMetaContent ] = useState(undefined); + const [ searchQuery, setSearchQuery ] = useState(""); + const [ listOffset ] = useState(0); + + const { t } = useTranslation(); + const { getLink } = useDocumentation(); + + const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); + const isInvitedAdminInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( + "consoleSettings.invitedExternalAdmins" + ); + + /** + * Retrieve the application data for the console application, filtering by name. + */ + const { data: applicationListData } = useApplicationList( + null, + null, + null, + `name eq ${ApplicationManagementConstants.CONSOLE_APP_NAME}` + ); + + /** + * Build the roles filter to search for roles specific to the console application. + */ + const roleSearchFilter: string = useMemo(() => { + if (applicationListData?.applications && applicationListData?.applications?.length > 0) { + return `audience.value eq ${applicationListData?.applications[0]?.id}`; + } + + return null; + }, [ applicationListData ]); + + // Username input validation error messages. + const USERNAME_REGEX_VIOLATION_ERROR_MESSAGE: string = t("users:guestUsers.fields." + + "username.validations.regExViolation"); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + useEffect(() => { + if (!roleSearchFilter) { + return; + } + + // Fetch users to select as internal admins. + if (administratorType === AdminAccountTypes.INTERNAL) { + setListItemLimit(UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT); + setUserListMetaContent(new Map([ + [ "name", "name" ], + [ "emails", "emails" ], + [ "roles", "roles" ], + [ "userName", "userName" ], + [ "id", "" ], + [ "profileUrl", "profileUrl" ], + [ "meta.lastModified", "meta.lastModified" ], + [ "meta.created", "" ] + ])); + } + + const roleOptions: DropdownProps[] = []; + + if (userRoleOptions.length === 0) { + setUserRoleOptionsRequestLoading(true); + getRolesList(null, roleSearchFilter) + .then((response: AxiosResponse) => { + setRolesList(response.data.Resources); + response.data.Resources.map((role: RolesInterface, index: number) => { + if(role.displayName === "system" || + role.displayName === "everyone" || + role.displayName === "selfsignup" + ) { + return; + } + + if ( + role.displayName?.split("/")?.length < 2 && + role.displayName?.split("/")[0] === "Application" + ) { + return; + } + + if (isInvitedAdminInConsoleSettingsEnabled || + ( + !isInvitedAdminInConsoleSettingsEnabled && role.meta?.systemRole + ) + + ){ + roleOptions?.push({ + key: index, + text: role?.displayName, + value: role?.displayName + }); + } + }); + setUserRoleList(roleOptions); + }).finally(() => { + setUserRoleOptionsRequestLoading(false); + }); + } + }, [ roleSearchFilter ]); + + useEffect(() => { + if (userListMetaContent) { + const attributes: string = generateAttributesString(userListMetaContent.values()); + + if (isEmpty(searchQuery)) { + setUsersList([]); + + return; + } + + getUserList(listItemLimit, listOffset, searchQuery, attributes, null); + } + }, [ listOffset, listItemLimit, searchQuery ]); + + useEffect(() => { + if (administratorType === AdminAccountTypes.INTERNAL) { + setFinishButtonDisabled(isEmpty(checkedAssignedListItems)); + } + }, [ checkedAssignedListItems ]); + + /** + * The following method accepts a Map and returns the values as a string. + * + * @param attributeMap - IterableIterator + * @returns attribute string + */ + const generateAttributesString = (attributeMap: IterableIterator) => { + const attArray: string[] = []; + const iterator1: IterableIterator = attributeMap[ Symbol.iterator ](); + + for (const attribute of iterator1) { + if (attribute !== "") { + attArray.push(attribute); + } + } + + return attArray.toString(); + }; + + const getUserList = (limit: number, offset: number, filter: string, attribute: string, userStore: string) => { + + setUserListRequestLoading(true); + + getUsersList(limit, offset, filter, attribute, userStore) + .then((response: UserListInterface) => { + // Exclude JIT users, internal admin users and collaborators. + const responseUsers: UserBasicInterface[] = response?.Resources?.filter( + (user: UserBasicInterface) => + !user[ SCIMConfigs.scim.enterpriseSchema ]?.userSourceId && + !isAdminUser(user) && + !isCollaboratorUser(user)); + + if (responseUsers) { + responseUsers.sort((userObject: UserBasicInterface, comparedUserObject: UserBasicInterface) => + userObject.name?.givenName?.localeCompare(comparedUserObject.name?.givenName) + ); + + setUsersList(responseUsers); + } else { + setUsersList([]); + } + }) + .finally(() => { + setUserListRequestLoading(false); + }); + }; + + const getFormValues = (values: Map): void => { + eventPublisher.publish("manage-users-collaborator-role", { + type: kebabCase(values.get("role").toString()) + }); + + const inviteUser: UserInviteInterface = { + email: values.get("email").toString(), + roles: [ values.get("role").toString() ] + }; + + if (triggerSubmit) { + onSubmit(inviteUser); + } + }; + + /** + * This is a debounced function to handle the user search by email address. + * Debounced to limit getUsers() api call. + * @param FormEvent - form event + * @param string - query + */ + const handleSearchFieldChange: DebouncedFunc<(e: FormEvent, query: string) => void> + = useCallback(debounce((e: FormEvent, query: string) => { + if (query === "") { + setSearchQuery(""); + } else { + const processedQuery: string = "emails co " + query; + + setSearchQuery(processedQuery); + } + }, 1000), []); + + const handleAssignedItemCheckboxChange = (user: UserBasicInterface) => { + const checkedUsers: UserBasicInterface[] = [ ...checkedAssignedListItems ]; + + if (checkedUsers.some((item: UserBasicInterface) => item.id === user.id)) { + checkedUsers.splice(checkedUsers.indexOf(user), 1); + setCheckedAssignedListItems(checkedUsers); + } else { + checkedUsers.push(user); + setCheckedAssignedListItems(checkedUsers); + } + }; + + const resolveListItemElement = (listItemValue: string) => { + + return ( + <> + { listItemValue } + + ); + }; + + const processInternalAdminFormData = (roleName: string): void => { + const selectedRoles: RolesInterface[] = rolesList?.filter( + (role: RolesInterface) => role.displayName == roleName); + const processedFormData: InternalAdminFormDataInterface = { + checkedUsers: checkedAssignedListItems, + selectedRoles: selectedRoles + }; + + if (triggerSubmit) { + onSubmit(processedFormData); + } + }; + + /** + * The modal to add new user internally. + */ + const inviteInternalUserForm = () => ( + processInternalAdminFormData(administratorConfig.adminRoleName) } + submitState={ triggerSubmit } + > + + { t("extensions:manage.users.wizard.addAdmin.internal.selectUser") } + + + { t("extensions:manage.users.wizard.addAdmin.internal.hint") } + + , + { value }: { value: string; }) => { + handleSearchFieldChange(e, value); + } } + data-componentid={ `${ componentId }-transfer-component` } + > + + { + usersList?.map((user: UserBasicInterface, index: number) => { + + const header: string = getUserNameWithoutDomain(user?.userName); + const subHeader: string = UserManagementUtils.resolveUserListSubheader(user); + + return ( + handleAssignedItemCheckboxChange(user) } + key={ index } + listItem={ { + listItemElement: resolveListItemElement(header), + listItemValue: subHeader + } } + listItemId={ user.id } + listItemIndex={ index } + isItemChecked={ checkedAssignedListItems.some((item: UserBasicInterface) => + item.id === user.id) } + showSecondaryActions={ false } + showListSubItem={ true } + listSubItem={ header !== subHeader && ( +
+ + + { subHeader } + + +
+ ) } + data-componentid={ `${ componentId }-unselected-transfer-list-item-${ index }` } + /> + ); + } ) + } +
+
+
+ ); + + /** + * The modal to add new user externally. + */ + const inviteExternalUserForm = () => ( + ) => { + onSubmit(getFormValues(values)); + } } + submitState={ triggerSubmit } + > + { + (isUserRoleOptionsRequestLoading || + !userRoleOptions || + userRoleOptions?.length <= 0 + ) ? ( + + ) : ( + + + + { + // Check whether username is a valid email. + // check username validity against userstore regex + if (value && (!FormValidation.email(value) || !SharedUserStoreUtils + .validateInputAgainstRegEx(value, window["AppUtils"] + .getConfig().extensions.collaboratorUsernameRegex))) { + validation.isValid = false; + validation.errorMessages.push(USERNAME_REGEX_VIOLATION_ERROR_MESSAGE); + } + } } + type="email" + tabIndex={ 5 } + maxLength={ 50 } + /> + + + + + { + + } + + + { "Select a role to assign to the user." + + " The access level of the user is determined by the role." } + + { t("extensions:common.learnMore") } + + + + + + ) + } + + ); + + return ( + administratorType === AdminAccountTypes.INTERNAL + ? inviteInternalUserForm() + : inviteExternalUserForm() + ); +}; + +/** + * Default props for the component. + */ +AddAdminUserBasic.defaultProps = { + "data-componentid": "add-admin-user-basic" +}; diff --git a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx new file mode 100644 index 00000000000..ac968f0e0cd --- /dev/null +++ b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx @@ -0,0 +1,894 @@ +/** + * Copyright (c) 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 { InternalAdminFormDataInterface } from "@wso2is/admin.administrators.v1/models"; +import { isAdminUser } from "@wso2is/admin.administrators.v1/utils/administrators"; +import { + AddAdminUserBasic +} from "@wso2is/admin.administrators.v1/wizard/steps/admin-user-basic"; +import { useApplicationList } from "@wso2is/admin.applications.v1/api"; +import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; +import { UserBasicInterface } from "@wso2is/admin.core.v1"; +import { administratorConfig } from "@wso2is/admin.extensions.v1/configs/administrator"; +import { updateRoleDetails, useRolesList } from "@wso2is/admin.roles.v2/api/roles"; +import { PatchRoleDataInterface } from "@wso2is/admin.roles.v2/models/roles"; +import { sendInvite, useUsersList } from "@wso2is/admin.users.v1/api"; +import { getUserWizardStepIcons } from "@wso2is/admin.users.v1/configs/ui"; +import { AdminAccountTypes, UserManagementConstants } from "@wso2is/admin.users.v1/constants/user-management-constants"; +import { UserInviteInterface } from "@wso2is/admin.users.v1/models"; +import { PRIMARY_USERSTORE } from "@wso2is/admin.userstores.v1/constants/user-store-constants"; +import { + AlertLevels, + IdentifiableComponentInterface, + RolesInterface, + TestableComponentInterface +} from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { useTrigger } from "@wso2is/forms"; +import { + ConfirmationModal, + GenericIconProps, + LinkButton, + PrimaryButton, + useWizardAlert +} from "@wso2is/react-components"; +import { AxiosError } from "axios"; +import intersection from "lodash-es/intersection"; +import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import { Grid, Icon, Modal } from "semantic-ui-react"; + +interface AddUserWizardPropsInterface extends IdentifiableComponentInterface, TestableComponentInterface { + closeWizard: () => void; + compact?: boolean; + currentStep?: number; + updateList?: () => void; + rolesList?: any; + emailVerificationEnabled: boolean; + requiredSteps?: WizardStepsFormTypes[] | string[]; + submitStep?: WizardStepsFormTypes | string; + showStepper?: boolean; + onSuccessfulUserAddition?: (id: string) => void; + onInvitationSendSuccessful?: () => void; + conditionallyShowStepper?: boolean; + adminTypeSelection? :string; + /** + * On user update callback. + * + * @param userId - ID of the updated user. + */ + onUserUpdate?: () => void; +} + +/** + * Interface for wizard step. + */ +interface WizardStepInterface { + content: ReactElement; + icon: GenericIconProps | any; + name: string; + title: string; +} + +/** + * Enum for wizard steps form types. + * @readonly + */ +export enum WizardStepsFormTypes { + BASIC_DETAILS = "BasicDetails" +} + +/** + * New Administrator creation wizard. + * + * @param props - Props injected to the component. + * @returns Add administrator wizard component. + */ +export const AddAdministratorWizard: FunctionComponent = ( + props: AddUserWizardPropsInterface +): ReactElement => { + + const { + adminTypeSelection, + closeWizard, + currentStep, + requiredSteps, + onInvitationSendSuccessful, + onUserUpdate, + [ "data-componentid" ]: componentId, + [ "data-testid" ]: testId + } = props; + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ submitGeneralSettings, setSubmitGeneralSettings ] = useTrigger(); + + const [ partiallyCompletedStep, setPartiallyCompletedStep ] = useState(undefined); + const [ currentWizardStep, setCurrentWizardStep ] = useState(currentStep); + const [ wizardSteps, setWizardSteps ] = useState([]); + const [ isStepsUpdated, setIsStepsUpdated ] = useState(false); + const [ isSubmitting, setIsSubmitting ] = useState(false); + const [ isFinishButtonDisabled, setFinishButtonDisabled ] = useState(false); + const [ searchQuery, setSearchQuery ] = useState(null); + const [ invite, setInvite ] = useState(null); + const [ showAdminRoleAddConfirmationModal, setShowAdminRoleAddConfirmationModal ] = useState(false); + const [ confirmationModalLoading, setConfirmationModalLoading ] = useState(false); + const [ adminRoleId, setAdminRoleId ] = useState(null); + const [ selectedUser, setSelectedUser ] = useState(null); + + const [ alert, setAlert, alertComponent ] = useWizardAlert(); + + // Excluding groups and roles from getUserList API call to improve performance. + const excludedAttributes: string = UserManagementConstants.GROUPS_ATTRIBUTE; + + const { + data: originalAdminUserList, + error: adminUserListFetchRequestError + } = useUsersList( + 1, + 0, + searchQuery, + null, + PRIMARY_USERSTORE, + excludedAttributes, + !!searchQuery + ); + + /** + * Retrieve the application data for the console application, filtering by name. + */ + const { + data: applicationListData , + error: applicationListFetchRequestError + } = useApplicationList( + null, + null, + null, + `name eq ${ApplicationManagementConstants.CONSOLE_APP_NAME}`, + !!searchQuery + ); + + /** + * Build the roles filter to search for roles specific to the console application. + */ + const roleSearchFilter: string = useMemo(() => { + if (applicationListData?.applications && applicationListData?.applications?.length > 0) { + return `audience.value eq ${applicationListData?.applications[0]?.id}`; + } + + return null; + }, [ applicationListData ]); + + /** + * Retrieve the roles list. + * Only retrieve roles when there an edge case where the user + * is already in the system and the user is not an admin user. + */ + const { + data: rolesList, + error: rolesListFetchRequestError + } = useRolesList( + null, + null, + roleSearchFilter, + null, + !!searchQuery + ); + + useEffect(() => { + setWizardSteps(filterSteps([ + WizardStepsFormTypes.BASIC_DETAILS ])); + setIsStepsUpdated(true); + }, []); + + /** + * This hook will check if the user is an admin user and prompt to add the admin role to the user. + */ + useEffect(() => { + if (!originalAdminUserList || !invite?.email) { + return; + } + + // If total results are 0, the user does not exists. + // This means there is a pending invitation for the user. + if (originalAdminUserList.totalResults <= 0) { + dispatch(addAlert({ + description: t( + "extensions:manage.invite.notifications.sendInvite.inviteAlreadyExistsError.description", + { userName: invite.email } + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.invite.notifications.sendInvite.inviteAlreadyExistsError.message" + ) + })); + setIsSubmitting(false); + closeWizard(); + + return; + } + + // Check if the user in the first index is with Administrator role. + // If an admin, show an error message. + if (isAdminUser(originalAdminUserList.Resources[ 0 ])) { + dispatch(addAlert({ + description: t( + "extensions:manage.invite.notifications.sendInvite.userAlreadyExistsError.description", + { userName: invite.email } + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.invite.notifications.sendInvite.userAlreadyExistsError.message" + ) + })); + setIsSubmitting(false); + closeWizard(); + } else { + // If not, prompt to assign the admin role to the user. + setIsSubmitting(false); + setShowAdminRoleAddConfirmationModal(true); + setSelectedUser(originalAdminUserList.Resources[ 0 ]); + } + }, [ originalAdminUserList ]); + + /** + * Dispatches error notifications for Admin user fetch request error. + */ + useEffect(() => { + if (!adminUserListFetchRequestError) { + return; + } + + if (adminUserListFetchRequestError?.response?.data?.description) { + dispatch(addAlert({ + description: adminUserListFetchRequestError?.response?.data?.description + ?? adminUserListFetchRequestError?.response?.data?.detail + ?? t("users:notifications.fetchUsers.error.description"), + level: AlertLevels.ERROR, + message: adminUserListFetchRequestError?.response?.data?.message + ?? t("users:notifications.fetchUsers.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("users:notifications.fetchUsers.genericError." + + "description"), + level: AlertLevels.ERROR, + message: t("users:notifications.fetchUsers.genericError.message") + })); + }, [ adminUserListFetchRequestError ]); + + /** + * Dispatches error notifications for application list fetch request error. + */ + useEffect(() => { + if (!applicationListFetchRequestError) { + return; + } + + if (applicationListFetchRequestError.response + && applicationListFetchRequestError.response.data + && applicationListFetchRequestError.response.data.description) { + dispatch(addAlert({ + description: applicationListFetchRequestError.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications." + + "fetchApplications.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applications:notifications.fetchApplications" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications." + + "fetchApplications.genericError.message") + })); + }, [ applicationListFetchRequestError ]); + + /** + * Dispatches error notifications for roles list fetch request error. + */ + useEffect(() => { + if (!rolesListFetchRequestError) { + return; + } + + if (rolesListFetchRequestError?.response?.data?.description) { + dispatch(addAlert({ + description: rolesListFetchRequestError?.response?.data?.description + ?? rolesListFetchRequestError?.response?.data?.detail + ?? t("roles:notifications.fetchRoles.error.description"), + level: AlertLevels.ERROR, + message: rolesListFetchRequestError?.response?.data?.message + ?? t("roles:notifications.fetchRoles.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("roles:notifications.fetchRoles.genericError.description"), + level: AlertLevels.ERROR, + message: t("roles:notifications.fetchRoles.genericError.message") + })); + }, [ rolesListFetchRequestError ]); + + /** + * Get the administrator role from the roles list. + */ + useEffect(() => { + if (!rolesList || rolesList.Resources.length <= 0) { + return; + } + + const adminRole: RolesInterface = rolesList.Resources.find((role: RolesInterface) => + role.displayName === administratorConfig.adminRoleName); + + // If the admin role is not found, show an error message. + if (!adminRole) { + dispatch(addAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.noAdminRoleError.description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.noAdminRoleError.message" + ) + })); + } else { + // Else, set the admin role ID. + setAdminRoleId(adminRole?.id); + } + }, [ rolesList ]); + + /** + * 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(currentWizardStep - 1); + setPartiallyCompletedStep(undefined); + }, [ partiallyCompletedStep ]); + + /** + * Navigates to the next step. + */ + const navigateToNext = () => { + switch (wizardSteps[ currentWizardStep ]?.name) { + case WizardStepsFormTypes.BASIC_DETAILS: + setSubmitGeneralSettings(); + + break; + default: + break; + } + }; + + /** + * Navigates to the previous step. + */ + const navigateToPrevious = () => { + setPartiallyCompletedStep(currentWizardStep); + }; + + /** + * This function handles assigning the roles to the user. + */ + const assignUserRoles = async (users: UserBasicInterface[], roles: RolesInterface[]) => { + const roleIds: string[] = []; + const userList: {display: string, value: string}[] = []; + + if (users.length > 0) { + users.map((user: UserBasicInterface) => { + userList.push({ + display: user.userName, + value: user.id + }); + }); + } + + // Payload for the update role request. + const roleData: PatchRoleDataInterface = { + Operations: [ + { + op: "add", + value: { + users: userList + } + } + ], + schemas: [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] + }; + + if (roles.length > 0) { + roles.map((role: RolesInterface) => { + roleIds.push(role.id); + }); + + for (const roleId of roleIds) { + setIsSubmitting(true); + + await updateRoleDetails(roleId, roleData) + .catch((error: AxiosError) => { + if (!error.response || error.response.status === 401) { + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" + ) + }); + } else if (error.response && error.response.data && error.response.data.detail) { + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description", + { description: error.response.data.detail } + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" + ) + }); + } else { + // Generic error message + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError" + + ".description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.message" + ) + }); + } + }) + .finally(() => { + setIsSubmitting(false); + }); + } + dispatch(addAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.message" + ) + })); + closeWizard(); + onUserUpdate(); + } + }; + + /** + * Assigns the admin role to the user. + */ + const assignAdminRole = () => { + if (!selectedUser || !adminRoleId) { + return; + } + + // Payload for the update role request. + const roleData: PatchRoleDataInterface = { + Operations: [ + { + op: "add", + value: { + users: [ + { + display: selectedUser?.userName, + value: selectedUser?.id + } + ] + } + } + ], + schemas: [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] + }; + + setConfirmationModalLoading(true); + + updateRoleDetails(adminRoleId, roleData) + .then(() => { + dispatch(addAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.message" + ) + })); + onUserUpdate(); + }) + .catch((error: AxiosError) => { + if (!error.response || error.response.status === 401) { + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" + ) + }); + } else if (error.response && error.response.data && error.response.data.detail) { + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description", + { description: error.response.data.detail } + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" + ) + }); + } else { + // Generic error message + setAlert({ + description: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.message" + ) + }); + } + }) + .finally(() => { + setIsSubmitting(false); + setConfirmationModalLoading(false); + closeWizard(); + }); + }; + + /** + * This function handles sending the invitation to the external admin user. + */ + const sendExternalInvitation = (invite: UserInviteInterface) => { + if (invite != null) { + setIsSubmitting(true); + + sendInvite(invite) + .then(() => { + dispatch(addAlert({ + description: t( + "console:manage.features.invite.notifications.sendInvite.success.description" + ), + level: AlertLevels.SUCCESS, + message: t( + "console:manage.features.invite.notifications.sendInvite.success.message" + ) + })); + setIsSubmitting(false); + closeWizard(); + onInvitationSendSuccessful(); + }) + .catch((error: AxiosError) => { + // Axios throws a generic `Network Error` for 401 status. + // As a temporary solution, a check to see if a response + // is available has be used. + if (!error.response || error.response.status === 401) { + setIsSubmitting(false); + closeWizard(); + dispatch(addAlert({ + description: t( + "console:manage.features.invite.notifications.sendInvite.error.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.invite.notifications.sendInvite.error.message" + ) + })); + } else if (error.response.status === 403 && + error?.response?.data?.code === UserManagementConstants.ERROR_COLLABORATOR_USER_LIMIT_REACHED) { + setIsSubmitting(false); + closeWizard(); + dispatch(addAlert({ + description: t( + "extensions:manage.invite.notifications.sendInvite.limitReachError.description" + ), + level: AlertLevels.ERROR, + message: t( + "extensions:manage.invite.notifications.sendInvite.limitReachError.message" + ) + })); + } else if (error.response.status === 409) { + // User already exists in the system. + // We need to check if this user is having the Administator role. + handleAlredyExistingUser(invite); + } else if (error?.response?.data?.description) { + setIsSubmitting(false); + closeWizard(); + dispatch(addAlert({ + description: t( + "console:manage.features.invite.notifications.sendInvite.error.description", + { description: error.response.data.description } + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.invite.notifications.sendInvite.error.message" + ) + })); + } else { + setIsSubmitting(false); + closeWizard(); + // Generic error message + dispatch(addAlert({ + description: t( + "console:manage.features.invite.notifications.sendInvite.genericError.description" + ), + level: AlertLevels.ERROR, + message: t( + "console:manage.features.invite.notifications.sendInvite.genericError.message" + ) + })); + } + }); + } + }; + + /** + * Handles the already existing user. + * + * @param error - Axios error response. + */ + const handleAlredyExistingUser = (invite: UserInviteInterface): void => { + if (!invite?.email) { + setIsSubmitting(false); + + return; + } + + if (!selectedUser || selectedUser?.userName !== invite.email) { + // If the user is not selected. Query the user. + setSearchQuery(`userName eq ${ invite.email }`); + setInvite(invite); + } else { + // If the user is already selected, prompt to assign the admin role. + // Handles cancelling and re-opening the modal. + setShowAdminRoleAddConfirmationModal(true); + setIsSubmitting(false); + } + }; + + /** + * This function handles sending the invitation to the external admin user. + */ + const sendInternalInvitation = (data: InternalAdminFormDataInterface) => { + assignUserRoles(data?.checkedUsers, data?.selectedRoles); + }; + + /** + * Resolves the step content. + * + * @returns Step content. + */ + const resolveStepContent = (): ReactElement => { + switch (wizardSteps[ currentWizardStep ]?.name) { + case WizardStepsFormTypes.BASIC_DETAILS: + return getAdminBasicDetailsWizardStep()?.content; + } + }; + + /** + * Filters the steps evaluating the requested steps. + * + * @param steps - Steps to filter. + * @returns Filtered 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(getAdminBasicDetailsWizardStep()); + } + }); + + return filteredSteps; + }; + + if (!requiredSteps) { + return getStepContent(steps); + } + + return getStepContent(intersection(steps, requiredSteps)); + }; + + /** + * Basic Wizard Step. + * @returns Basic details wizard step. + */ + const getAdminBasicDetailsWizardStep = (): WizardStepInterface => { + + return { + content: ( + + (adminTypeSelection === AdminAccountTypes.INTERNAL) + ? sendInternalInvitation(values as InternalAdminFormDataInterface) + : sendExternalInvitation(values as UserInviteInterface) + } + setFinishButtonDisabled={ setFinishButtonDisabled } + /> + ), + icon: getUserWizardStepIcons().general, + name: WizardStepsFormTypes.BASIC_DETAILS, + title: t("console:manage.features.user.modals.addUserWizard.steps.basicDetails") + }; + }; + + return ( + wizardSteps && isStepsUpdated ? ( + <> + + + { adminTypeSelection === AdminAccountTypes.INTERNAL + ? t("extensions:manage.users.wizard.addAdmin.internal.title") + : t("extensions:manage.users.wizard.addAdmin.external.title") + } + + + { alert && alertComponent } + { resolveStepContent() } + + + + + + closeWizard() } + > + { t("common:cancel") } + + + + { (currentWizardStep < wizardSteps?.length - 1) && ( + + { t("console:manage.features.user.modals.addUserWizard.buttons.next") } + + + ) } + { currentWizardStep === wizardSteps?.length - 1 && ( + + { + adminTypeSelection === AdminAccountTypes.INTERNAL + ? t("extensions:manage.features.user.addUser.add") + : t("extensions:manage.features.user.addUser.invite") + } + + ) } + { (wizardSteps?.length > 1 && currentWizardStep > 0) && ( + + + { t("console:manage.features.user.modals.addUserWizard.buttons.previous") } + + ) } + + + + + + { + selectedUser && adminRoleId && ( + { + setSubmitGeneralSettings(); + setShowAdminRoleAddConfirmationModal(false); + } } + type="warning" + open={ showAdminRoleAddConfirmationModal } + assertionHint={ + t("extensions:manage.invite.assignAdminUser.confirmationModal.assertionHint") } + assertionType="checkbox" + primaryAction="Confirm" + secondaryAction="Cancel" + onSecondaryActionClick={ (): void => { + setSubmitGeneralSettings(); + setShowAdminRoleAddConfirmationModal(false); + setAlert(null); + } } + onPrimaryActionClick={ (): void => { + setConfirmationModalLoading(true); + assignAdminRole(); + } } + closeOnDimmerClick={ false } + > + + { t("extensions:manage.invite.assignAdminUser.confirmationModal.header") } + + + { t("extensions:manage.invite.assignAdminUser.confirmationModal.message") } + + + ) + } + + ) : null + ); +}; + +/** + * Default props for the add administrator wizard. + */ +AddAdministratorWizard.defaultProps = { + compact: false, + conditionallyShowStepper: false, + currentStep: 0, + emailVerificationEnabled: false, + showStepper: true, + submitStep: WizardStepsFormTypes.BASIC_DETAILS +}; diff --git a/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx b/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx index f3f61157899..871bbfbf18b 100644 --- a/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx @@ -26,7 +26,6 @@ import { import { useOrganizationConfigV2 } from "@wso2is/admin.administrators.v1/api/useOrganizationConfigV2"; import { AdministratorConstants } from "@wso2is/admin.administrators.v1/constants/users"; import { UseOrganizationConfigType } from "@wso2is/admin.administrators.v1/models/organization"; -import { AddAdministratorWizard } from "@wso2is/admin.administrators.v1/wizard/add-administrator-wizard"; import { AdvancedSearchWithBasicFilters, AppConstants, @@ -64,6 +63,7 @@ import AdministratorsTable from "./administrators-table"; import useAdministrators from "../../../hooks/use-administrators"; import useBulkAssignAdministratorRoles from "../../../hooks/use-bulk-assign-user-roles"; import AddExistingUserWizard from "../add-existing-user-wizard/add-existing-user-wizard"; +import { AddAdministratorWizard } from "../add-external-admin-wizard/add-external-admin-wizard"; import InviteNewAdministratorWizard from "../invite-new-administrator-wizard/invite-new-administrator-wizard"; /** diff --git a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx index f11dfcc9864..e00a377d271 100644 --- a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx @@ -186,8 +186,8 @@ const ConsoleAdministrators: FunctionComponent = value={ activeAdministratorGroup } onChange={ (_: ChangeEvent, value: string) => setActiveAdministratorGroup(value) } > - } label="Administrators" /> - } label="Privileged Users" /> + } label="Asgardeo" /> + } label={ organizationName + " organization" } /> ); } diff --git a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.scss b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.scss index c4772fc655a..43dbbc3d7d7 100644 --- a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.scss +++ b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.scss @@ -6,4 +6,9 @@ .submit-button { margin-top: 2rem; } + + .multi-option-radio-group { + gap: 40px; + margin: 20px 0; + } } diff --git a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx index d96315bb968..43a9ec81bb9 100644 --- a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx +++ b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx @@ -30,11 +30,14 @@ import { RoleAudienceTypes } from "@wso2is/admin.roles.v2/constants/role-constan import { RoleConstants } from "@wso2is/core/constants"; import { FeatureAccessConfigInterface, IdentifiableComponentInterface, RolesInterface } from "@wso2is/core/models"; import { ResourceTab, ResourceTabPaneInterface } from "@wso2is/react-components"; -import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import React, { ChangeEvent, FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import ConsoleRolePermissions from "./console-role-permissions"; import "./console-roles-edit.scss"; +import RadioGroup from "@oxygen-ui/react/RadioGroup"; +import { FormControlLabel } from "@mui/material"; +import Radio from "@oxygen-ui/react/Radio"; /** * Captures props needed for edit role component @@ -64,40 +67,41 @@ interface ConsoleRolesEditPropsInterface extends IdentifiableComponentInterface * @param props - contains role details to be edited. */ const ConsoleRolesEdit: FunctionComponent = ( - props: ConsoleRolesEditPropsInterface): ReactElement => { - - const { - isLoading, - roleObject, - onRoleUpdate, - defaultActiveIndex - } = props; + props: ConsoleRolesEditPropsInterface +): ReactElement => { + const { isLoading, roleObject, onRoleUpdate, defaultActiveIndex } = props; const { t } = useTranslation(); const { isFirstLevelOrganization, isSubOrganization, organizationType } = useGetCurrentOrganizationType(); const userRolesFeatureConfig: FeatureAccessConfigInterface = useSelector( - (state: AppState) => state?.config?.ui?.features?.userRoles); + (state: AppState) => state?.config?.ui?.features?.userRoles + ); const hasRolesUpdatePermissions: boolean = useRequiredScopes(userRolesFeatureConfig?.scopes?.update); const administratorRoleDisplayName: string = useSelector( - (state: AppState) => state?.config?.ui?.administratorRoleDisplayName); + (state: AppState) => state?.config?.ui?.administratorRoleDisplayName + ); - const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = - useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); + const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = useSelector( + (state: AppState) => state?.config?.ui?.features?.consoleSettings + ); const isConsoleRolesEditable: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( "consoleSettings.editableConsoleRoles" ); + const isPrivilegedUsersInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( + "consoleSettings.privilegedUsers" + ); const [ isAdminRole, setIsAdminRole ] = useState(false); const [ isEnterpriseLoginEnabled, setIsEnterpriseLoginEnabled ] = useState(false); + const [ activeUserStore, setActiveUserStore ] = useState("PRIMARY"); const organizationName: string = store.getState().auth.tenantDomain; const useOrgConfig: UseOrganizationConfigType = useOrganizationConfigV2; - const saasFeatureStatus : FeatureStatus = useCheckFeatureStatus( - FeatureGateConstants.SAAS_FEATURES_IDENTIFIER); + const saasFeatureStatus: FeatureStatus = useCheckFeatureStatus(FeatureGateConstants.SAAS_FEATURES_IDENTIFIER); const { data: OrganizationConfig, @@ -121,10 +125,12 @@ const ConsoleRolesEdit: FunctionComponent = ( * Set the if the role is `Internal/admin`. */ useEffect(() => { - if(roleObject) { - setIsAdminRole(roleObject.displayName === RoleConstants.ADMIN_ROLE || - roleObject?.displayName === RoleConstants.ADMIN_GROUP || - roleObject?.displayName === administratorRoleDisplayName); + if (roleObject) { + setIsAdminRole( + roleObject.displayName === RoleConstants.ADMIN_ROLE || + roleObject?.displayName === RoleConstants.ADMIN_GROUP || + roleObject?.displayName === administratorRoleDisplayName + ); } }, [ roleObject ]); @@ -135,8 +141,9 @@ const ConsoleRolesEdit: FunctionComponent = ( render: () => ( = ( = ( ) }, - ( (isFirstLevelOrganization() && isEnterpriseLoginEnabled) || isSubOrganization() ) && { + ((isFirstLevelOrganization() && isEnterpriseLoginEnabled) || isSubOrganization()) && { menuItem: t("roles:edit.menuItems.groups"), render: () => ( @@ -181,10 +185,34 @@ const ConsoleRolesEdit: FunctionComponent = ( menuItem: t("roles:edit.menuItems.users"), render: () => ( + { isFirstLevelOrganization() && + isEnterpriseLoginEnabled && + isPrivilegedUsersInConsoleSettingsEnabled && ( + , value: string) => { + setActiveUserStore(value); + } } + > + } label="Asgardeo" /> + } + label={ organizationName + " organization" } + /> + + ) } + @@ -211,13 +239,7 @@ const ConsoleRolesEdit: FunctionComponent = ( return panes; }; - return ( - - ); + return ; }; ConsoleRolesEdit.defaultProps = { diff --git a/features/admin.roles.v2/components/edit-role/edit-role-users.tsx b/features/admin.roles.v2/components/edit-role/edit-role-users.tsx index 356c53b1892..3d799d2b181 100644 --- a/features/admin.roles.v2/components/edit-role/edit-role-users.tsx +++ b/features/admin.roles.v2/components/edit-role/edit-role-users.tsx @@ -81,6 +81,7 @@ export const RoleUsersList: FunctionComponent = ( onRoleUpdate, isReadOnly, tabIndex, + activeUserStore, [ "data-componentid" ]: componentId } = props; @@ -97,7 +98,7 @@ export const RoleUsersList: FunctionComponent = ( const [ activeOption, setActiveOption ] = useState(undefined); const [ availableUserStores, setAvailableUserStores ] = useState([]); const [ selectedUserStoreDomainName, setSelectedUserStoreDomainName ] = useState( - disabledUserstores.includes(RemoteUserStoreConstants.PRIMARY_USER_STORE_NAME) ? "DEFAULT" : "Primary" + activeUserStore ?? disabledUserstores.includes(RemoteUserStoreConstants.PRIMARY_USER_STORE_NAME) ? "DEFAULT" : "PRIMARY" ); const [ isPlaceholderVisible, setIsPlaceholderVisible ] = useState(true); const [ selectedUsersFromUserStore, setSelectedUsersFromUserStore ] = useState([]); @@ -125,10 +126,14 @@ export const RoleUsersList: FunctionComponent = ( const isUserBelongToSelectedUserStore = (user: UserBasicInterface, userStoreName: string) => { const userNameChunks: string[] = user.userName.split("/"); - return (userNameChunks.length === 1 && userStoreName === "Primary") + return (userNameChunks.length === 1 && userStoreName === "PRIMARY") || (userNameChunks.length === 2 && userNameChunks[0] === userStoreName.toUpperCase()); }; + useEffect(() => { + setSelectedUserStoreDomainName(activeUserStore); + }, [ activeUserStore ]); + useEffect(() => { if (!role?.users?.length) { return; @@ -189,6 +194,7 @@ export const RoleUsersList: FunctionComponent = ( if (!isReadOnly && userResponse?.totalResults > 0 && Array.isArray(userResponse?.Resources)) { const usersAvailableToSelect: UserBasicInterface[] = userResponse?.Resources?.filter( (user: UserBasicInterface) => { + console.log(selectedUserStoreDomainName, user); const isUserInSelectedUserStore: boolean = isUserBelongToSelectedUserStore( user, selectedUserStoreDomainName ); @@ -295,6 +301,8 @@ export const RoleUsersList: FunctionComponent = ( path: "/v2/Roles/" + role.id }; + debugger; + const selectedUsers: UserBasicInterface[] = Object.keys(selectedAllUsers).map((userStoreName: string) => { return selectedAllUsers[userStoreName]; }).flat(); @@ -303,8 +311,16 @@ export const RoleUsersList: FunctionComponent = ( const removedUsers: RolesMemberInterface[] = role?.users?.filter((user: RolesMemberInterface) => { return selectedUsers?.find( (selectedUser: UserBasicInterface) => selectedUser.id === user.value) === undefined; + }).filter(user => { + console.log(user); + const userNameChunks: string[] = user.display.split("/"); + + return (userNameChunks.length === 1 && selectedUserStoreDomainName === "PRIMARY") + || (userNameChunks.length === 2 && userNameChunks[0] === selectedUserStoreDomainName.toUpperCase()); }) ?? []; + + const removeOperations: PatchUserRemoveOpInterface[] = removedUsers?.map((user: RolesMemberInterface) => { return ({ "op": "remove", @@ -390,33 +406,35 @@ export const RoleUsersList: FunctionComponent = ( { users && availableUserStores && !isReadOnly && ( - - - ) => { + updateSelectedAllUsers(); + setSelectedUsersFromUserStore([]); + setSelectedUserStoreDomainName(e.target.value as string); + } } - } - data-componentid={ `${ componentId }-user-store-domain-dropdown` } - > - { isUserStoresLoading - ?

{ t("common:loading") }

- : availableUserStores?.map((userstore: UserStoreListItem) => - ( - { userstore.name } - ) - ) - } - -
-
+ data-componentid={ `${ componentId }-user-store-domain-dropdown` } + > + { isUserStoresLoading + ?

{ t("common:loading") }

+ : availableUserStores?.map((userstore: UserStoreListItem) => + ( + { userstore.name } + ) + ) + } + + +
+ ) } Date: Fri, 11 Oct 2024 07:54:42 +0530 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=A6=8B=20Add=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/ninety-ads-teach.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/ninety-ads-teach.md diff --git a/.changeset/ninety-ads-teach.md b/.changeset/ninety-ads-teach.md new file mode 100644 index 00000000000..bb42e8b342a --- /dev/null +++ b/.changeset/ninety-ads-teach.md @@ -0,0 +1,6 @@ +--- +"@wso2is/admin.console-settings.v1": patch +"@wso2is/admin.roles.v2": patch +--- + +Bug fixes related to conosle settings in primary organizations for managed deployments From 1c786afe7963b9973012a9e0bebc55c2a7045a26 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 11 Oct 2024 14:54:00 +0530 Subject: [PATCH 3/8] Migrate internal and external user invitation forms to React final form --- .../wizard/steps/admin-user-basic.tsx | 389 ++++++++++-------- 1 file changed, 226 insertions(+), 163 deletions(-) diff --git a/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx b/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx index db30a253440..9f012156a33 100644 --- a/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx +++ b/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx @@ -16,9 +16,11 @@ * under the License. */ +import { FeatureAccessConfigInterface } from "@wso2is/access-control"; import { useApplicationList } from "@wso2is/admin.applications.v1/api/application"; import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; import { + AppState, SharedUserStoreUtils, UIConstants, UserBasicInterface, @@ -33,7 +35,9 @@ import { UserInviteInterface, UserListInterface } from "@wso2is/admin.users.v1/m import { UserManagementUtils } from "@wso2is/admin.users.v1/utils"; import { getUserNameWithoutDomain } from "@wso2is/core/helpers"; import { IdentifiableComponentInterface, RolesInterface } from "@wso2is/core/models"; -import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; +import { FinalForm, FinalFormField, FormRenderProps, TextFieldAdapter } from "@wso2is/form"; +import { AutocompleteFieldAdapter } from "@wso2is/form/src"; +import { Validation } from "@wso2is/forms"; import { ContentLoader, DocumentationLink, @@ -51,6 +55,7 @@ import isEmpty from "lodash-es/isEmpty"; import kebabCase from "lodash-es/kebabCase"; import React, { FormEvent, ReactElement, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; import { Divider, DropdownProps, Grid, Header } from "semantic-ui-react"; import { InternalAdminFormDataInterface } from "../../models"; import { isAdminUser, isCollaboratorUser } from "../../utils/administrators"; @@ -95,6 +100,12 @@ export const AddAdminUserBasic: React.FunctionComponent const { t } = useTranslation(); const { getLink } = useDocumentation(); + const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = + useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); + const isInvitedAdminInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( + "consoleSettings.invitedExternalAdmins" + ); + /** * Retrieve the application data for the console application, filtering by name. */ @@ -150,16 +161,29 @@ export const AddAdminUserBasic: React.FunctionComponent .then((response: AxiosResponse) => { setRolesList(response.data.Resources); response.data.Resources.map((role: RolesInterface, index: number) => { - if ((role.meta?.systemRole) && - role.displayName !== "system" && - role.displayName !== "everyone" && - role.displayName !== "selfsignup" && - (role.displayName?.split("/")?.length < 2 && - role.displayName?.split("/")[0] !== "Application") + if(role.displayName === "system" || + role.displayName === "everyone" || + role.displayName === "selfsignup" + ) { + return; + } + + if ( + role.displayName?.split("/")?.length < 2 && + role.displayName?.split("/")[0] === "Application" ) { + return; + } + + if (isInvitedAdminInConsoleSettingsEnabled || + ( + !isInvitedAdminInConsoleSettingsEnabled && role.meta?.systemRole + ) + + ){ roleOptions?.push({ key: index, - text: role?.displayName, + label: role?.displayName, value: role?.displayName }); } @@ -191,6 +215,15 @@ export const AddAdminUserBasic: React.FunctionComponent } }, [ checkedAssignedListItems ]); + useEffect(() => { + document + ?.getElementById( + administratorType === AdminAccountTypes.INTERNAL + ? "inviteInternalUserForm" + : "inviteExternalUserForm" + )?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); + }, [ triggerSubmit ]); + /** * The following method accepts a Map and returns the values as a string. * @@ -238,14 +271,14 @@ export const AddAdminUserBasic: React.FunctionComponent }); }; - const getFormValues = (values: Map): void => { + const getFormValues = (values: {email: string, roles: DropdownProps[]}): void => { eventPublisher.publish("manage-users-collaborator-role", { - type: kebabCase(values.get("role").toString()) + type: kebabCase(values.roles.toString()) }); const inviteUser: UserInviteInterface = { - email: values.get("email").toString(), - roles: [ values.get("role").toString() ] + email: values.email.toString(), + roles: values.roles.map((role: DropdownProps) => role.value as string) }; if (triggerSubmit) { @@ -308,170 +341,200 @@ export const AddAdminUserBasic: React.FunctionComponent * The modal to add new user internally. */ const inviteInternalUserForm = () => ( - processInternalAdminFormData(administratorConfig.adminRoleName) } - submitState={ triggerSubmit } - > + <> { t("extensions:manage.users.wizard.addAdmin.internal.selectUser") } - - { t("extensions:manage.users.wizard.addAdmin.internal.hint") } - - , - { value }: { value: string; }) => { - handleSearchFieldChange(e, value); - } } - data-componentid={ `${ componentId }-transfer-component` } - > - - { - usersList?.map((user: UserBasicInterface, index: number) => { - - const header: string = getUserNameWithoutDomain(user?.userName); - const subHeader: string = UserManagementUtils.resolveUserListSubheader(user); + { t("extensions:manage.users.wizard.addAdmin.internal.hint") } + processInternalAdminFormData(administratorConfig.adminRoleName) } + render={ ({ handleSubmit }: FormRenderProps) => { + return (
+ , { value }: { value: string }) => { + handleSearchFieldChange(e, value); + } + } + data-componentid={ `${componentId}-transfer-component` } + > + + { usersList?.map((user: UserBasicInterface, index: number) => { + const header: string = getUserNameWithoutDomain(user?.userName); + const subHeader: string = UserManagementUtils.resolveUserListSubheader(user); + + return ( + handleAssignedItemCheckboxChange(user) } + key={ index } + listItem={ { + listItemElement: resolveListItemElement(header), + listItemValue: subHeader + } } + listItemId={ user.id } + listItemIndex={ index } + isItemChecked={ checkedAssignedListItems.some( + (item: UserBasicInterface) => item.id === user.id + ) } + showSecondaryActions={ false } + showListSubItem={ true } + listSubItem={ + header !== subHeader && ( +
+ + + { subHeader } + + +
+ ) + } + data-componentid={ `${componentId}-unselected-transfer-list-item-${index}` } + /> + ); + }) } +
+
+
); - return ( - handleAssignedItemCheckboxChange(user) } - key={ index } - listItem={ { - listItemElement: resolveListItemElement(header), - listItemValue: subHeader - } } - listItemId={ user.id } - listItemIndex={ index } - isItemChecked={ checkedAssignedListItems.some((item: UserBasicInterface) => - item.id === user.id) } - showSecondaryActions={ false } - showListSubItem={ true } - listSubItem={ header !== subHeader && ( -
- - - { subHeader } - - -
- ) } - data-componentid={ `${ componentId }-unselected-transfer-list-item-${ index }` } - /> - ); - } ) - } -
-
-
+ } + } + > + ); /** * The modal to add new user externally. */ const inviteExternalUserForm = () => ( - ) => { - onSubmit(getFormValues(values)); - } } - submitState={ triggerSubmit } - > + <> + { (isUserRoleOptionsRequestLoading || - !userRoleOptions || - userRoleOptions?.length <= 0 - ) ? ( - - ) : ( - - - - { - // Check whether username is a valid email. - // check username validity against userstore regex - if (value && (!FormValidation.email(value) || !SharedUserStoreUtils - .validateInputAgainstRegEx(value, window["AppUtils"] - .getConfig().extensions.collaboratorUsernameRegex))) { - validation.isValid = false; - validation.errorMessages.push(USERNAME_REGEX_VIOLATION_ERROR_MESSAGE); - } - } } - type="email" - tabIndex={ 5 } - maxLength={ 50 } - /> - - - - - - - { "Select a role to assign to the user." + - " The access level of the user is determined by the role." } - - { t("extensions:common.learnMore") } - - - - - - ) + !userRoleOptions || + userRoleOptions?.length <= 0 + ) ? + : + ( { + onSubmit(getFormValues(values)); + } } + render={ ({ handleSubmit }: FormRenderProps) => { + return ( +
+ + + + { + // Check whether username is a valid email. + // check username validity against userstore regex + if ( + value && + ( + !FormValidation.email(value) || + !SharedUserStoreUtils.validateInputAgainstRegEx( + value, window["AppUtils"] + .getConfig().extensions. + collaboratorUsernameRegex + ) + ) + ) { + validation.isValid = false; + validation.errorMessages.push( + USERNAME_REGEX_VIOLATION_ERROR_MESSAGE + ); + } + } } + type="email" + component={ TextFieldAdapter } + tabIndex={ 5 } + maxLength={ 50 } + /> + + + + + + + { "Select a role to assign to the user." + + " The access level of the user is determined by the role." } + + { t("extensions:common.learnMore") } + + + + + +
+ + ); + } } + />) } -
+ + + + ); return ( From df37b29fb0bc2821a65aa8c3ace64e8b7588ebc3 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 11 Oct 2024 14:54:13 +0530 Subject: [PATCH 4/8] Address minor eslint issues --- .../console-administrators/console-administrators.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx index e00a377d271..4bd2cdc1e7d 100644 --- a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx @@ -187,7 +187,11 @@ const ConsoleAdministrators: FunctionComponent = onChange={ (_: ChangeEvent, value: string) => setActiveAdministratorGroup(value) } > } label="Asgardeo" /> - } label={ organizationName + " organization" } /> + } + label={ organizationName + " organization" } + /> ); } From 2183b1494f162b26432d19dae710b6bfa86d4a8e Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 11 Oct 2024 14:59:59 +0530 Subject: [PATCH 5/8] Address eslint warnings --- .../add-admin-user-basic.tsx | 5 +++-- .../console-roles-edit/console-roles-edit.tsx | 19 ++++++++++++------- .../components/edit-role/edit-role-users.tsx | 17 +++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx index e09dad53f60..900a33874be 100644 --- a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx @@ -56,7 +56,7 @@ import kebabCase from "lodash-es/kebabCase"; import React, { FormEvent, ReactElement, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Divider, Dropdown, DropdownDivider, DropdownItem, DropdownMenu, DropdownProps, Grid, Header } from "semantic-ui-react"; +import { Divider, DropdownProps, Grid, Header } from "semantic-ui-react"; /** * Proptypes for the add admin user basic component. @@ -98,7 +98,8 @@ export const AddAdminUserBasic: React.FunctionComponent const { t } = useTranslation(); const { getLink } = useDocumentation(); - const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); + const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = + useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); const isInvitedAdminInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( "consoleSettings.invitedExternalAdmins" ); diff --git a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx index 43a9ec81bb9..58897370769 100644 --- a/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx +++ b/features/admin.console-settings.v1/components/console-roles/console-roles-edit/console-roles-edit.tsx @@ -16,6 +16,9 @@ * under the License. */ +import FormControlLabel from "@oxygen-ui/react/FormControlLabel"; +import Radio from "@oxygen-ui/react/Radio"; +import RadioGroup from "@oxygen-ui/react/RadioGroup"; import { FeatureStatus, useCheckFeatureStatus, useRequiredScopes } from "@wso2is/access-control"; import { useOrganizationConfigV2 } from "@wso2is/admin.administrators.v1/api/useOrganizationConfigV2"; import { UseOrganizationConfigType } from "@wso2is/admin.administrators.v1/models"; @@ -35,9 +38,6 @@ import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import ConsoleRolePermissions from "./console-role-permissions"; import "./console-roles-edit.scss"; -import RadioGroup from "@oxygen-ui/react/RadioGroup"; -import { FormControlLabel } from "@mui/material"; -import Radio from "@oxygen-ui/react/Radio"; /** * Captures props needed for edit role component @@ -89,9 +89,10 @@ const ConsoleRolesEdit: FunctionComponent = ( const isConsoleRolesEditable: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( "consoleSettings.editableConsoleRoles" ); - const isPrivilegedUsersInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( - "consoleSettings.privilegedUsers" - ); + const isPrivilegedUsersInConsoleSettingsEnabled: boolean = + !consoleSettingsFeatureConfig?.disabledFeatures?.includes( + "consoleSettings.privilegedUsers" + ); const [ isAdminRole, setIsAdminRole ] = useState(false); const [ isEnterpriseLoginEnabled, setIsEnterpriseLoginEnabled ] = useState(false); @@ -239,7 +240,11 @@ const ConsoleRolesEdit: FunctionComponent = ( return panes; }; - return ; + return ( + ); }; ConsoleRolesEdit.defaultProps = { diff --git a/features/admin.roles.v2/components/edit-role/edit-role-users.tsx b/features/admin.roles.v2/components/edit-role/edit-role-users.tsx index 3d799d2b181..ebe6889fed2 100644 --- a/features/admin.roles.v2/components/edit-role/edit-role-users.tsx +++ b/features/admin.roles.v2/components/edit-role/edit-role-users.tsx @@ -98,7 +98,8 @@ export const RoleUsersList: FunctionComponent = ( const [ activeOption, setActiveOption ] = useState(undefined); const [ availableUserStores, setAvailableUserStores ] = useState([]); const [ selectedUserStoreDomainName, setSelectedUserStoreDomainName ] = useState( - activeUserStore ?? disabledUserstores.includes(RemoteUserStoreConstants.PRIMARY_USER_STORE_NAME) ? "DEFAULT" : "PRIMARY" + activeUserStore ?? + disabledUserstores.includes(RemoteUserStoreConstants.PRIMARY_USER_STORE_NAME) ? "DEFAULT" : "PRIMARY" ); const [ isPlaceholderVisible, setIsPlaceholderVisible ] = useState(true); const [ selectedUsersFromUserStore, setSelectedUsersFromUserStore ] = useState([]); @@ -194,7 +195,6 @@ export const RoleUsersList: FunctionComponent = ( if (!isReadOnly && userResponse?.totalResults > 0 && Array.isArray(userResponse?.Resources)) { const usersAvailableToSelect: UserBasicInterface[] = userResponse?.Resources?.filter( (user: UserBasicInterface) => { - console.log(selectedUserStoreDomainName, user); const isUserInSelectedUserStore: boolean = isUserBelongToSelectedUserStore( user, selectedUserStoreDomainName ); @@ -301,8 +301,6 @@ export const RoleUsersList: FunctionComponent = ( path: "/v2/Roles/" + role.id }; - debugger; - const selectedUsers: UserBasicInterface[] = Object.keys(selectedAllUsers).map((userStoreName: string) => { return selectedAllUsers[userStoreName]; }).flat(); @@ -311,8 +309,7 @@ export const RoleUsersList: FunctionComponent = ( const removedUsers: RolesMemberInterface[] = role?.users?.filter((user: RolesMemberInterface) => { return selectedUsers?.find( (selectedUser: UserBasicInterface) => selectedUser.id === user.value) === undefined; - }).filter(user => { - console.log(user); + }).filter((user: RolesMemberInterface) => { const userNameChunks: string[] = user.display.split("/"); return (userNameChunks.length === 1 && selectedUserStoreDomainName === "PRIMARY") @@ -415,10 +412,14 @@ export const RoleUsersList: FunctionComponent = ( (e: SelectChangeEvent) => { updateSelectedAllUsers(); setSelectedUsersFromUserStore([]); - setSelectedUserStoreDomainName(e.target.value as string); + setSelectedUserStoreDomainName( + e.target.value as string + ); } } - data-componentid={ `${ componentId }-user-store-domain-dropdown` } + data-componentid={ + `${ componentId }-user-store-domain-dropdown` + } > { isUserStoresLoading ?

{ t("common:loading") }

From 76ddcf19d2528adf15c16f38f581e9e47915e426 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Fri, 11 Oct 2024 15:21:12 +0530 Subject: [PATCH 6/8] Remove unused code --- .../add-admin-user-basic.tsx | 514 ---------- .../add-external-admin-wizard.tsx | 894 ------------------ .../administrators-list.tsx | 2 +- 3 files changed, 1 insertion(+), 1409 deletions(-) delete mode 100644 features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx delete mode 100644 features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx diff --git a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx deleted file mode 100644 index 900a33874be..00000000000 --- a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-admin-user-basic.tsx +++ /dev/null @@ -1,514 +0,0 @@ -/** - * Copyright (c) 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 { FeatureAccessConfigInterface } from "@wso2is/access-control"; -import { InternalAdminFormDataInterface } from "@wso2is/admin.administrators.v1/models"; -import { isAdminUser, isCollaboratorUser } from "@wso2is/admin.administrators.v1/utils/administrators"; -import { useApplicationList } from "@wso2is/admin.applications.v1/api/application"; -import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; -import { - AppState, - SharedUserStoreUtils, - UIConstants, - UserBasicInterface, - getUsersList -} from "@wso2is/admin.core.v1"; -import { EventPublisher } from "@wso2is/admin.core.v1/utils"; -import { administratorConfig } from "@wso2is/admin.extensions.v1/configs/administrator"; -import { SCIMConfigs } from "@wso2is/admin.extensions.v1/configs/scim"; -import { getRolesList } from "@wso2is/admin.roles.v2/api"; -import { AdminAccountTypes } from "@wso2is/admin.users.v1/constants/user-management-constants"; -import { UserInviteInterface, UserListInterface } from "@wso2is/admin.users.v1/models/user"; -import { UserManagementUtils } from "@wso2is/admin.users.v1/utils"; -import { getUserNameWithoutDomain } from "@wso2is/core/helpers"; -import { IdentifiableComponentInterface, RolesInterface } from "@wso2is/core/models"; -import { Field, FormValue, Forms, Validation } from "@wso2is/forms"; -import { - ContentLoader, - DocumentationLink, - Heading, - Hint, - TransferComponent, - TransferList, - TransferListItem, - useDocumentation -} from "@wso2is/react-components"; -import { FormValidation } from "@wso2is/validation"; -import { AxiosResponse } from "axios"; -import debounce, { DebouncedFunc } from "lodash-es/debounce"; -import isEmpty from "lodash-es/isEmpty"; -import kebabCase from "lodash-es/kebabCase"; -import React, { FormEvent, ReactElement, useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; -import { Divider, DropdownProps, Grid, Header } from "semantic-ui-react"; - -/** - * Proptypes for the add admin user basic component. - */ -interface AddAdminUserBasicProps extends IdentifiableComponentInterface { - triggerSubmit: boolean; - onSubmit: (values: any) => void; - administratorType: string; - setFinishButtonDisabled: (values: boolean) => void; -} - -/** - * Add admin user basic component. - * - * @returns add admin modal component. - */ -export const AddAdminUserBasic: React.FunctionComponent = ( - props: AddAdminUserBasicProps): ReactElement => { - - const { - administratorType, - triggerSubmit, - onSubmit, - setFinishButtonDisabled, - [ "data-componentid"]: componentId - } = props; - - const [ userRoleOptions, setUserRoleList ] = useState([]); - const [ rolesList, setRolesList ] = useState([]); - const [ usersList, setUsersList ] = useState([]); - const [ checkedAssignedListItems, setCheckedAssignedListItems ] = useState([]); - const [ isUserListRequestLoading, setUserListRequestLoading ] = useState(false); - const [ isUserRoleOptionsRequestLoading, setUserRoleOptionsRequestLoading ] = useState(false); - const [ listItemLimit, setListItemLimit ] = useState(0); - const [ userListMetaContent, setUserListMetaContent ] = useState(undefined); - const [ searchQuery, setSearchQuery ] = useState(""); - const [ listOffset ] = useState(0); - - const { t } = useTranslation(); - const { getLink } = useDocumentation(); - - const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = - useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); - const isInvitedAdminInConsoleSettingsEnabled: boolean = !consoleSettingsFeatureConfig?.disabledFeatures?.includes( - "consoleSettings.invitedExternalAdmins" - ); - - /** - * Retrieve the application data for the console application, filtering by name. - */ - const { data: applicationListData } = useApplicationList( - null, - null, - null, - `name eq ${ApplicationManagementConstants.CONSOLE_APP_NAME}` - ); - - /** - * Build the roles filter to search for roles specific to the console application. - */ - const roleSearchFilter: string = useMemo(() => { - if (applicationListData?.applications && applicationListData?.applications?.length > 0) { - return `audience.value eq ${applicationListData?.applications[0]?.id}`; - } - - return null; - }, [ applicationListData ]); - - // Username input validation error messages. - const USERNAME_REGEX_VIOLATION_ERROR_MESSAGE: string = t("users:guestUsers.fields." + - "username.validations.regExViolation"); - - const eventPublisher: EventPublisher = EventPublisher.getInstance(); - - useEffect(() => { - if (!roleSearchFilter) { - return; - } - - // Fetch users to select as internal admins. - if (administratorType === AdminAccountTypes.INTERNAL) { - setListItemLimit(UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT); - setUserListMetaContent(new Map([ - [ "name", "name" ], - [ "emails", "emails" ], - [ "roles", "roles" ], - [ "userName", "userName" ], - [ "id", "" ], - [ "profileUrl", "profileUrl" ], - [ "meta.lastModified", "meta.lastModified" ], - [ "meta.created", "" ] - ])); - } - - const roleOptions: DropdownProps[] = []; - - if (userRoleOptions.length === 0) { - setUserRoleOptionsRequestLoading(true); - getRolesList(null, roleSearchFilter) - .then((response: AxiosResponse) => { - setRolesList(response.data.Resources); - response.data.Resources.map((role: RolesInterface, index: number) => { - if(role.displayName === "system" || - role.displayName === "everyone" || - role.displayName === "selfsignup" - ) { - return; - } - - if ( - role.displayName?.split("/")?.length < 2 && - role.displayName?.split("/")[0] === "Application" - ) { - return; - } - - if (isInvitedAdminInConsoleSettingsEnabled || - ( - !isInvitedAdminInConsoleSettingsEnabled && role.meta?.systemRole - ) - - ){ - roleOptions?.push({ - key: index, - text: role?.displayName, - value: role?.displayName - }); - } - }); - setUserRoleList(roleOptions); - }).finally(() => { - setUserRoleOptionsRequestLoading(false); - }); - } - }, [ roleSearchFilter ]); - - useEffect(() => { - if (userListMetaContent) { - const attributes: string = generateAttributesString(userListMetaContent.values()); - - if (isEmpty(searchQuery)) { - setUsersList([]); - - return; - } - - getUserList(listItemLimit, listOffset, searchQuery, attributes, null); - } - }, [ listOffset, listItemLimit, searchQuery ]); - - useEffect(() => { - if (administratorType === AdminAccountTypes.INTERNAL) { - setFinishButtonDisabled(isEmpty(checkedAssignedListItems)); - } - }, [ checkedAssignedListItems ]); - - /** - * The following method accepts a Map and returns the values as a string. - * - * @param attributeMap - IterableIterator - * @returns attribute string - */ - const generateAttributesString = (attributeMap: IterableIterator) => { - const attArray: string[] = []; - const iterator1: IterableIterator = attributeMap[ Symbol.iterator ](); - - for (const attribute of iterator1) { - if (attribute !== "") { - attArray.push(attribute); - } - } - - return attArray.toString(); - }; - - const getUserList = (limit: number, offset: number, filter: string, attribute: string, userStore: string) => { - - setUserListRequestLoading(true); - - getUsersList(limit, offset, filter, attribute, userStore) - .then((response: UserListInterface) => { - // Exclude JIT users, internal admin users and collaborators. - const responseUsers: UserBasicInterface[] = response?.Resources?.filter( - (user: UserBasicInterface) => - !user[ SCIMConfigs.scim.enterpriseSchema ]?.userSourceId && - !isAdminUser(user) && - !isCollaboratorUser(user)); - - if (responseUsers) { - responseUsers.sort((userObject: UserBasicInterface, comparedUserObject: UserBasicInterface) => - userObject.name?.givenName?.localeCompare(comparedUserObject.name?.givenName) - ); - - setUsersList(responseUsers); - } else { - setUsersList([]); - } - }) - .finally(() => { - setUserListRequestLoading(false); - }); - }; - - const getFormValues = (values: Map): void => { - eventPublisher.publish("manage-users-collaborator-role", { - type: kebabCase(values.get("role").toString()) - }); - - const inviteUser: UserInviteInterface = { - email: values.get("email").toString(), - roles: [ values.get("role").toString() ] - }; - - if (triggerSubmit) { - onSubmit(inviteUser); - } - }; - - /** - * This is a debounced function to handle the user search by email address. - * Debounced to limit getUsers() api call. - * @param FormEvent - form event - * @param string - query - */ - const handleSearchFieldChange: DebouncedFunc<(e: FormEvent, query: string) => void> - = useCallback(debounce((e: FormEvent, query: string) => { - if (query === "") { - setSearchQuery(""); - } else { - const processedQuery: string = "emails co " + query; - - setSearchQuery(processedQuery); - } - }, 1000), []); - - const handleAssignedItemCheckboxChange = (user: UserBasicInterface) => { - const checkedUsers: UserBasicInterface[] = [ ...checkedAssignedListItems ]; - - if (checkedUsers.some((item: UserBasicInterface) => item.id === user.id)) { - checkedUsers.splice(checkedUsers.indexOf(user), 1); - setCheckedAssignedListItems(checkedUsers); - } else { - checkedUsers.push(user); - setCheckedAssignedListItems(checkedUsers); - } - }; - - const resolveListItemElement = (listItemValue: string) => { - - return ( - <> - { listItemValue } - - ); - }; - - const processInternalAdminFormData = (roleName: string): void => { - const selectedRoles: RolesInterface[] = rolesList?.filter( - (role: RolesInterface) => role.displayName == roleName); - const processedFormData: InternalAdminFormDataInterface = { - checkedUsers: checkedAssignedListItems, - selectedRoles: selectedRoles - }; - - if (triggerSubmit) { - onSubmit(processedFormData); - } - }; - - /** - * The modal to add new user internally. - */ - const inviteInternalUserForm = () => ( - processInternalAdminFormData(administratorConfig.adminRoleName) } - submitState={ triggerSubmit } - > - - { t("extensions:manage.users.wizard.addAdmin.internal.selectUser") } - - - { t("extensions:manage.users.wizard.addAdmin.internal.hint") } - - , - { value }: { value: string; }) => { - handleSearchFieldChange(e, value); - } } - data-componentid={ `${ componentId }-transfer-component` } - > - - { - usersList?.map((user: UserBasicInterface, index: number) => { - - const header: string = getUserNameWithoutDomain(user?.userName); - const subHeader: string = UserManagementUtils.resolveUserListSubheader(user); - - return ( - handleAssignedItemCheckboxChange(user) } - key={ index } - listItem={ { - listItemElement: resolveListItemElement(header), - listItemValue: subHeader - } } - listItemId={ user.id } - listItemIndex={ index } - isItemChecked={ checkedAssignedListItems.some((item: UserBasicInterface) => - item.id === user.id) } - showSecondaryActions={ false } - showListSubItem={ true } - listSubItem={ header !== subHeader && ( -
- - - { subHeader } - - -
- ) } - data-componentid={ `${ componentId }-unselected-transfer-list-item-${ index }` } - /> - ); - } ) - } -
-
-
- ); - - /** - * The modal to add new user externally. - */ - const inviteExternalUserForm = () => ( - ) => { - onSubmit(getFormValues(values)); - } } - submitState={ triggerSubmit } - > - { - (isUserRoleOptionsRequestLoading || - !userRoleOptions || - userRoleOptions?.length <= 0 - ) ? ( - - ) : ( - - - - { - // Check whether username is a valid email. - // check username validity against userstore regex - if (value && (!FormValidation.email(value) || !SharedUserStoreUtils - .validateInputAgainstRegEx(value, window["AppUtils"] - .getConfig().extensions.collaboratorUsernameRegex))) { - validation.isValid = false; - validation.errorMessages.push(USERNAME_REGEX_VIOLATION_ERROR_MESSAGE); - } - } } - type="email" - tabIndex={ 5 } - maxLength={ 50 } - /> - - - - - { - - } - - - { "Select a role to assign to the user." + - " The access level of the user is determined by the role." } - - { t("extensions:common.learnMore") } - - - - - - ) - } - - ); - - return ( - administratorType === AdminAccountTypes.INTERNAL - ? inviteInternalUserForm() - : inviteExternalUserForm() - ); -}; - -/** - * Default props for the component. - */ -AddAdminUserBasic.defaultProps = { - "data-componentid": "add-admin-user-basic" -}; diff --git a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx b/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx deleted file mode 100644 index ac968f0e0cd..00000000000 --- a/features/admin.console-settings.v1/components/console-administrators/add-external-admin-wizard/add-external-admin-wizard.tsx +++ /dev/null @@ -1,894 +0,0 @@ -/** - * Copyright (c) 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 { InternalAdminFormDataInterface } from "@wso2is/admin.administrators.v1/models"; -import { isAdminUser } from "@wso2is/admin.administrators.v1/utils/administrators"; -import { - AddAdminUserBasic -} from "@wso2is/admin.administrators.v1/wizard/steps/admin-user-basic"; -import { useApplicationList } from "@wso2is/admin.applications.v1/api"; -import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; -import { UserBasicInterface } from "@wso2is/admin.core.v1"; -import { administratorConfig } from "@wso2is/admin.extensions.v1/configs/administrator"; -import { updateRoleDetails, useRolesList } from "@wso2is/admin.roles.v2/api/roles"; -import { PatchRoleDataInterface } from "@wso2is/admin.roles.v2/models/roles"; -import { sendInvite, useUsersList } from "@wso2is/admin.users.v1/api"; -import { getUserWizardStepIcons } from "@wso2is/admin.users.v1/configs/ui"; -import { AdminAccountTypes, UserManagementConstants } from "@wso2is/admin.users.v1/constants/user-management-constants"; -import { UserInviteInterface } from "@wso2is/admin.users.v1/models"; -import { PRIMARY_USERSTORE } from "@wso2is/admin.userstores.v1/constants/user-store-constants"; -import { - AlertLevels, - IdentifiableComponentInterface, - RolesInterface, - TestableComponentInterface -} from "@wso2is/core/models"; -import { addAlert } from "@wso2is/core/store"; -import { useTrigger } from "@wso2is/forms"; -import { - ConfirmationModal, - GenericIconProps, - LinkButton, - PrimaryButton, - useWizardAlert -} from "@wso2is/react-components"; -import { AxiosError } from "axios"; -import intersection from "lodash-es/intersection"; -import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useDispatch } from "react-redux"; -import { Dispatch } from "redux"; -import { Grid, Icon, Modal } from "semantic-ui-react"; - -interface AddUserWizardPropsInterface extends IdentifiableComponentInterface, TestableComponentInterface { - closeWizard: () => void; - compact?: boolean; - currentStep?: number; - updateList?: () => void; - rolesList?: any; - emailVerificationEnabled: boolean; - requiredSteps?: WizardStepsFormTypes[] | string[]; - submitStep?: WizardStepsFormTypes | string; - showStepper?: boolean; - onSuccessfulUserAddition?: (id: string) => void; - onInvitationSendSuccessful?: () => void; - conditionallyShowStepper?: boolean; - adminTypeSelection? :string; - /** - * On user update callback. - * - * @param userId - ID of the updated user. - */ - onUserUpdate?: () => void; -} - -/** - * Interface for wizard step. - */ -interface WizardStepInterface { - content: ReactElement; - icon: GenericIconProps | any; - name: string; - title: string; -} - -/** - * Enum for wizard steps form types. - * @readonly - */ -export enum WizardStepsFormTypes { - BASIC_DETAILS = "BasicDetails" -} - -/** - * New Administrator creation wizard. - * - * @param props - Props injected to the component. - * @returns Add administrator wizard component. - */ -export const AddAdministratorWizard: FunctionComponent = ( - props: AddUserWizardPropsInterface -): ReactElement => { - - const { - adminTypeSelection, - closeWizard, - currentStep, - requiredSteps, - onInvitationSendSuccessful, - onUserUpdate, - [ "data-componentid" ]: componentId, - [ "data-testid" ]: testId - } = props; - - const { t } = useTranslation(); - const dispatch: Dispatch = useDispatch(); - - const [ submitGeneralSettings, setSubmitGeneralSettings ] = useTrigger(); - - const [ partiallyCompletedStep, setPartiallyCompletedStep ] = useState(undefined); - const [ currentWizardStep, setCurrentWizardStep ] = useState(currentStep); - const [ wizardSteps, setWizardSteps ] = useState([]); - const [ isStepsUpdated, setIsStepsUpdated ] = useState(false); - const [ isSubmitting, setIsSubmitting ] = useState(false); - const [ isFinishButtonDisabled, setFinishButtonDisabled ] = useState(false); - const [ searchQuery, setSearchQuery ] = useState(null); - const [ invite, setInvite ] = useState(null); - const [ showAdminRoleAddConfirmationModal, setShowAdminRoleAddConfirmationModal ] = useState(false); - const [ confirmationModalLoading, setConfirmationModalLoading ] = useState(false); - const [ adminRoleId, setAdminRoleId ] = useState(null); - const [ selectedUser, setSelectedUser ] = useState(null); - - const [ alert, setAlert, alertComponent ] = useWizardAlert(); - - // Excluding groups and roles from getUserList API call to improve performance. - const excludedAttributes: string = UserManagementConstants.GROUPS_ATTRIBUTE; - - const { - data: originalAdminUserList, - error: adminUserListFetchRequestError - } = useUsersList( - 1, - 0, - searchQuery, - null, - PRIMARY_USERSTORE, - excludedAttributes, - !!searchQuery - ); - - /** - * Retrieve the application data for the console application, filtering by name. - */ - const { - data: applicationListData , - error: applicationListFetchRequestError - } = useApplicationList( - null, - null, - null, - `name eq ${ApplicationManagementConstants.CONSOLE_APP_NAME}`, - !!searchQuery - ); - - /** - * Build the roles filter to search for roles specific to the console application. - */ - const roleSearchFilter: string = useMemo(() => { - if (applicationListData?.applications && applicationListData?.applications?.length > 0) { - return `audience.value eq ${applicationListData?.applications[0]?.id}`; - } - - return null; - }, [ applicationListData ]); - - /** - * Retrieve the roles list. - * Only retrieve roles when there an edge case where the user - * is already in the system and the user is not an admin user. - */ - const { - data: rolesList, - error: rolesListFetchRequestError - } = useRolesList( - null, - null, - roleSearchFilter, - null, - !!searchQuery - ); - - useEffect(() => { - setWizardSteps(filterSteps([ - WizardStepsFormTypes.BASIC_DETAILS ])); - setIsStepsUpdated(true); - }, []); - - /** - * This hook will check if the user is an admin user and prompt to add the admin role to the user. - */ - useEffect(() => { - if (!originalAdminUserList || !invite?.email) { - return; - } - - // If total results are 0, the user does not exists. - // This means there is a pending invitation for the user. - if (originalAdminUserList.totalResults <= 0) { - dispatch(addAlert({ - description: t( - "extensions:manage.invite.notifications.sendInvite.inviteAlreadyExistsError.description", - { userName: invite.email } - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.invite.notifications.sendInvite.inviteAlreadyExistsError.message" - ) - })); - setIsSubmitting(false); - closeWizard(); - - return; - } - - // Check if the user in the first index is with Administrator role. - // If an admin, show an error message. - if (isAdminUser(originalAdminUserList.Resources[ 0 ])) { - dispatch(addAlert({ - description: t( - "extensions:manage.invite.notifications.sendInvite.userAlreadyExistsError.description", - { userName: invite.email } - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.invite.notifications.sendInvite.userAlreadyExistsError.message" - ) - })); - setIsSubmitting(false); - closeWizard(); - } else { - // If not, prompt to assign the admin role to the user. - setIsSubmitting(false); - setShowAdminRoleAddConfirmationModal(true); - setSelectedUser(originalAdminUserList.Resources[ 0 ]); - } - }, [ originalAdminUserList ]); - - /** - * Dispatches error notifications for Admin user fetch request error. - */ - useEffect(() => { - if (!adminUserListFetchRequestError) { - return; - } - - if (adminUserListFetchRequestError?.response?.data?.description) { - dispatch(addAlert({ - description: adminUserListFetchRequestError?.response?.data?.description - ?? adminUserListFetchRequestError?.response?.data?.detail - ?? t("users:notifications.fetchUsers.error.description"), - level: AlertLevels.ERROR, - message: adminUserListFetchRequestError?.response?.data?.message - ?? t("users:notifications.fetchUsers.error.message") - })); - - return; - } - - dispatch(addAlert({ - description: t("users:notifications.fetchUsers.genericError." + - "description"), - level: AlertLevels.ERROR, - message: t("users:notifications.fetchUsers.genericError.message") - })); - }, [ adminUserListFetchRequestError ]); - - /** - * Dispatches error notifications for application list fetch request error. - */ - useEffect(() => { - if (!applicationListFetchRequestError) { - return; - } - - if (applicationListFetchRequestError.response - && applicationListFetchRequestError.response.data - && applicationListFetchRequestError.response.data.description) { - dispatch(addAlert({ - description: applicationListFetchRequestError.response.data.description, - level: AlertLevels.ERROR, - message: t("applications:notifications." + - "fetchApplications.error.message") - })); - - return; - } - - dispatch(addAlert({ - description: t("applications:notifications.fetchApplications" + - ".genericError.description"), - level: AlertLevels.ERROR, - message: t("applications:notifications." + - "fetchApplications.genericError.message") - })); - }, [ applicationListFetchRequestError ]); - - /** - * Dispatches error notifications for roles list fetch request error. - */ - useEffect(() => { - if (!rolesListFetchRequestError) { - return; - } - - if (rolesListFetchRequestError?.response?.data?.description) { - dispatch(addAlert({ - description: rolesListFetchRequestError?.response?.data?.description - ?? rolesListFetchRequestError?.response?.data?.detail - ?? t("roles:notifications.fetchRoles.error.description"), - level: AlertLevels.ERROR, - message: rolesListFetchRequestError?.response?.data?.message - ?? t("roles:notifications.fetchRoles.error.message") - })); - - return; - } - - dispatch(addAlert({ - description: t("roles:notifications.fetchRoles.genericError.description"), - level: AlertLevels.ERROR, - message: t("roles:notifications.fetchRoles.genericError.message") - })); - }, [ rolesListFetchRequestError ]); - - /** - * Get the administrator role from the roles list. - */ - useEffect(() => { - if (!rolesList || rolesList.Resources.length <= 0) { - return; - } - - const adminRole: RolesInterface = rolesList.Resources.find((role: RolesInterface) => - role.displayName === administratorConfig.adminRoleName); - - // If the admin role is not found, show an error message. - if (!adminRole) { - dispatch(addAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.noAdminRoleError.description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.noAdminRoleError.message" - ) - })); - } else { - // Else, set the admin role ID. - setAdminRoleId(adminRole?.id); - } - }, [ rolesList ]); - - /** - * 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(currentWizardStep - 1); - setPartiallyCompletedStep(undefined); - }, [ partiallyCompletedStep ]); - - /** - * Navigates to the next step. - */ - const navigateToNext = () => { - switch (wizardSteps[ currentWizardStep ]?.name) { - case WizardStepsFormTypes.BASIC_DETAILS: - setSubmitGeneralSettings(); - - break; - default: - break; - } - }; - - /** - * Navigates to the previous step. - */ - const navigateToPrevious = () => { - setPartiallyCompletedStep(currentWizardStep); - }; - - /** - * This function handles assigning the roles to the user. - */ - const assignUserRoles = async (users: UserBasicInterface[], roles: RolesInterface[]) => { - const roleIds: string[] = []; - const userList: {display: string, value: string}[] = []; - - if (users.length > 0) { - users.map((user: UserBasicInterface) => { - userList.push({ - display: user.userName, - value: user.id - }); - }); - } - - // Payload for the update role request. - const roleData: PatchRoleDataInterface = { - Operations: [ - { - op: "add", - value: { - users: userList - } - } - ], - schemas: [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] - }; - - if (roles.length > 0) { - roles.map((role: RolesInterface) => { - roleIds.push(role.id); - }); - - for (const roleId of roleIds) { - setIsSubmitting(true); - - await updateRoleDetails(roleId, roleData) - .catch((error: AxiosError) => { - if (!error.response || error.response.status === 401) { - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" - ) - }); - } else if (error.response && error.response.data && error.response.data.detail) { - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description", - { description: error.response.data.detail } - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" - ) - }); - } else { - // Generic error message - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError" + - ".description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.message" - ) - }); - } - }) - .finally(() => { - setIsSubmitting(false); - }); - } - dispatch(addAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.description" - ), - level: AlertLevels.SUCCESS, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.message" - ) - })); - closeWizard(); - onUserUpdate(); - } - }; - - /** - * Assigns the admin role to the user. - */ - const assignAdminRole = () => { - if (!selectedUser || !adminRoleId) { - return; - } - - // Payload for the update role request. - const roleData: PatchRoleDataInterface = { - Operations: [ - { - op: "add", - value: { - users: [ - { - display: selectedUser?.userName, - value: selectedUser?.id - } - ] - } - } - ], - schemas: [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ] - }; - - setConfirmationModalLoading(true); - - updateRoleDetails(adminRoleId, roleData) - .then(() => { - dispatch(addAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.description" - ), - level: AlertLevels.SUCCESS, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.message" - ) - })); - onUserUpdate(); - }) - .catch((error: AxiosError) => { - if (!error.response || error.response.status === 401) { - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" - ) - }); - } else if (error.response && error.response.data && error.response.data.detail) { - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description", - { description: error.response.data.detail } - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message" - ) - }); - } else { - // Generic error message - setAlert({ - description: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.message" - ) - }); - } - }) - .finally(() => { - setIsSubmitting(false); - setConfirmationModalLoading(false); - closeWizard(); - }); - }; - - /** - * This function handles sending the invitation to the external admin user. - */ - const sendExternalInvitation = (invite: UserInviteInterface) => { - if (invite != null) { - setIsSubmitting(true); - - sendInvite(invite) - .then(() => { - dispatch(addAlert({ - description: t( - "console:manage.features.invite.notifications.sendInvite.success.description" - ), - level: AlertLevels.SUCCESS, - message: t( - "console:manage.features.invite.notifications.sendInvite.success.message" - ) - })); - setIsSubmitting(false); - closeWizard(); - onInvitationSendSuccessful(); - }) - .catch((error: AxiosError) => { - // Axios throws a generic `Network Error` for 401 status. - // As a temporary solution, a check to see if a response - // is available has be used. - if (!error.response || error.response.status === 401) { - setIsSubmitting(false); - closeWizard(); - dispatch(addAlert({ - description: t( - "console:manage.features.invite.notifications.sendInvite.error.description" - ), - level: AlertLevels.ERROR, - message: t( - "console:manage.features.invite.notifications.sendInvite.error.message" - ) - })); - } else if (error.response.status === 403 && - error?.response?.data?.code === UserManagementConstants.ERROR_COLLABORATOR_USER_LIMIT_REACHED) { - setIsSubmitting(false); - closeWizard(); - dispatch(addAlert({ - description: t( - "extensions:manage.invite.notifications.sendInvite.limitReachError.description" - ), - level: AlertLevels.ERROR, - message: t( - "extensions:manage.invite.notifications.sendInvite.limitReachError.message" - ) - })); - } else if (error.response.status === 409) { - // User already exists in the system. - // We need to check if this user is having the Administator role. - handleAlredyExistingUser(invite); - } else if (error?.response?.data?.description) { - setIsSubmitting(false); - closeWizard(); - dispatch(addAlert({ - description: t( - "console:manage.features.invite.notifications.sendInvite.error.description", - { description: error.response.data.description } - ), - level: AlertLevels.ERROR, - message: t( - "console:manage.features.invite.notifications.sendInvite.error.message" - ) - })); - } else { - setIsSubmitting(false); - closeWizard(); - // Generic error message - dispatch(addAlert({ - description: t( - "console:manage.features.invite.notifications.sendInvite.genericError.description" - ), - level: AlertLevels.ERROR, - message: t( - "console:manage.features.invite.notifications.sendInvite.genericError.message" - ) - })); - } - }); - } - }; - - /** - * Handles the already existing user. - * - * @param error - Axios error response. - */ - const handleAlredyExistingUser = (invite: UserInviteInterface): void => { - if (!invite?.email) { - setIsSubmitting(false); - - return; - } - - if (!selectedUser || selectedUser?.userName !== invite.email) { - // If the user is not selected. Query the user. - setSearchQuery(`userName eq ${ invite.email }`); - setInvite(invite); - } else { - // If the user is already selected, prompt to assign the admin role. - // Handles cancelling and re-opening the modal. - setShowAdminRoleAddConfirmationModal(true); - setIsSubmitting(false); - } - }; - - /** - * This function handles sending the invitation to the external admin user. - */ - const sendInternalInvitation = (data: InternalAdminFormDataInterface) => { - assignUserRoles(data?.checkedUsers, data?.selectedRoles); - }; - - /** - * Resolves the step content. - * - * @returns Step content. - */ - const resolveStepContent = (): ReactElement => { - switch (wizardSteps[ currentWizardStep ]?.name) { - case WizardStepsFormTypes.BASIC_DETAILS: - return getAdminBasicDetailsWizardStep()?.content; - } - }; - - /** - * Filters the steps evaluating the requested steps. - * - * @param steps - Steps to filter. - * @returns Filtered 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(getAdminBasicDetailsWizardStep()); - } - }); - - return filteredSteps; - }; - - if (!requiredSteps) { - return getStepContent(steps); - } - - return getStepContent(intersection(steps, requiredSteps)); - }; - - /** - * Basic Wizard Step. - * @returns Basic details wizard step. - */ - const getAdminBasicDetailsWizardStep = (): WizardStepInterface => { - - return { - content: ( - - (adminTypeSelection === AdminAccountTypes.INTERNAL) - ? sendInternalInvitation(values as InternalAdminFormDataInterface) - : sendExternalInvitation(values as UserInviteInterface) - } - setFinishButtonDisabled={ setFinishButtonDisabled } - /> - ), - icon: getUserWizardStepIcons().general, - name: WizardStepsFormTypes.BASIC_DETAILS, - title: t("console:manage.features.user.modals.addUserWizard.steps.basicDetails") - }; - }; - - return ( - wizardSteps && isStepsUpdated ? ( - <> - - - { adminTypeSelection === AdminAccountTypes.INTERNAL - ? t("extensions:manage.users.wizard.addAdmin.internal.title") - : t("extensions:manage.users.wizard.addAdmin.external.title") - } - - - { alert && alertComponent } - { resolveStepContent() } - - - - - - closeWizard() } - > - { t("common:cancel") } - - - - { (currentWizardStep < wizardSteps?.length - 1) && ( - - { t("console:manage.features.user.modals.addUserWizard.buttons.next") } - - - ) } - { currentWizardStep === wizardSteps?.length - 1 && ( - - { - adminTypeSelection === AdminAccountTypes.INTERNAL - ? t("extensions:manage.features.user.addUser.add") - : t("extensions:manage.features.user.addUser.invite") - } - - ) } - { (wizardSteps?.length > 1 && currentWizardStep > 0) && ( - - - { t("console:manage.features.user.modals.addUserWizard.buttons.previous") } - - ) } - - - - - - { - selectedUser && adminRoleId && ( - { - setSubmitGeneralSettings(); - setShowAdminRoleAddConfirmationModal(false); - } } - type="warning" - open={ showAdminRoleAddConfirmationModal } - assertionHint={ - t("extensions:manage.invite.assignAdminUser.confirmationModal.assertionHint") } - assertionType="checkbox" - primaryAction="Confirm" - secondaryAction="Cancel" - onSecondaryActionClick={ (): void => { - setSubmitGeneralSettings(); - setShowAdminRoleAddConfirmationModal(false); - setAlert(null); - } } - onPrimaryActionClick={ (): void => { - setConfirmationModalLoading(true); - assignAdminRole(); - } } - closeOnDimmerClick={ false } - > - - { t("extensions:manage.invite.assignAdminUser.confirmationModal.header") } - - - { t("extensions:manage.invite.assignAdminUser.confirmationModal.message") } - - - ) - } - - ) : null - ); -}; - -/** - * Default props for the add administrator wizard. - */ -AddAdministratorWizard.defaultProps = { - compact: false, - conditionallyShowStepper: false, - currentStep: 0, - emailVerificationEnabled: false, - showStepper: true, - submitStep: WizardStepsFormTypes.BASIC_DETAILS -}; diff --git a/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx b/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx index 871bbfbf18b..f3f61157899 100644 --- a/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/administrators-list/administrators-list.tsx @@ -26,6 +26,7 @@ import { import { useOrganizationConfigV2 } from "@wso2is/admin.administrators.v1/api/useOrganizationConfigV2"; import { AdministratorConstants } from "@wso2is/admin.administrators.v1/constants/users"; import { UseOrganizationConfigType } from "@wso2is/admin.administrators.v1/models/organization"; +import { AddAdministratorWizard } from "@wso2is/admin.administrators.v1/wizard/add-administrator-wizard"; import { AdvancedSearchWithBasicFilters, AppConstants, @@ -63,7 +64,6 @@ import AdministratorsTable from "./administrators-table"; import useAdministrators from "../../../hooks/use-administrators"; import useBulkAssignAdministratorRoles from "../../../hooks/use-bulk-assign-user-roles"; import AddExistingUserWizard from "../add-existing-user-wizard/add-existing-user-wizard"; -import { AddAdministratorWizard } from "../add-external-admin-wizard/add-external-admin-wizard"; import InviteNewAdministratorWizard from "../invite-new-administrator-wizard/invite-new-administrator-wizard"; /** From 9e14452f58f7a546b954ce6f5bb84c9bd6376e26 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Sun, 13 Oct 2024 17:38:35 +0530 Subject: [PATCH 7/8] Add i18n translations for organization name --- .../console-administrators/console-administrators.tsx | 7 +++++-- modules/i18n/src/models/namespaces/common-ns.ts | 1 + modules/i18n/src/translations/de-DE/portals/common.ts | 1 + modules/i18n/src/translations/en-US/portals/common.ts | 1 + modules/i18n/src/translations/es-ES/portals/common.ts | 1 + modules/i18n/src/translations/fr-FR/portals/common.ts | 1 + modules/i18n/src/translations/ja-JP/portals/common.ts | 1 + modules/i18n/src/translations/pt-BR/portals/common.ts | 1 + modules/i18n/src/translations/pt-PT/portals/common.ts | 1 + modules/i18n/src/translations/si-LK/portals/common.ts | 1 + modules/i18n/src/translations/ta-IN/portals/common.ts | 1 + modules/i18n/src/translations/zh-CN/portals/common.ts | 1 + 12 files changed, 16 insertions(+), 2 deletions(-) diff --git a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx index 4bd2cdc1e7d..a618b3e9287 100644 --- a/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx +++ b/features/admin.console-settings.v1/components/console-administrators/console-administrators.tsx @@ -36,6 +36,7 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import AdministratorsList from "./administrators-list/administrators-list"; import InvitedAdministratorsList from "./invited-administrators/invited-administrators-list"; @@ -57,6 +58,7 @@ const ConsoleAdministrators: FunctionComponent = const { [ "data-componentid" ]: componentId } = props; const { isFirstLevelOrganization, isSubOrganization } = useGetCurrentOrganizationType(); + const { t } = useTranslation(); const consoleSettingsFeatureConfig: FeatureAccessConfigInterface = useSelector((state: AppState) => state?.config?.ui?.features?.consoleSettings); @@ -71,6 +73,7 @@ const ConsoleAdministrators: FunctionComponent = const [ isEnterpriseLoginEnabled, setIsEnterpriseLoginEnabled ] = useState(false); const organizationName: string = store.getState().auth.tenantDomain; + const productName: string = useSelector((state: AppState) => state.config.ui.productName); const useOrgConfig: UseOrganizationConfigType = useOrganizationConfigV2; @@ -186,11 +189,11 @@ const ConsoleAdministrators: FunctionComponent = value={ activeAdministratorGroup } onChange={ (_: ChangeEvent, value: string) => setActiveAdministratorGroup(value) } > - } label="Asgardeo" /> + } label={ productName } /> } - label={ organizationName + " organization" } + label={ t("common:organizationName", { orgName: organizationName }) } /> ); diff --git a/modules/i18n/src/models/namespaces/common-ns.ts b/modules/i18n/src/models/namespaces/common-ns.ts index ece16adc9b1..153e86b59d1 100644 --- a/modules/i18n/src/models/namespaces/common-ns.ts +++ b/modules/i18n/src/models/namespaces/common-ns.ts @@ -112,6 +112,7 @@ export interface CommonNS { name: string; new: string; next: string; + organizationName: string; operatingSystem: string; operations: string; overview: string; diff --git a/modules/i18n/src/translations/de-DE/portals/common.ts b/modules/i18n/src/translations/de-DE/portals/common.ts index 0911a13a1db..d35df713e2f 100644 --- a/modules/i18n/src/translations/de-DE/portals/common.ts +++ b/modules/i18n/src/translations/de-DE/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { "okay": "Okay", "operatingSystem": "Betriebssystem", "operations": "Betrieb", + "organizationName": "{{orgName}} Organisation", "overview": "Überblick", "personalInfo": "Persönliche Informationen", "pin": "PIN", diff --git a/modules/i18n/src/translations/en-US/portals/common.ts b/modules/i18n/src/translations/en-US/portals/common.ts index af966decbdc..366249b93dd 100755 --- a/modules/i18n/src/translations/en-US/portals/common.ts +++ b/modules/i18n/src/translations/en-US/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { okay: "Okay", operatingSystem: "Operating system", operations: "Operations", + organizationName: "{{orgName}} organization", overview: "Overview", personalInfo: "Personal Info", pin: "Pin", diff --git a/modules/i18n/src/translations/es-ES/portals/common.ts b/modules/i18n/src/translations/es-ES/portals/common.ts index cd8d42e8c5a..618e363ce42 100644 --- a/modules/i18n/src/translations/es-ES/portals/common.ts +++ b/modules/i18n/src/translations/es-ES/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { okay: "Okey", operatingSystem: "Sistema operativo", operations: "Operaciones", + organizationName: "organización {{orgName}}", overview: "Descripción general", personalInfo: "Información personal", pin: "Alfiler", diff --git a/modules/i18n/src/translations/fr-FR/portals/common.ts b/modules/i18n/src/translations/fr-FR/portals/common.ts index 9a9f94ec193..1a7e67cdbae 100755 --- a/modules/i18n/src/translations/fr-FR/portals/common.ts +++ b/modules/i18n/src/translations/fr-FR/portals/common.ts @@ -132,6 +132,7 @@ export const common: CommonNS = { okay: "d'accord", operatingSystem: "Système d'exploitation", operations: "Opérations", + organizationName: "{{orgName}} organisation", overview: "Vue d'ensemble", personalInfo: "Informations personnelles", pin: "Épingler", diff --git a/modules/i18n/src/translations/ja-JP/portals/common.ts b/modules/i18n/src/translations/ja-JP/portals/common.ts index 72559214a7b..83b7dd3003f 100644 --- a/modules/i18n/src/translations/ja-JP/portals/common.ts +++ b/modules/i18n/src/translations/ja-JP/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { "okay": "わかった", "operatingSystem": "オペレーティング·システム", "operations": "オペレーション", + "organizationName": "{{orgName}} 組織", "overview": "概要", "personalInfo": "個人情報", "pin": "ピン", diff --git a/modules/i18n/src/translations/pt-BR/portals/common.ts b/modules/i18n/src/translations/pt-BR/portals/common.ts index b8024c105e0..63ac8afe13a 100755 --- a/modules/i18n/src/translations/pt-BR/portals/common.ts +++ b/modules/i18n/src/translations/pt-BR/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { okay: "Ok", operatingSystem: "Sistema operacional", operations: "Operações", + organizationName: "Organização {{orgName}}", overview: "Visão Geral", personalInfo: "Informação Pessoal", pin: "Fixar", diff --git a/modules/i18n/src/translations/pt-PT/portals/common.ts b/modules/i18n/src/translations/pt-PT/portals/common.ts index 3896e003e16..cf39b4cb18c 100755 --- a/modules/i18n/src/translations/pt-PT/portals/common.ts +++ b/modules/i18n/src/translations/pt-PT/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { okay: "OK", operatingSystem: "Sistema operacional", operations: "Operações", + organizationName: "Organização {{orgName}}", overview: "visão global", personalInfo: "Informação pessoal", pin: "Bastão", diff --git a/modules/i18n/src/translations/si-LK/portals/common.ts b/modules/i18n/src/translations/si-LK/portals/common.ts index 6a474be30c6..b4e3aa1be2a 100755 --- a/modules/i18n/src/translations/si-LK/portals/common.ts +++ b/modules/i18n/src/translations/si-LK/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { okay: "හරි", operatingSystem: "මෙහෙයුම් පද්ධතිය", operations: "මෙහෙයුම්", + organizationName: "{{orgName}} සංවිධානය", overview: "දළ විශ්ලේෂණය", personalInfo: "පෞද්ගලික තොරතුරු", pin: "තබා ගන්න", diff --git a/modules/i18n/src/translations/ta-IN/portals/common.ts b/modules/i18n/src/translations/ta-IN/portals/common.ts index 38fd648748f..f783fe59cb1 100755 --- a/modules/i18n/src/translations/ta-IN/portals/common.ts +++ b/modules/i18n/src/translations/ta-IN/portals/common.ts @@ -132,6 +132,7 @@ export const common: CommonNS = { okay: "சரி", operatingSystem: "இயங்கு தளம்", operations: "செயற்பாடுகள்", + organizationName: "{{orgName}} நிறுவனம்", overview: "கண்ணோட்டம்", personalInfo: "பயனர் விபரம்", pin: "பொருத்து", diff --git a/modules/i18n/src/translations/zh-CN/portals/common.ts b/modules/i18n/src/translations/zh-CN/portals/common.ts index b7fcd679a5a..d7e0e806c76 100755 --- a/modules/i18n/src/translations/zh-CN/portals/common.ts +++ b/modules/i18n/src/translations/zh-CN/portals/common.ts @@ -131,6 +131,7 @@ export const common: CommonNS = { "okay": "好的", "operatingSystem": "操作系统", "operations": "操作", + "organizationName": "{{orgName}} 组织", "overview": "概述", "personalInfo": "个人信息", "pin": "别针", From 7589832361361877ba713f1b9b6e1d0fbe636a57 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Mon, 14 Oct 2024 10:24:06 +0530 Subject: [PATCH 8/8] Use constants for form IDs --- features/admin.administrators.v1/constants/users.ts | 4 ++++ .../wizard/steps/admin-user-basic.tsx | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/features/admin.administrators.v1/constants/users.ts b/features/admin.administrators.v1/constants/users.ts index 405232223e5..5337456432b 100644 --- a/features/admin.administrators.v1/constants/users.ts +++ b/features/admin.administrators.v1/constants/users.ts @@ -64,6 +64,10 @@ export class AdministratorConstants { // Timeout for the debounce function. public static readonly DEBOUNCE_TIMEOUT: number = 1000; + public static readonly INVITE_INTERNAL_USER_FORM_ID: string = "inviteInternalUserForm"; + + public static readonly INVITE_EXTERNAL_USER_FORM_ID: string = "inviteExternalUserForm"; + /** * Get the consumer users paths as a map. * diff --git a/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx b/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx index 9f012156a33..434d6208af9 100644 --- a/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx +++ b/features/admin.administrators.v1/wizard/steps/admin-user-basic.tsx @@ -57,6 +57,7 @@ import React, { FormEvent, ReactElement, useCallback, useEffect, useMemo, useSta import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Divider, DropdownProps, Grid, Header } from "semantic-ui-react"; +import { AdministratorConstants } from "../../constants"; import { InternalAdminFormDataInterface } from "../../models"; import { isAdminUser, isCollaboratorUser } from "../../utils/administrators"; @@ -219,8 +220,8 @@ export const AddAdminUserBasic: React.FunctionComponent document ?.getElementById( administratorType === AdminAccountTypes.INTERNAL - ? "inviteInternalUserForm" - : "inviteExternalUserForm" + ? AdministratorConstants.INVITE_INTERNAL_USER_FORM_ID + : AdministratorConstants.INVITE_EXTERNAL_USER_FORM_ID )?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); }, [ triggerSubmit ]);