Added Recently viewed feature for Landing page (#440)

* Added Recently viewed feature for Landing page

* Updated for fqn instead of id

* Adding service type to storage  with entity fqn
This commit is contained in:
darth-coder00 2021-09-08 19:34:33 +05:30 committed by GitHub
parent 297fdaea8d
commit 3f447dc19a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 337 additions and 29 deletions

View File

@ -3821,6 +3821,12 @@
"@types/react": "*"
}
},
"@types/reactjs-localstorage": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/reactjs-localstorage/-/reactjs-localstorage-1.0.0.tgz",
"integrity": "sha512-0Y3f/vGR3yz46yfoqtyVljTfnKxYZVC87p2AXouO3Fc14/aBOUt8oKTLCDej8/sd5+yqx6WA2YBUsT2eSXx5cA==",
"dev": true
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -17555,6 +17561,11 @@
"prop-types": "^15.6.2"
}
},
"reactjs-localstorage": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/reactjs-localstorage/-/reactjs-localstorage-1.0.1.tgz",
"integrity": "sha512-eNSwfiVyVDhfjw9h83PBvgqQcFxmyK6KGPrPlutuaaTlALRZVQLZ0MAN72iU9UT+bKtscwtdogHQ2ikZ7rjT+w=="
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@ -64,6 +64,7 @@
"react-select": "^3.1.1",
"react-syntax-highlighter": "^15.4.4",
"react-tippy": "^1.4.0",
"reactjs-localstorage": "^1.0.1",
"recharts": "^1.8.5",
"rehype-raw": "^6.0.0",
"remark-gfm": "^1.0.0",
@ -122,6 +123,7 @@
"@types/react-dom": "^16.9.9",
"@types/react-router-dom": "^5.1.7",
"@types/react-test-renderer": "^17.0.0",
"@types/reactjs-localstorage": "^1.0.0",
"@types/testing-library__jest-dom": "^5.9.5",
"babel-eslint": "^10.1.0",
"babel-jest": "^24.9.0",

View File

