Feat #8639 UI : Show charts latest value in data insight overview card and rename datasets to data assets (#8641)

* Fix #8639 UI : Show charts latest value in data insight overview card and rename datasets to data assets

* Add data summary for entities chart

* Add Percentage symbol for Percentage chart

* Add web charts summary data

* Address review comments

* Change active user index
This commit is contained in:
Sachin Chaurasiya 2022-11-10 21:18:49 +05:30 committed by GitHub
parent b2e2d9cff3
commit 2e158237ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 369 additions and 62 deletions

View File

@ -79,6 +79,7 @@ const DailyActiveUsersChart: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-active-user-card"
id={DataInsightChartType.DailyActiveUsers}
loading={isLoading}
title={
<>

View File

@ -12,6 +12,7 @@
*/
@label-color: #37352f90;
@summary-card-bg-hover: #f0f1f3;
.data-insight-card {
.ant-card-head {
@ -22,3 +23,12 @@
.ant-typography.data-insight-label-text {
color: @label-color;
}
.summary-card-item {
cursor: pointer;
padding: 8px;
&:hover {
background-color: @summary-card-bg-hover;
border-radius: 6px;
}
}

View File

@ -14,6 +14,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { act } from 'react-test-renderer';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import DataInsightSummary from './DataInsightSummary';
jest.mock('react-i18next', () => ({
@ -27,6 +28,8 @@ const mockFilter = {
endTs: 1668000248671,
};
const mockScrollFunction = jest.fn();
jest.mock('../../axiosAPIs/DataInsightAPI', () => ({
getAggregateChartData: jest.fn().mockImplementation(() => Promise.resolve()),
}));
@ -34,14 +37,21 @@ jest.mock('../../axiosAPIs/DataInsightAPI', () => ({
describe('Test DataInsightSummary Component', () => {
it('Should render the overview data', async () => {
await act(async () => {
render(<DataInsightSummary chartFilter={mockFilter} />);
render(
<DataInsightSummary
chartFilter={mockFilter}
onScrollToChart={mockScrollFunction}
/>
);
});
const summaryCard = screen.getByTestId('summary-card');
const allEntityCount = screen.getByTestId('summary-item-latest');
const totalEntitiesByType = screen.getByTestId(
`summary-item-${DataInsightChartType.TotalEntitiesByType}`
);
expect(summaryCard).toBeInTheDocument();
expect(allEntityCount).toBeInTheDocument();
expect(totalEntitiesByType).toBeInTheDocument();
});
});

View File

@ -11,51 +11,138 @@
* limitations under the License.
*/
import { Card, Col, Row, Typography } from 'antd';
import { Card, Col, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import { getUserPath } from '../../constants/constants';
import {
ENTITIES_CHARTS,
WEB_CHARTS,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import {
DataInsightChartResult,
DataInsightChartType,
} from '../../generated/dataInsight/dataInsightChartResult';
import { MostActiveUsers } from '../../generated/dataInsight/type/mostActiveUsers';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getGraphDataByEntityType } from '../../utils/DataInsightUtils';
import {
getEntitiesChartSummary,
getWebChartSummary,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import UserPopOverCard from '../common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import './DataInsightDetail.less';
interface Props {
chartFilter: ChartFilter;
onScrollToChart: (chartType: DataInsightChartType) => void;
}
const DataInsightSummary: FC<Props> = ({ chartFilter }) => {
const [totalEntitiesByType, setTotalEntitiesByType] =
useState<DataInsightChartResult>();
const DataInsightSummary: FC<Props> = ({ chartFilter, onScrollToChart }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [entitiesCharts, setEntitiesChart] = useState<
(DataInsightChartResult | undefined)[]
>([]);
const [webCharts, setWebCharts] = useState<
(DataInsightChartResult | undefined)[]
>([]);
const { total, latestData = {} } = useMemo(() => {
return getGraphDataByEntityType(
totalEntitiesByType?.data ?? [],
DataInsightChartType.TotalEntitiesByType
const [mostActiveUser, setMostActiveUser] = useState<MostActiveUsers>();
const entitiesSummaryList = useMemo(
() => getEntitiesChartSummary(entitiesCharts),
[entitiesCharts]
);
const webSummaryList = useMemo(
() => getWebChartSummary(webCharts),
[webCharts]
);
}, [totalEntitiesByType]);
const { t } = useTranslation();
const fetchTotalEntitiesByType = async () => {
const fetchEntitiesChartData = async () => {
setIsLoading(true);
try {
const promises = ENTITIES_CHARTS.map((chartName) => {
const params = {
...chartFilter,
dataInsightChartName: chartName,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
};
return getAggregateChartData(params);
});
const responses = await Promise.allSettled(promises);
const chartDataList = responses
.map((response) => {
if (response.status === 'fulfilled') {
return response.value;
}
return;
})
.filter(Boolean);
setEntitiesChart(chartDataList);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
const fetchMostActiveUser = async () => {
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.TotalEntitiesByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
dataInsightChartName: DataInsightChartType.MostActiveUsers,
dataReportIndex: DataReportIndex.WebAnalyticUserActivityReportDataIndex,
};
const response = await getAggregateChartData(params);
if (response.data && response.data.length) {
setMostActiveUser(response.data[0]);
}
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
setTotalEntitiesByType(response);
const fetchWebChartData = async () => {
setIsLoading(true);
try {
const promises = WEB_CHARTS.map((chart) => {
const params = {
...chartFilter,
dataInsightChartName: chart.chart,
dataReportIndex: chart.index,
};
return getAggregateChartData(params);
});
const responses = await Promise.allSettled(promises);
const chartDataList = responses
.map((response) => {
if (response.status === 'fulfilled') {
return response.value;
}
return;
})
.filter(Boolean);
setWebCharts(chartDataList);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
@ -64,7 +151,9 @@ const DataInsightSummary: FC<Props> = ({ chartFilter }) => {
};
useEffect(() => {
fetchTotalEntitiesByType();
fetchEntitiesChartData();
fetchMostActiveUser();
fetchWebChartData();
}, [chartFilter]);
return (
@ -78,27 +167,65 @@ const DataInsightSummary: FC<Props> = ({ chartFilter }) => {
</Typography.Title>
}>
<Row data-testid="summary-card-content" gutter={[16, 16]}>
<Col data-testid="summary-item-latest" span={4}>
{/* summary of entity charts */}
{entitiesSummaryList.map((summary) => (
<Col
className="summary-card-item"
data-testid={`summary-item-${summary.id}`}
key={summary.id}
span={6}
onClick={() => onScrollToChart(summary.id)}>
<Typography.Text className="data-insight-label-text">
Latest
{summary.label}
</Typography.Text>
<Typography className="font-semibold text-2xl">{total}</Typography>
</Col>
{Object.entries(latestData).map((summary) => {
const label = summary[0];
const value = summary[1] as number;
return label !== 'timestamp' ? (
<Col data-testid={`summary-item-${label}`} key={label} span={4}>
<Typography.Text className="data-insight-label-text">
{label}
</Typography.Text>
<Typography className="font-semibold text-2xl">
{value}
<Typography className="font-semibold text-2xl m--ml-0.5">
{summary.latest}
{summary.id.startsWith('Percentage') ? '%' : ''}
</Typography>
</Col>
) : null;
})}
))}
{/* summary for web charts */}
{webSummaryList.map((summary) => (
<Col
className="summary-card-item"
data-testid={`summary-item-${summary.id}`}
key={summary.id}
span={6}
onClick={() => onScrollToChart(summary.id)}>
<Typography.Text className="data-insight-label-text">
{summary.label}
</Typography.Text>
<Typography className="font-semibold text-2xl m--ml-0.5">
{summary.latest}
{summary.id.startsWith('Percentage') ? '%' : ''}
</Typography>
</Col>
))}
{/* summary of most active user */}
{mostActiveUser && mostActiveUser.userName && (
<Col
data-testid={`summary-item-${DataInsightChartType.MostActiveUsers}`}
key={DataInsightChartType.MostActiveUsers}
span={6}>
<Typography.Text className="data-insight-label-text d-block">
{t('label.most-active-user')}
</Typography.Text>
<UserPopOverCard userName={mostActiveUser.userName}>
<Space>
<ProfilePicture
id=""
name={mostActiveUser.userName}
type="circle"
/>
<Link to={getUserPath(mostActiveUser.userName)}>
{mostActiveUser.userName}
</Link>
</Space>
</UserPopOverCard>
</Col>
)}
</Row>
</Card>
);

View File

@ -93,6 +93,7 @@ const DescriptionInsight: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-description-percentage-card"
id={DataInsightChartType.PercentageOfEntitiesWithDescriptionByType}
loading={isLoading}
title={
<>

View File

@ -93,6 +93,7 @@ const OwnerInsight: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-summary-card-percentage"
id={DataInsightChartType.PercentageOfEntitiesWithOwnerByType}
loading={isLoading}
title={
<>

View File

@ -90,6 +90,7 @@ const PageViewsByEntitiesChart: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-page-views-card"
id={DataInsightChartType.PageViewsByEntities}
loading={isLoading}
title={
<>

View File

@ -89,6 +89,7 @@ const TierInsight: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-summary-card-percentage"
id={DataInsightChartType.TotalEntitiesByTier}
loading={isLoading}
title={
<>

View File

@ -64,9 +64,9 @@ const TopViewEntities: FC<Props> = ({ chartFilter }) => {
const columns: ColumnsType<MostViewedEntities> = useMemo(
() => [
{
title: t('label.entity-name'),
title: t('label.data-asset'),
dataIndex: 'entityFqn',
key: 'entityName',
key: 'dataAsset',
render: (entityFqn: string) => (
<Typography.Text>{getDecodedFqn(entityFqn)}</Typography.Text>
),
@ -82,7 +82,7 @@ const TopViewEntities: FC<Props> = ({ chartFilter }) => {
<Typography.Text>{owner}</Typography.Text>
</Space>
) : (
<Typography.Text>{t('label.no-owner')}</Typography.Text>
<Typography.Text>--</Typography.Text>
),
},
{
@ -107,7 +107,7 @@ const TopViewEntities: FC<Props> = ({ chartFilter }) => {
{t('label.data-insight-top-viewed-entity-summary')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.most-viewed-datasets')}
{t('message.most-viewed-data-assets')}
</Typography.Text>
</>
}>

View File

@ -92,6 +92,7 @@ const TotalEntityInsight: FC<Props> = ({ chartFilter }) => {
<Card
className="data-insight-card"
data-testid="entity-summary-card"
id={DataInsightChartType.TotalEntitiesByType}
loading={isLoading}
title={
<>

View File

@ -13,6 +13,8 @@
import i18n from 'i18next';
import { Margin } from 'recharts/types/util/types';
import { DataReportIndex } from '../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../generated/dataInsight/dataInsightChartResult';
import { ChartFilter } from '../interface/data-insight.interface';
import {
getCurrentDateTimeMillis,
@ -67,7 +69,7 @@ export const TIER_BAR_COLOR_MAP: Record<string, string> = {
};
export const DATA_INSIGHT_TAB = {
Datasets: 'Datasets',
DataAssets: 'Data assets',
'Web Analytics': 'Web Analytics',
};
@ -117,3 +119,57 @@ export const INITIAL_CHART_FILTER: ChartFilter = {
startTs: getPastDaysDateTimeMillis(DEFAULT_DAYS),
endTs: getCurrentDateTimeMillis(),
};
export const ENTITIES_CHARTS = [
DataInsightChartType.TotalEntitiesByType,
DataInsightChartType.PercentageOfEntitiesWithDescriptionByType,
DataInsightChartType.PercentageOfEntitiesWithOwnerByType,
DataInsightChartType.TotalEntitiesByTier,
];
export const WEB_CHARTS = [
{
chart: DataInsightChartType.PageViewsByEntities,
index: DataReportIndex.WebAnalyticEntityViewReportDataIndex,
},
{
chart: DataInsightChartType.DailyActiveUsers,
index: DataReportIndex.WebAnalyticUserActivityReportDataIndex,
},
];
export const WEB_SUMMARY_LIST = [
{
label: i18n.t('label.page-views-by-entities'),
latest: 0,
id: DataInsightChartType.PageViewsByEntities,
},
{
label: i18n.t('label.daily-active-user'),
latest: 0,
id: DataInsightChartType.DailyActiveUsers,
},
];
export const ENTITIES_SUMMARY_LIST = [
{
label: i18n.t('label.total-data-assets'),
latest: 0,
id: DataInsightChartType.TotalEntitiesByType,
},
{
label: i18n.t('label.data-assets-with-field', { field: 'description' }),
latest: 0,
id: DataInsightChartType.PercentageOfEntitiesWithDescriptionByType,
},
{
label: i18n.t('label.data-assets-with-field', { field: 'owners' }),
latest: 0,
id: DataInsightChartType.PercentageOfEntitiesWithOwnerByType,
},
{
label: i18n.t('label.total-data-assets-with-tiers'),
latest: 0,
id: DataInsightChartType.TotalEntitiesByTier,
},
];

View File

@ -175,12 +175,12 @@
"scopes-comma-separated": "Scopes value comma separated",
"find-in-table": "Find in table",
"data-insight-summary": "OpenMetadata health at a glance",
"data-insight-description-summary": "Percentage of Datasets With Description",
"data-insight-owner-summary": "Percentage of Datasets With Owners",
"data-insight-tier-summary": "Total Datasets by Tier",
"data-insight-description-summary": "Percentage of Data assets With Description",
"data-insight-owner-summary": "Percentage of Data assets With Owners",
"data-insight-tier-summary": "Total Data assets by Tier",
"data-insight-active-user-summary": "Most Active Users",
"data-insight-top-viewed-entity-summary": "Most Viewed Datasets",
"data-insight-total-entity-summary": "Total Datasets",
"data-insight-top-viewed-entity-summary": "Most Viewed Data assets",
"data-insight-total-entity-summary": "Total Data assets",
"user": "User",
"team": "Team",
"most-recent-session": "Most Recent Session",
@ -227,12 +227,17 @@
"read-more": "read more",
"read-less": "read less",
"no-owner": "No Owner",
"page-views-by-entities": "Page views by datasets",
"page-views-by-entities": "Page views by data assets",
"daily-active-user": "Daily active users on the platform",
"collapse-all": "Collapse All",
"expand-all": "Expand All",
"search-lineage": "Search Lineage",
"edit-lineage": "Edit Lineage"
"edit-lineage": "Edit Lineage",
"data-asset": "Data asset",
"total-data-assets": "Total data assets",
"data-assets-with-field": "Data assets with {{field}}",
"total-data-assets-with-tiers": "Total Data assets with tiers",
"most-active-user": "Most active user"
},
"message": {
"service-email-required": "Service account Email is required",
@ -255,11 +260,11 @@
"no-ingestion-description": "To view Ingestion Data, run the MetaData Ingestion. Please refer to this doc to schedule the",
"fetch-pipeline-status-error": "Error while fetching pipeline status.",
"data-insight-page-views": "Displays the number of time an dataset type was viewed.",
"field-insight": "Display the percentage of datasets with {{field}} by type.",
"total-entity-insight": "Display the total of datasets by type.",
"field-insight": "Display the percentage of data assets with {{field}} by type.",
"total-entity-insight": "Display the total of data assets by type.",
"active-users": "Display the number of users active.",
"most-active-users": "Displays the most active users on the platform based on page views.",
"most-viewed-datasets": "Displays the most viewed datasets."
"most-viewed-data-assets": "Displays the most viewed data assets."
},
"server": {
"no-followed-entities": "You have not followed anything yet.",

View File

@ -22,7 +22,7 @@ import {
Space,
Typography,
} from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { searchQuery } from '../../axiosAPIs/searchAPI';
import { autocomplete } from '../../components/AdvancedSearch/AdvancedSearch.constants';
@ -40,10 +40,12 @@ import {
DATA_INSIGHT_TAB,
DAY_FILTER,
DEFAULT_DAYS,
ENTITIES_CHARTS,
INITIAL_CHART_FILTER,
TIER_FILTER,
} from '../../constants/DataInsight.constants';
import { SearchIndex } from '../../enums/search.enum';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getTeamFilter } from '../../utils/DataInsightUtils';
import {
@ -57,10 +59,12 @@ const fetchTeamSuggestions = autocomplete(SearchIndex.TEAM);
const DataInsightPage = () => {
const [teamsOptions, setTeamOptions] = useState<SelectProps['options']>([]);
const [activeTab, setActiveTab] = useState(DATA_INSIGHT_TAB.Datasets);
const [activeTab, setActiveTab] = useState(DATA_INSIGHT_TAB.DataAssets);
const [chartFilter, setChartFilter] =
useState<ChartFilter>(INITIAL_CHART_FILTER);
const [selectedChart, setSelectedChart] = useState<DataInsightChartType>();
useEffect(() => {
setChartFilter(INITIAL_CHART_FILTER);
}, []);
@ -120,6 +124,25 @@ const DataInsightPage = () => {
}
};
const handleScrollToChart = (chartType: DataInsightChartType) => {
if (ENTITIES_CHARTS.includes(chartType)) {
setActiveTab(DATA_INSIGHT_TAB.DataAssets);
} else {
setActiveTab(DATA_INSIGHT_TAB['Web Analytics']);
}
setSelectedChart(chartType);
};
useLayoutEffect(() => {
if (selectedChart) {
const element = document.getElementById(selectedChart);
if (element) {
element.scrollIntoView({ block: 'center', behavior: 'smooth' });
setSelectedChart(undefined);
}
}
}, [selectedChart]);
useEffect(() => {
fetchDefaultTeamOptions();
}, []);
@ -187,7 +210,10 @@ const DataInsightPage = () => {
</Card>
</Col>
<Col span={24}>
<DataInsightSummary chartFilter={chartFilter} />
<DataInsightSummary
chartFilter={chartFilter}
onScrollToChart={handleScrollToChart}
/>
</Col>
<Col span={24}>
<Radio.Group
@ -200,7 +226,7 @@ const DataInsightPage = () => {
onChange={(e) => setActiveTab(e.target.value)}
/>
</Col>
{activeTab === DATA_INSIGHT_TAB.Datasets && (
{activeTab === DATA_INSIGHT_TAB.DataAssets && (
<>
<Col span={24}>
<TotalEntityInsight chartFilter={chartFilter} />
@ -225,10 +251,10 @@ const DataInsightPage = () => {
<PageViewsByEntitiesChart chartFilter={chartFilter} />
</Col>
<Col span={24}>
<TopActiveUsers chartFilter={chartFilter} />
<DailyActiveUsersChart chartFilter={chartFilter} />
</Col>
<Col span={24}>
<DailyActiveUsersChart chartFilter={chartFilter} />
<TopActiveUsers chartFilter={chartFilter} />
</Col>
</>
)}

View File

@ -397,3 +397,7 @@
.m--ml-1 {
margin-left: -0.25rem /* -4px */;
}
.m--ml-0\.5 {
margin-left: -0.125rem /* -2px */;
}

View File

@ -12,10 +12,14 @@
*/
import { Card, Typography } from 'antd';
import { isInteger, last, toNumber } from 'lodash';
import { isInteger, isUndefined, last, toNumber } from 'lodash';
import React from 'react';
import { ListItem, ListValues } from 'react-awesome-query-builder';
import { LegendProps, Surface } from 'recharts';
import {
ENTITIES_SUMMARY_LIST,
WEB_SUMMARY_LIST,
} from '../constants/DataInsight.constants';
import {
DataInsightChartResult,
DataInsightChartType,
@ -242,3 +246,62 @@ export const getFormattedActiveUsersData = (activeUsers: DailyActiveUsers[]) =>
? getFormattedDateFromMilliSeconds(user.timestamp)
: '',
}));
export const getEntitiesChartSummary = (
chartResults: (DataInsightChartResult | undefined)[]
) => {
const updatedSummaryList = ENTITIES_SUMMARY_LIST.map((summary) => {
// grab the current chart type
const chartData = chartResults.find(
(chart) => chart?.chartType === summary.id
);
// return default summary if chart data is undefined else calculate the latest count for chartType
if (isUndefined(chartData)) return summary;
else {
if (chartData.chartType === DataInsightChartType.TotalEntitiesByTier) {
const { total } = getGraphDataByTierType(chartData.data ?? []);
return { ...summary, latest: total };
} else {
const { total } = getGraphDataByEntityType(
chartData.data ?? [],
chartData.chartType
);
return { ...summary, latest: total };
}
}
});
return updatedSummaryList;
};
export const getWebChartSummary = (
chartResults: (DataInsightChartResult | undefined)[]
) => {
const updatedSummary = WEB_SUMMARY_LIST.map((summary) => {
// grab the current chart type
const chartData = chartResults.find(
(chart) => chart?.chartType === summary.id
);
// return default summary if chart data is undefined else calculate the latest count for chartType
if (isUndefined(chartData)) return summary;
else {
if (chartData.chartType === DataInsightChartType.DailyActiveUsers) {
const latestData = last(chartData.data);
return { ...summary, latest: latestData?.activeUsers ?? 0 };
} else {
const { total } = getGraphDataByEntityType(
chartData.data ?? [],
chartData.chartType
);
return { ...summary, latest: total };
}
}
});
return updatedSummary;
};