diff --git a/.changeset/large-chefs-fail.md b/.changeset/large-chefs-fail.md new file mode 100644 index 00000000000..b46fc6ad8b1 --- /dev/null +++ b/.changeset/large-chefs-fail.md @@ -0,0 +1,8 @@ +--- +"@wso2is/admin.console-settings.v1": patch +"@wso2is/admin.claims.v1": patch +"@wso2is/admin.users.v1": patch +"@wso2is/console": patch +--- + +Refactor immutable attribute configuration view and improve administrator listing in the console settings page diff --git a/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx b/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx index cbafa436dc8..ca22fdbde66 100644 --- a/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx +++ b/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx @@ -96,7 +96,7 @@ import { Dispatch } from "redux"; import { Divider, Grid, Icon, Form as SemanticForm } from "semantic-ui-react"; import { deleteAClaim, getExternalClaims, updateAClaim } from "../../../api"; import useGetClaimDialects from "../../../api/use-get-claim-dialects"; -import { ClaimFeatureDictionaryKeys, ClaimManagementConstants } from "../../../constants"; +import { ClaimManagementConstants } from "../../../constants"; import "./edit-basic-details-local-claims.scss"; /** @@ -122,6 +122,14 @@ const READONLY_CLAIM_CONFIGS: string[] = [ ClaimManagementConstants.APPLICATION_ROLES_CLAIM_URI ]; +// Immutable claims that have default labels and display order. +const IMMUTABLE_CLAIMS_WITH_DEFAULT_LABELS: string[] = [ + ClaimManagementConstants.USER_ID_CLAIM_URI, + ClaimManagementConstants.USER_NAME_CLAIM_URI, + ClaimManagementConstants.CREATED_CLAIM_URI, + ClaimManagementConstants.MODIFIED_CLAIM_URI +]; + /** * This component renders the Basic Details pane of the edit local claim screen * @@ -166,11 +174,6 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - if (hideSpecialClaims || isAgentAttribute) { + if (isAgentAttribute) { return true; } else { return !hasAttributeUpdatePermissions; } - }, [ featureConfig, allowedScopes, hideSpecialClaims ]); + }, [ featureConfig, allowedScopes, isAgentAttribute ]); const deleteConfirmation = (): ReactElement => ( { - - // If hideUserIdDisplayConfigurations is true, disable the checkbox for user id claim. - // Refer issue - https://github.com/wso2/product-is/issues/24906 - if (!hideUserIdDisplayConfigurations && claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI) { - // Disable only the admin console checkbox as the support is only tested in Admin Console for now. - // Further supported can be given under effort tracked - // with issue - https://github.com/wso2/product-is/issues/25482 - if (isAdminConsole) { - return false; - } + const isSupportedByDefaultCheckboxDisabled = ( + isAdminConsole?: boolean, + isSelfRegistration?: boolean + ): boolean => { + // For immutable (special) claims, disable self-registration as they are created after user creation + if (isSelfRegistration && hideSpecialClaims) { + return true; + } + + if (claim?.claimURI === ClaimManagementConstants.USER_NAME_CLAIM_URI) { + return true; + } + + if ((claim?.claimURI === ClaimManagementConstants.CREATED_CLAIM_URI + || claim?.claimURI === ClaimManagementConstants.MODIFIED_CLAIM_URI) && isAdminConsole) { + return true; + } + + if (claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI) { return true; } @@ -903,7 +924,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent @@ -1058,7 +1081,8 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - const isRequiredCheckboxDisabled: boolean = isReadOnly || isSubOrganization() || !hasMapping + const isRequiredCheckboxDisabled: boolean = hideSpecialClaims || isReadOnly || isSubOrganization() + || !hasMapping || dataType === ClaimDataType.COMPLEX || ( accountVerificationEnabled @@ -1092,7 +1116,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent @@ -1140,8 +1166,8 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - const isReadOnlyCheckboxDisabled: boolean = isReadOnly || isSubOrganization() || !hasMapping - || dataType === ClaimDataType.COMPLEX; + const isReadOnlyCheckboxDisabled: boolean = hideSpecialClaims || isReadOnly || isSubOrganization() + || !hasMapping || dataType === ClaimDataType.COMPLEX; return ( @@ -1161,7 +1187,9 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { @@ -1173,7 +1201,9 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { @@ -1187,7 +1217,9 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { setIsSelfRegistrationReadOnly(value); @@ -1212,14 +1244,9 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - // If we are not using default labels and order, the display name field is editable for - // user ID and username claims. + // The following claims are editable if default labels and order are not used. // Refer issue - https://github.com/wso2/product-is/issues/24906 - if (!useDefaultLabelsAndOrder && (claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI - || claim?.claimURI === ClaimManagementConstants.USER_NAME_CLAIM_URI)) { - return false; - } - if (claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI) { + if (useDefaultLabelsAndOrder && IMMUTABLE_CLAIMS_WITH_DEFAULT_LABELS.includes(claim?.claimURI)) { return true; } @@ -1233,15 +1260,8 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - // Show for user ID claim when hideUserIdDisplayConfigurations is false. - // Refer issue - https://github.com/wso2/product-is/issues/24906 - if (!hideUserIdDisplayConfigurations && claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI) { - return true; - } - return !isDistinctAttributeProfilesDisabled && claim && - !hideSpecialClaims && mappingChecked && claim.claimURI !== ClaimManagementConstants.GROUPS_CLAIM_URI && !isAgentAttribute; @@ -1254,19 +1274,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { - // Show for username and user ID claims when useDefaultLabelsAndOrder is false. - // Refer issue - https://github.com/wso2/product-is/issues/24906 - if (!useDefaultLabelsAndOrder && (claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI - || claim?.claimURI === ClaimManagementConstants.USER_NAME_CLAIM_URI)) { - return true; - } - - // Show for user ID claim when hideUserIdDisplayConfigurations is false. - if (!hideUserIdDisplayConfigurations && claim?.claimURI === ClaimManagementConstants.USER_ID_CLAIM_URI) { - return true; - } - - return !hideSpecialClaims && !isSubOrganization(); + return !isSubOrganization(); }; return ( @@ -1335,10 +1343,7 @@ export const EditBasicDetailsLocalClaims: FunctionComponent { // Hides on user_id, username claims - !READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) - && claim.claimURI !== ClaimManagementConstants.USER_NAME_CLAIM_URI + (!READONLY_CLAIM_CONFIGS.includes(claim?.claimURI)) && resolveAttributeSupportedByDefaultRow() } { - !READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) - && claim.claimURI !== ClaimManagementConstants.USER_ID_CLAIM_URI - && attributeConfig.editAttributes.showRequiredCheckBox + // Show for system claims (hideSpecialClaims=true) + (!READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) + && attributeConfig.editAttributes.showRequiredCheckBox) && resolveAttributeRequiredRow() } { // Hides on user_id, username and email claims - !READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) - && claim.claimURI !== ClaimManagementConstants.USER_ID_CLAIM_URI - && claim.claimURI !== ClaimManagementConstants.USER_NAME_CLAIM_URI - && claim.claimURI !== ClaimManagementConstants.EMAIL_CLAIM_URI + (!READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) + && claim.claimURI !== ClaimManagementConstants.EMAIL_CLAIM_URI) && resolveAttributeReadOnlyRow() } diff --git a/features/admin.claims.v1/constants/claim-management-constants.ts b/features/admin.claims.v1/constants/claim-management-constants.ts index c2aa85ee417..1d8cb0c0398 100644 --- a/features/admin.claims.v1/constants/claim-management-constants.ts +++ b/features/admin.claims.v1/constants/claim-management-constants.ts @@ -229,6 +229,8 @@ export class ClaimManagementConstants { */ public static readonly USER_ID_CLAIM_URI: string = "http://wso2.org/claims/userid"; public static readonly USER_NAME_CLAIM_URI: string = "http://wso2.org/claims/username"; + public static readonly CREATED_CLAIM_URI: string = "http://wso2.org/claims/created"; + public static readonly MODIFIED_CLAIM_URI: string = "http://wso2.org/claims/modified"; public static readonly GROUPS_CLAIM_URI: string = "http://wso2.org/claims/groups"; public static readonly ROLES_CLAIM_URI: string = "http://wso2.org/claims/roles"; public static readonly APPLICATION_ROLES_CLAIM_URI: string = "http://wso2.org/claims/applicationRoles"; diff --git a/features/admin.console-settings.v1/hooks/use-administrators.ts b/features/admin.console-settings.v1/hooks/use-administrators.ts index de7dab5eafa..cdc1d95904a 100644 --- a/features/admin.console-settings.v1/hooks/use-administrators.ts +++ b/features/admin.console-settings.v1/hooks/use-administrators.ts @@ -16,7 +16,7 @@ * under the License. */ -import { UserBasicInterface, UserListInterface, UserRoleInterface } from "@wso2is/admin.core.v1/models/users"; +import { UserBasicInterface, UserListInterface } from "@wso2is/admin.core.v1/models/users"; import { AppState } from "@wso2is/admin.core.v1/store"; import { SCIMConfigs } from "@wso2is/admin.extensions.v1/configs/scim"; import { useGetCurrentOrganizationType } from "@wso2is/admin.organizations.v1/hooks/use-get-organization-type"; @@ -26,13 +26,13 @@ import { useGetParentOrgUserInvites } import { InvitationsInterface } from "@wso2is/admin.users.v1/components/guests/models/invite"; import { UserAccountTypes } from "@wso2is/admin.users.v1/constants/user-management-constants"; import { UserManagementUtils } from "@wso2is/admin.users.v1/utils/user-management-utils"; -import { MultiValueAttributeInterface, RolesInterface } from "@wso2is/core/models"; +import { MultiValueAttributeInterface } from "@wso2is/core/models"; import { AxiosError } from "axios"; import cloneDeep from "lodash-es/cloneDeep"; import isEmpty from "lodash-es/isEmpty"; import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import useConsoleRoles from "./use-console-roles"; +import useConsoleSettings from "./use-console-settings"; /** * Props interface of {@link UseAdministrators} @@ -103,6 +103,24 @@ const useAdministrators = ( const { isSubOrganization } = useGetCurrentOrganizationType(); + const { consoleId } = useConsoleSettings(); + + // Build the filter to fetch only users with console roles + const adminUsersFilter: string = useMemo(() => { + if (!consoleId) { + return null; + } + + const roleFilter: string = `roles.audienceId eq ${consoleId}`; + + // Combine with any additional filter if provided + if (filter && filter !== "") { + return `${roleFilter} and ${filter}`; + } + + return roleFilter; + }, [ consoleId, filter ]); + const { data: originalAdminUserList, error: adminUserListFetchError, @@ -111,11 +129,11 @@ const useAdministrators = ( } = useUsersList( modifiedLimit, startIndex + 1, - filter === "" ? null : filter, + adminUsersFilter, attributes, domain, excludedAttributes, - shouldFetch + shouldFetch && !!consoleId ); const { @@ -123,8 +141,6 @@ const useAdministrators = ( mutate: mutateInvitedAdministratorsListFetchRequest } = useGetParentOrgUserInvites(isSubOrganization()); - const { consoleRoles } = useConsoleRoles(null, null); - /** * Transform the original users list response from the API. * @@ -139,26 +155,13 @@ const useAdministrators = ( const clonedUserList: UserListInterface = cloneDeep(usersList); const processedUserList: UserBasicInterface[] = []; - /** - * Checks whether administrator role is present in the user. - */ - const isAdminUser = (user: UserBasicInterface): boolean => { - return user?.roles?.some((userRole: UserRoleInterface) => { - return consoleRoles?.Resources?.some((consoleRole: RolesInterface) => { - return consoleRole.id === userRole.value; - }); - }); - }; - const isOwner = (user: UserBasicInterface): boolean => { return user[SCIMConfigs.scim.systemSchema]?.userAccountType === UserAccountTypes.OWNER; }; clonedUserList.Resources = clonedUserList?.Resources?.map((resource: UserBasicInterface) => { - // Filter out users belong to groups named "Administrator" - if (!isAdminUser(resource)) { - return null; - } + // Since we're already filtering by console roles at the API level, + // all returned users are administrators. We just need to sort them properly. if (isOwner(resource) && UserManagementUtils.isAuthenticatedUser(authenticatedUser, resource?.userName)) { processedUserList[0] = resource; @@ -234,12 +237,12 @@ const useAdministrators = ( }; const administrators: UserListInterface = useMemo(() => { - if (isEmpty(originalAdminUserList) || isEmpty(consoleRoles)) { + if (isEmpty(originalAdminUserList)) { return {}; } return transformUserList(originalAdminUserList); - }, [ originalAdminUserList, consoleRoles ]); + }, [ originalAdminUserList ]); return { adminUserListFetchError, diff --git a/features/admin.users.v1/components/user-profile/user-profile-form.tsx b/features/admin.users.v1/components/user-profile/user-profile-form.tsx index 0a021ce8835..b6b2f6363fc 100644 --- a/features/admin.users.v1/components/user-profile/user-profile-form.tsx +++ b/features/admin.users.v1/components/user-profile/user-profile-form.tsx @@ -816,6 +816,13 @@ const UserProfileForm: FunctionComponent = ({ return false; } + // Filter out meta fields (userId, created, modified) as they are already rendered separately + if (schema.name === "id" || + schema.name === "meta.created" || + schema.name === "meta.lastModified") { + return false; + } + if (schema.schemaUri === ProfileConstants.SCIM2_CORE_USER_SCHEMA_ATTRIBUTES.emails && !commonExtensionConfig?.userEditSection?.showEmail) { return false; @@ -885,6 +892,33 @@ const UserProfileForm: FunctionComponent = ({ return true; }; + /** + * Get the display name for a schema field. + * If useDefaultLabelsAndOrder is enabled, returns the translated fallback key. + * Otherwise, attempts to find the schema and return its displayName. + * Falls back to the translated key or schemaName if schema is not found. + * + * @param schemaName - The schema name to look up. + * @param fallbackI18nKey - Optional i18n key to use as fallback. + * @returns The display name for the schema field. + */ + const getSchemaDisplayName = (schemaName: string, fallbackI18nKey?: string): string => { + + if (useDefaultLabelsAndOrder && fallbackI18nKey) { + return t(fallbackI18nKey); + } + + const schema: ProfileSchemaInterface = flattenedProfileSchema.find( + (s: ProfileSchemaInterface) => s.name === schemaName + ); + + if (!schema) { + return fallbackI18nKey ? t(fallbackI18nKey) : schemaName; + } + + return schema.displayName; + }; + return ( = ({ key="userID" component={ TextFieldAdapter } initialValue={ profileData?.id } - label={ t("user:profile.fields.userId") } + label={ getSchemaDisplayName("id", "user:profile.fields.userId") } ariaLabel="userID" name="userID" type="text" @@ -967,7 +1001,10 @@ const UserProfileForm: FunctionComponent = ({ = ({ = ( let filterAttributeOptions: DropdownChild[] = [ { key: 0, - text: t("users:advancedSearch.form.dropdown." + "filterAttributeOptions.username"), + text: t("users:advancedSearch.form.dropdown.filterAttributeOptions.username"), value: "userName" } ]; + // Add created time and modified time + const createdTimeOption: DropdownChild = { + key: "meta.created", + text: t("users:advancedSearch.form.dropdown.filterAttributeOptions.createdTime"), + value: "urn:ietf:params:scim:schemas:core:2.0:meta.created" + }; + + const modifiedTimeOption: DropdownChild = { + key: "meta.lastModified", + text: t("users:advancedSearch.form.dropdown.filterAttributeOptions.modifiedTime"), + value: "urn:ietf:params:scim:schemas:core:2.0:meta.lastModified" + }; + + const userIdOption: DropdownChild = { + key: "id", + text: t("users:advancedSearch.form.dropdown.filterAttributeOptions.userId"), + value: "urn:ietf:params:scim:schemas:core:2.0:id" + }; + + filterAttributeOptions.push(createdTimeOption); + filterAttributeOptions.push(modifiedTimeOption); + filterAttributeOptions.push(userIdOption); + if (useConsoleAttributeList) { - filterAttributeOptions = filterAttributeOptions.concat(userSearchAttributes); + // Filter out duplicates based on value + const existingValues: Set = new Set( + filterAttributeOptions.map((option: DropdownChild) => option.value) + ); + + // Also exclude the SCIM format userName as it's already added + existingValues.add("urn:ietf:params:scim:schemas:core:2.0:User:userName"); + + const uniqueUserSearchAttributes: DropdownChild[] = userSearchAttributes.filter( + (option: DropdownChild) => !existingValues.has(option.value) + ); + + filterAttributeOptions = filterAttributeOptions.concat(uniqueUserSearchAttributes); } return filterAttributeOptions; diff --git a/modules/i18n/src/models/namespaces/users-ns.ts b/modules/i18n/src/models/namespaces/users-ns.ts index 3adcaf91fae..a98b3ccea9b 100644 --- a/modules/i18n/src/models/namespaces/users-ns.ts +++ b/modules/i18n/src/models/namespaces/users-ns.ts @@ -201,7 +201,10 @@ export interface usersNS { form: { dropdown: { filterAttributeOptions: { + createdTime: string; + modifiedTime: string; username: string; + userId: string; email: string; }; }; diff --git a/modules/i18n/src/translations/en-US/portals/users.ts b/modules/i18n/src/translations/en-US/portals/users.ts index f524348a2e9..6fc9fb55034 100644 --- a/modules/i18n/src/translations/en-US/portals/users.ts +++ b/modules/i18n/src/translations/en-US/portals/users.ts @@ -47,7 +47,10 @@ export const users: usersNS = { form: { dropdown: { filterAttributeOptions: { + createdTime: "Created Time", email: "Email", + modifiedTime: "Modified Time", + userId: "User ID", username: "Username" } },