@ -33,6 +33,15 @@ export const getDashboards: Function = (
return APIClient.get(url);
};
export const getDashboardDetails: Function = (
id: string,
arrQueryFields: string
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/dashboards/${id}`, arrQueryFields);
return APIClient.get(url);
};
export const getDashboardByFqn: Function = (
fqn: string,
arrQueryFields: string

View File

@ -33,6 +33,15 @@ export const getTopics: Function = (
return APIClient.get(url);
};
export const getTopicDetails: Function = (
id: string,
arrQueryFields: string
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/topics/${id}`, arrQueryFields);
return APIClient.get(url);
};
export const getTopicByFqn: Function = (
fqn: string,
arrQueryFields: string

View File

@ -0,0 +1,161 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 { ColumnTags, FormatedTableData } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { getDashboardByFqn } from '../../axiosAPIs/dashboardAPI';
import { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
import { getTopicByFqn } from '../../axiosAPIs/topicsAPI';
import { EntityType } from '../../enums/entity.enum';
import { SearchIndex } from '../../enums/search.enum';
import { getRecentlyViewedData } from '../../utils/CommonUtils';
import { getOwnerFromId, getTierFromTableTags } from '../../utils/TableUtils';
import { getTableTags } from '../../utils/TagsUtils';
import TableDataCard from '../common/table-data-card/TableDataCard';
import Onboarding from '../onboarding/Onboarding';
const RecentlyViewed: FunctionComponent = () => {
const recentlyViewedData = getRecentlyViewedData();
const [data, setData] = useState<Array<FormatedTableData>>([]);
const fetchRecentlyViewedEntity = async () => {
const arrData: Array<FormatedTableData> = [];
for (const oData of recentlyViewedData) {
// for (let i = 0; i < recentlyViewedData.length; i++) {
// const oData = recentlyViewedData[i];
switch (oData.entityType) {
case EntityType.DATASET: {
const res = await getTableDetailsByFQN(
oData.fqn,
'database, usageSummary, tags, owner'
);
const {
description,
id,
name,
columns,
owner,
usageSummary,
fullyQualifiedName,
} = res.data;
const tableTags = getTableTags(columns || []);
arrData.push({
description,
fullyQualifiedName,
id,
index: SearchIndex.TABLE,
name,
owner: getOwnerFromId(owner?.id)?.name || '--',
serviceType: oData.serviceType,
tags: tableTags.map((tag) => tag.tagFQN),
tier: getTierFromTableTags(tableTags),
weeklyPercentileRank: usageSummary?.weeklyStats.percentileRank || 0,
});
break;
}
case EntityType.TOPIC: {
const res = await getTopicByFqn(oData.fqn, 'owner, service, tags');
const { description, id, name, tags, owner, fullyQualifiedName } =
res.data;
arrData.push({
description,
fullyQualifiedName,
id,
index: SearchIndex.TOPIC,
name,
owner: getOwnerFromId(owner?.id)?.name || '--',
serviceType: oData.serviceType,
tags: (tags as Array<ColumnTags>).map((tag) => tag.tagFQN),
tier: getTierFromTableTags(tags as Array<ColumnTags>),
});
break;
}
case EntityType.DASHBOARD: {
const res = await getDashboardByFqn(
oData.fqn,
'owner, service, tags, usageSummary'
);
const {
description,
id,
displayName,
tags,
owner,
fullyQualifiedName,
} = res.data;
arrData.push({
description,
fullyQualifiedName,
id,
index: SearchIndex.DASHBOARD,
name: displayName,
owner: getOwnerFromId(owner?.id)?.name || '--',
serviceType: oData.serviceType,
tags: (tags as Array<ColumnTags>).map((tag) => tag.tagFQN),
tier: getTierFromTableTags(tags as Array<ColumnTags>),
});
break;
}
default:
break;
}
}
setData(arrData);
};
useEffect(() => {
if (recentlyViewedData.length) {
fetchRecentlyViewedEntity();
}
}, []);
return (
<>
{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={item.tier?.split('.')[1]}
usage={item.weeklyPercentileRank}
/>
</div>
);
})
) : (
<Onboarding />
)}
</>
);
};
export default RecentlyViewed;

View File

