/** * Copyright (c) 2021-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Show } from "@thiva/access-control"; import { hasRequiredScopes, isFeatureEnabled, resolveUserstore } from "@thiva/core/helpers"; import { LoadableComponentInterface, SBACInterface, TestableComponentInterface } from "@thiva/core/models"; import { AnimatedAvatar, AppAvatar, ConfirmationModal, DataTable, EmptyPlaceholder, LinkButton, Popup, PrimaryButton, TableActionsInterface, TableColumnInterface } from "@thiva/react-components"; import React, { ReactElement, ReactNode, SyntheticEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { Header, Icon, SemanticICONS } from "semantic-ui-react"; import { resolveGroupName } from "./utils/group-utils"; import { AppConstants, AppState, FeatureConfigInterface, UIConstants, getEmptyPlaceholderIllustrations, history } from "@thiva/admin.core.v1"; import { GroupConstants } from "@thiva/admin.groups.v1/constants"; import { GroupsInterface } from "@thiva/admin.groups.v1/models"; import { CONSUMER_USERSTORE } from "@thiva/admin.userstores.v1/constants"; interface GroupListProps extends SBACInterface, LoadableComponentInterface, TestableComponentInterface { /** * Advanced Search component. */ advancedSearch?: ReactNode; /** * Default list item limit. */ defaultListItemLimit?: number; /** * Groups list. */ groupList: GroupsInterface[]; /** * Group delete callback. * @param group - Deleting group. */ handleGroupDelete?: (group: GroupsInterface) => void; /** * On list item select callback. */ onListItemClick?: (event: SyntheticEvent, group: GroupsInterface) => void; /** * Callback for the search query clear action. */ onSearchQueryClear?: () => void; /** * Callback to be fired when clicked on the empty list placeholder action. */ onEmptyListPlaceholderActionClick?: () => void; /** * Search query for the list. */ searchQuery?: string; /** * Enable selection styles. */ selection?: boolean; /** * Show list item actions. */ showListItemActions?: boolean; /** * Show/Hide meta content. */ showMetaContent?: boolean; /** * List of readOnly user stores. */ readOnlyUserStores?: string[]; /** * Selected user-store from the dropdown */ selectedUserStoreOption?: string; } /** * List component for Group Management list * * @param props - contains the role list as a prop to populate */ export const GroupList: React.FunctionComponent = (props: GroupListProps): ReactElement => { const { advancedSearch, defaultListItemLimit, handleGroupDelete, isLoading, readOnlyUserStores, featureConfig, onEmptyListPlaceholderActionClick, onSearchQueryClear, groupList, selection, searchQuery, showListItemActions, selectedUserStoreOption, [ "data-testid" ]: testId } = props; const { t } = useTranslation(); const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); const [ showGroupDeleteConfirmation, setShowDeleteConfirmationModal ] = useState(false); const [ currentDeletedGroup, setCurrentDeletedGroup ] = useState(); const handleGroupEdit = (groupId: string) => { history.push(AppConstants.getPaths().get("GROUP_EDIT").replace(":id", groupId)); }; /** * Shows list placeholders. */ const showPlaceholders = (): ReactElement => { // When the search returns empty. if (searchQuery && groupList?.length === 0) { return ( { t("roles:list.emptyPlaceholders.search.action") } ) } image={ getEmptyPlaceholderIllustrations().emptySearch } imageSize="tiny" title={ t("roles:list.emptyPlaceholders.search.title") } subtitle={ [ t("roles:list.emptyPlaceholders.search.subtitles.0", { searchQuery: searchQuery }), t("roles:list.emptyPlaceholders.search.subtitles.1") ] } /> ); } if (groupList?.length !== 0) { return ( <> { t("roles:list.emptyPlaceholders.emptyRoleList.action", { type: "Group" }) } ) } image={ getEmptyPlaceholderIllustrations().newList } imageSize="tiny" title={ "No groups available" } subtitle={ [ t("roles:list.emptyPlaceholders.emptyRoleList.subtitles.0", { type: "groups" }) ] } /> ); } return null; }; /** * Resolves data table columns. */ const resolveTableColumns = (): TableColumnInterface[] => { return [ { allowToggleVisibility: false, dataIndex: "name", id: "name", key: "name", render: (group: GroupsInterface): ReactNode => (
) } size="mini" spaced="right" data-testid={ `${ testId }-item-image` } />
{ resolveGroupName(group) }
), title: t("console:manage.features.groups.list.columns.name") }, { allowToggleVisibility: false, dataIndex: "type", id: "type", key: "type", render: (group: GroupsInterface): ReactNode => { const grpName: string = resolveUserstore(group.displayName); if (grpName === CONSUMER_USERSTORE) { return CONSUMER_USERSTORE; } return grpName.toUpperCase(); }, title: ( <> { t("console:manage.features.groups.list.columns.source") } ) } content="Where group is managed." position="top center" size="mini" hideOnScroll inverted /> ) }, { allowToggleVisibility: false, dataIndex: "action", id: "actions", key: "actions", textAlign: "right", title: null } ]; }; /** * Resolves data table actions. */ const resolveTableActions = (): TableActionsInterface[] => { if (!showListItemActions) { return; } const actions: TableActionsInterface[] = [ { hidden: (): boolean => !isFeatureEnabled(featureConfig?.groups, GroupConstants.FEATURE_DICTIONARY.get("GROUP_READ")) || !hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.create, allowedScopes), icon: (group: GroupsInterface): SemanticICONS => { const userStore: string = group?.displayName?.split("/").length > 1 ? group?.displayName?.split("/")[0] : "PRIMARY"; return !isFeatureEnabled(featureConfig?.groups, GroupConstants.FEATURE_DICTIONARY.get("GROUP_UPDATE")) || readOnlyUserStores?.includes(userStore.toString()) ? "eye" : "pencil alternate"; }, onClick: (e: SyntheticEvent, group: GroupsInterface): void => handleGroupEdit(group.id), popupText: (group: GroupsInterface): string => { const userStore: string = group?.displayName?.split("/").length > 1 ? group?.displayName?.split("/")[0] : "PRIMARY"; return !isFeatureEnabled(featureConfig?.groups, GroupConstants.FEATURE_DICTIONARY.get("GROUP_UPDATE")) || readOnlyUserStores?.includes(userStore.toString()) ? t("common:view") : t("common:edit"); }, renderer: "semantic-icon" } ]; actions.push({ hidden: (group: GroupsInterface): boolean => { const userStore: string = group?.displayName?.split("/").length > 1 ? group?.displayName?.split("/")[0] : "PRIMARY"; return !hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.delete, allowedScopes) || readOnlyUserStores?.includes(userStore.toString()) || userStore.toString() !== CONSUMER_USERSTORE; }, icon: (): SemanticICONS => "trash alternate", onClick: (e: SyntheticEvent, group: GroupsInterface): void => { setCurrentDeletedGroup(group); setShowDeleteConfirmationModal(!showGroupDeleteConfirmation); }, popupText: (): string => t("roles:list.popups.delete", { type: "Group" }), renderer: "semantic-icon" }); return actions; }; return ( <> showHeader className="groups-list" externalSearch={ advancedSearch } isLoading={ isLoading } loadingStateOptions={ { count: defaultListItemLimit ?? UIConstants.DEFAULT_RESOURCE_LIST_ITEM_LIMIT, imageType: "square" } } actions={ resolveTableActions() } columns={ resolveTableColumns() } data={ groupList } onRowClick={ (e: SyntheticEvent, group: GroupsInterface): void => { hasRequiredScopes(featureConfig?.groups, featureConfig?.groups?.scopes?.create, allowedScopes) && handleGroupEdit(group?.id); } } placeholders={ showPlaceholders() } selectable={ selection } transparent={ !isLoading && (showPlaceholders() !== null) } data-testid={ testId } /> { showGroupDeleteConfirmation && ( setShowDeleteConfirmationModal(false) } type="negative" open={ showGroupDeleteConfirmation } assertionHint={ t("roles:list.confirmations" + ".deleteItem.assertionHint") } assertionType="checkbox" primaryAction="Confirm" secondaryAction="Cancel" onSecondaryActionClick={ (): void => setShowDeleteConfirmationModal(false) } onPrimaryActionClick={ (): void => { handleGroupDelete(currentDeletedGroup); setShowDeleteConfirmationModal(false); } } closeOnDimmerClick={ false } > { t("roles:list.confirmations.deleteItem.header") } { t("roles:list.confirmations.deleteItem.message", { type: "group" }) } { t("roles:list.confirmations.deleteItem.content", { type: "group" }) } ) } ); }; /** * Default props for the component. */ GroupList.defaultProps = { selection: true, showListItemActions: true, showMetaContent: true };