From 689e5beaea8ca6959dd03b22cebf05a6156e0033 Mon Sep 17 00:00:00 2001 From: darth-coder00 <86726556+darth-coder00@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:39:09 +0530 Subject: [PATCH] Feat: Revamping Landing page for new design (#1541) * Feat: Revamping Landing page for new design * Added Filter support for feeds * Added Feeds card component. * Removed Unwanted codes * Added API support for feeddata * Adding types * Added support for view all following and owned data. * Adding day seperator. * Added support to view data owned by team. * Adding support for feedFilters * Moved sorting from feed component to mydada component. * Adding testid * Added License to new file * Adding utils to get rekative date and time. * Adding getOwnerIds Util * Adding No data placholder. Co-authored-by: Sachin-chaurasiya --- .../resources/ui/src/assets/svg/ingestion.svg | 11 +- .../src/components/EntityList/EntityList.tsx | 62 ++++ .../MyAssetStats/MyAssetStats.component.tsx | 137 ++++++++ .../MyAssetStats/MyAssetStats.test.tsx | 142 +++++++++ .../components/MyData/MyData.component.tsx | 299 ++++++++++-------- .../src/components/MyData/MyData.interface.ts | 26 +- .../ui/src/components/MyData/MyData.test.tsx | 8 + .../RecentSearchedTerms.tsx | 54 ++++ .../ui/src/components/app-bar/Appbar.tsx | 4 +- .../common/FeedCard/FeedCards.component.tsx | 80 +++++ .../src/components/onboarding/Onboarding.tsx | 41 +-- .../recently-viewed/RecentlyViewed.tsx | 36 +-- .../ui/src/constants/Mydata.constants.ts | 6 + .../resources/ui/src/constants/constants.ts | 2 +- .../ui/src/constants/explore.constants.ts | 4 +- .../resources/ui/src/enums/mydata.enum.ts | 6 + .../resources/ui/src/interface/types.d.ts | 6 +- .../pages/MyDataPage/MyDataPage.component.tsx | 141 +++++++-- .../ui/src/pages/database-details/index.tsx | 2 +- .../main/resources/ui/src/utils/APIUtils.js | 2 + .../resources/ui/src/utils/CommonUtils.tsx | 114 ++++--- .../ui/src/utils/EntityVersionUtils.tsx | 17 +- .../resources/ui/src/utils/MyDataUtils.ts | 25 ++ .../main/resources/ui/src/utils/TimeUtils.ts | 16 + .../src/main/resources/ui/tailwind.config.js | 2 +- 25 files changed, 975 insertions(+), 268 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/EntityList/EntityList.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/RecentSearchedTerms/RecentSearchedTerms.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/FeedCard/FeedCards.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/MyDataUtils.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ingestion.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ingestion.svg index 2e6b1a5632c..f17345127cf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ingestion.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ingestion.svg @@ -1,5 +1,6 @@ - - - - - \ No newline at end of file + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityList/EntityList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityList/EntityList.tsx new file mode 100644 index 00000000000..96e6dad84ca --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityList/EntityList.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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 { FormatedTableData } from 'Models'; +import React, { FunctionComponent } from 'react'; +import { Link } from 'react-router-dom'; +import { getEntityIcon, getEntityLink } from '../../utils/TableUtils'; + +interface Prop { + entityList: Array; + headerText: string | JSX.Element; + noDataPlaceholder: JSX.Element; + testIDText: string; +} + +const EntityList: FunctionComponent = ({ + entityList = [], + headerText, + noDataPlaceholder, + testIDText, +}: Prop) => { + return ( + <> +
+ {headerText} +
+ {entityList.length + ? entityList.map((item, index) => { + return ( +
+
+ {getEntityIcon(item.index)} + + + +
+
+ ); + }) + : noDataPlaceholder} + + ); +}; + +export default EntityList; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx new file mode 100644 index 00000000000..a0919a3d2f9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.component.tsx @@ -0,0 +1,137 @@ +/* + * Copyright 2021 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 { isNil } from 'lodash'; +import { observer } from 'mobx-react'; +import { EntityCounts } from 'Models'; +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import AppState from '../../AppState'; +import { getCountBadge } from '../../utils/CommonUtils'; +import SVGIcons, { Icons } from '../../utils/SvgUtils'; + +type Props = { + countServices: number; + ingestionCount: number; + entityCounts: EntityCounts; +}; +type Summary = { + icon: string; + data: string; + count?: number; + link?: string; + dataTestId?: string; +}; + +const MyAssetStats: FunctionComponent = ({ + countServices, + entityCounts, + ingestionCount, +}: Props) => { + const { users, userTeams } = AppState; + const [dataSummary, setdataSummary] = useState>({}); + + const getSummarydata = () => { + return { + tables: { + icon: Icons.TABLE_GREY, + data: 'Tables', + count: entityCounts.tableCount, + link: `/explore/tables`, + dataTestId: 'tables', + }, + topics: { + icon: Icons.TOPIC_GREY, + data: 'Topics', + count: entityCounts.topicCount, + link: `/explore/topics`, + dataTestId: 'topics', + }, + dashboards: { + icon: Icons.DASHBOARD_GREY, + data: 'Dashboards', + count: entityCounts.dashboardCount, + link: `/explore/dashboards`, + dataTestId: 'dashboards', + }, + pipelines: { + icon: Icons.PIPELINE_GREY, + data: 'Pipelines', + count: entityCounts.pipelineCount, + link: `/explore/pipelines`, + dataTestId: 'pipelines', + }, + service: { + icon: Icons.SERVICE, + data: 'Services', + count: countServices, + link: `/services`, + dataTestId: 'service', + }, + ingestion: { + icon: Icons.INGESTION, + data: 'Ingestion', + count: ingestionCount, + link: `/ingestion`, + dataTestId: 'ingestion', + }, + user: { + icon: Icons.USERS, + data: 'Users', + count: users.length, + link: `/teams`, + dataTestId: 'user', + }, + terms: { + icon: Icons.TERMS, + data: 'Teams', + count: userTeams.length, + link: `/teams`, + dataTestId: 'terms', + }, + }; + }; + + useEffect(() => { + setdataSummary(getSummarydata()); + }, [userTeams, users, countServices]); + + return ( +
+ {Object.values(dataSummary).map((data, index) => ( +
+
+ + {data.link ? ( + + + + ) : ( +

{data.data}

+ )} +
+ {!isNil(data.count) && getCountBadge(data.count, '', false)} +
+ ))} +
+ ); +}; + +export default observer(MyAssetStats); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx new file mode 100644 index 00000000000..f2e528a5984 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyAssetStats/MyAssetStats.test.tsx @@ -0,0 +1,142 @@ +/* + * Copyright 2021 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 { getByTestId, getByText, render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import MyAssetStats from './MyAssetStats.component'; + +describe('Test MyDataHeader Component', () => { + it('Component should render', () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const myDataHeader = getByTestId(container, 'data-header-container'); + + expect(myDataHeader).toBeInTheDocument(); + }); + + it('Should have main title', () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const mainTitle = getByTestId(container, 'main-title'); + + expect(mainTitle).toBeInTheDocument(); + }); + + it('Should have 7 data summary details', () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + const dataSummary = getByTestId(container, 'data-summary-container'); + + expect(dataSummary.childElementCount).toBe(7); + }); + + it('Should display same count as provided by props', () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(getByText(container, /40 tables/i)).toBeInTheDocument(); + expect(getByText(container, /13 topics/i)).toBeInTheDocument(); + expect(getByText(container, /10 dashboards/i)).toBeInTheDocument(); + expect(getByText(container, /3 pipelines/i)).toBeInTheDocument(); + expect(getByText(container, /4 services/i)).toBeInTheDocument(); + }); + + it('OnClick it should redirect to respective page', () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); + const tables = getByTestId(container, 'tables'); + const topics = getByTestId(container, 'topics'); + const dashboards = getByTestId(container, 'dashboards'); + const pipelines = getByTestId(container, 'pipelines'); + const service = getByTestId(container, 'service'); + const user = getByTestId(container, 'user'); + const terms = getByTestId(container, 'terms'); + + expect(tables).toHaveAttribute('href', '/explore/tables'); + expect(topics).toHaveAttribute('href', '/explore/topics'); + expect(dashboards).toHaveAttribute('href', '/explore/dashboards'); + expect(pipelines).toHaveAttribute('href', '/explore/pipelines'); + expect(service).toHaveAttribute('href', '/services'); + expect(user).toHaveAttribute('href', '/teams'); + expect(terms).toHaveAttribute('href', '/teams'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx index 2cae76c9e02..e071cc3908b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.component.tsx @@ -11,157 +11,206 @@ * limitations under the License. */ -import { isEmpty } from 'lodash'; -import { FormatedTableData } from 'Models'; -import React, { useEffect, useRef, useState } from 'react'; -import { Ownership } from '../../enums/mydata.enum'; -import { formatDataResponse } from '../../utils/APIUtils'; -import { getCurrentUserId } from '../../utils/CommonUtils'; +import { observer } from 'mobx-react'; +import React, { + Fragment, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { Link } from 'react-router-dom'; +import AppState from '../../AppState'; +import { getExplorePathWithSearch } from '../../constants/constants'; +import { filterList } from '../../constants/Mydata.constants'; +import { FeedFilter, Ownership } from '../../enums/mydata.enum'; +import { getOwnerIds } from '../../utils/CommonUtils'; +import { getSummary } from '../../utils/EntityVersionUtils'; +import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant'; +import { getRelativeDateByTimeStamp } from '../../utils/TimeUtils'; +import { Button } from '../buttons/Button/Button'; import ErrorPlaceHolderES from '../common/error-with-placeholder/ErrorPlaceHolderES'; -import PageContainer from '../containers/PageContainer'; -import MyDataHeader from '../MyDataHeader/MyDataHeader.component'; +import FeedCards from '../common/FeedCard/FeedCards.component'; +import PageLayout from '../containers/PageLayout'; +import DropDownList from '../dropdown/DropDownList'; +import EntityList from '../EntityList/EntityList'; +import MyAssetStats from '../MyAssetStats/MyAssetStats.component'; +import Onboarding from '../onboarding/Onboarding'; import RecentlyViewed from '../recently-viewed/RecentlyViewed'; -import SearchedData from '../searched-data/SearchedData'; +import RecentSearchedTerms from '../RecentSearchedTerms/RecentSearchedTerms'; import { MyDataProps } from './MyData.interface'; const MyData: React.FC = ({ error, countServices, ingestionCount, - userDetails, - searchResult, - fetchData, + ownedData, + followedData, entityCounts, + feedData, + feedFilter, + feedFilterHandler, }: MyDataProps): React.ReactElement => { - const [data, setData] = useState>([]); - const [currentPage, setCurrentPage] = useState(1); - const [totalNumberOfValue, setTotalNumberOfValues] = useState(0); - const [isEntityLoading, setIsEntityLoading] = useState(true); - const [currentTab, setCurrentTab] = useState(1); - const [filter, setFilter] = useState(''); - + const [fieldListVisible, setFieldListVisible] = useState(false); const isMounted = useRef(false); - const getActiveTabClass = (tab: number) => { - return tab === currentTab ? 'active' : ''; + const handleDropDown = ( + _e: React.MouseEvent, + value?: string + ) => { + feedFilterHandler((value as FeedFilter) || FeedFilter.ALL); + setFieldListVisible(false); }; - - const getFilters = (): string => { - if (filter === 'owner' && userDetails.teams) { - const userTeams = !isEmpty(userDetails) - ? userDetails.teams.map((team) => `${filter}:${team.id}`) - : []; - const ownerIds = [...userTeams, `${filter}:${getCurrentUserId()}`]; - - return `(${ownerIds.join(' OR ')})`; - } - - return `${filter}:${getCurrentUserId()}`; - }; - - const handleTabChange = (tab: number, filter: string) => { - if (currentTab !== tab) { - setIsEntityLoading(true); - setCurrentTab(tab); - setFilter(filter); - setCurrentPage(1); - } - }; - - const getTabs = () => { + const getFilterDropDown = () => { return ( -
- + +
+ + {fieldListVisible && ( + + )} +
+
+ ); + }; + + const getLinkByFilter = (filter: Ownership) => { + return `${getExplorePathWithSearch()}?${filter}=${getOwnerIds( + filter, + AppState.userDetails + ).join()}`; + }; + + const getLeftPanel = () => { + return ( +
+ +
+ +
+ +
); }; - const paginate = (pageNumber: number) => { - setCurrentPage(pageNumber); - }; + const getRightPanel = useCallback(() => { + return ( +
+ + My Data + {ownedData.length ? ( + + + View All + + + ) : null} +
+ } + noDataPlaceholder={<>You have not owned anything yet!} + testIDText="My data" + /> +
+ + Following + {followedData.length ? ( + + + View All + + + ) : null} +
+ } + noDataPlaceholder={<>You have not followed anything yet!} + testIDText="Following data" + /> +
+
+ ); + }, [ownedData, followedData]); - useEffect(() => { - if (isMounted.current && Boolean(currentTab === 2 || currentTab === 3)) { - setIsEntityLoading(true); - fetchData({ - queryString: '', - from: currentPage, - filters: filter ? getFilters() : '', - sortField: '', - sortOrder: '', - }); - } - }, [currentPage, filter]); + const getFeedsData = useCallback(() => { + const feeds = feedData + .map((f) => ({ + name: f.name, + fqn: f.fullyQualifiedName, + entityType: f.entityType, + changeDescriptions: f.changeDescriptions, + })) + .map((d) => { + return ( + d.changeDescriptions + .filter((c) => c.fieldsAdded || c.fieldsDeleted || c.fieldsUpdated) + .map((change) => ({ + updatedAt: change.updatedAt, + updatedBy: change.updatedBy, + entityName: d.name, + description:
{getSummary(change, true)}
, + entityType: d.entityType, + fqn: d.fqn, + relativeDay: getRelativeDateByTimeStamp(change.updatedAt), + })) || [] + ); + }) + .flat(1) + .sort((a, b) => b.updatedAt - a.updatedAt); + const relativeDays = [...new Set(feeds.map((f) => f.relativeDay))]; - useEffect(() => { - if (searchResult) { - const hits = searchResult.data.hits.hits; - if (hits.length > 0) { - setTotalNumberOfValues(searchResult.data.hits.total.value); - setData(formatDataResponse(hits)); - } else { - setData([]); - setTotalNumberOfValues(0); - } - } - - setIsEntityLoading(false); - }, [searchResult]); + return { feeds, relativeDays }; + }, [feedData]); useEffect(() => { isMounted.current = true; }, []); return ( - -
- - {getTabs()} - {error && Boolean(currentTab === 2 || currentTab === 3) ? ( - - ) : ( - 0 ? true : false} - totalValue={totalNumberOfValue}> - {currentTab === 1 ? : null} - - )} -
-
+ + {error ? ( + + ) : ( + + {getFeedsData().feeds.length > 0 ? ( + + {getFilterDropDown()} + + + ) : ( + + )} + + )} + ); }; -export default MyData; +export default observer(MyData); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.interface.ts index c16e1b022fe..62cb2d81415 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.interface.ts @@ -11,15 +11,33 @@ * limitations under the License. */ -import { EntityCounts, SearchDataFunctionType, SearchResponse } from 'Models'; -import { User } from '../../generated/entity/teams/user'; +import { + EntityCounts, + FormatedTableData, + SearchDataFunctionType, + SearchResponse, +} from 'Models'; +import { FeedFilter } from '../../enums/mydata.enum'; +import { ChangeDescription, User } from '../../generated/entity/teams/user'; export interface MyDataProps { error: string; ingestionCount: number; countServices: number; - userDetails: User; + userDetails?: User; searchResult: SearchResponse | undefined; - fetchData: (value: SearchDataFunctionType) => void; + ownedData: Array; + followedData: Array; + feedData: Array< + FormatedTableData & { + entityType: string; + changeDescriptions: Array< + ChangeDescription & { updatedAt: number; updatedBy: string } + >; + } + >; + feedFilter: string; + feedFilterHandler: (v: FeedFilter) => void; + fetchData?: (value: SearchDataFunctionType) => void; entityCounts: EntityCounts; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx index 8ddd9887df9..1e60425fe2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx @@ -23,6 +23,7 @@ import { SearchResponse } from 'Models'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { User } from '../../generated/entity/teams/user'; +import { formatDataResponse } from '../../utils/APIUtils'; import MyDataPage from './MyData.component'; const mockData = { @@ -247,6 +248,8 @@ jest.mock('../../utils/ServiceUtils', () => ({ getTotalEntityCountByService: jest.fn().mockReturnValue(2), })); +const feedFilterHandler = jest.fn(); + const fetchData = jest.fn(); describe('Test MyData page', () => { @@ -261,8 +264,13 @@ describe('Test MyData page', () => { pipelineCount: 1, }} error="" + feedData={formatDataResponse(mockData.data.hits.hits)} + feedFilter="" + feedFilterHandler={feedFilterHandler} fetchData={fetchData} + followedData={formatDataResponse(mockData.data.hits.hits)} ingestionCount={0} + ownedData={formatDataResponse(mockData.data.hits.hits)} searchResult={mockData as unknown as SearchResponse} userDetails={mockUserDetails as unknown as User} />, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/RecentSearchedTerms/RecentSearchedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/RecentSearchedTerms/RecentSearchedTerms.tsx new file mode 100644 index 00000000000..9095f02a15f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/RecentSearchedTerms/RecentSearchedTerms.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2021 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 React, { FunctionComponent } from 'react'; +import { Link } from 'react-router-dom'; +import { getExplorePathWithSearch } from '../../constants/constants'; +import { getRecentlySearchedData } from '../../utils/CommonUtils'; + +const RecentSearchedTerms: FunctionComponent = () => { + const recentlySearchedTerms = getRecentlySearchedData(); + + return ( + <> +
+ Recently Searched Terms +
+ {recentlySearchedTerms.length ? ( + recentlySearchedTerms.map((item, index) => { + return ( +
+
+ + + +
+
+ ); + }) + ) : ( + <>No searched terms! + )} + + ); +}; + +export default RecentSearchedTerms; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Appbar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Appbar.tsx index c301ef805df..328a9586a1b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Appbar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/app-bar/Appbar.tsx @@ -34,7 +34,7 @@ import { import { urlGitbookDocs, urlJoinSlack } from '../../constants/url.const'; import { useAuth } from '../../hooks/authHooks'; import { userSignOut } from '../../utils/AuthUtils'; -import { addToRecentSearch } from '../../utils/CommonUtils'; +import { addToRecentSearched } from '../../utils/CommonUtils'; import { inPageSearchOptions, isInPageSearchAllowed, @@ -215,7 +215,7 @@ const Appbar: React.FC = (): JSX.Element => { const target = e.target as HTMLInputElement; if (e.key === 'Enter') { setIsOpen(false); - addToRecentSearch(target.value); + addToRecentSearched(target.value); history.push( getExplorePathWithSearch( target.value, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedCard/FeedCards.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedCard/FeedCards.component.tsx new file mode 100644 index 00000000000..56437212810 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedCard/FeedCards.component.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2021 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 React, { FC, Fragment, ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { getEntityLink } from '../../../utils/TableUtils'; +import { getTimeByTimeStamp } from '../../../utils/TimeUtils'; +import Avatar from '../avatar/Avatar'; +interface Feed { + updatedAt: number; + updatedBy: string; + description: ReactNode; + entityName: string; + entityType: string; + fqn: string; + relativeDay: string; +} + +interface FeedCardsProp { + feeds: Array; + relativeDays: Array; +} + +const FeedCards: FC = ({ + feeds = [], + relativeDays = [], +}: FeedCardsProp) => { + return ( + + {relativeDays.map((d, i) => ( +
+
+
+
+ + {d} + +
+
+ {feeds + .filter((f) => f.relativeDay === d) + .map((feed, i) => ( +
+
+ +
+ {feed.updatedBy} + + updated{' '} + + {feed.entityName} + + + {getTimeByTimeStamp(feed.updatedAt)} + + +
+
+
{feed.description}
+
+ ))} +
+ ))} +
+ ); +}; + +export default FeedCards; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/onboarding/Onboarding.tsx b/openmetadata-ui/src/main/resources/ui/src/components/onboarding/Onboarding.tsx index a359a4411c5..dccb0b5d33b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/onboarding/Onboarding.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/onboarding/Onboarding.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import React from 'react'; +import classNames from 'classnames'; +import React, { FC } from 'react'; import SVGIcons, { Icons } from '../../utils/SvgUtils'; const data = [ @@ -20,10 +21,18 @@ const data = [ 'Follow the datasets that you frequently use to stay informed about it.', ]; -const Onboarding: React.FC = () => { +interface OnboardingProp { + showLogo?: boolean; +} + +const Onboarding: FC = ({ + showLogo = true, +}: OnboardingProp) => { return (
@@ -49,21 +58,17 @@ const Onboarding: React.FC = () => { ))}
- -
- {/* */} - -
+ {showLogo ? ( +
+ +
+ ) : null}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/recently-viewed/RecentlyViewed.tsx b/openmetadata-ui/src/main/resources/ui/src/components/recently-viewed/RecentlyViewed.tsx index 18573b74d68..911d9ac139f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/recently-viewed/RecentlyViewed.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/recently-viewed/RecentlyViewed.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { isString } from 'lodash'; import { FormatedTableData } from 'Models'; import React, { FunctionComponent, useEffect, useState } from 'react'; import { getDashboardByFqn } from '../../axiosAPIs/dashboardAPI'; @@ -26,9 +25,8 @@ import { } from '../../utils/CommonUtils'; import { getOwnerFromId, getTierTags } from '../../utils/TableUtils'; import { getTableTags } from '../../utils/TagsUtils'; -import TableDataCard from '../common/table-data-card/TableDataCard'; +import EntityList from '../EntityList/EntityList'; import Loader from '../Loader/Loader'; -import Onboarding from '../onboarding/Onboarding'; const RecentlyViewed: FunctionComponent = () => { const recentlyViewedData = getRecentlyViewedData(); @@ -184,32 +182,12 @@ const RecentlyViewed: FunctionComponent = () => { {isLoading ? ( ) : ( - <> - {data.length ? ( - data.map((item, index) => { - return ( -
- -
- ); - }) - ) : ( - - )} - + No recently viewed data!} + testIDText="Recently Viewed" + /> )} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Mydata.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Mydata.constants.ts index d1992a530bb..6d30a59b045 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Mydata.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Mydata.constants.ts @@ -50,3 +50,9 @@ export const getFilters = ( : `${facetFilterString}` }`; }; + +export const filterList = [ + { name: 'All Activity Feeds', value: 'all' }, + { name: 'My Data Activity Feeds', value: 'owner' }, + { name: 'Followed Data Activity Feeds', value: 'followers' }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 830f221243f..0d78687b633 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -19,7 +19,7 @@ export const LIST_SIZE = 5; export const SIDEBAR_WIDTH_COLLAPSED = 290; export const SIDEBAR_WIDTH_EXPANDED = 290; export const LOCALSTORAGE_RECENTLY_VIEWED = 'recentlyViewedData'; -export const LOCALSTORAGE_RECENTLY_SEARCH = 'recentlySearchData'; +export const LOCALSTORAGE_RECENTLY_SEARCHED = 'recentlySearchedData'; export const oidcTokenKey = 'oidcIdToken'; export const imageTypes = { image: 's96-c', diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/explore.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/explore.constants.ts index 6bc52cde596..5538fd59099 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/explore.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/explore.constants.ts @@ -49,7 +49,9 @@ export const getQueryParam = (urlSearchQuery = ''): FilterObject => { .map((filter) => { const arrFilter = filter.split('='); - return { [arrFilter[0]]: [arrFilter[1]] }; + return { + [arrFilter[0]]: [arrFilter[1]].map((r) => r.split(',')).flat(1), + }; }) .reduce((prev, curr) => { return Object.assign(prev, curr); diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/mydata.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/mydata.enum.ts index b22284696f1..9878c7c4ea1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/mydata.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/mydata.enum.ts @@ -15,3 +15,9 @@ export enum Ownership { OWNER = 'owner', FOLLOWERS = 'followers', } + +export enum FeedFilter { + ALL = 'all', + OWNED = 'owner', + FOLLOWING = 'followers', +} diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts index 567464b2b02..91bbda7cc9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts @@ -391,15 +391,15 @@ declare module 'Models' { timestamp: number; } - interface RecentlySearchData { + interface RecentlySearchedData { term: string; timestamp: number; } export interface RecentlyViewed { data: Array; } - export interface SearchData { - data: Array; + export interface RecentlySearched { + data: Array; } export type DatasetSchemaTableTab = 'schema' | 'sample_data'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx index 32292428707..8cc78b55677 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx @@ -11,25 +11,33 @@ * limitations under the License. */ -import { AxiosError } from 'axios'; -import { isUndefined } from 'lodash'; +import { AxiosError, AxiosResponse } from 'axios'; +import { isEmpty, isNil, isUndefined } from 'lodash'; import { observer } from 'mobx-react'; -import { EntityCounts, SearchDataFunctionType, SearchResponse } from 'Models'; +import { EntityCounts, FormatedTableData, SearchResponse } from 'Models'; import React, { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import AppState from '../../AppState'; import { getIngestionWorkflows } from '../../axiosAPIs/ingestionWorkflowAPI'; import { searchData } from '../../axiosAPIs/miscAPI'; +import PageContainerV1 from '../../components/containers/PageContainerV1'; import Loader from '../../components/Loader/Loader'; import MyData from '../../components/MyData/MyData.component'; -import { PAGE_SIZE } from '../../constants/constants'; import { myDataEntityCounts, myDataSearchIndex, } from '../../constants/Mydata.constants'; +import { FeedFilter, Ownership } from '../../enums/mydata.enum'; +import { ChangeDescription } from '../../generated/entity/teams/user'; +import { useAuth } from '../../hooks/authHooks'; +import { formatDataResponse } from '../../utils/APIUtils'; import { getEntityCountByType } from '../../utils/EntityUtils'; +import { getMyDataFilters } from '../../utils/MyDataUtils'; import { getAllServices } from '../../utils/ServiceUtils'; const MyDataPage = () => { + const location = useLocation(); + const { isAuthDisabled } = useAuth(location.pathname); const [error, setError] = useState(''); const [countServices, setCountServices] = useState(); const [ingestionCount, setIngestionCount] = useState(); @@ -37,18 +45,28 @@ const MyDataPage = () => { const [searchResult, setSearchResult] = useState(); const [entityCounts, setEntityCounts] = useState(); - const fetchData = (value: SearchDataFunctionType, fetchService = false) => { + const [ownedData, setOwnedData] = useState>(); + const [followedData, setFollowedData] = useState>(); + const [feedData, setFeedData] = useState< + Array< + FormatedTableData & { + entityType: string; + changeDescriptions: Array< + ChangeDescription & { updatedAt: number; updatedBy: string } + >; + } + > + >(); + const [feedFilter, setFeedFilter] = useState(FeedFilter.ALL); + + const feedFilterHandler = (filter: FeedFilter) => { + setFeedFilter(filter); + }; + + const fetchData = (fetchService = false) => { setError(''); - searchData( - value.queryString, - value.from, - value.size ?? PAGE_SIZE, - value.filters, - value.sortField, - value.sortOrder, - myDataSearchIndex - ) + searchData('', 1, 0, '', '', '', myDataSearchIndex) .then((res: SearchResponse) => { setSearchResult(res); if (isUndefined(entityCounts)) { @@ -75,22 +93,84 @@ const MyDataPage = () => { setIsLoading(false); }; - useEffect(() => { - fetchData( - { - queryString: '', - from: 1, - filters: '', - size: 0, - sortField: '', - sortOrder: '', - }, - isUndefined(countServices) + const fetchMyData = () => { + const ownedEntity = searchData( + '', + 1, + 5, + getMyDataFilters(Ownership.OWNER, AppState.userDetails), + 'last_updated_timestamp', + '', + myDataSearchIndex ); + + const followedEntity = searchData( + '', + 1, + 5, + getMyDataFilters(Ownership.FOLLOWERS, AppState.userDetails), + 'last_updated_timestamp', + '', + myDataSearchIndex + ); + + Promise.allSettled([ownedEntity, followedEntity]).then( + ([resOwnedEntity, resFollowedEntity]) => { + if (resOwnedEntity.status === 'fulfilled') { + setOwnedData(formatDataResponse(resOwnedEntity.value.data.hits.hits)); + } + if (resFollowedEntity.status === 'fulfilled') { + setFollowedData( + formatDataResponse(resFollowedEntity.value.data.hits.hits) + ); + } + } + ); + }; + + const getFeedData = () => { + searchData( + '', + 1, + 20, + feedFilter !== FeedFilter.ALL + ? getMyDataFilters( + feedFilter === FeedFilter.OWNED + ? Ownership.OWNER + : Ownership.FOLLOWERS, + AppState.userDetails + ) + : '', + 'last_updated_timestamp', + '', + myDataSearchIndex + ).then((res: AxiosResponse) => { + if (res.data) { + setFeedData(formatDataResponse(res.data.hits.hits)); + } + }); + }; + + useEffect(() => { + fetchData(true); }, []); + useEffect(() => { + getFeedData(); + }, [feedFilter]); + + useEffect(() => { + if ( + ((isAuthDisabled && AppState.users.length) || + !isEmpty(AppState.userDetails)) && + (isNil(ownedData) || isNil(followedData)) + ) { + fetchMyData(); + } + }, [AppState.userDetails, AppState.users, isAuthDisabled]); + return ( - <> + {!isUndefined(countServices) && !isUndefined(entityCounts) && !isUndefined(ingestionCount) && @@ -99,15 +179,18 @@ const MyDataPage = () => { countServices={countServices} entityCounts={entityCounts} error={error} - fetchData={fetchData} + feedData={feedData || []} + feedFilter={feedFilter} + feedFilterHandler={feedFilterHandler} + followedData={followedData || []} ingestionCount={ingestionCount} + ownedData={ownedData || []} searchResult={searchResult} - userDetails={AppState.userDetails} /> ) : ( )} - + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx index ac9b718f6aa..1c8c98412da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx @@ -373,7 +373,7 @@ const DatabaseDetails: FunctionComponent = () => { setActiveTab={activeTabHandler} tabs={tabs} /> -
+
{activeTab === 1 && ( <> { newData.tier = hit._source.tier; newData.owner = hit._source.owner; newData.highlight = hit.highlight; + newData.entityType = hit._source.entity_type; + newData.changeDescriptions = hit._source.change_descriptions; return newData; }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 607a3ff90b5..9b177221340 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -14,19 +14,21 @@ import classNames from 'classnames'; import { isEmpty, isUndefined } from 'lodash'; import { - RecentlySearchData, + RecentlySearched, + RecentlySearchedData, RecentlyViewed, RecentlyViewedData, - SearchData, } from 'Models'; import React from 'react'; import { reactLocalStorage } from 'reactjs-localstorage'; import AppState from '../AppState'; import { - LOCALSTORAGE_RECENTLY_SEARCH, + LOCALSTORAGE_RECENTLY_SEARCHED, LOCALSTORAGE_RECENTLY_VIEWED, TITLE_FOR_NON_OWNER_ACTION, } from '../constants/constants'; +import { Ownership } from '../enums/mydata.enum'; +import { User } from '../generated/entity/teams/user'; import { UserTeam } from '../interface/team.interface'; export const arraySorterByKey = ( @@ -152,11 +154,51 @@ export const getCountBadge = ( ); }; -export const addToRecentSearch = (searchTerm: string): void => { +export const getRecentlyViewedData = (): Array => { + const recentlyViewed: RecentlyViewed = reactLocalStorage.getObject( + LOCALSTORAGE_RECENTLY_VIEWED + ) as RecentlyViewed; + + if (recentlyViewed?.data) { + return recentlyViewed.data; + } + + return []; +}; + +export const getRecentlySearchedData = (): Array => { + const recentlySearch: RecentlySearched = reactLocalStorage.getObject( + LOCALSTORAGE_RECENTLY_SEARCHED + ) as RecentlySearched; + if (recentlySearch?.data) { + return recentlySearch.data; + } + + return []; +}; + +export const setRecentlyViewedData = ( + recentData: Array +): void => { + reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_VIEWED, { + data: recentData, + }); +}; + +export const setRecentlySearchedData = ( + recentData: Array +): void => { + reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_SEARCHED, { + data: recentData, + }); +}; + +export const addToRecentSearched = (searchTerm: string): void => { const searchData = { term: searchTerm, timestamp: Date.now() }; - let recentlySearch: SearchData = reactLocalStorage.getObject( - LOCALSTORAGE_RECENTLY_SEARCH - ) as SearchData; + const recentlySearch: RecentlySearched = reactLocalStorage.getObject( + LOCALSTORAGE_RECENTLY_SEARCHED + ) as RecentlySearched; + let arrSearchedData: RecentlySearched['data'] = []; if (recentlySearch?.data) { const arrData = recentlySearch.data // search term is case-insensetive so we should also take care of it. @@ -164,8 +206,8 @@ export const addToRecentSearch = (searchTerm: string): void => { .filter((item) => item.term !== searchData.term) .sort( arraySorterByKey('timestamp', true) as ( - a: RecentlySearchData, - b: RecentlySearchData + a: RecentlySearchedData, + b: RecentlySearchedData ) => number ); arrData.unshift(searchData); @@ -173,13 +215,11 @@ export const addToRecentSearch = (searchTerm: string): void => { if (arrData.length > 5) { arrData.pop(); } - recentlySearch.data = arrData; + arrSearchedData = arrData; } else { - recentlySearch = { - data: [searchData], - }; + arrSearchedData = [searchData]; } - reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_SEARCH, recentlySearch); + setRecentlySearchedData(arrSearchedData); }; export const addToRecentViewed = (eData: RecentlyViewedData): void => { @@ -210,37 +250,6 @@ export const addToRecentViewed = (eData: RecentlyViewedData): void => { reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_VIEWED, recentlyViewed); }; -export const getRecentlyViewedData = (): Array => { - const recentlyViewed: RecentlyViewed = reactLocalStorage.getObject( - LOCALSTORAGE_RECENTLY_VIEWED - ) as RecentlyViewed; - - if (recentlyViewed?.data) { - return recentlyViewed.data; - } - - return []; -}; - -export const getRecentlySearchData = (): Array => { - const recentlySearch: SearchData = reactLocalStorage.getObject( - LOCALSTORAGE_RECENTLY_SEARCH - ) as SearchData; - if (recentlySearch?.data) { - return recentlySearch.data; - } - - return []; -}; - -export const setRecentlyViewedData = ( - recentData: Array -): void => { - reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_VIEWED, { - data: recentData, - }); -}; - export const getHtmlForNonAdminAction = (isClaimOwner: boolean) => { return ( <> @@ -249,3 +258,18 @@ export const getHtmlForNonAdminAction = (isClaimOwner: boolean) => { ); }; + +export const getOwnerIds = ( + filter: Ownership, + userDetails: User +): Array => { + if (filter === Ownership.OWNER && userDetails.teams) { + const userTeams = !isEmpty(userDetails) + ? userDetails.teams.map((team) => team.id) + : []; + + return [...userTeams, getCurrentUserId()]; + } else { + return [getCurrentUserId()]; + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx index 827b9f6f452..a3d9a041d31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx @@ -189,7 +189,10 @@ export const summaryFormatter = (v: FieldChange) => { } }; -export const getSummary = (changeDescription: ChangeDescription) => { +export const getSummary = ( + changeDescription: ChangeDescription, + isPrefix = false +) => { const fieldsAdded = [...(changeDescription?.fieldsAdded || [])]; const fieldsDeleted = [...(changeDescription?.fieldsDeleted || [])]; const fieldsUpdated = [...(changeDescription?.fieldsUpdated || [])]; @@ -198,17 +201,23 @@ export const getSummary = (changeDescription: ChangeDescription) => { {fieldsAdded?.length > 0 ? (

- {fieldsAdded?.map(summaryFormatter).join(', ')} has been added + {`${isPrefix ? '+ Added' : ''} ${fieldsAdded + ?.map(summaryFormatter) + .join(', ')} ${!isPrefix ? `has been added` : ''}`}{' '}

) : null} {fieldsUpdated?.length ? (

- {fieldsUpdated?.map(summaryFormatter).join(', ')} has been updated + {`${isPrefix ? 'Edited' : ''} ${fieldsUpdated + ?.map(summaryFormatter) + .join(', ')} ${!isPrefix ? `has been updated` : ''}`}{' '}

) : null} {fieldsDeleted?.length ? (

- {fieldsDeleted?.map(summaryFormatter).join(', ')} has been deleted + {`${isPrefix ? '- Removed' : ''} ${fieldsDeleted + ?.map(summaryFormatter) + .join(', ')} ${!isPrefix ? `has been Deleted` : ''}`}{' '}

) : null}
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MyDataUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MyDataUtils.ts new file mode 100644 index 00000000000..ab86e5f56a9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MyDataUtils.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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 { Ownership } from '../enums/mydata.enum'; +import { User } from '../generated/entity/teams/user'; +import { getOwnerIds } from './CommonUtils'; + +export const getMyDataFilters = ( + filter: Ownership, + userDetails: User +): string => { + return `(${getOwnerIds(filter, userDetails) + .map((id) => `${filter}:${id}`) + .join(' OR ')})`; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts index 57b93f48479..43aef7fbc23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import moment from 'moment'; const msPerSecond = 1000; const msPerMinute = 60 * msPerSecond; @@ -87,3 +88,18 @@ export const getRelativeTime = (timestamp: number): string => { export const getRelativeDay = (timestamp: number): string => { return getRelativeDayDifference(Date.now(), timestamp); }; + +export const getRelativeDateByTimeStamp = (timeStamp: number): string => { + return moment(timeStamp).calendar(null, { + sameDay: '[Today]', + nextDay: 'DD MMMM YYYY', + nextWeek: 'DD MMMM YYYY', + lastDay: '[Yesterday]', + lastWeek: 'DD MMMM YYYY', + sameElse: 'DD MMMM YYYY', + }); +}; + +export const getTimeByTimeStamp = (timeStamp: number): string => { + return moment(timeStamp, 'x').format('hh:mm A'); +}; diff --git a/openmetadata-ui/src/main/resources/ui/tailwind.config.js b/openmetadata-ui/src/main/resources/ui/tailwind.config.js index 272173d0aca..5414f049255 100644 --- a/openmetadata-ui/src/main/resources/ui/tailwind.config.js +++ b/openmetadata-ui/src/main/resources/ui/tailwind.config.js @@ -125,7 +125,7 @@ module.exports = { 120: '30rem', }, minWidth: { - badgeCount: '24px', + badgeCount: '30px', }, maxHeight: { 32: '8rem',