diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts index dcf6d4fd56a..2095bbd350f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts @@ -10,7 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Suggestion } from '../../../generated/entity/feed/suggestion'; +import { + Suggestion, + SuggestionType, +} from '../../../generated/entity/feed/suggestion'; import { EntityReference } from '../../../generated/entity/type'; export interface SuggestionsContextType { @@ -18,6 +21,8 @@ export interface SuggestionsContextType { suggestions: Suggestion[]; suggestionsByUser: Map; loading: boolean; + loadingAccept: boolean; + loadingReject: boolean; allSuggestionsUsers: EntityReference[]; onUpdateActiveUser: (user: EntityReference) => void; fetchSuggestions: (entityFqn: string) => void; @@ -25,6 +30,10 @@ export interface SuggestionsContextType { suggestion: Suggestion, action: SuggestionAction ) => void; + acceptRejectAllSuggestions: ( + suggestionType: SuggestionType, + status: SuggestionAction + ) => void; } export enum SuggestionAction { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx new file mode 100644 index 00000000000..0ac0d75afa0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright 2024 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 React from 'react'; +import { SuggestionType } from '../../../generated/entity/feed/suggestion'; +import { mockEntityPermissions } from '../../../pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock'; +import { + aproveRejectAllSuggestions, + getSuggestionsList, + updateSuggestionStatus, +} from '../../../rest/suggestionsAPI'; +import SuggestionsProvider, { + useSuggestionsContext, +} from './SuggestionsProvider'; +import { SuggestionAction } from './SuggestionsProvider.interface'; + +const suggestions = [ + { + id: '1', + description: 'Test suggestion1', + createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + { + id: '2', + description: 'Test suggestion2', + createdBy: { id: '2', name: 'Avatar 2', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, +]; + +jest.mock('../../../hooks/useFqn', () => ({ + useFqn: jest.fn().mockReturnValue({ fqn: 'mockFQN' }), +})); + +jest.mock('../../../rest/suggestionsAPI', () => ({ + getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve()), + aproveRejectAllSuggestions: jest.fn(), + updateSuggestionStatus: jest.fn(), +})); + +jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockImplementation(() => ({ + permissions: mockEntityPermissions, + })), +})); + +describe('SuggestionsProvider', () => { + it('renders provider and fetches data', async () => { + await act(async () => { + render( + + + + ); + }); + + expect(getSuggestionsList).toHaveBeenCalled(); + }); + + it('calls approveRejectAllSuggestions when button is clicked', () => { + render( + + + + ); + + const button = screen.getByText('Active User'); + fireEvent.click(button); + + const acceptAllBtn = screen.getByText('Accept All'); + fireEvent.click(acceptAllBtn); + + expect(aproveRejectAllSuggestions).toHaveBeenCalledWith( + '1', + 'mockFQN', + SuggestionType.SuggestDescription, + SuggestionAction.Accept + ); + }); + + it('calls approveRejectAllSuggestions when reject button is clicked', () => { + render( + + + + ); + + const button = screen.getByText('Active User'); + fireEvent.click(button); + + const rejectAll = screen.getByText('Reject All'); + fireEvent.click(rejectAll); + + expect(aproveRejectAllSuggestions).toHaveBeenCalledWith( + '1', + 'mockFQN', + SuggestionType.SuggestDescription, + SuggestionAction.Reject + ); + }); + + it('calls accept suggestion when accept button is clicked', () => { + render( + + + + ); + + const acceptBtn = screen.getByText('Accept One'); + fireEvent.click(acceptBtn); + + expect(updateSuggestionStatus).toHaveBeenCalledWith( + suggestions[0], + SuggestionAction.Accept + ); + }); + + it('calls reject suggestion when accept button is clicked', () => { + render( + + + + ); + + const rejectBtn = screen.getByText('Reject One'); + fireEvent.click(rejectBtn); + + expect(updateSuggestionStatus).toHaveBeenCalledWith( + suggestions[0], + SuggestionAction.Reject + ); + }); +}); + +function TestComponent() { + const { + acceptRejectAllSuggestions, + onUpdateActiveUser, + acceptRejectSuggestion, + } = useSuggestionsContext(); + + return ( + <> + + + + + + + ); +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx index 6357f429bdc..0b13e57e66a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx @@ -25,11 +25,15 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; -import { Suggestion } from '../../../generated/entity/feed/suggestion'; +import { + Suggestion, + SuggestionType, +} from '../../../generated/entity/feed/suggestion'; import { EntityReference } from '../../../generated/entity/type'; import { useFqn } from '../../../hooks/useFqn'; import { usePub } from '../../../hooks/usePubSub'; import { + aproveRejectAllSuggestions, getSuggestionsList, updateSuggestionStatus, } from '../../../rest/suggestionsAPI'; @@ -45,6 +49,9 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { const { t } = useTranslation(); const { fqn: entityFqn } = useFqn(); const [activeUser, setActiveUser] = useState(); + const [loadingAccept, setLoadingAccept] = useState(false); + const [loadingReject, setLoadingReject] = useState(false); + const [allSuggestionsUsers, setAllSuggestionsUsers] = useState< EntityReference[] >([]); @@ -122,6 +129,38 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { return suggestionsByUser.get(activeUser?.name ?? '') ?? []; }, [activeUser, suggestionsByUser]); + const acceptRejectAllSuggestions = useCallback( + async (suggestionType: SuggestionType, status: SuggestionAction) => { + if (status === SuggestionAction.Accept) { + setLoadingAccept(true); + } else { + setLoadingReject(true); + } + try { + await aproveRejectAllSuggestions( + activeUser?.id ?? '', + entityFqn, + suggestionType, + status + ); + + await fetchSuggestions(entityFqn); + if (status === SuggestionAction.Accept) { + selectedUserSuggestions.forEach((suggestion) => { + publish('updateDetails', suggestion); + }); + } + setActiveUser(undefined); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setLoadingAccept(false); + setLoadingReject(false); + } + }, + [activeUser, entityFqn, selectedUserSuggestions] + ); + useEffect(() => { if (!isEmpty(permissions) && !isEmpty(entityFqn)) { fetchSuggestions(entityFqn); @@ -135,10 +174,13 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { selectedUserSuggestions, entityFqn, loading, + loadingAccept, + loadingReject, allSuggestionsUsers, onUpdateActiveUser, fetchSuggestions, acceptRejectSuggestion, + acceptRejectAllSuggestions, }; }, [ suggestions, @@ -146,10 +188,13 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { selectedUserSuggestions, entityFqn, loading, + loadingAccept, + loadingReject, allSuggestionsUsers, onUpdateActiveUser, fetchSuggestions, acceptRejectSuggestion, + acceptRejectAllSuggestions, ]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.test.tsx new file mode 100644 index 00000000000..dbe01b8f98e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider'; +import SuggestionsSlider from './SuggestionsSlider'; + +jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({ + useSuggestionsContext: jest.fn(), +})); + +jest.mock('../../common/AvatarCarousel/AvatarCarousel', () => { + return jest.fn(() =>

Avatar Carousel

); +}); + +describe('SuggestionsSlider', () => { + it('renders buttons when there are selected user suggestions', () => { + (useSuggestionsContext as jest.Mock).mockReturnValue({ + selectedUserSuggestions: [{ id: '1' }, { id: '2' }], + acceptRejectAllSuggestions: jest.fn(), + loadingAccept: false, + loadingReject: false, + }); + + render(); + + expect(screen.getByTestId('accept-all-suggestions')).toBeInTheDocument(); + expect(screen.getByTestId('reject-all-suggestions')).toBeInTheDocument(); + }); + + it('calls acceptRejectAllSuggestions on button click', () => { + const acceptRejectAllSuggestions = jest.fn(); + (useSuggestionsContext as jest.Mock).mockReturnValue({ + selectedUserSuggestions: [{ id: '1' }, { id: '2' }], + acceptRejectAllSuggestions, + loadingAccept: false, + loadingReject: false, + }); + + render(); + fireEvent.click(screen.getByTestId('accept-all-suggestions')); + + expect(acceptRejectAllSuggestions).toHaveBeenCalled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx index cae06424948..94eb966d19a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx @@ -13,11 +13,18 @@ import { Button, Typography } from 'antd'; import { t } from 'i18next'; import React from 'react'; +import { SuggestionType } from '../../../generated/entity/feed/suggestion'; import AvatarCarousel from '../../common/AvatarCarousel/AvatarCarousel'; import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider'; +import { SuggestionAction } from '../SuggestionsProvider/SuggestionsProvider.interface'; const SuggestionsSlider = () => { - const { selectedUserSuggestions } = useSuggestionsContext(); + const { + selectedUserSuggestions, + acceptRejectAllSuggestions, + loadingAccept, + loadingReject, + } = useSuggestionsContext(); return (
@@ -27,12 +34,33 @@ const SuggestionsSlider = () => { {selectedUserSuggestions.length > 0 && ( <> - -