diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CreateUser/CreateUser.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CreateUser/CreateUser.component.tsx index 37d7395e3be..da6780d5b5e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CreateUser/CreateUser.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/CreateUser/CreateUser.component.tsx @@ -42,6 +42,7 @@ const CreateUser = ({ const markdownRef = useRef(); const [description] = useState(''); const [email, setEmail] = useState(''); + const [displayName, setDisplayName] = useState(''); const [isAdmin, setIsAdmin] = useState(false); const [isBot, setIsBot] = useState(false); const [selectedRoles, setSelectedRoles] = useState>( @@ -52,6 +53,7 @@ const CreateUser = ({ ); const [showErrorMsg, setShowErrorMsg] = useState({ email: false, + displayName: false, validEmail: false, }); @@ -77,6 +79,12 @@ const CreateUser = ({ break; + case 'displayName': + setDisplayName(value); + setShowErrorMsg({ ...showErrorMsg, displayName: false }); + + break; + default: break; } @@ -166,6 +174,7 @@ const CreateUser = ({ const userProfile: CreateUserSchema = { description: markdownRef.current?.getEditorContent() || undefined, name: email.split('@')[0], + displayName, roles: validRole.length ? validRole : undefined, teams: validTeam.length ? validTeam : undefined, email: email, @@ -223,7 +232,7 @@ const CreateUser = ({
Create User
+ + + + + {showErrorMsg.email + ? errorMsg(jsonData['form-error-messages']['empty-email']) + : showErrorMsg.validEmail + ? errorMsg(jsonData['form-error-messages']['invalid-email']) + : null} + - + { return jest.fn().mockReturnValue(

Tabs

); }); +jest.mock('../ActivityFeed/ActivityFeedList/ActivityFeedList.tsx', () => { + return jest.fn().mockReturnValue(

FeedCards

); +}); + +const mockObserve = jest.fn(); +const mockunObserve = jest.fn(); + +window.IntersectionObserver = jest.fn().mockImplementation(() => ({ + observe: mockObserve, + unobserve: mockunObserve, +})); + +const mockFetchFeedHandler = jest.fn(); +const feedFilterHandler = jest.fn(); +const fetchData = jest.fn(); +const postFeed = jest.fn(); +const mockPaging = { + after: 'MTY0OTIzNTQ3MzExMg==', + total: 202, +}; + +const mockProp = { + feedData: [], + feedFilter: FeedFilter.ALL, + feedFilterHandler: feedFilterHandler, + fetchData: fetchData, + fetchFeedHandler: mockFetchFeedHandler, + isFeedLoading: false, + paging: mockPaging, + postFeedHandler: postFeed, +}; + describe('Test User Component', () => { it('Should render user component', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); const tabs = await findByTestId(container, 'tabs'); - const noAssets = await findByTestId(container, 'no-assets'); const leftPanel = await findByTestId(container, 'left-panel'); expect(tabs).toBeInTheDocument(); - expect(noAssets).toBeInTheDocument(); expect(leftPanel).toBeInTheDocument(); }); it('Should render non deleted teams', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); const teamFinance = await findByTestId(container, 'Finance'); const teamDataPlatform = await findByTestId(container, 'Data_Platform'); @@ -130,12 +167,30 @@ describe('Test User Component', () => { }); it('Should not render deleted teams', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); const deletedTeam = queryByTestId(container, 'Customer_Support'); expect(deletedTeam).not.toBeInTheDocument(); }); + + it('Should create an observer if IntersectionObserver is available', async () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const obServerElement = await findByTestId(container, 'observer-element'); + + expect(obServerElement).toBeInTheDocument(); + + expect(mockObserve).toHaveBeenCalled(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx index 40d10127b06..a63315192dd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx @@ -11,28 +11,77 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import { EntityThread } from 'Models'; +import React, { Fragment, RefObject, useEffect, useState } from 'react'; +import { filterList, observerOptions } from '../../constants/Mydata.constants'; import { AssetsType } from '../../enums/entity.enum'; +import { FeedFilter } from '../../enums/mydata.enum'; import { EntityReference, User } from '../../generated/entity/teams/user'; +import { Paging } from '../../generated/type/paging'; +import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'; import UserCard from '../../pages/teams/UserCard'; import { getNonDeletedTeams } from '../../utils/CommonUtils'; +import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; +import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList'; +import { Button } from '../buttons/Button/Button'; import Avatar from '../common/avatar/Avatar'; import TabsPane from '../common/TabsPane/TabsPane'; import PageLayout from '../containers/PageLayout'; +import DropDownList from '../dropdown/DropDownList'; +import Loader from '../Loader/Loader'; +import Onboarding from '../onboarding/Onboarding'; -type Props = { +interface Props { userData: User; -}; + feedData: EntityThread[]; + feedFilter: FeedFilter; + paging: Paging; + isFeedLoading: boolean; + feedFilterHandler: (v: FeedFilter) => void; + fetchFeedHandler: (filterType: FeedFilter, after?: string) => void; + postFeedHandler: (value: string, id: string) => void; + deletePostHandler?: (threadId: string, postId: string) => void; +} -const Users = ({ userData }: Props) => { +const Users = ({ + userData, + feedData, + feedFilter, + feedFilterHandler, + isFeedLoading, + postFeedHandler, + deletePostHandler, + fetchFeedHandler, + paging, +}: Props) => { const [activeTab, setActiveTab] = useState(1); - + const [fieldListVisible, setFieldListVisible] = useState(false); + const [elementRef, isInView] = useInfiniteScroll(observerOptions); const activeTabHandler = (tab: number) => { setActiveTab(tab); }; + const handleDropDown = ( + _e: React.MouseEvent, + value?: string + ) => { + feedFilterHandler((value as FeedFilter) || FeedFilter.ALL); + setFieldListVisible(false); + }; + const tabs = [ + { + name: 'Activity Feed', + icon: { + alt: 'activity_feed', + name: 'activity_feed', + title: 'Activity Feed', + selectedName: 'activity-feed-color', + }, + isProtected: false, + position: 1, + }, { name: 'Owned Data', icon: { @@ -42,7 +91,7 @@ const Users = ({ userData }: Props) => { selectedName: 'owned-data', }, isProtected: false, - position: 1, + position: 2, }, { name: 'Following', @@ -53,7 +102,7 @@ const Users = ({ userData }: Props) => { selectedName: 'following', }, isProtected: false, - position: 2, + position: 3, }, ]; @@ -88,6 +137,7 @@ const Users = ({ userData }: Props) => {

{userData.email}

+

{userData.description}

Teams
@@ -150,6 +200,80 @@ const Users = ({ userData }: Props) => { ); }; + const getFilterDropDown = () => { + return ( + +
+ + {fieldListVisible && ( + + )} +
+
+ ); + }; + + const getLoader = () => { + return isFeedLoading ? : null; + }; + + const getFeedTabData = () => { + return ( + + {feedData?.length > 0 || feedFilter !== FeedFilter.ALL ? ( + + {getFilterDropDown()} + + + ) : ( + + )} +
}> + {getLoader()} +
+
+ ); + }; + + const fetchMoreFeed = ( + isElementInView: boolean, + pagingObj: Paging, + isLoading: boolean + ) => { + if (isElementInView && pagingObj?.after && !isLoading) { + fetchFeedHandler(feedFilter, pagingObj.after); + } + }; + + useEffect(() => { + fetchMoreFeed(isInView as boolean, paging, isFeedLoading); + }, [isInView, paging, isFeedLoading]); + return (
@@ -161,14 +285,15 @@ const Users = ({ userData }: Props) => { />
- {activeTab === 1 && + {activeTab === 1 && getFeedTabData()} + {activeTab === 2 && getEntityData( getAssets(userData?.owns || []), `${ userData?.displayName || userData?.name || 'User' } does not own anything yet` )} - {activeTab === 2 && + {activeTab === 3 && getEntityData( getAssets(userData?.follows || []), `${ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx index d6a00cfa269..2a475e7faa7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx @@ -12,21 +12,37 @@ */ import { AxiosError, AxiosResponse } from 'axios'; +import { observer } from 'mobx-react'; +import { EntityThread } from 'Models'; import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; +import AppState from '../../AppState'; +import { getFeedsWithFilter, postFeedById } from '../../axiosAPIs/feedsAPI'; import { getUserByName } from '../../axiosAPIs/userAPI'; import PageContainerV1 from '../../components/containers/PageContainerV1'; import Loader from '../../components/Loader/Loader'; import Users from '../../components/Users/Users.component'; +import { + onConfirmText, + onErrorText, + onUpdatedConversastionError, +} from '../../constants/feed.constants'; +import { FeedFilter } from '../../enums/mydata.enum'; import { User } from '../../generated/entity/teams/user'; +import { Paging } from '../../generated/type/paging'; import jsonData from '../../jsons/en'; -import { showErrorToast } from '../../utils/ToastUtils'; +import { deletePost, getUpdatedThread } from '../../utils/FeedUtils'; +import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; const UserPage = () => { const { username } = useParams<{ [key: string]: string }>(); const [isLoading, setIsLoading] = useState(true); const [userData, setUserData] = useState({} as User); const [isError, setIsError] = useState(false); + const [feedFilter, setFeedFilter] = useState(FeedFilter.ALL); + const [entityThread, setEntityThread] = useState([]); + const [isFeedLoading, setIsFeedLoading] = useState(false); + const [paging, setPaging] = useState({} as Paging); const fetchUserData = () => { getUserByName(username, 'profile,roles,teams,follows,owns') @@ -63,16 +79,114 @@ const UserPage = () => { ); }; + const feedFilterHandler = (filter: FeedFilter) => { + setFeedFilter(filter); + }; + + const getFeedData = (filterType: FeedFilter, after?: string) => { + setIsFeedLoading(true); + const currentUserId = AppState.getCurrentUserDetails()?.id; + getFeedsWithFilter(currentUserId, filterType, after) + .then((res: AxiosResponse) => { + const { data, paging: pagingObj } = res.data; + setPaging(pagingObj); + + setEntityThread((prevData) => [...prevData, ...data]); + }) + .catch((err: AxiosError) => { + showErrorToast( + err, + jsonData['api-error-messages']['fetch-activity-feed-error'] + ); + }) + .finally(() => { + setIsFeedLoading(false); + }); + }; + + const postFeedHandler = (value: string, id: string) => { + const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name; + + const data = { + message: value, + from: currentUser, + }; + postFeedById(id, data) + .then((res: AxiosResponse) => { + if (res.data) { + const { id, posts } = res.data; + setEntityThread((pre) => { + return pre.map((thread) => { + if (thread.id === id) { + return { ...res.data, posts: posts.slice(-3) }; + } else { + return thread; + } + }); + }); + } + }) + .catch((err: AxiosError) => { + showErrorToast(err, jsonData['api-error-messages']['feed-post-error']); + }); + }; + + const deletePostHandler = (threadId: string, postId: string) => { + deletePost(threadId, postId) + .then(() => { + getUpdatedThread(threadId) + .then((data) => { + setEntityThread((pre) => { + return pre.map((thread) => { + if (thread.id === data.id) { + return { + ...thread, + posts: data.posts.slice(-3), + postsCount: data.postsCount, + }; + } else { + return thread; + } + }); + }); + }) + .catch((error) => { + const message = error?.message; + showErrorToast(message ?? onUpdatedConversastionError); + }); + showSuccessToast(onConfirmText); + }) + .catch((error) => { + const message = error?.message; + showErrorToast(message ?? onErrorText); + }); + }; + useEffect(() => { fetchUserData(); }, [username]); + useEffect(() => { + getFeedData(feedFilter); + setEntityThread([]); + }, [feedFilter]); + return ( {isLoading ? ( ) : !isError ? ( - + ) : ( )} @@ -80,4 +194,4 @@ const UserPage = () => { ); }; -export default UserPage; +export default observer(UserPage);