@ -42,6 +42,7 @@ type SearchedDataProp = {
showResultCount?: boolean;
searchText?: string;
showOnboardingTemplate?: boolean;
showOnlyChildren?: boolean;
};
const SearchedData: React.FC<SearchedDataProp> = ({
children,
@ -51,6 +52,7 @@ const SearchedData: React.FC<SearchedDataProp> = ({
paginate,
showResultCount = false,
showOnboardingTemplate = false,
showOnlyChildren = false,
searchText,
totalValue,
fetchLeftPanel,
@ -96,29 +98,33 @@ const SearchedData: React.FC<SearchedDataProp> = ({
<Loader />
) : (
<>
{totalValue > 0 || showOnboardingTemplate ? (
{totalValue > 0 || showOnboardingTemplate || showOnlyChildren ? (
<>
{children}
{showResultCount && searchText ? (
<div className="tw-mb-1">
{pluralize(totalValue, 'result')}
</div>
) : null}
{data.length > 0 ? (
<div className="tw-grid tw-grid-rows-1 tw-grid-cols-1">
{highlightSearchResult()}
{totalValue > PAGE_SIZE && data.length > 0 && (
<Pagination
currentPage={currentPage}
paginate={paginate}
sizePerPage={PAGE_SIZE}
totalNumberOfValues={totalValue}
/>
{!showOnlyChildren ? (
<>
{showResultCount && searchText ? (
<div className="tw-mb-1">
{pluralize(totalValue, 'result')}
</div>
) : null}
{data.length > 0 ? (
<div className="tw-grid tw-grid-rows-1 tw-grid-cols-1">
{highlightSearchResult()}
{totalValue > PAGE_SIZE && data.length > 0 && (
<Pagination
currentPage={currentPage}
paginate={paginate}
sizePerPage={PAGE_SIZE}
totalNumberOfValues={totalValue}
/>
)}
</div>
) : (
<Onboarding />
)}
</div>
) : (
<Onboarding />
)}
</>
) : null}
</>
) : (
<ErrorPlaceHolderES type="noData" />

View File

@ -21,6 +21,7 @@ export const API_RES_MAX_SIZE = 100000;
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 oidcTokenKey = 'oidcIdToken';
export const imageTypes = {
image: 's96-c',

View File

@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
export enum EntityType {
DATASET = 'dataset',
TOPIC = 'topic',
DASHBOARD = 'dashboard',
}

View File

@ -201,11 +201,10 @@ declare module 'Models' {
owner: string;
tableType?: string;
tags: string[];
tableEntity: TableEntity;
dailyStats: number;
dailyPercentileRank: number;
weeklyStats: number;
weeklyPercentileRank: number;
dailyStats?: number;
dailyPercentileRank?: number;
weeklyStats?: number;
weeklyPercentileRank?: number;
service?: string;
serviceType?: string;
tier: string;
@ -377,4 +376,14 @@ declare module 'Models' {
}
// topic interface end
interface RecentlyViewedData {
entityType: 'dataset' | 'topic' | 'dashboard';
fqn: string;
serviceType?: string;
timestamp: number;
}
export interface RecentlyViewed {
data: Array<RecentlyViewedData>;
}
}

View File

@ -24,9 +24,11 @@ import ManageTab from '../../components/my-data-details/ManageTab';
import TagsContainer from '../../components/tags-container/tags-container';
import Tags from '../../components/tags/tags';
import { getServiceDetailsPath } from '../../constants/constants';
import { EntityType } from '../../enums/entity.enum';
import { Chart } from '../../generated/entity/data/chart';
import { Dashboard, TagLabel } from '../../generated/entity/data/dashboard';
import {
addToRecentViewed,
getCurrentUserId,
getUserTeams,
isEven,
@ -147,6 +149,7 @@ const MyDashBoardPage = () => {
id,
description,
followers,
fullyQualifiedName,
service,
tags,
owner,
@ -182,6 +185,13 @@ const MyDashBoardPage = () => {
activeTitle: true,
},
]);
addToRecentViewed({
entityType: EntityType.DASHBOARD,
fqn: fullyQualifiedName,
serviceType: serviceRes.data.serviceType,
timestamp: 0,
});
}
);
fetchCharts(charts).then((charts) => setCharts(charts));

View File

@ -51,8 +51,10 @@ import {
getDatabaseDetailsPath,
getServiceDetailsPath,
} from '../../constants/constants';
import { EntityType } from '../../enums/entity.enum';
import useToastContext from '../../hooks/useToastContext';
import {
addToRecentViewed,
getCurrentUserId,
getPartialNameFromFQN,
getTableFQNFromColumnFQN,
@ -308,6 +310,7 @@ const MyDataDetailsPage = () => {
owner,
usageSummary,
followers,
fullyQualifiedName,
joins,
tags,
sampleData,
@ -347,6 +350,13 @@ const MyDataDetailsPage = () => {
activeTitle: true,
},
]);
addToRecentViewed({
entityType: EntityType.DATASET,
fqn: fullyQualifiedName,
serviceType: resService.data.serviceType,
timestamp: 0,
});
}
);
});

View File

@ -25,6 +25,7 @@ import { searchData } from '../../axiosAPIs/miscAPI';
import ErrorPlaceHolderES from '../../components/common/error-with-placeholder/ErrorPlaceHolderES';
import Loader from '../../components/Loader/Loader';
import MyDataHeader from '../../components/my-data/MyDataHeader';
import RecentlyViewed from '../../components/recently-viewed/RecentlyViewed';
import SearchedData from '../../components/searched-data/SearchedData';
import { ERROR500, PAGE_SIZE } from '../../constants/constants';
import { Ownership } from '../../enums/mydata.enum';
@ -41,7 +42,7 @@ const MyDataPage: React.FC = (): React.ReactElement => {
const [data, setData] = useState<Array<FormatedTableData>>([]);
const [currentPage, setCurrentPage] = useState<number>(1);
const [totalNumberOfValue, setTotalNumberOfValues] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [currentTab, setCurrentTab] = useState<number>(1);
const [error, setError] = useState<string>('');
const [filter, setFilter] = useState<string>('');
@ -119,7 +120,7 @@ const MyDataPage: React.FC = (): React.ReactElement => {
setFilter('');
setCurrentPage(1);
}}>
All
Recently Viewed
</button>
<button
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(2)}`}
@ -174,6 +175,7 @@ const MyDataPage: React.FC = (): React.ReactElement => {
data={data}
paginate={paginate}
searchText="*"
showOnlyChildren={currentTab === 1}
showResultCount={filter && data.length > 0 ? true : false}
totalValue={totalNumberOfValue}>
<>
@ -185,6 +187,7 @@ const MyDataPage: React.FC = (): React.ReactElement => {
)}
/>
{getTabs()}
{currentTab === 1 ? <RecentlyViewed /> : null}
</>
</SearchedData>
)}

View File

@ -19,7 +19,12 @@ import Loader from '../../components/Loader/Loader';
import ManageTab from '../../components/my-data-details/ManageTab';
import SchemaEditor from '../../components/schema-editor/SchemaEditor';
import { getServiceDetailsPath } from '../../constants/constants';
import { getCurrentUserId, getUserTeams } from '../../utils/CommonUtils';
import { EntityType } from '../../enums/entity.enum';
import {
addToRecentViewed,
getCurrentUserId,
getUserTeams,
} from '../../utils/CommonUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import {
getOwnerFromId,
@ -95,6 +100,7 @@ const MyTopicDetailPage = () => {
id,
description,
followers,
fullyQualifiedName,
name,
partitions,
schemaType,
@ -137,6 +143,13 @@ const MyTopicDetailPage = () => {
activeTitle: true,
},
]);
addToRecentViewed({
entityType: EntityType.TOPIC,
fqn: fullyQualifiedName,
serviceType: serviceRes.data.serviceType,
timestamp: 0,
});
}
);
setLoading(false);

View File

@ -1,7 +1,9 @@
import { isEmpty } from 'lodash';
import { UserTeam } from 'Models';
import { RecentlyViewed, RecentlyViewedData, UserTeam } from 'Models';
import React from 'react';
import { reactLocalStorage } from 'reactjs-localstorage';
import AppState from '../AppState';
import { LOCALSTORAGE_RECENTLY_VIEWED } from '../constants/constants';
import { countBackground } from './styleconstant';
export const arraySorterByKey = (
@ -103,3 +105,43 @@ export const getCountBadge = (count = 0) => {
</span>
);
};
export const addToRecentViewed = (eData: RecentlyViewedData): void => {
const entityData = { ...eData, timestamp: Date.now() };
let recentlyViewed: RecentlyViewed = reactLocalStorage.getObject(
LOCALSTORAGE_RECENTLY_VIEWED
) as RecentlyViewed;
if (recentlyViewed?.data) {
const arrData = recentlyViewed.data
.filter((item) => item.fqn !== entityData.fqn)
.sort(
arraySorterByKey('timestamp', true) as (
a: RecentlyViewedData,
b: RecentlyViewedData
) => number
);
arrData.unshift(entityData);
if (arrData.length > 5) {
arrData.pop();
}
recentlyViewed.data = arrData;
} else {
recentlyViewed = {
data: [entityData],
};
}
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 [];
};