diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx index 7cb5ae86a78..bea8c5e00fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx @@ -13,27 +13,20 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Button, Col, Input, Row, Space, Tooltip, Typography } from 'antd'; import Description from 'components/common/description/Description'; +import OwnerWidgetWrapper from 'components/common/OwnerWidget/OwnerWidgetWrapper.component'; import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture'; -import DropDownList from 'components/dropdown/DropDownList'; import ReviewerModal from 'components/Modals/ReviewerModal/ReviewerModal.component'; import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface'; -import { WILD_CARD_CHAR } from 'constants/char.constants'; import { getUserPath } from 'constants/constants'; import { NO_PERMISSION_FOR_ACTION } from 'constants/HelperTextUtil'; import { EntityReference, Glossary } from 'generated/entity/data/glossary'; import { GlossaryTerm } from 'generated/entity/data/glossaryTerm'; -import { cloneDeep, debounce, includes, isEqual } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { cloneDeep, includes, isEqual } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { getEntityName } from 'utils/EntityUtils'; -import { getOwnerList } from 'utils/ManageUtils'; import SVGIcons, { Icons } from 'utils/SvgUtils'; -import { - isCurrentUserAdmin, - searchFormattedUsersAndTeams, - suggestFormattedUsersAndTeams, -} from 'utils/UserDataUtils'; export interface GlossaryHeaderProps { supportAddOwner?: boolean; @@ -53,10 +46,7 @@ const GlossaryHeader = ({ const [isNameEditing, setIsNameEditing] = useState(false); const [isDescriptionEditable, setIsDescriptionEditable] = useState(false); - const [listVisible, setListVisible] = useState(false); - const [isUserLoading, setIsUserLoading] = useState(false); - const [listOwners, setListOwners] = useState(getOwnerList()); - const [searchText, setSearchText] = useState(''); + const [isEditOwner, setIsEditOwner] = useState(false); const [showReviewerModal, setShowReviewerModal] = useState(false); const editDisplayNamePermission = useMemo(() => { @@ -95,71 +85,6 @@ const GlossaryHeader = ({ setIsDescriptionEditable(false); } }; - const getOwnerSearch = useCallback( - (searchQuery = WILD_CARD_CHAR, from = 1) => { - setIsUserLoading(true); - searchFormattedUsersAndTeams(searchQuery, from) - .then((res) => { - const { users, teams } = res; - setListOwners(getOwnerList(users, teams, false, searchQuery)); - }) - .catch(() => { - setListOwners([]); - }) - .finally(() => { - setIsUserLoading(false); - }); - }, - [setListOwners, setIsUserLoading] - ); - const handleSelectOwnerDropdown = () => { - setListVisible((visible) => { - const newState = !visible; - - if (newState) { - getOwnerSearch(); - } - - return newState; - }); - }; - const getOwnerSuggestion = useCallback( - (qSearchText = '') => { - setIsUserLoading(true); - suggestFormattedUsersAndTeams(qSearchText) - .then((res) => { - const { users, teams } = res; - setListOwners(getOwnerList(users, teams, false, qSearchText)); - }) - .catch(() => { - setListOwners([]); - }) - .finally(() => { - setIsUserLoading(false); - }); - }, - [setListOwners, setIsUserLoading] - ); - - const debouncedOnChange = useCallback( - (text: string): void => { - if (text) { - getOwnerSuggestion(text); - } else { - getOwnerSearch(); - } - }, - [getOwnerSuggestion, getOwnerSearch] - ); - - const debounceOnSearch = useCallback(debounce(debouncedOnChange, 400), [ - debouncedOnChange, - ]); - - const handleOwnerSearch = (text: string) => { - setSearchText(text); - debounceOnSearch(text); - }; const handleRemoveReviewer = (id: string) => { let updatedGlossary = cloneDeep(selectedData); @@ -174,39 +99,23 @@ const GlossaryHeader = ({ onUpdate(updatedGlossary); }; - const prepareOwner = (updatedOwner?: EntityReference) => { - return !isEqual(updatedOwner, selectedData.owner) - ? updatedOwner - : undefined; - }; - const handleOwnerSelection = ( - _e: React.MouseEvent, - value = '' - ) => { - const owner = listOwners.find((item) => item.value === value); - - if (owner) { - const newOwner = prepareOwner({ - type: owner.type, - id: owner.value || '', - }); - if (newOwner) { - const updatedData = { - ...selectedData, - owner: newOwner, - }; - onUpdate(updatedData); - } + const handleUpdatedOwner = (newOwner: Glossary['owner']) => { + if (newOwner) { + const updatedData = { + ...selectedData, + owner: newOwner, + }; + onUpdate(updatedData); } - setListVisible(false); }; - const onRemoveOwner = () => { + + const handleRemoveOwner = () => { const updatedData = { ...selectedData, owner: undefined, }; onUpdate(updatedData); - setListVisible(false); + setIsEditOwner(false); }; const handleReviewerSave = (data: Array) => { @@ -330,23 +239,16 @@ const GlossaryHeader = ({ } size="small" type="text" - onClick={handleSelectOwnerDropdown} + onClick={() => setIsEditOwner(true)} /> - {listVisible && ( - setIsEditOwner(false)} + removeOwner={handleRemoveOwner} + updateUser={handleUpdatedOwner} + visible={isEditOwner} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.component.tsx index f9691bc4480..f188397c669 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.component.tsx @@ -11,31 +11,20 @@ * limitations under the License. */ -import { AxiosError } from 'axios'; +import DropDownList from 'components/dropdown/DropDownList'; +import { WILD_CARD_CHAR } from 'constants/char.constants'; +import { Table } from 'generated/entity/data/table'; +import { EntityReference } from 'generated/type/entityReference'; import { debounce, isEqual, lowerCase } from 'lodash'; import { LoadingState } from 'Models'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { getGroupTypeTeams } from 'rest/userAPI'; -import { getEntityName } from 'utils/EntityUtils'; -import { default as AppState, default as appState } from '../../../AppState'; -import { WILD_CARD_CHAR } from '../../../constants/char.constants'; -import { Table } from '../../../generated/entity/data/table'; -import { EntityReference } from '../../../generated/type/entityReference'; -import { useAuth } from '../../../hooks/authHooks'; -import { getOwnerList } from '../../../utils/ManageUtils'; -import { showErrorToast } from '../../../utils/ToastUtils'; -import { - isCurrentUserAdmin, - searchFormattedUsersAndTeams, -} from '../../../utils/UserDataUtils'; -import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'; -import DropDownList from '../../dropdown/DropDownList'; +import { getOwnerList, OwnerItem } from 'utils/ManageUtils'; +import { searchFormattedUsersAndTeams } from 'utils/UserDataUtils'; import './OwnerWidgetWrapper.style.less'; interface OwnerWidgetWrapperProps { currentOwner?: Table['owner']; updateUser?: (value: Table['owner']) => void; - isListLoading?: boolean; visible: boolean; currentUser?: EntityReference; allowTeamOwner?: boolean; @@ -52,78 +41,35 @@ const OwnerWidgetWrapper = ({ hideWidget, removeOwner, }: OwnerWidgetWrapperProps) => { - const { isAuthDisabled } = useAuthContext(); - const { isAdminUser } = useAuth(); const [statusOwner, setStatusOwner] = useState('initial'); - const [listOwners, setListOwners] = useState< - { - name: string; - value: string | undefined; - group: string; - type: string; - }[] - >([]); + const [ownersList, setOwnersList] = useState([]); const [isUserLoading, setIsUserLoading] = useState(true); const [owner, setOwner] = useState(currentUser); const [searchText, setSearchText] = useState(''); - const userDetails = useMemo(() => { - const userData = AppState.getCurrentUserDetails(); - - return [ - { - name: getEntityName(userData), - value: userData?.id, - group: 'Users', - type: 'user', - }, - ]; - }, [appState.users, appState.userDetails]); const [totalUsersCount, setTotalUsersCount] = useState(0); const [totalTeamsCount, setTotalTeamsCount] = useState(0); - const fetchGroupTypeTeams = async () => { - try { - if (listOwners.length === 0) { - const data = await getGroupTypeTeams(); - const updatedData = data.map((team) => ({ - name: getEntityName(team), - value: team.id, - group: 'Teams', - type: 'team', - })); - // set team count for logged in user - setTotalTeamsCount(data.length); - setListOwners([...updatedData, ...userDetails]); - } - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsUserLoading(false); - } - }; - const getOwnerSearch = useCallback( (searchQuery = WILD_CARD_CHAR, from = 1) => { setIsUserLoading(true); searchFormattedUsersAndTeams(searchQuery, from) .then((res) => { const { users, teams, teamsTotal, usersTotal } = res; - // set team and user count for admin user setTotalTeamsCount(teamsTotal ?? 0); setTotalUsersCount(usersTotal ?? 0); - setListOwners(getOwnerList(users, teams, false, searchQuery)); + setOwnersList(getOwnerList(users, teams, false)); }) .catch(() => { - setListOwners([]); + setOwnersList([]); }) .finally(() => { setIsUserLoading(false); }); }, - [setListOwners, setIsUserLoading] + [setOwnersList, setIsUserLoading] ); const debouncedOnChange = useCallback( @@ -145,7 +91,7 @@ const OwnerWidgetWrapper = ({ _e: React.MouseEvent, value = '' ) => { - const owner = listOwners.find((item) => item.value === value); + const owner = ownersList.find((item) => item.value === value); if (owner) { const newOwner = prepareOwner({ @@ -179,8 +125,7 @@ const OwnerWidgetWrapper = ({ */ const handleTotalCountForGroup = (groupName: string) => { if (lowerCase(groupName) === 'users') { - // if user is admin return total user count otherwise return 1 - return isAdminUser ? totalUsersCount : 1; + return totalUsersCount; } else if (lowerCase(groupName) === 'teams') { return totalTeamsCount; } else { @@ -190,19 +135,9 @@ const OwnerWidgetWrapper = ({ useEffect(() => { if (visible) { - if (isAuthDisabled || !isAdminUser) { - fetchGroupTypeTeams(); - } else { - handleOwnerSearch(''); - } + handleOwnerSearch(searchText ?? ''); } - }, [visible]); - - useEffect(() => { - if (visible) { - debounceOnSearch(searchText); - } - }, [searchText]); + }, [visible, searchText]); const ownerGroupList = useMemo(() => { return allowTeamOwner ? ['Teams', 'Users'] : ['Users']; @@ -233,15 +168,15 @@ const OwnerWidgetWrapper = ({ return visible ? ( 1 ? 'tab' : 'label'} isLoading={isUserLoading} listGroups={ownerGroupList} removeOwner={handleRemoveOwner} - showSearchBar={isCurrentUserAdmin()} value={owner?.id || ''} onSearchTextChange={handleSearchOwnerDropdown} onSelect={handleOwnerSelection} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.test.tsx new file mode 100644 index 00000000000..504ae7c14fb --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerWidget/OwnerWidgetWrapper.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Collate. + * Licensed 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 { act, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import OwnerWidgetWrapper from './OwnerWidgetWrapper.component'; + +const mockSearchAPI = jest.fn(); +const mockHideWidget = jest.fn(); + +jest.mock('utils/UserDataUtils', () => ({ + searchFormattedUsersAndTeams: mockSearchAPI, +})); + +const mockProps = { + visible: true, + currentUser: { id: '1', type: 'User' }, + hideWidget: mockHideWidget, +}; + +describe('OwnerWidgetWrapper', () => { + it('Should renders the component when visible is true', () => { + render(); + const dropDownList = screen.getByTestId('dropdown-list'); + const searchBox = screen.getByTestId('searchInputText'); + + expect(dropDownList).toBeInTheDocument(); + expect(searchBox).toBeInTheDocument(); + }); + + it('Should not render the component when visible is false', () => { + render(); + + const component = screen.queryByTestId('dropdown-list'); + + expect(component).toBeNull(); + }); + + it('Search Should work', async () => { + render(); + + const searchInput = screen.getByTestId('searchInputText'); + + await act(async () => { + fireEvent.change(searchInput, { + target: { + value: 'user1', + }, + }); + }); + + expect(searchInput).toHaveValue('user1'); + }); + + it('Hide Widget should work', async () => { + render(); + + const backdropButton = screen.getByTestId('backdrop-button'); + + await act(async () => { + userEvent.click(backdropButton); + }); + + expect(mockHideWidget).toHaveBeenCalled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ManageUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ManageUtils.ts index 87ebb4e1d83..9bd8f10c0b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ManageUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ManageUtils.ts @@ -11,73 +11,64 @@ * limitations under the License. */ -import { toString } from 'lodash'; +import { ReactNode } from 'react'; import AppState from '../AppState'; -import { WILD_CARD_CHAR } from '../constants/char.constants'; import { Team } from '../generated/entity/teams/team'; import { User } from '../generated/entity/teams/user'; -import { EntityReference } from '../generated/type/entityUsage'; import { getEntityName } from './EntityUtils'; +export type OwnerItem = { + name: string; + value: string; + group: string; + type: string; +} & Record; + /** * @param listUsers - List of users * @param listTeams - List of teams - * @param excludeCurrentUser - Wether to exclude current user to be on list. Needed when calls from searching - * @param searchQuery - search query for user or team - * @returns List of user or team + * @param excludeCurrentUser - Whether to exclude current user from the list + * @param searchQuery - Search query for user or team + * @returns List of users or teams */ export const getOwnerList = ( - listUsers?: User[], - listTeams?: Team[], - excludeCurrentUser?: boolean, - searchQuery?: string -) => { + listUsers: User[] = [], + listTeams: Team[] = [], + excludeCurrentUser = false +): OwnerItem[] => { const userDetails = AppState.getCurrentUserDetails(); - const isAdminIncludeInQuery = - getEntityName(userDetails).includes(toString(searchQuery)) || - searchQuery === WILD_CARD_CHAR - ? true - : false; + const users = listUsers.flatMap((user) => + user.id !== userDetails?.id + ? [ + { + name: getEntityName(user), + value: user.id, + group: 'Users', + type: 'user', + }, + ] + : [] + ); - if (userDetails?.isAdmin) { - const users = (listUsers || []) - .map((user) => ({ - name: getEntityName(user as unknown as EntityReference), - value: user.id, - group: 'Users', - type: 'user', - })) - .filter((u) => u.value !== userDetails.id); - const teams = (listTeams || []).map((team) => ({ - name: getEntityName(team), - value: team.id, - group: 'Teams', - type: 'team', - })); + const teams = listTeams.map((team) => ({ + name: getEntityName(team), + value: team.id, + group: 'Teams', + type: 'team', + })); - return [ - ...(!excludeCurrentUser && isAdminIncludeInQuery - ? [ - { - name: getEntityName(userDetails), - value: userDetails.id, - group: 'Users', - type: 'user', - }, - ] - : []), - ...users, - ...teams, - ]; - } else { - return [ - { - name: getEntityName(userDetails), - value: userDetails?.id, - group: 'Users', - type: 'user', - }, - ]; - } + const currentUser = + !excludeCurrentUser && userDetails + ? [ + { + name: getEntityName(userDetails), + value: userDetails.id, + group: 'Users', + type: 'user', + }, + ] + : []; + + return [...currentUser, ...users, ...teams]; };