From 6efb626fe481a63c91dbff0f6fafcb69aaf66bc5 Mon Sep 17 00:00:00 2001 From: Pavindu Lakshan Date: Wed, 18 Dec 2024 17:21:31 +0530 Subject: [PATCH] Add UI support for configuring discoverable groups for an application --- .../components/forms/application-audience.tsx | 305 ++++++++++++++++++ .../components/forms/general-details-form.tsx | 125 ++++--- .../models/application.ts | 19 ++ features/admin.applications.v1/package.json | 1 + pnpm-lock.yaml | 3 + 5 files changed, 400 insertions(+), 53 deletions(-) create mode 100644 features/admin.applications.v1/components/forms/application-audience.tsx diff --git a/features/admin.applications.v1/components/forms/application-audience.tsx b/features/admin.applications.v1/components/forms/application-audience.tsx new file mode 100644 index 00000000000..a4599661386 --- /dev/null +++ b/features/admin.applications.v1/components/forms/application-audience.tsx @@ -0,0 +1,305 @@ +/** + * 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 AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Autocomplete, + Chip, + Grid, + IconButton, + MenuItem, + Select, + SelectChangeEvent, + TextField, + Typography +} from "@mui/material"; +import { ApplicationManagementConstants, UserStoreProperty, getAUserStore } from "@wso2is/admin.core.v1"; +import { userstoresConfig } from "@wso2is/admin.extensions.v1"; +import { useGroupList } from "@wso2is/admin.groups.v1/api/groups"; +import { GroupsInterface } from "@wso2is/admin.groups.v1/models/groups"; +import { useUserStores } from "@wso2is/admin.userstores.v1/api"; +import { RemoteUserStoreManagerType } from "@wso2is/admin.userstores.v1/constants"; +import { UserStoreItem, UserStoreListItem, UserStorePostData } from "@wso2is/admin.userstores.v1/models/user-stores"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { Hint, useDocumentation } from "@wso2is/react-components"; +import React, { useEffect, useMemo } from "react"; +import { Trans } from "react-i18next"; +import { ApplicationInterface, DiscoverableGroup } from "../../models/application"; + +interface GroupAssignmentProps extends IdentifiableComponentInterface { + application: ApplicationInterface, + discoverableGroups: DiscoverableGroup[], + setDiscoverableGroups: React.Dispatch> +} + +const GroupAssignmentRow = ({ + index, + discoverableGroup, + userStoreOptions, + handleGroupChange, + handleUserStoreChange, + handleAddRow, + handleDeleteRow, + discoverableGroups +}: { + index: number; + discoverableGroup: DiscoverableGroup; + userStoreOptions: any; + handleGroupChange: any; + handleUserStoreChange: any; + handleAddRow: any; + handleDeleteRow: any; + discoverableGroups: any +}) => { + const { + data: groupList, + isLoading: isGroupListFetchRequestLoading, + error: groupListFetchError + } = useGroupList(discoverableGroup.userStore); + + useEffect(() => { + if (!isGroupListFetchRequestLoading && groupListFetchError) { + addAlert({ + description: "Error occurred while getting user groups", + message: "Something went wrong!", + type: AlertLevels.ERROR + }); + } + }, [ isGroupListFetchRequestLoading, groupListFetchError ]); + + return ( + <> + + + + + + group.displayName) || [] } + value={ discoverableGroup.groups } + onChange={ + ( + event: React.SyntheticEvent, + newValue: any + ) => handleGroupChange(index, newValue) + } + renderTags={ (value, getTagProps) => + value.map((option, i) => ( + + )) + } + renderInput={ (params) => ( + + ) } + /> + + + + handleDeleteRow(index) } + > + + + { index === discoverableGroups.length - 1 && ( + + + + ) } + + + ); +}; + +export default function GroupAssignment({ + application, + discoverableGroups, + setDiscoverableGroups, + ["data-componentid"]: componentId = "application-audience" +}: GroupAssignmentProps) { + const { getLink } = useDocumentation(); + + const { + data: userStoreList, + isLoading: isUserStoreListFetchRequestLoading, + error: userStoreFetchError + } = useUserStores({ + filter: null, + limit: null, + offset: null, + sort: null + }); + + const userStoreOptions: UserStoreItem[] = useMemo(() => { + const storeOptions: UserStoreItem[] = [ + { + key: -1, + text: userstoresConfig.primaryUserstoreName, + value: userstoresConfig.primaryUserstoreName + } + ]; + + if (userStoreList?.length > 0) { + userStoreList.map((store: UserStoreListItem, index: number) => { + if (store.name.toUpperCase() !== userstoresConfig.primaryUserstoreName) { + getAUserStore(store.id).then((response: UserStorePostData) => { + const isDisabled: boolean = response.properties.find( + (property: UserStoreProperty) => property.name === "Disabled")?.value === "true"; + + if (!isDisabled) { + const storeOption: UserStoreItem = { + disabled: store.typeName === RemoteUserStoreManagerType.RemoteUserStoreManager, + key: index, + text: store.name, + value: store.name + }; + + storeOptions.push(storeOption); + } + }); + } + }); + } + + return storeOptions; + }, [ userStoreList ]); + + useEffect(() => { + if (!isUserStoreListFetchRequestLoading && userStoreFetchError) { + addAlert({ + description: "Error when getting the user stores", + level: AlertLevels.ERROR, + message: "Something went wrong!" + }); + } + }, [ isUserStoreListFetchRequestLoading, userStoreFetchError ]); + + const handleAddRow = () => { + setDiscoverableGroups([ ...discoverableGroups, { groups: [], userStore: "" } ]); + }; + + const handleDeleteRow = (index: number) => { + const updatedRows: DiscoverableGroup[] = discoverableGroups.filter( + (_: DiscoverableGroup, i: number) => i !== index); + + setDiscoverableGroups(updatedRows); + }; + + const handleGroupChange = (index: number, newGroups: string[]) => { + const updatedRows: DiscoverableGroup[] = [ ...discoverableGroups ]; + + updatedRows[index].groups = newGroups; + setDiscoverableGroups(updatedRows); + }; + + const handleUserStoreChange = (index: number, value: string) => { + const updatedRows: DiscoverableGroup[] = [ ...discoverableGroups ]; + + updatedRows[index].userStore = value; + setDiscoverableGroups(updatedRows); + }; + + return ( +
+ Application audience + + + { " " } + { getLink( + "develop.applications.managementApplication.selfServicePortal" + ) === undefined + ? ( + + My Account + + ) + : ( + window.open( + getLink( + "develop.applications.managementApplication" + + ".selfServicePortal" + ), + "_blank" + ) + } + > + My Account + + ) + } + + + + + + User store + + + Groups + + + + { discoverableGroups.map((discoverableGroup: DiscoverableGroup, index: number) => ( + + )) } + +
+ ); +} diff --git a/features/admin.applications.v1/components/forms/general-details-form.tsx b/features/admin.applications.v1/components/forms/general-details-form.tsx index c5f050f7fc2..7214d1c00f9 100644 --- a/features/admin.applications.v1/components/forms/general-details-form.tsx +++ b/features/admin.applications.v1/components/forms/general-details-form.tsx @@ -22,7 +22,7 @@ import { PaletteIcon } from "@oxygen-ui/react-icons"; import { ApplicationTabComponentsFilter } from "@wso2is/admin.application-templates.v1/components/application-tab-components-filter"; import { AppConstants, AppState, UIConfigInterface, history } from "@wso2is/admin.core.v1"; -import { ApplicationTabIDs, applicationConfig } from "@wso2is/admin.extensions.v1"; +import { ApplicationTabIDs, applicationConfig, userstoresConfig } from "@wso2is/admin.extensions.v1"; import { FeatureStatusLabel } from "@wso2is/admin.feature-gate.v1/models/feature-status"; import { OrganizationType } from "@wso2is/admin.organizations.v1/constants"; import { TestableComponentInterface } from "@wso2is/core/models"; @@ -42,9 +42,10 @@ import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Divider, Grid } from "semantic-ui-react"; +import GroupAssignment from "./application-audience"; import { useMyAccountStatus } from "../../api/application"; import { ApplicationManagementConstants } from "../../constants/application-management"; -import { ApplicationInterface } from "../../models/application"; +import { ApplicationInterface, DiscoverableGroup } from "../../models/application"; /** * Proptypes for the applications general details form component. @@ -182,6 +183,10 @@ export const GeneralDetailsForm: FunctionComponent(discoverability); + const [ discoverableGroups, setDiscoverableGroups ] = useState([ + { groups: [], userStore: userstoresConfig.primaryUserstoreId } + ]); + const [ isMyAccountEnabled, setMyAccountStatus ] = useState(AppConstants.DEFAULT_MY_ACCOUNT_STATUS); const [ isM2MApplication, setM2MApplication ] = useState(false); @@ -223,7 +228,8 @@ export const GeneralDetailsForm: FunctionComponent - - + + + setDiscoverability(value) } - hint={ ( - setDiscoverability(value) } + hint={ ( + - { " " } - { getLink( - "develop.applications.managementApplication.selfServicePortal" - ) === undefined - ? ( - + } + tOptions={ { myAccount: "My Account" } } + > + { " " } + { getLink( + "develop.applications.managementApplication.selfServicePortal" + ) === undefined + ? ( + My Account - - ) - : ( - window.open( - getLink( - "develop.applications.managementApplication" + + ) + : ( + window.open( + getLink( + "develop.applications.managementApplication" + ".selfServicePortal" - ), - "_blank" - ) - } - > + ), + "_blank" + ) + } + > My Account - - ) - } - - ) } - width={ 16 } - /> - - + + ) + } + + ) } + width={ 16 } + /> + + + { isDiscoverable && ( + + + + + + ) } + ) : null } { diff --git a/features/admin.applications.v1/models/application.ts b/features/admin.applications.v1/models/application.ts index c97efb32c65..56f725130e6 100644 --- a/features/admin.applications.v1/models/application.ts +++ b/features/admin.applications.v1/models/application.ts @@ -263,12 +263,31 @@ export interface ApplicationAdvancedConfigurationsViewInterface { androidAttestationServiceCredentials?: string; } +/** + * Type for defining the discoverable groups for a userstore + */ +export interface DiscoverableGroup { + /** + * Userstore name + */ + userStore: string; + /** + * Groups assigned from the userstore for discoverability + */ + groups: string[]; +} + /** * Captures application related configuration. */ export interface AdvancedConfigurationsInterface { saas?: boolean; discoverableByEndUsers?: boolean; + /** + * List of groups the application is discoverable to. + * values are sent in the format "userstoreId/groupId" + */ + discoverableGroups?: DiscoverableGroup[]; certificate?: CertificateInterface; skipLoginConsent?: boolean; skipLogoutConsent?: boolean; diff --git a/features/admin.applications.v1/package.json b/features/admin.applications.v1/package.json index c0dc473da39..62b7bb55c77 100644 --- a/features/admin.applications.v1/package.json +++ b/features/admin.applications.v1/package.json @@ -27,6 +27,7 @@ "@wso2is/admin.core.v1": "^2.34.59", "@wso2is/admin.extensions.v1": "^2.35.18", "@wso2is/admin.feature-gate.v1": "^1.4.59", + "@wso2is/admin.groups.v1": "^2.26.18", "@wso2is/admin.identity-providers.v1": "^2.26.59", "@wso2is/admin.login-flow.ai.v1": "^2.26.59", "@wso2is/admin.oidc-scopes.v1": "^2.25.59", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 421d1c3ad1d..7d24170085a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2625,6 +2625,9 @@ importers: '@wso2is/admin.feature-gate.v1': specifier: ^1.4.59 version: link:../admin.feature-gate.v1 + '@wso2is/admin.groups.v1': + specifier: ^2.26.18 + version: link:../admin.groups.v1 '@wso2is/admin.identity-providers.v1': specifier: ^2.26.59 version: link:../admin.identity-providers.v1