mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 12:08:31 +00:00
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 <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
parent
64386b035e
commit
689e5beaea
@ -1,5 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="512" height="512" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<path d="M481.429,332.892c-26.337-26.357-62.882-37.523-109.815-24.945L204.256,140.419l2.212-8.364 c9.639-36.166-0.776-75.041-27.172-101.437C152.42,3.721,114.212-6.148,78.077,3.778c-5.153,1.415-9.164,5.464-10.529,10.631 c-1.365,5.167,0.132,10.659,3.909,14.438l40.297,40.297c11.781,11.81,11.666,30.724,0.029,42.392 c-11.545,11.576-30.951,11.558-42.45,0.029L29.028,71.257c-3.779-3.781-9.287-5.264-14.454-3.891 c-5.168,1.372-9.202,5.393-10.612,10.551c-9.781,35.738-0.159,74.183,26.846,101.188c26.326,26.345,62.825,37.551,109.786,24.946 l167.371,167.528c-12.49,46.919-1.716,83.11,24.975,109.801c26.91,26.93,65.136,36.726,101.192,26.833 c5.154-1.414,9.166-5.464,10.532-10.631c1.366-5.167-0.13-10.66-3.909-14.44l-40.288-40.288 c-11.781-11.81-11.666-30.726-0.029-42.392c11.689-11.629,31.052-11.444,42.45-0.015l40.308,40.297 c3.779,3.779,9.287,5.262,14.453,3.889c5.167-1.373,9.201-5.392,10.611-10.549C518.041,398.352,508.421,359.897,481.429,332.892z" fill="#6b7280" data-original="#000000" style=""></path>
|
||||
<path d="M160.551,266.584L17.559,409.594c-23.401,23.401-23.401,61.455,0,84.855c23.401,23.401,61.455,23.401,84.855,0 l142.989-143.006L160.551,266.584z M88.322,447.898c-5.86,5.86-15.35,5.86-21.21,0c-5.859-5.859-5.859-15.351,0-21.21 l90.98-90.997c5.859-5.859,15.352-5.859,21.21,0c5.859,5.859,5.859,15.351,0,21.21L88.322,447.898z" fill="#6b7280" data-original="#000000" style=""></path>
|
||||
<path d="M507.596,30.253L481.737,4.394c-4.867-4.867-12.42-5.797-18.322-2.258l-79.547,47.723 c-8.37,5.021-9.791,16.568-2.891,23.469l6.332,6.33l-100.98,100.567l42.435,42.435l100.98-100.567l8.919,8.921 c6.901,6.899,18.449,5.479,23.469-2.891l47.723-79.547C513.393,42.673,512.463,35.12,507.596,30.253z" fill="#6b7280" data-original="#000000" style=""></path>
|
||||
</svg>
|
||||
<svg width="400" height="399" viewBox="0 0 400 399" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M400 190.5V150.5H354C318 150.5 285.333 203.167 274 229.5L240.5 211L254.5 309.5L342.5 270L308.5 249.5C308.5 249.5 328 190.5 364 190.5H400Z" fill="#6b7280"/>
|
||||
<path d="M0.5 190.5V150.5H46.5C82.5 150.5 115.167 203.167 126.5 229.5L160 211L146 309.5L58 270L92 249.5C92 249.5 72.5 190.5 36.5 190.5H0.5Z" fill="#6b7280"/>
|
||||
<path d="M219 0H179.5V109H129.5L200.5 192.5L268 109H219V0Z" fill="#6b7280"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M399 310V381.807C399 391.302 391.292 399 381.784 399H18.2163C8.70799 399 1 391.302 1 381.807V310.399H39.2886V361H361.5V310H399Z" fill="#6b7280"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 702 B |
@ -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<FormatedTableData>;
|
||||
headerText: string | JSX.Element;
|
||||
noDataPlaceholder: JSX.Element;
|
||||
testIDText: string;
|
||||
}
|
||||
|
||||
const EntityList: FunctionComponent<Prop> = ({
|
||||
entityList = [],
|
||||
headerText,
|
||||
noDataPlaceholder,
|
||||
testIDText,
|
||||
}: Prop) => {
|
||||
return (
|
||||
<>
|
||||
<h6 className="tw-heading tw-mb-3" data-testid="filter-heading">
|
||||
{headerText}
|
||||
</h6>
|
||||
{entityList.length
|
||||
? entityList.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
className="tw-flex tw-items-center tw-justify-between tw-mb-2"
|
||||
data-testid={`${testIDText}-${item.name}`}
|
||||
key={index}>
|
||||
<div className="tw-flex">
|
||||
{getEntityIcon(item.index)}
|
||||
<Link
|
||||
className="tw-font-medium tw-pl-2"
|
||||
to={getEntityLink(item.index, item.fullyQualifiedName)}>
|
||||
<button className="tw-text-grey-body hover:tw-text-primary-hover hover:tw-underline">
|
||||
{item.name}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: noDataPlaceholder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityList;
|
||||
@ -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<Props> = ({
|
||||
countServices,
|
||||
entityCounts,
|
||||
ingestionCount,
|
||||
}: Props) => {
|
||||
const { users, userTeams } = AppState;
|
||||
const [dataSummary, setdataSummary] = useState<Record<string, Summary>>({});
|
||||
|
||||
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 (
|
||||
<div className="tw-mb-3" data-testid="data-summary-container">
|
||||
{Object.values(dataSummary).map((data, index) => (
|
||||
<div
|
||||
className="tw-flex tw-items-center tw-justify-between tw-mb-2"
|
||||
key={index}>
|
||||
<div className="tw-flex">
|
||||
<SVGIcons alt="icon" className="tw-h-4 tw-w-4" icon={data.icon} />
|
||||
{data.link ? (
|
||||
<Link
|
||||
className="tw-font-medium tw-pl-2"
|
||||
data-testid={data.dataTestId}
|
||||
to={data.link}>
|
||||
<button className="tw-text-grey-body hover:tw-text-primary-hover hover:tw-underline">
|
||||
{data.data}
|
||||
</button>
|
||||
</Link>
|
||||
) : (
|
||||
<p className="tw-font-medium tw-pl-2">{data.data}</p>
|
||||
)}
|
||||
</div>
|
||||
{!isNil(data.count) && getCountBadge(data.count, '', false)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(MyAssetStats);
|
||||
@ -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(
|
||||
<MyAssetStats
|
||||
countServices={193}
|
||||
entityCounts={{
|
||||
tableCount: 40,
|
||||
topicCount: 13,
|
||||
dashboardCount: 10,
|
||||
pipelineCount: 3,
|
||||
}}
|
||||
ingestionCount={0}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const myDataHeader = getByTestId(container, 'data-header-container');
|
||||
|
||||
expect(myDataHeader).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should have main title', () => {
|
||||
const { container } = render(
|
||||
<MyAssetStats
|
||||
countServices={193}
|
||||
entityCounts={{
|
||||
tableCount: 40,
|
||||
topicCount: 13,
|
||||
dashboardCount: 10,
|
||||
pipelineCount: 3,
|
||||
}}
|
||||
ingestionCount={0}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const mainTitle = getByTestId(container, 'main-title');
|
||||
|
||||
expect(mainTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should have 7 data summary details', () => {
|
||||
const { container } = render(
|
||||
<MyAssetStats
|
||||
countServices={193}
|
||||
entityCounts={{
|
||||
tableCount: 40,
|
||||
topicCount: 13,
|
||||
dashboardCount: 10,
|
||||
pipelineCount: 3,
|
||||
}}
|
||||
ingestionCount={0}
|
||||
/>,
|
||||
{
|
||||
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(
|
||||
<MyAssetStats
|
||||
countServices={4}
|
||||
entityCounts={{
|
||||
tableCount: 40,
|
||||
topicCount: 13,
|
||||
dashboardCount: 10,
|
||||
pipelineCount: 3,
|
||||
}}
|
||||
ingestionCount={0}
|
||||
/>,
|
||||
{
|
||||
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(
|
||||
<MyAssetStats
|
||||
countServices={4}
|
||||
entityCounts={{
|
||||
tableCount: 40,
|
||||
topicCount: 13,
|
||||
dashboardCount: 10,
|
||||
pipelineCount: 3,
|
||||
}}
|
||||
ingestionCount={0}
|
||||
/>,
|
||||
{
|
||||
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');
|
||||
});
|
||||
});
|
||||
@ -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<MyDataProps> = ({
|
||||
error,
|
||||
countServices,
|
||||
ingestionCount,
|
||||
userDetails,
|
||||
searchResult,
|
||||
fetchData,
|
||||
ownedData,
|
||||
followedData,
|
||||
entityCounts,
|
||||
feedData,
|
||||
feedFilter,
|
||||
feedFilterHandler,
|
||||
}: MyDataProps): React.ReactElement => {
|
||||
const [data, setData] = useState<Array<FormatedTableData>>([]);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [totalNumberOfValue, setTotalNumberOfValues] = useState<number>(0);
|
||||
const [isEntityLoading, setIsEntityLoading] = useState<boolean>(true);
|
||||
const [currentTab, setCurrentTab] = useState<number>(1);
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
|
||||
const [fieldListVisible, setFieldListVisible] = useState<boolean>(false);
|
||||
const isMounted = useRef(false);
|
||||
|
||||
const getActiveTabClass = (tab: number) => {
|
||||
return tab === currentTab ? 'active' : '';
|
||||
const handleDropDown = (
|
||||
_e: React.MouseEvent<HTMLElement, 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 (
|
||||
<div className="tw-mb-3 tw--mt-4" data-testid="tabs">
|
||||
<nav className="tw-flex tw-flex-row tw-gh-tabs-container tw-px-4">
|
||||
<button
|
||||
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(1)}`}
|
||||
data-testid="tab"
|
||||
id="recentlyViewedTab"
|
||||
onClick={() => handleTabChange(1, '')}>
|
||||
Recently Viewed
|
||||
</button>
|
||||
<button
|
||||
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(2)}`}
|
||||
data-testid="tab"
|
||||
id="myDataTab"
|
||||
onClick={() => handleTabChange(2, Ownership.OWNER)}>
|
||||
My Data
|
||||
</button>
|
||||
<button
|
||||
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(3)}`}
|
||||
data-testid="tab"
|
||||
id="followingTab"
|
||||
onClick={() => handleTabChange(3, Ownership.FOLLOWERS)}>
|
||||
Following
|
||||
</button>
|
||||
</nav>
|
||||
<Fragment>
|
||||
<div className="tw-relative tw-mt-5">
|
||||
<Button
|
||||
data-testid="feeds"
|
||||
size="custom"
|
||||
theme="default"
|
||||
variant="text"
|
||||
onClick={() => setFieldListVisible((visible) => !visible)}>
|
||||
<span className="tw-text-grey-body tw-font-normal">
|
||||
{filterList.find((f) => f.value === feedFilter)?.name}
|
||||
</span>
|
||||
<DropDownIcon />
|
||||
</Button>
|
||||
{fieldListVisible && (
|
||||
<DropDownList
|
||||
dropDownList={filterList}
|
||||
value={feedFilter}
|
||||
onSelect={handleDropDown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getLinkByFilter = (filter: Ownership) => {
|
||||
return `${getExplorePathWithSearch()}?${filter}=${getOwnerIds(
|
||||
filter,
|
||||
AppState.userDetails
|
||||
).join()}`;
|
||||
};
|
||||
|
||||
const getLeftPanel = () => {
|
||||
return (
|
||||
<div className="tw-mt-5">
|
||||
<MyAssetStats
|
||||
countServices={countServices}
|
||||
entityCounts={entityCounts}
|
||||
ingestionCount={ingestionCount}
|
||||
/>
|
||||
<div className="tw-filter-seperator" />
|
||||
<RecentlyViewed />
|
||||
<div className="tw-filter-seperator tw-mt-3" />
|
||||
<RecentSearchedTerms />
|
||||
<div className="tw-filter-seperator tw-mt-3" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const paginate = (pageNumber: number) => {
|
||||
setCurrentPage(pageNumber);
|
||||
};
|
||||
const getRightPanel = useCallback(() => {
|
||||
return (
|
||||
<div className="tw-mt-5">
|
||||
<EntityList
|
||||
entityList={ownedData}
|
||||
headerText={
|
||||
<div className="tw-flex tw-justify-between">
|
||||
My Data
|
||||
{ownedData.length ? (
|
||||
<Link
|
||||
data-testid="my-data"
|
||||
to={getLinkByFilter(Ownership.OWNER)}>
|
||||
<span className="link-text tw-font-light tw-text-xs">
|
||||
View All
|
||||
</span>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
noDataPlaceholder={<>You have not owned anything yet!</>}
|
||||
testIDText="My data"
|
||||
/>
|
||||
<div className="tw-filter-seperator tw-mt-3" />
|
||||
<EntityList
|
||||
entityList={followedData}
|
||||
headerText={
|
||||
<div className="tw-flex tw-justify-between">
|
||||
Following
|
||||
{followedData.length ? (
|
||||
<Link
|
||||
data-testid="following-data"
|
||||
to={getLinkByFilter(Ownership.FOLLOWERS)}>
|
||||
<span className="link-text tw-font-light tw-text-xs">
|
||||
View All
|
||||
</span>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
noDataPlaceholder={<>You have not followed anything yet!</>}
|
||||
testIDText="Following data"
|
||||
/>
|
||||
<div className="tw-filter-seperator tw-mt-3" />
|
||||
</div>
|
||||
);
|
||||
}, [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: <div>{getSummary(change, true)}</div>,
|
||||
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 (
|
||||
<PageContainer>
|
||||
<div className="container-fluid" data-testid="fluid-container">
|
||||
<MyDataHeader
|
||||
countServices={countServices}
|
||||
entityCounts={entityCounts}
|
||||
ingestionCount={ingestionCount}
|
||||
/>
|
||||
{getTabs()}
|
||||
{error && Boolean(currentTab === 2 || currentTab === 3) ? (
|
||||
<ErrorPlaceHolderES errorMessage={error} type="error" />
|
||||
) : (
|
||||
<SearchedData
|
||||
showOnboardingTemplate
|
||||
currentPage={currentPage}
|
||||
data={data}
|
||||
isLoading={currentTab === 1 ? false : isEntityLoading}
|
||||
paginate={paginate}
|
||||
searchText="*"
|
||||
showOnlyChildren={currentTab === 1}
|
||||
showResultCount={filter && data.length > 0 ? true : false}
|
||||
totalValue={totalNumberOfValue}>
|
||||
{currentTab === 1 ? <RecentlyViewed /> : null}
|
||||
</SearchedData>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
<PageLayout leftPanel={getLeftPanel()} rightPanel={getRightPanel()}>
|
||||
{error ? (
|
||||
<ErrorPlaceHolderES errorMessage={error} type="error" />
|
||||
) : (
|
||||
<Fragment>
|
||||
{getFeedsData().feeds.length > 0 ? (
|
||||
<Fragment>
|
||||
{getFilterDropDown()}
|
||||
<FeedCards {...getFeedsData()} />
|
||||
</Fragment>
|
||||
) : (
|
||||
<Onboarding showLogo={false} />
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyData;
|
||||
export default observer(MyData);
|
||||
|
||||
@ -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<FormatedTableData>;
|
||||
followedData: Array<FormatedTableData>;
|
||||
feedData: Array<
|
||||
FormatedTableData & {
|
||||
entityType: string;
|
||||
changeDescriptions: Array<
|
||||
ChangeDescription & { updatedAt: number; updatedBy: string }
|
||||
>;
|
||||
}
|
||||
>;
|
||||
feedFilter: string;
|
||||
feedFilterHandler: (v: FeedFilter) => void;
|
||||
fetchData?: (value: SearchDataFunctionType) => void;
|
||||
entityCounts: EntityCounts;
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>,
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<h6 className="tw-heading tw-mb-3" data-testid="filter-heading">
|
||||
Recently Searched Terms
|
||||
</h6>
|
||||
{recentlySearchedTerms.length ? (
|
||||
recentlySearchedTerms.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
className="tw-flex tw-items-center tw-justify-between tw-mb-2"
|
||||
data-testid={`Recently-Search-${item.term}`}
|
||||
key={index}>
|
||||
<div className="tw-flex">
|
||||
<Link
|
||||
className="tw-font-medium"
|
||||
to={getExplorePathWithSearch(item.term)}>
|
||||
<button className="tw-text-grey-body hover:tw-text-primary-hover hover:tw-underline">
|
||||
<i className="fa fa-search tw-text-grey-muted tw-pr-2" />
|
||||
{item.term}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<>No searched terms!</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentSearchedTerms;
|
||||
@ -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,
|
||||
|
||||
@ -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<Feed>;
|
||||
relativeDays: Array<string>;
|
||||
}
|
||||
|
||||
const FeedCards: FC<FeedCardsProp> = ({
|
||||
feeds = [],
|
||||
relativeDays = [],
|
||||
}: FeedCardsProp) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{relativeDays.map((d, i) => (
|
||||
<div className="tw-grid tw-grid-rows-1 tw-grid-cols-1 tw-mt-3" key={i}>
|
||||
<div className="tw-relative tw-mb-3">
|
||||
<div className="tw-flex tw-justify-center">
|
||||
<hr className="tw-absolute tw-top-3 tw-border-b-2 tw-border-main tw-w-full tw-z-0" />
|
||||
<span className="tw-bg-white tw-px-4 tw-py-px tw-border tw-border-main tw-rounded tw-z-10 tw-text-grey-muted tw-font-normal">
|
||||
{d}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{feeds
|
||||
.filter((f) => f.relativeDay === d)
|
||||
.map((feed, i) => (
|
||||
<div
|
||||
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md tw-mb-3"
|
||||
key={i}>
|
||||
<div className="tw-flex tw-mb-1">
|
||||
<Avatar name={feed.updatedBy} width="24" />
|
||||
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
|
||||
{feed.updatedBy}
|
||||
<span className="tw-pl-1 tw-font-normal">
|
||||
updated{' '}
|
||||
<Link to={getEntityLink(feed.entityType, feed.fqn)}>
|
||||
<span className="link-text">{feed.entityName}</span>
|
||||
</Link>
|
||||
<span className="tw-text-grey-muted tw-pl-1 tw-text-xs">
|
||||
{getTimeByTimeStamp(feed.updatedAt)}
|
||||
</span>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div className="tw-pl-7">{feed.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedCards;
|
||||
@ -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<OnboardingProp> = ({
|
||||
showLogo = true,
|
||||
}: OnboardingProp) => {
|
||||
return (
|
||||
<div
|
||||
className="tw-flex tw-items-center tw-justify-around tw-mt-20"
|
||||
className={classNames(
|
||||
'tw-flex tw-items-center tw-justify-around tw-mt-10'
|
||||
)}
|
||||
data-testid="onboarding">
|
||||
<div className="tw-p-4" style={{ maxWidth: '700px' }}>
|
||||
<div className="tw-mb-6">
|
||||
@ -49,21 +58,17 @@ const Onboarding: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* <img
|
||||
alt=""
|
||||
className="tw-h-auto tw-w-full tw-filter tw-grayscale tw-opacity-50"
|
||||
src={logo}
|
||||
/> */}
|
||||
<SVGIcons
|
||||
alt="OpenMetadata Logo"
|
||||
className="tw-h-auto tw-filter tw-grayscale tw-opacity-50"
|
||||
data-testid="logo"
|
||||
icon={Icons.LOGO_SMALL}
|
||||
width="350"
|
||||
/>
|
||||
</div>
|
||||
{showLogo ? (
|
||||
<div>
|
||||
<SVGIcons
|
||||
alt="OpenMetadata Logo"
|
||||
className="tw-h-auto tw-filter tw-grayscale tw-opacity-50"
|
||||
data-testid="logo"
|
||||
icon={Icons.LOGO_SMALL}
|
||||
width="350"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
{data.length ? (
|
||||
data.map((item, index) => {
|
||||
return (
|
||||
<div className="tw-mb-3" key={index}>
|
||||
<TableDataCard
|
||||
description={item.description}
|
||||
fullyQualifiedName={item.fullyQualifiedName}
|
||||
indexType={item.index}
|
||||
name={item.name}
|
||||
owner={item.owner}
|
||||
serviceType={item.serviceType || '--'}
|
||||
tableType={item.tableType}
|
||||
tags={item.tags}
|
||||
tier={
|
||||
isString(item.tier) ? item.tier?.split('.')[1] : item.tier
|
||||
}
|
||||
usage={item.weeklyPercentileRank}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Onboarding />
|
||||
)}
|
||||
</>
|
||||
<EntityList
|
||||
entityList={data}
|
||||
headerText="Recently Viewed"
|
||||
noDataPlaceholder={<>No recently viewed data!</>}
|
||||
testIDText="Recently Viewed"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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' },
|
||||
];
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -15,3 +15,9 @@ export enum Ownership {
|
||||
OWNER = 'owner',
|
||||
FOLLOWERS = 'followers',
|
||||
}
|
||||
|
||||
export enum FeedFilter {
|
||||
ALL = 'all',
|
||||
OWNED = 'owner',
|
||||
FOLLOWING = 'followers',
|
||||
}
|
||||
|
||||
@ -391,15 +391,15 @@ declare module 'Models' {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface RecentlySearchData {
|
||||
interface RecentlySearchedData {
|
||||
term: string;
|
||||
timestamp: number;
|
||||
}
|
||||
export interface RecentlyViewed {
|
||||
data: Array<RecentlyViewedData>;
|
||||
}
|
||||
export interface SearchData {
|
||||
data: Array<RecentlySearchData>;
|
||||
export interface RecentlySearched {
|
||||
data: Array<RecentlySearchedData>;
|
||||
}
|
||||
|
||||
export type DatasetSchemaTableTab = 'schema' | 'sample_data';
|
||||
|
||||
@ -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<string>('');
|
||||
const [countServices, setCountServices] = useState<number>();
|
||||
const [ingestionCount, setIngestionCount] = useState<number>();
|
||||
@ -37,18 +45,28 @@ const MyDataPage = () => {
|
||||
const [searchResult, setSearchResult] = useState<SearchResponse>();
|
||||
const [entityCounts, setEntityCounts] = useState<EntityCounts>();
|
||||
|
||||
const fetchData = (value: SearchDataFunctionType, fetchService = false) => {
|
||||
const [ownedData, setOwnedData] = useState<Array<FormatedTableData>>();
|
||||
const [followedData, setFollowedData] = useState<Array<FormatedTableData>>();
|
||||
const [feedData, setFeedData] = useState<
|
||||
Array<
|
||||
FormatedTableData & {
|
||||
entityType: string;
|
||||
changeDescriptions: Array<
|
||||
ChangeDescription & { updatedAt: number; updatedBy: string }
|
||||
>;
|
||||
}
|
||||
>
|
||||
>();
|
||||
const [feedFilter, setFeedFilter] = useState<FeedFilter>(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 (
|
||||
<>
|
||||
<PageContainerV1>
|
||||
{!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}
|
||||
/>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
</PageContainerV1>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -373,7 +373,7 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
setActiveTab={activeTabHandler}
|
||||
tabs={tabs}
|
||||
/>
|
||||
<div className="tw-bg-white tw-flex-grow">
|
||||
<div className="tw-bg-white tw-flex-grow tw-py-4">
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<table
|
||||
|
||||
@ -40,6 +40,8 @@ export const formatDataResponse = (hits) => {
|
||||
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;
|
||||
});
|
||||
|
||||
@ -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<RecentlyViewedData> => {
|
||||
const recentlyViewed: RecentlyViewed = reactLocalStorage.getObject(
|
||||
LOCALSTORAGE_RECENTLY_VIEWED
|
||||
) as RecentlyViewed;
|
||||
|
||||
if (recentlyViewed?.data) {
|
||||
return recentlyViewed.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getRecentlySearchedData = (): Array<RecentlySearchedData> => {
|
||||
const recentlySearch: RecentlySearched = reactLocalStorage.getObject(
|
||||
LOCALSTORAGE_RECENTLY_SEARCHED
|
||||
) as RecentlySearched;
|
||||
if (recentlySearch?.data) {
|
||||
return recentlySearch.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const setRecentlyViewedData = (
|
||||
recentData: Array<RecentlyViewedData>
|
||||
): void => {
|
||||
reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_VIEWED, {
|
||||
data: recentData,
|
||||
});
|
||||
};
|
||||
|
||||
export const setRecentlySearchedData = (
|
||||
recentData: Array<RecentlySearchedData>
|
||||
): 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<RecentlyViewedData> => {
|
||||
const recentlyViewed: RecentlyViewed = reactLocalStorage.getObject(
|
||||
LOCALSTORAGE_RECENTLY_VIEWED
|
||||
) as RecentlyViewed;
|
||||
|
||||
if (recentlyViewed?.data) {
|
||||
return recentlyViewed.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getRecentlySearchData = (): Array<RecentlySearchData> => {
|
||||
const recentlySearch: SearchData = reactLocalStorage.getObject(
|
||||
LOCALSTORAGE_RECENTLY_SEARCH
|
||||
) as SearchData;
|
||||
if (recentlySearch?.data) {
|
||||
return recentlySearch.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const setRecentlyViewedData = (
|
||||
recentData: Array<RecentlyViewedData>
|
||||
): 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<string> => {
|
||||
if (filter === Ownership.OWNER && userDetails.teams) {
|
||||
const userTeams = !isEmpty(userDetails)
|
||||
? userDetails.teams.map((team) => team.id)
|
||||
: [];
|
||||
|
||||
return [...userTeams, getCurrentUserId()];
|
||||
} else {
|
||||
return [getCurrentUserId()];
|
||||
}
|
||||
};
|
||||
|
||||
@ -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) => {
|
||||
<Fragment>
|
||||
{fieldsAdded?.length > 0 ? (
|
||||
<p className="tw-mb-2">
|
||||
{fieldsAdded?.map(summaryFormatter).join(', ')} has been added
|
||||
{`${isPrefix ? '+ Added' : ''} ${fieldsAdded
|
||||
?.map(summaryFormatter)
|
||||
.join(', ')} ${!isPrefix ? `has been added` : ''}`}{' '}
|
||||
</p>
|
||||
) : null}
|
||||
{fieldsUpdated?.length ? (
|
||||
<p className="tw-mb-2">
|
||||
{fieldsUpdated?.map(summaryFormatter).join(', ')} has been updated
|
||||
{`${isPrefix ? 'Edited' : ''} ${fieldsUpdated
|
||||
?.map(summaryFormatter)
|
||||
.join(', ')} ${!isPrefix ? `has been updated` : ''}`}{' '}
|
||||
</p>
|
||||
) : null}
|
||||
{fieldsDeleted?.length ? (
|
||||
<p className="tw-mb-2">
|
||||
{fieldsDeleted?.map(summaryFormatter).join(', ')} has been deleted
|
||||
{`${isPrefix ? '- Removed' : ''} ${fieldsDeleted
|
||||
?.map(summaryFormatter)
|
||||
.join(', ')} ${!isPrefix ? `has been Deleted` : ''}`}{' '}
|
||||
</p>
|
||||
) : null}
|
||||
</Fragment>
|
||||
|
||||
@ -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 ')})`;
|
||||
};
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -125,7 +125,7 @@ module.exports = {
|
||||
120: '30rem',
|
||||
},
|
||||
minWidth: {
|
||||
badgeCount: '24px',
|
||||
badgeCount: '30px',
|
||||
},
|
||||
maxHeight: {
|
||||
32: '8rem',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user