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:
darth-coder00 2021-12-08 19:39:09 +05:30 committed by GitHub
parent 64386b035e
commit 689e5beaea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 975 additions and 268 deletions

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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');
});
});

View File

@ -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);

View File

@ -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;
}

View File

@ -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}
/>,

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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"
/>
)}
</>
);

View File

@ -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' },
];

View File

@ -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',

View File

@ -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);

View File

@ -15,3 +15,9 @@ export enum Ownership {
OWNER = 'owner',
FOLLOWERS = 'followers',
}
export enum FeedFilter {
ALL = 'all',
OWNED = 'owner',
FOLLOWING = 'followers',
}

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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

View File

@ -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;
});

View File

@ -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()];
}
};

View File

@ -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>

View File

@ -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 ')})`;
};

View File

@ -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');
};

View File

@ -125,7 +125,7 @@ module.exports = {
120: '30rem',
},
minWidth: {
badgeCount: '24px',
badgeCount: '30px',
},
maxHeight: {
32: '8rem',