diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-exit.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-exit.svg new file mode 100644 index 00000000000..058c8c553d0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-exit.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx index 57a138ffe6c..380ba61870c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx @@ -205,24 +205,28 @@ const AppDetails = () => { }, }, ]), - { - label: ( - - ), - key: 'uninstall-button', - onClick: () => { - setShowDeleteModel(true); - setShowActions(false); - setAction(AppAction.UNINSTALL); - }, - }, + ...(appData?.system + ? [] + : [ + { + label: ( + + ), + key: 'uninstall-button', + onClick: () => { + setShowDeleteModel(true); + setShowActions(false); + setAction(AppAction.UNINSTALL); + }, + }, + ]), ]; const onConfigSave = async (data: IChangeEvent) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less index 8c0d0ad0901..99afb2ab186 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less @@ -42,3 +42,7 @@ td .suggested-alert-footer { padding: 4px 12px; } + +.close-suggestion-btn { + gap: 6px; +} 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 810b49e81df..d7b07573218 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 @@ -14,6 +14,7 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Button, Space, Typography } from 'antd'; import { t } from 'i18next'; import React from 'react'; +import { ReactComponent as ExitIcon } from '../../../assets/svg/ic-exit.svg'; import { SuggestionType } from '../../../generated/entity/feed/suggestion'; import AvatarCarousel from '../../common/AvatarCarousel/AvatarCarousel'; import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider'; @@ -35,7 +36,7 @@ const SuggestionsSlider = () => { {selectedUserSuggestions.length > 0 && ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx index 9edf8baf92e..ebdddcf238c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx @@ -14,22 +14,30 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import AvatarCarousel from './AvatarCarousel'; +const suggestions = [ + { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + { + id: '2', + description: 'Test suggestion', + createdBy: { id: '2', name: 'Avatar 2', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, +]; + +const suggByUser = new Map([ + ['Avatar 1', [suggestions[0]]], + ['Avatar 2', [suggestions[1]]], +]); + jest.mock('../../Suggestions/SuggestionsProvider/SuggestionsProvider', () => ({ useSuggestionsContext: jest.fn().mockImplementation(() => ({ - suggestions: [ - { - id: '1', - description: 'Test suggestion', - createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, - entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', - }, - { - id: '2', - description: 'Test suggestion', - createdBy: { id: '2', name: 'Avatar 2', type: 'user' }, - entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', - }, - ], + suggestions: suggestions, + suggestionsByUser: suggByUser, allSuggestionsUsers: [ { id: '1', name: 'Avatar 1', type: 'user' }, { id: '2', name: 'Avatar 2', type: 'user' }, @@ -51,22 +59,9 @@ jest.mock('../ProfilePicture/ProfilePicture', () => ); jest.mock('../../../rest/suggestionsAPI', () => ({ - getSuggestionsList: jest.fn().mockImplementation(() => - Promise.resolve([ - { - id: '1', - description: 'Test suggestion', - createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, - entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', - }, - { - id: '2', - description: 'Test suggestion', - createdBy: { id: '1', name: 'Avatar 2', type: 'user' }, - entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', - }, - ]) - ), + getSuggestionsList: jest + .fn() + .mockImplementation(() => Promise.resolve(suggestions)), })); describe('AvatarCarousel', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx index 58e52d48f4b..21e95fd74ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx @@ -11,12 +11,16 @@ * limitations under the License. */ import { LeftOutlined, RightOutlined } from '@ant-design/icons'; -import { Badge, Button, Carousel } from 'antd'; -import classNames from 'classnames'; -import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Carousel } from 'antd'; +import React, { + RefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; -import UserPopOverCard from '../PopOverCard/UserPopOverCard'; -import ProfilePicture from '../ProfilePicture/ProfilePicture'; +import AvatarCarouselItem from '../AvatarCarouselItem/AvatarCarouselItem'; import './avatar-carousel.less'; interface AvatarCarouselProps { @@ -30,6 +34,7 @@ const AvatarCarousel = ({ showArrows = false }: AvatarCarouselProps) => { selectedUserSuggestions, } = useSuggestionsContext(); const [currentSlide, setCurrentSlide] = useState(-1); + const avatarBtnRefs = useRef[]>([]); const prevSlide = useCallback(() => { setCurrentSlide((prev) => (prev === 0 ? avatarList.length - 1 : prev - 1)); @@ -39,10 +44,17 @@ const AvatarCarousel = ({ showArrows = false }: AvatarCarouselProps) => { setCurrentSlide((prev) => (prev === avatarList.length - 1 ? 0 : prev + 1)); }, [avatarList]); + const handleMouseOut = useCallback(() => { + avatarBtnRefs.current.forEach((ref: any) => { + ref.current?.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + }); + }, [avatarBtnRefs]); + const onProfileClick = useCallback( (index: number) => { const activeUser = avatarList[index]; onUpdateActiveUser(activeUser); + handleMouseOut(); }, [avatarList] ); @@ -75,30 +87,16 @@ const AvatarCarousel = ({ showArrows = false }: AvatarCarouselProps) => { afterChange={(current) => setCurrentSlide(current)} dots={false} slidesToShow={avatarList.length < 3 ? avatarList.length : 3}> - {avatarList.map((avatar, index) => { - const isActive = currentSlide === index; - - const button = ( - - ); - - return ( - - {isActive ? ( // Show Badge only for active item - {button} - ) : ( - button - )} - - ); - })} + {avatarList.map((avatar, index) => ( + + ))} {showArrows && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less index a3059609a85..52be99a10d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less @@ -29,7 +29,7 @@ .avatar-carousel-container { .slick-slide { - width: 32px !important; + width: 28px !important; } .slick-list { overflow: visible !important; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx new file mode 100644 index 00000000000..d0eefcd2203 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { EntityReference } from '../../../generated/entity/type'; +import AvatarCarouselItem from './AvatarCarouselItem'; + +const suggestions = [ + { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + { + id: '2', + description: 'Test suggestion', + createdBy: { id: '2', name: 'Avatar 2', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, +]; + +const suggByUser = new Map([ + ['Avatar 1', [suggestions[0]]], + ['Avatar 2', [suggestions[1]]], +]); + +jest.mock('../../Suggestions/SuggestionsProvider/SuggestionsProvider', () => ({ + useSuggestionsContext: jest.fn().mockImplementation(() => ({ + suggestions: suggestions, + suggestionsByUser: suggByUser, + allSuggestionsUsers: [ + { id: '1', name: 'Avatar 1', type: 'user' }, + { id: '2', name: 'Avatar 2', type: 'user' }, + ], + acceptRejectSuggestion: jest.fn(), + selectedUserSuggestions: [], + onUpdateActiveUser: jest.fn(), + })), + __esModule: true, + default: 'SuggestionsProvider', +})); + +jest.mock('../../../rest/suggestionsAPI', () => ({ + getSuggestionsList: jest + .fn() + .mockImplementation(() => Promise.resolve(suggestions)), +})); + +describe('AvatarCarouselItem', () => { + const avatar: EntityReference = { + id: '1', + name: 'Test Avatar', + type: 'user', + }; + const index = 0; + const onAvatarClick = jest.fn(); + const avatarBtnRefs = { current: [] }; + const isActive = false; + + it('renders AvatarCarouselItem with ProfilePicture component', () => { + const { getByTestId } = render( + + ); + + expect( + getByTestId(`avatar-carousel-item-${avatar.id}`) + ).toBeInTheDocument(); + }); + + it('calls onAvatarClick function when clicked', () => { + const { getByTestId } = render( + + ); + + const button = getByTestId(`avatar-carousel-item-${avatar.id}`); + button.click(); + + expect(onAvatarClick).toHaveBeenCalledWith(index); + }); + + it('sets isActive class when isActive is true', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId(`avatar-carousel-item-${avatar.id}`)).toHaveClass( + 'active' + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.tsx new file mode 100644 index 00000000000..0863b84970f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarouselItem/AvatarCarouselItem.tsx @@ -0,0 +1,68 @@ +/* + * 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 { Badge, Button } from 'antd'; +import classNames from 'classnames'; +import React, { RefObject, useCallback, useRef } from 'react'; +import { EntityReference } from '../../../generated/entity/type'; +import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; +import UserPopOverCard from '../PopOverCard/UserPopOverCard'; +import ProfilePicture from '../ProfilePicture/ProfilePicture'; + +interface AvatarCarouselItemProps { + avatar: EntityReference; + index: number; + onAvatarClick: (index: number) => void; + avatarBtnRefs: React.MutableRefObject[]>; + isActive: boolean; +} + +const AvatarCarouselItem = ({ + avatar, + index, + avatarBtnRefs, + onAvatarClick, + isActive, +}: AvatarCarouselItemProps) => { + const { suggestionsByUser } = useSuggestionsContext(); + const buttonRef = useRef(null); + avatarBtnRefs.current[index] = buttonRef; + const getUserSuggestionsCount = useCallback( + (userName: string) => { + return suggestionsByUser.get(userName) ?? []; + }, + [suggestionsByUser] + ); + + const button = ( + + ); + + return ( + + + {button} + + + ); +}; + +export default AvatarCarouselItem;